diff options
Diffstat (limited to 'novaclient')
22 files changed, 943 insertions, 109 deletions
diff --git a/novaclient/__init__.py b/novaclient/__init__.py index bfa75532..5e6ccb0b 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.11") diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index e50edea2..06fcc52b 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -19,8 +19,10 @@ import re from oslo_utils import strutils +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__) @@ -229,6 +231,77 @@ 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): + """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) + + 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""" @@ -270,8 +343,14 @@ def wraps(start_version, end_version=None): 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/client.py b/novaclient/client.py index 98dab06c..15925554 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -260,7 +260,7 @@ class HTTPClient(object): 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) @@ -404,7 +404,7 @@ class HTTPClient(object): # a nova endpoint directly without "v2/<tenant-id>". 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/shell.py b/novaclient/shell.py index acda8bf0..6957f831 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -51,7 +51,8 @@ from novaclient.i18n import _ from novaclient.openstack.common import cliutils from novaclient import utils -DEFAULT_OS_COMPUTE_API_VERSION = "2" +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" @@ -403,8 +404,8 @@ class OpenStackComputeShell(object): metavar='<compute-api-ver>', 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) @@ -445,11 +446,6 @@ class OpenStackComputeShell(object): 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', @@ -547,18 +543,6 @@ class OpenStackComputeShell(object): 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 +552,21 @@ class OpenStackComputeShell(object): 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 + 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 @@ -631,13 +612,13 @@ class OpenStackComputeShell(object): 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 @@ class OpenStackComputeShell(object): # 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 @@ class OpenStackComputeShell(object): 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 @@ class OpenStackComputeShell(object): " 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 = client.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 @@ class OpenStackComputeShell(object): 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/functional/base.py b/novaclient/tests/functional/base.py index 7f773c5b..f39dbf0e 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -155,7 +155,7 @@ class ClientTestBase(testtools.TestCase): 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. diff --git a/novaclient/tests/functional/fake_crypto.py b/novaclient/tests/functional/fake_crypto.py new file mode 100644 index 00000000..56df5151 --- /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_instances.py b/novaclient/tests/functional/test_instances.py index 5ddaa9bf..749f34a7 100644 --- a/novaclient/tests/functional/test_instances.py +++ b/novaclient/tests/functional/test_instances.py @@ -38,8 +38,10 @@ class TestInstanceCLI(base.ClientTestBase): 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" % - (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. diff --git a/novaclient/tests/functional/test_keypairs.py b/novaclient/tests/functional/test_keypairs.py new file mode 100644 index 00000000..02bf3ecd --- /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/functional/test_servers.py b/novaclient/tests/functional/test_servers.py new file mode 100644 index 00000000..9de2cb2c --- /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 0bece964..5f45b528 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -159,6 +159,16 @@ class Base(base.Fixture): 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/test_api_versions.py b/novaclient/tests/unit/test_api_versions.py index 2db3a885..02df69a4 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,12 @@ import mock +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 class APIVersionTestCase(utils.TestCase): @@ -247,3 +250,110 @@ class WrapsTestCase(utils.TestCase): some_func(obj, *some_args, **some_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): + 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_client.py b/novaclient/tests/unit/test_client.py index a97ecbe8..5038e183 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -137,10 +137,16 @@ class ClientTest(utils.TestCase): 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') @@ -361,6 +367,8 @@ class ClientTest(utils.TestCase): 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', @@ -375,6 +383,10 @@ class ClientTest(utils.TestCase): 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( diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 93a6a326..56eb7f0c 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -33,26 +33,30 @@ from novaclient.tests.unit import utils 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): @@ -104,6 +108,19 @@ class ShellTest(utils.TestCase): 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 +185,7 @@ class ShellTest(utils.TestCase): 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', @@ -179,6 +197,7 @@ class ShellTest(utils.TestCase): 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 +422,79 @@ class ShellTest(utils.TestCase): 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 bebff3a9..6814461d 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1677,6 +1677,12 @@ class FakeHTTPClient(base_client.HTTPClient): 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 # @@ -2325,7 +2331,14 @@ class FakeSessionMockClient(base_client.SessionClient, FakeHTTPClient): 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' diff --git a/novaclient/tests/unit/v2/test_keypairs.py b/novaclient/tests/unit/v2/test_keypairs.py index ac3602f6..109d350a 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 @@ class KeypairsTest(utils.FixturedTestCase): 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_servers.py b/novaclient/tests/unit/v2/test_servers.py index faf7dd67..741cab55 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -43,6 +43,20 @@ class ServersTest(utils.FixturedTestCase): 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/tests/unit/v2/test_services.py b/novaclient/tests/unit/v2/test_services.py index 2724342d..f7ab553a 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 @@ class ServicesTest(utils.TestCase): 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/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 3e1c4d70..3f190a80 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -75,11 +75,15 @@ class ShellTest(utils.TestCase): @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): @@ -936,6 +940,14 @@ class ShellTest(utils.TestCase): 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_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', @@ -2395,28 +2407,61 @@ class ShellTest(utils.TestCase): 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_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 test_keypair_import(self): + 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') @@ -2434,7 +2479,8 @@ class ShellTest(utils.TestCase): 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/keypairs.py b/novaclient/v2/keypairs.py index 96caff61..02196051 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 @@ class KeypairManager(base.ManagerWithFind): 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 @@ class KeypairManager(base.ManagerWithFind): 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/servers.py b/novaclient/v2/servers.py index d4aed3be..3c34f95a 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -572,32 +572,44 @@ class ServerManager(base.BootingManagerWithFind): 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): """ @@ -860,7 +872,7 @@ class ServerManager(base.BootingManagerWithFind): :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, diff --git a/novaclient/v2/services.py b/novaclient/v2/services.py index d51fa3eb..fcf80093 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 @@ class ServiceManager(base.ManagerWithFind): 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 @@ class ServiceManager(base.ManagerWithFind): 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 @@ class ServiceManager(base.ManagerWithFind): 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 1e74ff83..e0f71162 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -34,6 +34,7 @@ from oslo_utils import timeutils from oslo_utils import uuidutils import six +from novaclient import api_versions from novaclient import client from novaclient import exceptions from novaclient.i18n import _ @@ -1339,6 +1340,23 @@ def do_image_delete(cs, args): help=('Comma-separated list of sort keys and directions in the form' ' of <key>[:<asc|desc>]. The direction defaults to descending if' ' not specified.')) +@cliutils.arg( + '--marker', + dest='marker', + metavar='<marker>', + default=None, + help=('The last server uuid of the previous page; displays list of servers' + ' after "marker".')) +@cliutils.arg( + '--limit', + dest='limit', + metavar='<limit>', + 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.")) def do_list(cs, args): """List active servers.""" imageid = None @@ -1397,7 +1415,9 @@ 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, + 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'), @@ -2500,7 +2520,7 @@ def do_floating_ip_pool_list(cs, _args): '--host', dest='host', metavar='<host>', 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', @@ -2516,13 +2536,13 @@ def do_floating_ip_bulk_list(cs, args): '--interface', metavar='<interface>', 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='<range>', 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) @@ -2884,6 +2904,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='<name>', help=_('Name of key.')) @cliutils.arg( '--pub-key', @@ -2893,11 +2923,16 @@ def do_secgroup_delete_group_rule(cs, args): @cliutils.arg( '--pub_key', help=argparse.SUPPRESS) +@cliutils.arg( + '--key-type', + metavar='<key-type>', + 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() @@ -2911,7 +2946,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 @@ -2925,10 +2960,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) @@ -3542,6 +3587,21 @@ def do_service_disable(cs, args): utils.print_list([result], ['Host', 'Binary', 'Status']) +@api_versions.wraps("2.11") +@cliutils.arg('host', metavar='<hostname>', help=_('Name of host.')) +@cliutils.arg('binary', metavar='<binary>', 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='<id>', help=_('Id of service.')) def do_service_delete(cs, args): """Delete the service.""" @@ -4381,7 +4441,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) |