summaryrefslogtreecommitdiff
path: root/novaclient
diff options
context:
space:
mode:
Diffstat (limited to 'novaclient')
-rw-r--r--novaclient/__init__.py5
-rw-r--r--novaclient/api_versions.py85
-rw-r--r--novaclient/client.py4
-rw-r--r--novaclient/shell.py135
-rw-r--r--novaclient/tests/functional/base.py2
-rw-r--r--novaclient/tests/functional/fake_crypto.py49
-rw-r--r--novaclient/tests/functional/test_instances.py6
-rw-r--r--novaclient/tests/functional/test_keypairs.py125
-rw-r--r--novaclient/tests/functional/test_servers.py52
-rw-r--r--novaclient/tests/unit/fixture_data/servers.py10
-rw-r--r--novaclient/tests/unit/test_api_versions.py112
-rw-r--r--novaclient/tests/unit/test_client.py12
-rw-r--r--novaclient/tests/unit/test_shell.py100
-rw-r--r--novaclient/tests/unit/v2/fakes.py13
-rw-r--r--novaclient/tests/unit/v2/test_keypairs.py46
-rw-r--r--novaclient/tests/unit/v2/test_servers.py14
-rw-r--r--novaclient/tests/unit/v2/test_services.py26
-rw-r--r--novaclient/tests/unit/v2/test_shell.py78
-rw-r--r--novaclient/v2/keypairs.py17
-rw-r--r--novaclient/v2/servers.py60
-rw-r--r--novaclient/v2/services.py19
-rw-r--r--novaclient/v2/shell.py82
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)