diff --git a/awx/lib/site-packages/README b/awx/lib/site-packages/README index 89ef6bbb25..a48c3db9cd 100644 --- a/awx/lib/site-packages/README +++ b/awx/lib/site-packages/README @@ -45,7 +45,7 @@ pip==1.5.4 (pip/*, excluded bin/pip*) prettytable==0.7.2 (prettytable.py) pyrax==1.9.0 (pyrax/*) python-dateutil==2.2 (dateutil/*) -python-novaclient==2.17.0 (novaclient/*, excluded bin/nova) +python-novaclient==2.18.1 (novaclient/*, excluded bin/nova) python-swiftclient==2.0.3 (swiftclient/*, excluded bin/swift) pytz==2014.4 (pytz/*) rackspace-auth-openstack==1.3 (rackspace_auth_openstack/*) diff --git a/awx/lib/site-packages/novaclient/auth_plugin.py b/awx/lib/site-packages/novaclient/auth_plugin.py index 1c7227fdcb..da2c07b26f 100644 --- a/awx/lib/site-packages/novaclient/auth_plugin.py +++ b/awx/lib/site-packages/novaclient/auth_plugin.py @@ -20,7 +20,6 @@ import pkg_resources import six from novaclient import exceptions -from novaclient.openstack.common.gettextutils import _ from novaclient import utils @@ -40,7 +39,7 @@ def discover_auth_systems(): try: auth_plugin = ep.load() except (ImportError, pkg_resources.UnknownExtra, AttributeError) as e: - logger.debug(_("ERROR: Cannot load auth plugin %s") % ep.name) + logger.debug("ERROR: Cannot load auth plugin %s" % ep.name) logger.debug(e, exc_info=1) else: _discovered_plugins[ep.name] = auth_plugin diff --git a/awx/lib/site-packages/novaclient/base.py b/awx/lib/site-packages/novaclient/base.py index 11a6d6f807..a677e32296 100644 --- a/awx/lib/site-packages/novaclient/base.py +++ b/awx/lib/site-packages/novaclient/base.py @@ -20,11 +20,7 @@ Base utilities to build API operation managers and objects on top of. """ import abc -import contextlib -import hashlib import inspect -import os -import threading import six @@ -52,11 +48,18 @@ class Manager(utils.HookableMixin): etc.) and provide CRUD operations for them. """ resource_class = None - cache_lock = threading.RLock() def __init__(self, api): self.api = api + def _write_object_to_completion_cache(self, obj): + if hasattr(self.api, 'write_object_to_completion_cache'): + self.api.write_object_to_completion_cache(obj) + + def _clear_completion_cache_for_class(self, obj_class): + if hasattr(self.api, 'clear_completion_cache_for_class'): + self.api.clear_completion_cache_for_class(obj_class) + def _list(self, url, response_key, obj_class=None, body=None): if body: _resp, body = self.api.client.post(url, body=body) @@ -75,77 +78,22 @@ class Manager(utils.HookableMixin): except KeyError: pass - with self.completion_cache('human_id', obj_class, mode="w"): - with self.completion_cache('uuid', obj_class, mode="w"): - return [obj_class(self, res, loaded=True) - for res in data if res] + self._clear_completion_cache_for_class(obj_class) - @contextlib.contextmanager - def completion_cache(self, cache_type, obj_class, mode): - """ - The completion cache store items that can be used for bash - autocompletion, like UUIDs or human-friendly IDs. + objs = [] + for res in data: + if res: + obj = obj_class(self, res, loaded=True) + self._write_object_to_completion_cache(obj) + objs.append(obj) - A resource listing will clear and repopulate the cache. - - A resource create will append to the cache. - - Delete is not handled because listings are assumed to be performed - often enough to keep the cache reasonably up-to-date. - """ - # NOTE(wryan): This lock protects read and write access to the - # completion caches - with self.cache_lock: - base_dir = utils.env('NOVACLIENT_UUID_CACHE_DIR', - default="~/.novaclient") - - # NOTE(sirp): Keep separate UUID caches for each username + - # endpoint pair - username = utils.env('OS_USERNAME', 'NOVA_USERNAME') - url = utils.env('OS_URL', 'NOVA_URL') - uniqifier = hashlib.md5(username.encode('utf-8') + - url.encode('utf-8')).hexdigest() - - cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier)) - - try: - os.makedirs(cache_dir, 0o755) - except OSError: - # NOTE(kiall): This is typically either permission denied while - # attempting to create the directory, or the - # directory already exists. Either way, don't - # fail. - pass - - resource = obj_class.__name__.lower() - filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-')) - path = os.path.join(cache_dir, filename) - - cache_attr = "_%s_cache" % cache_type - - try: - setattr(self, cache_attr, open(path, mode)) - except IOError: - # NOTE(kiall): This is typically a permission denied while - # attempting to write the cache file. - pass - - try: - yield - finally: - cache = getattr(self, cache_attr, None) - if cache: - cache.close() - delattr(self, cache_attr) - - def write_to_completion_cache(self, cache_type, val): - cache = getattr(self, "_%s_cache" % cache_type, None) - if cache: - cache.write("%s\n" % val) + return objs def _get(self, url, response_key): _resp, body = self.api.client.get(url) - return self.resource_class(self, body[response_key], loaded=True) + obj = self.resource_class(self, body[response_key], loaded=True) + self._write_object_to_completion_cache(obj) + return obj def _create(self, url, body, response_key, return_raw=False, **kwargs): self.run_hooks('modify_body_for_create', body, **kwargs) @@ -153,9 +101,9 @@ class Manager(utils.HookableMixin): if return_raw: return body[response_key] - with self.completion_cache('human_id', self.resource_class, mode="a"): - with self.completion_cache('uuid', self.resource_class, mode="a"): - return self.resource_class(self, body[response_key]) + obj = self.resource_class(self, body[response_key]) + self._write_object_to_completion_cache(obj) + return obj def _delete(self, url): _resp, _body = self.api.client.delete(url) diff --git a/awx/lib/site-packages/novaclient/client.py b/awx/lib/site-packages/novaclient/client.py index 0b9aeeecc6..de4a645974 100644 --- a/awx/lib/site-packages/novaclient/client.py +++ b/awx/lib/site-packages/novaclient/client.py @@ -20,7 +20,12 @@ OpenStack Client interface. Handles the REST calls and responses. """ +import errno +import functools +import glob +import hashlib import logging +import os import time import requests @@ -35,21 +40,177 @@ from six.moves.urllib import parse from novaclient import exceptions from novaclient.openstack.common.gettextutils import _ +from novaclient.openstack.common import network_utils from novaclient import service_catalog from novaclient import utils - -_ADAPTERS = {} +SENSITIVE_HEADERS = ('X-Auth-Token',) -def _adapter_pool(url): +class _ClientConnectionPool(object): + + def __init__(self): + self._adapters = {} + + def get(self, url): + """ + Store and reuse HTTP adapters per Service URL. + """ + if url not in self._adapters: + self._adapters[url] = adapters.HTTPAdapter() + + return self._adapters[url] + + +class CompletionCache(object): + """The completion cache is how we support tab-completion with novaclient. + + The `Manager` writes object IDs and Human-IDs to the completion-cache on + object-show, object-list, and object-create calls. + + The `nova.bash_completion` script then uses these files to provide the + actual tab-completion. + + The cache directory layout is: + + ~/.novaclient/ + / + -id-cache + -human-id-cache """ - Store and reuse HTTP adapters per Service URL. - """ - if url not in _ADAPTERS: - _ADAPTERS[url] = adapters.HTTPAdapter() + def __init__(self, username, auth_url, attributes=('id', 'human_id')): + self.directory = self._make_directory_name(username, auth_url) + self.attributes = attributes - return _ADAPTERS[url] + def _make_directory_name(self, username, auth_url): + """Creates a unique directory name based on the auth_url and username + of the current user. + """ + uniqifier = hashlib.md5(username.encode('utf-8') + + auth_url.encode('utf-8')).hexdigest() + base_dir = utils.env('NOVACLIENT_UUID_CACHE_DIR', + default="~/.novaclient") + return os.path.expanduser(os.path.join(base_dir, uniqifier)) + + def _prepare_directory(self): + try: + os.makedirs(self.directory, 0o755) + except OSError: + # NOTE(kiall): This is typically either permission denied while + # attempting to create the directory, or the + # directory already exists. Either way, don't + # fail. + pass + + def clear_class(self, obj_class): + self._prepare_directory() + + resource = obj_class.__name__.lower() + resource_glob = os.path.join(self.directory, "%s-*-cache" % resource) + + for filename in glob.iglob(resource_glob): + try: + os.unlink(filename) + except OSError as e: + if e.errno != errno.ENOENT: + raise + + def _write_attribute(self, resource, attribute, value): + self._prepare_directory() + + filename = "%s-%s-cache" % (resource, attribute.replace('_', '-')) + path = os.path.join(self.directory, filename) + + with open(path, 'a') as f: + f.write("%s\n" % value) + + def write_object(self, obj): + resource = obj.__class__.__name__.lower() + + for attribute in self.attributes: + value = getattr(obj, attribute, None) + if value: + self._write_attribute(resource, attribute, value) + + +class SessionClient(object): + + def __init__(self, session, auth, interface, service_type, region_name): + self.session = session + self.auth = auth + + self.interface = interface + self.service_type = service_type + self.region_name = region_name + + def request(self, url, method, **kwargs): + kwargs.setdefault('user_agent', 'python-novaclient') + kwargs.setdefault('auth', self.auth) + kwargs.setdefault('authenticated', False) + + try: + kwargs['json'] = kwargs.pop('body') + except KeyError: + pass + + headers = kwargs.setdefault('headers', {}) + headers.setdefault('Accept', 'application/json') + + endpoint_filter = kwargs.setdefault('endpoint_filter', {}) + endpoint_filter.setdefault('interface', self.interface) + endpoint_filter.setdefault('service_type', self.service_type) + endpoint_filter.setdefault('region_name', self.region_name) + + resp = self.session.request(url, method, raise_exc=False, **kwargs) + + body = None + if resp.text: + try: + body = resp.json() + except ValueError: + pass + + if resp.status_code >= 400: + raise exceptions.from_response(resp, body, url, method) + + return resp, body + + def _cs_request(self, url, method, **kwargs): + # this function is mostly redundant but makes compatibility easier + kwargs.setdefault('authenticated', True) + return self.request(url, method, **kwargs) + + def get(self, url, **kwargs): + return self._cs_request(url, 'GET', **kwargs) + + def post(self, url, **kwargs): + return self._cs_request(url, 'POST', **kwargs) + + def put(self, url, **kwargs): + return self._cs_request(url, 'PUT', **kwargs) + + def delete(self, url, **kwargs): + return self._cs_request(url, 'DELETE', **kwargs) + + +def _original_only(f): + """Indicates and enforces that this function can only be used if we are + using the original HTTPClient object. + + We use this to specify that if you use the newer Session HTTP client then + you are aware that the way you use your client has been updated and certain + functions are no longer allowed to be used. + """ + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + if isinstance(self.client, SessionClient): + msg = ('This call is no longer available. The operation should ' + 'be performed on the session object instead.') + raise exceptions.InvalidUsage(msg) + + return f(self, *args, **kwargs) + + return wrapper class HTTPClient(object): @@ -64,12 +225,17 @@ class HTTPClient(object): os_cache=False, no_cache=True, http_log_debug=False, auth_system='keystone', auth_plugin=None, auth_token=None, - cacert=None, tenant_id=None): + cacert=None, tenant_id=None, user_id=None, + connection_pool=False): self.user = user + self.user_id = user_id self.password = password self.projectid = projectid self.tenant_id = tenant_id + self._connection_pool = (_ClientConnectionPool() + if connection_pool else None) + # This will be called by #_get_password if self.password is None. # EG if a password can only be obtained by prompting the user, but a # token is available, you don't want to prompt until the token has @@ -83,7 +249,7 @@ class HTTPClient(object): auth_url = auth_plugin.get_auth_url() if not auth_url: raise exceptions.EndpointNotFound() - self.auth_url = auth_url.rstrip('/') + self.auth_url = auth_url.rstrip('/') if auth_url else auth_url self.version = 'v1.1' self.region_name = region_name self.endpoint_type = endpoint_type @@ -91,7 +257,7 @@ class HTTPClient(object): self.service_name = service_name self.volume_service_name = volume_service_name self.timings = timings - self.bypass_url = bypass_url + self.bypass_url = bypass_url.rstrip('/') if bypass_url else bypass_url self.os_cache = os_cache or not no_cache self.http_log_debug = http_log_debug if timeout is not None: @@ -118,8 +284,8 @@ class HTTPClient(object): self.auth_system = auth_system self.auth_plugin = auth_plugin + self._session = None self._current_url = None - self._http = None self._logger = logging.getLogger(__name__) if self.http_log_debug and not self._logger.handlers: @@ -152,6 +318,16 @@ class HTTPClient(object): def reset_timings(self): self.times = [] + def safe_header(self, name, value): + if name in SENSITIVE_HEADERS: + # because in python3 byte string handling is ... ug + v = value.encode('utf-8') + h = hashlib.sha1(v) + d = h.hexdigest() + return name, "{SHA1}%s" % d + else: + return name, value + def http_log_req(self, method, url, kwargs): if not self.http_log_debug: return @@ -164,35 +340,52 @@ class HTTPClient(object): string_parts.append(" '%s'" % url) string_parts.append(' -X %s' % method) - for element in kwargs['headers']: - header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) + # because dict ordering changes from 2 to 3 + keys = sorted(kwargs['headers'].keys()) + for name in keys: + value = kwargs['headers'][name] + header = ' -H "%s: %s"' % self.safe_header(name, value) string_parts.append(header) if 'data' in kwargs: string_parts.append(" -d '%s'" % (kwargs['data'])) - self._logger.debug("\nREQ: %s\n" % "".join(string_parts)) + self._logger.debug("REQ: %s" % "".join(string_parts)) def http_log_resp(self, resp): if not self.http_log_debug: return - self._logger.debug(_("RESP: [%(status)s] %(headers)s\nRESP BODY: " - "%(text)s\n"), {'status': resp.status_code, - 'headers': resp.headers, - 'text': resp.text}) + self._logger.debug("RESP: [%(status)s] %(headers)s\nRESP BODY: " + "%(text)s\n", {'status': resp.status_code, + 'headers': resp.headers, + 'text': resp.text}) - def http(self, url): - magic_tuple = parse.urlsplit(url) - scheme, netloc, path, query, frag = magic_tuple - service_url = '%s://%s' % (scheme, netloc) - if self._current_url != service_url: - # Invalidate Session object in case the url is somehow changed - if self._http: - self._http.close() - self._current_url = service_url - self._logger.debug("New session created for: (%s)" % service_url) - self._http = requests.Session() - self._http.mount(service_url, _adapter_pool(service_url)) - return self._http + def open_session(self): + if not self._connection_pool: + self._session = requests.Session() + + def close_session(self): + if self._session and not self._connection_pool: + self._session.close() + self._session = None + + def _get_session(self, url): + if self._connection_pool: + magic_tuple = parse.urlsplit(url) + scheme, netloc, path, query, frag = magic_tuple + service_url = '%s://%s' % (scheme, netloc) + if self._current_url != service_url: + # Invalidate Session object in case the url is somehow changed + if self._session: + self._session.close() + self._current_url = service_url + self._logger.debug( + "New session created for: (%s)" % service_url) + self._session = requests.Session() + self._session.mount(service_url, + self._connection_pool.get(service_url)) + return self._session + elif self._session: + return self._session def request(self, url, method, **kwargs): kwargs.setdefault('headers', kwargs.get('headers', {})) @@ -207,7 +400,13 @@ class HTTPClient(object): kwargs['verify'] = self.verify_cert self.http_log_req(method, url, kwargs) - resp = self.http(url).request( + + request_func = requests.request + session = self._get_session(url) + if session: + request_func = session.request + + resp = request_func( method, url, **kwargs) @@ -347,14 +546,14 @@ class HTTPClient(object): # GET ...:5001/v2.0/tokens/#####/endpoints url = '/'.join([url, 'tokens', '%s?belongsTo=%s' % (self.proxy_token, self.proxy_tenant_id)]) - self._logger.debug(_("Using Endpoint URL: %s") % url) + self._logger.debug("Using Endpoint URL: %s" % url) resp, body = self._time_request( url, "GET", headers={'X-Auth-Token': self.auth_token}) return self._extract_service_catalog(url, resp, body, extract_token=False) def authenticate(self): - magic_tuple = parse.urlsplit(self.auth_url) + magic_tuple = network_utils.urlsplit(self.auth_url) scheme, netloc, path, query, frag = magic_tuple port = magic_tuple.port if port is None: @@ -456,6 +655,10 @@ class HTTPClient(object): if self.auth_token: body = {"auth": { "token": {"id": self.auth_token}}} + elif self.user_id: + body = {"auth": { + "passwordCredentials": {"userId": self.user_id, + "password": self._get_password()}}} else: body = {"auth": { "passwordCredentials": {"username": self.user, @@ -484,6 +687,53 @@ class HTTPClient(object): return self._extract_service_catalog(url, resp, respbody) +def _construct_http_client(username=None, password=None, project_id=None, + auth_url=None, insecure=False, timeout=None, + proxy_tenant_id=None, proxy_token=None, + region_name=None, endpoint_type='publicURL', + extensions=None, service_type='compute', + service_name=None, volume_service_name=None, + timings=False, bypass_url=None, os_cache=False, + no_cache=True, http_log_debug=False, + auth_system='keystone', auth_plugin=None, + auth_token=None, cacert=None, tenant_id=None, + user_id=None, connection_pool=False, session=None, + auth=None): + if session: + return SessionClient(session=session, + auth=auth, + interface=endpoint_type, + service_type=service_type, + region_name=region_name) + else: + # FIXME(jamielennox): username and password are now optional. Need + # to test that they were provided in this mode. + return HTTPClient(username, + password, + user_id=user_id, + projectid=project_id, + tenant_id=tenant_id, + auth_url=auth_url, + auth_token=auth_token, + insecure=insecure, + timeout=timeout, + auth_system=auth_system, + auth_plugin=auth_plugin, + proxy_token=proxy_token, + proxy_tenant_id=proxy_tenant_id, + region_name=region_name, + endpoint_type=endpoint_type, + service_type=service_type, + service_name=service_name, + volume_service_name=volume_service_name, + timings=timings, + bypass_url=bypass_url, + os_cache=os_cache, + http_log_debug=http_log_debug, + cacert=cacert, + connection_pool=connection_pool) + + def get_client_class(version): version_map = { '1.1': 'novaclient.v1_1.client.Client', diff --git a/awx/lib/site-packages/novaclient/exceptions.py b/awx/lib/site-packages/novaclient/exceptions.py index 9f22a9e3e0..cc3133bdcf 100644 --- a/awx/lib/site-packages/novaclient/exceptions.py +++ b/awx/lib/site-packages/novaclient/exceptions.py @@ -77,10 +77,17 @@ class ConnectionRefused(Exception): return "ConnectionRefused: %s" % repr(self.response) +class InstanceInErrorState(Exception): + """Instance is in the error state.""" + pass + + class ClientException(Exception): """ The base exception class for all exceptions this library raises. """ + message = 'Unknown Error' + def __init__(self, code, message=None, details=None, request_id=None, url=None, method=None): self.code = code @@ -192,6 +199,16 @@ _error_classes = [BadRequest, Unauthorized, Forbidden, NotFound, _code_map = dict((c.http_status, c) for c in _error_classes) +class InvalidUsage(RuntimeError): + """This function call is invalid in the way you are using this client. + + Due to the transition to using keystoneclient some function calls are no + longer available. You should make a similar call to the session object + instead. + """ + pass + + def from_response(response, body, url, method=None): """ Return an instance of an ClientException or subclass diff --git a/awx/lib/site-packages/novaclient/openstack/common/__init__.py b/awx/lib/site-packages/novaclient/openstack/common/__init__.py index 2a00f3bc49..d1223eaf76 100644 --- a/awx/lib/site-packages/novaclient/openstack/common/__init__.py +++ b/awx/lib/site-packages/novaclient/openstack/common/__init__.py @@ -1,2 +1,17 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import six + + six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox')) diff --git a/awx/lib/site-packages/novaclient/openstack/common/apiclient/auth.py b/awx/lib/site-packages/novaclient/openstack/common/apiclient/auth.py index ee1348afe0..67a1bf7ddd 100644 --- a/awx/lib/site-packages/novaclient/openstack/common/apiclient/auth.py +++ b/awx/lib/site-packages/novaclient/openstack/common/apiclient/auth.py @@ -213,8 +213,8 @@ class BaseAuthPlugin(object): :type service_type: string :param endpoint_type: Type of endpoint. Possible values: public or publicURL, - internal or internalURL, - admin or adminURL + internal or internalURL, + admin or adminURL :type endpoint_type: string :returns: tuple of token and endpoint strings :raises: EndpointException diff --git a/awx/lib/site-packages/novaclient/openstack/common/apiclient/base.py b/awx/lib/site-packages/novaclient/openstack/common/apiclient/base.py index 647f4a720b..1f90b88e92 100644 --- a/awx/lib/site-packages/novaclient/openstack/common/apiclient/base.py +++ b/awx/lib/site-packages/novaclient/openstack/common/apiclient/base.py @@ -30,6 +30,7 @@ import six from six.moves.urllib import parse from novaclient.openstack.common.apiclient import exceptions +from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import strutils @@ -74,8 +75,8 @@ class HookableMixin(object): :param cls: class that registers hooks :param hook_type: hook type, e.g., '__pre_parse_args__' - :param **args: args to be passed to every hook function - :param **kwargs: kwargs to be passed to every hook function + :param args: args to be passed to every hook function + :param kwargs: kwargs to be passed to every hook function """ hook_funcs = cls._hooks_map.get(hook_type) or [] for hook_func in hook_funcs: @@ -219,7 +220,10 @@ class ManagerWithFind(BaseManager): matches = self.findall(**kwargs) num_matches = len(matches) if num_matches == 0: - msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) + msg = _("No %(name)s matching %(args)s.") % { + 'name': self.resource_class.__name__, + 'args': kwargs + } raise exceptions.NotFound(msg) elif num_matches > 1: raise exceptions.NoUniqueMatch() @@ -373,7 +377,10 @@ class CrudManager(BaseManager): num = len(rl) if num == 0: - msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) + msg = _("No %(name)s matching %(args)s.") % { + 'name': self.resource_class.__name__, + 'args': kwargs + } raise exceptions.NotFound(404, msg) elif num > 1: raise exceptions.NoUniqueMatch @@ -441,8 +448,10 @@ class Resource(object): def human_id(self): """Human-readable ID which can be used for bash completion. """ - if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID: - return strutils.to_slug(getattr(self, self.NAME_ATTR)) + if self.HUMAN_ID: + name = getattr(self, self.NAME_ATTR, None) + if name is not None: + return strutils.to_slug(name) return None def _add_details(self, info): diff --git a/awx/lib/site-packages/novaclient/openstack/common/apiclient/client.py b/awx/lib/site-packages/novaclient/openstack/common/apiclient/client.py index f573c7b529..a3666daf0a 100644 --- a/awx/lib/site-packages/novaclient/openstack/common/apiclient/client.py +++ b/awx/lib/site-packages/novaclient/openstack/common/apiclient/client.py @@ -36,6 +36,7 @@ except ImportError: import requests from novaclient.openstack.common.apiclient import exceptions +from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import importutils @@ -46,6 +47,7 @@ class HTTPClient(object): """This client handles sending HTTP requests to OpenStack servers. Features: + - share authentication information between several clients to different services (e.g., for compute and image clients); - reissue authentication request for expired tokens; @@ -151,7 +153,7 @@ class HTTPClient(object): :param method: method of HTTP request :param url: URL of HTTP request :param kwargs: any other parameter that can be passed to -' requests.Session.request (such as `headers`) or `json` + requests.Session.request (such as `headers`) or `json` that will be encoded as JSON and used as `data` argument """ kwargs.setdefault("headers", kwargs.get("headers", {})) @@ -206,7 +208,7 @@ class HTTPClient(object): :param method: method of HTTP request :param url: URL of HTTP request :param kwargs: any other parameter that can be passed to -' `HTTPClient.request` + `HTTPClient.request` """ filter_args = { @@ -228,7 +230,7 @@ class HTTPClient(object): **filter_args) if not (token and endpoint): raise exceptions.AuthorizationFailure( - "Cannot find endpoint or token for request") + _("Cannot find endpoint or token for request")) old_token_endpoint = (token, endpoint) kwargs.setdefault("headers", {})["X-Auth-Token"] = token @@ -351,8 +353,12 @@ class BaseClient(object): try: client_path = version_map[str(version)] except (KeyError, ValueError): - msg = "Invalid %s client version '%s'. must be one of: %s" % ( - (api_name, version, ', '.join(version_map.keys()))) + msg = _("Invalid %(api_name)s client version '%(version)s'. " + "Must be one of: %(version_map)s") % { + 'api_name': api_name, + 'version': version, + 'version_map': ', '.join(version_map.keys()) + } raise exceptions.UnsupportedVersion(msg) return importutils.import_class(client_path) diff --git a/awx/lib/site-packages/novaclient/openstack/common/apiclient/exceptions.py b/awx/lib/site-packages/novaclient/openstack/common/apiclient/exceptions.py index ada1344f55..85b123ef8c 100644 --- a/awx/lib/site-packages/novaclient/openstack/common/apiclient/exceptions.py +++ b/awx/lib/site-packages/novaclient/openstack/common/apiclient/exceptions.py @@ -25,6 +25,8 @@ import sys import six +from novaclient.openstack.common.gettextutils import _ + class ClientException(Exception): """The base exception class for all exceptions this library raises. @@ -36,7 +38,7 @@ class MissingArgs(ClientException): """Supplied arguments are not sufficient for calling a function.""" def __init__(self, missing): self.missing = missing - msg = "Missing argument(s): %s" % ", ".join(missing) + msg = _("Missing arguments: %s") % ", ".join(missing) super(MissingArgs, self).__init__(msg) @@ -55,6 +57,11 @@ class CommandError(ClientException): pass +class ResourceNotFound(ClientException): + """Error in getting the resource.""" + pass + + class AuthorizationFailure(ClientException): """Cannot authorize API client.""" pass @@ -69,16 +76,16 @@ class AuthPluginOptionsMissing(AuthorizationFailure): """Auth plugin misses some options.""" def __init__(self, opt_names): super(AuthPluginOptionsMissing, self).__init__( - "Authentication failed. Missing options: %s" % + _("Authentication failed. Missing options: %s") % ", ".join(opt_names)) self.opt_names = opt_names class AuthSystemNotFound(AuthorizationFailure): - """User has specified a AuthSystem that is not installed.""" + """User has specified an AuthSystem that is not installed.""" def __init__(self, auth_system): super(AuthSystemNotFound, self).__init__( - "AuthSystemNotFound: %s" % repr(auth_system)) + _("AuthSystemNotFound: %s") % repr(auth_system)) self.auth_system = auth_system @@ -101,7 +108,7 @@ class AmbiguousEndpoints(EndpointException): """Found more than one matching endpoint in Service Catalog.""" def __init__(self, endpoints=None): super(AmbiguousEndpoints, self).__init__( - "AmbiguousEndpoints: %s" % repr(endpoints)) + _("AmbiguousEndpoints: %s") % repr(endpoints)) self.endpoints = endpoints @@ -109,7 +116,7 @@ class HttpError(ClientException): """The base exception class for all HTTP exceptions. """ http_status = 0 - message = "HTTP Error" + message = _("HTTP Error") def __init__(self, message=None, details=None, response=None, request_id=None, @@ -129,7 +136,7 @@ class HttpError(ClientException): class HTTPRedirection(HttpError): """HTTP Redirection.""" - message = "HTTP Redirection" + message = _("HTTP Redirection") class HTTPClientError(HttpError): @@ -137,7 +144,7 @@ class HTTPClientError(HttpError): Exception for cases in which the client seems to have erred. """ - message = "HTTP Client Error" + message = _("HTTP Client Error") class HttpServerError(HttpError): @@ -146,7 +153,7 @@ class HttpServerError(HttpError): Exception for cases in which the server is aware that it has erred or is incapable of performing the request. """ - message = "HTTP Server Error" + message = _("HTTP Server Error") class MultipleChoices(HTTPRedirection): @@ -156,7 +163,7 @@ class MultipleChoices(HTTPRedirection): """ http_status = 300 - message = "Multiple Choices" + message = _("Multiple Choices") class BadRequest(HTTPClientError): @@ -165,7 +172,7 @@ class BadRequest(HTTPClientError): The request cannot be fulfilled due to bad syntax. """ http_status = 400 - message = "Bad Request" + message = _("Bad Request") class Unauthorized(HTTPClientError): @@ -175,7 +182,7 @@ class Unauthorized(HTTPClientError): is required and has failed or has not yet been provided. """ http_status = 401 - message = "Unauthorized" + message = _("Unauthorized") class PaymentRequired(HTTPClientError): @@ -184,7 +191,7 @@ class PaymentRequired(HTTPClientError): Reserved for future use. """ http_status = 402 - message = "Payment Required" + message = _("Payment Required") class Forbidden(HTTPClientError): @@ -194,7 +201,7 @@ class Forbidden(HTTPClientError): to it. """ http_status = 403 - message = "Forbidden" + message = _("Forbidden") class NotFound(HTTPClientError): @@ -204,7 +211,7 @@ class NotFound(HTTPClientError): in the future. """ http_status = 404 - message = "Not Found" + message = _("Not Found") class MethodNotAllowed(HTTPClientError): @@ -214,7 +221,7 @@ class MethodNotAllowed(HTTPClientError): by that resource. """ http_status = 405 - message = "Method Not Allowed" + message = _("Method Not Allowed") class NotAcceptable(HTTPClientError): @@ -224,7 +231,7 @@ class NotAcceptable(HTTPClientError): acceptable according to the Accept headers sent in the request. """ http_status = 406 - message = "Not Acceptable" + message = _("Not Acceptable") class ProxyAuthenticationRequired(HTTPClientError): @@ -233,7 +240,7 @@ class ProxyAuthenticationRequired(HTTPClientError): The client must first authenticate itself with the proxy. """ http_status = 407 - message = "Proxy Authentication Required" + message = _("Proxy Authentication Required") class RequestTimeout(HTTPClientError): @@ -242,7 +249,7 @@ class RequestTimeout(HTTPClientError): The server timed out waiting for the request. """ http_status = 408 - message = "Request Timeout" + message = _("Request Timeout") class Conflict(HTTPClientError): @@ -252,7 +259,7 @@ class Conflict(HTTPClientError): in the request, such as an edit conflict. """ http_status = 409 - message = "Conflict" + message = _("Conflict") class Gone(HTTPClientError): @@ -262,7 +269,7 @@ class Gone(HTTPClientError): not be available again. """ http_status = 410 - message = "Gone" + message = _("Gone") class LengthRequired(HTTPClientError): @@ -272,7 +279,7 @@ class LengthRequired(HTTPClientError): required by the requested resource. """ http_status = 411 - message = "Length Required" + message = _("Length Required") class PreconditionFailed(HTTPClientError): @@ -282,7 +289,7 @@ class PreconditionFailed(HTTPClientError): put on the request. """ http_status = 412 - message = "Precondition Failed" + message = _("Precondition Failed") class RequestEntityTooLarge(HTTPClientError): @@ -291,7 +298,7 @@ class RequestEntityTooLarge(HTTPClientError): The request is larger than the server is willing or able to process. """ http_status = 413 - message = "Request Entity Too Large" + message = _("Request Entity Too Large") def __init__(self, *args, **kwargs): try: @@ -308,7 +315,7 @@ class RequestUriTooLong(HTTPClientError): The URI provided was too long for the server to process. """ http_status = 414 - message = "Request-URI Too Long" + message = _("Request-URI Too Long") class UnsupportedMediaType(HTTPClientError): @@ -318,7 +325,7 @@ class UnsupportedMediaType(HTTPClientError): not support. """ http_status = 415 - message = "Unsupported Media Type" + message = _("Unsupported Media Type") class RequestedRangeNotSatisfiable(HTTPClientError): @@ -328,7 +335,7 @@ class RequestedRangeNotSatisfiable(HTTPClientError): supply that portion. """ http_status = 416 - message = "Requested Range Not Satisfiable" + message = _("Requested Range Not Satisfiable") class ExpectationFailed(HTTPClientError): @@ -337,7 +344,7 @@ class ExpectationFailed(HTTPClientError): The server cannot meet the requirements of the Expect request-header field. """ http_status = 417 - message = "Expectation Failed" + message = _("Expectation Failed") class UnprocessableEntity(HTTPClientError): @@ -347,7 +354,7 @@ class UnprocessableEntity(HTTPClientError): errors. """ http_status = 422 - message = "Unprocessable Entity" + message = _("Unprocessable Entity") class InternalServerError(HttpServerError): @@ -356,7 +363,7 @@ class InternalServerError(HttpServerError): A generic error message, given when no more specific message is suitable. """ http_status = 500 - message = "Internal Server Error" + message = _("Internal Server Error") # NotImplemented is a python keyword. @@ -367,7 +374,7 @@ class HttpNotImplemented(HttpServerError): the ability to fulfill the request. """ http_status = 501 - message = "Not Implemented" + message = _("Not Implemented") class BadGateway(HttpServerError): @@ -377,7 +384,7 @@ class BadGateway(HttpServerError): response from the upstream server. """ http_status = 502 - message = "Bad Gateway" + message = _("Bad Gateway") class ServiceUnavailable(HttpServerError): @@ -386,7 +393,7 @@ class ServiceUnavailable(HttpServerError): The server is currently unavailable. """ http_status = 503 - message = "Service Unavailable" + message = _("Service Unavailable") class GatewayTimeout(HttpServerError): @@ -396,7 +403,7 @@ class GatewayTimeout(HttpServerError): response from the upstream server. """ http_status = 504 - message = "Gateway Timeout" + message = _("Gateway Timeout") class HttpVersionNotSupported(HttpServerError): @@ -405,7 +412,7 @@ class HttpVersionNotSupported(HttpServerError): The server does not support the HTTP protocol version used in the request. """ http_status = 505 - message = "HTTP Version Not Supported" + message = _("HTTP Version Not Supported") # _code_map contains all the classes that have http_status attribute. @@ -423,12 +430,17 @@ def from_response(response, method, url): :param method: HTTP method used for request :param url: URL used for request """ + + req_id = response.headers.get("x-openstack-request-id") + #NOTE(hdd) true for older versions of nova and cinder + if not req_id: + req_id = response.headers.get("x-compute-request-id") kwargs = { "http_status": response.status_code, "response": response, "method": method, "url": url, - "request_id": response.headers.get("x-compute-request-id"), + "request_id": req_id, } if "retry-after" in response.headers: kwargs["retry_after"] = response.headers["retry-after"] diff --git a/awx/lib/site-packages/novaclient/openstack/common/gettextutils.py b/awx/lib/site-packages/novaclient/openstack/common/gettextutils.py index 9823482742..e76b61a117 100644 --- a/awx/lib/site-packages/novaclient/openstack/common/gettextutils.py +++ b/awx/lib/site-packages/novaclient/openstack/common/gettextutils.py @@ -28,7 +28,6 @@ import gettext import locale from logging import handlers import os -import re from babel import localedata import six @@ -248,47 +247,22 @@ class Message(six.text_type): if other is None: params = (other,) elif isinstance(other, dict): - params = self._trim_dictionary_parameters(other) + # Merge the dictionaries + # Copy each item in case one does not support deep copy. + params = {} + if isinstance(self.params, dict): + for key, val in self.params.items(): + params[key] = self._copy_param(val) + for key, val in other.items(): + params[key] = self._copy_param(val) else: params = self._copy_param(other) return params - def _trim_dictionary_parameters(self, dict_param): - """Return a dict that only has matching entries in the msgid.""" - # NOTE(luisg): Here we trim down the dictionary passed as parameters - # to avoid carrying a lot of unnecessary weight around in the message - # object, for example if someone passes in Message() % locals() but - # only some params are used, and additionally we prevent errors for - # non-deepcopyable objects by unicoding() them. - - # Look for %(param) keys in msgid; - # Skip %% and deal with the case where % is first character on the line - keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', self.msgid) - - # If we don't find any %(param) keys but have a %s - if not keys and re.findall('(?:[^%]|^)%[a-z]', self.msgid): - # Apparently the full dictionary is the parameter - params = self._copy_param(dict_param) - else: - params = {} - # Save our existing parameters as defaults to protect - # ourselves from losing values if we are called through an - # (erroneous) chain that builds a valid Message with - # arguments, and then does something like "msg % kwds" - # where kwds is an empty dictionary. - src = {} - if isinstance(self.params, dict): - src.update(self.params) - src.update(dict_param) - for key in keys: - params[key] = self._copy_param(src[key]) - - return params - def _copy_param(self, param): try: return copy.deepcopy(param) - except TypeError: + except Exception: # Fallback to casting to unicode this will handle the # python code-like objects that can't be deep-copied return six.text_type(param) diff --git a/awx/lib/site-packages/novaclient/openstack/common/jsonutils.py b/awx/lib/site-packages/novaclient/openstack/common/jsonutils.py index 5595d0b2e3..b08a679c51 100644 --- a/awx/lib/site-packages/novaclient/openstack/common/jsonutils.py +++ b/awx/lib/site-packages/novaclient/openstack/common/jsonutils.py @@ -31,25 +31,29 @@ This module provides a few things: ''' +import codecs import datetime import functools import inspect import itertools -import json -try: - import xmlrpclib -except ImportError: - # NOTE(jaypipes): xmlrpclib was renamed to xmlrpc.client in Python3 - # however the function and object call signatures - # remained the same. This whole try/except block should - # be removed and replaced with a call to six.moves once - # six 1.4.2 is released. See http://bit.ly/1bqrVzu - import xmlrpc.client as xmlrpclib +import sys + +if sys.version_info < (2, 7): + # On Python <= 2.6, json module is not C boosted, so try to use + # simplejson module if available + try: + import simplejson as json + except ImportError: + import json +else: + import json import six +import six.moves.xmlrpc_client as xmlrpclib from novaclient.openstack.common import gettextutils from novaclient.openstack.common import importutils +from novaclient.openstack.common import strutils from novaclient.openstack.common import timeutils netaddr = importutils.try_import("netaddr") @@ -164,12 +168,12 @@ def dumps(value, default=to_primitive, **kwargs): return json.dumps(value, default=default, **kwargs) -def loads(s): - return json.loads(s) +def loads(s, encoding='utf-8'): + return json.loads(strutils.safe_decode(s, encoding)) -def load(s): - return json.load(s) +def load(fp, encoding='utf-8'): + return json.load(codecs.getreader(encoding)(fp)) try: diff --git a/awx/lib/site-packages/novaclient/openstack/common/network_utils.py b/awx/lib/site-packages/novaclient/openstack/common/network_utils.py new file mode 100644 index 0000000000..fa812b29f3 --- /dev/null +++ b/awx/lib/site-packages/novaclient/openstack/common/network_utils.py @@ -0,0 +1,108 @@ +# Copyright 2012 OpenStack Foundation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Network-related utilities and helper functions. +""" + +# TODO(jd) Use six.moves once +# https://bitbucket.org/gutworth/six/pull-request/28 +# is merged +try: + import urllib.parse + SplitResult = urllib.parse.SplitResult +except ImportError: + import urlparse + SplitResult = urlparse.SplitResult + +from six.moves.urllib import parse + + +def parse_host_port(address, default_port=None): + """Interpret a string as a host:port pair. + + An IPv6 address MUST be escaped if accompanied by a port, + because otherwise ambiguity ensues: 2001:db8:85a3::8a2e:370:7334 + means both [2001:db8:85a3::8a2e:370:7334] and + [2001:db8:85a3::8a2e:370]:7334. + + >>> parse_host_port('server01:80') + ('server01', 80) + >>> parse_host_port('server01') + ('server01', None) + >>> parse_host_port('server01', default_port=1234) + ('server01', 1234) + >>> parse_host_port('[::1]:80') + ('::1', 80) + >>> parse_host_port('[::1]') + ('::1', None) + >>> parse_host_port('[::1]', default_port=1234) + ('::1', 1234) + >>> parse_host_port('2001:db8:85a3::8a2e:370:7334', default_port=1234) + ('2001:db8:85a3::8a2e:370:7334', 1234) + + """ + if address[0] == '[': + # Escaped ipv6 + _host, _port = address[1:].split(']') + host = _host + if ':' in _port: + port = _port.split(':')[1] + else: + port = default_port + else: + if address.count(':') == 1: + host, port = address.split(':') + else: + # 0 means ipv4, >1 means ipv6. + # We prohibit unescaped ipv6 addresses with port. + host = address + port = default_port + + return (host, None if port is None else int(port)) + + +class ModifiedSplitResult(SplitResult): + """Split results class for urlsplit.""" + + # NOTE(dims): The functions below are needed for Python 2.6.x. + # We can remove these when we drop support for 2.6.x. + @property + def hostname(self): + netloc = self.netloc.split('@', 1)[-1] + host, port = parse_host_port(netloc) + return host + + @property + def port(self): + netloc = self.netloc.split('@', 1)[-1] + host, port = parse_host_port(netloc) + return port + + +def urlsplit(url, scheme='', allow_fragments=True): + """Parse a URL using urlparse.urlsplit(), splitting query and fragments. + This function papers over Python issue9374 when needed. + + The parameters are the same as urlparse.urlsplit. + """ + scheme, netloc, path, query, fragment = parse.urlsplit( + url, scheme, allow_fragments) + if allow_fragments and '#' in path: + path, fragment = path.split('#', 1) + if '?' in path: + path, query = path.split('?', 1) + return ModifiedSplitResult(scheme, netloc, + path, query, fragment) diff --git a/awx/lib/site-packages/novaclient/openstack/common/strutils.py b/awx/lib/site-packages/novaclient/openstack/common/strutils.py index 1383fbb7e5..c22063d904 100644 --- a/awx/lib/site-packages/novaclient/openstack/common/strutils.py +++ b/awx/lib/site-packages/novaclient/openstack/common/strutils.py @@ -98,7 +98,8 @@ def bool_from_string(subject, strict=False, default=False): def safe_decode(text, incoming=None, errors='strict'): - """Decodes incoming str using `incoming` if they're not already unicode. + """Decodes incoming text/bytes string using `incoming` if they're not + already unicode. :param incoming: Text's current encoding :param errors: Errors handling policy. See here for valid @@ -107,7 +108,7 @@ def safe_decode(text, incoming=None, errors='strict'): representation of it. :raises TypeError: If text is not an instance of str """ - if not isinstance(text, six.string_types): + if not isinstance(text, (six.string_types, six.binary_type)): raise TypeError("%s can't be decoded" % type(text)) if isinstance(text, six.text_type): @@ -137,7 +138,7 @@ def safe_decode(text, incoming=None, errors='strict'): def safe_encode(text, incoming=None, encoding='utf-8', errors='strict'): - """Encodes incoming str/unicode using `encoding`. + """Encodes incoming text/bytes string using `encoding`. If incoming is not specified, text is expected to be encoded with current python's default encoding. (`sys.getdefaultencoding`) @@ -150,7 +151,7 @@ def safe_encode(text, incoming=None, representation of it. :raises TypeError: If text is not an instance of str """ - if not isinstance(text, six.string_types): + if not isinstance(text, (six.string_types, six.binary_type)): raise TypeError("%s can't be encoded" % type(text)) if not incoming: diff --git a/awx/lib/site-packages/novaclient/shell.py b/awx/lib/site-packages/novaclient/shell.py index 919aa21ba0..08f5e8c401 100644 --- a/awx/lib/site-packages/novaclient/shell.py +++ b/awx/lib/site-packages/novaclient/shell.py @@ -285,6 +285,11 @@ class OpenStackComputeShell(object): parser.add_argument('--os_username', help=argparse.SUPPRESS) + parser.add_argument('--os-user-id', + metavar='', + default=utils.env('OS_USER_ID'), + help=_('Defaults to env[OS_USER_ID].')) + parser.add_argument('--os-password', metavar='', default=utils.env('OS_PASSWORD', 'NOVA_PASSWORD'), @@ -385,7 +390,9 @@ class OpenStackComputeShell(object): parser.add_argument('--bypass-url', metavar='', dest='bypass_url', - help="Use this API endpoint instead of the Service Catalog") + default=utils.env('NOVACLIENT_BYPASS_URL'), + help="Use this API endpoint instead of the Service Catalog. " + "Defaults to env[NOVACLIENT_BYPASS_URL]") parser.add_argument('--bypass_url', help=argparse.SUPPRESS) @@ -550,6 +557,7 @@ class OpenStackComputeShell(object): return 0 os_username = args.os_username + os_user_id = args.os_user_id os_password = None # Fetched and set later as needed os_tenant_name = args.os_tenant_name os_tenant_id = args.os_tenant_id @@ -606,9 +614,10 @@ class OpenStackComputeShell(object): auth_plugin.parse_opts(args) if not auth_plugin or not auth_plugin.opts: - if not os_username: + if not os_username and not os_user_id: raise exc.CommandError(_("You must provide a username " - "via either --os-username or env[OS_USERNAME]")) + "or user id via --os-username, --os-user-id, " + "env[OS_USERNAME] or env[OS_USER_ID]")) if not os_tenant_name and not os_tenant_id: raise exc.CommandError(_("You must provide a tenant name " @@ -639,8 +648,11 @@ class OpenStackComputeShell(object): raise exc.CommandError(_("You must provide an auth url " "via either --os-auth-url or env[OS_AUTH_URL]")) - self.cs = client.Client(options.os_compute_api_version, os_username, - os_password, os_tenant_name, tenant_id=os_tenant_id, + completion_cache = client.CompletionCache(os_username, os_auth_url) + + self.cs = client.Client(options.os_compute_api_version, + os_username, os_password, os_tenant_name, + tenant_id=os_tenant_id, user_id=os_user_id, auth_url=os_auth_url, insecure=insecure, region_name=os_region_name, endpoint_type=endpoint_type, extensions=self.extensions, service_type=service_type, @@ -649,7 +661,8 @@ class OpenStackComputeShell(object): volume_service_name=volume_service_name, timings=args.timings, bypass_url=bypass_url, os_cache=os_cache, http_log_debug=options.debug, - cacert=cacert, timeout=timeout) + cacert=cacert, timeout=timeout, + completion_cache=completion_cache) # Now check for the password/token of which pieces of the # identifying keyring key can come from the underlying client @@ -694,6 +707,12 @@ class OpenStackComputeShell(object): # sometimes need to be able to look up images information # via glance when connected to the nova api. image_service_type = 'image' + # NOTE(hdd): the password is needed again because creating a new + # Client without specifying bypass_url will force authentication. + # We can't reuse self.cs's bypass_url, because that's the URL for + # the nova service; we need to get glance's URL for this Client + if not os_password: + os_password = helper.password self.cs.image_cs = client.Client( options.os_compute_api_version, os_username, os_password, os_tenant_name, tenant_id=os_tenant_id, @@ -763,6 +782,11 @@ class OpenStackComputeShell(object): # I'm picky about my shell help. class OpenStackHelpFormatter(argparse.HelpFormatter): + def __init__(self, prog, indent_increment=2, max_help_position=32, + width=None): + super(OpenStackHelpFormatter, self).__init__(prog, indent_increment, + max_help_position, width) + def start_section(self, heading): # Title-case the headings heading = '%s%s' % (heading[0].upper(), heading[1:]) @@ -771,11 +795,14 @@ class OpenStackHelpFormatter(argparse.HelpFormatter): def main(): try: - OpenStackComputeShell().main(map(strutils.safe_decode, sys.argv[1:])) + argv = [strutils.safe_decode(a) for a in sys.argv[1:]] + OpenStackComputeShell().main(argv) except Exception as e: logger.debug(e, exc_info=1) - print("ERROR: %s" % strutils.safe_encode(six.text_type(e)), + details = {'name': strutils.safe_encode(e.__class__.__name__), + 'msg': strutils.safe_encode(six.text_type(e))} + print("ERROR (%(name)s): %(msg)s" % details, file=sys.stderr) sys.exit(1) except KeyboardInterrupt as e: diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/__init__.py b/awx/lib/site-packages/novaclient/tests/fixture_data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/agents.py b/awx/lib/site-packages/novaclient/tests/fixture_data/agents.py new file mode 100644 index 0000000000..a21f473dec --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/agents.py @@ -0,0 +1,57 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-agents' + + def setUp(self): + super(Fixture, self).setUp() + + post_os_agents = { + 'agent': { + 'url': '/xxx/xxx/xxx', + 'hypervisor': 'kvm', + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'version': '7.0', + 'architecture': 'x86', + 'os': 'win', + 'id': 1 + } + } + + httpretty.register_uri(httpretty.POST, self.url(), + body=jsonutils.dumps(post_os_agents), + content_type='application/json') + + put_os_agents_1 = { + "agent": { + "url": "/yyy/yyyy/yyyy", + "version": "8.0", + "md5hash": "add6bb58e139be103324d04d82d8f546", + 'id': 1 + } + } + + httpretty.register_uri(httpretty.PUT, self.url(1), + body=jsonutils.dumps(put_os_agents_1), + content_type='application/json') + + httpretty.register_uri(httpretty.DELETE, self.url(1), + content_type='application/json', + status=202) diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/aggregates.py b/awx/lib/site-packages/novaclient/tests/fixture_data/aggregates.py new file mode 100644 index 0000000000..33e30905fa --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/aggregates.py @@ -0,0 +1,52 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-aggregates' + + def setUp(self): + super(Fixture, self).setUp() + + get_os_aggregates = {"aggregates": [ + {'id': '1', + 'name': 'test', + 'availability_zone': 'nova1'}, + {'id': '2', + 'name': 'test2', + 'availability_zone': 'nova1'}, + ]} + + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_aggregates), + content_type='application/json') + + r = jsonutils.dumps({'aggregate': get_os_aggregates['aggregates'][0]}) + + httpretty.register_uri(httpretty.POST, self.url(), body=r, + content_type='application/json') + + for agg_id in (1, 2): + for method in (httpretty.GET, httpretty.PUT): + httpretty.register_uri(method, self.url(agg_id), body=r, + content_type='application/json') + + httpretty.register_uri(httpretty.POST, self.url(agg_id, 'action'), + body=r, content_type='application/json') + + httpretty.register_uri(httpretty.DELETE, self.url(1), status=202) diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/availability_zones.py b/awx/lib/site-packages/novaclient/tests/fixture_data/availability_zones.py new file mode 100644 index 0000000000..c23788f51a --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/availability_zones.py @@ -0,0 +1,99 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class V1(base.Fixture): + + base_url = 'os-availability-zone' + + zone_info_key = 'availabilityZoneInfo' + zone_name_key = 'zoneName' + zone_state_key = 'zoneState' + + def setUp(self): + super(V1, self).setUp() + + get_os_availability_zone = { + self.zone_info_key: [ + { + self.zone_name_key: "zone-1", + self.zone_state_key: {"available": True}, + "hosts": None + }, + { + self.zone_name_key: "zone-2", + self.zone_state_key: {"available": False}, + "hosts": None + } + ] + } + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_availability_zone), + content_type='application/json') + + get_os_zone_detail = { + self.zone_info_key: [ + { + self.zone_name_key: "zone-1", + self.zone_state_key: {"available": True}, + "hosts": { + "fake_host-1": { + "nova-compute": { + "active": True, + "available": True, + "updated_at": '2012-12-26 14:45:25' + } + } + } + }, + { + self.zone_name_key: "internal", + self.zone_state_key: {"available": True}, + "hosts": { + "fake_host-1": { + "nova-sched": { + "active": True, + "available": True, + "updated_at": '2012-12-26 14:45:25' + } + }, + "fake_host-2": { + "nova-network": { + "active": True, + "available": False, + "updated_at": '2012-12-26 14:45:24' + } + } + } + }, + { + self.zone_name_key: "zone-2", + self.zone_state_key: {"available": False}, + "hosts": None + } + ] + } + + httpretty.register_uri(httpretty.GET, self.url('detail'), + body=jsonutils.dumps(get_os_zone_detail), + content_type='application/json') + + +class V3(V1): + zone_info_key = 'availability_zone_info' + zone_name_key = 'zone_name' + zone_state_key = 'zone_state' diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/base.py b/awx/lib/site-packages/novaclient/tests/fixture_data/base.py new file mode 100644 index 0000000000..72f46d1dd3 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/base.py @@ -0,0 +1,38 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import fixtures +from six.moves.urllib import parse + +COMPUTE_URL = 'http://compute.host' + + +class Fixture(fixtures.Fixture): + + base_url = None + + def __init__(self, compute_url=COMPUTE_URL): + super(Fixture, self).__init__() + self.compute_url = compute_url + + def url(self, *args, **kwargs): + url_args = [self.compute_url] + + if self.base_url: + url_args.append(self.base_url) + + url = '/'.join(str(a).strip('/') for a in tuple(url_args) + args) + + if kwargs: + url += '?%s' % parse.urlencode(kwargs, doseq=True) + + return url diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/certs.py b/awx/lib/site-packages/novaclient/tests/fixture_data/certs.py new file mode 100644 index 0000000000..3ce0f0f072 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/certs.py @@ -0,0 +1,58 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-certificates' + + def get_os_certificates_root(self, **kw): + return ( + 200, + {}, + {'certificate': {'private_key': None, 'data': 'foo'}} + ) + + def post_os_certificates(self, **kw): + return ( + 200, + {}, + {'certificate': {'private_key': 'foo', 'data': 'bar'}} + ) + + def setUp(self): + super(Fixture, self).setUp() + + get_os_certificate = { + 'certificate': { + 'private_key': None, + 'data': 'foo' + } + } + httpretty.register_uri(httpretty.GET, self.url('root'), + body=jsonutils.dumps(get_os_certificate), + content_type='application/json') + + post_os_certificates = { + 'certificate': { + 'private_key': 'foo', + 'data': 'bar' + } + } + httpretty.register_uri(httpretty.POST, self.url(), + body=jsonutils.dumps(post_os_certificates), + content_type='application/json') diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/client.py b/awx/lib/site-packages/novaclient/tests/fixture_data/client.py new file mode 100644 index 0000000000..c4ca3ec38b --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/client.py @@ -0,0 +1,127 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import fixtures +import httpretty +from keystoneclient.auth.identity import v2 +from keystoneclient import session + +from novaclient.openstack.common import jsonutils +from novaclient.v1_1 import client as v1_1client +from novaclient.v3 import client as v3client + +IDENTITY_URL = 'http://identityserver:5000/v2.0' +COMPUTE_URL = 'http://compute.host' + + +class V1(fixtures.Fixture): + + def __init__(self, compute_url=COMPUTE_URL, identity_url=IDENTITY_URL): + super(V1, self).__init__() + self.identity_url = identity_url + self.compute_url = compute_url + self.client = None + + self.token = { + 'access': { + "token": { + "id": "ab48a9efdfedb23ty3494", + "expires": "2010-11-01T03:32:15-05:00", + "tenant": { + "id": "345", + "name": "My Project" + } + }, + "user": { + "id": "123", + "name": "jqsmith", + "roles": [ + { + "id": "234", + "name": "compute:admin", + }, + { + "id": "235", + "name": "object-store:admin", + "tenantId": "1", + } + ], + "roles_links": [], + }, + "serviceCatalog": [ + { + "name": "Cloud Servers", + "type": "compute", + "endpoints": [ + { + "publicURL": self.compute_url, + "internalURL": "https://compute1.host/v1/1", + }, + ], + "endpoints_links": [], + }, + { + "name": "Cloud Servers", + "type": "computev3", + "endpoints": [ + { + "publicURL": self.compute_url, + "internalURL": "https://compute1.host/v1/1", + }, + ], + "endpoints_links": [], + }, + ], + } + } + + def setUp(self): + super(V1, self).setUp() + httpretty.enable() + self.addCleanup(httpretty.disable) + + auth_url = '%s/tokens' % self.identity_url + httpretty.register_uri(httpretty.POST, auth_url, + body=jsonutils.dumps(self.token), + content_type='application/json') + self.client = self.new_client() + + def new_client(self): + return v1_1client.Client(username='xx', + api_key='xx', + project_id='xx', + auth_url=self.identity_url) + + +class V3(V1): + + def new_client(self): + return v3client.Client(username='xx', + password='xx', + project_id='xx', + auth_url=self.identity_url) + + +class SessionV1(V1): + + def new_client(self): + self.session = session.Session() + self.session.auth = v2.Password(self.identity_url, 'xx', 'xx') + return v1_1client.Client(session=self.session) + + +class SessionV3(V1): + + def new_client(self): + self.session = session.Session() + self.session.auth = v2.Password(self.identity_url, 'xx', 'xx') + return v3client.Client(session=self.session) diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/cloudpipe.py b/awx/lib/site-packages/novaclient/tests/fixture_data/cloudpipe.py new file mode 100644 index 0000000000..fffd2a1fe8 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/cloudpipe.py @@ -0,0 +1,40 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-cloudpipe' + + def setUp(self): + super(Fixture, self).setUp() + + get_os_cloudpipe = {'cloudpipes': [{'project_id': 1}]} + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_cloudpipe), + content_type='application/json') + + instance_id = '9d5824aa-20e6-4b9f-b967-76a699fc51fd' + post_os_cloudpipe = {'instance_id': instance_id} + httpretty.register_uri(httpretty.POST, self.url(), + body=jsonutils.dumps(post_os_cloudpipe), + content_type='application/json', + status=202) + + httpretty.register_uri(httpretty.PUT, self.url('configure-project'), + content_type='application/json', + status=202) diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/fixedips.py b/awx/lib/site-packages/novaclient/tests/fixture_data/fixedips.py new file mode 100644 index 0000000000..64a36d2132 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/fixedips.py @@ -0,0 +1,41 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-fixed-ips' + + def setUp(self): + super(Fixture, self).setUp() + + get_os_fixed_ips = { + "fixed_ip": { + 'cidr': '192.168.1.0/24', + 'address': '192.168.1.1', + 'hostname': 'foo', + 'host': 'bar' + } + } + httpretty.register_uri(httpretty.GET, self.url('192.168.1.1'), + body=jsonutils.dumps(get_os_fixed_ips), + content_type='application/json') + + httpretty.register_uri(httpretty.POST, + self.url('192.168.1.1', 'action'), + content_type='application/json', + status=202) diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/floatingips.py b/awx/lib/site-packages/novaclient/tests/fixture_data/floatingips.py new file mode 100644 index 0000000000..8dd590427a --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/floatingips.py @@ -0,0 +1,216 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests import fakes +from novaclient.tests.fixture_data import base + + +class FloatingFixture(base.Fixture): + + base_url = 'os-floating-ips' + + def setUp(self): + super(FloatingFixture, self).setUp() + + floating_ips = [{'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, + {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}] + + get_os_floating_ips = {'floating_ips': floating_ips} + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_floating_ips), + content_type='application/json') + + for ip in floating_ips: + get_os_floating_ip = {'floating_ip': ip} + httpretty.register_uri(httpretty.GET, self.url(ip['id']), + body=jsonutils.dumps(get_os_floating_ip), + content_type='application/json') + + httpretty.register_uri(httpretty.DELETE, self.url(ip['id']), + content_type='application/json', + status=204) + + def post_os_floating_ips(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + ip = floating_ips[0].copy() + ip['pool'] = body.get('pool') + ip = jsonutils.dumps({'floating_ip': ip}) + return 200, headers, ip + httpretty.register_uri(httpretty.POST, self.url(), + body=post_os_floating_ips, + content_type='application/json') + + +class DNSFixture(base.Fixture): + + base_url = 'os-floating-ip-dns' + + def setUp(self): + super(DNSFixture, self).setUp() + + get_os_floating_ip_dns = { + 'domain_entries': [ + {'domain': 'example.org'}, + {'domain': 'example.com'} + ] + } + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_floating_ip_dns), + content_type='application/json', + status=205) + + get_dns_testdomain_entries_testname = { + 'dns_entry': { + 'ip': "10.10.10.10", + 'name': 'testname', + 'type': "A", + 'domain': 'testdomain' + } + } + url = self.url('testdomain', 'entries', 'testname') + body = jsonutils.dumps(get_dns_testdomain_entries_testname) + httpretty.register_uri(httpretty.GET, url, + body=body, + content_type='application/json', + status=205) + + httpretty.register_uri(httpretty.DELETE, self.url('testdomain'), + status=200) + + url = self.url('testdomain', 'entries', 'testname') + httpretty.register_uri(httpretty.DELETE, url, status=200) + + def put_dns_testdomain_entries_testname(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + fakes.assert_has_keys(body['dns_entry'], + required=['ip', 'dns_type']) + return 205, headers, request.body + httpretty.register_uri(httpretty.PUT, url, + body=put_dns_testdomain_entries_testname, + content_type='application/json') + + url = self.url('testdomain', 'entries') + httpretty.register_uri(httpretty.GET, url, status=404) + + get_os_floating_ip_dns_testdomain_entries = { + 'dns_entries': [ + { + 'dns_entry': { + 'ip': '1.2.3.4', + 'name': "host1", + 'type': "A", + 'domain': 'testdomain' + } + }, + { + 'dns_entry': { + 'ip': '1.2.3.4', + 'name': "host2", + 'type': "A", + 'domain': 'testdomain' + } + }, + ] + } + body = jsonutils.dumps(get_os_floating_ip_dns_testdomain_entries) + httpretty.register_uri(httpretty.GET, url + '?ip=1.2.3.4', + body=body, + status=205, + content_type='application/json') + + def put_os_floating_ip_dns_testdomain(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + if body['domain_entry']['scope'] == 'private': + fakes.assert_has_keys(body['domain_entry'], + required=['availability_zone', 'scope']) + elif body['domain_entry']['scope'] == 'public': + fakes.assert_has_keys(body['domain_entry'], + required=['project', 'scope']) + else: + fakes.assert_has_keys(body['domain_entry'], + required=['project', 'scope']) + + headers['Content-Type'] = 'application/json' + return (205, headers, request.body) + + httpretty.register_uri(httpretty.PUT, self.url('testdomain'), + body=put_os_floating_ip_dns_testdomain) + + +class BulkFixture(base.Fixture): + + base_url = 'os-floating-ips-bulk' + + def setUp(self): + super(BulkFixture, self).setUp() + + get_os_floating_ips_bulk = { + 'floating_ip_info': [ + {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, + {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, + ] + } + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_floating_ips_bulk), + content_type='application/json') + httpretty.register_uri(httpretty.GET, self.url('testHost'), + body=jsonutils.dumps(get_os_floating_ips_bulk), + content_type='application/json') + + def put_os_floating_ips_bulk_delete(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + ip_range = body.get('ip_range') + data = {'floating_ips_bulk_delete': ip_range} + return 200, headers, jsonutils.dumps(data) + + httpretty.register_uri(httpretty.PUT, self.url('delete'), + body=put_os_floating_ips_bulk_delete, + content_type='application/json') + + def post_os_floating_ips_bulk(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + params = body.get('floating_ips_bulk_create') + pool = params.get('pool', 'defaultPool') + interface = params.get('interface', 'defaultInterface') + data = { + 'floating_ips_bulk_create': { + 'ip_range': '192.168.1.0/30', + 'pool': pool, + 'interface': interface + } + } + return 200, headers, jsonutils.dumps(data) + + httpretty.register_uri(httpretty.POST, self.url(), + body=post_os_floating_ips_bulk, + content_type='application/json') + + +class PoolsFixture(base.Fixture): + + base_url = 'os-floating-ip-pools' + + def setUp(self): + super(PoolsFixture, self).setUp() + + get_os_floating_ip_pools = { + 'floating_ip_pools': [ + {'name': 'foo'}, + {'name': 'bar'} + ] + } + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_floating_ip_pools), + content_type='application/json') diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/fping.py b/awx/lib/site-packages/novaclient/tests/fixture_data/fping.py new file mode 100644 index 0000000000..9542f83d4c --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/fping.py @@ -0,0 +1,49 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-fping' + + def setUp(self): + super(Fixture, self).setUp() + + get_os_fping_1 = { + 'server': { + "id": "1", + "project_id": "fake-project", + "alive": True, + } + } + httpretty.register_uri(httpretty.GET, self.url(1), + body=jsonutils.dumps(get_os_fping_1), + content_type='application/json') + + get_os_fping = { + 'servers': [ + get_os_fping_1['server'], + { + "id": "2", + "project_id": "fake-project", + "alive": True, + }, + ] + } + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_fping), + content_type='application/json') diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/hosts.py b/awx/lib/site-packages/novaclient/tests/fixture_data/hosts.py new file mode 100644 index 0000000000..193a5bb674 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/hosts.py @@ -0,0 +1,165 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty +from six.moves.urllib import parse + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class BaseFixture(base.Fixture): + + base_url = 'os-hosts' + + def setUp(self): + super(BaseFixture, self).setUp() + + get_os_hosts_host = { + 'host': [ + {'resource': {'project': '(total)', 'host': 'dummy', + 'cpu': 16, 'memory_mb': 32234, 'disk_gb': 128}}, + {'resource': {'project': '(used_now)', 'host': 'dummy', + 'cpu': 1, 'memory_mb': 2075, 'disk_gb': 45}}, + {'resource': {'project': '(used_max)', 'host': 'dummy', + 'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}}, + {'resource': {'project': 'admin', 'host': 'dummy', + 'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}} + ] + } + httpretty.register_uri(httpretty.GET, self.url('host'), + body=jsonutils.dumps(get_os_hosts_host), + content_type='application/json') + + def get_os_hosts(request, url, headers): + host, query = parse.splitquery(url) + zone = 'nova1' + + if query: + qs = parse.parse_qs(query) + try: + zone = qs['zone'][0] + except Exception: + pass + + data = { + 'hosts': [ + { + 'host': 'host1', + 'service': 'nova-compute', + 'zone': zone + }, + { + 'host': 'host1', + 'service': 'nova-cert', + 'zone': zone + } + ] + } + return 200, headers, jsonutils.dumps(data) + + httpretty.register_uri(httpretty.GET, self.url(), + body=get_os_hosts, + content_type='application/json') + + get_os_hosts_sample_host = { + 'host': [ + {'resource': {'host': 'sample_host'}} + ], + } + httpretty.register_uri(httpretty.GET, self.url('sample_host'), + body=jsonutils.dumps(get_os_hosts_sample_host), + content_type='application/json') + + httpretty.register_uri(httpretty.PUT, self.url('sample_host', 1), + body=jsonutils.dumps(self.put_host_1()), + content_type='application/json') + + httpretty.register_uri(httpretty.PUT, self.url('sample_host', 2), + body=jsonutils.dumps(self.put_host_2()), + content_type='application/json') + + httpretty.register_uri(httpretty.PUT, self.url('sample_host', 3), + body=jsonutils.dumps(self.put_host_3()), + content_type='application/json') + + url = self.url('sample_host', 'reboot') + httpretty.register_uri(httpretty.GET, url, + body=jsonutils.dumps(self.get_host_reboot()), + content_type='application/json') + + url = self.url('sample_host', 'startup') + httpretty.register_uri(httpretty.GET, url, + body=jsonutils.dumps(self.get_host_startup()), + content_type='application/json') + + url = self.url('sample_host', 'shutdown') + httpretty.register_uri(httpretty.GET, url, + body=jsonutils.dumps(self.get_host_shutdown()), + content_type='application/json') + + def put_os_hosts_sample_host(request, url, headers): + result = {'host': 'dummy'} + result.update(jsonutils.loads(request.body.decode('utf-8'))) + return 200, headers, jsonutils.dumps(result) + + httpretty.register_uri(httpretty.PUT, self.url('sample_host'), + body=put_os_hosts_sample_host, + content_type='application/json') + + +class V1(BaseFixture): + + def put_host_1(self): + return {'host': 'sample-host_1', + 'status': 'enabled'} + + def put_host_2(self): + return {'host': 'sample-host_2', + 'maintenance_mode': 'on_maintenance'} + + def put_host_3(self): + return {'host': 'sample-host_3', + 'status': 'enabled', + 'maintenance_mode': 'on_maintenance'} + + def get_host_reboot(self): + return {'host': 'sample_host', + 'power_action': 'reboot'} + + def get_host_startup(self): + return {'host': 'sample_host', + 'power_action': 'startup'} + + def get_host_shutdown(self): + return {'host': 'sample_host', + 'power_action': 'shutdown'} + + +class V3(V1): + def put_host_1(self): + return {'host': super(V3, self).put_host_1()} + + def put_host_2(self): + return {'host': super(V3, self).put_host_2()} + + def put_host_3(self): + return {'host': super(V3, self).put_host_3()} + + def get_host_reboot(self): + return {'host': super(V3, self).get_host_reboot()} + + def get_host_startup(self): + return {'host': super(V3, self).get_host_startup()} + + def get_host_shutdown(self): + return {'host': super(V3, self).get_host_shutdown()} diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/hypervisors.py b/awx/lib/site-packages/novaclient/tests/fixture_data/hypervisors.py new file mode 100644 index 0000000000..8133de4421 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/hypervisors.py @@ -0,0 +1,210 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class V1(base.Fixture): + + base_url = 'os-hypervisors' + + def setUp(self): + super(V1, self).setUp() + + get_os_hypervisors = { + 'hypervisors': [ + {'id': 1234, 'hypervisor_hostname': 'hyper1'}, + {'id': 5678, 'hypervisor_hostname': 'hyper2'}, + ] + } + + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_hypervisors), + content_type='application/json') + + get_os_hypervisors_detail = { + 'hypervisors': [ + { + 'id': 1234, + 'service': {'id': 1, 'host': 'compute1'}, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': 'xen', + 'hypervisor_version': 3, + 'hypervisor_hostname': 'hyper1', + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100 + }, + { + 'id': 2, + 'service': {'id': 2, 'host': 'compute2'}, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': 'xen', + 'hypervisor_version': 3, + 'hypervisor_hostname': 'hyper2', + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100 + } + ] + } + + httpretty.register_uri(httpretty.GET, self.url('detail'), + body=jsonutils.dumps(get_os_hypervisors_detail), + content_type='application/json') + + get_os_hypervisors_stats = { + 'hypervisor_statistics': { + 'count': 2, + 'vcpus': 8, + 'memory_mb': 20 * 1024, + 'local_gb': 500, + 'vcpus_used': 4, + 'memory_mb_used': 10 * 1024, + 'local_gb_used': 250, + 'free_ram_mb': 10 * 1024, + 'free_disk_gb': 250, + 'current_workload': 4, + 'running_vms': 4, + 'disk_available_least': 200, + } + } + + httpretty.register_uri(httpretty.GET, self.url('statistics'), + body=jsonutils.dumps(get_os_hypervisors_stats), + content_type='application/json') + + get_os_hypervisors_search = { + 'hypervisors': [ + {'id': 1234, 'hypervisor_hostname': 'hyper1'}, + {'id': 5678, 'hypervisor_hostname': 'hyper2'} + ] + } + + httpretty.register_uri(httpretty.GET, self.url('hyper', 'search'), + body=jsonutils.dumps(get_os_hypervisors_search), + content_type='application/json') + + get_hyper_server = { + 'hypervisors': [ + { + 'id': 1234, + 'hypervisor_hostname': 'hyper1', + 'servers': [ + {'name': 'inst1', 'uuid': 'uuid1'}, + {'name': 'inst2', 'uuid': 'uuid2'} + ] + }, + { + 'id': 5678, + 'hypervisor_hostname': 'hyper2', + 'servers': [ + {'name': 'inst3', 'uuid': 'uuid3'}, + {'name': 'inst4', 'uuid': 'uuid4'} + ] + } + ] + } + + httpretty.register_uri(httpretty.GET, self.url('hyper', 'servers'), + body=jsonutils.dumps(get_hyper_server), + content_type='application/json') + + get_os_hypervisors_1234 = { + 'hypervisor': { + 'id': 1234, + 'service': {'id': 1, 'host': 'compute1'}, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': 'xen', + 'hypervisor_version': 3, + 'hypervisor_hostname': 'hyper1', + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100 + } + } + + httpretty.register_uri(httpretty.GET, self.url(1234), + body=jsonutils.dumps(get_os_hypervisors_1234), + content_type='application/json') + + get_os_hypervisors_uptime = { + 'hypervisor': { + 'id': 1234, + 'hypervisor_hostname': 'hyper1', + 'uptime': 'fake uptime' + } + } + + httpretty.register_uri(httpretty.GET, self.url(1234, 'uptime'), + body=jsonutils.dumps(get_os_hypervisors_uptime), + content_type='application/json') + + +class V3(V1): + + def setUp(self): + super(V3, self).setUp() + + get_os_hypervisors_search = { + 'hypervisors': [ + {'id': 1234, 'hypervisor_hostname': 'hyper1'}, + {'id': 5678, 'hypervisor_hostname': 'hyper2'} + ] + } + + httpretty.register_uri(httpretty.GET, + self.url('search', query='hyper'), + body=jsonutils.dumps(get_os_hypervisors_search), + content_type='application/json') + + get_1234_servers = { + 'hypervisor': { + 'id': 1234, + 'hypervisor_hostname': 'hyper1', + 'servers': [ + {'name': 'inst1', 'id': 'uuid1'}, + {'name': 'inst2', 'id': 'uuid2'} + ] + }, + } + + httpretty.register_uri(httpretty.GET, self.url(1234, 'servers'), + body=jsonutils.dumps(get_1234_servers), + content_type='application/json') diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/images.py b/awx/lib/site-packages/novaclient/tests/fixture_data/images.py new file mode 100644 index 0000000000..09a134e5fc --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/images.py @@ -0,0 +1,119 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests import fakes +from novaclient.tests.fixture_data import base + + +class V1(base.Fixture): + + base_url = 'images' + + def setUp(self): + super(V1, self).setUp() + + get_images = { + 'images': [ + {'id': 1, 'name': 'CentOS 5.2'}, + {'id': 2, 'name': 'My Server Backup'} + ] + } + + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_images), + content_type='application/json') + + image_1 = { + 'id': 1, + 'name': 'CentOS 5.2', + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "status": "ACTIVE", + "metadata": { + "test_key": "test_value", + }, + "links": {}, + } + + image_2 = { + "id": 2, + "name": "My Server Backup", + "serverId": 1234, + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "status": "SAVING", + "progress": 80, + "links": {}, + } + + get_images_detail = {'images': [image_1, image_2]} + + httpretty.register_uri(httpretty.GET, self.url('detail'), + body=jsonutils.dumps(get_images_detail), + content_type='application/json') + + get_images_1 = {'image': image_1} + + httpretty.register_uri(httpretty.GET, self.url(1), + body=jsonutils.dumps(get_images_1), + content_type='application/json') + + get_images_2 = {'image': image_2} + + httpretty.register_uri(httpretty.GET, self.url(2), + body=jsonutils.dumps(get_images_2), + content_type='application/json') + + httpretty.register_uri(httpretty.GET, self.url(456), + body=jsonutils.dumps(get_images_2), + content_type='application/json') + + def post_images(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + assert list(body) == ['image'] + fakes.assert_has_keys(body['image'], required=['serverId', 'name']) + return 202, headers, jsonutils.dumps(images_1) + + httpretty.register_uri(httpretty.POST, self.url(), + body=post_images, + content_type='application/json') + + def post_images_1_metadata(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + assert list(body) == ['metadata'] + fakes.assert_has_keys(body['metadata'], required=['test_key']) + data = jsonutils.dumps({'metadata': image_1['metadata']}) + return 200, headers, data + + httpretty.register_uri(httpretty.POST, self.url(1, 'metadata'), + body=post_images_1_metadata, + content_type='application/json') + + for u in (1, 2, '1/metadata/test_key'): + httpretty.register_uri(httpretty.DELETE, self.url(u), + status=204) + + httpretty.register_uri(httpretty.HEAD, self.url(1), status=200, + x_image_meta_id=1, + x_image_meta_name='CentOS 5.2', + x_image_meta_updated='2010-10-10T12:00:00Z', + x_image_meta_created='2010-10-10T12:00:00Z', + x_image_meta_status='ACTIVE', + x_image_meta_property_test_key='test_value') + + +class V3(V1): + + base_url = 'v1/images' diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/keypairs.py b/awx/lib/site-packages/novaclient/tests/fixture_data/keypairs.py new file mode 100644 index 0000000000..b3400a76cb --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/keypairs.py @@ -0,0 +1,52 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests import fakes +from novaclient.tests.fixture_data import base + + +class V1(base.Fixture): + + base_url = 'os-keypairs' + + def setUp(self): + super(V1, self).setUp() + keypair = {'fingerprint': 'FAKE_KEYPAIR', 'name': 'test'} + + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps({'keypairs': [keypair]}), + content_type='application/json') + + httpretty.register_uri(httpretty.GET, self.url('test'), + body=jsonutils.dumps({'keypair': keypair}), + content_type='application/json') + + httpretty.register_uri(httpretty.DELETE, self.url('test'), status=202) + + def post_os_keypairs(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + assert list(body) == ['keypair'] + fakes.assert_has_keys(body['keypair'], required=['name']) + return 202, headers, jsonutils.dumps({'keypair': keypair}) + + httpretty.register_uri(httpretty.POST, self.url(), + body=post_os_keypairs, + content_type='application/json') + + +class V3(V1): + + base_url = 'keypairs' diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/limits.py b/awx/lib/site-packages/novaclient/tests/fixture_data/limits.py new file mode 100644 index 0000000000..5eca22e4d4 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/limits.py @@ -0,0 +1,82 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'limits' + + def setUp(self): + super(Fixture, self).setUp() + + get_limits = { + "limits": { + "rate": [ + { + "uri": "*", + "regex": ".*", + "limit": [ + { + "value": 10, + "verb": "POST", + "remaining": 2, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z" + }, + { + "value": 10, + "verb": "PUT", + "remaining": 2, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z" + }, + { + "value": 100, + "verb": "DELETE", + "remaining": 100, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z" + } + ] + }, + { + "uri": "*/servers", + "regex": "^/servers", + "limit": [ + { + "verb": "POST", + "value": 25, + "remaining": 24, + "unit": "DAY", + "next-available": "2011-12-15T22:42:45Z" + } + ] + } + ], + "absolute": { + "maxTotalRAMSize": 51200, + "maxServerMeta": 5, + "maxImageMeta": 5, + "maxPersonality": 5, + "maxPersonalitySize": 10240 + }, + }, + } + + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_limits), + content_type='application/json') diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/networks.py b/awx/lib/site-packages/novaclient/tests/fixture_data/networks.py new file mode 100644 index 0000000000..12bb4b1eb2 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/networks.py @@ -0,0 +1,62 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-networks' + + def setUp(self): + super(Fixture, self).setUp() + + get_os_networks = { + 'networks': [ + { + "label": "1", + "cidr": "10.0.0.0/24", + 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1' + } + ] + } + + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_networks), + content_type='application/json') + + def post_os_networks(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + data = jsonutils.dumps({'network': body}) + return 202, headers, data + + httpretty.register_uri(httpretty.POST, self.url(), + body=post_os_networks, + content_type='application/json') + + get_os_networks_1 = {'network': {"label": "1", "cidr": "10.0.0.0/24"}} + + httpretty.register_uri(httpretty.GET, self.url(1), + body=jsonutils.dumps(get_os_networks_1), + content_type='application/json') + + httpretty.register_uri(httpretty.DELETE, + self.url('networkdelete'), + stauts=202) + + for u in ('add', 'networkdisassociate/action', 'networktest/action', + '1/action', '2/action'): + httpretty.register_uri(httpretty.POST, self.url(u), stauts=202) diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/quotas.py b/awx/lib/site-packages/novaclient/tests/fixture_data/quotas.py new file mode 100644 index 0000000000..70db079dd6 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/quotas.py @@ -0,0 +1,87 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class V1(base.Fixture): + + base_url = 'os-quota-sets' + + def setUp(self): + super(V1, self).setUp() + + uuid = '97f4c221-bff4-4578-b030-0df4ef119353' + uuid2 = '97f4c221bff44578b0300df4ef119353' + test_json = jsonutils.dumps({'quota_set': self.test_quota('test')}) + + for u in ('test', 'tenant-id', 'tenant-id/defaults', + '%s/defaults' % uuid2): + httpretty.register_uri(httpretty.GET, self.url(u), + body=test_json, + content_type='application/json') + + quota_json = jsonutils.dumps({'quota_set': self.test_quota(uuid)}) + httpretty.register_uri(httpretty.PUT, self.url(uuid), + body=quota_json, + content_type='application/json') + httpretty.register_uri(httpretty.GET, self.url(uuid), + body=quota_json, + content_type='application/json') + + quota_json2 = jsonutils.dumps({'quota_set': self.test_quota(uuid2)}) + httpretty.register_uri(httpretty.PUT, self.url(uuid2), + body=quota_json2, + content_type='application/json') + httpretty.register_uri(httpretty.GET, self.url(uuid2), + body=quota_json2, + content_type='application/json') + + for u in ('test', uuid2): + httpretty.register_uri(httpretty.DELETE, self.url(u), status=202) + + def test_quota(self, tenant_id='test'): + return { + 'tenant_id': tenant_id, + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1 + } + + +class V3(V1): + + def setUp(self): + super(V3, self).setUp() + + get_detail = { + 'quota_set': { + 'cores': {'reserved': 0, 'in_use': 0, 'limit': 10}, + 'instances': {'reserved': 0, 'in_use': 4, 'limit': 50}, + 'ram': {'reserved': 0, 'in_use': 1024, 'limit': 51200} + } + } + + httpretty.register_uri(httpretty.GET, self.url('test', 'detail'), + body=jsonutils.dumps(get_detail), + content_type='application/json') diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/security_group_rules.py b/awx/lib/site-packages/novaclient/tests/fixture_data/security_group_rules.py new file mode 100644 index 0000000000..303cfee7c6 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/security_group_rules.py @@ -0,0 +1,57 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests import fakes +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-security-group-rules' + + def setUp(self): + super(Fixture, self).setUp() + + rule = { + 'id': 1, + 'parent_group_id': 1, + 'group_id': 2, + 'ip_protocol': 'TCP', + 'from_port': '22', + 'to_port': 22, + 'cidr': '10.0.0.0/8' + } + + get_rules = {'security_group_rules': [rule]} + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_rules), + content_type='application/json') + + for u in (1, 11, 12): + httpretty.register_uri(httpretty.DELETE, self.url(u), status=202) + + def post_rules(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + assert list(body) == ['security_group_rule'] + fakes.assert_has_keys(body['security_group_rule'], + required=['parent_group_id'], + optional=['group_id', 'ip_protocol', + 'from_port', 'to_port', 'cidr']) + + return 202, headers, jsonutils.dumps({'security_group_rule': rule}) + + httpretty.register_uri(httpretty.POST, self.url(), + body=post_rules, + content_type='application/json') diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/security_groups.py b/awx/lib/site-packages/novaclient/tests/fixture_data/security_groups.py new file mode 100644 index 0000000000..9f0c271ad8 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/security_groups.py @@ -0,0 +1,99 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests import fakes +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-security-groups' + + def setUp(self): + super(Fixture, self).setUp() + + security_group_1 = { + "name": "test", + "description": "FAKE_SECURITY_GROUP", + "tenant_id": "4ffc664c198e435e9853f2538fbcd7a7", + "id": 1, + "rules": [ + { + "id": 11, + "group": {}, + "ip_protocol": "TCP", + "from_port": 22, + "to_port": 22, + "parent_group_id": 1, + "ip_range": {"cidr": "10.0.0.0/8"} + }, + { + "id": 12, + "group": { + "tenant_id": "272bee4c1e624cd4a72a6b0ea55b4582", + "name": "test2" + }, + "ip_protocol": "TCP", + "from_port": 222, + "to_port": 222, + "parent_group_id": 1, + "ip_range": {} + } + ] + } + + security_group_2 = { + "name": "test2", + "description": "FAKE_SECURITY_GROUP2", + "tenant_id": "272bee4c1e624cd4a72a6b0ea55b4582", + "id": 2, + "rules": [] + } + + get_groups = {'security_groups': [security_group_1, security_group_2]} + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_groups), + content_type='application/json') + + get_group_1 = {'security_group': security_group_1} + httpretty.register_uri(httpretty.GET, self.url(1), + body=jsonutils.dumps(get_group_1), + content_type='application/json') + + httpretty.register_uri(httpretty.DELETE, self.url(1), status=202) + + def post_os_security_groups(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + assert list(body) == ['security_group'] + fakes.assert_has_keys(body['security_group'], + required=['name', 'description']) + r = jsonutils.dumps({'security_group': security_group_1}) + return 202, headers, r + + httpretty.register_uri(httpretty.POST, self.url(), + body=post_os_security_groups, + content_type='application/json') + + def put_os_security_groups_1(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + assert list(body) == ['security_group'] + fakes.assert_has_keys(body['security_group'], + required=['name', 'description']) + return 205, headers, request.body + + httpretty.register_uri(httpretty.PUT, self.url(1), + body=put_os_security_groups_1, + content_type='application/json') diff --git a/awx/lib/site-packages/novaclient/tests/fixture_data/server_groups.py b/awx/lib/site-packages/novaclient/tests/fixture_data/server_groups.py new file mode 100644 index 0000000000..65a0c78d25 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/fixture_data/server_groups.py @@ -0,0 +1,75 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-server-groups' + + def setUp(self): + super(Fixture, self).setUp() + + server_groups = [ + { + "members": [], + "metadata": {}, + "id": "2cbd51f4-fafe-4cdb-801b-cf913a6f288b", + "policies": [], + "name": "ig1" + }, + { + "members": [], + "metadata": {}, + "id": "4473bb03-4370-4bfb-80d3-dc8cffc47d94", + "policies": ["anti-affinity"], + "name": "ig2" + }, + { + "members": [], + "metadata": {"key": "value"}, + "id": "31ab9bdb-55e1-4ac3-b094-97eeb1b65cc4", + "policies": [], "name": "ig3" + }, + { + "members": ["2dccb4a1-02b9-482a-aa23-5799490d6f5d"], + "metadata": {}, + "id": "4890bb03-7070-45fb-8453-d34556c87d94", + "policies": ["anti-affinity"], + "name": "ig2" + } + ] + + get_server_groups = {'server_groups': server_groups} + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_server_groups), + content_type='application/json') + + server = server_groups[0] + server_json = jsonutils.dumps({'server_group': server}) + + def _register(method, *args): + httpretty.register_uri(method, self.url(*args), body=server_json) + + _register(httpretty.POST) + _register(httpretty.POST, server['id']) + _register(httpretty.GET, server['id']) + _register(httpretty.PUT, server['id']) + _register(httpretty.POST, server['id'], '/action') + + httpretty.register_uri(httpretty.DELETE, self.url(server['id']), + status=202) diff --git a/awx/lib/site-packages/novaclient/tests/test_auth_plugins.py b/awx/lib/site-packages/novaclient/tests/test_auth_plugins.py index a084594165..34e83bacad 100644 --- a/awx/lib/site-packages/novaclient/tests/test_auth_plugins.py +++ b/awx/lib/site-packages/novaclient/tests/test_auth_plugins.py @@ -92,11 +92,11 @@ class DeprecatedAuthPluginTest(utils.TestCase): @mock.patch.object(pkg_resources, "iter_entry_points", mock_iter_entry_points) - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): plugin = auth_plugin.DeprecatedAuthPlugin("fake") cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", auth_system="fake", + utils.AUTH_URL_V2, auth_system="fake", auth_plugin=plugin) cs.client.authenticate() @@ -121,12 +121,12 @@ class DeprecatedAuthPluginTest(utils.TestCase): @mock.patch.object(pkg_resources, "iter_entry_points", mock_iter_entry_points) - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): auth_plugin.discover_auth_systems() plugin = auth_plugin.DeprecatedAuthPlugin("notexists") cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", auth_system="notexists", + utils.AUTH_URL_V2, auth_system="notexists", auth_plugin=plugin) self.assertRaises(exceptions.AuthSystemNotFound, cs.client.authenticate) @@ -164,7 +164,7 @@ class DeprecatedAuthPluginTest(utils.TestCase): @mock.patch.object(pkg_resources, "iter_entry_points", mock_iter_entry_points) - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): plugin = auth_plugin.DeprecatedAuthPlugin("fakewithauthurl") cs = client.Client("username", "password", "project_id", @@ -197,7 +197,7 @@ class DeprecatedAuthPluginTest(utils.TestCase): class AuthPluginTest(utils.TestCase): - @mock.patch.object(requests.Session, "request") + @mock.patch.object(requests, "request") @mock.patch.object(pkg_resources, "iter_entry_points") def test_auth_system_success(self, mock_iter_entry_points, mock_request): """Test that we can authenticate using the auth system.""" @@ -217,7 +217,7 @@ class AuthPluginTest(utils.TestCase): auth_plugin.discover_auth_systems() plugin = auth_plugin.load_plugin("fake") cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", auth_system="fake", + utils.AUTH_URL_V2, auth_system="fake", auth_plugin=plugin) cs.client.authenticate() diff --git a/awx/lib/site-packages/novaclient/tests/test_client.py b/awx/lib/site-packages/novaclient/tests/test_client.py index d586702409..bf868f8211 100644 --- a/awx/lib/site-packages/novaclient/tests/test_client.py +++ b/awx/lib/site-packages/novaclient/tests/test_client.py @@ -14,9 +14,12 @@ # under the License. +import logging import mock import requests +import fixtures + import novaclient.client import novaclient.extension import novaclient.tests.fakes as fakes @@ -27,6 +30,16 @@ import novaclient.v3.client import json +class ClientConnectionPoolTest(utils.TestCase): + + @mock.patch("novaclient.client.adapters.HTTPAdapter") + def test_get(self, mock_http_adapter): + mock_http_adapter.side_effect = lambda: mock.Mock() + pool = novaclient.client._ClientConnectionPool() + self.assertEqual(pool.get("abc"), pool.get("abc")) + self.assertNotEqual(pool.get("abc"), pool.get("def")) + + class ClientTest(utils.TestCase): def test_client_with_timeout(self): @@ -43,9 +56,9 @@ class ClientTest(utils.TestCase): 'x-server-management-url': 'blah.com', 'x-auth-token': 'blah', } - with mock.patch('requests.Session.request', mock_request): + with mock.patch('requests.request', mock_request): instance.authenticate() - requests.Session.request.assert_called_with(mock.ANY, mock.ANY, + requests.request.assert_called_with(mock.ANY, mock.ANY, timeout=2, headers=mock.ANY, verify=mock.ANY) @@ -61,7 +74,7 @@ class ClientTest(utils.TestCase): instance.version = 'v2.0' mock_request = mock.Mock() mock_request.side_effect = novaclient.exceptions.Unauthorized(401) - with mock.patch('requests.Session.request', mock_request): + with mock.patch('requests.request', mock_request): try: instance.get('/servers/detail') except Exception: @@ -197,6 +210,26 @@ class ClientTest(utils.TestCase): cs.authenticate() self.assertTrue(mock_authenticate.called) + @mock.patch('novaclient.client.HTTPClient') + def test_contextmanager_v1_1(self, mock_http_client): + fake_client = mock.Mock() + mock_http_client.return_value = fake_client + with novaclient.v1_1.client.Client("user", "password", "project_id", + auth_url="foo/v2"): + pass + self.assertTrue(fake_client.open_session.called) + self.assertTrue(fake_client.close_session.called) + + @mock.patch('novaclient.client.HTTPClient') + def test_contextmanager_v3(self, mock_http_client): + fake_client = mock.Mock() + mock_http_client.return_value = fake_client + with novaclient.v3.client.Client("user", "password", "project_id", + auth_url="foo/v2"): + pass + self.assertTrue(fake_client.open_session.called) + self.assertTrue(fake_client.close_session.called) + def test_get_password_simple(self): cs = novaclient.client.HTTPClient("user", "password", "", "") cs.password_func = mock.Mock() @@ -216,3 +249,110 @@ class ClientTest(utils.TestCase): cs.password_func = mock.Mock() self.assertEqual(cs._get_password(), "password") self.assertFalse(cs.password_func.called) + + def test_auth_url_rstrip_slash(self): + cs = novaclient.client.HTTPClient("user", "password", "project_id", + auth_url="foo/v2/") + self.assertEqual(cs.auth_url, "foo/v2") + + def test_token_and_bypass_url(self): + cs = novaclient.client.HTTPClient(None, None, None, + auth_token="12345", + bypass_url="compute/v100/") + self.assertIsNone(cs.auth_url) + self.assertEqual(cs.auth_token, "12345") + self.assertEqual(cs.bypass_url, "compute/v100") + self.assertEqual(cs.management_url, "compute/v100") + + @mock.patch("novaclient.client.requests.Session") + def test_session(self, mock_session): + fake_session = mock.Mock() + mock_session.return_value = fake_session + cs = novaclient.client.HTTPClient("user", None, "", "") + cs.open_session() + self.assertEqual(cs._session, fake_session) + cs.close_session() + self.assertIsNone(cs._session) + + def test_session_connection_pool(self): + cs = novaclient.client.HTTPClient("user", None, "", + "", connection_pool=True) + cs.open_session() + self.assertIsNone(cs._session) + cs.close_session() + self.assertIsNone(cs._session) + + def test_get_session(self): + cs = novaclient.client.HTTPClient("user", None, "", "") + self.assertIsNone(cs._get_session("http://nooooooooo.com")) + + @mock.patch("novaclient.client.requests.Session") + def test_get_session_open_session(self, mock_session): + fake_session = mock.Mock() + mock_session.return_value = fake_session + cs = novaclient.client.HTTPClient("user", None, "", "") + cs.open_session() + self.assertEqual(fake_session, cs._get_session("http://example.com")) + + @mock.patch("novaclient.client.requests.Session") + @mock.patch("novaclient.client._ClientConnectionPool") + def test_get_session_connection_pool(self, mock_pool, mock_session): + service_url = "http://example.com" + + pool = mock.MagicMock() + pool.get.return_value = "http_adapter" + mock_pool.return_value = pool + cs = novaclient.client.HTTPClient("user", None, "", + "", connection_pool=True) + cs._current_url = "http://another.com" + + session = cs._get_session(service_url) + self.assertEqual(session, mock_session.return_value) + pool.get.assert_called_once_with(service_url) + mock_session().mount.assert_called_once_with(service_url, + 'http_adapter') + + def test_init_without_connection_pool(self): + cs = novaclient.client.HTTPClient("user", None, "", "") + self.assertIsNone(cs._connection_pool) + + @mock.patch("novaclient.client._ClientConnectionPool") + def test_init_with_proper_connection_pool(self, mock_pool): + fake_pool = mock.Mock() + mock_pool.return_value = fake_pool + cs = novaclient.client.HTTPClient("user", None, "", + connection_pool=True) + self.assertEqual(cs._connection_pool, fake_pool) + + def test_log_req(self): + self.logger = self.useFixture( + fixtures.FakeLogger( + format="%(message)s", + level=logging.DEBUG, + nuke_handlers=True + ) + ) + cs = novaclient.client.HTTPClient("user", None, "", + connection_pool=True) + cs.http_log_debug = True + cs.http_log_req('GET', '/foo', {'headers': {}}) + cs.http_log_req('GET', '/foo', {'headers': + {'X-Auth-Token': 'totally_bogus'} + }) + cs.http_log_req('GET', '/foo', {'headers': + {'X-Foo': 'bar', + 'X-Auth-Token': 'totally_bogus'} + }) + + output = self.logger.output.split('\n') + + self.assertIn("REQ: curl -i '/foo' -X GET", output) + self.assertIn( + "REQ: curl -i '/foo' -X GET -H " + '"X-Auth-Token: {SHA1}b42162b6ffdbd7c3c37b7c95b7ba9f51dda0236d"', + output) + self.assertIn( + "REQ: curl -i '/foo' -X GET -H " + '"X-Auth-Token: {SHA1}b42162b6ffdbd7c3c37b7c95b7ba9f51dda0236d"' + ' -H "X-Foo: bar"', + output) diff --git a/awx/lib/site-packages/novaclient/tests/test_http.py b/awx/lib/site-packages/novaclient/tests/test_http.py index e2fc4fa109..2797a438f5 100644 --- a/awx/lib/site-packages/novaclient/tests/test_http.py +++ b/awx/lib/site-packages/novaclient/tests/test_http.py @@ -13,6 +13,7 @@ import mock import requests +import six from novaclient import client from novaclient import exceptions @@ -37,10 +38,17 @@ bad_req_response = utils.TestResponse({ }) bad_req_mock_request = mock.Mock(return_value=(bad_req_response)) +unknown_error_response = utils.TestResponse({ + "status_code": 503, + "text": '', +}) +unknown_error_mock_request = mock.Mock(return_value=unknown_error_response) + def get_client(): cl = client.HTTPClient("username", "password", - "project_id", "auth_test") + "project_id", + utils.AUTH_URL_V2) return cl @@ -56,7 +64,7 @@ class ClientTest(utils.TestCase): def test_get(self): cl = get_authed_client() - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) @mock.patch('time.time', mock.Mock(return_value=1234)) def test_get_call(): resp, body = cl.get("/hi") @@ -78,7 +86,7 @@ class ClientTest(utils.TestCase): def test_post(self): cl = get_authed_client() - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_post_call(): cl.post("/hi", body=[1, 2, 3]) headers = { @@ -110,7 +118,7 @@ class ClientTest(utils.TestCase): def test_connection_refused(self): cl = get_client() - @mock.patch.object(requests.Session, "request", refused_mock_request) + @mock.patch.object(requests, "request", refused_mock_request) def test_refused_call(): self.assertRaises(exceptions.ConnectionRefused, cl.get, "/hi") @@ -119,7 +127,7 @@ class ClientTest(utils.TestCase): def test_bad_request(self): cl = get_client() - @mock.patch.object(requests.Session, "request", bad_req_mock_request) + @mock.patch.object(requests, "request", bad_req_mock_request) def test_refused_call(): self.assertRaises(exceptions.BadRequest, cl.get, "/hi") @@ -133,3 +141,16 @@ class ClientTest(utils.TestCase): cl2 = client.HTTPClient("username", "password", "project_id", "auth_test", http_log_debug=True) self.assertEqual(len(cl2._logger.handlers), 1) + + @mock.patch.object(requests, 'request', unknown_error_mock_request) + def test_unknown_server_error(self): + cl = get_client() + # This would be cleaner with the context manager version of + # assertRaises or assertRaisesRegexp, but both only appeared in + # Python 2.7 and testtools doesn't match that implementation yet + try: + cl.get('/hi') + except exceptions.ClientException as exc: + self.assertIn('Unknown Error', six.text_type(exc)) + else: + self.fail('Expected exceptions.ClientException') diff --git a/awx/lib/site-packages/novaclient/tests/test_shell.py b/awx/lib/site-packages/novaclient/tests/test_shell.py index 7cbb89b9b0..fdcc78e684 100644 --- a/awx/lib/site-packages/novaclient/tests/test_shell.py +++ b/awx/lib/site-packages/novaclient/tests/test_shell.py @@ -32,7 +32,7 @@ FAKE_ENV = {'OS_USERNAME': 'username', 'OS_TENANT_NAME': 'tenant_name', 'OS_AUTH_URL': 'http://no.where'} -FAKE_ENV2 = {'OS_USERNAME': 'username', +FAKE_ENV2 = {'OS_USER_ID': 'user_id', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', 'OS_AUTH_URL': 'http://no.where'} @@ -133,25 +133,38 @@ class ShellTest(utils.TestCase): matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) def test_no_username(self): - required = ('You must provide a username' - ' via either --os-username or env[OS_USERNAME]',) + required = ('You must provide a username or user id' + ' via --os-username, --os-user-id,' + ' env[OS_USERNAME] or env[OS_USER_ID]') self.make_env(exclude='OS_USERNAME') try: self.shell('list') except exceptions.CommandError as message: - self.assertEqual(required, message.args) + self.assertEqual(required, message.args[0]) + else: + self.fail('CommandError not raised') + + def test_no_user_id(self): + required = ('You must provide a username or user id' + ' via --os-username, --os-user-id,' + ' env[OS_USERNAME] or env[OS_USER_ID]') + self.make_env(exclude='OS_USER_ID', fake_env=FAKE_ENV2) + try: + self.shell('list') + except exceptions.CommandError as message: + self.assertEqual(required, message.args[0]) else: self.fail('CommandError not raised') def test_no_tenant_name(self): required = ('You must provide a tenant name or tenant id' ' via --os-tenant-name, --os-tenant-id,' - ' env[OS_TENANT_NAME] or env[OS_TENANT_ID]',) + ' env[OS_TENANT_NAME] or env[OS_TENANT_ID]') self.make_env(exclude='OS_TENANT_NAME') try: self.shell('list') except exceptions.CommandError as message: - self.assertEqual(required, message.args) + self.assertEqual(required, message.args[0]) else: self.fail('CommandError not raised') @@ -244,3 +257,17 @@ class ShellTest(utils.TestCase): @mock.patch('novaclient.client.Client') def test_v_unknown_service_type(self, mock_client): self._test_service_type('unknown', 'compute', mock_client) + + @mock.patch('sys.argv', ['nova']) + @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stderr', six.StringIO()) + def test_main_noargs(self): + # Ensure that main works with no command-line arguments + try: + novaclient.shell.main() + except SystemExit: + self.fail('Unexpected SystemExit') + + # We expect the normal usage as a result + self.assertIn('Command-line interface to the OpenStack Nova API', + sys.stdout.getvalue()) diff --git a/awx/lib/site-packages/novaclient/tests/test_utils.py b/awx/lib/site-packages/novaclient/tests/test_utils.py index ba6801198b..31678ca2a3 100644 --- a/awx/lib/site-packages/novaclient/tests/test_utils.py +++ b/awx/lib/site-packages/novaclient/tests/test_utils.py @@ -301,3 +301,23 @@ class ValidationsTestCase(test_utils.TestCase): self.fail("Invalid key passed validation: %s" % key) except exceptions.CommandError as ce: self.assertTrue(key in str(ce)) + + +class ResourceManagerExtraKwargsHookTestCase(test_utils.TestCase): + def test_get_resource_manager_extra_kwargs_hook_test(self): + do_foo = mock.MagicMock() + + def hook1(args): + return {'kwarg1': 'v_hook1'} + + def hook2(args): + return {'kwarg1': 'v_hook2'} + do_foo.resource_manager_kwargs_hooks = [hook1, hook2] + args = {} + exc = self.assertRaises(exceptions.NoUniqueMatch, + utils.get_resource_manager_extra_kwargs, + do_foo, + args) + except_error = ("Hook 'hook2' is attempting to redefine " + "attributes") + self.assertIn(except_error, six.text_type(exc)) diff --git a/awx/lib/site-packages/novaclient/tests/utils.py b/awx/lib/site-packages/novaclient/tests/utils.py index 11bfa827b4..be251998cd 100644 --- a/awx/lib/site-packages/novaclient/tests/utils.py +++ b/awx/lib/site-packages/novaclient/tests/utils.py @@ -14,9 +14,18 @@ import os import fixtures +import httpretty import requests +import six +import testscenarios import testtools +from novaclient.openstack.common import jsonutils + +AUTH_URL = "http://localhost:5002/auth_url" +AUTH_URL_V1 = "http://localhost:5002/auth_url/v1.0" +AUTH_URL_V2 = "http://localhost:5002/auth_url/v2.0" + class TestCase(testtools.TestCase): TEST_REQUEST_BASE = { @@ -35,6 +44,40 @@ class TestCase(testtools.TestCase): self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) +class FixturedTestCase(testscenarios.TestWithScenarios, TestCase): + + client_fixture_class = None + data_fixture_class = None + + def setUp(self): + super(FixturedTestCase, self).setUp() + + httpretty.reset() + self.data_fixture = None + self.client_fixture = None + self.cs = None + + if self.client_fixture_class: + self.client_fixture = self.useFixture(self.client_fixture_class()) + self.cs = self.client_fixture.client + + if self.data_fixture_class: + self.data_fixture = self.useFixture(self.data_fixture_class()) + + def assert_called(self, method, path, body=None): + self.assertEqual(httpretty.last_request().method, method) + self.assertEqual(httpretty.last_request().path, path) + + if body: + req_data = httpretty.last_request().body + if isinstance(req_data, six.binary_type): + req_data = req_data.decode('utf-8') + if not isinstance(body, six.string_types): + # json load if the input body to match against is not a string + req_data = jsonutils.loads(req_data) + self.assertEqual(req_data, body) + + class TestResponse(requests.Response): """ Class used to wrap requests.Response and provide some diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py b/awx/lib/site-packages/novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py index 34d958f080..c3e47b4581 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py @@ -32,10 +32,10 @@ cs = fakes.FakeClient(extensions=extensions) class AssistedVolumeSnapshotsTestCase(utils.TestCase): def test_create_snap(self): - res = cs.assisted_volume_snapshots.create('1', {}) + cs.assisted_volume_snapshots.create('1', {}) cs.assert_called('POST', '/os-assisted-volume-snapshots') def test_delete_snap(self): - res = cs.assisted_volume_snapshots.delete('x', {}) + cs.assisted_volume_snapshots.delete('x', {}) cs.assert_called('DELETE', '/os-assisted-volume-snapshots/x?delete_info={}') diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/fakes.py b/awx/lib/site-packages/novaclient/tests/v1_1/fakes.py index dd8fdccc4a..f61a02e928 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/fakes.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/fakes.py @@ -369,7 +369,10 @@ class FakeHTTPClient(base_client.HTTPClient): if 'personality' in body['server']: for pfile in body['server']['personality']: fakes.assert_has_keys(pfile, required=['path', 'contents']) - return (202, {}, self.get_servers_1234()[2]) + if body['server']['name'] == 'some-bad-server': + return (202, {}, self.get_servers_1235()[2]) + else: + return (202, {}, self.get_servers_1234()[2]) def post_os_volumes_boot(self, body, **kw): assert set(body.keys()) <= set(['server', 'os:scheduler_hints']) @@ -392,6 +395,13 @@ class FakeHTTPClient(base_client.HTTPClient): r = {'server': self.get_servers_detail()[2]['servers'][0]} return (200, {}, r) + def get_servers_1235(self, **kw): + r = {'server': self.get_servers_detail()[2]['servers'][0]} + r['server']['id'] = 1235 + r['server']['status'] = 'error' + r['server']['fault'] = {'message': 'something went wrong!'} + return (200, {}, r) + def get_servers_5678(self, **kw): r = {'server': self.get_servers_detail()[2]['servers'][1]} return (200, {}, r) @@ -405,6 +415,12 @@ class FakeHTTPClient(base_client.HTTPClient): fakes.assert_has_keys(body['server'], optional=['name', 'adminPass']) return (204, {}, body) + def delete_os_server_groups_12345(self, **kw): + return (202, {}, None) + + def delete_os_server_groups_56789(self, **kw): + return (202, {}, None) + def delete_servers_1234(self, **kw): return (202, {}, None) @@ -525,11 +541,11 @@ class FakeHTTPClient(base_client.HTTPClient): assert list(body[action]) == ['type'] assert body[action]['type'] in ['HARD', 'SOFT'] elif action == 'rebuild': - keys = list(body[action]) - if 'adminPass' in keys: - keys.remove('adminPass') - assert 'imageRef' in keys + body = body[action] + adminPass = body.get('adminPass', 'randompassword') + assert 'imageRef' in body _body = self.get_servers_1234()[2] + _body['server']['adminPass'] = adminPass elif action == 'resize': keys = body[action].keys() assert 'flavorRef' in keys @@ -625,6 +641,9 @@ class FakeHTTPClient(base_client.HTTPClient): raise AssertionError("Unexpected server action: %s" % action) return (resp, _headers, _body) + def post_servers_5678_action(self, body, **kw): + return self.post_servers_1234_action(body, **kw) + # # Cloudpipe # @@ -734,8 +753,11 @@ class FakeHTTPClient(base_client.HTTPClient): def get_flavors_512_MB_Server(self, **kw): raise exceptions.NotFound('404') + def get_flavors_128_MB_Server(self, **kw): + raise exceptions.NotFound('404') + def get_flavors_aa1(self, **kw): - # Aplhanumeric flavor id are allowed. + # Alphanumeric flavor id are allowed. return ( 200, {}, @@ -1003,12 +1025,24 @@ class FakeHTTPClient(base_client.HTTPClient): # Keypairs # def get_os_keypairs_test(self, *kw): - return (200, {}, {'keypair': self.get_os_keypairs()[2]['keypairs'][0]}) + return (200, {}, {'keypair': + self.get_os_keypairs()[2]['keypairs'][0]['keypair']}) def get_os_keypairs(self, *kw): return (200, {}, {"keypairs": [ - {'fingerprint': 'FAKE_KEYPAIR', 'name': 'test'} - ]}) + {"keypair": { + "public_key": "FAKE_SSH_RSA", + "private_key": "FAKE_PRIVATE_KEY", + "user_id": + "81e373b596d6466e99c4896826abaa46", + "name": "test", + "deleted": False, + "created_at": "2014-04-19T02:16:44.000000", + "updated_at": "2014-04-19T10:12:3.000000", + "figerprint": "FAKE_KEYPAIR", + "deleted_at": None, + "id": 4} + }]}) def delete_os_keypairs_test(self, **kw): return (202, {}, None) @@ -1017,7 +1051,7 @@ class FakeHTTPClient(base_client.HTTPClient): assert list(body) == ['keypair'] fakes.assert_has_keys(body['keypair'], required=['name']) - r = {'keypair': self.get_os_keypairs()[2]['keypairs'][0]} + r = {'keypair': self.get_os_keypairs()[2]['keypairs'][0]['keypair']} return (202, {}, r) # @@ -1486,6 +1520,9 @@ class FakeHTTPClient(base_client.HTTPClient): 'status': 'disabled', 'disabled_reason': body['disabled_reason']}}) + def delete_os_services_1(self, **kw): + return (204, {}, None) + # # Fixed IPs # @@ -1996,3 +2033,48 @@ class FakeHTTPClient(base_client.HTTPClient): return (200, {}, {'events': [ {'name': 'network-changed', 'server_uuid': '1234'}]}) + + # + # Server Groups + # + + def get_os_server_groups(self, *kw): + return (200, {}, + {"server_groups": [ + {"members": [], "metadata": {}, + "id": "2cbd51f4-fafe-4cdb-801b-cf913a6f288b", + "policies": [], "name": "ig1"}, + {"members": [], "metadata": {}, + "id": "4473bb03-4370-4bfb-80d3-dc8cffc47d94", + "policies": ["anti-affinity"], "name": "ig2"}, + {"members": [], "metadata": {"key": "value"}, + "id": "31ab9bdb-55e1-4ac3-b094-97eeb1b65cc4", + "policies": [], "name": "ig3"}, + {"members": ["2dccb4a1-02b9-482a-aa23-5799490d6f5d"], + "metadata": {}, + "id": "4890bb03-7070-45fb-8453-d34556c87d94", + "policies": ["anti-affinity"], "name": "ig2"}]}) + + def _return_server_group(self): + r = {'server_group': + self.get_os_server_groups()[2]['server_groups'][0]} + return (200, {}, r) + + def post_os_server_groups(self, body, **kw): + return self._return_server_group() + + def get_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b(self, + **kw): + return self._return_server_group() + + def put_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b(self, + **kw): + return self._return_server_group() + + def post_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b_action( + self, body, **kw): + return self._return_server_group() + + def delete_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b( + self, **kw): + return (202, {}, None) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_agents.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_agents.py index e89da9e8a0..46efe839a1 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_agents.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_agents.py @@ -13,35 +13,64 @@ # License for the specific language governing permissions and limitations # under the License. +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import agents as data +from novaclient.tests.fixture_data import client from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import agents -class AgentsTest(utils.TestCase): - def setUp(self): - super(AgentsTest, self).setUp() - self.cs = self._get_fake_client() - self.agent_type = self._get_agent_type() +class AgentsTest(utils.FixturedTestCase): - def _get_fake_client(self): - return fakes.FakeClient() + data_fixture_class = data.Fixture - def _get_agent_type(self): - return agents.Agent + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + + def stub_hypervisors(self, hypervisor='kvm'): + get_os_agents = {'agents': + [ + { + 'hypervisor': hypervisor, + 'os': 'win', + 'architecture': 'x86', + 'version': '7.0', + 'url': 'xxx://xxxx/xxx/xxx', + 'md5hash': 'add6bb58e139be103324d04d82d8f545', + 'id': 1 + }, + { + 'hypervisor': hypervisor, + 'os': 'linux', + 'architecture': 'x86', + 'version': '16.0', + 'url': 'xxx://xxxx/xxx/xxx1', + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'id': 2 + }, + ] + } + + httpretty.register_uri(httpretty.GET, self.data_fixture.url(), + body=jsonutils.dumps(get_os_agents), + content_type='application/json') def test_list_agents(self): + self.stub_hypervisors() ags = self.cs.agents.list() - self.cs.assert_called('GET', '/os-agents') + self.assert_called('GET', '/os-agents') for a in ags: - self.assertIsInstance(a, self.agent_type) + self.assertIsInstance(a, agents.Agent) self.assertEqual(a.hypervisor, 'kvm') def test_list_agents_with_hypervisor(self): + self.stub_hypervisors('xen') ags = self.cs.agents.list('xen') - self.cs.assert_called('GET', '/os-agents?hypervisor=xen') + self.assert_called('GET', '/os-agents?hypervisor=xen') for a in ags: - self.assertIsInstance(a, self.agent_type) + self.assertIsInstance(a, agents.Agent) self.assertEqual(a.hypervisor, 'xen') def test_agents_create(self): @@ -56,12 +85,12 @@ class AgentsTest(utils.TestCase): 'version': '7.0', 'architecture': 'x86', 'os': 'win'}} - self.cs.assert_called('POST', '/os-agents', body) + self.assert_called('POST', '/os-agents', body) self.assertEqual(1, ag._info.copy()['id']) def test_agents_delete(self): self.cs.agents.delete('1') - self.cs.assert_called('DELETE', '/os-agents/1') + self.assert_called('DELETE', '/os-agents/1') def _build_example_update_body(self): return {"para": { @@ -74,5 +103,5 @@ class AgentsTest(utils.TestCase): '/yyy/yyyy/yyyy', 'add6bb58e139be103324d04d82d8f546') body = self._build_example_update_body() - self.cs.assert_called('PUT', '/os-agents/1', body) + self.assert_called('PUT', '/os-agents/1', body) self.assertEqual(1, ag.id) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_aggregates.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_aggregates.py index ef03051667..6e7bd44e9c 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_aggregates.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_aggregates.py @@ -13,51 +13,47 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import aggregates as data +from novaclient.tests.fixture_data import client from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import aggregates -class AggregatesTest(utils.TestCase): - def setUp(self): - super(AggregatesTest, self).setUp() - self.cs = self._get_fake_client() - self.aggregate_type = self._get_aggregate_type() +class AggregatesTest(utils.FixturedTestCase): - def _get_fake_client(self): - return fakes.FakeClient() + data_fixture_class = data.Fixture - def _get_aggregate_type(self): - return aggregates.Aggregate + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] def test_list_aggregates(self): result = self.cs.aggregates.list() - self.cs.assert_called('GET', '/os-aggregates') + self.assert_called('GET', '/os-aggregates') for aggregate in result: self.assertIsInstance(aggregate, aggregates.Aggregate) def test_create_aggregate(self): body = {"aggregate": {"name": "test", "availability_zone": "nova1"}} aggregate = self.cs.aggregates.create("test", "nova1") - self.cs.assert_called('POST', '/os-aggregates', body) + self.assert_called('POST', '/os-aggregates', body) self.assertIsInstance(aggregate, aggregates.Aggregate) def test_get(self): aggregate = self.cs.aggregates.get("1") - self.cs.assert_called('GET', '/os-aggregates/1') + self.assert_called('GET', '/os-aggregates/1') self.assertIsInstance(aggregate, aggregates.Aggregate) aggregate2 = self.cs.aggregates.get(aggregate) - self.cs.assert_called('GET', '/os-aggregates/1') + self.assert_called('GET', '/os-aggregates/1') self.assertIsInstance(aggregate2, aggregates.Aggregate) def test_get_details(self): aggregate = self.cs.aggregates.get_details("1") - self.cs.assert_called('GET', '/os-aggregates/1') + self.assert_called('GET', '/os-aggregates/1') self.assertIsInstance(aggregate, aggregates.Aggregate) aggregate2 = self.cs.aggregates.get_details(aggregate) - self.cs.assert_called('GET', '/os-aggregates/1') + self.assert_called('GET', '/os-aggregates/1') self.assertIsInstance(aggregate2, aggregates.Aggregate) def test_update(self): @@ -66,11 +62,11 @@ class AggregatesTest(utils.TestCase): body = {"aggregate": values} result1 = aggregate.update(values) - self.cs.assert_called('PUT', '/os-aggregates/1', body) + self.assert_called('PUT', '/os-aggregates/1', body) self.assertIsInstance(result1, aggregates.Aggregate) result2 = self.cs.aggregates.update(2, values) - self.cs.assert_called('PUT', '/os-aggregates/2', body) + self.assert_called('PUT', '/os-aggregates/2', body) self.assertIsInstance(result2, aggregates.Aggregate) def test_update_with_availability_zone(self): @@ -79,7 +75,7 @@ class AggregatesTest(utils.TestCase): body = {"aggregate": values} result3 = self.cs.aggregates.update(aggregate, values) - self.cs.assert_called('PUT', '/os-aggregates/1', body) + self.assert_called('PUT', '/os-aggregates/1', body) self.assertIsInstance(result3, aggregates.Aggregate) def test_add_host(self): @@ -88,15 +84,15 @@ class AggregatesTest(utils.TestCase): body = {"add_host": {"host": "host1"}} result1 = aggregate.add_host(host) - self.cs.assert_called('POST', '/os-aggregates/1/action', body) + self.assert_called('POST', '/os-aggregates/1/action', body) self.assertIsInstance(result1, aggregates.Aggregate) result2 = self.cs.aggregates.add_host("2", host) - self.cs.assert_called('POST', '/os-aggregates/2/action', body) + self.assert_called('POST', '/os-aggregates/2/action', body) self.assertIsInstance(result2, aggregates.Aggregate) result3 = self.cs.aggregates.add_host(aggregate, host) - self.cs.assert_called('POST', '/os-aggregates/1/action', body) + self.assert_called('POST', '/os-aggregates/1/action', body) self.assertIsInstance(result3, aggregates.Aggregate) def test_remove_host(self): @@ -105,15 +101,15 @@ class AggregatesTest(utils.TestCase): body = {"remove_host": {"host": "host1"}} result1 = aggregate.remove_host(host) - self.cs.assert_called('POST', '/os-aggregates/1/action', body) + self.assert_called('POST', '/os-aggregates/1/action', body) self.assertIsInstance(result1, aggregates.Aggregate) result2 = self.cs.aggregates.remove_host("2", host) - self.cs.assert_called('POST', '/os-aggregates/2/action', body) + self.assert_called('POST', '/os-aggregates/2/action', body) self.assertIsInstance(result2, aggregates.Aggregate) result3 = self.cs.aggregates.remove_host(aggregate, host) - self.cs.assert_called('POST', '/os-aggregates/1/action', body) + self.assert_called('POST', '/os-aggregates/1/action', body) self.assertIsInstance(result3, aggregates.Aggregate) def test_set_metadata(self): @@ -122,24 +118,24 @@ class AggregatesTest(utils.TestCase): body = {"set_metadata": {"metadata": metadata}} result1 = aggregate.set_metadata(metadata) - self.cs.assert_called('POST', '/os-aggregates/1/action', body) + self.assert_called('POST', '/os-aggregates/1/action', body) self.assertIsInstance(result1, aggregates.Aggregate) result2 = self.cs.aggregates.set_metadata(2, metadata) - self.cs.assert_called('POST', '/os-aggregates/2/action', body) + self.assert_called('POST', '/os-aggregates/2/action', body) self.assertIsInstance(result2, aggregates.Aggregate) result3 = self.cs.aggregates.set_metadata(aggregate, metadata) - self.cs.assert_called('POST', '/os-aggregates/1/action', body) + self.assert_called('POST', '/os-aggregates/1/action', body) self.assertIsInstance(result3, aggregates.Aggregate) def test_delete_aggregate(self): aggregate = self.cs.aggregates.list()[0] aggregate.delete() - self.cs.assert_called('DELETE', '/os-aggregates/1') + self.assert_called('DELETE', '/os-aggregates/1') self.cs.aggregates.delete('1') - self.cs.assert_called('DELETE', '/os-aggregates/1') + self.assert_called('DELETE', '/os-aggregates/1') self.cs.aggregates.delete(aggregate) - self.cs.assert_called('DELETE', '/os-aggregates/1') + self.assert_called('DELETE', '/os-aggregates/1') diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_auth.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_auth.py index 7344bc76ee..c3df530b40 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_auth.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_auth.py @@ -25,7 +25,7 @@ from novaclient.v1_1 import client class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_authenticate_success(self): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", service_type='compute') + utils.AUTH_URL_V2, service_type='compute') resp = { "access": { "token": { @@ -57,7 +57,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): mock_request = mock.Mock(return_value=(auth_response)) - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): cs.client.authenticate() headers = { @@ -94,7 +94,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_authenticate_failure(self): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0") + utils.AUTH_URL_V2) resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} auth_response = utils.TestResponse({ "status_code": 401, @@ -111,7 +111,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_v1_auth_redirect(self): cs = client.Client("username", "password", "project_id", - "auth_url/v1.0", service_type='compute') + utils.AUTH_URL_V1, service_type='compute') dict_correct_response = { "access": { "token": { @@ -160,7 +160,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): mock_request = mock.Mock(side_effect=side_effect) - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): cs.client.authenticate() headers = { @@ -199,7 +199,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_v2_auth_redirect(self): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", service_type='compute') + utils.AUTH_URL_V2, service_type='compute') dict_correct_response = { "access": { "token": { @@ -248,7 +248,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): mock_request = mock.Mock(side_effect=side_effect) - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): cs.client.authenticate() headers = { @@ -287,7 +287,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_ambiguous_endpoints(self): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", service_type='compute') + utils.AUTH_URL_V2, service_type='compute') resp = { "access": { "token": { @@ -340,7 +340,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_authenticate_with_token_success(self): cs = client.Client("username", None, "project_id", - "auth_url/v2.0", service_type='compute') + utils.AUTH_URL_V2, service_type='compute') cs.client.auth_token = "FAKE_ID" resp = { "access": { @@ -373,7 +373,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): mock_request = mock.Mock(return_value=(auth_response)) - with mock.patch.object(requests.Session, "request", mock_request): + with mock.patch.object(requests, "request", mock_request): cs.client.authenticate() headers = { 'User-Agent': cs.client.USER_AGENT, @@ -405,7 +405,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): self.assertEqual(cs.client.auth_token, token_id) def test_authenticate_with_token_failure(self): - cs = client.Client("username", None, "project_id", "auth_url/v2.0") + cs = client.Client("username", None, "project_id", utils.AUTH_URL_V2) cs.client.auth_token = "FAKE_ID" resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} auth_response = utils.TestResponse({ @@ -421,7 +421,8 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): class AuthenticationTests(utils.TestCase): def test_authenticate_success(self): - cs = client.Client("username", "password", "project_id", "auth_url") + cs = client.Client("username", "password", + "project_id", utils.AUTH_URL) management_url = 'https://localhost/v1.1/443470' auth_response = utils.TestResponse({ 'status_code': 204, @@ -432,7 +433,7 @@ class AuthenticationTests(utils.TestCase): }) mock_request = mock.Mock(return_value=(auth_response)) - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): cs.client.authenticate() headers = { @@ -456,18 +457,20 @@ class AuthenticationTests(utils.TestCase): test_auth_call() def test_authenticate_failure(self): - cs = client.Client("username", "password", "project_id", "auth_url") + cs = client.Client("username", "password", + "project_id", utils.AUTH_URL) auth_response = utils.TestResponse({'status_code': 401}) mock_request = mock.Mock(return_value=(auth_response)) - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) test_auth_call() def test_auth_automatic(self): - cs = client.Client("username", "password", "project_id", "auth_url") + cs = client.Client("username", "password", + "project_id", utils.AUTH_URL) http_client = cs.client http_client.management_url = '' mock_request = mock.Mock(return_value=(None, None)) @@ -482,7 +485,8 @@ class AuthenticationTests(utils.TestCase): test_auth_call() def test_auth_manual(self): - cs = client.Client("username", "password", "project_id", "auth_url") + cs = client.Client("username", "password", + "project_id", utils.AUTH_URL) @mock.patch.object(cs.client, 'authenticate') def test_auth_call(m): diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_availability_zone.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_availability_zone.py index 6d742b1b6e..2144d8c19d 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_availability_zone.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_availability_zone.py @@ -16,24 +16,26 @@ import six +from novaclient.tests.fixture_data import availability_zones as data +from novaclient.tests.fixture_data import client from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import availability_zones -class AvailabilityZoneTest(utils.TestCase): +class AvailabilityZoneTest(utils.FixturedTestCase): # NOTE(cyeoh): import shell here so the V3 version of # this class can inherit off the v3 version of shell from novaclient.v1_1 import shell # noqa + data_fixture_class = data.V1 + + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + def setUp(self): super(AvailabilityZoneTest, self).setUp() - self.cs = self._get_fake_client() self.availability_zone_type = self._get_availability_zone_type() - def _get_fake_client(self): - return fakes.FakeClient() - def _get_availability_zone_type(self): return availability_zones.AvailabilityZone @@ -43,7 +45,7 @@ class AvailabilityZoneTest(utils.TestCase): def test_list_availability_zone(self): zones = self.cs.availability_zones.list(detailed=False) - self.cs.assert_called('GET', '/os-availability-zone') + self.assert_called('GET', '/os-availability-zone') for zone in zones: self.assertIsInstance(zone, self.availability_zone_type) @@ -63,7 +65,7 @@ class AvailabilityZoneTest(utils.TestCase): def test_detail_availability_zone(self): zones = self.cs.availability_zones.list(detailed=True) - self.cs.assert_called('GET', '/os-availability-zone/detail') + self.assert_called('GET', '/os-availability-zone/detail') for zone in zones: self.assertIsInstance(zone, self.availability_zone_type) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_certs.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_certs.py index 87e1d09f00..0a568d5851 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_certs.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_certs.py @@ -11,29 +11,26 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import certs as data +from novaclient.tests.fixture_data import client from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import certs -class CertsTest(utils.TestCase): - def setUp(self): - super(CertsTest, self).setUp() - self.cs = self._get_fake_client() - self.cert_type = self._get_cert_type() +class CertsTest(utils.FixturedTestCase): - def _get_fake_client(self): - return fakes.FakeClient() + data_fixture_class = data.Fixture + cert_type = certs.Certificate - def _get_cert_type(self): - return certs.Certificate + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] def test_create_cert(self): cert = self.cs.certs.create() - self.cs.assert_called('POST', '/os-certificates') - self.assertIsInstance(cert, certs.Certificate) + self.assert_called('POST', '/os-certificates') + self.assertIsInstance(cert, self.cert_type) def test_get_root_cert(self): cert = self.cs.certs.get() - self.cs.assert_called('GET', '/os-certificates/root') - self.assertIsInstance(cert, certs.Certificate) + self.assert_called('GET', '/os-certificates/root') + self.assertIsInstance(cert, self.cert_type) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_cloudpipe.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_cloudpipe.py index 8cb96aa429..0a319085bc 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_cloudpipe.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_cloudpipe.py @@ -11,30 +11,35 @@ # License for the specific language governing permissions and limitations # under the License. +import six + +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import cloudpipe as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import cloudpipe -cs = fakes.FakeClient() +class CloudpipeTest(utils.FixturedTestCase): + data_fixture_class = data.Fixture -class CloudpipeTest(utils.TestCase): + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] def test_list_cloudpipes(self): - cp = cs.cloudpipe.list() - cs.assert_called('GET', '/os-cloudpipe') + cp = self.cs.cloudpipe.list() + self.assert_called('GET', '/os-cloudpipe') [self.assertIsInstance(c, cloudpipe.Cloudpipe) for c in cp] def test_create(self): project = "test" - cp = cs.cloudpipe.create(project) + cp = self.cs.cloudpipe.create(project) body = {'cloudpipe': {'project_id': project}} - cs.assert_called('POST', '/os-cloudpipe', body) - self.assertIsInstance(cp, str) + self.assert_called('POST', '/os-cloudpipe', body) + self.assertIsInstance(cp, six.string_types) def test_update(self): - cs.cloudpipe.update("192.168.1.1", 2345) + self.cs.cloudpipe.update("192.168.1.1", 2345) body = {'configure_project': {'vpn_ip': "192.168.1.1", 'vpn_port': 2345}} - cs.assert_called('PUT', '/os-cloudpipe/configure-project', body) + self.assert_called('PUT', '/os-cloudpipe/configure-project', body) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_fixed_ips.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_fixed_ips.py index 42e856c96c..a081a8ecc9 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_fixed_ips.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_fixed_ips.py @@ -13,17 +13,21 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import fixedips as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes - -cs = fakes.FakeClient() -class FixedIpsTest(utils.TestCase): +class FixedIpsTest(utils.FixturedTestCase): + + data_fixture_class = data.Fixture + + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] def test_get_fixed_ip(self): - info = cs.fixed_ips.get(fixed_ip='192.168.1.1') - cs.assert_called('GET', '/os-fixed-ips/192.168.1.1') + info = self.cs.fixed_ips.get(fixed_ip='192.168.1.1') + self.assert_called('GET', '/os-fixed-ips/192.168.1.1') self.assertEqual(info.cidr, '192.168.1.0/24') self.assertEqual(info.address, '192.168.1.1') self.assertEqual(info.hostname, 'foo') @@ -31,10 +35,10 @@ class FixedIpsTest(utils.TestCase): def test_reserve_fixed_ip(self): body = {"reserve": None} - res = cs.fixed_ips.reserve(fixed_ip='192.168.1.1') - cs.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) + self.cs.fixed_ips.reserve(fixed_ip='192.168.1.1') + self.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) def test_unreserve_fixed_ip(self): body = {"unreserve": None} - res = cs.fixed_ips.unreserve(fixed_ip='192.168.1.1') - cs.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) + self.cs.fixed_ips.unreserve(fixed_ip='192.168.1.1') + self.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_flavor_access.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_flavor_access.py index ec937d01c9..b85f838ee5 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_flavor_access.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_flavor_access.py @@ -56,3 +56,15 @@ class FlavorAccessTest(utils.TestCase): cs.assert_called('POST', '/flavors/2/action', body) [self.assertIsInstance(a, flavor_access.FlavorAccess) for a in r] + + def test_repr_flavor_access(self): + flavor = cs.flavors.get(2) + tenant = 'proj3' + r = cs.flavor_access.add_tenant_access(flavor, tenant) + + def get_expected(flavor_access): + return ("" % + (flavor_access.flavor_id, flavor_access.tenant_id)) + + for a in r: + self.assertEqual(get_expected(a), repr(a)) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_flavors.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_flavors.py index c18305401a..8028cf0eef 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_flavors.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_flavors.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +import mock + from novaclient import exceptions from novaclient.tests import utils from novaclient.tests.v1_1 import fakes @@ -206,7 +208,12 @@ class FlavorsTest(utils.TestCase): for key in invalid_keys: self.assertRaises(exceptions.CommandError, f.set_keys, {key: 'v1'}) - def test_unset_keys(self): + @mock.patch.object(flavors.FlavorManager, '_delete') + def test_unset_keys(self, mock_delete): f = self.cs.flavors.get(1) - f.unset_keys(['k1']) - self.cs.assert_called('DELETE', '/flavors/1/os-extra_specs/k1') + keys = ['k1', 'k2'] + f.unset_keys(keys) + mock_delete.assert_has_calls([ + mock.call("/flavors/1/os-extra_specs/k1"), + mock.call("/flavors/1/os-extra_specs/k2") + ]) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_floating_ip_dns.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_floating_ip_dns.py index ecaae40698..df310b2690 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_floating_ip_dns.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_floating_ip_dns.py @@ -11,20 +11,20 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import floatingips as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import floating_ip_dns -cs = fakes.FakeClient() - - -class FloatingIPDNSDomainTest(utils.TestCase): +class FloatingIPDNSDomainTest(utils.FixturedTestCase): testdomain = "testdomain" + client_fixture_class = client.V1 + data_fixture_class = data.DNSFixture def test_dns_domains(self): - domainlist = cs.dns_domains.domains() + domainlist = self.cs.dns_domains.domains() self.assertEqual(len(domainlist), 2) for entry in domainlist: @@ -34,30 +34,33 @@ class FloatingIPDNSDomainTest(utils.TestCase): self.assertEqual(domainlist[1].domain, 'example.com') def test_create_private_domain(self): - cs.dns_domains.create_private(self.testdomain, 'test_avzone') - cs.assert_called('PUT', '/os-floating-ip-dns/%s' % - self.testdomain) + self.cs.dns_domains.create_private(self.testdomain, 'test_avzone') + self.assert_called('PUT', '/os-floating-ip-dns/%s' % + self.testdomain) def test_create_public_domain(self): - cs.dns_domains.create_public(self.testdomain, 'test_project') - cs.assert_called('PUT', '/os-floating-ip-dns/%s' % - self.testdomain) + self.cs.dns_domains.create_public(self.testdomain, 'test_project') + self.assert_called('PUT', '/os-floating-ip-dns/%s' % + self.testdomain) def test_delete_domain(self): - cs.dns_domains.delete(self.testdomain) - cs.assert_called('DELETE', '/os-floating-ip-dns/%s' % - self.testdomain) + self.cs.dns_domains.delete(self.testdomain) + self.assert_called('DELETE', '/os-floating-ip-dns/%s' % + self.testdomain) -class FloatingIPDNSEntryTest(utils.TestCase): +class FloatingIPDNSEntryTest(utils.FixturedTestCase): testname = "testname" testip = "1.2.3.4" testdomain = "testdomain" testtype = "A" + client_fixture_class = client.V1 + data_fixture_class = data.DNSFixture def test_get_dns_entries_by_ip(self): - entries = cs.dns_entries.get_for_ip(self.testdomain, ip=self.testip) + entries = self.cs.dns_entries.get_for_ip(self.testdomain, + ip=self.testip) self.assertEqual(len(entries), 2) for entry in entries: @@ -68,21 +71,21 @@ class FloatingIPDNSEntryTest(utils.TestCase): self.assertEqual(entries[1].dns_entry['ip'], self.testip) def test_get_dns_entry_by_name(self): - entry = cs.dns_entries.get(self.testdomain, - self.testname) + entry = self.cs.dns_entries.get(self.testdomain, + self.testname) self.assertIsInstance(entry, floating_ip_dns.FloatingIPDNSEntry) self.assertEqual(entry.name, self.testname) def test_create_entry(self): - cs.dns_entries.create(self.testdomain, - self.testname, - self.testip, - self.testtype) + self.cs.dns_entries.create(self.testdomain, + self.testname, + self.testip, + self.testtype) - cs.assert_called('PUT', '/os-floating-ip-dns/%s/entries/%s' % - (self.testdomain, self.testname)) + self.assert_called('PUT', '/os-floating-ip-dns/%s/entries/%s' % + (self.testdomain, self.testname)) def test_delete_entry(self): - cs.dns_entries.delete(self.testdomain, self.testname) - cs.assert_called('DELETE', '/os-floating-ip-dns/%s/entries/%s' % - (self.testdomain, self.testname)) + self.cs.dns_entries.delete(self.testdomain, self.testname) + self.assert_called('DELETE', '/os-floating-ip-dns/%s/entries/%s' % + (self.testdomain, self.testname)) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_floating_ip_pools.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_floating_ip_pools.py index 51124cfc9c..efdfabf30e 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_floating_ip_pools.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_floating_ip_pools.py @@ -14,18 +14,19 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import floatingips as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import floating_ip_pools -cs = fakes.FakeClient() +class TestFloatingIPPools(utils.FixturedTestCase): - -class TestFloatingIPPools(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.PoolsFixture def test_list_floating_ips(self): - fl = cs.floating_ip_pools.list() - cs.assert_called('GET', '/os-floating-ip-pools') + fl = self.cs.floating_ip_pools.list() + self.assert_called('GET', '/os-floating-ip-pools') [self.assertIsInstance(f, floating_ip_pools.FloatingIPPool) for f in fl] diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_floating_ips.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_floating_ips.py index 0b2313f51f..d14f8a7105 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_floating_ips.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_floating_ips.py @@ -14,38 +14,46 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import floatingips as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import floating_ips -cs = fakes.FakeClient() +class FloatingIPsTest(utils.FixturedTestCase): - -class FloatingIPsTest(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.FloatingFixture def test_list_floating_ips(self): - fl = cs.floating_ips.list() - cs.assert_called('GET', '/os-floating-ips') - [self.assertIsInstance(f, floating_ips.FloatingIP) for f in fl] + fips = self.cs.floating_ips.list() + self.assert_called('GET', '/os-floating-ips') + for fip in fips: + self.assertIsInstance(fip, floating_ips.FloatingIP) + + def test_list_floating_ips_all_tenants(self): + fips = self.cs.floating_ips.list(all_tenants=True) + self.assert_called('GET', '/os-floating-ips?all_tenants=1') + for fip in fips: + self.assertIsInstance(fip, floating_ips.FloatingIP) def test_delete_floating_ip(self): - fl = cs.floating_ips.list()[0] + fl = self.cs.floating_ips.list()[0] fl.delete() - cs.assert_called('DELETE', '/os-floating-ips/1') - cs.floating_ips.delete(1) - cs.assert_called('DELETE', '/os-floating-ips/1') - cs.floating_ips.delete(fl) - cs.assert_called('DELETE', '/os-floating-ips/1') + self.assert_called('DELETE', '/os-floating-ips/1') + self.cs.floating_ips.delete(1) + self.assert_called('DELETE', '/os-floating-ips/1') + self.cs.floating_ips.delete(fl) + self.assert_called('DELETE', '/os-floating-ips/1') def test_create_floating_ip(self): - fl = cs.floating_ips.create() - cs.assert_called('POST', '/os-floating-ips') + fl = self.cs.floating_ips.create() + self.assert_called('POST', '/os-floating-ips') self.assertIsNone(fl.pool) self.assertIsInstance(fl, floating_ips.FloatingIP) def test_create_floating_ip_with_pool(self): - fl = cs.floating_ips.create('foo') - cs.assert_called('POST', '/os-floating-ips') + fl = self.cs.floating_ips.create('nova') + self.assert_called('POST', '/os-floating-ips') self.assertEqual(fl.pool, 'nova') self.assertIsInstance(fl, floating_ips.FloatingIP) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_floating_ips_bulk.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_floating_ips_bulk.py index 01fb83725a..dd4f823c7a 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_floating_ips_bulk.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_floating_ips_bulk.py @@ -13,42 +13,43 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import floatingips as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import floating_ips_bulk -cs = fakes.FakeClient() +class FloatingIPsBulkTest(utils.FixturedTestCase): - -class FloatingIPsBulkTest(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.BulkFixture def test_list_floating_ips_bulk(self): - fl = cs.floating_ips_bulk.list() - cs.assert_called('GET', '/os-floating-ips-bulk') + fl = self.cs.floating_ips_bulk.list() + self.assert_called('GET', '/os-floating-ips-bulk') [self.assertIsInstance(f, floating_ips_bulk.FloatingIP) for f in fl] def test_list_floating_ips_bulk_host_filter(self): - fl = cs.floating_ips_bulk.list('testHost') - cs.assert_called('GET', '/os-floating-ips-bulk/testHost') + fl = self.cs.floating_ips_bulk.list('testHost') + self.assert_called('GET', '/os-floating-ips-bulk/testHost') [self.assertIsInstance(f, floating_ips_bulk.FloatingIP) for f in fl] def test_create_floating_ips_bulk(self): - fl = cs.floating_ips_bulk.create('192.168.1.0/30') + fl = self.cs.floating_ips_bulk.create('192.168.1.0/30') body = {'floating_ips_bulk_create': {'ip_range': '192.168.1.0/30'}} - cs.assert_called('POST', '/os-floating-ips-bulk', body) + self.assert_called('POST', '/os-floating-ips-bulk', body) self.assertEqual(fl.ip_range, body['floating_ips_bulk_create']['ip_range']) def test_create_floating_ips_bulk_with_pool_and_host(self): - fl = cs.floating_ips_bulk.create('192.168.1.0/30', 'poolTest', - 'interfaceTest') + fl = self.cs.floating_ips_bulk.create('192.168.1.0/30', 'poolTest', + 'interfaceTest') body = {'floating_ips_bulk_create': {'ip_range': '192.168.1.0/30', 'pool': 'poolTest', 'interface': 'interfaceTest'}} - cs.assert_called('POST', '/os-floating-ips-bulk', body) + self.assert_called('POST', '/os-floating-ips-bulk', body) self.assertEqual(fl.ip_range, body['floating_ips_bulk_create']['ip_range']) self.assertEqual(fl.pool, @@ -57,7 +58,7 @@ class FloatingIPsBulkTest(utils.TestCase): body['floating_ips_bulk_create']['interface']) def test_delete_floating_ips_bulk(self): - fl = cs.floating_ips_bulk.delete('192.168.1.0/30') + fl = self.cs.floating_ips_bulk.delete('192.168.1.0/30') body = {'ip_range': '192.168.1.0/30'} - cs.assert_called('PUT', '/os-floating-ips-bulk/delete', body) + self.assert_called('PUT', '/os-floating-ips-bulk/delete', body) self.assertEqual(fl.floating_ips_bulk_delete, body['ip_range']) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_fping.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_fping.py index aaeb9b389e..87f9af29e2 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_fping.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_fping.py @@ -13,49 +13,50 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import fping as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import fping -cs = fakes.FakeClient() +class FpingTest(utils.FixturedTestCase): - -class FpingTest(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.Fixture def test_fping_repr(self): - r = cs.fping.get(1) + r = self.cs.fping.get(1) self.assertEqual(repr(r), "") def test_list_fpings(self): - fl = cs.fping.list() - cs.assert_called('GET', '/os-fping') + fl = self.cs.fping.list() + self.assert_called('GET', '/os-fping') for f in fl: self.assertIsInstance(f, fping.Fping) self.assertEqual(f.project_id, "fake-project") self.assertEqual(f.alive, True) def test_list_fpings_all_tenants(self): - fl = cs.fping.list(all_tenants=True) + fl = self.cs.fping.list(all_tenants=True) for f in fl: self.assertIsInstance(f, fping.Fping) - cs.assert_called('GET', '/os-fping?all_tenants=1') + self.assert_called('GET', '/os-fping?all_tenants=1') def test_list_fpings_exclude(self): - fl = cs.fping.list(exclude=['1']) + fl = self.cs.fping.list(exclude=['1']) for f in fl: self.assertIsInstance(f, fping.Fping) - cs.assert_called('GET', '/os-fping?exclude=1') + self.assert_called('GET', '/os-fping?exclude=1') def test_list_fpings_include(self): - fl = cs.fping.list(include=['1']) + fl = self.cs.fping.list(include=['1']) for f in fl: self.assertIsInstance(f, fping.Fping) - cs.assert_called('GET', '/os-fping?include=1') + self.assert_called('GET', '/os-fping?include=1') def test_get_fping(self): - f = cs.fping.get(1) - cs.assert_called('GET', '/os-fping/1') + f = self.cs.fping.get(1) + self.assert_called('GET', '/os-fping/1') self.assertIsInstance(f, fping.Fping) self.assertEqual(f.project_id, "fake-project") self.assertEqual(f.alive, True) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_hosts.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_hosts.py index 2c243662d9..d8291c6aed 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_hosts.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_hosts.py @@ -11,69 +11,70 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import hosts as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import hosts -cs = fakes.FakeClient() +class HostsTest(utils.FixturedTestCase): - -class HostsTest(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.V1 def test_describe_resource(self): - hs = cs.hosts.get('host') - cs.assert_called('GET', '/os-hosts/host') + hs = self.cs.hosts.get('host') + self.assert_called('GET', '/os-hosts/host') [self.assertIsInstance(h, hosts.Host) for h in hs] def test_list_host(self): - hs = cs.hosts.list() - cs.assert_called('GET', '/os-hosts') + hs = self.cs.hosts.list() + self.assert_called('GET', '/os-hosts') [self.assertIsInstance(h, hosts.Host) for h in hs] [self.assertEqual(h.zone, 'nova1') for h in hs] def test_list_host_with_zone(self): - hs = cs.hosts.list('nova') - cs.assert_called('GET', '/os-hosts?zone=nova') + hs = self.cs.hosts.list('nova') + self.assert_called('GET', '/os-hosts?zone=nova') [self.assertIsInstance(h, hosts.Host) for h in hs] [self.assertEqual(h.zone, 'nova') for h in hs] def test_update_enable(self): - host = cs.hosts.get('sample_host')[0] + host = self.cs.hosts.get('sample_host')[0] values = {"status": "enabled"} result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', values) + self.assert_called('PUT', '/os-hosts/sample_host', values) self.assertIsInstance(result, hosts.Host) def test_update_maintenance(self): - host = cs.hosts.get('sample_host')[0] + host = self.cs.hosts.get('sample_host')[0] values = {"maintenance_mode": "enable"} result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', values) + self.assert_called('PUT', '/os-hosts/sample_host', values) self.assertIsInstance(result, hosts.Host) def test_update_both(self): - host = cs.hosts.get('sample_host')[0] + host = self.cs.hosts.get('sample_host')[0] values = {"status": "enabled", "maintenance_mode": "enable"} result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', values) + self.assert_called('PUT', '/os-hosts/sample_host', values) self.assertIsInstance(result, hosts.Host) def test_host_startup(self): - host = cs.hosts.get('sample_host')[0] - result = host.startup() - cs.assert_called( + host = self.cs.hosts.get('sample_host')[0] + host.startup() + self.assert_called( 'GET', '/os-hosts/sample_host/startup') def test_host_reboot(self): - host = cs.hosts.get('sample_host')[0] - result = host.reboot() - cs.assert_called( + host = self.cs.hosts.get('sample_host')[0] + host.reboot() + self.assert_called( 'GET', '/os-hosts/sample_host/reboot') def test_host_shutdown(self): - host = cs.hosts.get('sample_host')[0] - result = host.shutdown() - cs.assert_called( + host = self.cs.hosts.get('sample_host')[0] + host.shutdown() + self.assert_called( 'GET', '/os-hosts/sample_host/shutdown') diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_hypervisors.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_hypervisors.py index 3cf66c7832..8376d61f57 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_hypervisors.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_hypervisors.py @@ -13,17 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import hypervisors as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes -class HypervisorsTest(utils.TestCase): - def setUp(self): - super(HypervisorsTest, self).setUp() - self.cs = self._get_fake_client() +class HypervisorsTest(utils.FixturedTestCase): - def _get_fake_client(self): - return fakes.FakeClient() + client_fixture_class = client.V1 + data_fixture_class = data.V1 def compare_to_expected(self, expected, hyper): for key, value in expected.items(): @@ -36,7 +34,7 @@ class HypervisorsTest(utils.TestCase): ] result = self.cs.hypervisors.list(False) - self.cs.assert_called('GET', '/os-hypervisors') + self.assert_called('GET', '/os-hypervisors') for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) @@ -79,7 +77,7 @@ class HypervisorsTest(utils.TestCase): disk_available_least=100)] result = self.cs.hypervisors.list() - self.cs.assert_called('GET', '/os-hypervisors/detail') + self.assert_called('GET', '/os-hypervisors/detail') for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) @@ -91,7 +89,7 @@ class HypervisorsTest(utils.TestCase): ] result = self.cs.hypervisors.search('hyper') - self.cs.assert_called('GET', '/os-hypervisors/hyper/search') + self.assert_called('GET', '/os-hypervisors/hyper/search') for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) @@ -111,7 +109,7 @@ class HypervisorsTest(utils.TestCase): ] result = self.cs.hypervisors.search('hyper', True) - self.cs.assert_called('GET', '/os-hypervisors/hyper/servers') + self.assert_called('GET', '/os-hypervisors/hyper/servers') for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) @@ -137,7 +135,7 @@ class HypervisorsTest(utils.TestCase): disk_available_least=100) result = self.cs.hypervisors.get(1234) - self.cs.assert_called('GET', '/os-hypervisors/1234') + self.assert_called('GET', '/os-hypervisors/1234') self.compare_to_expected(expected, result) @@ -148,7 +146,7 @@ class HypervisorsTest(utils.TestCase): uptime="fake uptime") result = self.cs.hypervisors.uptime(1234) - self.cs.assert_called('GET', '/os-hypervisors/1234/uptime') + self.assert_called('GET', '/os-hypervisors/1234/uptime') self.compare_to_expected(expected, result) @@ -169,6 +167,6 @@ class HypervisorsTest(utils.TestCase): ) result = self.cs.hypervisors.statistics() - self.cs.assert_called('GET', '/os-hypervisors/statistics') + self.assert_called('GET', '/os-hypervisors/statistics') self.compare_to_expected(expected, result) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_images.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_images.py index e3968aa489..6ff8fe9c7f 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_images.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_images.py @@ -11,56 +11,56 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import images as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import images -cs = fakes.FakeClient() +class ImagesTest(utils.FixturedTestCase): - -class ImagesTest(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.V1 def test_list_images(self): - il = cs.images.list() - cs.assert_called('GET', '/images/detail') + il = self.cs.images.list() + self.assert_called('GET', '/images/detail') [self.assertIsInstance(i, images.Image) for i in il] def test_list_images_undetailed(self): - il = cs.images.list(detailed=False) - cs.assert_called('GET', '/images') + il = self.cs.images.list(detailed=False) + self.assert_called('GET', '/images') [self.assertIsInstance(i, images.Image) for i in il] def test_list_images_with_limit(self): - il = cs.images.list(limit=4) - cs.assert_called('GET', '/images/detail?limit=4') + self.cs.images.list(limit=4) + self.assert_called('GET', '/images/detail?limit=4') def test_get_image_details(self): - i = cs.images.get(1) - cs.assert_called('GET', '/images/1') + i = self.cs.images.get(1) + self.assert_called('GET', '/images/1') self.assertIsInstance(i, images.Image) self.assertEqual(i.id, 1) self.assertEqual(i.name, 'CentOS 5.2') def test_delete_image(self): - cs.images.delete(1) - cs.assert_called('DELETE', '/images/1') + self.cs.images.delete(1) + self.assert_called('DELETE', '/images/1') def test_delete_meta(self): - cs.images.delete_meta(1, {'test_key': 'test_value'}) - cs.assert_called('DELETE', '/images/1/metadata/test_key') + self.cs.images.delete_meta(1, {'test_key': 'test_value'}) + self.assert_called('DELETE', '/images/1/metadata/test_key') def test_set_meta(self): - cs.images.set_meta(1, {'test_key': 'test_value'}) - cs.assert_called('POST', '/images/1/metadata', + self.cs.images.set_meta(1, {'test_key': 'test_value'}) + self.assert_called('POST', '/images/1/metadata', {"metadata": {'test_key': 'test_value'}}) def test_find(self): - i = cs.images.find(name="CentOS 5.2") + i = self.cs.images.find(name="CentOS 5.2") self.assertEqual(i.id, 1) - cs.assert_called('GET', '/images', pos=-2) - cs.assert_called('GET', '/images/1', pos=-1) + self.assert_called('GET', '/images/1') - iml = cs.images.findall(status='SAVING') + iml = self.cs.images.findall(status='SAVING') self.assertEqual(len(iml), 1) self.assertEqual(iml[0].name, 'My Server Backup') diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_keypairs.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_keypairs.py index 90a6109248..7ebb23c2fb 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_keypairs.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_keypairs.py @@ -11,50 +11,54 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import keypairs as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import keypairs -class KeypairsTest(utils.TestCase): +class KeypairsTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.V1 + def setUp(self): super(KeypairsTest, self).setUp() - self.cs = self._get_fake_client() self.keypair_type = self._get_keypair_type() - self.keypair_prefix = keypairs.KeypairManager.keypair_prefix - - def _get_fake_client(self): - return fakes.FakeClient() + self.keypair_prefix = self._get_keypair_prefix() def _get_keypair_type(self): return keypairs.Keypair + def _get_keypair_prefix(self): + return keypairs.KeypairManager.keypair_prefix + def test_get_keypair(self): kp = self.cs.keypairs.get('test') - self.cs.assert_called('GET', '/%s/test' % self.keypair_prefix) + self.assert_called('GET', '/%s/test' % self.keypair_prefix) self.assertIsInstance(kp, keypairs.Keypair) self.assertEqual(kp.name, 'test') def test_list_keypairs(self): kps = self.cs.keypairs.list() - self.cs.assert_called('GET', '/%s' % self.keypair_prefix) + self.assert_called('GET', '/%s' % self.keypair_prefix) [self.assertIsInstance(kp, keypairs.Keypair) for kp in kps] def test_delete_keypair(self): kp = self.cs.keypairs.list()[0] kp.delete() - self.cs.assert_called('DELETE', '/%s/test' % self.keypair_prefix) + self.assert_called('DELETE', '/%s/test' % self.keypair_prefix) self.cs.keypairs.delete('test') - self.cs.assert_called('DELETE', '/%s/test' % self.keypair_prefix) + self.assert_called('DELETE', '/%s/test' % self.keypair_prefix) self.cs.keypairs.delete(kp) - self.cs.assert_called('DELETE', '/%s/test' % self.keypair_prefix) + self.assert_called('DELETE', '/%s/test' % self.keypair_prefix) def test_create_keypair(self): kp = self.cs.keypairs.create("foo") - self.cs.assert_called('POST', '/%s' % self.keypair_prefix) + self.assert_called('POST', '/%s' % self.keypair_prefix) self.assertIsInstance(kp, keypairs.Keypair) def test_import_keypair(self): kp = self.cs.keypairs.create("foo", "fake-public-key") - self.cs.assert_called('POST', '/%s' % self.keypair_prefix) + self.assert_called('POST', '/%s' % self.keypair_prefix) self.assertIsInstance(kp, keypairs.Keypair) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_limits.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_limits.py index 7561aa4ff9..212be45499 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_limits.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_limits.py @@ -11,28 +11,29 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import limits as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import limits -cs = fakes.FakeClient() +class LimitsTest(utils.FixturedTestCase): - -class LimitsTest(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.Fixture def test_get_limits(self): - obj = cs.limits.get() - cs.assert_called('GET', '/limits') + obj = self.cs.limits.get() + self.assert_called('GET', '/limits') self.assertIsInstance(obj, limits.Limits) def test_get_limits_for_a_tenant(self): - obj = cs.limits.get(tenant_id=1234) - cs.assert_called('GET', '/limits?tenant_id=1234') + obj = self.cs.limits.get(tenant_id=1234) + self.assert_called('GET', '/limits?tenant_id=1234') self.assertIsInstance(obj, limits.Limits) def test_absolute_limits(self): - obj = cs.limits.get() + obj = self.cs.limits.get() expected = ( limits.AbsoluteLimit("maxTotalRAMSize", 51200), @@ -49,7 +50,7 @@ class LimitsTest(utils.TestCase): self.assertTrue(limit in expected) def test_absolute_limits_reserved(self): - obj = cs.limits.get(reserved=True) + obj = self.cs.limits.get(reserved=True) expected = ( limits.AbsoluteLimit("maxTotalRAMSize", 51200), @@ -59,7 +60,7 @@ class LimitsTest(utils.TestCase): limits.AbsoluteLimit("maxPersonalitySize", 10240), ) - cs.assert_called('GET', '/limits?reserved=1') + self.assert_called('GET', '/limits?reserved=1') abs_limits = list(obj.absolute) self.assertEqual(len(abs_limits), len(expected)) @@ -67,7 +68,7 @@ class LimitsTest(utils.TestCase): self.assertTrue(limit in expected) def test_rate_limits(self): - obj = cs.limits.get() + obj = self.cs.limits.get() expected = ( limits.RateLimit('POST', '*', '.*', 10, 2, 'MINUTE', diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_networks.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_networks.py index 369772ea8f..8a63c443a9 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_networks.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_networks.py @@ -11,34 +11,35 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import networks as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import networks -cs = fakes.FakeClient() +class NetworksTest(utils.FixturedTestCase): - -class NetworksTest(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.Fixture def test_list_networks(self): - fl = cs.networks.list() - cs.assert_called('GET', '/os-networks') + fl = self.cs.networks.list() + self.assert_called('GET', '/os-networks') [self.assertIsInstance(f, networks.Network) for f in fl] def test_get_network(self): - f = cs.networks.get(1) - cs.assert_called('GET', '/os-networks/1') + f = self.cs.networks.get(1) + self.assert_called('GET', '/os-networks/1') self.assertIsInstance(f, networks.Network) def test_delete(self): - cs.networks.delete('networkdelete') - cs.assert_called('DELETE', '/os-networks/networkdelete') + self.cs.networks.delete('networkdelete') + self.assert_called('DELETE', '/os-networks/networkdelete') def test_create(self): - f = cs.networks.create(label='foo') - cs.assert_called('POST', '/os-networks', - {'network': {'label': 'foo'}}) + f = self.cs.networks.create(label='foo') + self.assert_called('POST', '/os-networks', + {'network': {'label': 'foo'}}) self.assertIsInstance(f, networks.Network) def test_create_allparams(self): @@ -56,43 +57,44 @@ class NetworksTest(utils.TestCase): 'multi_host': 'T', 'priority': '1', 'project_id': '1', + 'vlan': 5, 'vlan_start': 1, 'vpn_start': 1 } - f = cs.networks.create(**params) - cs.assert_called('POST', '/os-networks', {'network': params}) + f = self.cs.networks.create(**params) + self.assert_called('POST', '/os-networks', {'network': params}) self.assertIsInstance(f, networks.Network) def test_associate_project(self): - cs.networks.associate_project('networktest') - cs.assert_called('POST', '/os-networks/add', - {'id': 'networktest'}) + self.cs.networks.associate_project('networktest') + self.assert_called('POST', '/os-networks/add', + {'id': 'networktest'}) def test_associate_host(self): - cs.networks.associate_host('networktest', 'testHost') - cs.assert_called('POST', '/os-networks/networktest/action', - {'associate_host': 'testHost'}) + self.cs.networks.associate_host('networktest', 'testHost') + self.assert_called('POST', '/os-networks/networktest/action', + {'associate_host': 'testHost'}) def test_disassociate(self): - cs.networks.disassociate('networkdisassociate') - cs.assert_called('POST', - '/os-networks/networkdisassociate/action', - {'disassociate': None}) + self.cs.networks.disassociate('networkdisassociate') + self.assert_called('POST', + '/os-networks/networkdisassociate/action', + {'disassociate': None}) def test_disassociate_host_only(self): - cs.networks.disassociate('networkdisassociate', True, False) - cs.assert_called('POST', - '/os-networks/networkdisassociate/action', - {'disassociate_host': None}) + self.cs.networks.disassociate('networkdisassociate', True, False) + self.assert_called('POST', + '/os-networks/networkdisassociate/action', + {'disassociate_host': None}) def test_disassociate_project(self): - cs.networks.disassociate('networkdisassociate', False, True) - cs.assert_called('POST', - '/os-networks/networkdisassociate/action', - {'disassociate_project': None}) + self.cs.networks.disassociate('networkdisassociate', False, True) + self.assert_called('POST', + '/os-networks/networkdisassociate/action', + {'disassociate_project': None}) def test_add(self): - cs.networks.add('networkadd') - cs.assert_called('POST', '/os-networks/add', - {'id': 'networkadd'}) + self.cs.networks.add('networkadd') + self.assert_called('POST', '/os-networks/add', + {'id': 'networkadd'}) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_quotas.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_quotas.py index 06d7461d3e..8ff0d5967a 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_quotas.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_quotas.py @@ -13,39 +13,37 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import quotas as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes -class QuotaSetsTest(utils.TestCase): - def setUp(self): - super(QuotaSetsTest, self).setUp() - self.cs = self._get_fake_client() +class QuotaSetsTest(utils.FixturedTestCase): - def _get_fake_client(self): - return fakes.FakeClient() + client_fixture_class = client.V1 + data_fixture_class = data.V1 def test_tenant_quotas_get(self): tenant_id = 'test' self.cs.quotas.get(tenant_id) - self.cs.assert_called('GET', '/os-quota-sets/%s' % tenant_id) + self.assert_called('GET', '/os-quota-sets/%s' % tenant_id) def test_user_quotas_get(self): tenant_id = 'test' user_id = 'fake_user' self.cs.quotas.get(tenant_id, user_id=user_id) url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) - self.cs.assert_called('GET', url) + self.assert_called('GET', url) def test_tenant_quotas_defaults(self): tenant_id = '97f4c221bff44578b0300df4ef119353' self.cs.quotas.defaults(tenant_id) - self.cs.assert_called('GET', '/os-quota-sets/%s/defaults' % tenant_id) + self.assert_called('GET', '/os-quota-sets/%s/defaults' % tenant_id) def test_force_update_quota(self): q = self.cs.quotas.get('97f4c221bff44578b0300df4ef119353') q.update(cores=2, force=True) - self.cs.assert_called( + self.assert_called( 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', {'quota_set': {'force': True, 'cores': 2, @@ -54,11 +52,11 @@ class QuotaSetsTest(utils.TestCase): def test_quotas_delete(self): tenant_id = 'test' self.cs.quotas.delete(tenant_id) - self.cs.assert_called('DELETE', '/os-quota-sets/%s' % tenant_id) + self.assert_called('DELETE', '/os-quota-sets/%s' % tenant_id) def test_user_quotas_delete(self): tenant_id = 'test' user_id = 'fake_user' self.cs.quotas.delete(tenant_id, user_id=user_id) url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) - self.cs.assert_called('DELETE', url) + self.assert_called('DELETE', url) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_security_group_rules.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_security_group_rules.py index a7259619d0..bf69a86ed8 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_security_group_rules.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_security_group_rules.py @@ -12,21 +12,24 @@ # under the License. from novaclient import exceptions +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import security_group_rules as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import security_group_rules -cs = fakes.FakeClient() +class SecurityGroupRulesTest(utils.FixturedTestCase): + client_fixture_class = client.V1 + data_fixture_class = data.Fixture -class SecurityGroupRulesTest(utils.TestCase): def test_delete_security_group_rule(self): - cs.security_group_rules.delete(1) - cs.assert_called('DELETE', '/os-security-group-rules/1') + self.cs.security_group_rules.delete(1) + self.assert_called('DELETE', '/os-security-group-rules/1') def test_create_security_group_rule(self): - sg = cs.security_group_rules.create(1, "tcp", 1, 65535, "10.0.0.0/16") + sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, + "10.0.0.0/16") body = { "security_group_rule": { @@ -39,12 +42,12 @@ class SecurityGroupRulesTest(utils.TestCase): } } - cs.assert_called('POST', '/os-security-group-rules', body) + self.assert_called('POST', '/os-security-group-rules', body) self.assertTrue(isinstance(sg, security_group_rules.SecurityGroupRule)) def test_create_security_group_group_rule(self): - sg = cs.security_group_rules.create(1, "tcp", 1, 65535, "10.0.0.0/16", - 101) + sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, + "10.0.0.0/16", 101) body = { "security_group_rule": { @@ -57,25 +60,27 @@ class SecurityGroupRulesTest(utils.TestCase): } } - cs.assert_called('POST', '/os-security-group-rules', body) + self.assert_called('POST', '/os-security-group-rules', body) self.assertTrue(isinstance(sg, security_group_rules.SecurityGroupRule)) def test_invalid_parameters_create(self): self.assertRaises(exceptions.CommandError, - cs.security_group_rules.create, + self.cs.security_group_rules.create, 1, "invalid_ip_protocol", 1, 65535, "10.0.0.0/16", 101) self.assertRaises(exceptions.CommandError, - cs.security_group_rules.create, + self.cs.security_group_rules.create, 1, "tcp", "invalid_from_port", 65535, "10.0.0.0/16", 101) self.assertRaises(exceptions.CommandError, - cs.security_group_rules.create, + self.cs.security_group_rules.create, 1, "tcp", 1, "invalid_to_port", "10.0.0.0/16", 101) def test_security_group_rule_str(self): - sg = cs.security_group_rules.create(1, "tcp", 1, 65535, "10.0.0.0/16") + sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, + "10.0.0.0/16") self.assertEqual('1', str(sg)) def test_security_group_rule_del(self): - sg = cs.security_group_rules.create(1, "tcp", 1, 65535, "10.0.0.0/16") + sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, + "10.0.0.0/16") sg.delete() - cs.assert_called('DELETE', '/os-security-group-rules/1') + self.assert_called('DELETE', '/os-security-group-rules/1') diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_security_groups.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_security_groups.py index a46533d9a2..f5b771af1d 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_security_groups.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_security_groups.py @@ -11,18 +11,20 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import security_groups as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import security_groups -cs = fakes.FakeClient() +class SecurityGroupsTest(utils.FixturedTestCase): + client_fixture_class = client.V1 + data_fixture_class = data.Fixture -class SecurityGroupsTest(utils.TestCase): def _do_test_list_security_groups(self, search_opts, path): - sgs = cs.security_groups.list(search_opts=search_opts) - cs.assert_called('GET', path) + sgs = self.cs.security_groups.list(search_opts=search_opts) + self.assert_called('GET', path) for sg in sgs: self.assertIsInstance(sg, security_groups.SecurityGroup) @@ -39,34 +41,34 @@ class SecurityGroupsTest(utils.TestCase): {'all_tenants': 0}, '/os-security-groups') def test_get_security_groups(self): - sg = cs.security_groups.get(1) - cs.assert_called('GET', '/os-security-groups/1') + sg = self.cs.security_groups.get(1) + self.assert_called('GET', '/os-security-groups/1') self.assertIsInstance(sg, security_groups.SecurityGroup) self.assertEqual('1', str(sg)) def test_delete_security_group(self): - sg = cs.security_groups.list()[0] + sg = self.cs.security_groups.list()[0] sg.delete() - cs.assert_called('DELETE', '/os-security-groups/1') - cs.security_groups.delete(1) - cs.assert_called('DELETE', '/os-security-groups/1') - cs.security_groups.delete(sg) - cs.assert_called('DELETE', '/os-security-groups/1') + self.assert_called('DELETE', '/os-security-groups/1') + self.cs.security_groups.delete(1) + self.assert_called('DELETE', '/os-security-groups/1') + self.cs.security_groups.delete(sg) + self.assert_called('DELETE', '/os-security-groups/1') def test_create_security_group(self): - sg = cs.security_groups.create("foo", "foo barr") - cs.assert_called('POST', '/os-security-groups') + sg = self.cs.security_groups.create("foo", "foo barr") + self.assert_called('POST', '/os-security-groups') self.assertIsInstance(sg, security_groups.SecurityGroup) def test_update_security_group(self): - sg = cs.security_groups.list()[0] - secgroup = cs.security_groups.update(sg, "update", "update") - cs.assert_called('PUT', '/os-security-groups/1') + sg = self.cs.security_groups.list()[0] + secgroup = self.cs.security_groups.update(sg, "update", "update") + self.assert_called('PUT', '/os-security-groups/1') self.assertIsInstance(secgroup, security_groups.SecurityGroup) def test_refresh_security_group(self): - sg = cs.security_groups.get(1) - sg2 = cs.security_groups.get(1) + sg = self.cs.security_groups.get(1) + sg2 = self.cs.security_groups.get(1) self.assertEqual(sg.name, sg2.name) sg2.name = "should be test" self.assertNotEqual(sg.name, sg2.name) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_server_groups.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_server_groups.py new file mode 100644 index 0000000000..e341dc0c37 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_server_groups.py @@ -0,0 +1,53 @@ +# Copyright (c) 2014 VMware, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import server_groups as data +from novaclient.tests import utils +from novaclient.v1_1 import server_groups + + +class ServerGroupsTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.Fixture + + def test_list_server_groups(self): + result = self.cs.server_groups.list() + self.assert_called('GET', '/os-server-groups') + for server_group in result: + self.assertTrue(isinstance(server_group, + server_groups.ServerGroup)) + + def test_create_server_group(self): + kwargs = {'name': 'ig1', + 'policies': ['anti-affinity']} + server_group = self.cs.server_groups.create(**kwargs) + body = {'server_group': kwargs} + self.assert_called('POST', '/os-server-groups', body) + self.assertTrue(isinstance(server_group, + server_groups.ServerGroup)) + + def test_get_server_group(self): + id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' + server_group = self.cs.server_groups.get(id) + self.assert_called('GET', '/os-server-groups/%s' % id) + self.assertTrue(isinstance(server_group, + server_groups.ServerGroup)) + + def test_delete_server_group(self): + id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' + self.cs.server_groups.delete(id) + self.assert_called('DELETE', '/os-server-groups/%s' % id) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_servers.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_servers.py index a48204c196..728732474f 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_servers.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_servers.py @@ -104,6 +104,28 @@ class ServersTest(utils.TestCase): test_create_server_from_volume() + def test_create_server_boot_with_nics_ipv6(self): + old_boot = cs.servers._boot + nics = [{'net-id': '11111111-1111-1111-1111-111111111111', + 'v6-fixed-ip': '2001:db9:0:1::10'}] + + def wrapped_boot(url, key, *boot_args, **boot_kwargs): + self.assertEqual(boot_kwargs['nics'], nics) + return old_boot(url, key, *boot_args, **boot_kwargs) + + with mock.patch.object(cs.servers, '_boot', wrapped_boot): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + nics=nics + ) + cs.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + def test_create_server_userdata_file_object(self): s = cs.servers.create( name="My server", @@ -200,17 +222,17 @@ class ServersTest(utils.TestCase): cs.assert_called('DELETE', '/servers/1234') def test_delete_server_meta(self): - s = cs.servers.delete_meta(1234, ['test_key']) + cs.servers.delete_meta(1234, ['test_key']) cs.assert_called('DELETE', '/servers/1234/metadata/test_key') def test_set_server_meta(self): - s = cs.servers.set_meta(1234, {'test_key': 'test_value'}) - reval = cs.assert_called('POST', '/servers/1234/metadata', + cs.servers.set_meta(1234, {'test_key': 'test_value'}) + cs.assert_called('POST', '/servers/1234/metadata', {'metadata': {'test_key': 'test_value'}}) def test_set_server_meta_item(self): - s = cs.servers.set_meta_item(1234, 'test_key', 'test_value') - reval = cs.assert_called('PUT', '/servers/1234/metadata/test_key', + cs.servers.set_meta_item(1234, 'test_key', 'test_value') + cs.assert_called('PUT', '/servers/1234/metadata/test_key', {'meta': {'test_key': 'test_value'}}) def test_find(self): @@ -582,6 +604,32 @@ class ServersTest(utils.TestCase): s.interface_list() cs.assert_called('GET', '/servers/1234/os-interface') + def test_interface_list_result_string_representable(self): + """Test for bugs.launchpad.net/python-novaclient/+bug/1280453.""" + # According to https://github.com/openstack/nova/blob/master/ + # nova/api/openstack/compute/contrib/attach_interfaces.py#L33, + # the attach_interface extension get method will return a json + # object partly like this: + interface_list = [{ + 'net_id': 'd7745cf5-63f9-4883-b0ae-983f061e4f23', + 'port_id': 'f35079da-36d5-4513-8ec1-0298d703f70e', + 'mac_addr': 'fa:16:3e:4c:37:c8', + 'port_state': 'ACTIVE', + 'fixed_ips': [{ + 'subnet_id': 'f1ad93ad-2967-46ba-b403-e8cbbe65f7fa', + 'ip_address': '10.2.0.96' + }] + }] + # If server is not string representable, it will raise an exception, + # because attribute named 'name' cannot be found. + # Parameter 'loaded' must be True or it will try to get attribute + # 'id' then fails (lazy load detail), this is exactly same as + # novaclient.base.Manager._list() + s = servers.Server(servers.ServerManager, interface_list[0], + loaded=True) + # Trigger the __repr__ magic method + self.assertEqual('', '%r' % s) + def test_interface_attach(self): s = cs.servers.get(1234) s.interface_attach(None, None, None) diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_services.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_services.py index d66a111074..d5242e52b0 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_services.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_services.py @@ -78,6 +78,10 @@ class ServicesTest(utils.TestCase): self.assertIsInstance(service, self._get_service_type()) self.assertEqual(service.status, 'enabled') + def test_services_delete(self): + self.cs.services.delete('1') + self.cs.assert_called('DELETE', '/os-services/1') + def test_services_disable(self): service = self.cs.services.disable('host1', 'nova-cert') values = self._update_body("host1", "nova-cert") diff --git a/awx/lib/site-packages/novaclient/tests/v1_1/test_shell.py b/awx/lib/site-packages/novaclient/tests/v1_1/test_shell.py index 580d2ed558..da2fed520a 100644 --- a/awx/lib/site-packages/novaclient/tests/v1_1/test_shell.py +++ b/awx/lib/site-packages/novaclient/tests/v1_1/test_shell.py @@ -23,6 +23,7 @@ import os import fixtures import mock import six +from six.moves import builtins import novaclient.client from novaclient import exceptions @@ -128,20 +129,6 @@ class ShellTest(utils.TestCase): }}, ) - def test_boot_multiple(self): - self.run_command('boot --flavor 1 --image 1' - ' --num-instances 3 some-server') - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavorRef': '1', - 'name': 'some-server', - 'imageRef': '1', - 'min_count': 1, - 'max_count': 3, - }}, - ) - def test_boot_image_with(self): self.run_command("boot --flavor 1" " --image-with test_key=test_value some-server") @@ -172,8 +159,8 @@ class ShellTest(utils.TestCase): def test_boot_user_data(self): testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') - data = open(testfile).read() - expected_file_data = base64.b64encode(data.encode('utf-8')) + data = open(testfile).read().encode('utf-8') + expected_file_data = base64.b64encode(data).decode('utf-8') self.run_command( 'boot --flavor 1 --image 1 --user_data %s some-server' % testfile) self.assert_called_anytime( @@ -497,7 +484,33 @@ class ShellTest(utils.TestCase): }, ) - def tets_boot_nics_no_value(self): + def test_boot_nics_ipv6(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id=a=c,v6-fixed-ip=2001:db9:0:1::10 some-server') + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + 'networks': [ + {'uuid': 'a=c', 'fixed_ip': '2001:db9:0:1::10'}, + ], + }, + }, + ) + + def test_boot_nics_both_ipv4_and_ipv6(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id=a=c,v4-fixed-ip=10.0.0.1,' + 'v6-fixed-ip=2001:db9:0:1::10 some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nics_no_value(self): cmd = ('boot --image 1 --flavor 1 ' '--nic net-id some-server') self.assertRaises(exceptions.CommandError, self.run_command, cmd) @@ -512,10 +525,15 @@ class ShellTest(utils.TestCase): '--nic v4-fixed-ip=10.0.0.1 some-server') self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_nics_netid_and_portid(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic port-id=some=port,net-id=some=net some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_files(self): testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') data = open(testfile).read() - expected_file_data = base64.b64encode(data.encode('utf-8')) + expected = base64.b64encode(data.encode('utf-8')).decode('utf-8') cmd = ('boot some-server --flavor 1 --image 1' ' --file /tmp/foo=%s --file /tmp/bar=%s') @@ -530,8 +548,8 @@ class ShellTest(utils.TestCase): 'min_count': 1, 'max_count': 1, 'personality': [ - {'path': '/tmp/bar', 'contents': expected_file_data}, - {'path': '/tmp/foo', 'contents': expected_file_data}, + {'path': '/tmp/bar', 'contents': expected}, + {'path': '/tmp/foo', 'contents': expected}, ] }}, ) @@ -558,11 +576,69 @@ class ShellTest(utils.TestCase): }) def test_boot_invalid_num_instances(self): - cmd = 'boot --image 1 --flavor 1 --num-instances 1 server' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) cmd = 'boot --image 1 --flavor 1 --num-instances 0 server' self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_num_instances_and_count(self): + cmd = 'boot --image 1 --flavor 1 --num-instances 3 --min-count 3 serv' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + cmd = 'boot --image 1 --flavor 1 --num-instances 3 --max-count 3 serv' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_min_max_count(self): + self.run_command('boot --image 1 --flavor 1 --max-count 3 server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 3, + } + }) + self.run_command('boot --image 1 --flavor 1 --min-count 3 server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'server', + 'imageRef': '1', + 'min_count': 3, + 'max_count': 3, + } + }) + self.run_command('boot --image 1 --flavor 1 ' + '--min-count 3 --max-count 3 server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'server', + 'imageRef': '1', + 'min_count': 3, + 'max_count': 3, + } + }) + self.run_command('boot --image 1 --flavor 1 ' + '--min-count 3 --max-count 5 server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'server', + 'imageRef': '1', + 'min_count': 3, + 'max_count': 5, + } + }) + cmd = 'boot --image 1 --flavor 1 --min-count 3 --max-count 1 serv' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + @mock.patch('novaclient.v1_1.shell._poll_for_status') def test_boot_with_poll(self, poll_method): self.run_command('boot --flavor 1 --image 1 some-server --poll') @@ -581,6 +657,31 @@ class ShellTest(utils.TestCase): [mock.call(self.shell.cs.servers.get, 1234, 'building', ['active'])]) + def test_boot_with_poll_to_check_VM_state_error(self): + self.assertRaises(exceptions.InstanceInErrorState, self.run_command, + 'boot --flavor 1 --image 1 some-bad-server --poll') + + def test_boot_named_flavor(self): + self.run_command(["boot", "--image", "1", + "--flavor", "512 MB Server", + "--max-count", "3", "server"]) + self.assert_called('GET', '/images/1', pos=0) + self.assert_called('GET', '/flavors/512 MB Server', pos=1) + self.assert_called('GET', '/flavors?is_public=None', pos=2) + self.assert_called('GET', '/flavors?is_public=None', pos=3) + self.assert_called('GET', '/flavors/2', pos=4) + self.assert_called( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '2', + 'name': 'server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 3, + } + }, pos=5) + def test_flavor_list(self): self.run_command('flavor-list') self.assert_called_anytime('GET', '/flavors/detail') @@ -602,6 +703,22 @@ class ShellTest(utils.TestCase): self.run_command('flavor-show aa1') self.assert_called_anytime('GET', '/flavors/aa1') + def test_flavor_show_by_name(self): + self.run_command(['flavor-show', '128 MB Server']) + self.assert_called('GET', '/flavors/128 MB Server', pos=0) + self.assert_called('GET', '/flavors?is_public=None', pos=1) + self.assert_called('GET', '/flavors?is_public=None', pos=2) + self.assert_called('GET', '/flavors/aa1', pos=3) + self.assert_called('GET', '/flavors/aa1/os-extra_specs', pos=4) + + def test_flavor_show_by_name_priv(self): + self.run_command(['flavor-show', '512 MB Server']) + self.assert_called('GET', '/flavors/512 MB Server', pos=0) + self.assert_called('GET', '/flavors?is_public=None', pos=1) + self.assert_called('GET', '/flavors?is_public=None', pos=2) + self.assert_called('GET', '/flavors/2', pos=3) + self.assert_called('GET', '/flavors/2/os-extra_specs', pos=4) + def test_flavor_key_set(self): self.run_command('flavor-key 1 set k1=v1') self.assert_called('POST', '/flavors/1/os-extra_specs', @@ -735,43 +852,37 @@ class ShellTest(utils.TestCase): {'reboot': {'type': 'HARD'}}) def test_rebuild(self): - self.run_command('rebuild sample-server 1') - self.assert_called('GET', '/servers', pos=-8) - self.assert_called('GET', '/servers/1234', pos=-7) - self.assert_called('GET', '/images/1', pos=-6) + output = self.run_command('rebuild sample-server 1') + self.assert_called('GET', '/servers', pos=-6) + self.assert_called('GET', '/servers/1234', pos=-5) + self.assert_called('GET', '/images/1', pos=-4) self.assert_called('POST', '/servers/1234/action', - {'rebuild': {'imageRef': 1}}, pos=-5) + {'rebuild': {'imageRef': 1}}, pos=-3) self.assert_called('GET', '/flavors/1', pos=-2) self.assert_called('GET', '/images/2') + self.assertIn('adminPass', output) - self.run_command('rebuild sample-server 1 --rebuild-password asdf') - self.assert_called('GET', '/servers', pos=-8) - self.assert_called('GET', '/servers/1234', pos=-7) - self.assert_called('GET', '/images/1', pos=-6) + def test_rebuild_password(self): + output = self.run_command('rebuild sample-server 1' + ' --rebuild-password asdf') + self.assert_called('GET', '/servers', pos=-6) + self.assert_called('GET', '/servers/1234', pos=-5) + self.assert_called('GET', '/images/1', pos=-4) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': 1, 'adminPass': 'asdf'}}, - pos=-5) + pos=-3) self.assert_called('GET', '/flavors/1', pos=-2) self.assert_called('GET', '/images/2') + self.assertIn('adminPass', output) def test_rebuild_preserve_ephemeral(self): self.run_command('rebuild sample-server 1 --preserve-ephemeral') - self.assert_called('GET', '/servers', pos=-8) - self.assert_called('GET', '/servers/1234', pos=-7) - self.assert_called('GET', '/images/1', pos=-6) + self.assert_called('GET', '/servers', pos=-6) + self.assert_called('GET', '/servers/1234', pos=-5) + self.assert_called('GET', '/images/1', pos=-4) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': 1, - 'preserve_ephemeral': True}}, pos=-5) - self.assert_called('GET', '/flavors/1', pos=-2) - self.assert_called('GET', '/images/2') - - self.run_command('rebuild sample-server 1 --rebuild-password asdf') - self.assert_called('GET', '/servers', pos=-8) - self.assert_called('GET', '/servers/1234', pos=-7) - self.assert_called('GET', '/images/1', pos=-6) - self.assert_called('POST', '/servers/1234/action', - {'rebuild': {'imageRef': 1, 'adminPass': 'asdf'}}, - pos=-5) + 'preserve_ephemeral': True}}, pos=-3) self.assert_called('GET', '/flavors/1', pos=-2) self.assert_called('GET', '/images/2') @@ -1039,6 +1150,10 @@ class ShellTest(utils.TestCase): self.run_command('floating-ip-list') self.assert_called('GET', '/os-floating-ips') + def test_floating_ip_list_all_tenants(self): + self.run_command('floating-ip-list --all-tenants') + self.assert_called('GET', '/os-floating-ips?all_tenants=1') + def test_floating_ip_create(self): self.run_command('floating-ip-create') self.assert_called('GET', '/os-floating-ips/1') @@ -1251,6 +1366,20 @@ class ShellTest(utils.TestCase): self.assert_called('POST', '/servers/1234/action', {'os-resetState': {'state': 'active'}}) + def test_reset_state_multiple(self): + self.run_command('reset-state sample-server sample-server2') + self.assert_called('POST', '/servers/1234/action', + {'os-resetState': {'state': 'error'}}, pos=-4) + self.assert_called('POST', '/servers/5678/action', + {'os-resetState': {'state': 'error'}}, pos=-1) + + def test_reset_state_active_multiple(self): + self.run_command('reset-state --active sample-server sample-server2') + self.assert_called('POST', '/servers/1234/action', + {'os-resetState': {'state': 'active'}}, pos=-4) + self.assert_called('POST', '/servers/5678/action', + {'os-resetState': {'state': 'active'}}, pos=-1) + def test_reset_network(self): self.run_command('reset-network sample-server') self.assert_called('POST', '/servers/1234/action', @@ -1288,6 +1417,10 @@ class ShellTest(utils.TestCase): 'disabled_reason': 'no_reason'} self.assert_called('PUT', '/os-services/disable-log-reason', body) + def test_services_delete(self): + self.run_command('service-delete 1') + self.assert_called('DELETE', '/os-services/1') + def test_fixed_ips_get(self): self.run_command('fixed-ip-get 192.168.1.1') self.assert_called('GET', '/os-fixed-ips/192.168.1.1') @@ -1518,11 +1651,22 @@ class ShellTest(utils.TestCase): self.assert_called('GET', '/os-quota-class-sets/test') def test_quota_class_update(self): - self.run_command('quota-class-update 97f4c221bff44578b0300df4ef119353' - ' --instances=5') - self.assert_called('PUT', - '/os-quota-class-sets/97f4c221bff44578b0300' - 'df4ef119353') + # The list of args we can update. + args = ( + '--instances', '--cores', '--ram', '--floating-ips', '--fixed-ips', + '--metadata-items', '--injected-files', + '--injected-file-content-bytes', '--injected-file-path-bytes', + '--key-pairs', '--security-groups', '--security-group-rules' + ) + for arg in args: + self.run_command('quota-class-update ' + '97f4c221bff44578b0300df4ef119353 ' + '%s=5' % arg) + request_param = arg[2:].replace('-', '_') + body = {'quota_class_set': {request_param: 5}} + self.assert_called( + 'PUT', '/os-quota-class-sets/97f4c221bff44578b0300df4ef119353', + body) def test_network_list(self): self.run_command('network-list') @@ -1613,7 +1757,14 @@ class ShellTest(utils.TestCase): self.run_command('network-create --fixed-range-v4 192.168.0.0/24' ' --vlan=200 new_network') body = {'network': {'cidr': '192.168.0.0/24', 'label': 'new_network', - 'vlan_start': '200'}} + 'vlan': '200'}} + self.assert_called('POST', '/os-networks', body) + + def test_network_create_vlan_start(self): + self.run_command('network-create --fixed-range-v4 192.168.0.0/24' + ' --vlan-start=100 new_network') + body = {'network': {'cidr': '192.168.0.0/24', 'label': 'new_network', + 'vlan_start': '100'}} self.assert_called('POST', '/os-networks', body) def test_add_fixed_ip(self): @@ -1918,6 +2069,45 @@ class ShellTest(utils.TestCase): mock_system.assert_called_with("ssh -6 -p22 " "root@2607:f0d0:1002::4 -1") + def test_keypair_add(self): + self.run_command('keypair-add test') + self.assert_called('POST', '/os-keypairs', + {'keypair': + {'name': 'test'}}) + + @mock.patch.object(builtins, 'open', + mock.mock_open(read_data='FAKE_PUBLIC_KEY')) + def test_keypair_import(self): + self.run_command('keypair-add --pub-key test.pub test') + self.assert_called('POST', '/os-keypairs', + {'keypair': + {'public_key': 'FAKE_PUBLIC_KEY', + 'name': 'test'}}) + + def test_keypair_list(self): + self.run_command('keypair-list') + self.assert_called('GET', '/os-keypairs') + + def test_keypair_show(self): + self.run_command('keypair-show test') + self.assert_called('GET', '/os-keypairs/test') + + def test_keypair_delete(self): + self.run_command('keypair-delete test') + self.assert_called('DELETE', '/os-keypairs/test') + + def test_create_server_group(self): + self.run_command('server-group-create wjsg affinity') + self.assert_called('POST', '/os-server-groups', + {'server_group': + {'name': 'wjsg', + 'policies': ['affinity']}}) + + def test_delete_multi_server_groups(self): + self.run_command('server-group-delete 12345 56789') + self.assert_called('DELETE', '/os-server-groups/56789') + self.assert_called('DELETE', '/os-server-groups/12345', pos=-2) + class GetSecgroupTest(utils.TestCase): def test_with_integer(self): @@ -1949,3 +2139,15 @@ class GetSecgroupTest(utils.TestCase): novaclient.v1_1.shell._get_secgroup, cs, 'abc') + + def test_with_non_unique_name(self): + group_one = mock.MagicMock() + group_one.name = 'group_one' + cs = mock.Mock(**{ + 'security_groups.get.return_value': 'sec_group', + 'security_groups.list.return_value': [group_one, group_one], + }) + self.assertRaises(exceptions.NoUniqueMatch, + novaclient.v1_1.shell._get_secgroup, + cs, + 'group_one') diff --git a/awx/lib/site-packages/novaclient/tests/v3/fakes.py b/awx/lib/site-packages/novaclient/tests/v3/fakes.py index 1013910fcf..8bb9ffbd1a 100644 --- a/awx/lib/site-packages/novaclient/tests/v3/fakes.py +++ b/awx/lib/site-packages/novaclient/tests/v3/fakes.py @@ -120,6 +120,10 @@ class FakeHTTPClient(fakes_v1_1.FakeHTTPClient): # get_flavors_2_flavor_access = ( fakes_v1_1.FakeHTTPClient.get_flavors_2_os_flavor_access) + get_flavors_2_flavor_extra_specs = ( + fakes_v1_1.FakeHTTPClient.get_flavors_2_os_extra_specs) + get_flavors_aa1_flavor_extra_specs = ( + fakes_v1_1.FakeHTTPClient.get_flavors_aa1_os_extra_specs) # # Images @@ -170,7 +174,10 @@ class FakeHTTPClient(fakes_v1_1.FakeHTTPClient): required=['name', 'image_ref', 'flavor_ref'], optional=['metadata', 'personality', 'os-scheduler-hints:scheduler_hints']) - return (202, {}, self.get_servers_1234()[2]) + if body['server']['name'] == 'some-bad-server': + return (202, {}, self.get_servers_1235()[2]) + else: + return (202, {}, self.get_servers_1234()[2]) # # Server Actions @@ -204,9 +211,10 @@ class FakeHTTPClient(fakes_v1_1.FakeHTTPClient): 'create_image': ['name', 'metadata'], 'migrate_live': ['host', 'block_migration', 'disk_over_commit'], 'create_backup': ['name', 'backup_type', 'rotation'], - 'attach': ['volume_id', 'device'], 'detach': ['volume_id'], 'swap_volume_attachment': ['old_volume_id', 'new_volume_id']} + body_params_check_superset = { + 'attach': ['volume_id', 'device']} assert len(body.keys()) == 1 action = list(body)[0] @@ -224,6 +232,9 @@ class FakeHTTPClient(fakes_v1_1.FakeHTTPClient): if action in body_params_check_exact: assert set(body[action]) == set(body_params_check_exact[action]) + if action in body_params_check_superset: + assert set(body[action]) >= set(body_params_check_superset[action]) + if action == 'reboot': assert body[action]['type'] in ['HARD', 'SOFT'] elif action == 'confirm_resize': @@ -234,7 +245,8 @@ class FakeHTTPClient(fakes_v1_1.FakeHTTPClient): if action not in set.union(set(body_is_none_list), set(body_params_check_exact.keys()), - set(body_param_check_exists.keys())): + set(body_param_check_exists.keys()), + set(body_params_check_superset.keys())): raise AssertionError("Unexpected server action: %s" % action) return (resp, _headers, _body) @@ -329,8 +341,8 @@ class FakeHTTPClient(fakes_v1_1.FakeHTTPClient): {'id': 1234, 'hypervisor_hostname': 'hyper1', 'servers': [ - {'name': 'inst1', 'uuid': 'uuid1'}, - {'name': 'inst2', 'uuid': 'uuid2'} + {'name': 'inst1', 'id': 'uuid1'}, + {'name': 'inst2', 'id': 'uuid2'} ]}, }) @@ -341,3 +353,31 @@ class FakeHTTPClient(fakes_v1_1.FakeHTTPClient): get_keypairs = fakes_v1_1.FakeHTTPClient.get_os_keypairs delete_keypairs_test = fakes_v1_1.FakeHTTPClient.delete_os_keypairs_test post_keypairs = fakes_v1_1.FakeHTTPClient.post_os_keypairs + + # + # List all extensions + # + def get_extensions(self, **kw): + exts = [ + { + "alias": "os-multinic", + "description": "Multiple network support", + "name": "Multinic", + "version": 1, + }, + { + "alias": "os-extended-server-attributes", + "description": "Extended Server Attributes support.", + "name": "ExtendedServerAttributes", + "version": 1, + }, + { + "alias": "os-extended-status", + "description": "Extended Status support", + "name": "ExtendedStatus", + "version": 1, + }, + ] + return (200, {}, { + "extensions": exts, + }) diff --git a/awx/lib/site-packages/novaclient/tests/v3/test_agents.py b/awx/lib/site-packages/novaclient/tests/v3/test_agents.py index 06534c3a5a..bdd101bdc8 100644 --- a/awx/lib/site-packages/novaclient/tests/v3/test_agents.py +++ b/awx/lib/site-packages/novaclient/tests/v3/test_agents.py @@ -13,20 +13,17 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client from novaclient.tests.v1_1 import test_agents -from novaclient.tests.v3 import fakes -from novaclient.v3 import agents class AgentsTest(test_agents.AgentsTest): + + scenarios = [('original', {'client_fixture_class': client.V3}), + ('session', {'client_fixture_class': client.SessionV3})] + def _build_example_update_body(self): return {"agent": { "url": "/yyy/yyyy/yyyy", "version": "8.0", "md5hash": "add6bb58e139be103324d04d82d8f546"}} - - def _get_fake_client(self): - return fakes.FakeClient() - - def _get_agent_type(self): - return agents.Agent diff --git a/awx/lib/site-packages/novaclient/tests/v3/test_aggregates.py b/awx/lib/site-packages/novaclient/tests/v3/test_aggregates.py index f4ce8218a6..17e27d1621 100644 --- a/awx/lib/site-packages/novaclient/tests/v3/test_aggregates.py +++ b/awx/lib/site-packages/novaclient/tests/v3/test_aggregates.py @@ -12,19 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client from novaclient.tests.v1_1 import test_aggregates -from novaclient.tests.v3 import fakes -from novaclient.v3 import aggregates class AggregatesTest(test_aggregates.AggregatesTest): - def setUp(self): - super(AggregatesTest, self).setUp() - self.cs = self._get_fake_client() - self.aggregate_type = self._get_aggregate_type() - def _get_fake_client(self): - return fakes.FakeClient() - - def _get_aggregate_type(self): - return aggregates.Aggregate + scenarios = [('original', {'client_fixture_class': client.V3}), + ('session', {'client_fixture_class': client.SessionV3})] diff --git a/awx/lib/site-packages/novaclient/tests/v3/test_availability_zone.py b/awx/lib/site-packages/novaclient/tests/v3/test_availability_zone.py index 004ba95fa2..67a4415bf7 100644 --- a/awx/lib/site-packages/novaclient/tests/v3/test_availability_zone.py +++ b/awx/lib/site-packages/novaclient/tests/v3/test_availability_zone.py @@ -14,25 +14,23 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import availability_zones as data +from novaclient.tests.fixture_data import client from novaclient.tests.v1_1 import test_availability_zone -from novaclient.tests.v3 import fakes from novaclient.v3 import availability_zones class AvailabilityZoneTest(test_availability_zone.AvailabilityZoneTest): from novaclient.v3 import shell # noqa - def setUp(self): - super(AvailabilityZoneTest, self).setUp() - self.cs = self._get_fake_client() - self.availability_zone_type = self._get_availability_zone_type() + data_fixture_class = data.V3 + + scenarios = [('original', {'client_fixture_class': client.V3}), + ('session', {'client_fixture_class': client.SessionV3})] def _assertZone(self, zone, name, status): self.assertEqual(zone.zone_name, name) self.assertEqual(zone.zone_state, status) - def _get_fake_client(self): - return fakes.FakeClient() - def _get_availability_zone_type(self): return availability_zones.AvailabilityZone diff --git a/awx/lib/site-packages/novaclient/tests/v3/test_certs.py b/awx/lib/site-packages/novaclient/tests/v3/test_certs.py index 1e293adc02..30597fc10b 100644 --- a/awx/lib/site-packages/novaclient/tests/v3/test_certs.py +++ b/awx/lib/site-packages/novaclient/tests/v3/test_certs.py @@ -11,19 +11,11 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.v1_1 import fakes +from novaclient.tests.fixture_data import client from novaclient.tests.v1_1 import test_certs -from novaclient.v3 import certs class CertsTest(test_certs.CertsTest): - def setUp(self): - super(CertsTest, self).setUp() - self.cs = self._get_fake_client() - self.cert_type = self._get_cert_type() - def _get_fake_client(self): - return fakes.FakeClient() - - def _get_cert_type(self): - return certs.Certificate + scenarios = [('original', {'client_fixture_class': client.V3}), + ('session', {'client_fixture_class': client.SessionV3})] diff --git a/awx/lib/site-packages/novaclient/tests/v3/test_flavors.py b/awx/lib/site-packages/novaclient/tests/v3/test_flavors.py index ea76988a0a..cf3e865136 100644 --- a/awx/lib/site-packages/novaclient/tests/v3/test_flavors.py +++ b/awx/lib/site-packages/novaclient/tests/v3/test_flavors.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +import mock + from novaclient.tests.v1_1 import test_flavors from novaclient.tests.v3 import fakes from novaclient.v3 import flavors @@ -57,10 +59,15 @@ class FlavorsTest(test_flavors.FlavorsTest): self.cs.assert_called('POST', '/flavors/4/flavor-extra-specs', {"extra_specs": {key: 'v4'}}) - def test_unset_keys(self): + @mock.patch.object(flavors.FlavorManager, '_delete') + def test_unset_keys(self, mock_delete): f = self.cs.flavors.get(1) - f.unset_keys(['k1']) - self.cs.assert_called('DELETE', '/flavors/1/flavor-extra-specs/k1') + keys = ['k1', 'k2'] + f.unset_keys(keys) + mock_delete.assert_has_calls([ + mock.call("/flavors/1/flavor-extra-specs/k1"), + mock.call("/flavors/1/flavor-extra-specs/k2") + ]) def test_get_flavor_details_diablo(self): # Don't need for V3 API to work against diablo diff --git a/awx/lib/site-packages/novaclient/tests/v3/test_hosts.py b/awx/lib/site-packages/novaclient/tests/v3/test_hosts.py index 6ce5b56e9e..cbd47822b3 100644 --- a/awx/lib/site-packages/novaclient/tests/v3/test_hosts.py +++ b/awx/lib/site-packages/novaclient/tests/v3/test_hosts.py @@ -12,72 +12,73 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import hosts as data from novaclient.tests import utils -from novaclient.tests.v3 import fakes from novaclient.v3 import hosts -cs = fakes.FakeClient() +class HostsTest(utils.FixturedTestCase): - -class HostsTest(utils.TestCase): + client_fixture_class = client.V3 + data_fixture_class = data.V3 def test_describe_resource(self): - hs = cs.hosts.get('host') - cs.assert_called('GET', '/os-hosts/host') + hs = self.cs.hosts.get('host') + self.assert_called('GET', '/os-hosts/host') for h in hs: self.assertIsInstance(h, hosts.Host) def test_list_host(self): - hs = cs.hosts.list() - cs.assert_called('GET', '/os-hosts') + hs = self.cs.hosts.list() + self.assert_called('GET', '/os-hosts') for h in hs: self.assertIsInstance(h, hosts.Host) self.assertEqual(h.zone, 'nova1') def test_list_host_with_zone(self): - hs = cs.hosts.list('nova') - cs.assert_called('GET', '/os-hosts?zone=nova') + hs = self.cs.hosts.list('nova') + self.assert_called('GET', '/os-hosts?zone=nova') for h in hs: self.assertIsInstance(h, hosts.Host) self.assertEqual(h.zone, 'nova') def test_update_enable(self): - host = cs.hosts.get('sample_host')[0] + host = self.cs.hosts.get('sample_host')[0] values = {"status": "enabled"} result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) + self.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) self.assertIsInstance(result, hosts.Host) def test_update_maintenance(self): - host = cs.hosts.get('sample_host')[0] + host = self.cs.hosts.get('sample_host')[0] values = {"maintenance_mode": "enable"} result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) + self.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) self.assertIsInstance(result, hosts.Host) def test_update_both(self): - host = cs.hosts.get('sample_host')[0] + host = self.cs.hosts.get('sample_host')[0] values = {"status": "enabled", "maintenance_mode": "enable"} result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) + self.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) self.assertIsInstance(result, hosts.Host) def test_host_startup(self): - host = cs.hosts.get('sample_host')[0] - result = host.startup() - cs.assert_called( + host = self.cs.hosts.get('sample_host')[0] + host.startup() + self.assert_called( 'GET', '/os-hosts/sample_host/startup') def test_host_reboot(self): - host = cs.hosts.get('sample_host')[0] - result = host.reboot() - cs.assert_called( + host = self.cs.hosts.get('sample_host')[0] + host.reboot() + self.assert_called( 'GET', '/os-hosts/sample_host/reboot') def test_host_shutdown(self): - host = cs.hosts.get('sample_host')[0] - result = host.shutdown() - cs.assert_called( + host = self.cs.hosts.get('sample_host')[0] + host.shutdown() + self.assert_called( 'GET', '/os-hosts/sample_host/shutdown') diff --git a/awx/lib/site-packages/novaclient/tests/v3/test_hypervisors.py b/awx/lib/site-packages/novaclient/tests/v3/test_hypervisors.py index d2cbbb9acb..ee7c83fcba 100644 --- a/awx/lib/site-packages/novaclient/tests/v3/test_hypervisors.py +++ b/awx/lib/site-packages/novaclient/tests/v3/test_hypervisors.py @@ -13,17 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import hypervisors as data from novaclient.tests.v1_1 import test_hypervisors -from novaclient.tests.v3 import fakes class HypervisorsTest(test_hypervisors.HypervisorsTest): - def setUp(self): - super(HypervisorsTest, self).setUp() - self.cs = self._get_fake_client() - def _get_fake_client(self): - return fakes.FakeClient() + client_fixture_class = client.V3 + data_fixture_class = data.V3 def test_hypervisor_search(self): expected = [ @@ -32,7 +30,7 @@ class HypervisorsTest(test_hypervisors.HypervisorsTest): ] result = self.cs.hypervisors.search('hyper') - self.cs.assert_called('GET', '/os-hypervisors/search?query=hyper') + self.assert_called('GET', '/os-hypervisors/search?query=hyper') for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) @@ -41,10 +39,10 @@ class HypervisorsTest(test_hypervisors.HypervisorsTest): expected = dict(id=1234, hypervisor_hostname='hyper1', servers=[ - dict(name='inst1', uuid='uuid1'), - dict(name='inst2', uuid='uuid2')]) + dict(name='inst1', id='uuid1'), + dict(name='inst2', id='uuid2')]) result = self.cs.hypervisors.servers('1234') - self.cs.assert_called('GET', '/os-hypervisors/1234/servers') + self.assert_called('GET', '/os-hypervisors/1234/servers') self.compare_to_expected(expected, result) diff --git a/awx/lib/site-packages/novaclient/tests/v3/test_images.py b/awx/lib/site-packages/novaclient/tests/v3/test_images.py index c1e756514b..8b4694f005 100644 --- a/awx/lib/site-packages/novaclient/tests/v3/test_images.py +++ b/awx/lib/site-packages/novaclient/tests/v3/test_images.py @@ -13,45 +13,45 @@ # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import images as data from novaclient.tests import utils -from novaclient.tests.v3 import fakes from novaclient.v3 import images -cs = fakes.FakeClient() +class ImagesTest(utils.FixturedTestCase): - -class ImagesTest(utils.TestCase): + client_fixture_class = client.V3 + data_fixture_class = data.V3 def test_list_images(self): - il = cs.images.list() - cs.assert_called('GET', '/v1/images/detail') + il = self.cs.images.list() + self.assert_called('GET', '/v1/images/detail') for i in il: self.assertIsInstance(i, images.Image) def test_list_images_undetailed(self): - il = cs.images.list(detailed=False) - cs.assert_called('GET', '/v1/images') + il = self.cs.images.list(detailed=False) + self.assert_called('GET', '/v1/images') for i in il: self.assertIsInstance(i, images.Image) def test_list_images_with_limit(self): - il = cs.images.list(limit=4) - cs.assert_called('GET', '/v1/images/detail?limit=4') + self.cs.images.list(limit=4) + self.assert_called('GET', '/v1/images/detail?limit=4') def test_get_image_details(self): - i = cs.images.get(1) - cs.assert_called('HEAD', '/v1/images/1') + i = self.cs.images.get(1) + self.assert_called('HEAD', '/v1/images/1') self.assertIsInstance(i, images.Image) self.assertEqual(i.id, '1') self.assertEqual(i.name, 'CentOS 5.2') def test_find(self): - i = cs.images.find(name="CentOS 5.2") + i = self.cs.images.find(name="CentOS 5.2") self.assertEqual(i.id, '1') - cs.assert_called('GET', '/v1/images', pos=-2) - cs.assert_called('HEAD', '/v1/images/1', pos=-1) + self.assert_called('HEAD', '/v1/images/1') - iml = cs.images.findall(status='SAVING') + iml = self.cs.images.findall(status='SAVING') self.assertEqual(len(iml), 1) self.assertEqual(iml[0].name, 'My Server Backup') diff --git a/awx/lib/site-packages/novaclient/tests/v3/test_keypairs.py b/awx/lib/site-packages/novaclient/tests/v3/test_keypairs.py index fd1e4bac4d..157042fce0 100644 --- a/awx/lib/site-packages/novaclient/tests/v3/test_keypairs.py +++ b/awx/lib/site-packages/novaclient/tests/v3/test_keypairs.py @@ -12,20 +12,19 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import keypairs as data from novaclient.tests.v1_1 import test_keypairs -from novaclient.tests.v3 import fakes from novaclient.v3 import keypairs class KeypairsTest(test_keypairs.KeypairsTest): - def setUp(self): - super(KeypairsTest, self).setUp() - self.cs = self._get_fake_client() - self.keypair_type = self._get_keypair_type() - self.keypair_prefix = keypairs.KeypairManager.keypair_prefix - def _get_fake_client(self): - return fakes.FakeClient() + client_fixture_class = client.V3 + data_fixture_class = data.V3 def _get_keypair_type(self): return keypairs.Keypair + + def _get_keypair_prefix(self): + return keypairs.KeypairManager.keypair_prefix diff --git a/awx/lib/site-packages/novaclient/tests/v3/test_list_extensions.py b/awx/lib/site-packages/novaclient/tests/v3/test_list_extensions.py new file mode 100644 index 0000000000..61d387c3ce --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/v3/test_list_extensions.py @@ -0,0 +1,33 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import extension +from novaclient.tests import utils +from novaclient.tests.v3 import fakes +from novaclient.v3 import list_extensions + + +extensions = [ + extension.Extension("list_extensions", list_extensions), +] +cs = fakes.FakeClient(extensions=extensions) + + +class ListExtensionsTests(utils.TestCase): + def test_list_extensions(self): + all_exts = cs.list_extensions.show_all() + cs.assert_called('GET', '/extensions') + self.assertTrue(len(all_exts) > 0) + for r in all_exts: + self.assertTrue(len(r.summary) > 0) diff --git a/awx/lib/site-packages/novaclient/tests/v3/test_quotas.py b/awx/lib/site-packages/novaclient/tests/v3/test_quotas.py index 1e4eef19b8..53ca3258eb 100644 --- a/awx/lib/site-packages/novaclient/tests/v3/test_quotas.py +++ b/awx/lib/site-packages/novaclient/tests/v3/test_quotas.py @@ -12,22 +12,20 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import quotas as data from novaclient.tests.v1_1 import test_quotas -from novaclient.tests.v3 import fakes class QuotaSetsTest(test_quotas.QuotaSetsTest): - def setUp(self): - super(QuotaSetsTest, self).setUp() - self.cs = self._get_fake_client() - def _get_fake_client(self): - return fakes.FakeClient() + client_fixture_class = client.V3 + data_fixture_class = data.V3 def test_force_update_quota(self): q = self.cs.quotas.get('97f4c221bff44578b0300df4ef119353') q.update(cores=2, force=True) - self.cs.assert_called( + self.assert_called( 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', {'quota_set': {'force': True, 'cores': 2}}) @@ -35,11 +33,11 @@ class QuotaSetsTest(test_quotas.QuotaSetsTest): def test_tenant_quotas_get_detail(self): tenant_id = 'test' self.cs.quotas.get(tenant_id, detail=True) - self.cs.assert_called('GET', '/os-quota-sets/%s/detail' % tenant_id) + self.assert_called('GET', '/os-quota-sets/%s/detail' % tenant_id) def test_user_quotas_get_detail(self): tenant_id = 'test' user_id = 'fake_user' self.cs.quotas.get(tenant_id, user_id=user_id, detail=True) url = '/os-quota-sets/%s/detail?user_id=%s' % (tenant_id, user_id) - self.cs.assert_called('GET', url) + self.assert_called('GET', url) diff --git a/awx/lib/site-packages/novaclient/tests/v3/test_servers.py b/awx/lib/site-packages/novaclient/tests/v3/test_servers.py index 9d0abcd627..e03fc5f406 100644 --- a/awx/lib/site-packages/novaclient/tests/v3/test_servers.py +++ b/awx/lib/site-packages/novaclient/tests/v3/test_servers.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import mock import six from novaclient import exceptions @@ -74,6 +75,50 @@ class ServersTest(utils.TestCase): cs.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) + def test_create_server_boot_with_nics_ipv4(self): + old_boot = cs.servers._boot + nics = [{'net-id': '11111111-1111-1111-1111-111111111111', + 'v4-fixed-ip': '10.10.0.7'}] + + def wrapped_boot(url, key, *boot_args, **boot_kwargs): + self.assertEqual(boot_kwargs['nics'], nics) + return old_boot(url, key, *boot_args, **boot_kwargs) + + with mock.patch.object(cs.servers, '_boot', wrapped_boot): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + nics=nics + ) + cs.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + + def test_create_server_boot_with_nics_ipv6(self): + old_boot = cs.servers._boot + nics = [{'net-id': '11111111-1111-1111-1111-111111111111', + 'v6-fixed-ip': '2001:db9:0:1::10'}] + + def wrapped_boot(url, key, *boot_args, **boot_kwargs): + self.assertEqual(boot_kwargs['nics'], nics) + return old_boot(url, key, *boot_args, **boot_kwargs) + + with mock.patch.object(cs.servers, '_boot', wrapped_boot): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + nics=nics + ) + cs.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + def test_create_server_userdata_file_object(self): s = cs.servers.create( name="My server", @@ -121,6 +166,26 @@ class ServersTest(utils.TestCase): cs.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) + def test_create_server_return_reservation_id(self): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + reservation_id=True + ) + expected_body = { + 'server': { + 'name': 'My server', + 'image_ref': '1', + 'flavor_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 1, + 'os-multiple-create:return_reservation_id': True, + } + } + cs.assert_called('POST', '/servers', expected_body) + self.assertIsInstance(s, servers.Server) + def test_update_server(self): s = cs.servers.get(1234) @@ -147,12 +212,12 @@ class ServersTest(utils.TestCase): cs.assert_called('DELETE', '/servers/1234') def test_delete_server_meta(self): - s = cs.servers.delete_meta(1234, ['test_key']) + cs.servers.delete_meta(1234, ['test_key']) cs.assert_called('DELETE', '/servers/1234/metadata/test_key') def test_set_server_meta(self): - s = cs.servers.set_meta(1234, {'test_key': 'test_value'}) - reval = cs.assert_called('POST', '/servers/1234/metadata', + cs.servers.set_meta(1234, {'test_key': 'test_value'}) + cs.assert_called('POST', '/servers/1234/metadata', {'metadata': {'test_key': 'test_value'}}) def test_find(self): @@ -299,7 +364,8 @@ class ServersTest(utils.TestCase): cs.servers.get_console_output(s) self.assertEqual(cs.servers.get_console_output(s), success) - cs.assert_called('POST', '/servers/1234/action') + cs.assert_called('POST', '/servers/1234/action', + {'get_console_output': {'length': -1}}) def test_get_console_output_with_length(self): success = 'foo' @@ -307,11 +373,13 @@ class ServersTest(utils.TestCase): s = cs.servers.get(1234) s.get_console_output(length=50) self.assertEqual(s.get_console_output(length=50), success) - cs.assert_called('POST', '/servers/1234/action') + cs.assert_called('POST', '/servers/1234/action', + {'get_console_output': {'length': 50}}) cs.servers.get_console_output(s, length=50) self.assertEqual(cs.servers.get_console_output(s, length=50), success) - cs.assert_called('POST', '/servers/1234/action') + cs.assert_called('POST', '/servers/1234/action', + {'get_console_output': {'length': 50}}) def test_get_password(self): s = cs.servers.get(1234) diff --git a/awx/lib/site-packages/novaclient/tests/v3/test_shell.py b/awx/lib/site-packages/novaclient/tests/v3/test_shell.py index 85a839b498..3d9c2b598b 100644 --- a/awx/lib/site-packages/novaclient/tests/v3/test_shell.py +++ b/awx/lib/site-packages/novaclient/tests/v3/test_shell.py @@ -186,20 +186,6 @@ class ShellTest(utils.TestCase): }}, ) - def test_boot_multiple(self): - self.run_command('boot --flavor 1 --image 1' - ' --num-instances 3 some-server') - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavor_ref': '1', - 'name': 'some-server', - 'image_ref': '1', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 3, - }}, - ) - def test_boot_image_with(self): self.run_command("boot --flavor 1" " --image-with test_key=test_value some-server") @@ -240,6 +226,7 @@ class ShellTest(utils.TestCase): mock_open.assert_called_once_with(testfile) + user_data = base64.b64encode(file_text.encode('utf-8')).decode('utf-8') self.assert_called_anytime( 'POST', '/servers', {'server': { @@ -248,9 +235,7 @@ class ShellTest(utils.TestCase): 'image_ref': '1', 'os-multiple-create:min_count': 1, 'os-multiple-create:max_count': 1, - 'os-user-data:user_data': base64.b64encode( - file_text.encode('utf-8')) - }}, + 'os-user-data:user_data': user_data}}, ) def test_boot_avzone(self): @@ -428,7 +413,33 @@ class ShellTest(utils.TestCase): }, ) - def tets_boot_nics_no_value(self): + def test_boot_nics_ipv6(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id=a=c,v6-fixed-ip=2001:db9:0:1::10 some-server') + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavor_ref': '1', + 'name': 'some-server', + 'image_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 1, + 'networks': [ + {'uuid': 'a=c', 'fixed_ip': '2001:db9:0:1::10'}, + ], + }, + }, + ) + + def test_boot_nics_both_ipv4_and_ipv6(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id=a=c,v4-fixed-ip=10.0.0.1,' + 'v6-fixed-ip=2001:db9:0:1::10 some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nics_no_value(self): cmd = ('boot --image 1 --flavor 1 ' '--nic net-id some-server') self.assertRaises(exceptions.CommandError, self.run_command, cmd) @@ -443,6 +454,11 @@ class ShellTest(utils.TestCase): '--nic v4-fixed-ip=10.0.0.1 some-server') self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_nics_netid_and_portid(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic port-id=some=port,net-id=some=net some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_num_instances(self): self.run_command('boot --image 1 --flavor 1 --num-instances 3 server') self.assert_called_anytime( @@ -458,11 +474,69 @@ class ShellTest(utils.TestCase): }) def test_boot_invalid_num_instances(self): - cmd = 'boot --image 1 --flavor 1 --num-instances 1 server' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) cmd = 'boot --image 1 --flavor 1 --num-instances 0 server' self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_num_instances_and_count(self): + cmd = 'boot --image 1 --flavor 1 --num-instances 3 --min-count 3 serv' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + cmd = 'boot --image 1 --flavor 1 --num-instances 3 --max-count 3 serv' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_min_max_count(self): + self.run_command('boot --image 1 --flavor 1 --max-count 3 server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavor_ref': '1', + 'name': 'server', + 'image_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 3, + } + }) + self.run_command('boot --image 1 --flavor 1 --min-count 3 server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavor_ref': '1', + 'name': 'server', + 'image_ref': '1', + 'os-multiple-create:min_count': 3, + 'os-multiple-create:max_count': 3, + } + }) + self.run_command('boot --image 1 --flavor 1 ' + '--min-count 3 --max-count 3 server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavor_ref': '1', + 'name': 'server', + 'image_ref': '1', + 'os-multiple-create:min_count': 3, + 'os-multiple-create:max_count': 3, + } + }) + self.run_command('boot --image 1 --flavor 1 ' + '--min-count 3 --max-count 5 server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavor_ref': '1', + 'name': 'server', + 'image_ref': '1', + 'os-multiple-create:min_count': 3, + 'os-multiple-create:max_count': 5, + } + }) + cmd = 'boot --image 1 --flavor 1 --min-count 3 --max-count 1 serv' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + @mock.patch('novaclient.v3.shell._poll_for_status') def test_boot_with_poll(self, poll_method): self.run_command('boot --flavor 1 --image 1 some-server --poll') @@ -480,3 +554,43 @@ class ShellTest(utils.TestCase): poll_method.assert_has_calls( [mock.call(self.shell.cs.servers.get, 1234, 'building', ['active'])]) + + def test_boot_with_poll_to_check_VM_state_error(self): + self.assertRaises(exceptions.InstanceInErrorState, self.run_command, + 'boot --flavor 1 --image 1 some-bad-server --poll') + + def test_boot_named_flavor(self): + self.run_command(["boot", "--image", "1", + "--flavor", "512 MB Server", + "--max-count", "3", "server"]) + self.assert_called('GET', '/flavors/512 MB Server', pos=0) + self.assert_called('GET', '/flavors?is_public=None', pos=1) + self.assert_called('GET', '/flavors?is_public=None', pos=2) + self.assert_called('GET', '/flavors/2', pos=3) + self.assert_called( + 'POST', '/servers', + { + 'server': { + 'flavor_ref': '2', + 'name': 'server', + 'image_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 3, + } + }, pos=4) + + def test_flavor_show_by_name(self): + self.run_command(['flavor-show', '128 MB Server']) + self.assert_called('GET', '/flavors/128 MB Server', pos=0) + self.assert_called('GET', '/flavors?is_public=None', pos=1) + self.assert_called('GET', '/flavors?is_public=None', pos=2) + self.assert_called('GET', '/flavors/aa1', pos=3) + self.assert_called('GET', '/flavors/aa1/flavor-extra-specs', pos=4) + + def test_flavor_show_by_name_priv(self): + self.run_command(['flavor-show', '512 MB Server']) + self.assert_called('GET', '/flavors/512 MB Server', pos=0) + self.assert_called('GET', '/flavors?is_public=None', pos=1) + self.assert_called('GET', '/flavors?is_public=None', pos=2) + self.assert_called('GET', '/flavors/2', pos=3) + self.assert_called('GET', '/flavors/2/flavor-extra-specs', pos=4) diff --git a/awx/lib/site-packages/novaclient/tests/v3/test_volumes.py b/awx/lib/site-packages/novaclient/tests/v3/test_volumes.py index 82f6d54a06..cfe6d9eb33 100644 --- a/awx/lib/site-packages/novaclient/tests/v3/test_volumes.py +++ b/awx/lib/site-packages/novaclient/tests/v3/test_volumes.py @@ -26,16 +26,33 @@ class VolumesTest(utils.TestCase): return fakes.FakeClient() def test_attach_server_volume(self): - v = self.cs.volumes.attach_server_volume( + self.cs.volumes.attach_server_volume( server=1234, volume_id='15e59938-07d5-11e1-90e3-e3dffe0c5983', device='/dev/vdb' ) self.cs.assert_called('POST', '/servers/1234/action') + def test_attach_server_volume_disk_bus_device_type(self): + volume_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' + device = '/dev/vdb' + disk_bus = 'ide' + device_type = 'cdrom' + self.cs.volumes.attach_server_volume(server=1234, + volume_id=volume_id, + device=device, + disk_bus=disk_bus, + device_type=device_type) + body_params = {'volume_id': volume_id, + 'device': device, + 'disk_bus': disk_bus, + 'device_type': device_type} + body = {'attach': body_params} + self.cs.assert_called('POST', '/servers/1234/action', body) + def test_update_server_volume(self): vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' - v = self.cs.volumes.update_server_volume( + self.cs.volumes.update_server_volume( server=1234, old_volume_id='Work', new_volume_id=vol_id diff --git a/awx/lib/site-packages/novaclient/utils.py b/awx/lib/site-packages/novaclient/utils.py index c10d5c3b2a..bedd74347f 100644 --- a/awx/lib/site-packages/novaclient/utils.py +++ b/awx/lib/site-packages/novaclient/utils.py @@ -57,13 +57,14 @@ def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False): extra_kwargs = {} for hook in hooks: hook_kwargs = hook(args) - + hook_name = hook.__name__ conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys()) if conflicting_keys and not allow_conflicts: - raise Exception(_("Hook '%(hook_name)s' is attempting to redefine" - " attributes '%(conflicting_keys)s'") % - {'hook_name': hook_name, - 'conflicting_keys': conflicting_keys}) + msg = (_("Hook '%(hook_name)s' is attempting to redefine " + "attributes '%(conflicting_keys)s'") % + {'hook_name': hook_name, + 'conflicting_keys': conflicting_keys}) + raise exceptions.NoUniqueMatch(msg) extra_kwargs.update(hook_kwargs) @@ -203,7 +204,7 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): def find_resource(manager, name_or_id, **find_args): """Helper for the _find_* methods.""" - # for str id which is not uuid (for Flavor search currently) + # for str id which is not uuid (for Flavor and Keypair search currently) if getattr(manager, 'is_alphanum_id_allowed', False): try: return manager.get(name_or_id) @@ -351,7 +352,7 @@ def _load_entry_point(ep_name, name=None): def is_integer_like(val): """Returns validation of a value as an integer.""" try: - value = int(val) + int(val) return True except (TypeError, ValueError, AttributeError): return False diff --git a/awx/lib/site-packages/novaclient/v1_1/certs.py b/awx/lib/site-packages/novaclient/v1_1/certs.py index b33d521342..232d7c1fdb 100644 --- a/awx/lib/site-packages/novaclient/v1_1/certs.py +++ b/awx/lib/site-packages/novaclient/v1_1/certs.py @@ -37,7 +37,7 @@ class CertificateManager(base.Manager): def create(self): """ - Create a x509 certificates for a user in tenant. + Create a x509 certificate for a user in tenant. """ return self._create('/os-certificates', {}, 'certificate') diff --git a/awx/lib/site-packages/novaclient/v1_1/client.py b/awx/lib/site-packages/novaclient/v1_1/client.py index efeb5c993c..46034bb952 100644 --- a/awx/lib/site-packages/novaclient/v1_1/client.py +++ b/awx/lib/site-packages/novaclient/v1_1/client.py @@ -37,6 +37,7 @@ from novaclient.v1_1 import quota_classes from novaclient.v1_1 import quotas from novaclient.v1_1 import security_group_rules from novaclient.v1_1 import security_groups +from novaclient.v1_1 import server_groups from novaclient.v1_1 import servers from novaclient.v1_1 import services from novaclient.v1_1 import usage @@ -61,24 +62,46 @@ class Client(object): >>> client.flavors.list() ... + It is also possible to use an instance as a context manager in which + case there will be a session kept alive for the duration of the with + statement:: + + >>> with Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) as client: + ... client.servers.list() + ... client.flavors.list() + ... + + It is also possible to have a permanent (process-long) connection pool, + by passing a connection_pool=True:: + + >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, + ... AUTH_URL, connection_pool=True) """ - # FIXME(jesse): project_id isn't required to authenticate - def __init__(self, username, api_key, project_id, auth_url=None, - insecure=False, timeout=None, proxy_tenant_id=None, - proxy_token=None, region_name=None, - endpoint_type='publicURL', extensions=None, - service_type='compute', service_name=None, - volume_service_name=None, timings=False, - bypass_url=None, os_cache=False, no_cache=True, - http_log_debug=False, auth_system='keystone', - auth_plugin=None, auth_token=None, - cacert=None, tenant_id=None): + def __init__(self, username=None, api_key=None, project_id=None, + auth_url=None, insecure=False, timeout=None, + proxy_tenant_id=None, proxy_token=None, region_name=None, + endpoint_type='publicURL', extensions=None, + service_type='compute', service_name=None, + volume_service_name=None, timings=False, bypass_url=None, + os_cache=False, no_cache=True, http_log_debug=False, + auth_system='keystone', auth_plugin=None, auth_token=None, + cacert=None, tenant_id=None, user_id=None, + connection_pool=False, session=None, auth=None, + completion_cache=None): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument + + # NOTE(cyeoh): In the novaclient context (unlike Nova) the + # project_id is not the same as the tenant_id. Here project_id + # is a name (what the Nova API often refers to as a project or + # tenant name) and tenant_id is a UUID (what the Nova API + # often refers to as a project_id or tenant_id). + password = api_key self.projectid = project_id self.tenant_id = tenant_id + self.user_id = user_id self.flavors = flavors.FlavorManager(self) self.flavor_access = flavor_access.FlavorAccessManager(self) self.images = images.ImageManager(self) @@ -116,6 +139,7 @@ class Client(object): self.os_cache = os_cache or not no_cache self.availability_zones = \ availability_zones.AvailabilityZoneManager(self) + self.server_groups = server_groups.ServerGroupsManager(self) # Add in any extensions... if extensions: @@ -124,38 +148,66 @@ class Client(object): setattr(self, extension.name, extension.manager_class(self)) - self.client = client.HTTPClient(username, - password, - projectid=project_id, - tenant_id=tenant_id, - auth_url=auth_url, - auth_token=auth_token, - insecure=insecure, - timeout=timeout, - auth_system=auth_system, - auth_plugin=auth_plugin, - proxy_token=proxy_token, - proxy_tenant_id=proxy_tenant_id, - region_name=region_name, - endpoint_type=endpoint_type, - service_type=service_type, - service_name=service_name, - volume_service_name=volume_service_name, - timings=timings, - bypass_url=bypass_url, - os_cache=self.os_cache, - http_log_debug=http_log_debug, - cacert=cacert) + self.client = client._construct_http_client( + username=username, + password=password, + user_id=user_id, + project_id=project_id, + tenant_id=tenant_id, + auth_url=auth_url, + auth_token=auth_token, + insecure=insecure, + timeout=timeout, + auth_system=auth_system, + auth_plugin=auth_plugin, + proxy_token=proxy_token, + proxy_tenant_id=proxy_tenant_id, + region_name=region_name, + endpoint_type=endpoint_type, + service_type=service_type, + service_name=service_name, + volume_service_name=volume_service_name, + timings=timings, + bypass_url=bypass_url, + os_cache=self.os_cache, + http_log_debug=http_log_debug, + cacert=cacert, + connection_pool=connection_pool, + session=session, + auth=auth) + self.completion_cache = completion_cache + + def write_object_to_completion_cache(self, obj): + if self.completion_cache: + self.completion_cache.write_object(obj) + + def clear_completion_cache_for_class(self, obj_class): + if self.completion_cache: + self.completion_cache.clear_class(obj_class) + + @client._original_only + def __enter__(self): + self.client.open_session() + return self + + @client._original_only + def __exit__(self, t, v, tb): + self.client.close_session() + + @client._original_only def set_management_url(self, url): self.client.set_management_url(url) + @client._original_only def get_timings(self): return self.client.get_timings() + @client._original_only def reset_timings(self): self.client.reset_timings() + @client._original_only def authenticate(self): """ Authenticate against the server. diff --git a/awx/lib/site-packages/novaclient/v1_1/contrib/baremetal.py b/awx/lib/site-packages/novaclient/v1_1/contrib/baremetal.py index 825eedc9c8..d13018ce8b 100644 --- a/awx/lib/site-packages/novaclient/v1_1/contrib/baremetal.py +++ b/awx/lib/site-packages/novaclient/v1_1/contrib/baremetal.py @@ -194,7 +194,7 @@ def do_baremetal_node_create(cs, args): @utils.arg('node', metavar='', - help='ID of the node to delete.') + help=_('ID of the node to delete.')) def do_baremetal_node_delete(cs, args): """Remove a baremetal node and any associated interfaces.""" node = _find_baremetal_node(cs, args.node) @@ -219,10 +219,22 @@ def _translate_baremetal_node_keys(collection): def _print_baremetal_nodes_list(nodes): """Print the list of baremetal nodes.""" + + def _parse_address(fields): + macs = [] + for interface in fields.interfaces: + macs.append(interface['address']) + return ', '.join("%s" % i for i in macs) + + formatters = { + 'MAC Address': _parse_address + } + _translate_baremetal_node_keys(nodes) utils.print_list(nodes, [ 'ID', 'Host', + 'Task State', 'CPUs', 'Memory_MB', 'Disk_GB', @@ -231,7 +243,7 @@ def _print_baremetal_nodes_list(nodes): 'PM Username', 'PM Password', 'Terminal Port', - ]) + ], formatters=formatters) def do_baremetal_node_list(cs, _args): @@ -263,7 +275,7 @@ def _print_baremetal_node_interfaces(interfaces): @utils.arg('node', metavar='', - help="ID of node") + help=_("ID of node")) def do_baremetal_node_show(cs, args): """Show information about a baremetal node.""" node = _find_baremetal_node(cs, args.node) @@ -298,7 +310,7 @@ def do_baremetal_interface_remove(cs, args): cs.baremetal.remove_interface(args.node, args.address) -@utils.arg('node', metavar='', help="ID of node") +@utils.arg('node', metavar='', help=_("ID of node")) def do_baremetal_interface_list(cs, args): """List network interfaces associated with a baremetal node.""" interfaces = cs.baremetal.list_interfaces(args.node) diff --git a/awx/lib/site-packages/novaclient/v1_1/flavor_access.py b/awx/lib/site-packages/novaclient/v1_1/flavor_access.py index 29c1f84f5e..3b4ce43e63 100644 --- a/awx/lib/site-packages/novaclient/v1_1/flavor_access.py +++ b/awx/lib/site-packages/novaclient/v1_1/flavor_access.py @@ -21,7 +21,8 @@ from novaclient.openstack.common.gettextutils import _ class FlavorAccess(base.Resource): def __repr__(self): - return "" % self.name + return ("" % + (self.flavor_id, self.tenant_id)) class FlavorAccessManager(base.ManagerWithFind): diff --git a/awx/lib/site-packages/novaclient/v1_1/flavors.py b/awx/lib/site-packages/novaclient/v1_1/flavors.py index c47c476339..30536fc680 100644 --- a/awx/lib/site-packages/novaclient/v1_1/flavors.py +++ b/awx/lib/site-packages/novaclient/v1_1/flavors.py @@ -83,9 +83,9 @@ class Flavor(base.Resource): :param keys: A list of keys to be unset """ for k in keys: - return self.manager._delete( - "/flavors/%s/os-extra_specs/%s" % ( - base.getid(self), k)) + self.manager._delete( + "/flavors/%s/os-extra_specs/%s" % ( + base.getid(self), k)) def delete(self): """ diff --git a/awx/lib/site-packages/novaclient/v1_1/floating_ips.py b/awx/lib/site-packages/novaclient/v1_1/floating_ips.py index 25f5a74373..54979371f3 100644 --- a/awx/lib/site-packages/novaclient/v1_1/floating_ips.py +++ b/awx/lib/site-packages/novaclient/v1_1/floating_ips.py @@ -28,11 +28,14 @@ class FloatingIP(base.Resource): class FloatingIPManager(base.ManagerWithFind): resource_class = FloatingIP - def list(self): + def list(self, all_tenants=False): """ - List floating ips for a tenant + List floating ips """ - return self._list("/os-floating-ips", "floating_ips") + url = '/os-floating-ips' + if all_tenants: + url += '?all_tenants=1' + return self._list(url, "floating_ips") def create(self, pool=None): """ diff --git a/awx/lib/site-packages/novaclient/v1_1/keypairs.py b/awx/lib/site-packages/novaclient/v1_1/keypairs.py index 3a102a43d4..96caff6189 100644 --- a/awx/lib/site-packages/novaclient/v1_1/keypairs.py +++ b/awx/lib/site-packages/novaclient/v1_1/keypairs.py @@ -53,6 +53,7 @@ class Keypair(base.Resource): class KeypairManager(base.ManagerWithFind): resource_class = Keypair keypair_prefix = "os-keypairs" + is_alphanum_id_allowed = True def get(self, keypair): """ diff --git a/awx/lib/site-packages/novaclient/v1_1/networks.py b/awx/lib/site-packages/novaclient/v1_1/networks.py index d29faf26df..d4dc78b8ee 100644 --- a/awx/lib/site-packages/novaclient/v1_1/networks.py +++ b/awx/lib/site-packages/novaclient/v1_1/networks.py @@ -86,6 +86,7 @@ class NetworkManager(base.ManagerWithFind): :param multi_host: str :param priority: str :param project_id: str + :param vlan: int :param vlan_start: int :param vpn_start: int diff --git a/awx/lib/site-packages/novaclient/v1_1/server_groups.py b/awx/lib/site-packages/novaclient/v1_1/server_groups.py new file mode 100644 index 0000000000..be6ff8eefb --- /dev/null +++ b/awx/lib/site-packages/novaclient/v1_1/server_groups.py @@ -0,0 +1,71 @@ +# Copyright (c) 2014 VMware, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Server group interface. +""" + +from novaclient import base + + +class ServerGroup(base.Resource): + """ + A server group. + """ + NAME_ATTR = 'server_group_name' + + def __repr__(self): + return '' % self.id + + def delete(self): + self.manager.delete(self) + + +class ServerGroupsManager(base.ManagerWithFind): + """ + Manage :class:`ServerGroup` resources. + """ + resource_class = ServerGroup + + def list(self): + """Get a list of all server groups. + + :rtype: list of :class:`ServerGroup`. + """ + return self._list('/os-server-groups', 'server_groups') + + def get(self, id): + """Get a specific server group. + + :param id: The ID of the :class:`ServerGroup` to get. + :rtype: :class:`ServerGroup` + """ + return self._get('/os-server-groups/%s' % id, + 'server_group') + + def delete(self, id): + """Delete a specific server group. + + :param id: The ID of the :class:`ServerGroup` to delete. + """ + self._delete('/os-server-groups/%s' % id) + + def create(self, **kwargs): + """Create (allocate) a server group. + + :rtype: list of :class:`ServerGroup` + """ + body = {'server_group': kwargs} + return self._create('/os-server-groups', body, 'server_group') diff --git a/awx/lib/site-packages/novaclient/v1_1/servers.py b/awx/lib/site-packages/novaclient/v1_1/servers.py index cf284d94b0..4009990490 100644 --- a/awx/lib/site-packages/novaclient/v1_1/servers.py +++ b/awx/lib/site-packages/novaclient/v1_1/servers.py @@ -26,6 +26,7 @@ from six.moves.urllib import parse from novaclient import base from novaclient import crypto +from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import strutils from novaclient.v1_1.security_groups import SecurityGroup @@ -36,7 +37,7 @@ class Server(base.Resource): HUMAN_ID = True def __repr__(self): - return "" % self.name + return '' % getattr(self, 'name', 'unknown-name') def delete(self): """ @@ -434,8 +435,8 @@ class ServerManager(base.BootingManagerWithFind): connected networks, fixed ips, etc. :param scheduler_hints: (optional extension) arbitrary key-value pairs specified by the client to help boot an instance. - :param config_drive: (optional extension) value for config drive - either boolean, or volume-id + :param config_drive: (optional extension) If True, enable config drive + on the server. :param admin_pass: admin password for the server. :param disk_config: (optional extension) control how the disk is partitioned when the server is created. @@ -454,7 +455,8 @@ class ServerManager(base.BootingManagerWithFind): else: userdata = strutils.safe_encode(userdata) - body["server"]["user_data"] = base64.b64encode(userdata) + userdata_b64 = base64.b64encode(userdata).decode('utf-8') + body["server"]["user_data"] = userdata_b64 if meta: body["server"]["metadata"] = meta if reservation_id: @@ -490,9 +492,11 @@ class ServerManager(base.BootingManagerWithFind): data = file_or_string.read() else: data = file_or_string + + cont = base64.b64encode(data.encode('utf-8')).decode('utf-8') personality.append({ 'path': filepath, - 'contents': base64.b64encode(data.encode('utf-8')), + 'contents': cont, }) if availability_zone: @@ -520,8 +524,15 @@ class ServerManager(base.BootingManagerWithFind): # if value is empty string, do not send value in body if nic_info.get('net-id'): net_data['uuid'] = nic_info['net-id'] - if nic_info.get('v4-fixed-ip'): + if (nic_info.get('v4-fixed-ip') and + nic_info.get('v6-fixed-ip')): + raise base.exceptions.CommandError(_( + "Only one of 'v4-fixed-ip' and 'v6-fixed-ip' may be" + " provided.")) + elif nic_info.get('v4-fixed-ip'): net_data['fixed_ip'] = nic_info['v4-fixed-ip'] + elif nic_info.get('v6-fixed-ip'): + net_data['fixed_ip'] = nic_info['v6-fixed-ip'] if nic_info.get('port-id'): net_data['port'] = nic_info['port-id'] all_net_data.append(net_data) diff --git a/awx/lib/site-packages/novaclient/v1_1/services.py b/awx/lib/site-packages/novaclient/v1_1/services.py index f2588eaa18..d51fa3ebfd 100644 --- a/awx/lib/site-packages/novaclient/v1_1/services.py +++ b/awx/lib/site-packages/novaclient/v1_1/services.py @@ -69,3 +69,7 @@ class ServiceManager(base.ManagerWithFind): """Disable the service with reason.""" body = self._update_body(host, binary, reason) return self._update("/os-services/disable-log-reason", body, "service") + + def delete(self, service_id): + """Delete a service.""" + return self._delete("/os-services/%s" % service_id) diff --git a/awx/lib/site-packages/novaclient/v1_1/shell.py b/awx/lib/site-packages/novaclient/v1_1/shell.py index a043d3c09a..ed43277d8c 100644 --- a/awx/lib/site-packages/novaclient/v1_1/shell.py +++ b/awx/lib/site-packages/novaclient/v1_1/shell.py @@ -129,19 +129,8 @@ def _parse_block_device_mapping_v2(args, image): return bdm -def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): +def _boot(cs, args): """Boot a new server.""" - if min_count is None: - min_count = 1 - if max_count is None: - max_count = min_count - if min_count > max_count: - raise exceptions.CommandError(_("min_instances should be <= " - "max_instances")) - if not min_count or not max_count: - raise exceptions.CommandError(_("min_instances nor max_instances " - "should be 0")) - if args.image: image = _find_image(cs, args.image) else: @@ -157,10 +146,31 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): if not args.flavor: raise exceptions.CommandError(_("you need to specify a Flavor ID ")) - if args.num_instances is not None: - if args.num_instances <= 1: - raise exceptions.CommandError(_("num_instances should be > 1")) + min_count = 1 + max_count = 1 + # Don't let user mix num_instances and max_count/min_count. + if (args.num_instances is not None and + args.min_count is None and args.max_count is None): + if args.num_instances < 1: + raise exceptions.CommandError(_("num_instances should be >= 1")) max_count = args.num_instances + elif (args.num_instances is not None and + (args.min_count is not None or args.max_count is not None)): + raise exceptions.CommandError(_("Don't mix num-instances and " + "max/min-count")) + if args.min_count is not None: + if args.min_count < 1: + raise exceptions.CommandError(_("min_count should be >= 1")) + min_count = args.min_count + max_count = min_count + if args.max_count is not None: + if args.max_count < 1: + raise exceptions.CommandError(_("max_count should be >= 1")) + max_count = args.max_count + if (args.min_count is not None and args.max_count is not None and + args.min_count > args.max_count): + raise exceptions.CommandError(_( + "min_count should be <= max_count")) flavor = _find_flavor(cs, args.flavor) @@ -235,9 +245,10 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): for nic_str in args.nics: err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " "the form --nic , with at minimum net-id or port-id " - "specified.") % nic_str) - nic_info = {"net-id": "", "v4-fixed-ip": "", "port-id": ""} + "v6-fixed-ip=ip-addr,port-id=port-uuid>, with at minimum " + "net-id or port-id (but not both) specified.") % nic_str) + nic_info = {"net-id": "", "v4-fixed-ip": "", "v6-fixed-ip": "", + "port-id": ""} for kv_str in nic_str.split(","): try: @@ -250,7 +261,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): else: raise exceptions.CommandError(err_msg) - if not nic_info['net-id'] and not nic_info['port-id']: + if bool(nic_info['net-id']) == bool(nic_info['port-id']): raise exceptions.CommandError(err_msg) nics.append(nic_info) @@ -280,7 +291,6 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): meta=meta, files=files, key_name=key_name, - reservation_id=reservation_id, min_count=min_count, max_count=max_count, userdata=userdata, @@ -321,7 +331,17 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): default=None, type=int, metavar='', - help=_("boot multiple servers at a time (limited by quota).")) + help=argparse.SUPPRESS) +@utils.arg('--min-count', + default=None, + type=int, + metavar='', + help=_("Boot at least servers (limited by quota).")) +@utils.arg('--max-count', + default=None, + type=int, + metavar='', + help=_("Boot up to servers (limited by quota).")) @utils.arg('--meta', metavar="", action='append', @@ -374,16 +394,25 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): action='append', default=[], help=_("Block device mapping with the keys: " - "id=image_id, snapshot_id or volume_id, " + "id=UUID (image_id, snapshot_id or volume_id only if using source " + "image, snapshot or volume) " "source=source type (image, snapshot, volume or blank), " "dest=destination type of the block device (volume or local), " - "bus=device's bus, " - "device=name of the device (e.g. vda, xda, ...), " - "size=size of the block device in GB, " - "format=device will be formatted (e.g. swap, ext3, ntfs, ...), " - "bootindex=integer used for ordering the boot disks, " - "type=device type (e.g. disk, cdrom, ...) and " - "shutdown=shutdown behaviour (either preserve or remove).")) + "bus=device's bus (e.g. uml, lxc, virtio, ...; if omitted, " + "hypervisor driver chooses a suitable default, " + "honoured only if device type is supplied) " + "type=device type (e.g. disk, cdrom, ...; defaults to 'disk') " + "device=name of the device (e.g. vda, xda, ...; " + "if omitted, hypervisor driver chooses suitable device " + "depending on selected bus), " + "size=size of the block device in GB (if omitted, " + "hypervisor driver calculates size), " + "format=device will be formatted (e.g. swap, ntfs, ...; optional), " + "bootindex=integer used for ordering the boot disks " + "(for image backed instances it is equal to 0, " + "for others need to be specified) and " + "shutdown=shutdown behaviour (either preserve or remove, " + "for local destination set to remove).")) @utils.arg('--swap', metavar="", default=None, @@ -402,17 +431,19 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): help=_("Send arbitrary key/value pairs to the scheduler for custom " "use.")) @utils.arg('--nic', - metavar="", + metavar="", action='append', dest='nics', default=[], help=_("Create a NIC on the server. " "Specify option multiple times to create multiple NICs. " "net-id: attach NIC to network with this UUID " - "(required if no port-id), " + "(either port-id or net-id must be provided), " "v4-fixed-ip: IPv4 fixed address for NIC (optional), " + "v6-fixed-ip: IPv6 fixed address for NIC (optional), " "port-id: attach NIC to port with this UUID " - "(required if no net-id)")) + "(either port-id or net-id must be provided).")) @utils.arg('--config-drive', metavar="", dest='config_drive', @@ -422,7 +453,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): dest='poll', action="store_true", default=False, - help=_('Blocks while server builds so progress can be reported.')) + help=_('Report the new server boot progress until it completes.')) def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) @@ -494,7 +525,7 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states, elif status == "error": if not silent: print(_("\nError %s server") % action) - break + raise exceptions.InstanceInErrorState(obj.fault['message']) if not silent: print_progress(progress) @@ -555,7 +586,7 @@ def _print_flavor_list(flavors, show_extra_specs=False): 'Memory_MB', 'Disk', 'Ephemeral', - 'Swap', + 'Swap_MB', 'VCPUs', 'RXTX_Factor', 'Is_Public', @@ -711,7 +742,7 @@ def do_flavor_access_list(cs, args): help=_('Tenant ID to add flavor access for.')) def do_flavor_access_add(cs, args): """Add flavor access for the given tenant.""" - flavor = _find_flavor_for_admin(cs, args.flavor) + flavor = _find_flavor(cs, args.flavor) access_list = cs.flavor_access.add_tenant_access(flavor, args.tenant) columns = ['Flavor_ID', 'Tenant_ID'] utils.print_list(access_list, columns) @@ -724,7 +755,7 @@ def do_flavor_access_add(cs, args): help=_('Tenant ID to remove flavor access for.')) def do_flavor_access_remove(cs, args): """Remove flavor access for the given tenant.""" - flavor = _find_flavor_for_admin(cs, args.flavor) + flavor = _find_flavor(cs, args.flavor) access_list = cs.flavor_access.remove_tenant_access(flavor, args.tenant) columns = ['Flavor_ID', 'Tenant_ID'] utils.print_list(access_list, columns) @@ -813,7 +844,7 @@ def _filter_network_create_options(args): valid_args = ['label', 'cidr', 'vlan_start', 'vpn_start', 'cidr_v6', 'gateway', 'gateway_v6', 'bridge', 'bridge_interface', 'multi_host', 'dns1', 'dns2', 'uuid', 'fixed_cidr', - 'project_id', 'priority'] + 'project_id', 'priority', 'vlan'] kwargs = {} for k, v in args.__dict__.items(): if k in valid_args and v is not None: @@ -833,9 +864,14 @@ def _filter_network_create_options(args): dest="cidr_v6", help=_('IPv6 subnet (ex: fe80::/64')) @utils.arg('--vlan', - dest='vlan_start', + dest='vlan', metavar='', - help=_("vlan id")) + help=_("vlan id to be assigned to project")) +@utils.arg('--vlan-start', + dest='vlan_start', + metavar='', + help=_('First vlan ID to be assigned to project. Subsequent vlan' + ' IDs will be assigned incrementally')) @utils.arg('--vpn', dest='vpn_start', metavar='', @@ -1020,13 +1056,12 @@ def do_image_delete(cs, args): dest='ip', metavar='', default=None, - help=_('Search with regular expression match by IP address (Admin only).')) + help=_('Search with regular expression match by IP address.')) @utils.arg('--ip6', dest='ip6', metavar='', default=None, - help=_('Search with regular expression match by IPv6 address ' - '(Admin only).')) + help=_('Search with regular expression match by IPv6 address.')) @utils.arg('--name', dest='name', metavar='', @@ -1036,8 +1071,7 @@ def do_image_delete(cs, args): dest='instance_name', metavar='', default=None, - help=_('Search with regular expression match by server name ' - '(Admin only).')) + help=_('Search with regular expression match by server name.')) @utils.arg('--instance_name', help=argparse.SUPPRESS) @utils.arg('--status', @@ -1174,7 +1208,7 @@ def do_list(cs, args): dest='poll', action="store_true", default=False, - help=_('Blocks while server is rebooting.')) + help=_('Poll until reboot is complete.')) def do_reboot(cs, args): """Reboot a server.""" server = _find_server(cs, args.server) @@ -1198,7 +1232,7 @@ def do_reboot(cs, args): dest='poll', action="store_true", default=False, - help=_('Blocks while server rebuilds so progress can be reported.')) + help=_('Report the server rebuild progress until it completes.')) @utils.arg('--minimal', dest='minimal', action="store_true", @@ -1220,8 +1254,8 @@ def do_rebuild(cs, args): kwargs = utils.get_resource_manager_extra_kwargs(do_rebuild, args) kwargs['preserve_ephemeral'] = args.preserve_ephemeral - server.rebuild(image, _password, **kwargs) - _print_server(cs, args) + server = server.rebuild(image, _password, **kwargs) + _print_server(cs, args, server) if args.poll: _poll_for_status(cs.servers.get, server.id, 'rebuilding', ['active']) @@ -1241,7 +1275,7 @@ def do_rename(cs, args): dest='poll', action="store_true", default=False, - help=_('Blocks while servers resizes so progress can be reported.')) + help=_('Report the server resize progress until it completes.')) def do_resize(cs, args): """Resize a server.""" server = _find_server(cs, args.server) @@ -1270,7 +1304,7 @@ def do_resize_revert(cs, args): dest='poll', action="store_true", default=False, - help=_('Blocks while server migrates so progress can be reported.')) + help=_('Report the server migration progress until it completes.')) def do_migrate(cs, args): """Migrate a server. The new host will be selected by the scheduler.""" server = _find_server(cs, args.server) @@ -1331,13 +1365,15 @@ def do_resume(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_rescue(cs, args): - """Rescue a server.""" + """Reboots a server into rescue mode, which starts the machine + from the initial image, attaching the current boot disk as secondary. + """ utils.print_dict(_find_server(cs, args.server).rescue()[1]) @utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_unrescue(cs, args): - """Unrescue a server.""" + """Restart the server from normal boot disk again.""" _find_server(cs, args.server).unrescue() @@ -1400,7 +1436,8 @@ def do_root_password(cs, args): dest='poll', action="store_true", default=False, - help=_('Blocks while server snapshots so progress can be reported.')) + help=_('Report the snapshot progress and poll until image creation is ' + 'complete.')) def do_image_create(cs, args): """Create a new image by taking a snapshot of a running server.""" server = _find_server(cs, args.server) @@ -1535,6 +1572,7 @@ def do_delete(cs, args): for server in args.server: try: _find_server(cs, server).delete() + print(_("Request to delete server %s has been accepted.") % server) except Exception as e: failure_count += 1 print(e) @@ -1554,18 +1592,10 @@ def _find_image(cs, image): return utils.find_resource(cs.images, image) -def _find_flavor_for_admin(cs, flavor): - """Get a flavor for administrator by name, ID, or RAM size.""" - try: - return utils.find_resource(cs.flavors, flavor, is_public=None) - except exceptions.NotFound: - return cs.flavors.find(ram=flavor) - - def _find_flavor(cs, flavor): """Get a flavor by name, ID, or RAM size.""" try: - return utils.find_resource(cs.flavors, flavor) + return utils.find_resource(cs.flavors, flavor, is_public=None) except exceptions.NotFound: return cs.flavors.find(ram=flavor) @@ -1746,17 +1776,16 @@ def do_volume_attach(cs, args): metavar='', help=_('Name or ID of server.')) @utils.arg('attachment_id', - metavar='', + metavar='', help=_('Attachment ID of the volume.')) @utils.arg('new_volume', metavar='', help=_('ID of the volume to attach.')) def do_volume_update(cs, args): """Update volume attachment.""" - volume = cs.volumes.update_server_volume(_find_server(cs, args.server).id, - args.attachment_id, - args.new_volume) - _print_volume(volume) + cs.volumes.update_server_volume(_find_server(cs, args.server).id, + args.attachment_id, + args.new_volume) @utils.arg('server', @@ -1764,11 +1793,11 @@ def do_volume_update(cs, args): help=_('Name or ID of server.')) @utils.arg('attachment_id', metavar='', - help=_('Attachment ID of the volume.')) + help=_('ID of the volume to detach.')) def do_volume_detach(cs, args): """Detach a volume from a server.""" cs.volumes.delete_server_volume(_find_server(cs, args.server).id, - args.attachment_id) + args.attachment_id) @utils.service_type('volume') @@ -1843,7 +1872,7 @@ def do_volume_type_list(cs, args): @utils.arg('name', metavar='', - help=_("Name of the new flavor")) + help=_("Name of the new volume type")) @utils.service_type('volume') def do_volume_type_create(cs, args): """Create a new volume type.""" @@ -1856,7 +1885,7 @@ def do_volume_type_create(cs, args): help=_("Unique ID of the volume type to delete")) @utils.service_type('volume') def do_volume_type_delete(cs, args): - """Delete a specific flavor""" + """Delete a specific volume type.""" cs.volume_types.delete(args.id) @@ -1935,6 +1964,9 @@ def do_clear_password(cs, args): def _print_floating_ip_list(floating_ips): + convert = [('instance_id', 'server_id')] + _translate_keys(floating_ips, convert) + utils.print_list(floating_ips, ['Ip', 'Server Id', 'Fixed Ip', 'Pool']) @@ -2041,9 +2073,13 @@ def do_floating_ip_delete(cs, args): args.address) -def do_floating_ip_list(cs, _args): - """List floating ips for this tenant.""" - _print_floating_ip_list(cs.floating_ips.list()) +@utils.arg('--all-tenants', + action='store_true', + default=False, + help=_('Display floatingips from all tenants (Admin only).')) +def do_floating_ip_list(cs, args): + """List floating ips.""" + _print_floating_ip_list(cs.floating_ips.list(args.all_tenants)) def do_floating_ip_pool_list(cs, _args): @@ -2435,7 +2471,7 @@ def do_keypair_add(cs, args): @utils.arg('name', metavar='', help=_('Keypair name to delete.')) def do_keypair_delete(cs, args): """Delete keypair given by its name.""" - name = args.name + name = _find_keypair(cs, args.name) cs.keypairs.delete(name) @@ -2458,10 +2494,15 @@ def _print_keypair(keypair): help=_("Name or ID of keypair")) def do_keypair_show(cs, args): """Show details about the given keypair.""" - keypair = cs.keypairs.get(args.keypair) + keypair = _find_keypair(cs, args.keypair) _print_keypair(keypair) +def _find_keypair(cs, keypair): + """Get a keypair by name or ID.""" + return utils.find_resource(cs.keypairs, keypair) + + @utils.arg('--tenant', #nova db searches by project_id dest='tenant', @@ -2739,7 +2780,8 @@ def do_aggregate_update(cs, args): nargs='+', action='append', default=[], - help=_('Metadata to add/update to aggregate')) + help=_('Metadata to add/update to aggregate. ' + 'Specify only the key to delete a metadata item.')) def do_aggregate_set_metadata(cs, args): """Update the metadata associated with the aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) @@ -2828,14 +2870,27 @@ def do_live_migration(cs, args): args.disk_over_commit) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', nargs='+', + help=_('Name or ID of server(s).')) @utils.arg('--active', action='store_const', dest='state', default='error', const='active', help=_('Request the server be reset to "active" state instead ' 'of "error" state (the default).')) def do_reset_state(cs, args): """Reset the state of a server.""" - _find_server(cs, args.server).reset_state(args.state) + failure_flag = False + + for server in args.server: + try: + _find_server(cs, server).reset_state(args.state) + except Exception as e: + failure_flag = True + msg = "Reset state for server %s failed: %s" % (server, e) + print(msg) + + if failure_flag: + msg = "Unable to reset the state for the specified server(s)." + raise exceptions.CommandError(msg) @utils.arg('server', metavar='', help=_('Name or ID of server.')) @@ -2856,6 +2911,12 @@ def do_service_list(cs, args): # so as not to add the column when the extended ext is not enabled. if result and hasattr(result[0], 'disabled_reason'): columns.append("Disabled Reason") + + # NOTE(gtt): After https://review.openstack.org/#/c/39998/ nova will + # show id in response. + if result and hasattr(result[0], 'id'): + columns.insert(0, "Id") + utils.print_list(result, columns) @@ -2883,6 +2944,12 @@ def do_service_disable(cs, args): utils.print_list([result], ['Host', 'Binary', 'Status']) +@utils.arg('id', metavar='', help=_('Id of service.')) +def do_service_delete(cs, args): + """Delete the service.""" + cs.services.delete(args.id) + + @utils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) def do_fixed_ip_get(cs, args): """Retrieve info on a fixed ip.""" @@ -3328,6 +3395,11 @@ def do_quota_class_show(cs, args): @utils.arg('--floating_ips', type=int, help=argparse.SUPPRESS) +@utils.arg('--fixed-ips', + metavar='', + type=int, + default=None, + help=_('New value for the "fixed-ips" quota.')) @utils.arg('--metadata-items', metavar='', type=int, @@ -3512,3 +3584,73 @@ def do_availability_zone_list(cs, _args): _translate_availability_zone_keys(result) utils.print_list(result, ['Name', 'Status'], sortby_index=None) + + +def _print_server_group_details(server_group): + columns = ['Id', 'Name', 'Policies', 'Members', 'Metadata'] + utils.print_list(server_group, columns) + + +def do_server_group_list(cs, args): + """Print a list of all server groups.""" + server_groups = cs.server_groups.list() + _print_server_group_details(server_groups) + + +@utils.arg('name', metavar='', help='Server group name.') +# NOTE(wingwj): The '--policy' way is still reserved here for preserving +# the backwards compatibility of CLI, even if a user won't get this usage +# in '--help' description. It will be deprecated after an suitable deprecation +# period(probably 2 coordinated releases or so). +# +# Moreover, we imagine that a given user will use only positional parameters or +# only the "--policy" option. So we don't need to properly handle +# the possibility that they might mix them here. That usage is unsupported. +# The related discussion can be found in +# https://review.openstack.org/#/c/96382/2/. +@utils.arg('policy', + metavar='', + default=argparse.SUPPRESS, + nargs='*', + help='Policies for the server groups ' + '("affinity" or "anti-affinity")') +@utils.arg('--policy', + default=[], + action='append', + help=argparse.SUPPRESS) +def do_server_group_create(cs, args): + """Create a new server group with the specified details.""" + if not args.policy: + raise exceptions.CommandError(_("at least one policy must be " + "specified")) + kwargs = {'name': args.name, + 'policies': args.policy} + server_group = cs.server_groups.create(**kwargs) + _print_server_group_details([server_group]) + + +@utils.arg('id', metavar='', nargs='+', + help="Unique ID(s) of the server group to delete") +def do_server_group_delete(cs, args): + """Delete specific server group(s).""" + failure_count = 0 + + for sg in args.id: + try: + cs.server_groups.delete(sg) + print(_("Server group %s has been successfully deleted.") % sg) + except Exception as e: + failure_count += 1 + print(_("Delete for server group %(sg)s failed: %(e)s") % + {'sg': sg, 'e': e}) + if failure_count == len(args.id): + raise exceptions.CommandError(_("Unable to delete any of the " + "specified server groups.")) + + +@utils.arg('id', metavar='', + help="Unique ID of the server group to get") +def do_server_group_get(cs, args): + """Get a specific server group.""" + server_group = cs.server_groups.get(args.id) + _print_server_group_details([server_group]) diff --git a/awx/lib/site-packages/novaclient/v3/client.py b/awx/lib/site-packages/novaclient/v3/client.py index f26fdcf997..28ac908b73 100644 --- a/awx/lib/site-packages/novaclient/v3/client.py +++ b/awx/lib/site-packages/novaclient/v3/client.py @@ -24,7 +24,7 @@ from novaclient.v3 import hosts from novaclient.v3 import hypervisors from novaclient.v3 import images from novaclient.v3 import keypairs -from novaclient.v3 import quota_classes +from novaclient.v3 import list_extensions from novaclient.v3 import quotas from novaclient.v3 import servers from novaclient.v3 import services @@ -47,21 +47,42 @@ class Client(object): >>> client.flavors.list() ... + It is also possible to use an instance as a context manager in which + case there will be a session kept alive for the duration of the with + statement:: + + >>> with Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) as client: + ... client.servers.list() + ... client.flavors.list() + ... + + It is also possible to have a permanent (process-long) connection pool, + by passing a connection_pool=True:: + + >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, + ... AUTH_URL, connection_pool=True) """ - # FIXME(jesse): project_id isn't required to authenticate - def __init__(self, username, password, project_id, auth_url=None, - insecure=False, timeout=None, proxy_tenant_id=None, - proxy_token=None, region_name=None, - endpoint_type='publicURL', extensions=None, - service_type='computev3', service_name=None, - volume_service_name=None, timings=False, - bypass_url=None, os_cache=False, no_cache=True, - http_log_debug=False, auth_system='keystone', - auth_plugin=None, auth_token=None, - cacert=None, tenant_id=None): + def __init__(self, username=None, password=None, project_id=None, + auth_url=None, insecure=False, timeout=None, + proxy_tenant_id=None, proxy_token=None, region_name=None, + endpoint_type='publicURL', extensions=None, + service_type='computev3', service_name=None, + volume_service_name=None, timings=False, bypass_url=None, + os_cache=False, no_cache=True, http_log_debug=False, + auth_system='keystone', auth_plugin=None, auth_token=None, + cacert=None, tenant_id=None, user_id=None, + connection_pool=False, session=None, auth=None, + completion_cache=None): + # NOTE(cyeoh): In the novaclient context (unlike Nova) the + # project_id is not the same as the tenant_id. Here project_id + # is a name (what the Nova API often refers to as a project or + # tenant name) and tenant_id is a UUID (what the Nova API + # often refers to as a project_id or tenant_id). + self.projectid = project_id self.tenant_id = tenant_id + self.user_id = user_id self.os_cache = os_cache or not no_cache #TODO(bnemec): Add back in v3 extensions self.agents = agents.AgentsManager(self) @@ -69,6 +90,7 @@ class Client(object): self.availability_zones = \ availability_zones.AvailabilityZoneManager(self) self.certs = certs.CertificateManager(self) + self.list_extensions = list_extensions.ListExtManager(self) self.hosts = hosts.HostManager(self) self.flavors = flavors.FlavorManager(self) self.flavor_access = flavor_access.FlavorAccessManager(self) @@ -76,7 +98,6 @@ class Client(object): self.images = images.ImageManager(self) self.keypairs = keypairs.KeypairManager(self) self.quotas = quotas.QuotaSetManager(self) - self.quota_classes = quota_classes.QuotaClassSetManager(self) self.servers = servers.ServerManager(self) self.services = services.ServiceManager(self) self.usage = usage.UsageManager(self) @@ -89,38 +110,66 @@ class Client(object): setattr(self, extension.name, extension.manager_class(self)) - self.client = client.HTTPClient(username, - password, - projectid=project_id, - tenant_id=tenant_id, - auth_url=auth_url, - insecure=insecure, - timeout=timeout, - auth_system=auth_system, - auth_plugin=auth_plugin, - auth_token=auth_token, - proxy_token=proxy_token, - proxy_tenant_id=proxy_tenant_id, - region_name=region_name, - endpoint_type=endpoint_type, - service_type=service_type, - service_name=service_name, - volume_service_name=volume_service_name, - timings=timings, - bypass_url=bypass_url, - os_cache=os_cache, - http_log_debug=http_log_debug, - cacert=cacert) + self.client = client._construct_http_client( + username=username, + password=password, + user_id=user_id, + project_id=project_id, + tenant_id=tenant_id, + auth_url=auth_url, + auth_token=auth_token, + insecure=insecure, + timeout=timeout, + auth_system=auth_system, + auth_plugin=auth_plugin, + proxy_token=proxy_token, + proxy_tenant_id=proxy_tenant_id, + region_name=region_name, + endpoint_type=endpoint_type, + service_type=service_type, + service_name=service_name, + volume_service_name=volume_service_name, + timings=timings, + bypass_url=bypass_url, + os_cache=self.os_cache, + http_log_debug=http_log_debug, + cacert=cacert, + connection_pool=connection_pool, + session=session, + auth=auth) + self.completion_cache = completion_cache + + def write_object_to_completion_cache(self, obj): + if self.completion_cache: + self.completion_cache.write_object(obj) + + def clear_completion_cache_for_class(self, obj_class): + if self.completion_cache: + self.completion_cache.clear_class(obj_class) + + @client._original_only + def __enter__(self): + self.client.open_session() + return self + + @client._original_only + def __exit__(self, t, v, tb): + self.client.close_session() + + @client._original_only def set_management_url(self, url): self.client.set_management_url(url) + @client._original_only def get_timings(self): return self.client.get_timings() + @client._original_only def reset_timings(self): self.client.reset_timings() + @client._original_only def authenticate(self): """ Authenticate against the server. diff --git a/awx/lib/site-packages/novaclient/v3/flavors.py b/awx/lib/site-packages/novaclient/v3/flavors.py index 38f2885043..60553b2f59 100644 --- a/awx/lib/site-packages/novaclient/v3/flavors.py +++ b/awx/lib/site-packages/novaclient/v3/flavors.py @@ -72,9 +72,9 @@ class Flavor(base.Resource): :param keys: A list of keys to be unset """ for k in keys: - return self.manager._delete( - "/flavors/%s/flavor-extra-specs/%s" % ( - base.getid(self), k)) + self.manager._delete( + "/flavors/%s/flavor-extra-specs/%s" % ( + base.getid(self), k)) def delete(self): """ diff --git a/awx/lib/site-packages/novaclient/v3/list_extensions.py b/awx/lib/site-packages/novaclient/v3/list_extensions.py new file mode 100644 index 0000000000..bcc187494f --- /dev/null +++ b/awx/lib/site-packages/novaclient/v3/list_extensions.py @@ -0,0 +1,26 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +extension interface +""" + +from novaclient.v1_1.contrib import list_extensions + + +class ListExtResource(list_extensions.ListExtResource): + pass + + +class ListExtManager(list_extensions.ListExtManager): + pass diff --git a/awx/lib/site-packages/novaclient/v3/servers.py b/awx/lib/site-packages/novaclient/v3/servers.py index d8ebd0a132..0e028b5da3 100644 --- a/awx/lib/site-packages/novaclient/v3/servers.py +++ b/awx/lib/site-packages/novaclient/v3/servers.py @@ -26,6 +26,7 @@ from six.moves.urllib import parse from novaclient import base from novaclient import crypto +from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import strutils REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' @@ -403,13 +404,13 @@ class ServerManager(base.BootingManagerWithFind): else: userdata = strutils.safe_encode(userdata) - body["server"][ - "os-user-data:user_data"] = base64.b64encode(userdata) + data = base64.b64encode(userdata).decode('utf-8') + body["server"]["os-user-data:user_data"] = data if meta: body["server"]["metadata"] = meta if reservation_id: body["server"][ - "os-multiple-create:reservation_id"] = reservation_id + "os-multiple-create:return_reservation_id"] = reservation_id if key_name: body["server"]["key_name"] = key_name if scheduler_hints: @@ -457,8 +458,15 @@ class ServerManager(base.BootingManagerWithFind): # if value is empty string, do not send value in body if nic_info.get('net-id'): net_data['uuid'] = nic_info['net-id'] - if nic_info.get('v4-fixed-ip'): + if (nic_info.get('v4-fixed-ip') and + nic_info.get('v6-fixed-ip')): + raise base.exceptions.CommandError(_( + "Only one of 'v4-fixed-ip' and 'v6-fixed-ip' may be" + " provided.")) + elif nic_info.get('v4-fixed-ip'): net_data['fixed_ip'] = nic_info['v4-fixed-ip'] + elif nic_info.get('v6-fixed-ip'): + net_data['fixed_ip'] = nic_info['v6-fixed-ip'] if nic_info.get('port-id'): net_data['port'] = nic_info['port-id'] all_net_data.append(net_data) @@ -894,6 +902,11 @@ class ServerManager(base.BootingManagerWithFind): you would like to retrieve. :param length: The number of tail loglines you would like to retrieve. """ + if length is None: + # NOTE: On v3 get_console_output API, -1 means an unlimited length. + # Here translates None, which means an unlimited in the internal + # implementation, to -1. + length = -1 return self._action('get_console_output', server, {'length': length})[1]['output'] diff --git a/awx/lib/site-packages/novaclient/v3/shell.py b/awx/lib/site-packages/novaclient/v3/shell.py index 10a6975d80..9f4b13bdeb 100644 --- a/awx/lib/site-packages/novaclient/v3/shell.py +++ b/awx/lib/site-packages/novaclient/v3/shell.py @@ -60,19 +60,8 @@ def _match_image(cs, wanted_properties): return images_matched -def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): +def _boot(cs, args): """Boot a new server.""" - if min_count is None: - min_count = 1 - if max_count is None: - max_count = min_count - if min_count > max_count: - raise exceptions.CommandError("min_instances should be <= " - "max_instances") - if not min_count or not max_count: - raise exceptions.CommandError("min_instances nor max_instances should" - "be 0") - if args.image: image = _find_image(cs.image_cs, args.image) else: @@ -93,10 +82,31 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): if not args.flavor: raise exceptions.CommandError("you need to specify a Flavor ID ") - if args.num_instances is not None: - if args.num_instances <= 1: - raise exceptions.CommandError("num_instances should be > 1") + min_count = 1 + max_count = 1 + # Don't let user mix num_instances and max_count/min_count. + if (args.num_instances is not None and + args.min_count is None and args.max_count is None): + if args.num_instances < 1: + raise exceptions.CommandError("num_instances should be >= 1") max_count = args.num_instances + elif (args.num_instances is not None and + (args.min_count is not None or args.max_count is not None)): + raise exceptions.CommandError("Don't mix num-instances and " + "max/min-count") + if args.min_count is not None: + if args.min_count < 1: + raise exceptions.CommandError("min_count should be >= 1") + min_count = args.min_count + max_count = min_count + if args.max_count is not None: + if args.max_count < 1: + raise exceptions.CommandError("max_count should be >= 1") + max_count = args.max_count + if (args.min_count is not None and args.max_count is not None and + args.min_count > args.max_count): + raise exceptions.CommandError( + "min_count should be <= max_count") flavor = _find_flavor(cs, args.flavor) @@ -146,9 +156,10 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): for nic_str in args.nics: err_msg = ("Invalid nic argument '%s'. Nic arguments must be of the " "form --nic , with at minimum net-id or port-id " - "specified." % nic_str) - nic_info = {"net-id": "", "v4-fixed-ip": "", "port-id": ""} + "v6-fixed-ip=ip-addr,port-id=port-uuid>, with at minimum " + "net-id or port-id (but not both) specified." % nic_str) + nic_info = {"net-id": "", "v4-fixed-ip": "", "v6-fixed-ip": "", + "port-id": ""} for kv_str in nic_str.split(","): try: @@ -161,7 +172,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): else: raise exceptions.CommandError(err_msg) - if not nic_info['net-id'] and not nic_info['port-id']: + if bool(nic_info['net-id']) == bool(nic_info['port-id']): raise exceptions.CommandError(err_msg) nics.append(nic_info) @@ -191,7 +202,6 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): meta=meta, files=files, key_name=key_name, - reservation_id=reservation_id, min_count=min_count, max_count=max_count, userdata=userdata, @@ -223,7 +233,17 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): default=None, type=int, metavar='', - help="boot multiple servers at a time (limited by quota).") + help=argparse.SUPPRESS) +@utils.arg('--min-count', + default=None, + type=int, + metavar='', + help="Boot at least servers (limited by quota).") +@utils.arg('--max-count', + default=None, + type=int, + metavar='', + help="Boot up to servers (limited by quota).") @utils.arg('--meta', metavar="", action='append', @@ -278,17 +298,19 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): metavar='', help="Send arbitrary key/value pairs to the scheduler for custom use.") @utils.arg('--nic', - metavar="", + metavar="", action='append', dest='nics', default=[], help="Create a NIC on the server. " "Specify option multiple times to create multiple NICs. " "net-id: attach NIC to network with this UUID " - "(required if no port-id), " + "(either port-id or net-id must be provided), " "v4-fixed-ip: IPv4 fixed address for NIC (optional), " + "v6-fixed-ip: IPv6 fixed address for NIC (optional), " "port-id: attach NIC to port with this UUID " - "(required if no net-id)") + "(either port-id or net-id must be provided).") @utils.arg('--config-drive', metavar="", dest='config_drive', @@ -298,7 +320,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): dest='poll', action="store_true", default=False, - help='Blocks while server builds so progress can be reported.') + help='Report the new server boot progress until it completes.') def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) @@ -349,7 +371,7 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states, elif status == "error": if not silent: print("\nError %s server" % action) - break + raise exceptions.InstanceInErrorState(obj.fault['message']) if not silent: print_progress(progress) @@ -872,12 +894,12 @@ def do_image_delete(cs, args): dest='ip', metavar='', default=None, - help='Search with regular expression match by IP address (Admin only).') + help='Search with regular expression match by IP address.') @utils.arg('--ip6', dest='ip6', metavar='', default=None, - help='Search with regular expression match by IPv6 address (Admin only).') + help='Search with regular expression match by IPv6 address.') @utils.arg('--name', dest='name', metavar='', @@ -887,7 +909,7 @@ def do_image_delete(cs, args): dest='instance_name', metavar='', default=None, - help='Search with regular expression match by server name (Admin only).') + help='Search with regular expression match by server name.') @utils.arg('--instance_name', help=argparse.SUPPRESS) @utils.arg('--status', @@ -1024,7 +1046,7 @@ def do_list(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while server is rebooting.') + help='Poll until reboot is complete.') def do_reboot(cs, args): """Reboot a server.""" server = _find_server(cs, args.server) @@ -1048,7 +1070,7 @@ def do_reboot(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while server rebuilds so progress can be reported.') + help='Report the server rebuild progress until it completes.') @utils.arg('--minimal', dest='minimal', action="store_true", @@ -1065,8 +1087,8 @@ def do_rebuild(cs, args): _password = None kwargs = utils.get_resource_manager_extra_kwargs(do_rebuild, args) - server.rebuild(image, _password, **kwargs) - _print_server(cs, args) + server = server.rebuild(image, _password, **kwargs) + _print_server(cs, args, server) if args.poll: _poll_for_status(cs.servers.get, server.id, 'rebuilding', ['active']) @@ -1086,7 +1108,7 @@ def do_rename(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while server resizes so progress can be reported.') + help='Report the server resize progress until it completes.') def do_resize(cs, args): """Resize a server.""" server = _find_server(cs, args.server) @@ -1115,7 +1137,7 @@ def do_resize_revert(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while server migrates so progress can be reported.') + help='Report the server migration progress until it completes.') def do_migrate(cs, args): """Migrate a server. The new host will be selected by the scheduler.""" server = _find_server(cs, args.server) @@ -1176,13 +1198,15 @@ def do_resume(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') def do_rescue(cs, args): - """Rescue a server.""" + """Reboots a server into rescue mode, which starts the machine + from the initial image, attaching the current boot disk as secondary. + """ utils.print_dict(_find_server(cs, args.server).rescue()[1]) @utils.arg('server', metavar='', help='Name or ID of server.') def do_unrescue(cs, args): - """Unrescue a server.""" + """Restart the server from normal boot disk again.""" _find_server(cs, args.server).unrescue() @@ -1217,7 +1241,8 @@ def do_root_password(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while server snapshots so progress can be reported.') + help='Report the snapshot progress and poll until image creation is ' + 'complete.') def do_image_create(cs, args): """Create a new image by taking a snapshot of a running server.""" server = _find_server(cs, args.server) @@ -1347,6 +1372,7 @@ def do_delete(cs, args): for server in args.server: try: _find_server(cs, server).delete() + print("Request to delete server %s has been accepted." % server) except Exception as e: failure_count += 1 print(e) @@ -1369,7 +1395,7 @@ def _find_image(cs, image): def _find_flavor(cs, flavor): """Get a flavor by name, ID, or RAM size.""" try: - return utils.find_resource(cs.flavors, flavor) + return utils.find_resource(cs.flavors, flavor, is_public=None) except exceptions.NotFound: return cs.flavors.find(ram=flavor) @@ -1406,30 +1432,39 @@ def _translate_availability_zone_keys(collection): @utils.arg('device', metavar='', default=None, nargs='?', help='Name of the device e.g. /dev/vdb. ' 'Use "auto" for autoassign (if supported)') +@utils.arg('disk_bus', + metavar='', + default=None, + nargs='?', + help='The disk bus e.g. ide of the volume (optional).') +@utils.arg('device_type', + metavar='', + default=None, + nargs='?', + help='The device type e.g. cdrom of the volume (optional).') def do_volume_attach(cs, args): """Attach a volume to a server.""" if args.device == 'auto': args.device = None - volume = cs.volumes.attach_server_volume(_find_server(cs, args.server).id, - args.volume, - args.device) + cs.volumes.attach_server_volume(_find_server(cs, args.server).id, + args.volume, args.device, args.disk_bus, + args.device_type) @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('attachment_id', - metavar='', + metavar='', help='Attachment ID of the volume.') @utils.arg('new_volume', metavar='', help='ID of the volume to attach.') def do_volume_update(cs, args): """Update volume attachment.""" - volume = cs.volumes.update_server_volume(_find_server(cs, args.server).id, - args.attachment_id, - args.new_volume) + cs.volumes.update_server_volume(_find_server(cs, args.server).id, + args.attachment_id, args.new_volume) @utils.arg('server', @@ -1437,11 +1472,11 @@ def do_volume_update(cs, args): help='Name or ID of server.') @utils.arg('attachment_id', metavar='', - help='Attachment ID of the volume.') + help='ID of the volume to detach.') def do_volume_detach(cs, args): """Detach a volume from a server.""" cs.volumes.delete_server_volume(_find_server(cs, args.server).id, - args.attachment_id) + args.attachment_id) @utils.arg('server', metavar='', help='Name or ID of server.') @@ -1519,6 +1554,9 @@ def do_clear_password(cs, args): def _print_floating_ip_list(floating_ips): + convert = [('instance_id', 'server_id')] + _translate_keys(floating_ips, convert) + utils.print_list(floating_ips, ['Ip', 'Server Id', 'Fixed Ip', 'Pool']) @@ -1980,7 +2018,7 @@ def do_keypair_add(cs, args): @utils.arg('name', metavar='', help='Keypair name to delete.') def do_keypair_delete(cs, args): """Delete keypair given by its name.""" - name = args.name + name = _find_keypair(cs, args.name) cs.keypairs.delete(name) @@ -2003,10 +2041,15 @@ def _print_keypair(keypair): help="Name or ID of keypair") def do_keypair_show(cs, args): """Show details about the given keypair.""" - keypair = cs.keypairs.get(args.keypair) + keypair = _find_keypair(cs, args.keypair) _print_keypair(keypair) +def _find_keypair(cs, keypair): + """Get a keypair by name or ID.""" + return utils.find_resource(cs.keypairs, keypair) + + @utils.arg('--start', metavar='', help='Usage range start date ex 2012-01-20 (default: 4 weeks ago)', default=None) @@ -2339,14 +2382,27 @@ def do_live_migration(cs, args): args.disk_over_commit) -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', nargs='+', + help='Name or ID of server(s).') @utils.arg('--active', action='store_const', dest='state', default='error', const='active', help='Request the server be reset to "active" state instead ' 'of "error" state (the default).') def do_reset_state(cs, args): """Reset the state of a server.""" - _find_server(cs, args.server).reset_state(args.state) + failure_flag = False + + for server in args.server: + try: + _find_server(cs, server).reset_state(args.state) + except Exception as e: + failure_flag = True + msg = "Reset state for server %s failed: %s" % (server, e) + print(msg) + + if failure_flag: + msg = "Unable to reset the state for the specified server(s)." + raise exceptions.CommandError(msg) @utils.arg('server', metavar='', help='Name or ID of server.') @@ -2367,6 +2423,12 @@ def do_service_list(cs, args): # so as not to add the column when the extended ext is not enabled. if hasattr(result[0], 'disabled_reason'): columns.append("Disabled Reason") + + # NOTE(gtt): After https://review.openstack.org/#/c/39998/ nova will + # show id in response. + if result and hasattr(result[0], 'id'): + columns.insert(0, "Id") + utils.print_list(result, columns) @@ -2394,6 +2456,12 @@ def do_service_disable(cs, args): utils.print_list([result], ['Host', 'Binary', 'Status']) +@utils.arg('id', metavar='', help='Id of service.') +def do_service_delete(cs, args): + """Delete the service.""" + cs.services.delete(args.id) + + @utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') def do_fixed_ip_get(cs, args): """Retrieve info on a fixed ip.""" @@ -2574,6 +2642,15 @@ def do_credentials(cs, _args): utils.print_dict(catalog['access']['token'], "Token", wrap=int(_args.wrap)) +def do_extension_list(cs, _args): + """ + List all the os-api extensions that are available. + """ + extensions = cs.list_extensions.show_all() + fields = ["Name", "Summary", "Alias", "Version"] + utils.print_list(extensions, fields) + + @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('--port', dest='port', @@ -2777,49 +2854,6 @@ def do_quota_delete(cs, args): cs.quotas.delete(args.tenant) -@utils.arg('class_name', - metavar='', - help='Name of quota class to list the quotas for.') -def do_quota_class_show(cs, args): - """List the quotas for a quota class.""" - - _quota_show(cs.quota_classes.get(args.class_name)) - - -@utils.arg('class_name', - metavar='', - help='Name of quota class to set the quotas for.') -@utils.arg('--instances', - metavar='', - type=int, default=None, - help='New value for the "instances" quota.') -@utils.arg('--cores', - metavar='', - type=int, default=None, - help='New value for the "cores" quota.') -@utils.arg('--ram', - metavar='', - type=int, default=None, - help='New value for the "ram" quota.') -@utils.arg('--metadata-items', - metavar='', - type=int, - default=None, - help='New value for the "metadata-items" quota.') -@utils.arg('--metadata_items', - type=int, - help=argparse.SUPPRESS) -@utils.arg('--key-pairs', - metavar='', - type=int, - default=None, - help='New value for the "key-pairs" quota.') -def do_quota_class_update(cs, args): - """Update the quotas for a quota class.""" - - _quota_update(cs.quota_classes, args.class_name, args) - - @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('host', metavar='', help='Name or ID of target host.') @utils.arg('--password', diff --git a/awx/lib/site-packages/novaclient/v3/volumes.py b/awx/lib/site-packages/novaclient/v3/volumes.py index ec061a319c..53cc05f6c7 100644 --- a/awx/lib/site-packages/novaclient/v3/volumes.py +++ b/awx/lib/site-packages/novaclient/v3/volumes.py @@ -24,16 +24,23 @@ class VolumeManager(base.Manager): Manage :class:`Volume` resources. """ - def attach_server_volume(self, server, volume_id, device): + def attach_server_volume(self, server, volume_id, device, + disk_bus=None, device_type=None): """ Attach a volume identified by the volume ID to the given server ID :param server: The server (or it's ID) :param volume_id: The ID of the volume to attach. :param device: The device name + :param disk_bus: The disk bus of the volume + :param device_type: The device type of the volume :rtype: :class:`Volume` """ body = {'volume_id': volume_id, 'device': device} + if disk_bus: + body['disk_bus'] = disk_bus + if device_type: + body['device_type'] = device_type return self._action('attach', server, body) def update_server_volume(self, server, old_volume_id, new_volume_id):