diff --git a/awx/lib/site-packages/README b/awx/lib/site-packages/README index e5e8f2d5c8..c9376f750b 100644 --- a/awx/lib/site-packages/README +++ b/awx/lib/site-packages/README @@ -42,8 +42,8 @@ netaddr==0.7.14 (netaddr/*) os_client_config==0.6.0 (os_client_config/*) ordereddict==1.1 (ordereddict.py, needed for Python 2.6 support) os_diskconfig_python_novaclient_ext==0.1.2 (os_diskconfig_python_novaclient_ext/*) -os_networksv2_python_novaclient_ext==0.21 (os_networksv2_python_novaclient_ext.py) -os_virtual_interfacesv2_python_novaclient_ext==0.15 (os_virtual_interfacesv2_python_novaclient_ext.py) +os_networksv2_python_novaclient_ext==0.25 (os_networksv2_python_novaclient_ext.py) +os_virtual_interfacesv2_python_novaclient_ext==0.19 (os_virtual_interfacesv2_python_novaclient_ext.py) oslo.i18n==1.5.0 (oslo_i18n/* oslo.i18n/* which goes in oslo/i18n... created empty __init__.py at oslo/) oslo.config==1.9.3 (oslo_config/* oslo/config/* but not __init__.py from oslo/ used empty one instead) oslo.serialization==1.4.0 (oslo_serialization/* oslo/serialization/* but not __init__.py from oslo/ used empty one instead) @@ -59,7 +59,7 @@ python-dateutil==2.4.0 (dateutil/*) python-glanceclient==0.17.0 (glanceclient/*) python-ironicclient==0.5.0 (ironicclient/*) python-neutronclient==2.3.11 (neutronclient/*) -python-novaclient==2.18.1 (novaclient/*, excluded bin/nova) +python-novaclient==2.23.0 (novaclient/*, excluded bin/nova) python-swiftclient==2.2.0 (swiftclient/*, excluded bin/swift) python-troveclient==1.0.9 (troveclient/*) pytz==2014.10 (pytz/*) diff --git a/awx/lib/site-packages/novaclient/base.py b/awx/lib/site-packages/novaclient/base.py index a677e32296..01b4bac944 100644 --- a/awx/lib/site-packages/novaclient/base.py +++ b/awx/lib/site-packages/novaclient/base.py @@ -20,13 +20,17 @@ 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 from novaclient import exceptions from novaclient.openstack.common.apiclient import base -from novaclient import utils +from novaclient.openstack.common import cliutils Resource = base.Resource @@ -42,24 +46,17 @@ def getid(obj): return obj -class Manager(utils.HookableMixin): +class Manager(base.HookableMixin): """ Managers interact with a particular type of API (servers, flavors, images, etc.) and provide CRUD operations for them. """ 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) @@ -78,22 +75,86 @@ class Manager(utils.HookableMixin): except KeyError: pass - self._clear_completion_cache_for_class(obj_class) + with self.completion_cache('human_id', obj_class, mode="w"): + with self.completion_cache('uuid', obj_class, mode="w"): + return [obj_class(self, res, loaded=True) + for res in data if res] - objs = [] - for res in data: - if res: - obj = obj_class(self, res, loaded=True) - self._write_object_to_completion_cache(obj) - objs.append(obj) + @contextlib.contextmanager + def alternate_service_type(self, service_type): + original_service_type = self.api.client.service_type + self.api.client.service_type = service_type + try: + yield + finally: + self.api.client.service_type = original_service_type - return objs + @contextlib.contextmanager + def completion_cache(self, cache_type, obj_class, mode): + """ + The completion cache store items that can be used for bash + autocompletion, like UUIDs or human-friendly IDs. + + A resource listing will clear and repopulate the cache. + + 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 = cliutils.env('NOVACLIENT_UUID_CACHE_DIR', + default="~/.novaclient") + + # NOTE(sirp): Keep separate UUID caches for each username + + # endpoint pair + username = cliutils.env('OS_USERNAME', 'NOVA_USERNAME') + url = cliutils.env('OS_URL', 'NOVA_URL') + uniqifier = hashlib.md5(username.encode('utf-8') + + url.encode('utf-8')).hexdigest() + + cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier)) + + try: + os.makedirs(cache_dir, 0o755) + except OSError: + # NOTE(kiall): This is typically either permission denied while + # attempting to create the directory, or the + # directory already exists. Either way, don't + # fail. + pass + + resource = obj_class.__name__.lower() + filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-')) + path = os.path.join(cache_dir, filename) + + cache_attr = "_%s_cache" % cache_type + + try: + setattr(self, cache_attr, open(path, mode)) + except IOError: + # NOTE(kiall): This is typically a permission denied while + # attempting to write the cache file. + pass + + try: + yield + finally: + cache = getattr(self, cache_attr, None) + if cache: + cache.close() + delattr(self, cache_attr) + + def write_to_completion_cache(self, cache_type, val): + cache = getattr(self, "_%s_cache" % cache_type, None) + if cache: + cache.write("%s\n" % val) def _get(self, url, response_key): _resp, body = self.api.client.get(url) - obj = self.resource_class(self, body[response_key], loaded=True) - self._write_object_to_completion_cache(obj) - return obj + return self.resource_class(self, body[response_key], loaded=True) def _create(self, url, body, response_key, return_raw=False, **kwargs): self.run_hooks('modify_body_for_create', body, **kwargs) @@ -101,9 +162,9 @@ class Manager(utils.HookableMixin): if return_raw: return body[response_key] - obj = self.resource_class(self, body[response_key]) - self._write_object_to_completion_cache(obj) - return obj + with self.completion_cache('human_id', self.resource_class, mode="a"): + with self.completion_cache('uuid', self.resource_class, mode="a"): + return self.resource_class(self, body[response_key]) def _delete(self, url): _resp, _body = self.api.client.delete(url) @@ -131,9 +192,6 @@ class ManagerWithFind(Manager): def find(self, **kwargs): """ Find a single item with attributes matching ``**kwargs``. - - This isn't very efficient: it loads the entire list then filters on - the Python side. """ matches = self.findall(**kwargs) num_matches = len(matches) @@ -148,9 +206,6 @@ class ManagerWithFind(Manager): def findall(self, **kwargs): """ Find all items with attributes matching ``**kwargs``. - - This isn't very efficient: it loads the entire list then filters on - the Python side. """ found = [] searches = kwargs.items() @@ -173,6 +228,24 @@ class ManagerWithFind(Manager): del tmp_kwargs['is_public'] searches = tmp_kwargs.items() + if 'search_opts' in list_argspec.args: + # pass search_opts in to do server side based filtering. + # TODO(jogo) not all search_opts support regex, find way to + # identify when to use regex and when to use string matching. + # volumes does not support regex while servers does. So when + # doing findall on servers some client side filtering is still + # needed. + if "human_id" in kwargs: + list_kwargs['search_opts'] = {"name": kwargs["human_id"]} + elif "name" in kwargs: + list_kwargs['search_opts'] = {"name": kwargs["name"]} + elif "display_name" in kwargs: + list_kwargs['search_opts'] = {"name": kwargs["display_name"]} + if "all_tenants" in kwargs: + all_tenants = kwargs['all_tenants'] + list_kwargs['search_opts']['all_tenants'] = all_tenants + searches = [(k, v) for k, v in searches if k != 'all_tenants'] + listing = self.list(**list_kwargs) for obj in listing: @@ -204,11 +277,15 @@ class BootingManagerWithFind(ManagerWithFind): mapping_parts = mapping.split(':') source_id = mapping_parts[0] + bdm_dict['uuid'] = source_id + bdm_dict['boot_index'] = 0 if len(mapping_parts) == 1: bdm_dict['volume_id'] = source_id + bdm_dict['source_type'] = 'volume' elif len(mapping_parts) > 1: source_type = mapping_parts[1] + bdm_dict['source_type'] = source_type if source_type.startswith('snap'): bdm_dict['snapshot_id'] = source_id else: diff --git a/awx/lib/site-packages/novaclient/client.py b/awx/lib/site-packages/novaclient/client.py index de4a645974..32eb775054 100644 --- a/awx/lib/site-packages/novaclient/client.py +++ b/awx/lib/site-packages/novaclient/client.py @@ -20,14 +20,17 @@ OpenStack Client interface. Handles the REST calls and responses. """ -import errno +import copy import functools -import glob import hashlib import logging -import os +import re +import socket import time +from keystoneclient import adapter +from oslo.utils import importutils +from oslo.utils import netutils import requests from requests import adapters @@ -39,12 +42,19 @@ except ImportError: 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.i18n import _ from novaclient import service_catalog -from novaclient import utils -SENSITIVE_HEADERS = ('X-Auth-Token',) + +class TCPKeepAliveAdapter(adapters.HTTPAdapter): + """The custom adapter used to set TCP Keep-Alive on all connections.""" + def init_poolmanager(self, *args, **kwargs): + if requests.__version__ >= '2.4.1': + kwargs.setdefault('socket_options', [ + (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), + (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), + ]) + super(TCPKeepAliveAdapter, self).init_poolmanager(*args, **kwargs) class _ClientConnectionPool(object): @@ -57,140 +67,41 @@ class _ClientConnectionPool(object): Store and reuse HTTP adapters per Service URL. """ if url not in self._adapters: - self._adapters[url] = adapters.HTTPAdapter() + self._adapters[url] = TCPKeepAliveAdapter() return self._adapters[url] -class CompletionCache(object): - """The completion cache is how we support tab-completion with novaclient. +class SessionClient(adapter.LegacyJsonAdapter): - The `Manager` writes object IDs and Human-IDs to the completion-cache on - object-show, object-list, and object-create calls. - - The `nova.bash_completion` script then uses these files to provide the - actual tab-completion. - - The cache directory layout is: - - ~/.novaclient/ - / - -id-cache - -human-id-cache - """ - def __init__(self, username, auth_url, attributes=('id', 'human_id')): - self.directory = self._make_directory_name(username, auth_url) - self.attributes = attributes - - def _make_directory_name(self, username, auth_url): - """Creates a unique directory name based on the auth_url and username - of the current user. - """ - uniqifier = hashlib.md5(username.encode('utf-8') + - auth_url.encode('utf-8')).hexdigest() - base_dir = utils.env('NOVACLIENT_UUID_CACHE_DIR', - default="~/.novaclient") - return os.path.expanduser(os.path.join(base_dir, uniqifier)) - - def _prepare_directory(self): - try: - os.makedirs(self.directory, 0o755) - except OSError: - # NOTE(kiall): This is typically either permission denied while - # attempting to create the directory, or the - # directory already exists. Either way, don't - # fail. - pass - - def clear_class(self, obj_class): - self._prepare_directory() - - resource = obj_class.__name__.lower() - resource_glob = os.path.join(self.directory, "%s-*-cache" % resource) - - for filename in glob.iglob(resource_glob): - try: - os.unlink(filename) - except OSError as e: - if e.errno != errno.ENOENT: - raise - - def _write_attribute(self, resource, attribute, value): - self._prepare_directory() - - filename = "%s-%s-cache" % (resource, attribute.replace('_', '-')) - path = os.path.join(self.directory, filename) - - with open(path, 'a') as f: - f.write("%s\n" % value) - - def write_object(self, obj): - resource = obj.__class__.__name__.lower() - - for attribute in self.attributes: - value = getattr(obj, attribute, None) - if value: - self._write_attribute(resource, attribute, value) - - -class 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 __init__(self, *args, **kwargs): + self.times = [] + super(SessionClient, self).__init__(*args, **kwargs) def request(self, url, method, **kwargs): - kwargs.setdefault('user_agent', 'python-novaclient') - kwargs.setdefault('auth', self.auth) - kwargs.setdefault('authenticated', False) + # NOTE(jamielennox): The standard call raises errors from + # keystoneclient, where we need to raise the novaclient errors. + raise_exc = kwargs.pop('raise_exc', True) + start_time = time.time() + resp, body = super(SessionClient, self).request(url, + method, + raise_exc=False, + **kwargs) - try: - kwargs['json'] = kwargs.pop('body') - except KeyError: - pass + end_time = time.time() + self.times.append(('%s %s' % (method, url), + start_time, end_time)) - 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: + if raise_exc and resp.status_code >= 400: raise exceptions.from_response(resp, body, url, method) return resp, body - def _cs_request(self, url, method, **kwargs): - # this function is mostly redundant but makes compatibility easier - kwargs.setdefault('authenticated', True) - return self.request(url, method, **kwargs) + def get_timings(self): + return self.times - 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 reset_timings(self): + self.times = [] def _original_only(f): @@ -234,7 +145,7 @@ class HTTPClient(object): self.tenant_id = tenant_id self._connection_pool = (_ClientConnectionPool() - if connection_pool else None) + if connection_pool else None) # This will be called by #_get_password if self.password is None. # EG if a password can only be obtained by prompting the user, but a @@ -301,6 +212,11 @@ class HTTPClient(object): # otherwise we will get all the requests logging messages rql.setLevel(logging.WARNING) + # NOTE(melwitt): Service catalog is only set if bypass_url isn't + # used. Otherwise, we can cache using services_url. + self.service_catalog = None + self.services_url = {} + def use_token_cache(self, use_it): self.os_cache = use_it @@ -318,21 +234,47 @@ 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 _redact(self, target, path, text=None): + """Replace the value of a key in `target`. + + The key can be at the top level by specifying a list with a single + key as the path. Nested dictionaries are also supported by passing a + list of keys to be navigated to find the one that should be replaced. + In this case the last one is the one that will be replaced. + + :param dict target: the dictionary that may have a key to be redacted; + modified in place + :param list path: a list representing the nested structure in `target` + that should be redacted; modified in place + :param string text: optional text to use as a replacement for the + redacted key. if text is not specified, the + default text will be sha1 hash of the value being + redacted + """ + + key = path.pop() + + # move to the most nested dict + for p in path: + try: + target = target[p] + except KeyError: + return + + if key in target: + if text: + target[key] = text + else: + # because in python3 byte string handling is ... ug + value = target[key].encode('utf-8') + sha1sum = hashlib.sha1(value) + target[key] = "{SHA1}%s" % sha1sum.hexdigest() def http_log_req(self, method, url, kwargs): if not self.http_log_debug: return - string_parts = ['curl -i'] + string_parts = ['curl -g -i'] if not kwargs.get('verify', True): string_parts.append(' --insecure') @@ -340,24 +282,38 @@ class HTTPClient(object): string_parts.append(" '%s'" % url) string_parts.append(' -X %s' % method) + headers = copy.deepcopy(kwargs['headers']) + self._redact(headers, ['X-Auth-Token']) # because dict ordering changes from 2 to 3 - keys = sorted(kwargs['headers'].keys()) + keys = sorted(headers.keys()) for name in keys: - value = kwargs['headers'][name] - header = ' -H "%s: %s"' % self.safe_header(name, value) + value = headers[name] + header = ' -H "%s: %s"' % (name, value) string_parts.append(header) if 'data' in kwargs: - string_parts.append(" -d '%s'" % (kwargs['data'])) + data = json.loads(kwargs['data']) + self._redact(data, ['auth', 'passwordCredentials', 'password']) + string_parts.append(" -d '%s'" % json.dumps(data)) self._logger.debug("REQ: %s" % "".join(string_parts)) def http_log_resp(self, resp): if not self.http_log_debug: return + + if resp.text and resp.status_code != 400: + try: + body = json.loads(resp.text) + self._redact(body, ['access', 'token', 'id']) + except ValueError: + body = None + else: + body = None + self._logger.debug("RESP: [%(status)s] %(headers)s\nRESP BODY: " "%(text)s\n", {'status': resp.status_code, 'headers': resp.headers, - 'text': resp.text}) + 'text': json.dumps(body)}) def open_session(self): if not self._connection_pool: @@ -379,10 +335,10 @@ class HTTPClient(object): self._session.close() self._current_url = service_url self._logger.debug( - "New session created for: (%s)" % service_url) + "New session created for: (%s)" % service_url) self._session = requests.Session() self._session.mount(service_url, - self._connection_pool.get(service_url)) + self._connection_pool.get(service_url)) return self._session elif self._session: return self._session @@ -422,7 +378,7 @@ class HTTPClient(object): # or 'actively refused' in the body, so that's what we'll do. if resp.status_code == 400: if ('Connection refused' in resp.text or - 'actively refused' in resp.text): + 'actively refused' in resp.text): raise exceptions.ConnectionRefused(resp.text) try: body = json.loads(resp.text) @@ -446,6 +402,20 @@ class HTTPClient(object): def _cs_request(self, url, method, **kwargs): if not self.management_url: self.authenticate() + if url is None: + # To get API version information, it is necessary to GET + # a nova endpoint directly without "v2/". + magic_tuple = parse.urlsplit(self.management_url) + scheme, netloc, path, query, frag = magic_tuple + path = re.sub(r'v[1-9]/[a-z0-9]+$', '', path) + url = parse.urlunsplit((scheme, netloc, path, None, None)) + else: + if self.service_catalog: + url = self.get_service_url(self.service_type) + url + else: + # NOTE(melwitt): The service catalog is not available + # when bypass_url is used. + url = self.management_url + url # Perform the request once. If we get a 401 back then it # might be because the auth token expired, so try to @@ -455,8 +425,7 @@ class HTTPClient(object): if self.projectid: kwargs['headers']['X-Auth-Project-Id'] = self.projectid - resp, body = self._time_request(self.management_url + url, method, - **kwargs) + resp, body = self._time_request(url, method, **kwargs) return resp, body except exceptions.Unauthorized as e: try: @@ -467,8 +436,7 @@ class HTTPClient(object): self.keyring_saved = False self.authenticate() kwargs['headers']['X-Auth-Token'] = self.auth_token - resp, body = self._time_request(self.management_url + url, - method, **kwargs) + resp, body = self._time_request(url, method, **kwargs) return resp, body except exceptions.Unauthorized: raise e @@ -490,6 +458,19 @@ class HTTPClient(object): def delete(self, url, **kwargs): return self._cs_request(url, 'DELETE', **kwargs) + def get_service_url(self, service_type): + if service_type not in self.services_url: + url = self.service_catalog.url_for( + attr='region', + filter_value=self.region_name, + endpoint_type=self.endpoint_type, + service_type=service_type, + service_name=self.service_name, + volume_service_name=self.volume_service_name,) + url = url.rstrip('/') + self.services_url[service_type] = url + return self.services_url[service_type] + def _extract_service_catalog(self, url, resp, body, extract_token=True): """See what the auth service told us and process the response. We may get redirected to another site, fail or actually get @@ -506,14 +487,7 @@ class HTTPClient(object): self.auth_token = self.service_catalog.get_token() self.tenant_id = self.service_catalog.get_tenant_id() - management_url = self.service_catalog.url_for( - attr='region', - filter_value=self.region_name, - endpoint_type=self.endpoint_type, - service_type=self.service_type, - service_name=self.service_name, - volume_service_name=self.volume_service_name,) - self.management_url = management_url.rstrip('/') + self.management_url = self.get_service_url(self.service_type) return None except exceptions.AmbiguousEndpoints: print(_("Found more than one valid endpoint. Use a more " @@ -553,7 +527,11 @@ class HTTPClient(object): extract_token=False) def authenticate(self): - magic_tuple = network_utils.urlsplit(self.auth_url) + if not self.auth_url: + msg = _("Authentication requires 'auth_url', which should be " + "specified in '%s'") % self.__class__.__name__ + raise exceptions.AuthorizationFailure(msg) + magic_tuple = netutils.urlsplit(self.auth_url) scheme, netloc, path, query, frag = magic_tuple port = magic_tuple.port if port is None: @@ -698,13 +676,17 @@ def _construct_http_client(username=None, password=None, project_id=None, auth_system='keystone', auth_plugin=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, connection_pool=False, session=None, - auth=None): + auth=None, user_agent='python-novaclient', + interface=None, **kwargs): if session: return SessionClient(session=session, auth=auth, - interface=endpoint_type, + interface=interface or endpoint_type, service_type=service_type, - region_name=region_name) + region_name=region_name, + service_name=service_name, + user_agent=user_agent, + **kwargs) else: # FIXME(jamielennox): username and password are now optional. Need # to test that they were provided in this mode. @@ -736,9 +718,9 @@ def _construct_http_client(username=None, password=None, project_id=None, def get_client_class(version): version_map = { - '1.1': 'novaclient.v1_1.client.Client', - '2': 'novaclient.v1_1.client.Client', - '3': 'novaclient.v3.client.Client', + '1.1': 'novaclient.v2.client.Client', + '2': 'novaclient.v2.client.Client', + '3': 'novaclient.v2.client.Client', } try: client_path = version_map[str(version)] @@ -748,7 +730,7 @@ def get_client_class(version): 'keys': ', '.join(version_map.keys())} raise exceptions.UnsupportedVersion(msg) - return utils.import_class(client_path) + return importutils.import_class(client_path) def Client(version, *args, **kwargs): diff --git a/awx/lib/site-packages/novaclient/crypto.py b/awx/lib/site-packages/novaclient/crypto.py index d817be5bbe..21af60cdc4 100644 --- a/awx/lib/site-packages/novaclient/crypto.py +++ b/awx/lib/site-packages/novaclient/crypto.py @@ -22,7 +22,7 @@ class DecryptionFailure(Exception): def decrypt_password(private_key, password): - """Base64 decodes password and unecrypts it with private key. + """Base64 decodes password and unencrypts it with private key. Requires openssl binary available in the path. """ diff --git a/awx/lib/site-packages/novaclient/exceptions.py b/awx/lib/site-packages/novaclient/exceptions.py index cc3133bdcf..d550b754b6 100644 --- a/awx/lib/site-packages/novaclient/exceptions.py +++ b/awx/lib/site-packages/novaclient/exceptions.py @@ -105,6 +105,19 @@ class ClientException(Exception): return formatted_string +class RetryAfterException(ClientException): + """ + The base exception class for ClientExceptions that use Retry-After header. + """ + def __init__(self, *args, **kwargs): + try: + self.retry_after = int(kwargs.pop('retry_after')) + except (KeyError, ValueError): + self.retry_after = 0 + + super(RetryAfterException, self).__init__(*args, **kwargs) + + class BadRequest(ClientException): """ HTTP 400 - Bad request: you sent some malformed data. @@ -154,23 +167,15 @@ class Conflict(ClientException): message = "Conflict" -class OverLimit(ClientException): +class OverLimit(RetryAfterException): """ HTTP 413 - Over limit: you're over the API limits for this time period. """ http_status = 413 message = "Over limit" - def __init__(self, *args, **kwargs): - try: - self.retry_after = int(kwargs.pop('retry_after')) - except (KeyError, ValueError): - self.retry_after = 0 - super(OverLimit, self).__init__(*args, **kwargs) - - -class RateLimit(OverLimit): +class RateLimit(RetryAfterException): """ HTTP 429 - Rate limit: you've sent too many requests for this time period. """ @@ -220,6 +225,8 @@ def from_response(response, body, url, method=None): if resp.status_code != 200: raise exception_from_response(resp, rest.text) """ + cls = _code_map.get(response.status_code, ClientException) + kwargs = { 'code': response.status_code, 'method': method, @@ -230,7 +237,8 @@ def from_response(response, body, url, method=None): if response.headers: kwargs['request_id'] = response.headers.get('x-compute-request-id') - if 'retry-after' in response.headers: + if (issubclass(cls, RetryAfterException) and + 'retry-after' in response.headers): kwargs['retry_after'] = response.headers.get('retry-after') if body: @@ -245,5 +253,9 @@ def from_response(response, body, url, method=None): kwargs['message'] = message kwargs['details'] = details - cls = _code_map.get(response.status_code, ClientException) return cls(**kwargs) + + +class ResourceNotFound(Exception): + """Error in getting the resource.""" + pass diff --git a/awx/lib/site-packages/novaclient/extension.py b/awx/lib/site-packages/novaclient/extension.py index ac105070a6..9cfcc13d9e 100644 --- a/awx/lib/site-packages/novaclient/extension.py +++ b/awx/lib/site-packages/novaclient/extension.py @@ -14,10 +14,11 @@ # under the License. from novaclient import base +from novaclient.openstack.common.apiclient import base as common_base from novaclient import utils -class Extension(utils.HookableMixin): +class Extension(common_base.HookableMixin): """Extension descriptor.""" SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') diff --git a/awx/lib/site-packages/novaclient/i18n.py b/awx/lib/site-packages/novaclient/i18n.py new file mode 100644 index 0000000000..e9d39d4ab8 --- /dev/null +++ b/awx/lib/site-packages/novaclient/i18n.py @@ -0,0 +1,35 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""oslo.i18n integration module for novaclient. + +See http://docs.openstack.org/developer/oslo.i18n/usage.html . + +""" + +from oslo import i18n + + +_translators = i18n.TranslatorFactory(domain='novaclient') + +# The primary translation function using the well-known name "_" +_ = _translators.primary + +# Translators for log levels. +# +# The abbreviated names are meant to reflect the usual use of a short +# name like '_'. The "L" is for "log" and the other letter comes from +# the level. +_LI = _translators.log_info +_LW = _translators.log_warning +_LE = _translators.log_error +_LC = _translators.log_critical diff --git a/awx/lib/site-packages/novaclient/openstack/common/__init__.py b/awx/lib/site-packages/novaclient/openstack/common/__init__.py index d1223eaf76..e69de29bb2 100644 --- a/awx/lib/site-packages/novaclient/openstack/common/__init__.py +++ b/awx/lib/site-packages/novaclient/openstack/common/__init__.py @@ -1,17 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import six - - -six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox')) diff --git a/awx/lib/site-packages/novaclient/openstack/common/_i18n.py b/awx/lib/site-packages/novaclient/openstack/common/_i18n.py new file mode 100644 index 0000000000..6ae2ff239b --- /dev/null +++ b/awx/lib/site-packages/novaclient/openstack/common/_i18n.py @@ -0,0 +1,40 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""oslo.i18n integration module. + +See http://docs.openstack.org/developer/oslo.i18n/usage.html + +""" + +import oslo.i18n + + +# NOTE(dhellmann): This reference to o-s-l-o will be replaced by the +# application name when this module is synced into the separate +# repository. It is OK to have more than one translation function +# using the same domain, since there will still only be one message +# catalog. +_translators = oslo.i18n.TranslatorFactory(domain='novaclient') + +# The primary translation function using the well-known name "_" +_ = _translators.primary + +# Translators for log levels. +# +# The abbreviated names are meant to reflect the usual use of a short +# name like '_'. The "L" is for "log" and the other letter comes from +# the level. +_LI = _translators.log_info +_LW = _translators.log_warning +_LE = _translators.log_error +_LC = _translators.log_critical diff --git a/awx/lib/site-packages/novaclient/openstack/common/apiclient/base.py b/awx/lib/site-packages/novaclient/openstack/common/apiclient/base.py index 1f90b88e92..cc5c8b005a 100644 --- a/awx/lib/site-packages/novaclient/openstack/common/apiclient/base.py +++ b/awx/lib/site-packages/novaclient/openstack/common/apiclient/base.py @@ -26,12 +26,12 @@ Base utilities to build API operation managers and objects on top of. import abc import copy +from oslo.utils import strutils import six from six.moves.urllib import parse +from novaclient.openstack.common._i18n import _ from novaclient.openstack.common.apiclient import exceptions -from novaclient.openstack.common.gettextutils import _ -from novaclient.openstack.common import strutils def getid(obj): @@ -99,12 +99,13 @@ class BaseManager(HookableMixin): super(BaseManager, self).__init__() self.client = client - def _list(self, url, response_key, obj_class=None, json=None): + def _list(self, url, response_key=None, obj_class=None, json=None): """List the collection. :param url: a partial URL, e.g., '/servers' :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' + e.g., 'servers'. If response_key is None - all response body + will be used. :param obj_class: class for constructing the returned objects (self.resource_class will be used by default) :param json: data that will be encoded as JSON and passed in POST @@ -118,7 +119,7 @@ class BaseManager(HookableMixin): if obj_class is None: obj_class = self.resource_class - data = body[response_key] + data = body[response_key] if response_key is not None else body # NOTE(ja): keystone returns values as list as {'values': [ ... ]} # unlike other services which just return the list... try: @@ -128,15 +129,17 @@ class BaseManager(HookableMixin): return [obj_class(self, res, loaded=True) for res in data if res] - def _get(self, url, response_key): + def _get(self, url, response_key=None): """Get an object from collection. :param url: a partial URL, e.g., '/servers' :param response_key: the key to be looked up in response dictionary, - e.g., 'server' + e.g., 'server'. If response_key is None - all response body + will be used. """ body = self.client.get(url).json() - return self.resource_class(self, body[response_key], loaded=True) + data = body[response_key] if response_key is not None else body + return self.resource_class(self, data, loaded=True) def _head(self, url): """Retrieve request headers for an object. @@ -146,21 +149,23 @@ class BaseManager(HookableMixin): resp = self.client.head(url) return resp.status_code == 204 - def _post(self, url, json, response_key, return_raw=False): + def _post(self, url, json, response_key=None, return_raw=False): """Create an object. :param url: a partial URL, e.g., '/servers' :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' + e.g., 'server'. If response_key is None - all response body + will be used. :param return_raw: flag to force returning raw JSON instead of Python object of self.resource_class """ body = self.client.post(url, json=json).json() + data = body[response_key] if response_key is not None else body if return_raw: - return body[response_key] - return self.resource_class(self, body[response_key]) + return data + return self.resource_class(self, data) def _put(self, url, json=None, response_key=None): """Update an object with PUT method. @@ -169,7 +174,8 @@ class BaseManager(HookableMixin): :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' + e.g., 'servers'. If response_key is None - all response body + will be used. """ resp = self.client.put(url, json=json) # PUT requests may not return a body @@ -187,7 +193,8 @@ class BaseManager(HookableMixin): :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' + e.g., 'servers'. If response_key is None - all response body + will be used. """ body = self.client.patch(url, json=json).json() if response_key is not None: @@ -465,7 +472,7 @@ class Resource(object): def __getattr__(self, k): if k not in self.__dict__: - #NOTE(bcwaldon): disallow lazy-loading if already loaded once + # NOTE(bcwaldon): disallow lazy-loading if already loaded once if not self.is_loaded(): self.get() return self.__getattr__(k) diff --git a/awx/lib/site-packages/novaclient/openstack/common/apiclient/client.py b/awx/lib/site-packages/novaclient/openstack/common/apiclient/client.py index a3666daf0a..c591850e68 100644 --- a/awx/lib/site-packages/novaclient/openstack/common/apiclient/client.py +++ b/awx/lib/site-packages/novaclient/openstack/common/apiclient/client.py @@ -33,11 +33,11 @@ try: except ImportError: import json +from oslo.utils import importutils import requests +from novaclient.openstack.common._i18n import _ from novaclient.openstack.common.apiclient import exceptions -from novaclient.openstack.common.gettextutils import _ -from novaclient.openstack.common import importutils _logger = logging.getLogger(__name__) @@ -104,7 +104,7 @@ class HTTPClient(object): return string_parts = [ - "curl -i", + "curl -g -i", "-X '%s'" % method, "'%s'" % url, ] @@ -156,7 +156,7 @@ class HTTPClient(object): requests.Session.request (such as `headers`) or `json` that will be encoded as JSON and used as `data` argument """ - kwargs.setdefault("headers", kwargs.get("headers", {})) + kwargs.setdefault("headers", {}) kwargs["headers"]["User-Agent"] = self.user_agent if self.original_ip: kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % ( @@ -247,6 +247,10 @@ class HTTPClient(object): raise self.cached_token = None client.cached_endpoint = None + if self.auth_plugin.opts.get('token'): + self.auth_plugin.opts['token'] = None + if self.auth_plugin.opts.get('endpoint'): + self.auth_plugin.opts['endpoint'] = None self.authenticate() try: token, endpoint = self.auth_plugin.token_and_endpoint( @@ -357,8 +361,7 @@ class BaseClient(object): "Must be one of: %(version_map)s") % { 'api_name': api_name, 'version': version, - 'version_map': ', '.join(version_map.keys()) - } + 'version_map': ', '.join(version_map.keys())} raise exceptions.UnsupportedVersion(msg) return importutils.import_class(client_path) diff --git a/awx/lib/site-packages/novaclient/openstack/common/apiclient/exceptions.py b/awx/lib/site-packages/novaclient/openstack/common/apiclient/exceptions.py index 85b123ef8c..15e885b9bc 100644 --- a/awx/lib/site-packages/novaclient/openstack/common/apiclient/exceptions.py +++ b/awx/lib/site-packages/novaclient/openstack/common/apiclient/exceptions.py @@ -25,7 +25,7 @@ import sys import six -from novaclient.openstack.common.gettextutils import _ +from novaclient.openstack.common._i18n import _ class ClientException(Exception): @@ -34,14 +34,6 @@ class ClientException(Exception): pass -class MissingArgs(ClientException): - """Supplied arguments are not sufficient for calling a function.""" - def __init__(self, missing): - self.missing = missing - msg = _("Missing arguments: %s") % ", ".join(missing) - super(MissingArgs, self).__init__(msg) - - class ValidationError(ClientException): """Error in validation on API client side.""" pass @@ -57,11 +49,6 @@ class CommandError(ClientException): pass -class ResourceNotFound(ClientException): - """Error in getting the resource.""" - pass - - class AuthorizationFailure(ClientException): """Cannot authorize API client.""" pass @@ -432,7 +419,7 @@ def from_response(response, method, url): """ req_id = response.headers.get("x-openstack-request-id") - #NOTE(hdd) true for older versions of nova and cinder + # NOTE(hdd) true for older versions of nova and cinder if not req_id: req_id = response.headers.get("x-compute-request-id") kwargs = { @@ -452,8 +439,8 @@ def from_response(response, method, url): except ValueError: pass else: - if isinstance(body, dict): - error = list(body.values())[0] + if isinstance(body, dict) and isinstance(body.get("error"), dict): + error = body["error"] kwargs["message"] = error.get("message") kwargs["details"] = error.get("details") elif content_type.startswith("text/"): diff --git a/awx/lib/site-packages/novaclient/openstack/common/apiclient/fake_client.py b/awx/lib/site-packages/novaclient/openstack/common/apiclient/fake_client.py index cdb3cc1c9d..eeb9b810a7 100644 --- a/awx/lib/site-packages/novaclient/openstack/common/apiclient/fake_client.py +++ b/awx/lib/site-packages/novaclient/openstack/common/apiclient/fake_client.py @@ -33,7 +33,9 @@ from six.moves.urllib import parse from novaclient.openstack.common.apiclient import client -def assert_has_keys(dct, required=[], optional=[]): +def assert_has_keys(dct, required=None, optional=None): + required = required or [] + optional = optional or [] for k in required: try: assert k in dct @@ -79,7 +81,7 @@ class FakeHTTPClient(client.HTTPClient): def __init__(self, *args, **kwargs): self.callstack = [] self.fixtures = kwargs.pop("fixtures", None) or {} - if not args and not "auth_plugin" in kwargs: + if not args and "auth_plugin" not in kwargs: args = (None, ) super(FakeHTTPClient, self).__init__(*args, **kwargs) diff --git a/awx/lib/site-packages/novaclient/openstack/common/apiclient/utils.py b/awx/lib/site-packages/novaclient/openstack/common/apiclient/utils.py new file mode 100644 index 0000000000..09ddae43be --- /dev/null +++ b/awx/lib/site-packages/novaclient/openstack/common/apiclient/utils.py @@ -0,0 +1,87 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo.utils import encodeutils +import six + +from novaclient.openstack.common._i18n import _ +from novaclient.openstack.common.apiclient import exceptions +from novaclient.openstack.common import uuidutils + + +def find_resource(manager, name_or_id, **find_args): + """Look for resource in a given manager. + + Used as a helper for the _find_* methods. + Example: + + .. code-block:: python + + def _find_hypervisor(cs, hypervisor): + #Get a hypervisor by name or ID. + return cliutils.find_resource(cs.hypervisors, hypervisor) + """ + # first try to get entity as integer id + try: + return manager.get(int(name_or_id)) + except (TypeError, ValueError, exceptions.NotFound): + pass + + # now try to get entity as uuid + try: + if six.PY2: + tmp_id = encodeutils.safe_encode(name_or_id) + else: + tmp_id = encodeutils.safe_decode(name_or_id) + + if uuidutils.is_uuid_like(tmp_id): + return manager.get(tmp_id) + except (TypeError, ValueError, exceptions.NotFound): + pass + + # for str id which is not uuid + if getattr(manager, 'is_alphanum_id_allowed', False): + try: + return manager.get(name_or_id) + except exceptions.NotFound: + pass + + try: + try: + return manager.find(human_id=name_or_id, **find_args) + except exceptions.NotFound: + pass + + # finally try to find entity by name + try: + resource = getattr(manager, 'resource_class', None) + name_attr = resource.NAME_ATTR if resource else 'name' + kwargs = {name_attr: name_or_id} + kwargs.update(find_args) + return manager.find(**kwargs) + except exceptions.NotFound: + msg = _("No %(name)s with a name or " + "ID of '%(name_or_id)s' exists.") % \ + { + "name": manager.resource_class.__name__.lower(), + "name_or_id": name_or_id + } + raise exceptions.CommandError(msg) + except exceptions.NoUniqueMatch: + msg = _("Multiple %(name)s matches found for " + "'%(name_or_id)s', use an ID to be more specific.") % \ + { + "name": manager.resource_class.__name__.lower(), + "name_or_id": name_or_id + } + raise exceptions.CommandError(msg) diff --git a/awx/lib/site-packages/novaclient/openstack/common/cliutils.py b/awx/lib/site-packages/novaclient/openstack/common/cliutils.py index 6a96da57b2..ac2cf75734 100644 --- a/awx/lib/site-packages/novaclient/openstack/common/cliutils.py +++ b/awx/lib/site-packages/novaclient/openstack/common/cliutils.py @@ -24,14 +24,21 @@ import os import sys import textwrap +from oslo.utils import encodeutils +from oslo.utils import strutils import prettytable import six from six import moves -from novaclient.openstack.common.apiclient import exceptions -from novaclient.openstack.common.gettextutils import _ -from novaclient.openstack.common import strutils -from novaclient.openstack.common import uuidutils +from novaclient.openstack.common._i18n import _ + + +class MissingArgs(Exception): + """Supplied arguments are not sufficient for calling a function.""" + def __init__(self, missing): + self.missing = missing + msg = _("Missing arguments: %s") % ", ".join(missing) + super(MissingArgs, self).__init__(msg) def validate_args(fn, *args, **kwargs): @@ -56,7 +63,7 @@ def validate_args(fn, *args, **kwargs): required_args = argspec.args[:len(argspec.args) - num_defaults] def isbound(method): - return getattr(method, 'im_self', None) is not None + return getattr(method, '__self__', None) is not None if isbound(fn): required_args.pop(0) @@ -64,7 +71,7 @@ def validate_args(fn, *args, **kwargs): missing = [arg for arg in required_args if arg not in kwargs] missing = missing[len(args):] if missing: - raise exceptions.MissingArgs(missing) + raise MissingArgs(missing) def arg(*args, **kwargs): @@ -132,7 +139,7 @@ def isunauthenticated(func): def print_list(objs, fields, formatters=None, sortby_index=0, - mixed_case_fields=None): + mixed_case_fields=None, field_labels=None): """Print a list or objects as a table, one row per object. :param objs: iterable of :class:`Resource` @@ -141,14 +148,22 @@ def print_list(objs, fields, formatters=None, sortby_index=0, :param sortby_index: index of the field for sorting table rows :param mixed_case_fields: fields corresponding to object attributes that have mixed case names (e.g., 'serverId') + :param field_labels: Labels to use in the heading of the table, default to + fields. """ formatters = formatters or {} mixed_case_fields = mixed_case_fields or [] + field_labels = field_labels or fields + if len(field_labels) != len(fields): + raise ValueError(_("Field labels list %(labels)s has different number " + "of elements than fields list %(fields)s"), + {'labels': field_labels, 'fields': fields}) + if sortby_index is None: kwargs = {} else: - kwargs = {'sortby': fields[sortby_index]} - pt = prettytable.PrettyTable(fields, caching=False) + kwargs = {'sortby': field_labels[sortby_index]} + pt = prettytable.PrettyTable(field_labels) pt.align = 'l' for o in objs: @@ -165,7 +180,7 @@ def print_list(objs, fields, formatters=None, sortby_index=0, row.append(data) pt.add_row(row) - print(strutils.safe_encode(pt.get_string(**kwargs))) + print(encodeutils.safe_encode(pt.get_string(**kwargs))) def print_dict(dct, dict_property="Property", wrap=0): @@ -175,7 +190,7 @@ def print_dict(dct, dict_property="Property", wrap=0): :param dict_property: name of the first column :param wrap: wrapping for the second column """ - pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False) + pt = prettytable.PrettyTable([dict_property, 'Value']) pt.align = 'l' for k, v in six.iteritems(dct): # convert dict to str to check length @@ -193,7 +208,7 @@ def print_dict(dct, dict_property="Property", wrap=0): col1 = '' else: pt.add_row([k, v]) - print(strutils.safe_encode(pt.get_string())) + print(encodeutils.safe_encode(pt.get_string())) def get_password(max_password_prompts=3): @@ -217,76 +232,16 @@ def get_password(max_password_prompts=3): return pw -def find_resource(manager, name_or_id, **find_args): - """Look for resource in a given manager. - - Used as a helper for the _find_* methods. - Example: - - def _find_hypervisor(cs, hypervisor): - #Get a hypervisor by name or ID. - return cliutils.find_resource(cs.hypervisors, hypervisor) - """ - # first try to get entity as integer id - try: - return manager.get(int(name_or_id)) - except (TypeError, ValueError, exceptions.NotFound): - pass - - # now try to get entity as uuid - try: - tmp_id = strutils.safe_encode(name_or_id) - - if uuidutils.is_uuid_like(tmp_id): - return manager.get(tmp_id) - except (TypeError, ValueError, exceptions.NotFound): - pass - - # for str id which is not uuid - if getattr(manager, 'is_alphanum_id_allowed', False): - try: - return manager.get(name_or_id) - except exceptions.NotFound: - pass - - try: - try: - return manager.find(human_id=name_or_id, **find_args) - except exceptions.NotFound: - pass - - # finally try to find entity by name - try: - resource = getattr(manager, 'resource_class', None) - name_attr = resource.NAME_ATTR if resource else 'name' - kwargs = {name_attr: name_or_id} - kwargs.update(find_args) - return manager.find(**kwargs) - except exceptions.NotFound: - msg = _("No %(name)s with a name or " - "ID of '%(name_or_id)s' exists.") % \ - { - "name": manager.resource_class.__name__.lower(), - "name_or_id": name_or_id - } - raise exceptions.CommandError(msg) - except exceptions.NoUniqueMatch: - msg = _("Multiple %(name)s matches found for " - "'%(name_or_id)s', use an ID to be more specific.") % \ - { - "name": manager.resource_class.__name__.lower(), - "name_or_id": name_or_id - } - raise exceptions.CommandError(msg) - - def service_type(stype): """Adds 'service_type' attribute to decorated function. Usage: - @service_type('volume') - def mymethod(f): - ... + + .. code-block:: python + + @service_type('volume') + def mymethod(f): + ... """ def inner(f): f.service_type = stype diff --git a/awx/lib/site-packages/novaclient/service_catalog.py b/awx/lib/site-packages/novaclient/service_catalog.py index 26f4ccc5cc..6883f537bd 100644 --- a/awx/lib/site-packages/novaclient/service_catalog.py +++ b/awx/lib/site-packages/novaclient/service_catalog.py @@ -32,8 +32,8 @@ class ServiceCatalog(object): return self.catalog['access']['token']['tenant']['id'] def url_for(self, attr=None, filter_value=None, - service_type=None, endpoint_type='publicURL', - service_name=None, volume_service_name=None): + service_type=None, endpoint_type='publicURL', + service_name=None, volume_service_name=None): """Fetch the public URL from the Compute service for a particular endpoint attribute. If none given, return the first. See tests for sample service catalog. @@ -72,11 +72,11 @@ class ServiceCatalog(object): endpoints = service['endpoints'] for endpoint in endpoints: # Ignore 1.0 compute endpoints - if service.get("type") == 'compute' and \ - endpoint.get('versionId', '2') not in ('1.1', '2'): + if (service.get("type") == 'compute' and + endpoint.get('versionId', '2') not in ('1.1', '2')): continue - if not filter_value or \ - endpoint.get(attr).lower() == filter_value.lower(): + if (not filter_value or + endpoint.get(attr).lower() == filter_value.lower()): endpoint["serviceName"] = service.get("name") matching_endpoints.append(endpoint) @@ -84,6 +84,6 @@ class ServiceCatalog(object): raise novaclient.exceptions.EndpointNotFound() elif len(matching_endpoints) > 1: raise novaclient.exceptions.AmbiguousEndpoints( - endpoints=matching_endpoints) + endpoints=matching_endpoints) else: return matching_endpoints[0][endpoint_type] diff --git a/awx/lib/site-packages/novaclient/shell.py b/awx/lib/site-packages/novaclient/shell.py index 08f5e8c401..652713ba63 100644 --- a/awx/lib/site-packages/novaclient/shell.py +++ b/awx/lib/site-packages/novaclient/shell.py @@ -28,7 +28,14 @@ import logging import os import pkgutil import sys +import time +from keystoneclient.auth.identity.generic import password +from keystoneclient.auth.identity.generic import token +from keystoneclient.auth.identity import v3 as identity +from keystoneclient import session as ksession +from oslo.utils import encodeutils +from oslo.utils import strutils import pkg_resources import six @@ -45,14 +52,12 @@ import novaclient.auth_plugin from novaclient import client from novaclient import exceptions as exc import novaclient.extension +from novaclient.i18n import _ from novaclient.openstack.common import cliutils -from novaclient.openstack.common.gettextutils import _ -from novaclient.openstack.common import strutils from novaclient import utils -from novaclient.v1_1 import shell as shell_v1_1 -from novaclient.v3 import shell as shell_v3 +from novaclient.v2 import shell as shell_v2 -DEFAULT_OS_COMPUTE_API_VERSION = "1.1" +DEFAULT_OS_COMPUTE_API_VERSION = "2" DEFAULT_NOVA_ENDPOINT_TYPE = 'publicURL' # NOTE(cyeoh): Having the service type dependent on the API version # is pretty ugly, but we have to do this because traditionally the @@ -134,7 +139,7 @@ class SecretsHelper(object): if not HAS_KEYRING or not self.args.os_cache: return if (auth_token == self.auth_token and - management_url == self.management_url): + management_url == self.management_url): # Nothing changed.... return if not all([management_url, auth_token, tenant_id]): @@ -154,7 +159,7 @@ class SecretsHelper(object): self._password = self.args.os_password else: verify_pass = strutils.bool_from_string( - utils.env("OS_VERIFY_PASSWORD", default=False), True) + cliutils.env("OS_VERIFY_PASSWORD", default=False), True) self._password = self._prompt_password(verify_pass) if not self._password: raise exc.CommandError( @@ -219,17 +224,51 @@ class NovaClientArgumentParser(argparse.ArgumentParser): exits. """ self.print_usage(sys.stderr) - #FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value + # FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value choose_from = ' (choose from' progparts = self.prog.partition(' ') self.exit(2, _("error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'" - " for more information.\n") % - {'errmsg': message.split(choose_from)[0], - 'mainp': progparts[0], - 'subp': progparts[2]}) + " for more information.\n") % + {'errmsg': message.split(choose_from)[0], + 'mainp': progparts[0], + 'subp': progparts[2]}) + + def _get_option_tuples(self, option_string): + """returns (action, option, value) candidates for an option prefix + + Returns [first candidate] if all candidates refers to current and + deprecated forms of the same options: "nova boot ... --key KEY" + parsing succeed because --key could only match --key-name, + --key_name which are current/deprecated forms of the same option. + """ + option_tuples = (super(NovaClientArgumentParser, self) + ._get_option_tuples(option_string)) + if len(option_tuples) > 1: + normalizeds = [option.replace('_', '-') + for action, option, value in option_tuples] + if len(set(normalizeds)) == 1: + return option_tuples[:1] + return option_tuples class OpenStackComputeShell(object): + times = [] + + def _append_global_identity_args(self, parser): + # Register the CLI arguments that have moved to the session object. + ksession.Session.register_cli_options(parser) + + parser.set_defaults(insecure=cliutils.env('NOVACLIENT_INSECURE', + default=False)) + + identity.Password.register_argparse_arguments(parser) + + parser.set_defaults(os_username=cliutils.env('OS_USERNAME', + 'NOVA_USERNAME')) + parser.set_defaults(os_password=cliutils.env('OS_PASSWORD', + 'NOVA_PASSWORD')) + parser.set_defaults(os_auth_url=cliutils.env('OS_AUTH_URL', + 'NOVA_URL')) def get_base_parser(self): parser = NovaClientArgumentParser( @@ -242,7 +281,8 @@ class OpenStackComputeShell(object): ) # Global arguments - parser.add_argument('-h', '--help', + parser.add_argument( + '-h', '--help', action='store_true', help=argparse.SUPPRESS, ) @@ -251,154 +291,151 @@ class OpenStackComputeShell(object): action='version', version=novaclient.__version__) - parser.add_argument('--debug', + parser.add_argument( + '--debug', default=False, action='store_true', help=_("Print debugging output")) - parser.add_argument('--os-cache', + parser.add_argument( + '--os-cache', default=strutils.bool_from_string( - utils.env('OS_CACHE', default=False), True), + cliutils.env('OS_CACHE', default=False), True), action='store_true', help=_("Use the auth token cache. Defaults to False if " "env[OS_CACHE] is not set.")) - parser.add_argument('--timings', + parser.add_argument( + '--timings', default=False, action='store_true', help=_("Print call timing info")) - parser.add_argument('--timeout', - default=600, - metavar='', - type=positive_non_zero_float, - help=_("Set HTTP call timeout (in seconds)")) + parser.add_argument( + '--os-auth-token', + default=cliutils.env('OS_AUTH_TOKEN'), + help='Defaults to env[OS_AUTH_TOKEN]') - parser.add_argument('--os-auth-token', - default=utils.env('OS_AUTH_TOKEN'), - help='Defaults to env[OS_AUTH_TOKEN]') - - parser.add_argument('--os-username', - metavar='', - default=utils.env('OS_USERNAME', 'NOVA_USERNAME'), - help=_('Defaults to env[OS_USERNAME].')) - parser.add_argument('--os_username', + parser.add_argument( + '--os_username', help=argparse.SUPPRESS) - parser.add_argument('--os-user-id', - metavar='', - default=utils.env('OS_USER_ID'), - help=_('Defaults to env[OS_USER_ID].')) - - parser.add_argument('--os-password', - metavar='', - default=utils.env('OS_PASSWORD', 'NOVA_PASSWORD'), - help=_('Defaults to env[OS_PASSWORD].')) - parser.add_argument('--os_password', + parser.add_argument( + '--os_password', help=argparse.SUPPRESS) - parser.add_argument('--os-tenant-name', + parser.add_argument( + '--os-tenant-name', metavar='', - default=utils.env('OS_TENANT_NAME', 'NOVA_PROJECT_ID'), + default=cliutils.env('OS_TENANT_NAME', 'NOVA_PROJECT_ID'), help=_('Defaults to env[OS_TENANT_NAME].')) - parser.add_argument('--os_tenant_name', + parser.add_argument( + '--os_tenant_name', help=argparse.SUPPRESS) - parser.add_argument('--os-tenant-id', + parser.add_argument( + '--os-tenant-id', metavar='', - default=utils.env('OS_TENANT_ID'), + default=cliutils.env('OS_TENANT_ID'), help=_('Defaults to env[OS_TENANT_ID].')) - parser.add_argument('--os-auth-url', - metavar='', - default=utils.env('OS_AUTH_URL', 'NOVA_URL'), - help=_('Defaults to env[OS_AUTH_URL].')) - parser.add_argument('--os_auth_url', + parser.add_argument( + '--os_auth_url', help=argparse.SUPPRESS) - parser.add_argument('--os-region-name', + parser.add_argument( + '--os-region-name', metavar='', - default=utils.env('OS_REGION_NAME', 'NOVA_REGION_NAME'), + default=cliutils.env('OS_REGION_NAME', 'NOVA_REGION_NAME'), help=_('Defaults to env[OS_REGION_NAME].')) - parser.add_argument('--os_region_name', + parser.add_argument( + '--os_region_name', help=argparse.SUPPRESS) - parser.add_argument('--os-auth-system', + parser.add_argument( + '--os-auth-system', metavar='', - default=utils.env('OS_AUTH_SYSTEM'), + default=cliutils.env('OS_AUTH_SYSTEM'), help='Defaults to env[OS_AUTH_SYSTEM].') - parser.add_argument('--os_auth_system', + parser.add_argument( + '--os_auth_system', help=argparse.SUPPRESS) - parser.add_argument('--service-type', + parser.add_argument( + '--service-type', metavar='', help=_('Defaults to compute for most actions')) - parser.add_argument('--service_type', + parser.add_argument( + '--service_type', help=argparse.SUPPRESS) - parser.add_argument('--service-name', + parser.add_argument( + '--service-name', metavar='', - default=utils.env('NOVA_SERVICE_NAME'), + default=cliutils.env('NOVA_SERVICE_NAME'), help=_('Defaults to env[NOVA_SERVICE_NAME]')) - parser.add_argument('--service_name', + parser.add_argument( + '--service_name', help=argparse.SUPPRESS) - parser.add_argument('--volume-service-name', + parser.add_argument( + '--volume-service-name', metavar='', - default=utils.env('NOVA_VOLUME_SERVICE_NAME'), + default=cliutils.env('NOVA_VOLUME_SERVICE_NAME'), help=_('Defaults to env[NOVA_VOLUME_SERVICE_NAME]')) - parser.add_argument('--volume_service_name', + parser.add_argument( + '--volume_service_name', help=argparse.SUPPRESS) - parser.add_argument('--endpoint-type', + parser.add_argument( + '--os-endpoint-type', metavar='', - default=utils.env('NOVA_ENDPOINT_TYPE', - default=DEFAULT_NOVA_ENDPOINT_TYPE), - help=_('Defaults to env[NOVA_ENDPOINT_TYPE] or ') - + DEFAULT_NOVA_ENDPOINT_TYPE + '.') + dest='endpoint_type', + default=cliutils.env( + 'NOVA_ENDPOINT_TYPE', + default=cliutils.env( + 'OS_ENDPOINT_TYPE', + default=DEFAULT_NOVA_ENDPOINT_TYPE)), + help=_('Defaults to env[NOVA_ENDPOINT_TYPE], ' + 'env[OS_ENDPOINT_TYPE] or ') + + DEFAULT_NOVA_ENDPOINT_TYPE + '.') + + parser.add_argument( + '--endpoint-type', + help=argparse.SUPPRESS) # NOTE(dtroyer): We can't add --endpoint_type here due to argparse # thinking usage-list --end is ambiguous; but it # works fine with only --endpoint-type present # Go figure. I'm leaving this here for doc purposes. - #parser.add_argument('--endpoint_type', - # help=argparse.SUPPRESS) + # parser.add_argument('--endpoint_type', + # help=argparse.SUPPRESS) - parser.add_argument('--os-compute-api-version', + parser.add_argument( + '--os-compute-api-version', metavar='', - default=utils.env('OS_COMPUTE_API_VERSION', - default=DEFAULT_OS_COMPUTE_API_VERSION), + default=cliutils.env('OS_COMPUTE_API_VERSION', + default=DEFAULT_OS_COMPUTE_API_VERSION), help=_('Accepts 1.1 or 3, ' - 'defaults to env[OS_COMPUTE_API_VERSION].')) - parser.add_argument('--os_compute_api_version', + 'defaults to env[OS_COMPUTE_API_VERSION].')) + parser.add_argument( + '--os_compute_api_version', help=argparse.SUPPRESS) - parser.add_argument('--os-cacert', - metavar='', - default=utils.env('OS_CACERT', default=None), - help='Specify a CA bundle file to use in ' - 'verifying a TLS (https) server certificate. ' - 'Defaults to env[OS_CACERT]') - - parser.add_argument('--insecure', - default=utils.env('NOVACLIENT_INSECURE', default=False), - action='store_true', - help=_("Explicitly allow novaclient to perform \"insecure\" " - "SSL (https) requests. The server's certificate will " - "not be verified against any certificate authorities. " - "This option should be used with caution.")) - - parser.add_argument('--bypass-url', + parser.add_argument( + '--bypass-url', metavar='', dest='bypass_url', - default=utils.env('NOVACLIENT_BYPASS_URL'), + default=cliutils.env('NOVACLIENT_BYPASS_URL'), help="Use this API endpoint instead of the Service Catalog. " "Defaults to env[NOVACLIENT_BYPASS_URL]") parser.add_argument('--bypass_url', - help=argparse.SUPPRESS) + help=argparse.SUPPRESS) # The auth-system-plugins might require some extra options novaclient.auth_plugin.load_auth_system_opts(parser) + self._append_global_identity_args(parser) + return parser def get_subcommand_parser(self, version): @@ -409,12 +446,12 @@ class OpenStackComputeShell(object): try: actions_module = { - '1.1': shell_v1_1, - '2': shell_v1_1, - '3': shell_v3, + '1.1': shell_v2, + '2': shell_v2, + '3': shell_v2, }[version] except KeyError: - actions_module = shell_v1_1 + actions_module = shell_v2 self._find_actions(subparsers, actions_module) self._find_actions(subparsers, self) @@ -454,6 +491,10 @@ class OpenStackComputeShell(object): def _discover_via_contrib_path(self, version): module_path = os.path.dirname(os.path.abspath(__file__)) version_str = "v%s" % version.replace('.', '_') + # NOTE(akurilin): v1.1, v2 and v3 have one implementation, so + # we should discover contrib modules in one place. + if version_str in ["v1_1", "v3"]: + version_str = "v2" ext_path = os.path.join(module_path, version_str, 'contrib') ext_glob = os.path.join(ext_path, "*.py") @@ -474,7 +515,8 @@ class OpenStackComputeShell(object): yield name, module def _add_bash_completion_subparser(self, subparsers): - subparser = subparsers.add_parser('bash_completion', + subparser = subparsers.add_parser( + 'bash_completion', add_help=False, formatter_class=OpenStackHelpFormatter ) @@ -490,13 +532,14 @@ class OpenStackComputeShell(object): action_help = desc.strip() arguments = getattr(callback, 'arguments', []) - subparser = subparsers.add_parser(command, + subparser = subparsers.add_parser( + command, help=action_help, description=desc, add_help=False, - formatter_class=OpenStackHelpFormatter - ) - subparser.add_argument('-h', '--help', + formatter_class=OpenStackHelpFormatter) + subparser.add_argument( + '-h', '--help', action='help', help=argparse.SUPPRESS, ) @@ -515,6 +558,20 @@ class OpenStackComputeShell(object): logging.basicConfig(level=logging.DEBUG, format=streamformat) + def _get_keystone_auth(self, session, auth_url, **kwargs): + auth_token = kwargs.pop('auth_token', None) + if auth_token: + return token.Token(auth_url, auth_token, **kwargs) + else: + return password.Password( + auth_url, + username=kwargs.pop('username'), + user_id=kwargs.pop('user_id'), + password=kwargs.pop('password'), + user_domain_id=kwargs.pop('user_domain_id'), + user_domain_name=kwargs.pop('user_domain_name'), + **kwargs) + def main(self, argv): # Parse args once to find version and debug settings parser = self.get_base_parser() @@ -526,7 +583,7 @@ class OpenStackComputeShell(object): # build available subcommands based on version self.extensions = self._discover_extensions( - options.os_compute_api_version) + options.os_compute_api_version) self._run_extension_hooks('__pre_parse_args__') # NOTE(dtroyer): Hackery to handle --endpoint_type due to argparse @@ -538,7 +595,7 @@ class OpenStackComputeShell(object): argv[spot] = '--endpoint-type' subcommand_parser = self.get_subcommand_parser( - options.os_compute_api_version) + options.os_compute_api_version) self.parser = subcommand_parser if options.help or not argv: @@ -574,6 +631,9 @@ class OpenStackComputeShell(object): cacert = args.os_cacert timeout = args.timeout + keystone_session = None + keystone_auth = None + # We may have either, both or none of these. # If we have both, we don't need USERNAME, PASSWORD etc. # Fill in the blanks from the SecretsHelper if possible. @@ -591,6 +651,11 @@ class OpenStackComputeShell(object): if not endpoint_type: endpoint_type = DEFAULT_NOVA_ENDPOINT_TYPE + # This allow users to use endpoint_type as (internal, public or admin) + # just like other openstack clients (glance, cinder etc) + if endpoint_type in ['internal', 'public', 'admin']: + endpoint_type += 'URL' + if not service_type: os_compute_api_version = (options.os_compute_api_version or DEFAULT_OS_COMPUTE_API_VERSION) @@ -600,14 +665,21 @@ class OpenStackComputeShell(object): except KeyError: service_type = DEFAULT_NOVA_SERVICE_TYPE_MAP[ DEFAULT_OS_COMPUTE_API_VERSION] - service_type = utils.get_service_type(args.func) or service_type + service_type = cliutils.get_service_type(args.func) or service_type # If we have an auth token but no management_url, we must auth anyway. # Expired tokens are handled by client.py:_cs_request must_auth = not (cliutils.isunauthenticated(args.func) or (auth_token and management_url)) - #FIXME(usrleon): Here should be restrict for project id same as + # Do not use Keystone session for cases with no session support. The + # presence of auth_plugin means os_auth_system is present and is not + # keystone. + use_session = True + if auth_plugin or bypass_url or os_cache or volume_service_name: + use_session = False + + # FIXME(usrleon): Here should be restrict for project id same as # for os_username or os_password but for compatibility it is not. if must_auth: if auth_plugin: @@ -615,54 +687,86 @@ class OpenStackComputeShell(object): if not auth_plugin or not auth_plugin.opts: if not os_username and not os_user_id: - raise exc.CommandError(_("You must provide a username " - "or user id via --os-username, --os-user-id, " - "env[OS_USERNAME] or env[OS_USER_ID]")) + raise exc.CommandError( + _("You must provide a username " + "or user id via --os-username, --os-user-id, " + "env[OS_USERNAME] or env[OS_USER_ID]")) - if not os_tenant_name and not os_tenant_id: - raise exc.CommandError(_("You must provide a tenant name " - "or tenant id via --os-tenant-name, " - "--os-tenant-id, env[OS_TENANT_NAME] " - "or env[OS_TENANT_ID]")) + if not any([args.os_tenant_name, args.os_tenant_id, + args.os_project_id, args.os_project_name]): + raise exc.CommandError(_("You must provide a project name or" + " project id via --os-project-name," + " --os-project-id, env[OS_PROJECT_ID]" + " or env[OS_PROJECT_NAME]. You may" + " use os-project and os-tenant" + " interchangeably.")) if not os_auth_url: if os_auth_system and os_auth_system != 'keystone': os_auth_url = auth_plugin.get_auth_url() if not os_auth_url: - raise exc.CommandError(_("You must provide an auth url " - "via either --os-auth-url or env[OS_AUTH_URL] " - "or specify an auth_system which defines a " - "default url with --os-auth-system " - "or env[OS_AUTH_SYSTEM]")) + raise exc.CommandError( + _("You must provide an auth url " + "via either --os-auth-url or env[OS_AUTH_URL] " + "or specify an auth_system which defines a " + "default url with --os-auth-system " + "or env[OS_AUTH_SYSTEM]")) + + project_id = args.os_project_id or args.os_tenant_id + project_name = args.os_project_name or args.os_tenant_name + if use_session: + # Not using Nova auth plugin, so use keystone + start_time = time.time() + keystone_session = ksession.Session.load_from_cli_options(args) + keystone_auth = self._get_keystone_auth( + keystone_session, + args.os_auth_url, + username=args.os_username, + user_id=args.os_user_id, + user_domain_id=args.os_user_domain_id, + user_domain_name=args.os_user_domain_name, + password=args.os_password, + auth_token=args.os_auth_token, + project_id=project_id, + project_name=project_name, + project_domain_id=args.os_project_domain_id, + project_domain_name=args.os_project_domain_name) + end_time = time.time() + self.times.append( + ('%s %s' % ('auth_url', args.os_auth_url), + start_time, end_time)) if (options.os_compute_api_version and options.os_compute_api_version != '1.0'): - if not os_tenant_name and not os_tenant_id: - raise exc.CommandError(_("You must provide a tenant name " - "or tenant id via --os-tenant-name, " - "--os-tenant-id, env[OS_TENANT_NAME] " - "or env[OS_TENANT_ID]")) + if not any([args.os_tenant_id, args.os_tenant_name, + args.os_project_id, args.os_project_name]): + raise exc.CommandError(_("You must provide a project name or" + " project id via --os-project-name," + " --os-project-id, env[OS_PROJECT_ID]" + " or env[OS_PROJECT_NAME]. You may" + " use os-project and os-tenant" + " interchangeably.")) if not os_auth_url: - raise exc.CommandError(_("You must provide an auth url " - "via either --os-auth-url or env[OS_AUTH_URL]")) + raise exc.CommandError( + _("You must provide an auth url " + "via either --os-auth-url or env[OS_AUTH_URL]")) - completion_cache = client.CompletionCache(os_username, os_auth_url) - - self.cs = client.Client(options.os_compute_api_version, - os_username, os_password, os_tenant_name, - tenant_id=os_tenant_id, user_id=os_user_id, - auth_url=os_auth_url, insecure=insecure, - region_name=os_region_name, endpoint_type=endpoint_type, - extensions=self.extensions, service_type=service_type, - service_name=service_name, auth_system=os_auth_system, - auth_plugin=auth_plugin, auth_token=auth_token, - volume_service_name=volume_service_name, - timings=args.timings, bypass_url=bypass_url, - os_cache=os_cache, http_log_debug=options.debug, - cacert=cacert, timeout=timeout, - completion_cache=completion_cache) + self.cs = client.Client( + options.os_compute_api_version, + os_username, os_password, os_tenant_name, + tenant_id=os_tenant_id, user_id=os_user_id, + auth_url=os_auth_url, insecure=insecure, + region_name=os_region_name, endpoint_type=endpoint_type, + extensions=self.extensions, service_type=service_type, + service_name=service_name, auth_system=os_auth_system, + auth_plugin=auth_plugin, auth_token=auth_token, + volume_service_name=volume_service_name, + timings=args.timings, bypass_url=bypass_url, + os_cache=os_cache, http_log_debug=options.debug, + cacert=cacert, timeout=timeout, + session=keystone_session, auth=keystone_auth) # Now check for the password/token of which pieces of the # identifying keyring key can come from the underlying client @@ -695,13 +799,17 @@ class OpenStackComputeShell(object): # This does a couple of bits which are useful even if we've # got the token + service URL already. It exits fast in that case. if not cliutils.isunauthenticated(args.func): - self.cs.authenticate() + if not use_session: + # Only call authenticate() if Nova auth plugin is used. + # If keystone is used, authentication is handled as part + # of session. + self.cs.authenticate() except exc.Unauthorized: raise exc.CommandError(_("Invalid OpenStack Nova credentials.")) except exc.AuthorizationFailure: raise exc.CommandError(_("Unable to authorize user")) - if os_compute_api_version == "3" and service_type != 'image': + if options.os_compute_api_version == "3" and service_type != 'image': # NOTE(cyeoh): create an image based client because the # images api is no longer proxied by the V3 API and we # sometimes need to be able to look up images information @@ -724,12 +832,13 @@ 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, + session=keystone_session, auth=keystone_auth, cacert=cacert, timeout=timeout) args.func(self.cs, args) if args.timings: - self._dump_timings(self.cs.get_timings()) + self._dump_timings(self.times + self.cs.get_timings()) def _dump_timings(self, timings): class Tyme(object): @@ -764,8 +873,11 @@ class OpenStackComputeShell(object): commands.remove('bash_completion') print(' '.join(commands | options)) - @utils.arg('command', metavar='', nargs='?', - help='Display help for ') + @cliutils.arg( + 'command', + metavar='', + nargs='?', + help='Display help for ') def do_help(self, args): """ Display help about this program or one of its subcommands. @@ -785,7 +897,7 @@ class OpenStackHelpFormatter(argparse.HelpFormatter): def __init__(self, prog, indent_increment=2, max_help_position=32, width=None): super(OpenStackHelpFormatter, self).__init__(prog, indent_increment, - max_help_position, width) + max_help_position, width) def start_section(self, heading): # Title-case the headings @@ -795,19 +907,19 @@ class OpenStackHelpFormatter(argparse.HelpFormatter): def main(): try: - argv = [strutils.safe_decode(a) for a in sys.argv[1:]] + argv = [encodeutils.safe_decode(a) for a in sys.argv[1:]] OpenStackComputeShell().main(argv) except Exception as e: logger.debug(e, exc_info=1) - details = {'name': strutils.safe_encode(e.__class__.__name__), - 'msg': strutils.safe_encode(six.text_type(e))} + details = {'name': encodeutils.safe_encode(e.__class__.__name__), + 'msg': encodeutils.safe_encode(six.text_type(e))} print("ERROR (%(name)s): %(msg)s" % details, file=sys.stderr) sys.exit(1) - except KeyboardInterrupt as e: - print("Shutting down novaclient", file=sys.stderr) - sys.exit(1) + except KeyboardInterrupt: + print("... terminating nova client", file=sys.stderr) + sys.exit(130) if __name__ == "__main__": diff --git a/awx/lib/site-packages/novaclient/tests/functional/README.rst b/awx/lib/site-packages/novaclient/tests/functional/README.rst new file mode 100644 index 0000000000..be4ebd079f --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/functional/README.rst @@ -0,0 +1,50 @@ +===================================== +python-novaclient functional testing +===================================== + +Idea +------ + +Over time we have noticed two issues with novaclient unit tests. + +* Does not exercise the CLI +* We can get the expected server behavior wrong, and test the wrong thing. + +We are using functional tests, run against a running cloud +(primarily devstack), to address these two cases. + +Additionally these functional tests can be considered example uses +of python-novaclient. + +These tests started out in tempest as read only nova CLI tests, to make sure +the CLI didn't simply stacktrace when being used (which happened on +multiple occasions). + + +Testing Theory +---------------- + +We are treating python-novaclient as legacy code, so we do not want to spend a +lot of effort adding in missing features. In the future the CLI will move to +python-openstackclient, and the python API will be based on the OpenStack +SDK project. But until that happens we still need better functional testing, +to prevent regressions etc. + + +Since python-novaclient has two uses, CLI and python API, we should have two +sets of functional tests. CLI and python API. The python API tests should +never use the CLI. But the CLI tests can use the python API where adding +native support to the CLI for the required functionality would involve a +non trivial amount of work. + +Functional Test Guidelines +--------------------------- + +* Consume credentials via standard client environmental variables:: + + OS_USERNAME + OS_PASSWORD + OS_TENANT_NAME + OS_AUTH_URL + +* Try not to require an additional configuration file diff --git a/awx/lib/site-packages/novaclient/tests/functional/__init__.py b/awx/lib/site-packages/novaclient/tests/functional/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/awx/lib/site-packages/novaclient/tests/functional/base.py b/awx/lib/site-packages/novaclient/tests/functional/base.py new file mode 100644 index 0000000000..27015bda8a --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/functional/base.py @@ -0,0 +1,44 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os + +from tempest_lib.cli import base + + +class ClientTestBase(base.ClientTestBase): + """ + This is a first pass at a simple read only python-novaclient test. This + only exercises client commands that are read only. + + This should test commands: + * as a regular user + * as a admin user + * with and without optional parameters + * initially just check return codes, and later test command outputs + + """ + def _get_clients(self): + cli_dir = os.environ.get( + 'OS_NOVACLIENT_EXEC_DIR', + os.path.join(os.path.abspath('.'), '.tox/functional/bin')) + + return base.CLIClient( + username=os.environ.get('OS_USERNAME'), + password=os.environ.get('OS_PASSWORD'), + tenant_name=os.environ.get('OS_TENANT_NAME'), + uri=os.environ.get('OS_AUTH_URL'), + cli_dir=cli_dir) + + def nova(self, *args, **kwargs): + return self.clients.nova(*args, + **kwargs) diff --git a/awx/lib/site-packages/novaclient/tests/functional/hooks/post_test_hook.sh b/awx/lib/site-packages/novaclient/tests/functional/hooks/post_test_hook.sh new file mode 100755 index 0000000000..e9e35b24ec --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/functional/hooks/post_test_hook.sh @@ -0,0 +1,50 @@ +#!/bin/bash -xe + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# This script is executed inside post_test_hook function in devstack gate. + +function generate_testr_results { + if [ -f .testrepository/0 ]; then + sudo .tox/functional/bin/testr last --subunit > $WORKSPACE/testrepository.subunit + sudo mv $WORKSPACE/testrepository.subunit $BASE/logs/testrepository.subunit + sudo .tox/functional/bin/python /usr/local/jenkins/slave_scripts/subunit2html.py $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html + sudo gzip -9 $BASE/logs/testrepository.subunit + sudo gzip -9 $BASE/logs/testr_results.html + sudo chown jenkins:jenkins $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz + sudo chmod a+r $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz + fi +} + +export NOVACLIENT_DIR="$BASE/new/python-novaclient" + +# Get admin credentials +cd $BASE/new/devstack +source openrc admin admin + +# Go to the novaclient dir +cd $NOVACLIENT_DIR + +sudo chown -R jenkins:stack $NOVACLIENT_DIR + +# Run tests +echo "Running novaclient functional test suite" +set +e +# Preserve env for OS_ credentials +sudo -E -H -u jenkins tox -efunctional +EXIT_CODE=$? +set -e + +# Collect and parse result +generate_testr_results +exit $EXIT_CODE diff --git a/awx/lib/site-packages/novaclient/tests/functional/test_instances.py b/awx/lib/site-packages/novaclient/tests/functional/test_instances.py new file mode 100644 index 0000000000..c963a6b710 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/functional/test_instances.py @@ -0,0 +1,155 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import time +import uuid + +import novaclient.client +from novaclient.tests.functional import base + + +# TODO(sdague): content that probably should be in utils, also throw +# Exceptions when they fail. +def pick_flavor(flavors): + """Given a flavor list pick a reasonable one.""" + for flavor in flavors: + if flavor.name == 'm1.tiny': + return flavor + + for flavor in flavors: + if flavor.name == 'm1.small': + return flavor + + +def pick_image(images): + for image in images: + if image.name.startswith('cirros') and image.name.endswith('-uec'): + return image + + +def volume_id_from_cli_create(output): + """Scrape the volume id out of the 'volume create' command + + The cli for Nova automatically routes requests to the volumes + service end point. However the nova api low level commands don't + redirect to the correct service endpoint, so for volumes commands + (even setup ones) we use the cli for magic routing. + + This function lets us get the id out of the prettytable that's + dumped on the cli during create. + + """ + for line in output.split("\n"): + fields = line.split() + if len(fields) > 4: + if fields[1] == "id": + return fields[3] + + +def volume_at_status(output, volume_id, status): + for line in output.split("\n"): + fields = line.split() + if len(fields) > 4: + if fields[1] == volume_id: + return fields[3] == status + raise Exception("Volume %s did not reach status '%s' in output: %s" + % (volume_id, status, output)) + + +class TestInstanceCLI(base.ClientTestBase): + def setUp(self): + super(TestInstanceCLI, self).setUp() + # TODO(sdague): while we collect this information in + # tempest-lib, we do it in a way that's not available for top + # level tests. Long term this probably needs to be in the base + # class. + user = os.environ['OS_USERNAME'] + passwd = os.environ['OS_PASSWORD'] + tenant = os.environ['OS_TENANT_NAME'] + auth_url = os.environ['OS_AUTH_URL'] + + # TODO(sdague): we made a lot of fun of the glanceclient team + # for version as int in first parameter. I guess we know where + # they copied it from. + self.client = novaclient.client.Client( + 2, user, passwd, tenant, + auth_url=auth_url) + + # pick some reasonable flavor / image combo + self.flavor = pick_flavor(self.client.flavors.list()) + self.image = pick_image(self.client.images.list()) + + def test_attach_volume(self): + """Test we can attach a volume via the cli. + + This test was added after bug 1423695. That bug exposed + inconsistencies in how to talk to API services from the CLI + vs. API level. The volumes api calls that were designed to + populate the completion cache were incorrectly routed to the + Nova endpoint. Novaclient volumes support actually talks to + Cinder endpoint directly. + + This would case volume-attach to return a bad error code, + however it does this *after* the attach command is correctly + dispatched. So the volume-attach still works, but the user is + presented a 404 error. + + This test ensures we can do a through path test of: boot, + create volume, attach volume, detach volume, delete volume, + destroy. + + """ + # TODO(sdague): better random name + name = str(uuid.uuid4()) + + # Boot via the cli, as we're primarily testing the cli in this test + self.nova('boot', params="--flavor %s --image %s %s --poll" % + (self.flavor.name, self.image.name, name)) + + # Be nice about cleaning up, however, use the API for this to avoid + # parsing text. + servers = self.client.servers.list(search_opts={"name": name}) + # the name is a random uuid, there better only be one + self.assertEqual(1, len(servers), servers) + server = servers[0] + self.addCleanup(server.delete) + + # create a volume for attachment. We use the CLI because it + # magic routes to cinder, however the low level API does not. + volume_id = volume_id_from_cli_create( + self.nova('volume-create', params="1")) + self.addCleanup(self.nova, 'volume-delete', params=volume_id) + + # allow volume to become available + for x in xrange(60): + volumes = self.nova('volume-list') + if volume_at_status(volumes, volume_id, 'available'): + break + time.sleep(1) + else: + self.fail("Volume %s not available after 60s" % volume_id) + + # attach the volume + self.nova('volume-attach', params="%s %s" % (name, volume_id)) + + # volume needs to transition to 'in-use' to be attached + for x in xrange(60): + volumes = self.nova('volume-list') + if volume_at_status(volumes, volume_id, 'in-use'): + break + time.sleep(1) + else: + self.fail("Volume %s not attached after 60s" % volume_id) + + # clean up on success + self.nova('volume-detach', params="%s %s" % (name, volume_id)) diff --git a/awx/lib/site-packages/novaclient/tests/functional/test_readonly_nova.py b/awx/lib/site-packages/novaclient/tests/functional/test_readonly_nova.py new file mode 100644 index 0000000000..2ea4ec986b --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/functional/test_readonly_nova.py @@ -0,0 +1,171 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest_lib import decorators +from tempest_lib import exceptions + +from novaclient.tests.functional import base + + +class SimpleReadOnlyNovaClientTest(base.ClientTestBase): + + """ + read only functional python-novaclient tests. + + This only exercises client commands that are read only. + """ + + def test_admin_fake_action(self): + self.assertRaises(exceptions.CommandFailed, + self.nova, + 'this-does-nova-exist') + + # NOTE(jogo): Commands in order listed in 'nova help' + + def test_admin_absolute_limites(self): + self.nova('absolute-limits') + self.nova('absolute-limits', params='--reserved') + + def test_admin_aggregate_list(self): + self.nova('aggregate-list') + + def test_admin_availability_zone_list(self): + self.assertIn("internal", self.nova('availability-zone-list')) + + def test_admin_cloudpipe_list(self): + self.nova('cloudpipe-list') + + def test_admin_credentials(self): + self.nova('credentials') + + # "Neutron does not provide this feature" + def test_admin_dns_domains(self): + self.nova('dns-domains') + + @decorators.skip_because(bug="1157349") + def test_admin_dns_list(self): + self.nova('dns-list') + + def test_admin_endpoints(self): + self.nova('endpoints') + + def test_admin_flavor_acces_list(self): + self.assertRaises(exceptions.CommandFailed, + self.nova, + 'flavor-access-list') + # Failed to get access list for public flavor type + self.assertRaises(exceptions.CommandFailed, + self.nova, + 'flavor-access-list', + params='--flavor m1.tiny') + + def test_admin_flavor_list(self): + self.assertIn("Memory_MB", self.nova('flavor-list')) + + def test_admin_floating_ip_bulk_list(self): + self.nova('floating-ip-bulk-list') + + def test_admin_floating_ip_list(self): + self.nova('floating-ip-list') + + def test_admin_floating_ip_pool_list(self): + self.nova('floating-ip-pool-list') + + def test_admin_host_list(self): + self.nova('host-list') + + def test_admin_hypervisor_list(self): + self.nova('hypervisor-list') + + def test_admin_image_list(self): + self.nova('image-list') + + @decorators.skip_because(bug="1157349") + def test_admin_interface_list(self): + self.nova('interface-list') + + def test_admin_keypair_list(self): + self.nova('keypair-list') + + def test_admin_list(self): + self.nova('list') + self.nova('list', params='--all-tenants 1') + self.nova('list', params='--all-tenants 0') + self.assertRaises(exceptions.CommandFailed, + self.nova, + 'list', + params='--all-tenants bad') + + def test_admin_network_list(self): + self.nova('network-list') + + def test_admin_rate_limits(self): + self.nova('rate-limits') + + def test_admin_secgroup_list(self): + self.nova('secgroup-list') + + @decorators.skip_because(bug="1157349") + def test_admin_secgroup_list_rules(self): + self.nova('secgroup-list-rules') + + def test_admin_server_group_list(self): + self.nova('server-group-list') + + def test_admin_servce_list(self): + self.nova('service-list') + + def test_admin_usage(self): + self.nova('usage') + + def test_admin_usage_list(self): + self.nova('usage-list') + + def test_admin_volume_list(self): + self.nova('volume-list') + + def test_admin_volume_snapshot_list(self): + self.nova('volume-snapshot-list') + + def test_admin_volume_type_list(self): + self.nova('volume-type-list') + + def test_admin_help(self): + self.nova('help') + + def test_admin_list_extensions(self): + self.nova('list-extensions') + + def test_admin_net_list(self): + self.nova('net-list') + + def test_agent_list(self): + self.nova('agent-list') + self.nova('agent-list', flags='--debug') + + def test_migration_list(self): + self.nova('migration-list') + self.nova('migration-list', flags='--debug') + + # Optional arguments: + + def test_admin_version(self): + self.nova('', flags='--version') + + def test_admin_debug_list(self): + self.nova('list', flags='--debug') + + def test_admin_timeout(self): + self.nova('list', flags='--timeout %d' % 10) + + def test_admin_timing(self): + self.nova('list', flags='--timing') diff --git a/awx/lib/site-packages/novaclient/tests/functional/test_volumes_api.py b/awx/lib/site-packages/novaclient/tests/functional/test_volumes_api.py new file mode 100644 index 0000000000..e427c2e212 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/functional/test_volumes_api.py @@ -0,0 +1,111 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import time +import uuid + +import six.moves + +from novaclient import client +from novaclient import exceptions +from novaclient.tests.functional import base + + +def wait_for_delete(test, name, thing, get_func): + thing.delete() + for x in six.moves.range(60): + try: + thing = get_func(thing.id) + except exceptions.NotFound: + break + time.sleep(1) + else: + test.fail('%s %s still not deleted after 60s' % (name, thing.id)) + + +class TestVolumesAPI(base.ClientTestBase): + + def setUp(self): + super(TestVolumesAPI, self).setUp() + user = os.environ['OS_USERNAME'] + passwd = os.environ['OS_PASSWORD'] + tenant = os.environ['OS_TENANT_NAME'] + auth_url = os.environ['OS_AUTH_URL'] + + self.client = client.Client(2, user, passwd, tenant, auth_url=auth_url) + + def test_volumes_snapshots_types_create_get_list_delete(self): + # Create a volume + volume = self.client.volumes.create(1) + + # Make sure we can still list servers after using the volume endpoint + self.client.servers.list() + + # This cleanup tests volume delete + self.addCleanup(volume.delete) + + # Wait for the volume to become available + for x in six.moves.range(60): + volume = self.client.volumes.get(volume.id) + if volume.status == 'available': + break + elif volume.status == 'error': + self.fail('Volume %s is in error state' % volume.id) + time.sleep(1) + else: + self.fail('Volume %s not available after 60s' % volume.id) + + # List all volumes + self.client.volumes.list() + + # Create a volume snapshot + snapshot = self.client.volume_snapshots.create(volume.id) + + # This cleanup tests volume snapshot delete. The volume + # can't be deleted until the dependent snapshot is gone + self.addCleanup(wait_for_delete, self, 'Snapshot', snapshot, + self.client.volume_snapshots.get) + + # Wait for the snapshot to become available + for x in six.moves.range(60): + snapshot = self.client.volume_snapshots.get(snapshot.id) + if snapshot.status == 'available': + break + elif snapshot.status == 'error': + self.fail('Snapshot %s is in error state' % snapshot.id) + time.sleep(1) + else: + self.fail('Snapshot %s not available after 60s' % snapshot.id) + + # List snapshots + self.client.volume_snapshots.list() + + # List servers again to make sure things are still good + self.client.servers.list() + + # Create a volume type + # TODO(melwitt): Use a better random name + name = str(uuid.uuid4()) + volume_type = self.client.volume_types.create(name) + + # This cleanup tests volume type delete + self.addCleanup(self.client.volume_types.delete, volume_type.id) + + # Get the volume type + volume_type = self.client.volume_types.get(volume_type.id) + + # List all volume types + self.client.volume_types.list() + + # One more servers list + self.client.servers.list() diff --git a/awx/lib/site-packages/novaclient/tests/unit/__init__.py b/awx/lib/site-packages/novaclient/tests/unit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/awx/lib/site-packages/novaclient/tests/unit/fakes.py b/awx/lib/site-packages/novaclient/tests/unit/fakes.py new file mode 100644 index 0000000000..ef6b59b523 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fakes.py @@ -0,0 +1,92 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +A fake server that "responds" to API methods with pre-canned responses. + +All of these responses come from the spec, so if for some reason the spec's +wrong the tests might raise AssertionError. I've indicated in comments the +places where actual behavior differs from the spec. +""" + +from novaclient import base + + +def assert_has_keys(dict, required=[], optional=[]): + keys = dict.keys() + for k in required: + try: + assert k in keys + except AssertionError: + extra_keys = set(keys).difference(set(required + optional)) + raise AssertionError("found unexpected keys: %s" % + list(extra_keys)) + + +class FakeClient(object): + + def assert_called(self, method, url, body=None, pos=-1): + """ + Assert than an API method was just called. + """ + expected = (method, url) + called = self.client.callstack[pos][0:2] + + assert self.client.callstack, \ + "Expected %s %s but no calls were made." % expected + + assert expected == called, \ + 'Expected %s %s; got %s %s' % (expected + called) + + if body is not None: + if self.client.callstack[pos][2] != body: + raise AssertionError('%r != %r' % + (self.client.callstack[pos][2], body)) + + def assert_called_anytime(self, method, url, body=None): + """ + Assert than an API method was called anytime in the test. + """ + expected = (method, url) + + assert self.client.callstack, \ + "Expected %s %s but no calls were made." % expected + + found = False + for entry in self.client.callstack: + if expected == entry[0:2]: + found = True + break + + assert found, 'Expected %s; got %s' % (expected, self.client.callstack) + if body is not None: + try: + assert entry[2] == body + except AssertionError: + print(entry[2]) + print("!=") + print(body) + raise + + self.client.callstack = [] + + def clear_callstack(self): + self.client.callstack = [] + + def authenticate(self): + pass + + +# Fake class that will be used as an extension +class FakeManager(base.Manager): + pass diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/__init__.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/agents.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/agents.py new file mode 100644 index 0000000000..46a0bb67b2 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/agents.py @@ -0,0 +1,54 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-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 + } + } + + self.requests.register_uri('POST', self.url(), + json=post_os_agents, + headers=self.json_headers) + + put_os_agents_1 = { + "agent": { + "url": "/yyy/yyyy/yyyy", + "version": "8.0", + "md5hash": "add6bb58e139be103324d04d82d8f546", + 'id': 1 + } + } + + self.requests.register_uri('PUT', self.url(1), + json=put_os_agents_1, + headers=self.json_headers) + + self.requests.register_uri('DELETE', self.url(1), + headers=self.json_headers, + status_code=202) diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/aggregates.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/aggregates.py new file mode 100644 index 0000000000..ed743ed1f9 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/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. + +from novaclient.tests.unit.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'}, + ]} + + self.requests.register_uri('GET', self.url(), + json=get_os_aggregates, + headers=self.json_headers) + + get_aggregates_1 = {'aggregate': get_os_aggregates['aggregates'][0]} + + self.requests.register_uri('POST', self.url(), + json=get_aggregates_1, + headers=self.json_headers) + + for agg_id in (1, 2): + for method in ('GET', 'PUT'): + self.requests.register_uri(method, self.url(agg_id), + json=get_aggregates_1, + headers=self.json_headers) + + self.requests.register_uri('POST', self.url(agg_id, 'action'), + json=get_aggregates_1, + headers=self.json_headers) + + self.requests.register_uri('DELETE', self.url(1), status_code=202) diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/availability_zones.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/availability_zones.py new file mode 100644 index 0000000000..36659d70a5 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/availability_zones.py @@ -0,0 +1,91 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import base + + +class 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 + } + ] + } + + self.requests.register_uri('GET', self.url(), + json=get_os_availability_zone, + headers=self.json_headers) + + 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 + } + ] + } + + self.requests.register_uri('GET', self.url('detail'), + json=get_os_zone_detail, + headers=self.json_headers) diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/base.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/base.py new file mode 100644 index 0000000000..6a2e238b58 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/base.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 fixtures +from six.moves.urllib import parse + +COMPUTE_URL = 'http://compute.host' + + +class Fixture(fixtures.Fixture): + + base_url = None + json_headers = {'Content-Type': 'application/json'} + + def __init__(self, requests, compute_url=COMPUTE_URL): + super(Fixture, self).__init__() + self.requests = requests + self.compute_url = compute_url + + def url(self, *args, **kwargs): + 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/unit/fixture_data/certs.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/certs.py new file mode 100644 index 0000000000..1dedaa4a33 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/certs.py @@ -0,0 +1,55 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-certificates' + + def get_os_certificates_root(self, **kw): + return ( + 200, + {}, + {'certificate': {'private_key': None, 'data': 'foo'}} + ) + + def post_os_certificates(self, **kw): + return ( + 200, + {}, + {'certificate': {'private_key': 'foo', 'data': 'bar'}} + ) + + def setUp(self): + super(Fixture, self).setUp() + + get_os_certificate = { + 'certificate': { + 'private_key': None, + 'data': 'foo' + } + } + self.requests.register_uri('GET', self.url('root'), + json=get_os_certificate, + headers=self.json_headers) + + post_os_certificates = { + 'certificate': { + 'private_key': 'foo', + 'data': 'bar' + } + } + self.requests.register_uri('POST', self.url(), + json=post_os_certificates, + headers=self.json_headers) diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/client.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/client.py new file mode 100644 index 0000000000..5f933b7502 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/client.py @@ -0,0 +1,65 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 keystoneclient.auth.identity import v2 +from keystoneclient import fixture +from keystoneclient import session + +from novaclient.v2 import client as v2client + +IDENTITY_URL = 'http://identityserver:5000/v2.0' +COMPUTE_URL = 'http://compute.host' + + +class V1(fixtures.Fixture): + + def __init__(self, requests, + compute_url=COMPUTE_URL, identity_url=IDENTITY_URL): + super(V1, self).__init__() + self.identity_url = identity_url + self.compute_url = compute_url + self.client = None + self.requests = requests + + self.token = fixture.V2Token() + self.token.set_scope() + + s = self.token.add_service('compute') + s.add_endpoint(self.compute_url) + + s = self.token.add_service('computev3') + s.add_endpoint(self.compute_url) + + def setUp(self): + super(V1, self).setUp() + + auth_url = '%s/tokens' % self.identity_url + headers = {'X-Content-Type': 'application/json'} + self.requests.register_uri('POST', auth_url, + json=self.token, + headers=headers) + self.client = self.new_client() + + def new_client(self): + return v2client.Client(username='xx', + api_key='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 v2client.Client(session=self.session) diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/cloudpipe.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/cloudpipe.py new file mode 100644 index 0000000000..abe590563b --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/cloudpipe.py @@ -0,0 +1,37 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-cloudpipe' + + def setUp(self): + super(Fixture, self).setUp() + + get_os_cloudpipe = {'cloudpipes': [{'project_id': 1}]} + self.requests.register_uri('GET', self.url(), + json=get_os_cloudpipe, + headers=self.json_headers) + + instance_id = '9d5824aa-20e6-4b9f-b967-76a699fc51fd' + post_os_cloudpipe = {'instance_id': instance_id} + self.requests.register_uri('POST', self.url(), + json=post_os_cloudpipe, + headers=self.json_headers, + status_code=202) + + self.requests.register_uri('PUT', self.url('configure-project'), + headers=self.json_headers, + status_code=202) diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/fixedips.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/fixedips.py new file mode 100644 index 0000000000..fb677e91ce --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/fixedips.py @@ -0,0 +1,39 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-fixed-ips' + + def setUp(self): + super(Fixture, self).setUp() + + get_os_fixed_ips = { + "fixed_ip": { + 'cidr': '192.168.1.0/24', + 'address': '192.168.1.1', + 'hostname': 'foo', + 'host': 'bar' + } + } + + self.requests.register_uri('GET', self.url('192.168.1.1'), + json=get_os_fixed_ips, + headers=self.json_headers) + + self.requests.register_uri('POST', + self.url('192.168.1.1', 'action'), + headers=self.json_headers, + status_code=202) diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/floatingips.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/floatingips.py new file mode 100644 index 0000000000..10772a798d --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/floatingips.py @@ -0,0 +1,211 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo.serialization import jsonutils + +from novaclient.tests.unit import fakes +from novaclient.tests.unit.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} + self.requests.register_uri('GET', self.url(), + json=get_os_floating_ips, + headers=self.json_headers) + + for ip in floating_ips: + get_os_floating_ip = {'floating_ip': ip} + self.requests.register_uri('GET', self.url(ip['id']), + json=get_os_floating_ip, + headers=self.json_headers) + + self.requests.register_uri('DELETE', self.url(ip['id']), + headers=self.json_headers, + status_code=204) + + def post_os_floating_ips(request, context): + body = jsonutils.loads(request.body) + ip = floating_ips[0].copy() + ip['pool'] = body.get('pool') + return {'floating_ip': ip} + self.requests.register_uri('POST', self.url(), + json=post_os_floating_ips, + headers=self.json_headers) + + +class DNSFixture(base.Fixture): + + base_url = 'os-floating-ip-dns' + + def setUp(self): + super(DNSFixture, self).setUp() + + get_os_floating_ip_dns = { + 'domain_entries': [ + {'domain': 'example.org'}, + {'domain': 'example.com'} + ] + } + self.requests.register_uri('GET', self.url(), + json=get_os_floating_ip_dns, + headers=self.json_headers, + status_code=205) + + get_dns_testdomain_entries_testname = { + 'dns_entry': { + 'ip': "10.10.10.10", + 'name': 'testname', + 'type': "A", + 'domain': 'testdomain' + } + } + url = self.url('testdomain', 'entries', 'testname') + self.requests.register_uri('GET', url, + json=get_dns_testdomain_entries_testname, + headers=self.json_headers, + status_code=205) + + self.requests.register_uri('DELETE', self.url('testdomain')) + + url = self.url('testdomain', 'entries', 'testname') + self.requests.register_uri('DELETE', url) + + def put_dns_testdomain_entries_testname(request, context): + body = jsonutils.loads(request.body) + fakes.assert_has_keys(body['dns_entry'], + required=['ip', 'dns_type']) + context.status_code = 205 + return request.body + self.requests.register_uri('PUT', url, + text=put_dns_testdomain_entries_testname, + headers=self.json_headers) + + url = self.url('testdomain', 'entries') + self.requests.register_uri('GET', url, status_code=404) + + get_os_floating_ip_dns_testdomain = { + 'dns_entries': [ + { + 'dns_entry': { + 'ip': '1.2.3.4', + 'name': "host1", + 'type': "A", + 'domain': 'testdomain' + } + }, + { + 'dns_entry': { + 'ip': '1.2.3.4', + 'name': "host2", + 'type': "A", + 'domain': 'testdomain' + } + }, + ] + } + self.requests.register_uri('GET', url + '?ip=1.2.3.4', + json=get_os_floating_ip_dns_testdomain, + status_code=205, + headers=self.json_headers) + + def put_os_floating_ip_dns_testdomain(request, context): + body = jsonutils.loads(request.body) + if body['domain_entry']['scope'] == 'private': + fakes.assert_has_keys(body['domain_entry'], + required=['availability_zone', 'scope']) + elif body['domain_entry']['scope'] == 'public': + fakes.assert_has_keys(body['domain_entry'], + required=['project', 'scope']) + else: + fakes.assert_has_keys(body['domain_entry'], + required=['project', 'scope']) + + return request.body + + self.requests.register_uri('PUT', self.url('testdomain'), + text=put_os_floating_ip_dns_testdomain, + status_code=205, + headers=self.json_headers) + + +class BulkFixture(base.Fixture): + + base_url = 'os-floating-ips-bulk' + + def setUp(self): + super(BulkFixture, self).setUp() + + get_os_floating_ips_bulk = { + 'floating_ip_info': [ + {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, + {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, + ] + } + self.requests.register_uri('GET', self.url(), + json=get_os_floating_ips_bulk, + headers=self.json_headers) + self.requests.register_uri('GET', self.url('testHost'), + json=get_os_floating_ips_bulk, + headers=self.json_headers) + + def put_os_floating_ips_bulk_delete(request, context): + body = jsonutils.loads(request.body) + ip_range = body.get('ip_range') + return {'floating_ips_bulk_delete': ip_range} + + self.requests.register_uri('PUT', self.url('delete'), + json=put_os_floating_ips_bulk_delete, + headers=self.json_headers) + + def post_os_floating_ips_bulk(request, context): + body = jsonutils.loads(request.body) + params = body.get('floating_ips_bulk_create') + pool = params.get('pool', 'defaultPool') + interface = params.get('interface', 'defaultInterface') + return { + 'floating_ips_bulk_create': { + 'ip_range': '192.168.1.0/30', + 'pool': pool, + 'interface': interface + } + } + + self.requests.register_uri('POST', self.url(), + json=post_os_floating_ips_bulk, + headers=self.json_headers) + + +class PoolsFixture(base.Fixture): + + base_url = 'os-floating-ip-pools' + + def setUp(self): + super(PoolsFixture, self).setUp() + + get_os_floating_ip_pools = { + 'floating_ip_pools': [ + {'name': 'foo'}, + {'name': 'bar'} + ] + } + self.requests.register_uri('GET', self.url(), + json=get_os_floating_ip_pools, + headers=self.json_headers) diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/fping.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/fping.py new file mode 100644 index 0000000000..6c6cd4f89c --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/fping.py @@ -0,0 +1,46 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-fping' + + def setUp(self): + super(Fixture, self).setUp() + + get_os_fping_1 = { + 'server': { + "id": "1", + "project_id": "fake-project", + "alive": True, + } + } + self.requests.register_uri('GET', self.url(1), + json=get_os_fping_1, + headers=self.json_headers) + + get_os_fping = { + 'servers': [ + get_os_fping_1['server'], + { + "id": "2", + "project_id": "fake-project", + "alive": True, + }, + ] + } + self.requests.register_uri('GET', self.url(), + json=get_os_fping, + headers=self.json_headers) diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/hosts.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/hosts.py new file mode 100644 index 0000000000..5c1ff60a39 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/hosts.py @@ -0,0 +1,149 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo.serialization import jsonutils +from six.moves.urllib import parse + +from novaclient.tests.unit.fixture_data import base + + +class BaseFixture(base.Fixture): + + base_url = 'os-hosts' + + def setUp(self): + super(BaseFixture, self).setUp() + + get_os_hosts_host = { + 'host': [ + {'resource': {'project': '(total)', 'host': 'dummy', + 'cpu': 16, 'memory_mb': 32234, 'disk_gb': 128}}, + {'resource': {'project': '(used_now)', 'host': 'dummy', + 'cpu': 1, 'memory_mb': 2075, 'disk_gb': 45}}, + {'resource': {'project': '(used_max)', 'host': 'dummy', + 'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}}, + {'resource': {'project': 'admin', 'host': 'dummy', + 'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}} + ] + } + + headers = {'Content-Type': 'application/json'} + + self.requests.register_uri('GET', self.url('host'), + json=get_os_hosts_host, + headers=headers) + + def get_os_hosts(request, context): + host, query = parse.splitquery(request.url) + zone = 'nova1' + service = None + + if query: + qs = parse.parse_qs(query) + try: + zone = qs['zone'][0] + except Exception: + pass + + try: + service = qs['service'][0] + except Exception: + pass + + return { + 'hosts': [ + { + 'host': 'host1', + 'service': service or 'nova-compute', + 'zone': zone + }, + { + 'host': 'host1', + 'service': service or 'nova-cert', + 'zone': zone + } + ] + } + + self.requests.register_uri('GET', self.url(), + json=get_os_hosts, + headers=headers) + + get_os_hosts_sample_host = { + 'host': [ + {'resource': {'host': 'sample_host'}} + ], + } + self.requests.register_uri('GET', self.url('sample_host'), + json=get_os_hosts_sample_host, + headers=headers) + + self.requests.register_uri('PUT', self.url('sample_host', 1), + json=self.put_host_1(), + headers=headers) + + self.requests.register_uri('PUT', self.url('sample_host', 2), + json=self.put_host_2(), + headers=headers) + + self.requests.register_uri('PUT', self.url('sample_host', 3), + json=self.put_host_3(), + headers=headers) + + self.requests.register_uri('GET', self.url('sample_host', 'reboot'), + json=self.get_host_reboot(), + headers=headers) + + self.requests.register_uri('GET', self.url('sample_host', 'startup'), + json=self.get_host_startup(), + headers=headers) + + self.requests.register_uri('GET', self.url('sample_host', 'shutdown'), + json=self.get_host_shutdown(), + headers=headers) + + def put_os_hosts_sample_host(request, context): + result = {'host': 'dummy'} + result.update(jsonutils.loads(request.body)) + return result + + self.requests.register_uri('PUT', self.url('sample_host'), + json=put_os_hosts_sample_host, + headers=headers) + + +class V1(BaseFixture): + + def put_host_1(self): + return {'host': 'sample-host_1', + 'status': 'enabled'} + + def put_host_2(self): + return {'host': 'sample-host_2', + 'maintenance_mode': 'on_maintenance'} + + def put_host_3(self): + return {'host': 'sample-host_3', + 'status': 'enabled', + 'maintenance_mode': 'on_maintenance'} + + def get_host_reboot(self): + return {'host': 'sample_host', + 'power_action': 'reboot'} + + def get_host_startup(self): + return {'host': 'sample_host', + 'power_action': 'startup'} + + def get_host_shutdown(self): + return {'host': 'sample_host', + 'power_action': 'shutdown'} diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/hypervisors.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/hypervisors.py new file mode 100644 index 0000000000..62952f081f --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/hypervisors.py @@ -0,0 +1,182 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import base + + +class 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'}, + ] + } + + self.headers = {'Content-Type': 'application/json'} + + self.requests.register_uri('GET', self.url(), + json=get_os_hypervisors, + headers=self.headers) + + get_os_hypervisors_detail = { + 'hypervisors': [ + { + '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 + } + ] + } + + self.requests.register_uri('GET', self.url('detail'), + json=get_os_hypervisors_detail, + headers=self.headers) + + 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, + } + } + + self.requests.register_uri('GET', self.url('statistics'), + json=get_os_hypervisors_stats, + headers=self.headers) + + get_os_hypervisors_search = { + 'hypervisors': [ + {'id': 1234, 'hypervisor_hostname': 'hyper1'}, + {'id': 5678, 'hypervisor_hostname': 'hyper2'} + ] + } + + self.requests.register_uri('GET', self.url('hyper', 'search'), + json=get_os_hypervisors_search, + headers=self.headers) + + 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'} + ] + } + ] + } + + self.requests.register_uri('GET', self.url('hyper', 'servers'), + json=get_hyper_server, + headers=self.headers) + + 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 + } + } + + self.requests.register_uri('GET', self.url(1234), + json=get_os_hypervisors_1234, + headers=self.headers) + + get_os_hypervisors_uptime = { + 'hypervisor': { + 'id': 1234, + 'hypervisor_hostname': 'hyper1', + 'uptime': 'fake uptime' + } + } + + self.requests.register_uri('GET', self.url(1234, 'uptime'), + json=get_os_hypervisors_uptime, + headers=self.headers) diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/images.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/images.py new file mode 100644 index 0000000000..4ec472581c --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/images.py @@ -0,0 +1,86 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo.serialization import jsonutils + +from novaclient.tests.unit import fakes +from novaclient.tests.unit.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'} + ] + } + + headers = {'Content-Type': 'application/json'} + + self.requests.register_uri('GET', self.url(), + json=get_images, + headers=headers) + + 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": {}, + } + + self.requests.register_uri('GET', self.url('detail'), + json={'images': [image_1, image_2]}, + headers=headers) + + self.requests.register_uri('GET', self.url(1), + json={'image': image_1}, + headers=headers) + + def post_images_1_metadata(request, context): + body = jsonutils.loads(request.body) + assert list(body) == ['metadata'] + fakes.assert_has_keys(body['metadata'], required=['test_key']) + return {'metadata': image_1['metadata']} + + self.requests.register_uri('POST', self.url(1, 'metadata'), + json=post_images_1_metadata, + headers=headers) + + for u in (1, '1/metadata/test_key'): + self.requests.register_uri('DELETE', self.url(u), status_code=204) + + +class V3(V1): + + base_url = 'v1/images' diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/keypairs.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/keypairs.py new file mode 100644 index 0000000000..9314c58a59 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/keypairs.py @@ -0,0 +1,47 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo.serialization import jsonutils + +from novaclient.tests.unit import fakes +from novaclient.tests.unit.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'} + + headers = {'Content-Type': 'application/json'} + + self.requests.register_uri('GET', self.url(), + json={'keypairs': [keypair]}, + headers=headers) + + self.requests.register_uri('GET', self.url('test'), + json={'keypair': keypair}, + headers=headers) + + self.requests.register_uri('DELETE', self.url('test'), status_code=202) + + def post_os_keypairs(request, context): + body = jsonutils.loads(request.body) + assert list(body) == ['keypair'] + fakes.assert_has_keys(body['keypair'], required=['name']) + return {'keypair': keypair} + + self.requests.register_uri('POST', self.url(), + json=post_os_keypairs, + headers=headers) diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/limits.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/limits.py new file mode 100644 index 0000000000..55d324d955 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/limits.py @@ -0,0 +1,80 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = '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 + }, + }, + } + + headers = {'Content-Type': 'application/json'} + self.requests.register_uri('GET', self.url(), + json=get_limits, + headers=headers) diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/networks.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/networks.py new file mode 100644 index 0000000000..9ef692c3ee --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/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. + +from oslo.serialization import jsonutils + +from novaclient.tests.unit.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-networks' + + def setUp(self): + super(Fixture, self).setUp() + + get_os_networks = { + 'networks': [ + { + "label": "1", + "cidr": "10.0.0.0/24", + 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1' + } + ] + } + + headers = {'Content-Type': 'application/json'} + + self.requests.register_uri('GET', self.url(), + json=get_os_networks, + headers=headers) + + def post_os_networks(request, context): + body = jsonutils.loads(request.body) + return {'network': body} + + self.requests.register_uri("POST", self.url(), + json=post_os_networks, + headers=headers) + + get_os_networks_1 = {'network': {"label": "1", "cidr": "10.0.0.0/24"}} + + self.requests.register_uri('GET', self.url(1), + json=get_os_networks_1, + headers=headers) + + self.requests.register_uri('DELETE', + self.url('networkdelete'), + status_code=202) + + for u in ('add', 'networkdisassociate/action', 'networktest/action', + '1/action', '2/action'): + self.requests.register_uri('POST', self.url(u), status_code=202) diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/quotas.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/quotas.py new file mode 100644 index 0000000000..83f448f49c --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/quotas.py @@ -0,0 +1,66 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.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 = {'quota_set': self.test_quota('test')} + self.headers = {'Content-Type': 'application/json'} + + for u in ('test', 'tenant-id', 'tenant-id/defaults', + '%s/defaults' % uuid2): + self.requests.register_uri('GET', self.url(u), + json=test_json, + headers=self.headers) + + self.requests.register_uri('PUT', self.url(uuid), + json={'quota_set': self.test_quota(uuid)}, + headers=self.headers) + + self.requests.register_uri('GET', self.url(uuid), + json={'quota_set': self.test_quota(uuid)}, + headers=self.headers) + + self.requests.register_uri('PUT', self.url(uuid2), + json={'quota_set': self.test_quota(uuid2)}, + headers=self.headers) + self.requests.register_uri('GET', self.url(uuid2), + json={'quota_set': self.test_quota(uuid2)}, + headers=self.headers) + + for u in ('test', uuid2): + self.requests.register_uri('DELETE', self.url(u), status_code=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 + } diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/security_group_rules.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/security_group_rules.py new file mode 100644 index 0000000000..5f1d2fad5b --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/security_group_rules.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. + +from oslo.serialization import jsonutils + +from novaclient.tests.unit import fakes +from novaclient.tests.unit.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-security-group-rules' + + def setUp(self): + super(Fixture, self).setUp() + + rule = { + 'id': 1, + 'parent_group_id': 1, + 'group_id': 2, + 'ip_protocol': 'TCP', + 'from_port': '22', + 'to_port': 22, + 'cidr': '10.0.0.0/8' + } + + headers = {'Content-Type': 'application/json'} + + self.requests.register_uri('GET', self.url(), + json={'security_group_rules': [rule]}, + headers=headers) + + for u in (1, 11, 12): + self.requests.register_uri('DELETE', self.url(u), status_code=202) + + def post_rules(request, context): + body = jsonutils.loads(request.body) + assert list(body) == ['security_group_rule'] + fakes.assert_has_keys(body['security_group_rule'], + required=['parent_group_id'], + optional=['group_id', 'ip_protocol', + 'from_port', 'to_port', 'cidr']) + + return {'security_group_rule': rule} + + self.requests.register_uri('POST', self.url(), + json=post_rules, + headers=headers, + status_code=202) diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/security_groups.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/security_groups.py new file mode 100644 index 0000000000..ac5d182c6f --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/security_groups.py @@ -0,0 +1,100 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo.serialization import jsonutils + +from novaclient.tests.unit import fakes +from novaclient.tests.unit.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-security-groups' + + def setUp(self): + super(Fixture, self).setUp() + + security_group_1 = { + "name": "test", + "description": "FAKE_SECURITY_GROUP", + "tenant_id": "4ffc664c198e435e9853f2538fbcd7a7", + "id": 1, + "rules": [ + { + "id": 11, + "group": {}, + "ip_protocol": "TCP", + "from_port": 22, + "to_port": 22, + "parent_group_id": 1, + "ip_range": {"cidr": "10.0.0.0/8"} + }, + { + "id": 12, + "group": { + "tenant_id": "272bee4c1e624cd4a72a6b0ea55b4582", + "name": "test2" + }, + "ip_protocol": "TCP", + "from_port": 222, + "to_port": 222, + "parent_group_id": 1, + "ip_range": {} + } + ] + } + + security_group_2 = { + "name": "test2", + "description": "FAKE_SECURITY_GROUP2", + "tenant_id": "272bee4c1e624cd4a72a6b0ea55b4582", + "id": 2, + "rules": [] + } + + get_groups = {'security_groups': [security_group_1, security_group_2]} + headers = {'Content-Type': 'application/json'} + + self.requests.register_uri('GET', self.url(), + json=get_groups, + headers=headers) + + get_group_1 = {'security_group': security_group_1} + self.requests.register_uri('GET', self.url(1), + json=get_group_1, + headers=headers) + + self.requests.register_uri('DELETE', self.url(1), status_code=202) + + def post_os_security_groups(request, context): + body = jsonutils.loads(request.body) + assert list(body) == ['security_group'] + fakes.assert_has_keys(body['security_group'], + required=['name', 'description']) + return {'security_group': security_group_1} + + self.requests.register_uri('POST', self.url(), + json=post_os_security_groups, + headers=headers, + status_code=202) + + def put_os_security_groups_1(request, context): + body = jsonutils.loads(request.body) + assert list(body) == ['security_group'] + fakes.assert_has_keys(body['security_group'], + required=['name', 'description']) + return body + + self.requests.register_uri('PUT', self.url(1), + json=put_os_security_groups_1, + headers=headers, + status_code=205) diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/server_groups.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/server_groups.py new file mode 100644 index 0000000000..1fcfdea55c --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/server_groups.py @@ -0,0 +1,74 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo.serialization import jsonutils + +from novaclient.tests.unit.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" + } + ] + + headers = {'Content-Type': 'application/json'} + + self.requests.register_uri('GET', self.url(), + json={'server_groups': server_groups}, + headers=headers) + + server = server_groups[0] + server_j = jsonutils.dumps({'server_group': server}) + + def _register(method, *args): + self.requests.register_uri(method, self.url(*args), text=server_j) + + _register('POST') + _register('POST', server['id']) + _register('GET', server['id']) + _register('PUT', server['id']) + _register('POST', server['id'], '/action') + + self.requests.register_uri('DELETE', self.url(server['id']), + status_code=202) diff --git a/awx/lib/site-packages/novaclient/tests/unit/fixture_data/servers.py b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/servers.py new file mode 100644 index 0000000000..3766f4290e --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/fixture_data/servers.py @@ -0,0 +1,427 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo.serialization import jsonutils + +from novaclient.tests.unit import fakes +from novaclient.tests.unit.fixture_data import base +from novaclient.tests.unit.v2 import fakes as v2_fakes + + +class Base(base.Fixture): + + base_url = 'servers' + + def setUp(self): + super(Base, self).setUp() + + get_servers = { + "servers": [ + {'id': 1234, 'name': 'sample-server'}, + {'id': 5678, 'name': 'sample-server2'} + ] + } + + self.requests.register_uri('GET', self.url(), + json=get_servers, + headers=self.json_headers) + + self.server_1234 = { + "id": 1234, + "name": "sample-server", + "image": { + "id": 2, + "name": "sample image", + }, + "flavor": { + "id": 1, + "name": "256 MB Server", + }, + "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0", + "status": "BUILD", + "progress": 60, + "addresses": { + "public": [ + { + "version": 4, + "addr": "1.2.3.4", + }, + { + "version": 4, + "addr": "5.6.7.8", + }], + "private": [{ + "version": 4, + "addr": "10.11.12.13", + }], + }, + "metadata": { + "Server Label": "Web Head 1", + "Image Version": "2.1" + }, + "OS-EXT-SRV-ATTR:host": "computenode1", + "security_groups": [{ + 'id': 1, 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }], + "OS-EXT-MOD:some_thing": "mod_some_thing_value", + } + + self.server_5678 = { + "id": 5678, + "name": "sample-server2", + "image": { + "id": 2, + "name": "sample image", + }, + "flavor": { + "id": 1, + "name": "256 MB Server", + }, + "hostId": "9e107d9d372bb6826bd81d3542a419d6", + "status": "ACTIVE", + "addresses": { + "public": [ + { + "version": 4, + "addr": "4.5.6.7", + }, + { + "version": 4, + "addr": "5.6.9.8", + }], + "private": [{ + "version": 4, + "addr": "10.13.12.13", + }], + }, + "metadata": { + "Server Label": "DB 1" + }, + "OS-EXT-SRV-ATTR:host": "computenode2", + "security_groups": [ + { + 'id': 1, 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }, + { + 'id': 2, 'name': 'securitygroup2', + 'description': 'ANOTHER_FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }], + } + + self.server_9012 = { + "id": 9012, + "name": "sample-server3", + "image": "", + "flavor": { + "id": 1, + "name": "256 MB Server", + }, + "hostId": "9e107d9d372bb6826bd81d3542a419d6", + "status": "ACTIVE", + "addresses": { + "public": [ + { + "version": 4, + "addr": "4.5.6.7", + }, + { + "version": 4, + "addr": "5.6.9.8", + }], + "private": [{ + "version": 4, + "addr": "10.13.12.13", + }], + }, + "metadata": { + "Server Label": "DB 1" + } + } + + servers = [self.server_1234, self.server_5678, self.server_9012] + get_servers_detail = {"servers": servers} + + self.requests.register_uri('GET', self.url('detail'), + json=get_servers_detail, + headers=self.json_headers) + + self.server_1235 = self.server_1234.copy() + self.server_1235['id'] = 1235 + self.server_1235['status'] = 'error' + self.server_1235['fault'] = {'message': 'something went wrong!'} + + for s in servers + [self.server_1235]: + self.requests.register_uri('GET', self.url(s['id']), + json={'server': s}, + headers=self.json_headers) + + for s in (1234, 5678): + self.requests.register_uri('DELETE', self.url(s), status_code=202) + + for k in ('test_key', 'key1', 'key2'): + self.requests.register_uri('DELETE', + self.url(1234, 'metadata', k), + status_code=204) + + metadata1 = {'metadata': {'test_key': 'test_value'}} + self.requests.register_uri('POST', self.url(1234, 'metadata'), + json=metadata1, + headers=self.json_headers) + self.requests.register_uri('PUT', + self.url(1234, 'metadata', 'test_key'), + json=metadata1, + headers=self.json_headers) + + self.diagnostic = {'data': 'Fake diagnostics'} + + metadata2 = {'metadata': {'key1': 'val1'}} + for u in ('uuid1', 'uuid2', 'uuid3', 'uuid4'): + self.requests.register_uri('POST', self.url(u, 'metadata'), + json=metadata2, status_code=204) + self.requests.register_uri('DELETE', + self.url(u, 'metadata', 'key1'), + json=self.diagnostic, + headers=self.json_headers) + + get_security_groups = { + "security_groups": [{ + 'id': 1, + 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'rules': []}] + } + + self.requests.register_uri('GET', + self.url('1234', 'os-security-groups'), + json=get_security_groups) + + self.requests.register_uri('POST', self.url(), + json=self.post_servers, + headers=self.json_headers) + + self.requests.register_uri('POST', self.url('1234', 'action'), + json=self.post_servers_1234_action, + headers=self.json_headers) + + get_os_interface = { + "interfaceAttachments": [ + { + "port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + }, + { + "port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + } + ] + } + + self.requests.register_uri('GET', + self.url('1234', 'os-interface'), + json=get_os_interface, + headers=self.json_headers) + + interface_data = {'interfaceAttachment': {}} + self.requests.register_uri('POST', + self.url('1234', 'os-interface'), + json=interface_data, + headers=self.json_headers) + + def put_servers_1234(request, context): + body = jsonutils.loads(request.body) + assert list(body) == ['server'] + fakes.assert_has_keys(body['server'], + optional=['name', 'adminPass']) + return request.body + + self.requests.register_uri('PUT', self.url(1234), + text=put_servers_1234, + status_code=204, + headers=self.json_headers) + + def post_os_volumes_boot(request, context): + body = jsonutils.loads(request.body) + assert (set(body.keys()) <= + set(['server', 'os:scheduler_hints'])) + + fakes.assert_has_keys(body['server'], + required=['name', 'flavorRef'], + optional=['imageRef']) + + data = body['server'] + + # Require one, and only one, of the keys for bdm + if 'block_device_mapping' not in data: + if 'block_device_mapping_v2' not in data: + msg = "missing required keys: 'block_device_mapping'" + raise AssertionError(msg) + elif 'block_device_mapping_v2' in data: + msg = "found extra keys: 'block_device_mapping'" + raise AssertionError(msg) + + return {'server': self.server_9012} + + # NOTE(jamielennox): hack to make os_volumes mock go to the right place + base_url = self.base_url + self.base_url = None + self.requests.register_uri('POST', self.url('os-volumes_boot'), + json=post_os_volumes_boot, + status_code=202, + headers=self.json_headers) + self.base_url = base_url + + # + # Server password + # + + self.requests.register_uri('DELETE', + self.url(1234, 'os-server-password'), + status_code=202) + + +class V1(Base): + + def setUp(self): + super(V1, self).setUp() + + # + # Server Addresses + # + + add = self.server_1234['addresses'] + self.requests.register_uri('GET', self.url(1234, 'ips'), + json={'addresses': add}, + headers=self.json_headers) + + self.requests.register_uri('GET', self.url(1234, 'ips', 'public'), + json={'public': add['public']}, + headers=self.json_headers) + + self.requests.register_uri('GET', self.url(1234, 'ips', 'private'), + json={'private': add['private']}, + headers=self.json_headers) + + self.requests.register_uri('DELETE', + self.url(1234, 'ips', 'public', '1.2.3.4'), + status_code=202) + + self.requests.register_uri('GET', + self.url('1234', 'diagnostics'), + json=self.diagnostic) + + self.requests.register_uri('DELETE', + self.url('1234', 'os-interface', 'port-id')) + + # Testing with the following password and key + # + # Clear password: FooBar123 + # + # RSA Private Key: novaclient/tests/unit/idfake.pem + # + # Encrypted password + # OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r + # qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho + # QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw + # /y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N + # tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk + # Hi/fmZZNQQqj1Ijq0caOIw== + + get_server_password = { + 'password': + 'OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r' + 'qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho' + 'QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw' + '/y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N' + 'tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk' + 'Hi/fmZZNQQqj1Ijq0caOIw=='} + self.requests.register_uri('GET', + self.url(1234, 'os-server-password'), + json=get_server_password) + + def post_servers(self, request, context): + body = jsonutils.loads(request.body) + context.status_code = 202 + assert (set(body.keys()) <= + set(['server', 'os:scheduler_hints'])) + fakes.assert_has_keys(body['server'], + required=['name', 'imageRef', 'flavorRef'], + optional=['metadata', 'personality']) + if 'personality' in body['server']: + for pfile in body['server']['personality']: + fakes.assert_has_keys(pfile, required=['path', 'contents']) + if body['server']['name'] == 'some-bad-server': + body = self.server_1235 + else: + body = self.server_1234 + + return {'server': body} + + def post_servers_1234_action(self, request, context): + _body = '' + body = jsonutils.loads(request.body) + context.status_code = 202 + assert len(body.keys()) == 1 + action = list(body)[0] + + if v2_fakes.FakeHTTPClient.check_server_actions(body): + # NOTE(snikitin): No need to do any operations here. This 'pass' + # is needed to avoid AssertionError in the last 'else' statement + # if we found 'action' in method check_server_actions and + # raise AssertionError if we didn't find 'action' at all. + pass + elif action == 'rebuild': + body = body[action] + adminPass = body.get('adminPass', 'randompassword') + assert 'imageRef' in body + _body = self.server_1234.copy() + _body['adminPass'] = adminPass + elif action == 'confirmResize': + assert body[action] is None + # This one method returns a different response code + context.status_code = 204 + return None + elif action == 'rescue': + if body[action]: + keys = set(body[action].keys()) + assert not (keys - set(['adminPass', 'rescue_image_ref'])) + else: + assert body[action] is None + _body = {'adminPass': 'RescuePassword'} + elif action == 'createImage': + assert set(body[action].keys()) == set(['name', 'metadata']) + context.headers['location'] = "http://blah/images/456" + elif action == 'os-getConsoleOutput': + assert list(body[action]) == ['length'] + context.status_code = 202 + return {'output': 'foo'} + elif action == 'os-getSerialConsole': + assert list(body[action]) == ['type'] + elif action == 'evacuate': + keys = list(body[action]) + if 'adminPass' in keys: + keys.remove('adminPass') + assert set(keys) == set(['host', 'onSharedStorage']) + else: + raise AssertionError("Unexpected server action: %s" % action) + return {'server': _body} diff --git a/awx/lib/site-packages/novaclient/tests/unit/idfake.pem b/awx/lib/site-packages/novaclient/tests/unit/idfake.pem new file mode 100644 index 0000000000..f7b4466419 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/idfake.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA9QstF/7prDY7a9La7GS9TpMX+MWWXQgK6pHRLakDFp1WX1Q3 +Vly7rWitaZUGirUPMm181oJXBwkKlAxFD7hKjyHYaSswNszPYIAsVkc1+AO5epXz +g9kUBNtfg44Pg72UecwLrZ8JpmNZpJlKQOx6vF+yi7JmHrrIf6il/grIGUPzoT2L +yReimpyPoBrGtXhJYaCJ/XbKg1idRZiQdmwh1F/OmZWn9p0wunnsv08a0+qIywuw +WhG9/Zy9fjnEByfusS6gI0GIxDRL4RWzOqphd3PZzunwIBgEKFhgiki9+2DgcRVO +9I5wnDvfwQREJRZWh1uJa5ZTcfPa1EzZryVeOQIDAQABAoIBABxO3Te/cBk/7p9n +LXlPrfrszUEk+ljm+/PbQpIGy1+Kb5b1sKrebaP7ysS+vZG6lvXZZimVxx398mXm +APhu7tYYL9r+bUR3ZqGcTQLumRJ8w6mgtxANPN3Oxfr5p1stxIBJjTPSgpfhNFLq +joRvjUJDv+mZg2ibZVwyDHMLpdAdKp+3XMdyTLZcH9esqwii+natix7rHd1RuF85 +L1dfpxjkItwhgHsfdYS++5X3fRByFOhQ+Nhabh/kPQbQMcteRn1bN6zeCWBSglNb +Ka/ZrXb6ApRUc22Ji62mNO2ZPPekLJeCHk2h2E7ezYX+sGDNvvd/jHVDJJ20FjD1 +Z9KXuK0CgYEA/2vniy9yWd925QQtWbmrxgy6yj89feMH/LTv4qP298rGZ2nqxsyd +9pdBdb4NMsi4HmV5PG1hp3VRNBHl53DNh5eqzT8WEXnIF+sbrIU3KzrCVAx1kZTl ++OWKA6aVUsvvO3y85SOvInnsV+IsOGmU4/WBSjYoe39Bo7mq/YuZB9MCgYEA9ZlB +KBm6PjFdHQGNgedXahWzRcwC+ALCYqequPYqJolNzhrK4Uc2sWPSGdnldcHZ4XCQ +wbfCxUSwrMpA1oyuIQ0U4aowmOw5DjIueBWI8XBYEVRBlwvJwbXpBZ/DspGzTUDx +MBrrEwEaMadQvxhRnAzhp0rQAepatcz6Fgb1JkMCgYBMwDLiew5kfSav6JJsDMPW +DksurNQgeNEUmZYfx19V1EPMHWKj/CZXS9oqtEIpCXFyCNHmW4PlmvYcrGgmJJpN +7UAwzo0mES8UKNy2+Yy7W7u7H8dQSKrWILtZH3xtVcR8Xp4wSIm+1V40hkz9YpSP +71y7XQzLF1E1DnyYFZOVawKBgAFrmHfd5jjT2kD/sEzPBK9lXrsJmf7LLUqaw578 +NXQxmRSXDRNOcR+Hf0CNBQmwTE1EdGHaaTLw2cC2Drfu6lbgl31SmaNYwl+1pJUn +MrqKtseq4BI6jDkljypsKRqQQyQwOvTXQwLCH9+nowzn3Bj17hwkj51jOJESlWOp +OKO3AoGBALm+jjqyqX7gSnqK3FAumB8mlhv3yI1Wr1ctwe18mKfKbz17HxXRu9pF +K/6e7WMCA1p+jhoE8gj1h2WBcH0nV2qt8Ye8gJBbCi4dhI08o4AfrIV47oZx1RlO +qYcA1U9lyaODY5SL8+6PHOy5J/aYtuA+wvfEnWiCIdKQrhWetcn3 +-----END RSA PRIVATE KEY----- diff --git a/awx/lib/site-packages/novaclient/tests/unit/test_auth_plugins.py b/awx/lib/site-packages/novaclient/tests/unit/test_auth_plugins.py new file mode 100644 index 0000000000..675102ca35 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/test_auth_plugins.py @@ -0,0 +1,328 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse + +from keystoneclient import fixture +import mock +import pkg_resources +import requests + +try: + import json +except ImportError: + import simplejson as json + +from novaclient import auth_plugin +from novaclient import exceptions +from novaclient.tests.unit import utils +from novaclient.v2 import client + + +def mock_http_request(resp=None): + """Mock an HTTP Request.""" + if not resp: + resp = fixture.V2Token() + resp.set_scope() + s = resp.add_service('compute') + s.add_endpoint("http://localhost:8774/v1.1", region='RegionOne') + + auth_response = utils.TestResponse({ + "status_code": 200, + "text": json.dumps(resp), + }) + return mock.Mock(return_value=(auth_response)) + + +def requested_headers(cs): + """Return requested passed headers.""" + return { + 'User-Agent': cs.client.USER_AGENT, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + + +class DeprecatedAuthPluginTest(utils.TestCase): + def test_auth_system_success(self): + class MockEntrypoint(pkg_resources.EntryPoint): + def load(self): + return self.authenticate + + def authenticate(self, cls, auth_url): + cls._authenticate(auth_url, {"fake": "me"}) + + def mock_iter_entry_points(_type, name): + if _type == 'openstack.client.authenticate': + return [MockEntrypoint("fake", "fake", ["fake"])] + else: + return [] + + mock_request = mock_http_request() + + @mock.patch.object(pkg_resources, "iter_entry_points", + mock_iter_entry_points) + @mock.patch.object(requests, "request", mock_request) + def test_auth_call(): + plugin = auth_plugin.DeprecatedAuthPlugin("fake") + cs = client.Client("username", "password", "project_id", + utils.AUTH_URL_V2, auth_system="fake", + auth_plugin=plugin) + cs.client.authenticate() + + headers = requested_headers(cs) + token_url = cs.client.auth_url + "/tokens" + + mock_request.assert_called_with( + "POST", + token_url, + headers=headers, + data='{"fake": "me"}', + allow_redirects=True, + **self.TEST_REQUEST_BASE) + + test_auth_call() + + def test_auth_system_not_exists(self): + def mock_iter_entry_points(_t, name=None): + return [pkg_resources.EntryPoint("fake", "fake", ["fake"])] + + mock_request = mock_http_request() + + @mock.patch.object(pkg_resources, "iter_entry_points", + mock_iter_entry_points) + @mock.patch.object(requests, "request", mock_request) + def test_auth_call(): + auth_plugin.discover_auth_systems() + plugin = auth_plugin.DeprecatedAuthPlugin("notexists") + cs = client.Client("username", "password", "project_id", + utils.AUTH_URL_V2, auth_system="notexists", + auth_plugin=plugin) + self.assertRaises(exceptions.AuthSystemNotFound, + cs.client.authenticate) + + test_auth_call() + + def test_auth_system_defining_auth_url(self): + class MockAuthUrlEntrypoint(pkg_resources.EntryPoint): + def load(self): + return self.auth_url + + def auth_url(self): + return "http://faked/v2.0" + + class MockAuthenticateEntrypoint(pkg_resources.EntryPoint): + def load(self): + return self.authenticate + + def authenticate(self, cls, auth_url): + cls._authenticate(auth_url, {"fake": "me"}) + + def mock_iter_entry_points(_type, name): + if _type == 'openstack.client.auth_url': + return [MockAuthUrlEntrypoint("fakewithauthurl", + "fakewithauthurl", + ["auth_url"])] + elif _type == 'openstack.client.authenticate': + return [MockAuthenticateEntrypoint("fakewithauthurl", + "fakewithauthurl", + ["authenticate"])] + else: + return [] + + mock_request = mock_http_request() + + @mock.patch.object(pkg_resources, "iter_entry_points", + mock_iter_entry_points) + @mock.patch.object(requests, "request", mock_request) + def test_auth_call(): + plugin = auth_plugin.DeprecatedAuthPlugin("fakewithauthurl") + cs = client.Client("username", "password", "project_id", + auth_system="fakewithauthurl", + auth_plugin=plugin) + cs.client.authenticate() + self.assertEqual("http://faked/v2.0", cs.client.auth_url) + + test_auth_call() + + @mock.patch.object(pkg_resources, "iter_entry_points") + def test_client_raises_exc_without_auth_url(self, mock_iter_entry_points): + class MockAuthUrlEntrypoint(pkg_resources.EntryPoint): + def load(self): + return self.auth_url + + def auth_url(self): + return None + + mock_iter_entry_points.side_effect = lambda _t, name: [ + MockAuthUrlEntrypoint("fakewithauthurl", + "fakewithauthurl", + ["auth_url"])] + + plugin = auth_plugin.DeprecatedAuthPlugin("fakewithauthurl") + self.assertRaises( + exceptions.EndpointNotFound, + client.Client, "username", "password", "project_id", + auth_system="fakewithauthurl", auth_plugin=plugin) + + +class AuthPluginTest(utils.TestCase): + @mock.patch.object(requests, "request") + @mock.patch.object(pkg_resources, "iter_entry_points") + def test_auth_system_success(self, mock_iter_entry_points, mock_request): + """Test that we can authenticate using the auth system.""" + class MockEntrypoint(pkg_resources.EntryPoint): + def load(self): + return FakePlugin + + class FakePlugin(auth_plugin.BaseAuthPlugin): + def authenticate(self, cls, auth_url): + cls._authenticate(auth_url, {"fake": "me"}) + + mock_iter_entry_points.side_effect = lambda _t: [ + MockEntrypoint("fake", "fake", ["FakePlugin"])] + + mock_request.side_effect = mock_http_request() + + auth_plugin.discover_auth_systems() + plugin = auth_plugin.load_plugin("fake") + cs = client.Client("username", "password", "project_id", + utils.AUTH_URL_V2, auth_system="fake", + auth_plugin=plugin) + cs.client.authenticate() + + headers = requested_headers(cs) + token_url = cs.client.auth_url + "/tokens" + + mock_request.assert_called_with( + "POST", + token_url, + headers=headers, + data='{"fake": "me"}', + allow_redirects=True, + **self.TEST_REQUEST_BASE) + + @mock.patch.object(pkg_resources, "iter_entry_points") + def test_discover_auth_system_options(self, mock_iter_entry_points): + """Test that we can load the auth system options.""" + class FakePlugin(auth_plugin.BaseAuthPlugin): + @staticmethod + def add_opts(parser): + parser.add_argument('--auth_system_opt', + default=False, + action='store_true', + help="Fake option") + return parser + + class MockEntrypoint(pkg_resources.EntryPoint): + def load(self): + return FakePlugin + + mock_iter_entry_points.side_effect = lambda _t: [ + MockEntrypoint("fake", "fake", ["FakePlugin"])] + + parser = argparse.ArgumentParser() + auth_plugin.discover_auth_systems() + auth_plugin.load_auth_system_opts(parser) + opts, args = parser.parse_known_args(['--auth_system_opt']) + + self.assertTrue(opts.auth_system_opt) + + @mock.patch.object(pkg_resources, "iter_entry_points") + def test_parse_auth_system_options(self, mock_iter_entry_points): + """Test that we can parse the auth system options.""" + class MockEntrypoint(pkg_resources.EntryPoint): + def load(self): + return FakePlugin + + class FakePlugin(auth_plugin.BaseAuthPlugin): + def __init__(self): + self.opts = {"fake_argument": True} + + def parse_opts(self, args): + return self.opts + + mock_iter_entry_points.side_effect = lambda _t: [ + MockEntrypoint("fake", "fake", ["FakePlugin"])] + + auth_plugin.discover_auth_systems() + plugin = auth_plugin.load_plugin("fake") + + plugin.parse_opts([]) + self.assertIn("fake_argument", plugin.opts) + + @mock.patch.object(pkg_resources, "iter_entry_points") + def test_auth_system_defining_url(self, mock_iter_entry_points): + """Test the auth_system defining an url.""" + class MockEntrypoint(pkg_resources.EntryPoint): + def load(self): + return FakePlugin + + class FakePlugin(auth_plugin.BaseAuthPlugin): + def get_auth_url(self): + return "http://faked/v2.0" + + mock_iter_entry_points.side_effect = lambda _t: [ + MockEntrypoint("fake", "fake", ["FakePlugin"])] + + auth_plugin.discover_auth_systems() + plugin = auth_plugin.load_plugin("fake") + + cs = client.Client("username", "password", "project_id", + auth_system="fakewithauthurl", + auth_plugin=plugin) + self.assertEqual("http://faked/v2.0", cs.client.auth_url) + + @mock.patch.object(pkg_resources, "iter_entry_points") + def test_exception_if_no_authenticate(self, mock_iter_entry_points): + """Test that no authenticate raises a proper exception.""" + class MockEntrypoint(pkg_resources.EntryPoint): + def load(self): + return FakePlugin + + class FakePlugin(auth_plugin.BaseAuthPlugin): + pass + + mock_iter_entry_points.side_effect = lambda _t: [ + MockEntrypoint("fake", "fake", ["FakePlugin"])] + + auth_plugin.discover_auth_systems() + plugin = auth_plugin.load_plugin("fake") + + self.assertRaises( + exceptions.EndpointNotFound, + client.Client, "username", "password", "project_id", + auth_system="fake", auth_plugin=plugin) + + @mock.patch.object(pkg_resources, "iter_entry_points") + def test_exception_if_no_url(self, mock_iter_entry_points): + """Test that no auth_url at all raises exception.""" + class MockEntrypoint(pkg_resources.EntryPoint): + def load(self): + return FakePlugin + + class FakePlugin(auth_plugin.BaseAuthPlugin): + pass + + mock_iter_entry_points.side_effect = lambda _t: [ + MockEntrypoint("fake", "fake", ["FakePlugin"])] + + auth_plugin.discover_auth_systems() + plugin = auth_plugin.load_plugin("fake") + + self.assertRaises( + exceptions.EndpointNotFound, + client.Client, "username", "password", "project_id", + auth_system="fake", auth_plugin=plugin) diff --git a/awx/lib/site-packages/novaclient/tests/unit/test_base.py b/awx/lib/site-packages/novaclient/tests/unit/test_base.py new file mode 100644 index 0000000000..1ba1d32b00 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/test_base.py @@ -0,0 +1,69 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base +from novaclient import exceptions +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import flavors + + +cs = fakes.FakeClient() + + +class BaseTest(utils.TestCase): + + def test_resource_repr(self): + r = base.Resource(None, dict(foo="bar", baz="spam")) + self.assertEqual("", repr(r)) + + def test_getid(self): + self.assertEqual(4, base.getid(4)) + + class TmpObject(object): + id = 4 + self.assertEqual(4, base.getid(TmpObject)) + + def test_resource_lazy_getattr(self): + f = flavors.Flavor(cs.flavors, {'id': 1}) + self.assertEqual('256 mb server', f.name) + cs.assert_called('GET', '/flavors/1') + + # Missing stuff still fails after a second get + self.assertRaises(AttributeError, getattr, f, 'blahblah') + + def test_eq(self): + # Two resources of the same type with the same id: equal + r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) + r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) + self.assertEqual(r1, r2) + + # Two resoruces of different types: never equal + r1 = base.Resource(None, {'id': 1}) + r2 = flavors.Flavor(None, {'id': 1}) + self.assertNotEqual(r1, r2) + + # Two resources with no ID: equal if their info is equal + r1 = base.Resource(None, {'name': 'joe', 'age': 12}) + r2 = base.Resource(None, {'name': 'joe', 'age': 12}) + self.assertEqual(r1, r2) + + def test_findall_invalid_attribute(self): + # Make sure findall with an invalid attribute doesn't cause errors. + # The following should not raise an exception. + cs.flavors.findall(vegetable='carrot') + + # However, find() should raise an error + self.assertRaises(exceptions.NotFound, + cs.flavors.find, + vegetable='carrot') diff --git a/awx/lib/site-packages/novaclient/tests/unit/test_client.py b/awx/lib/site-packages/novaclient/tests/unit/test_client.py new file mode 100644 index 0000000000..d240321f7e --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/test_client.py @@ -0,0 +1,390 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import json +import logging +import socket + +import fixtures +import mock +import requests + +import novaclient.client +import novaclient.extension +from novaclient.tests.unit import utils +import novaclient.v2.client + + +class TCPKeepAliveAdapterTest(utils.TestCase): + + @mock.patch.object(requests.adapters.HTTPAdapter, 'init_poolmanager') + def test_init_poolmanager(self, mock_init_poolmgr): + adapter = novaclient.client.TCPKeepAliveAdapter() + kwargs = {} + adapter.init_poolmanager(**kwargs) + if requests.__version__ >= '2.4.1': + kwargs.setdefault('socket_options', [ + (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), + (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), + ]) + # NOTE(melwitt): This is called twice because + # HTTPAdapter.__init__ calls it first. + self.assertEqual(2, mock_init_poolmgr.call_count) + mock_init_poolmgr.assert_called_with(**kwargs) + + +class ClientConnectionPoolTest(utils.TestCase): + + @mock.patch("novaclient.client.TCPKeepAliveAdapter") + 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): + instance = novaclient.client.HTTPClient(user='user', + password='password', + projectid='project', + timeout=2, + auth_url="http://www.blah.com") + self.assertEqual(2, instance.timeout) + mock_request = mock.Mock() + mock_request.return_value = requests.Response() + mock_request.return_value.status_code = 200 + mock_request.return_value.headers = { + 'x-server-management-url': 'blah.com', + 'x-auth-token': 'blah', + } + with mock.patch('requests.request', mock_request): + instance.authenticate() + requests.request.assert_called_with( + mock.ANY, mock.ANY, timeout=2, headers=mock.ANY, + verify=mock.ANY) + + def test_client_reauth(self): + instance = novaclient.client.HTTPClient(user='user', + password='password', + projectid='project', + timeout=2, + auth_url="http://www.blah.com") + instance.auth_token = 'foobar' + instance.management_url = 'http://example.com' + instance.get_service_url = mock.Mock(return_value='http://example.com') + instance.version = 'v2.0' + mock_request = mock.Mock() + mock_request.side_effect = novaclient.exceptions.Unauthorized(401) + with mock.patch('requests.request', mock_request): + try: + instance.get('/servers/detail') + except Exception: + pass + get_headers = {'X-Auth-Project-Id': 'project', + 'X-Auth-Token': 'foobar', + 'User-Agent': 'python-novaclient', + 'Accept': 'application/json'} + reauth_headers = {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': 'python-novaclient'} + data = { + "auth": { + "tenantName": "project", + "passwordCredentials": { + "username": "user", + "password": "password" + } + } + } + + expected = [mock.call('GET', + 'http://example.com/servers/detail', + timeout=mock.ANY, + headers=get_headers, + verify=mock.ANY), + mock.call('POST', 'http://www.blah.com/tokens', + timeout=mock.ANY, + headers=reauth_headers, + allow_redirects=mock.ANY, + data=mock.ANY, + verify=mock.ANY)] + self.assertEqual(expected, mock_request.call_args_list) + token_post_call = mock_request.call_args_list[1] + self.assertEqual(data, json.loads(token_post_call[1]['data'])) + + @mock.patch.object(novaclient.client.HTTPClient, 'request', + return_value=(200, "{'versions':[]}")) + def _check_version_url(self, management_url, version_url, mock_request): + projectid = '25e469aa1848471b875e68cde6531bc5' + instance = novaclient.client.HTTPClient(user='user', + password='password', + projectid=projectid, + auth_url="http://www.blah.com") + instance.auth_token = 'foobar' + instance.management_url = management_url % projectid + mock_get_service_url = mock.Mock(return_value=instance.management_url) + instance.get_service_url = mock_get_service_url + instance.version = 'v2.0' + + # If passing None as the part of url, a client accesses the url which + # doesn't include "v2/" for getting API version info. + instance.get(None) + mock_request.assert_called_once_with(version_url, 'GET', + headers=mock.ANY) + mock_request.reset_mock() + + # Otherwise, a client accesses the url which includes "v2/". + instance.get('servers') + url = instance.management_url + 'servers' + mock_request.assert_called_once_with(url, 'GET', headers=mock.ANY) + + def test_client_version_url(self): + self._check_version_url('http://foo.com/v2/%s', 'http://foo.com/') + + def test_client_version_url_with_project_name(self): + self._check_version_url('http://foo.com/nova/v2/%s', + 'http://foo.com/nova/') + + def test_get_client_class_v3(self): + output = novaclient.client.get_client_class('3') + self.assertEqual(output, novaclient.v2.client.Client) + + def test_get_client_class_v2(self): + output = novaclient.client.get_client_class('2') + self.assertEqual(output, novaclient.v2.client.Client) + + def test_get_client_class_v2_int(self): + output = novaclient.client.get_client_class(2) + self.assertEqual(output, novaclient.v2.client.Client) + + def test_get_client_class_v1_1(self): + output = novaclient.client.get_client_class('1.1') + self.assertEqual(output, novaclient.v2.client.Client) + + def test_get_client_class_unknown(self): + self.assertRaises(novaclient.exceptions.UnsupportedVersion, + novaclient.client.get_client_class, '0') + + def test_client_with_os_cache_enabled(self): + cs = novaclient.v2.client.Client("user", "password", "project_id", + auth_url="foo/v2", os_cache=True) + self.assertTrue(cs.os_cache) + self.assertTrue(cs.client.os_cache) + + def test_client_with_os_cache_disabled(self): + cs = novaclient.v2.client.Client("user", "password", "project_id", + auth_url="foo/v2", os_cache=False) + self.assertFalse(cs.os_cache) + self.assertFalse(cs.client.os_cache) + + def test_client_with_no_cache_enabled(self): + cs = novaclient.v2.client.Client("user", "password", "project_id", + auth_url="foo/v2", no_cache=True) + self.assertFalse(cs.os_cache) + self.assertFalse(cs.client.os_cache) + + def test_client_with_no_cache_disabled(self): + cs = novaclient.v2.client.Client("user", "password", "project_id", + auth_url="foo/v2", no_cache=False) + self.assertTrue(cs.os_cache) + self.assertTrue(cs.client.os_cache) + + def test_client_set_management_url_v1_1(self): + cs = novaclient.v2.client.Client("user", "password", "project_id", + auth_url="foo/v2") + cs.set_management_url("blabla") + self.assertEqual("blabla", cs.client.management_url) + + def test_client_get_reset_timings_v1_1(self): + cs = novaclient.v2.client.Client("user", "password", "project_id", + auth_url="foo/v2") + self.assertEqual(0, len(cs.get_timings())) + cs.client.times.append("somevalue") + self.assertEqual(1, len(cs.get_timings())) + self.assertEqual("somevalue", cs.get_timings()[0]) + + cs.reset_timings() + self.assertEqual(0, len(cs.get_timings())) + + @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.v2.client.Client("user", "password", "project_id", + auth_url="foo/v2"): + pass + self.assertTrue(fake_client.open_session.called) + self.assertTrue(fake_client.close_session.called) + + def test_get_password_simple(self): + cs = novaclient.client.HTTPClient("user", "password", "", "") + cs.password_func = mock.Mock() + self.assertEqual("password", cs._get_password()) + self.assertFalse(cs.password_func.called) + + def test_get_password_none(self): + cs = novaclient.client.HTTPClient("user", None, "", "") + self.assertIsNone(cs._get_password()) + + def test_get_password_func(self): + cs = novaclient.client.HTTPClient("user", None, "", "") + cs.password_func = mock.Mock(return_value="password") + self.assertEqual("password", cs._get_password()) + cs.password_func.assert_called_once_with() + + cs.password_func = mock.Mock() + self.assertEqual("password", cs._get_password()) + self.assertFalse(cs.password_func.called) + + def test_auth_url_rstrip_slash(self): + cs = novaclient.client.HTTPClient("user", "password", "project_id", + auth_url="foo/v2/") + self.assertEqual("foo/v2", cs.auth_url) + + def test_token_and_bypass_url(self): + cs = novaclient.client.HTTPClient(None, None, None, + auth_token="12345", + bypass_url="compute/v100/") + self.assertIsNone(cs.auth_url) + self.assertEqual("12345", cs.auth_token) + self.assertEqual("compute/v100", cs.bypass_url) + self.assertEqual("compute/v100", cs.management_url) + + @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'}}) + cs.http_log_req('GET', '/foo', {'headers': {}, + 'data': + '{"auth": {"passwordCredentials": ' + '{"password": "zhaoqin"}}}'}) + + output = self.logger.output.split('\n') + + self.assertIn("REQ: curl -g -i '/foo' -X GET", output) + self.assertIn( + "REQ: curl -g -i '/foo' -X GET -H " + '"X-Auth-Token: {SHA1}b42162b6ffdbd7c3c37b7c95b7ba9f51dda0236d"', + output) + self.assertIn( + "REQ: curl -g -i '/foo' -X GET -H " + '"X-Auth-Token: {SHA1}b42162b6ffdbd7c3c37b7c95b7ba9f51dda0236d"' + ' -H "X-Foo: bar"', + output) + self.assertIn( + "REQ: curl -g -i '/foo' -X GET -d " + '\'{"auth": {"passwordCredentials": {"password":' + ' "{SHA1}4fc49c6a671ce889078ff6b250f7066cf6d2ada2"}}}\'', + output) + + def test_log_resp(self): + self.logger = self.useFixture( + fixtures.FakeLogger( + format="%(message)s", + level=logging.DEBUG, + nuke_handlers=True + ) + ) + + cs = novaclient.client.HTTPClient("user", None, "", + connection_pool=True) + cs.http_log_debug = True + text = ('{"access": {"token": {"id": "zhaoqin"}}}') + resp = utils.TestResponse({'status_code': 200, 'headers': {}, + 'text': text}) + + cs.http_log_resp(resp) + output = self.logger.output.split('\n') + + self.assertIn('RESP: [200] {}', output) + self.assertIn('RESP BODY: {"access": {"token": {"id":' + ' "{SHA1}4fc49c6a671ce889078ff6b250f7066cf6d2ada2"}}}', + output) diff --git a/awx/lib/site-packages/novaclient/tests/unit/test_discover.py b/awx/lib/site-packages/novaclient/tests/unit/test_discover.py new file mode 100644 index 0000000000..f8414c664f --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/test_discover.py @@ -0,0 +1,80 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import imp +import inspect + +import mock +import pkg_resources + +import novaclient.shell +from novaclient.tests.unit import utils + + +class DiscoverTest(utils.TestCase): + + def test_discover_via_entry_points(self): + + def mock_iter_entry_points(group): + if group == 'novaclient.extension': + fake_ep = mock.Mock() + fake_ep.name = 'foo' + fake_ep.module = imp.new_module('foo') + fake_ep.load.return_value = fake_ep.module + return [fake_ep] + + @mock.patch.object(pkg_resources, 'iter_entry_points', + mock_iter_entry_points) + def test(): + shell = novaclient.shell.OpenStackComputeShell() + for name, module in shell._discover_via_entry_points(): + self.assertEqual('foo', name) + self.assertTrue(inspect.ismodule(module)) + + test() + + def test_discover_extensions(self): + + def mock_discover_via_python_path(self): + yield 'foo', imp.new_module('foo') + + def mock_discover_via_contrib_path(self, version): + yield 'bar', imp.new_module('bar') + + def mock_discover_via_entry_points(self): + yield 'baz', imp.new_module('baz') + + @mock.patch.object(novaclient.shell.OpenStackComputeShell, + '_discover_via_python_path', + mock_discover_via_python_path) + @mock.patch.object(novaclient.shell.OpenStackComputeShell, + '_discover_via_contrib_path', + mock_discover_via_contrib_path) + @mock.patch.object(novaclient.shell.OpenStackComputeShell, + '_discover_via_entry_points', + mock_discover_via_entry_points) + def test(): + shell = novaclient.shell.OpenStackComputeShell() + extensions = shell._discover_extensions('1.1') + self.assertEqual(3, len(extensions)) + names = sorted(['foo', 'bar', 'baz']) + sorted_extensions = sorted(extensions, key=lambda ext: ext.name) + for i in range(len(names)): + ext = sorted_extensions[i] + name = names[i] + self.assertEqual(ext.name, name) + self.assertTrue(inspect.ismodule(ext.module)) + + test() diff --git a/awx/lib/site-packages/novaclient/tests/unit/test_http.py b/awx/lib/site-packages/novaclient/tests/unit/test_http.py new file mode 100644 index 0000000000..a80d94426f --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/test_http.py @@ -0,0 +1,217 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +import requests +import six + +from novaclient import client +from novaclient import exceptions +from novaclient.tests.unit import utils + + +fake_response = utils.TestResponse({ + "status_code": 200, + "text": '{"hi": "there"}', +}) +mock_request = mock.Mock(return_value=(fake_response)) + +refused_response = utils.TestResponse({ + "status_code": 400, + "text": '[Errno 111] Connection refused', +}) +refused_mock_request = mock.Mock(return_value=(refused_response)) + +bad_req_response = utils.TestResponse({ + "status_code": 400, + "text": '', +}) +bad_req_mock_request = mock.Mock(return_value=(bad_req_response)) + +unknown_error_response = utils.TestResponse({ + "status_code": 503, + "text": '', +}) +unknown_error_mock_request = mock.Mock(return_value=unknown_error_response) + +retry_after_response = utils.TestResponse({ + "status_code": 413, + "text": '', + "headers": { + "retry-after": "5" + }, +}) +retry_after_mock_request = mock.Mock(return_value=retry_after_response) + +retry_after_no_headers_response = utils.TestResponse({ + "status_code": 413, + "text": '', +}) +retry_after_no_headers_mock_request = mock.Mock( + return_value=retry_after_no_headers_response) + +retry_after_non_supporting_response = utils.TestResponse({ + "status_code": 403, + "text": '', + "headers": { + "retry-after": "5" + }, +}) +retry_after_non_supporting_mock_request = mock.Mock( + return_value=retry_after_non_supporting_response) + + +def get_client(): + cl = client.HTTPClient("username", "password", + "project_id", + utils.AUTH_URL_V2) + return cl + + +def get_authed_client(): + cl = get_client() + cl.management_url = "http://example.com" + cl.auth_token = "token" + cl.get_service_url = mock.Mock(return_value="http://example.com") + return cl + + +class ClientTest(utils.TestCase): + + def test_get(self): + cl = get_authed_client() + + @mock.patch.object(requests, "request", mock_request) + @mock.patch('time.time', mock.Mock(return_value=1234)) + def test_get_call(): + resp, body = cl.get("/hi") + headers = {"X-Auth-Token": "token", + "X-Auth-Project-Id": "project_id", + "User-Agent": cl.USER_AGENT, + 'Accept': 'application/json'} + mock_request.assert_called_with( + "GET", + "http://example.com/hi", + headers=headers, + **self.TEST_REQUEST_BASE) + # Automatic JSON parsing + self.assertEqual({"hi": "there"}, body) + + test_get_call() + + def test_post(self): + cl = get_authed_client() + + @mock.patch.object(requests, "request", mock_request) + def test_post_call(): + cl.post("/hi", body=[1, 2, 3]) + headers = { + "X-Auth-Token": "token", + "X-Auth-Project-Id": "project_id", + "Content-Type": "application/json", + 'Accept': 'application/json', + "User-Agent": cl.USER_AGENT + } + mock_request.assert_called_with( + "POST", + "http://example.com/hi", + headers=headers, + data='[1, 2, 3]', + **self.TEST_REQUEST_BASE) + + test_post_call() + + def test_auth_failure(self): + cl = get_client() + + # response must not have x-server-management-url header + @mock.patch.object(requests.Session, "request", mock_request) + def test_auth_call(): + self.assertRaises(exceptions.AuthorizationFailure, cl.authenticate) + + test_auth_call() + + def test_auth_failure_due_to_miss_of_auth_url(self): + cl = client.HTTPClient("username", "password") + + self.assertRaises(exceptions.AuthorizationFailure, cl.authenticate) + + def test_connection_refused(self): + cl = get_client() + + @mock.patch.object(requests, "request", refused_mock_request) + def test_refused_call(): + self.assertRaises(exceptions.ConnectionRefused, cl.get, "/hi") + + test_refused_call() + + def test_bad_request(self): + cl = get_client() + + @mock.patch.object(requests, "request", bad_req_mock_request) + def test_refused_call(): + self.assertRaises(exceptions.BadRequest, cl.get, "/hi") + + test_refused_call() + + def test_client_logger(self): + cl1 = client.HTTPClient("username", "password", "project_id", + "auth_test", http_log_debug=True) + self.assertEqual(1, len(cl1._logger.handlers)) + + cl2 = client.HTTPClient("username", "password", "project_id", + "auth_test", http_log_debug=True) + self.assertEqual(1, len(cl2._logger.handlers)) + + @mock.patch.object(requests, 'request', unknown_error_mock_request) + def test_unknown_server_error(self): + cl = get_client() + # This would be cleaner with the context manager version of + # assertRaises or assertRaisesRegexp, but both only appeared in + # Python 2.7 and testtools doesn't match that implementation yet + try: + cl.get('/hi') + except exceptions.ClientException as exc: + self.assertIn('Unknown Error', six.text_type(exc)) + else: + self.fail('Expected exceptions.ClientException') + + @mock.patch.object(requests, "request", retry_after_mock_request) + def test_retry_after_request(self): + cl = get_client() + + try: + cl.get("/hi") + except exceptions.OverLimit as exc: + self.assertEqual(5, exc.retry_after) + else: + self.fail('Expected exceptions.OverLimit') + + @mock.patch.object(requests, "request", + retry_after_no_headers_mock_request) + def test_retry_after_request_no_headers(self): + cl = get_client() + + try: + cl.get("/hi") + except exceptions.OverLimit as exc: + self.assertEqual(0, exc.retry_after) + else: + self.fail('Expected exceptions.OverLimit') + + @mock.patch.object(requests, "request", + retry_after_non_supporting_mock_request) + def test_retry_after_request_non_supporting_exc(self): + cl = get_client() + + self.assertRaises(exceptions.Forbidden, cl.get, "/hi") diff --git a/awx/lib/site-packages/novaclient/tests/unit/test_service_catalog.py b/awx/lib/site-packages/novaclient/tests/unit/test_service_catalog.py new file mode 100644 index 0000000000..33be77791e --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/test_service_catalog.py @@ -0,0 +1,73 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 keystoneclient import fixture + +from novaclient import exceptions +from novaclient import service_catalog +from novaclient.tests.unit import utils + + +SERVICE_CATALOG = fixture.V2Token() +SERVICE_CATALOG.set_scope() + +_s = SERVICE_CATALOG.add_service('compute') +_e = _s.add_endpoint("https://compute1.host/v1/1") +_e["tenantId"] = "1" +_e["versionId"] = "1.0" +_e = _s.add_endpoint("https://compute1.host/v1.1/2", region="North") +_e["tenantId"] = "2" +_e["versionId"] = "1.1" +_e = _s.add_endpoint("https://compute1.host/v2/1", region="North") +_e["tenantId"] = "1" +_e["versionId"] = "2" + +_s = SERVICE_CATALOG.add_service('volume') +_e = _s.add_endpoint("https://volume1.host/v1/1", region="South") +_e["tenantId"] = "1" +_e = _s.add_endpoint("https://volume1.host/v1.1/2", region="South") +_e["tenantId"] = "2" + + +class ServiceCatalogTest(utils.TestCase): + def test_building_a_service_catalog(self): + sc = service_catalog.ServiceCatalog(SERVICE_CATALOG) + + self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for, + service_type='compute') + self.assertEqual("https://compute1.host/v2/1", + sc.url_for('tenantId', '1', service_type='compute')) + self.assertEqual("https://compute1.host/v1.1/2", + sc.url_for('tenantId', '2', service_type='compute')) + + self.assertRaises(exceptions.EndpointNotFound, sc.url_for, + "region", "South", service_type='compute') + + def test_building_a_service_catalog_insensitive_case(self): + sc = service_catalog.ServiceCatalog(SERVICE_CATALOG) + # Matching south (and catalog has South). + self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for, + 'region', 'south', service_type='volume') + + def test_alternate_service_type(self): + sc = service_catalog.ServiceCatalog(SERVICE_CATALOG) + + self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for, + service_type='volume') + self.assertEqual("https://volume1.host/v1/1", + sc.url_for('tenantId', '1', service_type='volume')) + self.assertEqual("https://volume1.host/v1.1/2", + sc.url_for('tenantId', '2', service_type='volume')) + + self.assertRaises(exceptions.EndpointNotFound, sc.url_for, + "region", "North", service_type='volume') diff --git a/awx/lib/site-packages/novaclient/tests/unit/test_shell.py b/awx/lib/site-packages/novaclient/tests/unit/test_shell.py new file mode 100644 index 0000000000..e0995113bb --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/test_shell.py @@ -0,0 +1,389 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 distutils.version as dist_version +import re +import sys + +import fixtures +from keystoneclient import fixture +import mock +import prettytable +import requests_mock +import six +from testtools import matchers + +import novaclient.client +from novaclient import exceptions +import novaclient.shell +from novaclient.tests.unit import utils + +FAKE_ENV = {'OS_USERNAME': 'username', + 'OS_PASSWORD': 'password', + 'OS_TENANT_NAME': 'tenant_name', + 'OS_AUTH_URL': 'http://no.where/v2.0'} + +FAKE_ENV2 = {'OS_USER_ID': 'user_id', + 'OS_PASSWORD': 'password', + 'OS_TENANT_ID': 'tenant_id', + 'OS_AUTH_URL': 'http://no.where/v2.0'} + +FAKE_ENV3 = {'OS_USER_ID': 'user_id', + 'OS_PASSWORD': 'password', + 'OS_TENANT_ID': 'tenant_id', + 'OS_AUTH_URL': 'http://no.where/v2.0', + 'NOVA_ENDPOINT_TYPE': 'novaURL', + 'OS_ENDPOINT_TYPE': 'osURL'} + +FAKE_ENV4 = {'OS_USER_ID': 'user_id', + 'OS_PASSWORD': 'password', + 'OS_TENANT_ID': 'tenant_id', + 'OS_AUTH_URL': 'http://no.where/v2.0', + 'NOVA_ENDPOINT_TYPE': 'internal', + 'OS_ENDPOINT_TYPE': 'osURL'} + + +def _create_ver_list(versions): + return {'versions': {'values': versions}} + + +class ParserTest(utils.TestCase): + + def setUp(self): + super(ParserTest, self).setUp() + self.parser = novaclient.shell.NovaClientArgumentParser() + + def test_ambiguous_option(self): + self.parser.add_argument('--tic') + self.parser.add_argument('--tac') + + try: + self.parser.parse_args(['--t']) + except SystemExit as err: + self.assertEqual(2, err.code) + else: + self.fail('SystemExit not raised') + + def test_not_really_ambiguous_option(self): + # current/deprecated forms of the same option + self.parser.add_argument('--tic-tac', action="store_true") + self.parser.add_argument('--tic_tac', action="store_true") + args = self.parser.parse_args(['--tic']) + self.assertTrue(args.tic_tac) + + +class ShellTest(utils.TestCase): + + _msg_no_tenant_project = ("You must provide a project name or project" + " id via --os-project-name, --os-project-id," + " env[OS_PROJECT_ID] or env[OS_PROJECT_NAME]." + " You may use os-project and os-tenant" + " interchangeably.") + + def make_env(self, exclude=None, fake_env=FAKE_ENV): + env = dict((k, v) for k, v in fake_env.items() if k != exclude) + self.useFixture(fixtures.MonkeyPatch('os.environ', env)) + + def setUp(self): + super(ShellTest, self).setUp() + self.useFixture(fixtures.MonkeyPatch( + 'novaclient.client.get_client_class', + mock.MagicMock)) + self.nc_util = mock.patch( + 'novaclient.openstack.common.cliutils.isunauthenticated').start() + self.nc_util.return_value = False + + def shell(self, argstr, exitcodes=(0,)): + orig = sys.stdout + orig_stderr = sys.stderr + try: + sys.stdout = six.StringIO() + sys.stderr = six.StringIO() + _shell = novaclient.shell.OpenStackComputeShell() + _shell.main(argstr.split()) + except SystemExit: + exc_type, exc_value, exc_traceback = sys.exc_info() + self.assertIn(exc_value.code, exitcodes) + finally: + stdout = sys.stdout.getvalue() + sys.stdout.close() + sys.stdout = orig + stderr = sys.stderr.getvalue() + sys.stderr.close() + sys.stderr = orig_stderr + return (stdout, stderr) + + def register_keystone_discovery_fixture(self, mreq): + v2_url = "http://no.where/v2.0" + v2_version = fixture.V2Discovery(v2_url) + mreq.register_uri( + 'GET', v2_url, json=_create_ver_list([v2_version]), + status_code=200) + + def test_help_unknown_command(self): + self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo') + + def test_invalid_timeout(self): + for f in [0, -1, -10]: + cmd_text = '--timeout %s' % (f) + stdout, stderr = self.shell(cmd_text, exitcodes=[0, 2]) + required = [ + 'argument --timeout: %s must be greater than 0' % (f), + ] + for r in required: + self.assertIn(r, stderr) + + def test_help(self): + required = [ + '.*?^usage: ', + '.*?^\s+root-password\s+Change the admin password', + '.*?^See "nova help COMMAND" for help on a specific command', + ] + stdout, stderr = self.shell('help') + for r in required: + self.assertThat((stdout + stderr), + matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) + + def test_help_on_subcommand(self): + required = [ + '.*?^usage: nova root-password', + '.*?^Change the admin password', + '.*?^Positional arguments:', + ] + stdout, stderr = self.shell('help root-password') + for r in required: + self.assertThat((stdout + stderr), + matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) + + def test_help_no_options(self): + required = [ + '.*?^usage: ', + '.*?^\s+root-password\s+Change the admin password', + '.*?^See "nova help COMMAND" for help on a specific command', + ] + stdout, stderr = self.shell('') + for r in required: + self.assertThat((stdout + stderr), + matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) + + def test_bash_completion(self): + stdout, stderr = self.shell('bash-completion') + # just check we have some output + required = [ + '.*--matching', + '.*--wrap', + '.*help', + '.*secgroup-delete-rule', + '.*--priority'] + for r in required: + self.assertThat((stdout + stderr), + matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) + + def test_no_username(self): + required = ('You must provide a username 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[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 = self._msg_no_tenant_project + self.make_env(exclude='OS_TENANT_NAME') + 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_id(self): + required = self._msg_no_tenant_project + self.make_env(exclude='OS_TENANT_ID', fake_env=FAKE_ENV2) + try: + self.shell('list') + except exceptions.CommandError as message: + self.assertEqual(required, message.args[0]) + else: + self.fail('CommandError not raised') + + def test_no_auth_url(self): + required = ('You must provide an auth url' + ' via either --os-auth-url or env[OS_AUTH_URL] or' + ' specify an auth_system which defines a default url' + ' with --os-auth-system or env[OS_AUTH_SYSTEM]',) + self.make_env(exclude='OS_AUTH_URL') + try: + self.shell('list') + except exceptions.CommandError as message: + self.assertEqual(required, message.args) + else: + self.fail('CommandError not raised') + + @mock.patch('novaclient.client.Client') + @requests_mock.Mocker() + def test_nova_endpoint_type(self, mock_client, m_requests): + self.make_env(fake_env=FAKE_ENV3) + self.register_keystone_discovery_fixture(m_requests) + self.shell('list') + client_kwargs = mock_client.call_args_list[0][1] + self.assertEqual(client_kwargs['endpoint_type'], 'novaURL') + + @mock.patch('novaclient.client.Client') + @requests_mock.Mocker() + def test_endpoint_type_like_other_clients(self, mock_client, m_requests): + self.make_env(fake_env=FAKE_ENV4) + self.register_keystone_discovery_fixture(m_requests) + self.shell('list') + client_kwargs = mock_client.call_args_list[0][1] + self.assertEqual(client_kwargs['endpoint_type'], 'internalURL') + + @mock.patch('novaclient.client.Client') + @requests_mock.Mocker() + def test_os_endpoint_type(self, mock_client, m_requests): + self.make_env(exclude='NOVA_ENDPOINT_TYPE', fake_env=FAKE_ENV3) + self.register_keystone_discovery_fixture(m_requests) + self.shell('list') + client_kwargs = mock_client.call_args_list[0][1] + self.assertEqual(client_kwargs['endpoint_type'], 'osURL') + + @mock.patch('novaclient.client.Client') + def test_default_endpoint_type(self, mock_client): + self.make_env() + self.shell('list') + client_kwargs = mock_client.call_args_list[0][1] + self.assertEqual(client_kwargs['endpoint_type'], 'publicURL') + + @mock.patch('sys.stdin', side_effect=mock.MagicMock) + @mock.patch('getpass.getpass', return_value='password') + @requests_mock.Mocker() + def test_password(self, mock_getpass, mock_stdin, m_requests): + mock_stdin.encoding = "utf-8" + + # default output of empty tables differs depending between prettytable + # versions + if (hasattr(prettytable, '__version__') and + dist_version.StrictVersion(prettytable.__version__) < + dist_version.StrictVersion('0.7.2')): + ex = '\n' + else: + ex = '\n'.join([ + '+----+------+--------+------------+-------------+----------+', + '| ID | Name | Status | Task State | Power State | Networks |', + '+----+------+--------+------------+-------------+----------+', + '+----+------+--------+------------+-------------+----------+', + '' + ]) + self.make_env(exclude='OS_PASSWORD') + self.register_keystone_discovery_fixture(m_requests) + stdout, stderr = self.shell('list') + self.assertEqual((stdout + stderr), ex) + + @mock.patch('sys.stdin', side_effect=mock.MagicMock) + @mock.patch('getpass.getpass', side_effect=EOFError) + def test_no_password(self, mock_getpass, mock_stdin): + required = ('Expecting a password provided' + ' via either --os-password, env[OS_PASSWORD],' + ' or prompted response',) + self.make_env(exclude='OS_PASSWORD') + try: + self.shell('list') + except exceptions.CommandError as message: + self.assertEqual(required, message.args) + else: + self.fail('CommandError not raised') + + def _test_service_type(self, version, service_type, mock_client): + if version is None: + cmd = 'list' + else: + cmd = ('--service_type %s --os-compute-api-version %s list' % + (service_type, version)) + self.make_env() + self.shell(cmd) + _, client_kwargs = mock_client.call_args_list[0] + self.assertEqual(service_type, client_kwargs['service_type']) + + @mock.patch('novaclient.client.Client') + def test_default_service_type(self, mock_client): + self._test_service_type(None, 'compute', mock_client) + + @mock.patch('novaclient.client.Client') + def test_v1_1_service_type(self, mock_client): + self._test_service_type('1.1', 'compute', mock_client) + + @mock.patch('novaclient.client.Client') + def test_v2_service_type(self, mock_client): + self._test_service_type('2', 'compute', mock_client) + + @mock.patch('novaclient.client.Client') + def test_v3_service_type(self, mock_client): + self._test_service_type('3', 'computev3', mock_client) + + @mock.patch('novaclient.client.Client') + def test_v_unknown_service_type(self, mock_client): + self._test_service_type('unknown', 'compute', mock_client) + + @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()) + + @mock.patch.object(novaclient.shell.OpenStackComputeShell, 'main') + def test_main_keyboard_interrupt(self, mock_compute_shell): + # Ensure that exit code is 130 for KeyboardInterrupt + mock_compute_shell.side_effect = KeyboardInterrupt() + try: + novaclient.shell.main() + except SystemExit as ex: + self.assertEqual(ex.code, 130) + + +class ShellTestKeystoneV3(ShellTest): + def make_env(self, exclude=None, fake_env=FAKE_ENV): + if 'OS_AUTH_URL' in fake_env: + fake_env.update({'OS_AUTH_URL': 'http://no.where/v3'}) + env = dict((k, v) for k, v in fake_env.items() if k != exclude) + self.useFixture(fixtures.MonkeyPatch('os.environ', env)) + + def register_keystone_discovery_fixture(self, mreq): + v3_url = "http://no.where/v3" + v3_version = fixture.V3Discovery(v3_url) + mreq.register_uri( + 'GET', v3_url, json=_create_ver_list([v3_version]), + status_code=200) diff --git a/awx/lib/site-packages/novaclient/tests/unit/test_utils.py b/awx/lib/site-packages/novaclient/tests/unit/test_utils.py new file mode 100644 index 0000000000..da68ce792b --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/test_utils.py @@ -0,0 +1,357 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import sys + +import mock +import six + +from novaclient import base +from novaclient import exceptions +from novaclient.tests.unit import utils as test_utils +from novaclient import utils + +UUID = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0' + + +class FakeResource(object): + NAME_ATTR = 'name' + + def __init__(self, _id, properties): + self.id = _id + try: + self.name = properties['name'] + except KeyError: + pass + + +class FakeManager(base.ManagerWithFind): + + resource_class = FakeResource + + resources = [ + FakeResource('1234', {'name': 'entity_one'}), + FakeResource('12345', {'name': 'UPPER'}), + FakeResource('123456', {'name': 'lower'}), + FakeResource('1234567', {'name': 'Mixed'}), + FakeResource('12345678', {'name': 'mixed'}), + FakeResource(UUID, {'name': 'entity_two'}), + FakeResource('5678', {'name': '9876'}), + FakeResource('01234', {'name': 'entity_three'}) + ] + + is_alphanum_id_allowed = None + + def __init__(self, alphanum_id_allowed=False): + self.is_alphanum_id_allowed = alphanum_id_allowed + + def get(self, resource_id): + for resource in self.resources: + if resource.id == str(resource_id): + return resource + raise exceptions.NotFound(resource_id) + + def list(self): + return self.resources + + +class FakeDisplayResource(object): + NAME_ATTR = 'display_name' + + def __init__(self, _id, properties): + self.id = _id + try: + self.display_name = properties['display_name'] + except KeyError: + pass + + +class FakeDisplayManager(FakeManager): + + resource_class = FakeDisplayResource + + resources = [ + FakeDisplayResource('4242', {'display_name': 'entity_three'}), + ] + + +class FindResourceTestCase(test_utils.TestCase): + + def setUp(self): + super(FindResourceTestCase, self).setUp() + self.manager = FakeManager(None) + + def test_find_none(self): + """Test a few non-valid inputs.""" + self.assertRaises(exceptions.CommandError, + utils.find_resource, + self.manager, + 'asdf') + self.assertRaises(exceptions.CommandError, + utils.find_resource, + self.manager, + None) + self.assertRaises(exceptions.CommandError, + utils.find_resource, + self.manager, + {}) + + def test_find_by_integer_id(self): + output = utils.find_resource(self.manager, 1234) + self.assertEqual(output, self.manager.get('1234')) + + def test_find_by_str_id(self): + output = utils.find_resource(self.manager, '1234') + self.assertEqual(output, self.manager.get('1234')) + + def test_find_by_uuid(self): + output = utils.find_resource(self.manager, UUID) + self.assertEqual(output, self.manager.get(UUID)) + + def test_find_by_str_name(self): + output = utils.find_resource(self.manager, 'entity_one') + self.assertEqual(output, self.manager.get('1234')) + + def test_find_by_str_upper_name(self): + output = utils.find_resource(self.manager, 'UPPER') + self.assertEqual(output, self.manager.get('12345')) + + def test_find_by_str_lower_name(self): + output = utils.find_resource(self.manager, 'lower') + self.assertEqual(output, self.manager.get('123456')) + + def test_find_by_str_mix_name(self): + output = utils.find_resource(self.manager, 'Mixed') + self.assertEqual(output, self.manager.get('1234567')) + + def test_find_by_str_lower_name_mixed(self): + output = utils.find_resource(self.manager, 'mixed') + self.assertEqual(output, self.manager.get('12345678')) + + def test_find_by_str_display_name(self): + display_manager = FakeDisplayManager(None) + output = utils.find_resource(display_manager, 'entity_three') + self.assertEqual(output, display_manager.get('4242')) + + def test_find_in_alphanum_allowed_manager_by_str_id_(self): + alphanum_manager = FakeManager(True) + output = utils.find_resource(alphanum_manager, '01234') + self.assertEqual(output, alphanum_manager.get('01234')) + + +class _FakeResult(object): + def __init__(self, name, value): + self.name = name + self.value = value + + +class PrintResultTestCase(test_utils.TestCase): + @mock.patch('sys.stdout', six.StringIO()) + def test_print_dict(self): + dict = {'key': 'value'} + utils.print_dict(dict) + self.assertEqual('+----------+-------+\n' + '| Property | Value |\n' + '+----------+-------+\n' + '| key | value |\n' + '+----------+-------+\n', + sys.stdout.getvalue()) + + @mock.patch('sys.stdout', six.StringIO()) + def test_print_dict_wrap(self): + dict = {'key1': 'not wrapped', + 'key2': 'this will be wrapped'} + utils.print_dict(dict, wrap=16) + self.assertEqual('+----------+--------------+\n' + '| Property | Value |\n' + '+----------+--------------+\n' + '| key1 | not wrapped |\n' + '| key2 | this will be |\n' + '| | wrapped |\n' + '+----------+--------------+\n', + sys.stdout.getvalue()) + + @mock.patch('sys.stdout', six.StringIO()) + def test_print_list_sort_by_str(self): + objs = [_FakeResult("k1", 1), + _FakeResult("k3", 2), + _FakeResult("k2", 3)] + + utils.print_list(objs, ["Name", "Value"], sortby_index=0) + + self.assertEqual('+------+-------+\n' + '| Name | Value |\n' + '+------+-------+\n' + '| k1 | 1 |\n' + '| k2 | 3 |\n' + '| k3 | 2 |\n' + '+------+-------+\n', + sys.stdout.getvalue()) + + @mock.patch('sys.stdout', six.StringIO()) + def test_print_list_sort_by_integer(self): + objs = [_FakeResult("k1", 1), + _FakeResult("k3", 2), + _FakeResult("k2", 3)] + + utils.print_list(objs, ["Name", "Value"], sortby_index=1) + + self.assertEqual('+------+-------+\n' + '| Name | Value |\n' + '+------+-------+\n' + '| k1 | 1 |\n' + '| k3 | 2 |\n' + '| k2 | 3 |\n' + '+------+-------+\n', + sys.stdout.getvalue()) + + # without sorting + @mock.patch('sys.stdout', six.StringIO()) + def test_print_list_sort_by_none(self): + objs = [_FakeResult("k1", 1), + _FakeResult("k3", 3), + _FakeResult("k2", 2)] + + utils.print_list(objs, ["Name", "Value"], sortby_index=None) + + self.assertEqual('+------+-------+\n' + '| Name | Value |\n' + '+------+-------+\n' + '| k1 | 1 |\n' + '| k3 | 3 |\n' + '| k2 | 2 |\n' + '+------+-------+\n', + sys.stdout.getvalue()) + + @mock.patch('sys.stdout', six.StringIO()) + def test_print_dict_dictionary(self): + dict = {'k': {'foo': 'bar'}} + utils.print_dict(dict) + self.assertEqual('+----------+----------------+\n' + '| Property | Value |\n' + '+----------+----------------+\n' + '| k | {"foo": "bar"} |\n' + '+----------+----------------+\n', + sys.stdout.getvalue()) + + @mock.patch('sys.stdout', six.StringIO()) + def test_print_dict_list_dictionary(self): + dict = {'k': [{'foo': 'bar'}]} + utils.print_dict(dict) + self.assertEqual('+----------+------------------+\n' + '| Property | Value |\n' + '+----------+------------------+\n' + '| k | [{"foo": "bar"}] |\n' + '+----------+------------------+\n', + sys.stdout.getvalue()) + + @mock.patch('sys.stdout', six.StringIO()) + def test_print_dict_list(self): + dict = {'k': ['foo', 'bar']} + utils.print_dict(dict) + self.assertEqual('+----------+----------------+\n' + '| Property | Value |\n' + '+----------+----------------+\n' + '| k | ["foo", "bar"] |\n' + '+----------+----------------+\n', + sys.stdout.getvalue()) + + +class FlattenTestCase(test_utils.TestCase): + def test_flattening(self): + squashed = utils.flatten_dict( + {'a1': {'b1': 1234, + 'b2': 'string', + 'b3': set((1, 2, 3)), + 'b4': {'c1': ['l', 'l', ['l']], + 'c2': 'string'}}, + 'a2': ['l'], + 'a3': ('t',)}) + + self.assertEqual({'a1_b1': 1234, + 'a1_b2': 'string', + 'a1_b3': set([1, 2, 3]), + 'a1_b4_c1': ['l', 'l', ['l']], + 'a1_b4_c2': 'string', + 'a2': ['l'], + 'a3': ('t',)}, + squashed) + + def test_pretty_choice_dict(self): + d = {} + r = utils.pretty_choice_dict(d) + self.assertEqual("", r) + + d = {"k1": "v1", + "k2": "v2", + "k3": "v3"} + r = utils.pretty_choice_dict(d) + self.assertEqual("'k1=v1', 'k2=v2', 'k3=v3'", r) + + +class ValidationsTestCase(test_utils.TestCase): + def test_validate_flavor_metadata_keys_with_valid_keys(self): + valid_keys = ['key1', 'month.price', 'I-Am:AK-ey.01-', 'spaces and _'] + utils.validate_flavor_metadata_keys(valid_keys) + + def test_validate_flavor_metadata_keys_with_invalid_keys(self): + invalid_keys = ['/1', '?1', '%1', '<', '>', '\1'] + for key in invalid_keys: + try: + utils.validate_flavor_metadata_keys([key]) + self.fail("Invalid key passed validation: %s" % key) + except exceptions.CommandError as ce: + self.assertIn(key, str(ce)) + + +class ResourceManagerExtraKwargsHookTestCase(test_utils.TestCase): + def test_get_resource_manager_extra_kwargs_hook_test(self): + do_foo = mock.MagicMock() + + def hook1(args): + return {'kwarg1': 'v_hook1'} + + def hook2(args): + return {'kwarg1': 'v_hook2'} + do_foo.resource_manager_kwargs_hooks = [hook1, hook2] + args = {} + exc = self.assertRaises(exceptions.NoUniqueMatch, + utils.get_resource_manager_extra_kwargs, + do_foo, + args) + except_error = ("Hook 'hook2' is attempting to redefine " + "attributes") + self.assertIn(except_error, six.text_type(exc)) + + +class DoActionOnManyTestCase(test_utils.TestCase): + + def _test_do_action_on_many(self, side_effect, fail): + action = mock.Mock(side_effect=side_effect) + + if fail: + self.assertRaises(exceptions.CommandError, + utils.do_action_on_many, + action, [1, 2], 'success with %s', 'error') + else: + utils.do_action_on_many(action, [1, 2], 'success with %s', 'error') + action.assert_has_calls([mock.call(1), mock.call(2)]) + + def test_do_action_on_many_success(self): + self._test_do_action_on_many([None, None], fail=False) + + def test_do_action_on_many_first_fails(self): + self._test_do_action_on_many([Exception(), None], fail=True) + + def test_do_action_on_many_last_fails(self): + self._test_do_action_on_many([None, Exception()], fail=True) diff --git a/awx/lib/site-packages/novaclient/tests/unit/utils.py b/awx/lib/site-packages/novaclient/tests/unit/utils.py new file mode 100644 index 0000000000..ab1ddcd185 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/utils.py @@ -0,0 +1,128 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os + +import fixtures +import mock +from oslo.serialization import jsonutils +import requests +from requests_mock.contrib import fixture as requests_mock_fixture +import six +import testscenarios +import testtools + +AUTH_URL = "http://localhost:5002/auth_url" +AUTH_URL_V1 = "http://localhost:5002/auth_url/v1.0" +AUTH_URL_V2 = "http://localhost:5002/auth_url/v2.0" + + +def _patch_mock_to_raise_for_invalid_assert_calls(): + def raise_for_invalid_assert_calls(wrapped): + def wrapper(_self, name): + valid_asserts = [ + 'assert_called_with', + 'assert_called_once_with', + 'assert_has_calls', + 'assert_any_calls'] + + if name.startswith('assert') and name not in valid_asserts: + raise AttributeError('%s is not a valid mock assert method' + % name) + + return wrapped(_self, name) + return wrapper + mock.Mock.__getattr__ = raise_for_invalid_assert_calls( + mock.Mock.__getattr__) + +# NOTE(gibi): needs to be called only once at import time +# to patch the mock lib +_patch_mock_to_raise_for_invalid_assert_calls() + + +class TestCase(testtools.TestCase): + TEST_REQUEST_BASE = { + 'verify': True, + } + + def setUp(self): + super(TestCase, self).setUp() + if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or + os.environ.get('OS_STDOUT_CAPTURE') == '1'): + stdout = self.useFixture(fixtures.StringStream('stdout')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) + if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or + os.environ.get('OS_STDERR_CAPTURE') == '1'): + stderr = self.useFixture(fixtures.StringStream('stderr')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) + + +class FixturedTestCase(testscenarios.TestWithScenarios, TestCase): + + client_fixture_class = None + data_fixture_class = None + + def setUp(self): + super(FixturedTestCase, self).setUp() + + self.requests = self.useFixture(requests_mock_fixture.Fixture()) + self.data_fixture = None + self.client_fixture = None + self.cs = None + + if self.client_fixture_class: + fix = self.client_fixture_class(self.requests) + self.client_fixture = self.useFixture(fix) + self.cs = self.client_fixture.client + + if self.data_fixture_class: + fix = self.data_fixture_class(self.requests) + self.data_fixture = self.useFixture(fix) + + def assert_called(self, method, path, body=None): + self.assertEqual(self.requests.last_request.method, method) + self.assertEqual(self.requests.last_request.path_url, path) + + if body: + req_data = self.requests.last_request.body + if isinstance(req_data, six.binary_type): + req_data = req_data.decode('utf-8') + if not isinstance(body, six.string_types): + # 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 + convenience to initialize with a dict + """ + + def __init__(self, data): + super(TestResponse, self).__init__() + self._text = None + if isinstance(data, dict): + self.status_code = data.get('status_code') + self.headers = data.get('headers') + # Fake the text attribute to streamline Response creation + self._text = data.get('text') + else: + self.status_code = data + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + @property + def text(self): + return self._text diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/__init__.py b/awx/lib/site-packages/novaclient/tests/unit/v2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/__init__.py b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/fakes.py b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/fakes.py new file mode 100644 index 0000000000..8703395845 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/fakes.py @@ -0,0 +1,151 @@ +# Copyright 2012 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import client + + +class FakeClient(fakes.FakeClient): + def __init__(self, *args, **kwargs): + client.Client.__init__(self, 'username', 'password', + 'project_id', 'auth_url', + extensions=kwargs.get('extensions')) + self.client = FakeHTTPClient(**kwargs) + + +class FakeHTTPClient(fakes.FakeHTTPClient): + def get_os_tenant_networks(self): + return (200, {}, { + 'networks': [{"label": "1", "cidr": "10.0.0.0/24", + 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1'}]}) + + def get_os_tenant_networks_1(self, **kw): + return (200, {}, { + 'network': {"label": "1", "cidr": "10.0.0.0/24", + 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1'}}) + + def post_os_tenant_networks(self, **kw): + return (201, {}, { + 'network': {"label": "1", "cidr": "10.0.0.0/24", + 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1'}}) + + def delete_os_tenant_networks_1(self, **kw): + return (204, {}, None) + + def get_os_baremetal_nodes(self, **kw): + return ( + 200, {}, { + 'nodes': [ + { + "id": 1, + "instance_uuid": None, + "interfaces": [], + "cpus": 2, + "local_gb": 10, + "memory_mb": 5, + "pm_address": "2.3.4.5", + "pm_user": "pmuser", + "pm_password": "pmpass", + "prov_mac_address": "aa:bb:cc:dd:ee:ff", + "prov_vlan_id": 1, + "service_host": "somehost", + "terminal_port": 8080, + } + ] + } + ) + + def get_os_baremetal_nodes_1(self, **kw): + return ( + 200, {}, { + 'node': { + "id": 1, + "instance_uuid": None, + "pm_address": "1.2.3.4", + "interfaces": [], + "cpus": 2, + "local_gb": 10, + "memory_mb": 5, + "pm_user": "pmuser", + "pm_password": "pmpass", + "prov_mac_address": "aa:bb:cc:dd:ee:ff", + "prov_vlan_id": 1, + "service_host": "somehost", + "terminal_port": 8080, + } + } + ) + + def post_os_baremetal_nodes(self, **kw): + return ( + 200, {}, { + 'node': { + "id": 1, + "instance_uuid": None, + "cpus": 2, + "local_gb": 10, + "memory_mb": 5, + "pm_address": "2.3.4.5", + "pm_user": "pmuser", + "pm_password": "pmpass", + "prov_mac_address": "aa:bb:cc:dd:ee:ff", + "prov_vlan_id": 1, + "service_host": "somehost", + "terminal_port": 8080, + } + } + ) + + def delete_os_baremetal_nodes_1(self, **kw): + return (202, {}, {}) + + def post_os_baremetal_nodes_1_action(self, **kw): + body = kw['body'] + action = list(body)[0] + if action == "add_interface": + return ( + 200, {}, { + 'interface': { + "id": 2, + "address": "bb:cc:dd:ee:ff:aa", + "datapath_id": 1, + "port_no": 2, + } + } + ) + elif action == "remove_interface": + return (202, {}, {}) + else: + return (500, {}, {}) + + def post_os_assisted_volume_snapshots(self, **kw): + return (202, {}, {'snapshot': {'id': 'blah', 'volumeId': '1'}}) + + def delete_os_assisted_volume_snapshots_x(self, **kw): + return (202, {}, {}) + + def post_os_server_external_events(self, **kw): + return (200, {}, { + 'events': [ + {'name': 'test-event', + 'status': 'completed', + 'tag': 'tag', + 'server_uuid': 'fake-uuid1'}, + {'name': 'test-event', + 'status': 'completed', + 'tag': 'tag', + 'server_uuid': 'fake-uuid2'}]}) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_assisted_volume_snapshots.py b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_assisted_volume_snapshots.py new file mode 100644 index 0000000000..8ab732aed2 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_assisted_volume_snapshots.py @@ -0,0 +1,42 @@ +# Copyright (C) 2013, Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Assisted volume snapshots - to be used by Cinder and not end users. +""" + +from novaclient import extension +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2.contrib import fakes +from novaclient.v2.contrib import assisted_volume_snapshots as assisted_snaps + + +extensions = [ + extension.Extension(assisted_snaps.__name__.split(".")[-1], + assisted_snaps), +] +cs = fakes.FakeClient(extensions=extensions) + + +class AssistedVolumeSnapshotsTestCase(utils.TestCase): + + def test_create_snap(self): + cs.assisted_volume_snapshots.create('1', {}) + cs.assert_called('POST', '/os-assisted-volume-snapshots') + + def test_delete_snap(self): + 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/unit/v2/contrib/test_baremetal.py b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_baremetal.py new file mode 100644 index 0000000000..a5ee414677 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_baremetal.py @@ -0,0 +1,64 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from novaclient import extension +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2.contrib import fakes +from novaclient.v2.contrib import baremetal + + +extensions = [ + extension.Extension(baremetal.__name__.split(".")[-1], baremetal), +] +cs = fakes.FakeClient(extensions=extensions) + + +class BaremetalExtensionTest(utils.TestCase): + + def test_list_nodes(self): + nl = cs.baremetal.list() + cs.assert_called('GET', '/os-baremetal-nodes') + for n in nl: + self.assertIsInstance(n, baremetal.BareMetalNode) + + def test_get_node(self): + n = cs.baremetal.get(1) + cs.assert_called('GET', '/os-baremetal-nodes/1') + self.assertIsInstance(n, baremetal.BareMetalNode) + + def test_create_node(self): + n = cs.baremetal.create("service_host", 1, 1024, 2048, + "aa:bb:cc:dd:ee:ff") + cs.assert_called('POST', '/os-baremetal-nodes') + self.assertIsInstance(n, baremetal.BareMetalNode) + + def test_delete_node(self): + n = cs.baremetal.get(1) + cs.baremetal.delete(n) + cs.assert_called('DELETE', '/os-baremetal-nodes/1') + + def test_node_add_interface(self): + i = cs.baremetal.add_interface(1, "bb:cc:dd:ee:ff:aa", 1, 2) + cs.assert_called('POST', '/os-baremetal-nodes/1/action') + self.assertIsInstance(i, baremetal.BareMetalNodeInterface) + + def test_node_remove_interface(self): + cs.baremetal.remove_interface(1, "bb:cc:dd:ee:ff:aa") + cs.assert_called('POST', '/os-baremetal-nodes/1/action') + + def test_node_list_interfaces(self): + cs.baremetal.list_interfaces(1) + cs.assert_called('GET', '/os-baremetal-nodes/1') diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_cells.py b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_cells.py new file mode 100644 index 0000000000..e411502bca --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_cells.py @@ -0,0 +1,42 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import extension +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2.contrib import fakes +from novaclient.v2.contrib import cells + + +extensions = [ + extension.Extension(cells.__name__.split(".")[-1], + cells), +] +cs = fakes.FakeClient(extensions=extensions) + + +class CellsExtensionTests(utils.TestCase): + def test_get_cells(self): + cell_name = 'child_cell' + cs.cells.get(cell_name) + cs.assert_called('GET', '/os-cells/%s' % cell_name) + + def test_get_capacities_for_a_given_cell(self): + cell_name = 'child_cell' + cs.cells.capacities(cell_name) + cs.assert_called('GET', '/os-cells/%s/capacities' % cell_name) + + def test_get_capacities_for_all_cells(self): + cs.cells.capacities() + cs.assert_called('GET', '/os-cells/capacities') diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_instance_actions.py b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_instance_actions.py new file mode 100644 index 0000000000..0b0400a296 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_instance_actions.py @@ -0,0 +1,43 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import extension +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2.contrib import fakes +from novaclient.v2.contrib import instance_action + + +extensions = [ + extension.Extension(instance_action.__name__.split(".")[-1], + instance_action), +] +cs = fakes.FakeClient(extensions=extensions) + + +class InstanceActionExtensionTests(utils.TestCase): + def test_list_instance_actions(self): + server_uuid = '1234' + cs.instance_action.list(server_uuid) + cs.assert_called( + 'GET', '/servers/%s/os-instance-actions' % + server_uuid) + + def test_get_instance_action(self): + server_uuid = '1234' + request_id = 'req-abcde12345' + cs.instance_action.get(server_uuid, request_id) + cs.assert_called( + 'GET', '/servers/%s/os-instance-actions/%s' + % (server_uuid, request_id)) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_list_extensions.py b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_list_extensions.py new file mode 100644 index 0000000000..3fa5253bae --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_list_extensions.py @@ -0,0 +1,33 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import extension +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2.contrib import list_extensions + + +extensions = [ + extension.Extension(list_extensions.__name__.split(".")[-1], + list_extensions), +] +cs = fakes.FakeClient(extensions=extensions) + + +class ListExtensionsTests(utils.TestCase): + def 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/unit/v2/contrib/test_migrations.py b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_migrations.py new file mode 100644 index 0000000000..881fd1e505 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_migrations.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. + +from novaclient import extension +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2.contrib import migrations + +extensions = [ + extension.Extension(migrations.__name__.split(".")[-1], + migrations), +] +cs = fakes.FakeClient(extensions=extensions) + + +class MigrationsTest(utils.TestCase): + + def test_list_migrations(self): + ml = cs.migrations.list() + cs.assert_called('GET', '/os-migrations') + for m in ml: + self.assertIsInstance(m, migrations.Migration) + + def test_list_migrations_with_filters(self): + ml = cs.migrations.list('host1', 'finished', 'child1') + + cs.assert_called('GET', + '/os-migrations?cell_name=child1&host=host1' + '&status=finished') + for m in ml: + self.assertIsInstance(m, migrations.Migration) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_server_external_events.py b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_server_external_events.py new file mode 100644 index 0000000000..1b941c9d67 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_server_external_events.py @@ -0,0 +1,44 @@ +# Copyright (C) 2014, Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +External event triggering for servers, not to be used by users. +""" + +from novaclient import extension +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2.contrib import fakes +from novaclient.v2.contrib import server_external_events as ext_events + + +extensions = [ + extension.Extension(ext_events.__name__.split(".")[-1], + ext_events), +] +cs = fakes.FakeClient(extensions=extensions) + + +class ServerExternalEventsTestCase(utils.TestCase): + def test_external_event(self): + events = [{'server_uuid': 'fake-uuid1', + 'name': 'test-event', + 'status': 'completed', + 'tag': 'tag'}, + {'server_uuid': 'fake-uuid2', + 'name': 'test-event', + 'status': 'completed', + 'tag': 'tag'}] + result = cs.server_external_events.create(events) + self.assertEqual(events, result) + cs.assert_called('POST', '/os-server-external-events') diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_tenant_networks.py b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_tenant_networks.py new file mode 100644 index 0000000000..13159ce5fe --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/contrib/test_tenant_networks.py @@ -0,0 +1,46 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import extension +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2.contrib import fakes +from novaclient.v2.contrib import tenant_networks + + +extensions = [ + extension.Extension(tenant_networks.__name__.split(".")[-1], + tenant_networks), +] +cs = fakes.FakeClient(extensions=extensions) + + +class TenantNetworkExtensionTests(utils.TestCase): + def test_list_tenant_networks(self): + nets = cs.tenant_networks.list() + cs.assert_called('GET', '/os-tenant-networks') + self.assertTrue(len(nets) > 0) + + def test_get_tenant_network(self): + cs.tenant_networks.get(1) + cs.assert_called('GET', '/os-tenant-networks/1') + + def test_create_tenant_networks(self): + cs.tenant_networks.create(label="net", + cidr="10.0.0.0/24") + cs.assert_called('POST', '/os-tenant-networks') + + def test_delete_tenant_networks(self): + cs.tenant_networks.delete(1) + cs.assert_called('DELETE', '/os-tenant-networks/1') diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/fakes.py b/awx/lib/site-packages/novaclient/tests/unit/v2/fakes.py new file mode 100644 index 0000000000..343c421e2e --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/fakes.py @@ -0,0 +1,2204 @@ +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime + +import mock +from oslo.utils import strutils +import six +from six.moves.urllib import parse + +from novaclient import client as base_client +from novaclient import exceptions +from novaclient.tests.unit import fakes +from novaclient.tests.unit import utils +from novaclient.v2 import client + + +class FakeClient(fakes.FakeClient, client.Client): + + def __init__(self, *args, **kwargs): + client.Client.__init__(self, 'username', 'password', + 'project_id', 'auth_url', + extensions=kwargs.get('extensions')) + self.client = FakeHTTPClient(**kwargs) + + +class FakeHTTPClient(base_client.HTTPClient): + + def __init__(self, **kwargs): + self.username = 'username' + self.password = 'password' + self.auth_url = 'auth_url' + self.tenant_id = 'tenant_id' + self.callstack = [] + self.projectid = 'projectid' + self.user = 'user' + self.region_name = 'region_name' + self.endpoint_type = 'endpoint_type' + self.service_type = 'service_type' + self.service_name = 'service_name' + self.volume_service_name = 'volume_service_name' + self.timings = 'timings' + self.bypass_url = 'bypass_url' + self.os_cache = 'os_cache' + self.http_log_debug = 'http_log_debug' + + def _cs_request(self, url, method, **kwargs): + # Check that certain things are called correctly + if method in ['GET', 'DELETE']: + assert 'body' not in kwargs + elif method == 'PUT': + assert 'body' in kwargs + + # Call the method + args = parse.parse_qsl(parse.urlparse(url)[4]) + kwargs.update(args) + munged_url = url.rsplit('?', 1)[0] + munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') + munged_url = munged_url.replace('-', '_') + munged_url = munged_url.replace(' ', '_') + + callback = "%s_%s" % (method.lower(), munged_url) + + if not hasattr(self, callback): + raise AssertionError('Called unknown API method: %s %s, ' + 'expected fakes method name: %s' % + (method, url, callback)) + + # Note the call + self.callstack.append((method, url, kwargs.get('body'))) + + status, headers, body = getattr(self, callback)(**kwargs) + r = utils.TestResponse({ + "status_code": status, + "text": body, + "headers": headers, + }) + return r, body + + # + # agents + # + + def get_os_agents(self, **kw): + hypervisor = kw.get('hypervisor', 'kvm') + return (200, {}, { + 'agents': + [{'hypervisor': hypervisor, + 'os': 'win', + 'architecture': 'x86', + 'version': '7.0', + 'url': 'xxx://xxxx/xxx/xxx', + 'md5hash': 'add6bb58e139be103324d04d82d8f545', + 'id': 1}, + {'hypervisor': hypervisor, + 'os': 'linux', + 'architecture': 'x86', + 'version': '16.0', + 'url': 'xxx://xxxx/xxx/xxx1', + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'id': 2}]}) + + def post_os_agents(self, body): + return (200, {}, {'agent': { + 'url': '/xxx/xxx/xxx', + 'hypervisor': body['agent']['hypervisor'], + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'version': '7.0', + 'architecture': 'x86', + 'os': 'win', + 'id': 1}}) + + def delete_os_agents_1(self, **kw): + return (202, {}, None) + + def put_os_agents_1(self, body, **kw): + return (200, {}, { + "agent": {"url": "/yyy/yyyy/yyyy", + "version": "8.0", + "md5hash": "add6bb58e139be103324d04d82d8f546", + 'id': 1}}) + + # + # List all extensions + # + + def get_extensions(self, **kw): + exts = [ + { + "alias": "NMN", + "description": "Multiple network support", + "links": [], + "name": "Multinic", + "namespace": ("http://docs.openstack.org/" + "compute/ext/multinic/api/v1.1"), + "updated": "2011-06-09T00:00:00+00:00" + }, + { + "alias": "OS-DCF", + "description": "Disk Management Extension", + "links": [], + "name": "DiskConfig", + "namespace": ("http://docs.openstack.org/" + "compute/ext/disk_config/api/v1.1"), + "updated": "2011-09-27T00:00:00+00:00" + }, + { + "alias": "OS-EXT-SRV-ATTR", + "description": "Extended Server Attributes support.", + "links": [], + "name": "ExtendedServerAttributes", + "namespace": ("http://docs.openstack.org/" + "compute/ext/extended_status/api/v1.1"), + "updated": "2011-11-03T00:00:00+00:00" + }, + { + "alias": "OS-EXT-STS", + "description": "Extended Status support", + "links": [], + "name": "ExtendedStatus", + "namespace": ("http://docs.openstack.org/" + "compute/ext/extended_status/api/v1.1"), + "updated": "2011-11-03T00:00:00+00:00" + }, + ] + return (200, {}, { + "extensions": exts, + }) + + # + # Limits + # + + def get_limits(self, **kw): + return (200, {}, {"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 + }, + }}) + + # + # Servers + # + + def get_servers(self, **kw): + return (200, {}, {"servers": [ + {'id': 1234, 'name': 'sample-server'}, + {'id': 5678, 'name': 'sample-server2'} + ]}) + + def get_servers_detail(self, **kw): + return (200, {}, {"servers": [ + { + "id": 1234, + "name": "sample-server", + "image": { + "id": 2, + "name": "sample image", + }, + "flavor": { + "id": 1, + "name": "256 mb server", + }, + "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0", + "status": "BUILD", + "progress": 60, + "addresses": { + "public": [ + { + "version": 4, + "addr": "1.2.3.4", + }, + { + "version": 4, + "addr": "5.6.7.8", + }], + "private": [{ + "version": 4, + "addr": "10.11.12.13", + }], + }, + "metadata": { + "Server Label": "Web Head 1", + "Image Version": "2.1" + }, + "OS-EXT-SRV-ATTR:host": "computenode1", + "security_groups": [{ + 'id': 1, 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }], + "OS-EXT-MOD:some_thing": "mod_some_thing_value", + }, + { + "id": 5678, + "name": "sample-server2", + "image": { + "id": 2, + "name": "sample image", + }, + "flavor": { + "id": 1, + "name": "256 mb server", + }, + "hostId": "9e107d9d372bb6826bd81d3542a419d6", + "status": "ACTIVE", + "addresses": { + "public": [ + { + "version": 4, + "addr": "4.5.6.7", + }, + { + "version": 4, + "addr": "5.6.9.8", + }], + "private": [{ + "version": 4, + "addr": "10.13.12.13", + }], + }, + "metadata": { + "Server Label": "DB 1" + }, + "OS-EXT-SRV-ATTR:host": "computenode2", + "security_groups": [ + { + 'id': 1, 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }, + { + 'id': 2, 'name': 'securitygroup2', + 'description': 'ANOTHER_FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }], + }, + { + "id": 9012, + "name": "sample-server3", + "image": "", + "flavor": { + "id": 1, + "name": "256 mb server", + }, + "hostId": "9e107d9d372bb6826bd81d3542a419d6", + "status": "ACTIVE", + "addresses": { + "public": [ + { + "version": 4, + "addr": "4.5.6.7", + }, + { + "version": 4, + "addr": "5.6.9.8", + }], + "private": [{ + "version": 4, + "addr": "10.13.12.13", + }], + }, + "metadata": { + "Server Label": "DB 1" + } + } + ]}) + + def post_servers(self, body, **kw): + assert set(body.keys()) <= set(['server', 'os:scheduler_hints']) + fakes.assert_has_keys( + body['server'], + required=['name', 'imageRef', 'flavorRef'], + optional=['metadata', 'personality']) + if 'personality' in body['server']: + for pfile in body['server']['personality']: + fakes.assert_has_keys(pfile, required=['path', 'contents']) + if body['server']['name'] == 'some-bad-server': + 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']) + fakes.assert_has_keys( + body['server'], + required=['name', 'flavorRef'], + optional=['imageRef']) + + # Require one, and only one, of the keys for bdm + if 'block_device_mapping' not in body['server']: + if 'block_device_mapping_v2' not in body['server']: + raise AssertionError( + "missing required keys: 'block_device_mapping'" + ) + elif 'block_device_mapping_v2' in body['server']: + raise AssertionError("found extra keys: 'block_device_mapping'") + + return (202, {}, self.get_servers_9012()[2]) + + def get_servers_1234(self, **kw): + 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) + + def get_servers_9012(self, **kw): + r = {'server': self.get_servers_detail()[2]['servers'][2]} + return (200, {}, r) + + def put_servers_1234(self, body, **kw): + assert list(body) == ['server'] + fakes.assert_has_keys(body['server'], optional=['name', 'adminPass']) + return (204, {}, body) + + def delete_os_server_groups_12345(self, **kw): + return (202, {}, None) + + def delete_os_server_groups_56789(self, **kw): + return (202, {}, None) + + def delete_servers_1234(self, **kw): + return (202, {}, None) + + def delete_servers_5678(self, **kw): + return (202, {}, None) + + def delete_servers_1234_metadata_test_key(self, **kw): + return (204, {}, None) + + def delete_servers_1234_metadata_key1(self, **kw): + return (204, {}, None) + + def delete_servers_1234_metadata_key2(self, **kw): + return (204, {}, None) + + def post_servers_1234_metadata(self, **kw): + return (204, {}, {'metadata': {'test_key': 'test_value'}}) + + def put_servers_1234_metadata_test_key(self, **kw): + return (200, {}, {'meta': {'test_key': 'test_value'}}) + + def get_servers_1234_diagnostics(self, **kw): + return (200, {}, {'data': 'Fake diagnostics'}) + + def post_servers_uuid1_metadata(self, **kw): + return (204, {}, {'metadata': {'key1': 'val1'}}) + + def post_servers_uuid2_metadata(self, **kw): + return (204, {}, {'metadata': {'key1': 'val1'}}) + + def post_servers_uuid3_metadata(self, **kw): + return (204, {}, {'metadata': {'key1': 'val1'}}) + + def post_servers_uuid4_metadata(self, **kw): + return (204, {}, {'metadata': {'key1': 'val1'}}) + + def delete_servers_uuid1_metadata_key1(self, **kw): + return (200, {}, {'data': 'Fake diagnostics'}) + + def delete_servers_uuid2_metadata_key1(self, **kw): + return (200, {}, {'data': 'Fake diagnostics'}) + + def delete_servers_uuid3_metadata_key1(self, **kw): + return (200, {}, {'data': 'Fake diagnostics'}) + + def delete_servers_uuid4_metadata_key1(self, **kw): + return (200, {}, {'data': 'Fake diagnostics'}) + + def get_servers_1234_os_security_groups(self, **kw): + return (200, {}, { + "security_groups": [{ + 'id': 1, + 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'rules': []}] + }) + + # + # Server Addresses + # + + def get_servers_1234_ips(self, **kw): + return (200, {}, { + 'addresses': + self.get_servers_1234()[1]['server']['addresses']}) + + def get_servers_1234_ips_public(self, **kw): + return (200, {}, { + 'public': + self.get_servers_1234_ips()[1]['addresses']['public']}) + + def get_servers_1234_ips_private(self, **kw): + return ( + 200, {}, + {'private': + self.get_servers_1234_ips()[1]['addresses']['private']}) + + def delete_servers_1234_ips_public_1_2_3_4(self, **kw): + return (202, {}, None) + + # + # Server password + # + + # Testing with the following password and key + # + # Clear password: FooBar123 + # + # RSA Private Key: novaclient/tests/unit/idfake.pem + # + # Encrypted password + # OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r + # qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho + # QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw + # /y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N + # tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk + # Hi/fmZZNQQqj1Ijq0caOIw== + def get_servers_1234_os_server_password(self, **kw): + return (200, {}, { + 'password': + 'OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r' + 'qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho' + 'QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw' + '/y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N' + 'tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk' + 'Hi/fmZZNQQqj1Ijq0caOIw=='}) + + def delete_servers_1234_os_server_password(self, **kw): + return (202, {}, None) + + # + # Server actions + # + + none_actions = ['revertResize', 'migrate', 'os-stop', 'os-start', + 'forceDelete', 'restore', 'pause', 'unpause', 'unlock', + 'unrescue', 'resume', 'suspend', 'lock', 'shelve', + 'shelveOffload', 'unshelve', 'resetNetwork'] + type_actions = ['os-getVNCConsole', 'os-getSPICEConsole', + 'os-getRDPConsole'] + + @classmethod + def check_server_actions(cls, body): + action = list(body)[0] + if action == 'reboot': + assert list(body[action]) == ['type'] + assert body[action]['type'] in ['HARD', 'SOFT'] + elif action == 'resize': + assert 'flavorRef' in body[action] + elif action in cls.none_actions: + assert body[action] is None + elif action == 'addFixedIp': + assert list(body[action]) == ['networkId'] + elif action in ['removeFixedIp', 'removeFloatingIp']: + assert list(body[action]) == ['address'] + elif action == 'addFloatingIp': + assert (list(body[action]) == ['address'] or + sorted(list(body[action])) == ['address', 'fixed_address']) + elif action == 'changePassword': + assert list(body[action]) == ['adminPass'] + elif action in cls.type_actions: + assert list(body[action]) == ['type'] + elif action == 'os-migrateLive': + assert set(body[action].keys()) == set(['host', 'block_migration', + 'disk_over_commit']) + elif action == 'os-resetState': + assert list(body[action]) == ['state'] + elif action == 'resetNetwork': + assert body[action] is None + elif action in ['addSecurityGroup', 'removeSecurityGroup']: + assert list(body[action]) == ['name'] + elif action == 'createBackup': + assert set(body[action]) == set(['name', 'backup_type', + 'rotation']) + else: + return False + return True + + def post_servers_1234_action(self, body, **kw): + _headers = None + _body = None + resp = 202 + assert len(body.keys()) == 1 + action = list(body)[0] + + if self.check_server_actions(body): + # NOTE(snikitin): No need to do any operations here. This 'pass' + # is needed to avoid AssertionError in the last 'else' statement + # if we found 'action' in method check_server_actions and + # raise AssertionError if we didn't find 'action' at all. + pass + elif action == 'rebuild': + body = body[action] + adminPass = body.get('adminPass', 'randompassword') + assert 'imageRef' in body + _body = self.get_servers_1234()[2] + _body['server']['adminPass'] = adminPass + elif action == 'confirmResize': + assert body[action] is None + # This one method returns a different response code + return (204, {}, None) + elif action == 'rescue': + if body[action]: + keys = set(body[action].keys()) + assert not (keys - set(['adminPass', 'rescue_image_ref'])) + else: + assert body[action] is None + _body = {'adminPass': 'RescuePassword'} + elif action == 'createImage': + assert set(body[action].keys()) == set(['name', 'metadata']) + _headers = dict(location="http://blah/images/456") + elif action == 'os-getConsoleOutput': + assert list(body[action]) == ['length'] + return (202, {}, {'output': 'foo'}) + elif action == 'evacuate': + keys = list(body[action]) + if 'adminPass' in keys: + keys.remove('adminPass') + if 'host' in keys: + keys.remove('host') + assert set(keys) == set(['onSharedStorage']) + else: + 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 + # + + def get_os_cloudpipe(self, **kw): + return ( + 200, + {}, + {'cloudpipes': [{'project_id': 1}]} + ) + + def post_os_cloudpipe(self, **ks): + return ( + 202, + {}, + {'instance_id': '9d5824aa-20e6-4b9f-b967-76a699fc51fd'} + ) + + def put_os_cloudpipe_configure_project(self, **kw): + return (202, {}, None) + + # + # Flavors + # + + def get_flavors(self, **kw): + status, header, flavors = self.get_flavors_detail(**kw) + for flavor in flavors['flavors']: + for k in list(flavor): + if k not in ['id', 'name']: + del flavor[k] + + return (200, {}, flavors) + + def get_flavors_detail(self, **kw): + flavors = {'flavors': [ + {'id': 1, 'name': '256 mb server', 'ram': 256, 'disk': 10, + 'OS-FLV-EXT-DATA:ephemeral': 10, + 'os-flavor-access:is_public': True, + 'links': {}}, + {'id': 2, 'name': '512 mb server', 'ram': 512, 'disk': 20, + 'OS-FLV-EXT-DATA:ephemeral': 20, + 'os-flavor-access:is_public': False, + 'links': {}}, + {'id': 4, 'name': '1024 mb server', 'ram': 1024, 'disk': 10, + 'OS-FLV-EXT-DATA:ephemeral': 10, + 'os-flavor-access:is_public': True, + 'links': {}}, + {'id': 'aa1', 'name': '128 mb server', 'ram': 128, 'disk': 0, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'os-flavor-access:is_public': True, + 'links': {}} + ]} + + if 'is_public' not in kw: + filter_is_public = True + else: + if kw['is_public'].lower() == 'none': + filter_is_public = None + else: + filter_is_public = strutils.bool_from_string(kw['is_public'], + True) + + if filter_is_public is not None: + if filter_is_public: + flavors['flavors'] = [ + v for v in flavors['flavors'] + if v['os-flavor-access:is_public'] + ] + else: + flavors['flavors'] = [ + v for v in flavors['flavors'] + if not v['os-flavor-access:is_public'] + ] + + return (200, {}, flavors) + + def get_flavors_1(self, **kw): + return ( + 200, + {}, + {'flavor': + self.get_flavors_detail(is_public='None')[2]['flavors'][0]} + ) + + def get_flavors_2(self, **kw): + return ( + 200, + {}, + {'flavor': + self.get_flavors_detail(is_public='None')[2]['flavors'][1]} + ) + + def get_flavors_3(self, **kw): + # Diablo has no ephemeral + return ( + 200, + {}, + {'flavor': { + 'id': 3, + 'name': '256 mb server', + 'ram': 256, + 'disk': 10, + }}, + ) + + def get_flavors_512_mb_server(self, **kw): + raise exceptions.NotFound('404') + + def get_flavors_128_mb_server(self, **kw): + raise exceptions.NotFound('404') + + def get_flavors_aa1(self, **kw): + # Alphanumeric flavor id are allowed. + return ( + 200, + {}, + {'flavor': + self.get_flavors_detail(is_public='None')[2]['flavors'][3]} + ) + + def get_flavors_4(self, **kw): + return ( + 200, + {}, + {'flavor': + self.get_flavors_detail(is_public='None')[2]['flavors'][2]} + ) + + def delete_flavors_flavordelete(self, **kw): + return (202, {}, None) + + def delete_flavors_2(self, **kw): + return (202, {}, None) + + def post_flavors(self, body, **kw): + return ( + 202, + {}, + {'flavor': + self.get_flavors_detail(is_public='None')[2]['flavors'][0]} + ) + + def get_flavors_1_os_extra_specs(self, **kw): + return ( + 200, + {}, + {'extra_specs': {"k1": "v1"}}) + + def get_flavors_2_os_extra_specs(self, **kw): + return ( + 200, + {}, + {'extra_specs': {"k2": "v2"}}) + + def get_flavors_aa1_os_extra_specs(self, **kw): + return ( + 200, {}, + {'extra_specs': {"k3": "v3"}}) + + def get_flavors_4_os_extra_specs(self, **kw): + return ( + 200, + {}, + {'extra_specs': {"k4": "v4"}}) + + def post_flavors_1_os_extra_specs(self, body, **kw): + assert list(body) == ['extra_specs'] + fakes.assert_has_keys(body['extra_specs'], + required=['k1']) + return ( + 200, + {}, + {'extra_specs': {"k1": "v1"}}) + + def post_flavors_4_os_extra_specs(self, body, **kw): + assert list(body) == ['extra_specs'] + + return ( + 200, + {}, + body) + + def delete_flavors_1_os_extra_specs_k1(self, **kw): + return (204, {}, None) + + # + # Flavor access + # + + def get_flavors_1_os_flavor_access(self, **kw): + return (404, {}, None) + + def get_flavors_2_os_flavor_access(self, **kw): + return ( + 200, {}, + {'flavor_access': [{'flavor_id': '2', 'tenant_id': 'proj1'}, + {'flavor_id': '2', 'tenant_id': 'proj2'}]}) + + def post_flavors_2_action(self, body, **kw): + return (202, {}, self.get_flavors_2_os_flavor_access()[2]) + + # + # Floating IPs + # + + def get_os_floating_ip_pools(self): + return ( + 200, + {}, + {'floating_ip_pools': [{'name': 'foo'}, {'name': 'bar'}]} + ) + + def get_os_floating_ips(self, **kw): + return ( + 200, + {}, + {'floating_ips': [ + {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, + {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, + ]}, + ) + + def get_os_floating_ips_1(self, **kw): + return ( + 200, {}, {'floating_ip': {'id': 1, 'fixed_ip': '10.0.0.1', + 'ip': '11.0.0.1'}}) + + def post_os_floating_ips(self, body): + if body.get('pool'): + return ( + 200, {}, {'floating_ip': {'id': 1, 'fixed_ip': '10.0.0.1', + 'ip': '11.0.0.1', + 'pool': 'nova'}}) + else: + return ( + 200, {}, {'floating_ip': {'id': 1, 'fixed_ip': '10.0.0.1', + 'ip': '11.0.0.1', + 'pool': None}}) + + def delete_os_floating_ips_1(self, **kw): + return (204, {}, None) + + def get_os_floating_ip_dns(self, **kw): + return (205, {}, {'domain_entries': + [{'domain': 'example.org'}, + {'domain': 'example.com'}]}) + + def get_os_floating_ip_dns_testdomain_entries(self, **kw): + if kw.get('ip'): + return (205, {}, { + 'dns_entries': [ + {'dns_entry': {'ip': kw.get('ip'), + 'name': "host1", + 'type': "A", + 'domain': 'testdomain'}}, + {'dns_entry': {'ip': kw.get('ip'), + 'name': "host2", + 'type': "A", + 'domain': 'testdomain'}}]}) + else: + return (404, {}, None) + + def get_os_floating_ip_dns_testdomain_entries_testname(self, **kw): + return (205, {}, { + 'dns_entry': {'ip': "10.10.10.10", + 'name': 'testname', + 'type': "A", + 'domain': 'testdomain'}}) + + def put_os_floating_ip_dns_testdomain(self, body, **kw): + if body['domain_entry']['scope'] == 'private': + fakes.assert_has_keys(body['domain_entry'], + required=['availability_zone', 'scope']) + elif body['domain_entry']['scope'] == 'public': + fakes.assert_has_keys(body['domain_entry'], + required=['project', 'scope']) + + else: + fakes.assert_has_keys(body['domain_entry'], + required=['project', 'scope']) + return (205, {}, body) + + def put_os_floating_ip_dns_testdomain_entries_testname(self, body, **kw): + fakes.assert_has_keys(body['dns_entry'], + required=['ip', 'dns_type']) + return (205, {}, body) + + def delete_os_floating_ip_dns_testdomain(self, **kw): + return (200, {}, None) + + def delete_os_floating_ip_dns_testdomain_entries_testname(self, **kw): + return (200, {}, None) + + def get_os_floating_ips_bulk(self, **kw): + return (200, {}, {'floating_ip_info': [ + {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, + {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, + ]}) + + def get_os_floating_ips_bulk_testHost(self, **kw): + return (200, {}, {'floating_ip_info': [ + {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, + {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, + ]}) + + def post_os_floating_ips_bulk(self, **kw): + params = kw.get('body').get('floating_ips_bulk_create') + pool = params.get('pool', 'defaultPool') + interface = params.get('interface', 'defaultInterface') + return (200, {}, {'floating_ips_bulk_create': + {'ip_range': '192.168.1.0/30', + 'pool': pool, + 'interface': interface}}) + + def put_os_floating_ips_bulk_delete(self, **kw): + ip_range = kw.get('body').get('ip_range') + return (200, {}, {'floating_ips_bulk_delete': ip_range}) + + # + # Images + # + def get_images(self, **kw): + return (200, {}, {'images': [ + {'id': 1, 'name': 'CentOS 5.2'}, + {'id': 2, 'name': 'My Server Backup'} + ]}) + + def get_images_detail(self, **kw): + return (200, {}, {'images': [ + { + 'id': 1, + 'name': 'CentOS 5.2', + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "status": "ACTIVE", + "metadata": { + "test_key": "test_value", + }, + "links": {}, + }, + { + "id": 2, + "name": "My Server Backup", + "serverId": 1234, + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "status": "SAVING", + "progress": 80, + "links": {}, + } + ]}) + + def get_images_1(self, **kw): + return (200, {}, {'image': self.get_images_detail()[2]['images'][0]}) + + def get_images_2(self, **kw): + return (200, {}, {'image': self.get_images_detail()[2]['images'][1]}) + + def get_images_456(self, **kw): + return (200, {}, {'image': self.get_images_detail()[2]['images'][1]}) + + def post_images(self, body, **kw): + assert list(body) == ['image'] + fakes.assert_has_keys(body['image'], required=['serverId', 'name']) + return (202, {}, self.get_images_1()[2]) + + def post_images_1_metadata(self, body, **kw): + assert list(body) == ['metadata'] + fakes.assert_has_keys(body['metadata'], + required=['test_key']) + return ( + 200, + {}, + {'metadata': self.get_images_1()[2]['image']['metadata']}) + + def delete_images_1(self, **kw): + return (204, {}, None) + + def delete_images_2(self, **kw): + return (204, {}, None) + + def delete_images_1_metadata_test_key(self, **kw): + return (204, {}, None) + + # + # Keypairs + # + def get_os_keypairs_test(self, *kw): + return (200, {}, {'keypair': + self.get_os_keypairs()[2]['keypairs'][0]['keypair']}) + + def get_os_keypairs(self, *kw): + return (200, {}, { + "keypairs": [{"keypair": { + "public_key": "FAKE_SSH_RSA", + "private_key": "FAKE_PRIVATE_KEY", + "user_id": "81e373b596d6466e99c4896826abaa46", + "name": "test", + "deleted": False, + "created_at": "2014-04-19T02:16:44.000000", + "updated_at": "2014-04-19T10:12:3.000000", + "figerprint": "FAKE_KEYPAIR", + "deleted_at": None, + "id": 4}} + ]}) + + def delete_os_keypairs_test(self, **kw): + return (202, {}, None) + + def post_os_keypairs(self, body, **kw): + assert list(body) == ['keypair'] + fakes.assert_has_keys(body['keypair'], + required=['name']) + r = {'keypair': self.get_os_keypairs()[2]['keypairs'][0]['keypair']} + return (202, {}, r) + + # + # Virtual Interfaces + # + def get_servers_1234_os_virtual_interfaces(self, **kw): + return (200, {}, {"virtual_interfaces": [ + {'id': 'fakeid', 'mac_address': 'fakemac'} + ]}) + + # + # Quotas + # + + def get_os_quota_sets_test(self, **kw): + return (200, {}, { + 'quota_set': { + 'tenant_id': 'test', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def get_os_quota_sets_tenant_id(self, **kw): + return (200, {}, { + 'quota_set': { + 'tenant_id': 'test', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def get_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): + return (200, {}, { + 'quota_set': { + 'tenant_id': '97f4c221bff44578b0300df4ef119353', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def put_os_quota_sets_97f4c221_bff4_4578_b030_0df4ef119353(self, **kw): + return (200, {}, { + 'quota_set': { + 'tenant_id': '97f4c221-bff4-4578-b030-0df4ef119353', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def get_os_quota_sets_97f4c221_bff4_4578_b030_0df4ef119353(self, **kw): + return (200, {}, { + 'quota_set': { + 'tenant_id': '97f4c221-bff4-4578-b030-0df4ef119353', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def get_os_quota_sets_97f4c221bff44578b0300df4ef119353_defaults(self): + return (200, {}, { + 'quota_set': { + 'tenant_id': 'test', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def get_os_quota_sets_tenant_id_defaults(self): + return (200, {}, { + 'quota_set': { + 'tenant_id': 'test', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def put_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): + assert list(body) == ['quota_set'] + fakes.assert_has_keys(body['quota_set'], + required=['tenant_id']) + return (200, {}, { + 'quota_set': { + 'tenant_id': '97f4c221bff44578b0300df4ef119353', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def delete_os_quota_sets_test(self, **kw): + return (202, {}, {}) + + def delete_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): + return (202, {}, {}) + + # + # Quota Classes + # + + def get_os_quota_class_sets_test(self, **kw): + return (200, {}, { + 'quota_class_set': { + 'id': 'test', + 'metadata_items': 1, + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'key_pairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def put_os_quota_class_sets_test(self, body, **kw): + assert list(body) == ['quota_class_set'] + return (200, {}, { + 'quota_class_set': { + 'metadata_items': 1, + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'key_pairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def put_os_quota_class_sets_97f4c221bff44578b0300df4ef119353(self, + body, **kw): + assert list(body) == ['quota_class_set'] + return (200, {}, { + 'quota_class_set': { + 'metadata_items': 1, + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'key_pairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + # + # Security Groups + # + def get_os_security_groups(self, **kw): + return (200, {}, {"security_groups": [ + {"name": "test", + "description": "FAKE_SECURITY_GROUP", + "tenant_id": "4ffc664c198e435e9853f2538fbcd7a7", + "id": 1, + "rules": [ + {"id": 11, + "group": {}, + "ip_protocol": "TCP", + "from_port": 22, + "to_port": 22, + "parent_group_id": 1, + "ip_range": + {"cidr": "10.0.0.0/8"}}, + {"id": 12, + "group": { + "tenant_id": + "272bee4c1e624cd4a72a6b0ea55b4582", + "name": "test2"}, + + "ip_protocol": "TCP", + "from_port": 222, + "to_port": 222, + "parent_group_id": 1, + "ip_range": {}}, + {"id": 14, + "group": { + "tenant_id": + "272bee4c1e624cd4a72a6b0ea55b4582", + "name": "test4"}, + + "ip_protocol": "TCP", + "from_port": -1, + "to_port": -1, + "parent_group_id": 1, + "ip_range": {}}]}, + {"name": "test2", + "description": "FAKE_SECURITY_GROUP2", + "tenant_id": "272bee4c1e624cd4a72a6b0ea55b4582", + "id": 2, + "rules": []}, + {"name": "test4", + "description": "FAKE_SECURITY_GROUP4", + "tenant_id": "272bee4c1e624cd4a72a6b0ea55b4582", + "id": 4, + "rules": []} + ]}) + + def get_os_security_groups_1(self, **kw): + return (200, {}, {"security_group": + {'id': 1, 'name': 'test', 'description': 'FAKE_SECURITY_GROUP'} + }) + + def delete_os_security_groups_1(self, **kw): + return (202, {}, None) + + def post_os_security_groups(self, body, **kw): + assert list(body) == ['security_group'] + fakes.assert_has_keys(body['security_group'], + required=['name', 'description']) + r = {'security_group': + self.get_os_security_groups()[2]['security_groups'][0]} + return (202, {}, r) + + def put_os_security_groups_1(self, body, **kw): + assert list(body) == ['security_group'] + fakes.assert_has_keys(body['security_group'], + required=['name', 'description']) + return (205, {}, body) + + # + # Security Group Rules + # + def get_os_security_group_rules(self, **kw): + return (200, {}, {"security_group_rules": [ + {'id': 1, 'parent_group_id': 1, 'group_id': 2, + 'ip_protocol': 'TCP', 'from_port': 22, 'to_port': 22, + 'cidr': '10.0.0.0/8'} + ]}) + + def delete_os_security_group_rules_1(self, **kw): + return (202, {}, None) + + def delete_os_security_group_rules_11(self, **kw): + return (202, {}, None) + + def delete_os_security_group_rules_12(self, **kw): + return (202, {}, None) + + def delete_os_security_group_rules_14(self, **kw): + return (202, {}, None) + + def post_os_security_group_rules(self, body, **kw): + assert list(body) == ['security_group_rule'] + fakes.assert_has_keys( + body['security_group_rule'], + required=['parent_group_id'], + optional=['group_id', 'ip_protocol', 'from_port', + 'to_port', 'cidr']) + r = {'security_group_rule': + self.get_os_security_group_rules()[2]['security_group_rules'][0]} + return (202, {}, r) + + # + # Security Group Default Rules + # + def get_os_security_group_default_rules(self, **kw): + return (200, {}, {"security_group_default_rules": [ + {'id': 1, 'ip_protocol': 'TCP', 'from_port': 22, + 'to_port': 22, 'cidr': '10.0.0.0/8'} + ]}) + + def delete_os_security_group_default_rules_1(self, **kw): + return (202, {}, None) + + def delete_os_security_group_default_rules_11(self, **kw): + return (202, {}, None) + + def delete_os_security_group_default_rules_12(self, **kw): + return (202, {}, None) + + def post_os_security_group_default_rules(self, body, **kw): + assert list(body) == ['security_group_default_rule'] + fakes.assert_has_keys(body['security_group_default_rule'], + optional=['ip_protocol', 'from_port', + 'to_port', 'cidr']) + rules = self.get_os_security_group_default_rules() + r = {'security_group_default_rule': + rules[2]['security_group_default_rules'][0]} + return (202, {}, r) + + # + # Tenant Usage + # + def get_os_simple_tenant_usage(self, **kw): + return (200, {}, + {six.u('tenant_usages'): [{ + six.u('total_memory_mb_usage'): 25451.762807466665, + six.u('total_vcpus_usage'): 49.71047423333333, + six.u('total_hours'): 49.71047423333333, + six.u('tenant_id'): + six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('stop'): six.u('2012-01-22 19:48:41.750722'), + six.u('server_usages'): [{ + six.u('hours'): 49.71047423333333, + six.u('uptime'): 27035, + six.u('local_gb'): 0, + six.u('ended_at'): None, + six.u('name'): six.u('f15image1'), + six.u('tenant_id'): + six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('vcpus'): 1, + six.u('memory_mb'): 512, + six.u('state'): six.u('active'), + six.u('flavor'): six.u('m1.tiny'), + six.u('started_at'): + six.u('2012-01-20 18:06:06.479998')}], + six.u('start'): six.u('2011-12-25 19:48:41.750687'), + six.u('total_local_gb_usage'): 0.0}]}) + + def get_os_simple_tenant_usage_tenantfoo(self, **kw): + return (200, {}, + {six.u('tenant_usage'): { + six.u('total_memory_mb_usage'): 25451.762807466665, + six.u('total_vcpus_usage'): 49.71047423333333, + six.u('total_hours'): 49.71047423333333, + six.u('tenant_id'): + six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('stop'): six.u('2012-01-22 19:48:41.750722'), + six.u('server_usages'): [{ + six.u('hours'): 49.71047423333333, + six.u('uptime'): 27035, six.u('local_gb'): 0, + six.u('ended_at'): None, + six.u('name'): six.u('f15image1'), + six.u('tenant_id'): + six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('vcpus'): 1, six.u('memory_mb'): 512, + six.u('state'): six.u('active'), + six.u('flavor'): six.u('m1.tiny'), + six.u('started_at'): + six.u('2012-01-20 18:06:06.479998')}], + six.u('start'): six.u('2011-12-25 19:48:41.750687'), + six.u('total_local_gb_usage'): 0.0}}) + + def get_os_simple_tenant_usage_test(self, **kw): + return (200, {}, {six.u('tenant_usage'): { + six.u('total_memory_mb_usage'): 25451.762807466665, + six.u('total_vcpus_usage'): 49.71047423333333, + six.u('total_hours'): 49.71047423333333, + six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('stop'): six.u('2012-01-22 19:48:41.750722'), + six.u('server_usages'): [{ + six.u('hours'): 49.71047423333333, + six.u('uptime'): 27035, six.u('local_gb'): 0, + six.u('ended_at'): None, + six.u('name'): six.u('f15image1'), + six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('vcpus'): 1, six.u('memory_mb'): 512, + six.u('state'): six.u('active'), + six.u('flavor'): six.u('m1.tiny'), + six.u('started_at'): six.u('2012-01-20 18:06:06.479998')}], + six.u('start'): six.u('2011-12-25 19:48:41.750687'), + six.u('total_local_gb_usage'): 0.0}}) + + def get_os_simple_tenant_usage_tenant_id(self, **kw): + return (200, {}, {six.u('tenant_usage'): { + six.u('total_memory_mb_usage'): 25451.762807466665, + six.u('total_vcpus_usage'): 49.71047423333333, + six.u('total_hours'): 49.71047423333333, + six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('stop'): six.u('2012-01-22 19:48:41.750722'), + six.u('server_usages'): [{ + six.u('hours'): 49.71047423333333, + six.u('uptime'): 27035, six.u('local_gb'): 0, + six.u('ended_at'): None, + six.u('name'): six.u('f15image1'), + six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('vcpus'): 1, six.u('memory_mb'): 512, + six.u('state'): six.u('active'), + six.u('flavor'): six.u('m1.tiny'), + six.u('started_at'): six.u('2012-01-20 18:06:06.479998')}], + six.u('start'): six.u('2011-12-25 19:48:41.750687'), + six.u('total_local_gb_usage'): 0.0}}) + # + # Certificates + # + + def get_os_certificates_root(self, **kw): + return ( + 200, + {}, + {'certificate': {'private_key': None, 'data': 'foo'}} + ) + + def post_os_certificates(self, **kw): + return ( + 200, + {}, + {'certificate': {'private_key': 'foo', 'data': 'bar'}} + ) + + # + # Aggregates + # + + def get_os_aggregates(self, *kw): + return (200, {}, {"aggregates": [ + {'id': '1', + 'name': 'test', + 'availability_zone': 'nova1'}, + {'id': '2', + 'name': 'test2', + 'availability_zone': 'nova1'}, + {'id': '3', + 'name': 'test3', + 'metadata': {'test': "dup", "none_key": "Nine"}}, + ]}) + + def _return_aggregate(self): + r = {'aggregate': self.get_os_aggregates()[2]['aggregates'][0]} + return (200, {}, r) + + def _return_aggregate_3(self): + r = {'aggregate': self.get_os_aggregates()[2]['aggregates'][2]} + return (200, {}, r) + + def get_os_aggregates_1(self, **kw): + return self._return_aggregate() + + def get_os_aggregates_3(self, **kw): + return self._return_aggregate_3() + + def post_os_aggregates(self, body, **kw): + return self._return_aggregate() + + def put_os_aggregates_1(self, body, **kw): + return self._return_aggregate() + + def put_os_aggregates_2(self, body, **kw): + return self._return_aggregate() + + def put_os_aggregates_3(self, body, **kw): + return self._return_aggregate_3() + + def post_os_aggregates_1_action(self, body, **kw): + return self._return_aggregate() + + def post_os_aggregates_2_action(self, body, **kw): + return self._return_aggregate() + + def post_os_aggregates_3_action(self, body, **kw): + return self._return_aggregate_3() + + def delete_os_aggregates_1(self, **kw): + return (202, {}, None) + + # + # Services + # + def get_os_services(self, **kw): + host = kw.get('host', 'host1') + binary = kw.get('binary', 'nova-compute') + return (200, {}, {'services': [{'binary': binary, + 'host': host, + 'zone': 'nova', + 'status': 'enabled', + 'state': 'up', + 'updated_at': datetime.datetime( + 2012, 10, 29, 13, 42, 2)}, + {'binary': binary, + 'host': host, + 'zone': 'nova', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime.datetime( + 2012, 9, 18, 8, 3, 38)}, + ]}) + + def put_os_services_enable(self, body, **kw): + return (200, {}, {'service': {'host': body['host'], + 'binary': body['binary'], + 'status': 'enabled'}}) + + def put_os_services_disable(self, body, **kw): + return (200, {}, {'service': {'host': body['host'], + 'binary': body['binary'], + 'status': 'disabled'}}) + + def put_os_services_disable_log_reason(self, body, **kw): + return (200, {}, {'service': { + 'host': body['host'], + 'binary': body['binary'], + 'status': 'disabled', + 'disabled_reason': body['disabled_reason']}}) + + def delete_os_services_1(self, **kw): + return (204, {}, None) + + # + # Fixed IPs + # + def get_os_fixed_ips_192_168_1_1(self, *kw): + return (200, {}, {"fixed_ip": {'cidr': '192.168.1.0/24', + 'address': '192.168.1.1', + 'hostname': 'foo', + 'host': 'bar'}}) + + def post_os_fixed_ips_192_168_1_1_action(self, body, **kw): + return (202, {}, None) + + # + # Hosts + # + def get_os_hosts_host(self, *kw): + return (200, {}, {'host': + [{'resource': {'project': '(total)', 'host': 'dummy', + 'cpu': 16, 'memory_mb': 32234, 'disk_gb': 128}}, + {'resource': {'project': '(used_now)', 'host': 'dummy', + 'cpu': 1, 'memory_mb': 2075, 'disk_gb': 45}}, + {'resource': {'project': '(used_max)', 'host': 'dummy', + 'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}}, + {'resource': {'project': 'admin', 'host': 'dummy', + 'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}}]}) + + def get_os_hosts(self, **kw): + zone = kw.get('zone', 'nova1') + return (200, {}, {'hosts': [{'host': 'host1', + 'service': 'nova-compute', + 'zone': zone}, + {'host': 'host1', + 'service': 'nova-cert', + 'zone': zone}]}) + + def get_os_hosts_sample_host(self, *kw): + return (200, {}, {'host': [{'resource': {'host': 'sample_host'}}], }) + + def put_os_hosts_sample_host_1(self, body, **kw): + return (200, {}, {'host': 'sample-host_1', + 'status': 'enabled'}) + + def put_os_hosts_sample_host_2(self, body, **kw): + return (200, {}, {'host': 'sample-host_2', + 'maintenance_mode': 'on_maintenance'}) + + def put_os_hosts_sample_host_3(self, body, **kw): + return (200, {}, {'host': 'sample-host_3', + 'status': 'enabled', + 'maintenance_mode': 'on_maintenance'}) + + def get_os_hosts_sample_host_reboot(self, **kw): + return (200, {}, {'host': 'sample_host', + 'power_action': 'reboot'}) + + def get_os_hosts_sample_host_startup(self, **kw): + return (200, {}, {'host': 'sample_host', + 'power_action': 'startup'}) + + def get_os_hosts_sample_host_shutdown(self, **kw): + return (200, {}, {'host': 'sample_host', + 'power_action': 'shutdown'}) + + def put_os_hosts_sample_host(self, body, **kw): + result = {'host': 'dummy'} + result.update(body) + return (200, {}, result) + + def get_os_hypervisors(self, **kw): + return (200, {}, { + "hypervisors": [ + {'id': 1234, 'hypervisor_hostname': 'hyper1'}, + {'id': 5678, 'hypervisor_hostname': 'hyper2'}]}) + + def get_os_hypervisors_detail(self, **kw): + return (200, {}, { + "hypervisors": [ + {'id': 1234, + 'service': {'id': 1, 'host': 'compute1'}, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': "xen", + 'hypervisor_version': 3, + 'hypervisor_hostname': "hyper1", + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100}, + {'id': 2, + 'service': {'id': 2, 'host': "compute2"}, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': "xen", + 'hypervisor_version': 3, + 'hypervisor_hostname': "hyper2", + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100}] + }) + + def get_os_hypervisors_statistics(self, **kw): + return (200, {}, { + "hypervisor_statistics": { + 'count': 2, + 'vcpus': 8, + 'memory_mb': 20 * 1024, + 'local_gb': 500, + 'vcpus_used': 4, + 'memory_mb_used': 10 * 1024, + 'local_gb_used': 250, + 'free_ram_mb': 10 * 1024, + 'free_disk_gb': 250, + 'current_workload': 4, + 'running_vms': 4, + 'disk_available_least': 200} + }) + + def get_os_hypervisors_hyper_search(self, **kw): + return (200, {}, { + 'hypervisors': [ + {'id': 1234, 'hypervisor_hostname': 'hyper1'}, + {'id': 5678, 'hypervisor_hostname': 'hyper2'}]}) + + def get_os_hypervisors_hyper_servers(self, **kw): + return (200, {}, { + 'hypervisors': [ + {'id': 1234, + 'hypervisor_hostname': 'hyper1', + 'servers': [ + {'name': 'inst1', 'uuid': 'uuid1'}, + {'name': 'inst2', 'uuid': 'uuid2'}]}, + {'id': 5678, + 'hypervisor_hostname': 'hyper2', + 'servers': [ + {'name': 'inst3', 'uuid': 'uuid3'}, + {'name': 'inst4', 'uuid': 'uuid4'}]}] + }) + + def get_os_hypervisors_hyper_no_servers_servers(self, **kw): + return (200, {}, {'hypervisors': + [{'id': 1234, 'hypervisor_hostname': 'hyper1'}]}) + + def get_os_hypervisors_1234(self, **kw): + return (200, {}, { + 'hypervisor': + {'id': 1234, + 'service': {'id': 1, 'host': 'compute1'}, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': "xen", + 'hypervisor_version': 3, + 'hypervisor_hostname': "hyper1", + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100}}) + + def get_os_hypervisors_1234_uptime(self, **kw): + return (200, {}, { + 'hypervisor': {'id': 1234, + 'hypervisor_hostname': "hyper1", + 'uptime': "fake uptime"}}) + + def get_os_networks(self, **kw): + return (200, {}, {'networks': [{"label": "1", "cidr": "10.0.0.0/24", + 'project_id': + '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1', 'vlan': '1234'}]}) + + def delete_os_networks_1(self, **kw): + return (202, {}, None) + + def post_os_networks(self, **kw): + return (202, {}, {'network': kw}) + + def get_os_networks_1(self, **kw): + return (200, {}, {'network': {"label": "1", "cidr": "10.0.0.0/24", + "id": "1"}}) + + def delete_os_networks_networkdelete(self, **kw): + return (202, {}, None) + + def post_os_networks_add(self, **kw): + return (202, {}, None) + + def post_os_networks_networkdisassociate_action(self, **kw): + return (202, {}, None) + + def get_os_fping(self, **kw): + return ( + 200, {}, { + 'servers': [ + { + "id": "1", + "project_id": "fake-project", + "alive": True, + }, + { + "id": "2", + "project_id": "fake-project", + "alive": True, + }, + ] + } + ) + + def get_os_fping_1(self, **kw): + return ( + 200, {}, { + 'server': { + "id": "1", + "project_id": "fake-project", + "alive": True, + } + } + ) + + def post_os_networks_1_action(self, **kw): + return (202, {}, None) + + def post_os_networks_networktest_action(self, **kw): + return (202, {}, None) + + def post_os_networks_2_action(self, **kw): + return (202, {}, None) + + def get_os_tenant_networks(self, **kw): + return (200, {}, {'networks': [{"label": "1", "cidr": "10.0.0.0/24", + 'project_id': + '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1', 'vlan': '1234'}]}) + + def get_os_tenant_networks_1(self, **kw): + return (200, {}, {'network': {"label": "1", "cidr": "10.0.0.0/24", + "id": "1"}}) + + def post_os_tenant_networks(self, **kw): + return (202, {}, {'network': {"label": "new_network1", + "cidr1": "10.0.1.0/24"}}) + + def delete_os_tenant_networks_1(self, **kw): + return (202, {}, None) + + def get_os_availability_zone(self, **kw): + return (200, {}, { + "availabilityZoneInfo": [ + {"zoneName": "zone-1", + "zoneState": {"available": True}, + "hosts": None}, + {"zoneName": "zone-2", + "zoneState": {"available": False}, + "hosts": None}] + }) + + def get_os_availability_zone_detail(self, **kw): + return (200, {}, { + "availabilityZoneInfo": [ + {"zoneName": "zone-1", + "zoneState": {"available": True}, + "hosts": { + "fake_host-1": { + "nova-compute": { + "active": True, + "available": True, + "updated_at": datetime.datetime( + 2012, 12, 26, 14, 45, 25, 0)}}}}, + {"zoneName": "internal", + "zoneState": {"available": True}, + "hosts": { + "fake_host-1": { + "nova-sched": { + "active": True, + "available": True, + "updated_at": datetime.datetime( + 2012, 12, 26, 14, 45, 25, 0)}}, + "fake_host-2": { + "nova-network": { + "active": True, + "available": False, + "updated_at": datetime.datetime( + 2012, 12, 26, 14, 45, 24, 0)}}}}, + {"zoneName": "zone-2", + "zoneState": {"available": False}, + "hosts": None}]}) + + def get_servers_1234_os_interface(self, **kw): + return (200, {}, { + "interfaceAttachments": [ + {"port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + }, + {"port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + }] + }) + + def post_servers_1234_os_interface(self, **kw): + return (200, {}, {'interfaceAttachment': {}}) + + def delete_servers_1234_os_interface_port_id(self, **kw): + return (200, {}, None) + + # NOTE (vkhomenko): + # Volume responses was taken from: + # https://wiki.openstack.org/wiki/CreateVolumeFromImage + # http://jorgew.github.com/block-storage-api/content/ + # GET_listDetailVolumes_v1__tenantId__volumes_detail_.html + # I suppose they are outdated and should be updated after Cinder released + + def get_volumes_detail(self, **kw): + return (200, {}, {"volumes": [ + { + "display_name": "Work", + "display_description": "volume for work", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [ + {"id": "3333", + "links": ''}], + "metadata": {}}, + { + "display_name": "Work2", + "display_description": "volume for work2", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-ee32ba30feaa", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [ + {"id": "2222", + "links": ''}], + "metadata": {}}]}) + + def get_volumes(self, **kw): + return (200, {}, {"volumes": [ + { + "display_name": "Work", + "display_description": "volume for work", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [ + {"id": "3333", + "links": ''}], + "metadata": {}}, + { + "display_name": "Work2", + "display_description": "volume for work2", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-ee32ba30feaa", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [ + {"id": "2222", + "links": ''}], + "metadata": {}}]}) + + def get_volumes_15e59938_07d5_11e1_90e3_e3dffe0c5983(self, **kw): + return (200, {}, { + "volume": self.get_volumes_detail()[2]['volumes'][0]}) + + def get_volumes_15e59938_07d5_11e1_90e3_ee32ba30feaa(self, **kw): + return (200, {}, { + "volume": self.get_volumes_detail()[2]['volumes'][1]}) + + def post_volumes(self, **kw): + return (200, {}, {"volume": + {"status": "creating", + "display_name": "vol-007", + "attachments": [(0)], + "availability_zone": "cinder", + "created_at": "2012-08-13T10:57:17.000000", + "display_description": "create volume from image", + "image_id": "f4cf905f-7c58-4d7b-8314-8dd8a2d1d483", + "volume_type": "None", + "metadata": {}, + "id": "5cb239f6-1baf-4fe1-bd78-c852cf00fa39", + "size": 1}}) + + def delete_volumes_15e59938_07d5_11e1_90e3_e3dffe0c5983(self, **kw): + return (200, {}, {}) + + def delete_volumes_15e59938_07d5_11e1_90e3_ee32ba30feaa(self, **kw): + return (200, {}, {}) + + def post_servers_1234_os_volume_attachments(self, **kw): + return (200, {}, { + "volumeAttachment": + {"device": "/dev/vdb", + "volumeId": 2}}) + + def put_servers_1234_os_volume_attachments_Work(self, **kw): + return (200, {}, {"volumeAttachment": {"volumeId": 2}}) + + def get_servers_1234_os_volume_attachments(self, **kw): + return (200, {}, { + "volumeAttachments": [ + {"display_name": "Work", + "display_description": "volume for work", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [{"id": "3333", "links": ''}], + "metadata": {}}]}) + + def get_servers_1234_os_volume_attachments_Work(self, **kw): + return (200, {}, { + "volumeAttachment": + {"display_name": "Work", + "display_description": "volume for work", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [{"id": "3333", "links": ''}], + "metadata": {}}}) + + def delete_servers_1234_os_volume_attachments_Work(self, **kw): + return (200, {}, {}) + + def get_servers_1234_os_instance_actions(self, **kw): + return (200, {}, { + "instanceActions": + [{"instance_uuid": "1234", + "user_id": "b968c25e04ab405f9fe4e6ca54cce9a5", + "start_time": "2013-03-25T13:45:09.000000", + "request_id": "req-abcde12345", + "action": "create", + "message": None, + "project_id": "04019601fe3648c0abd4f4abfb9e6106"}]}) + + def get_servers_1234_os_instance_actions_req_abcde12345(self, **kw): + return (200, {}, { + "instanceAction": + {"instance_uuid": "1234", + "user_id": "b968c25e04ab405f9fe4e6ca54cce9a5", + "start_time": "2013-03-25T13:45:09.000000", + "request_id": "req-abcde12345", + "action": "create", + "message": None, + "project_id": "04019601fe3648c0abd4f4abfb9e6106"}}) + + def post_servers_uuid1_action(self, **kw): + return 202, {}, {} + + def post_servers_uuid2_action(self, **kw): + return 202, {}, {} + + def post_servers_uuid3_action(self, **kw): + return 202, {}, {} + + def post_servers_uuid4_action(self, **kw): + return 202, {}, {} + + def get_os_cells_child_cell(self, **kw): + cell = {'cell': { + 'username': 'cell1_user', + 'name': 'cell1', + 'rpc_host': '10.0.1.10', + 'info': { + 'username': 'cell1_user', + 'rpc_host': '10.0.1.10', + 'type': 'child', + 'name': 'cell1', + 'rpc_port': 5673}, + 'type': 'child', + 'rpc_port': 5673, + 'loaded': True + }} + return (200, {}, cell) + + def get_os_cells_capacities(self, **kw): + cell_capacities_response = {"cell": {"capacities": {"ram_free": { + "units_by_mb": {"8192": 0, "512": 13, "4096": 1, "2048": 3, + "16384": 0}, "total_mb": 7680}, "disk_free": { + "units_by_mb": {"81920": 11, "20480": 46, "40960": 23, "163840": 5, + "0": 0}, "total_mb": 1052672}}}} + return (200, {}, cell_capacities_response) + + def get_os_cells_child_cell_capacities(self, **kw): + return self.get_os_cells_capacities() + + def get_os_migrations(self, **kw): + migrations = {'migrations': [ + { + "created_at": "2012-10-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": 1234, + "instance_uuid": "instance_id_123", + "new_instance_type_id": 2, + "old_instance_type_id": 1, + "source_compute": "compute1", + "source_node": "node1", + "status": "Done", + "updated_at": "2012-10-29T13:42:02.000000" + }]} + return (200, {}, migrations) + + def post_os_server_external_events(self, **kw): + return (200, {}, {'events': [ + {'name': 'network-changed', + 'server_uuid': '1234'}]}) + + # + # Server Groups + # + + def get_os_server_groups(self, *kw): + return (200, {}, + {"server_groups": [ + {"members": [], "metadata": {}, + "id": "2cbd51f4-fafe-4cdb-801b-cf913a6f288b", + "policies": [], "name": "ig1"}, + {"members": [], "metadata": {}, + "id": "4473bb03-4370-4bfb-80d3-dc8cffc47d94", + "policies": ["anti-affinity"], "name": "ig2"}, + {"members": [], "metadata": {"key": "value"}, + "id": "31ab9bdb-55e1-4ac3-b094-97eeb1b65cc4", + "policies": [], "name": "ig3"}, + {"members": ["2dccb4a1-02b9-482a-aa23-5799490d6f5d"], + "metadata": {}, + "id": "4890bb03-7070-45fb-8453-d34556c87d94", + "policies": ["anti-affinity"], "name": "ig2"}]}) + + def _return_server_group(self): + r = {'server_group': + self.get_os_server_groups()[2]['server_groups'][0]} + return (200, {}, r) + + def post_os_server_groups(self, body, **kw): + return self._return_server_group() + + def get_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b(self, **kw): + return self._return_server_group() + + def put_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b(self, **kw): + return self._return_server_group() + + def post_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b_action( + self, body, **kw): + return self._return_server_group() + + def delete_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b( + self, **kw): + return (202, {}, None) + + +class FakeSessionClient(fakes.FakeClient, client.Client): + + def __init__(self, *args, **kwargs): + client.Client.__init__(self, 'username', 'password', + 'project_id', 'auth_url', + extensions=kwargs.get('extensions')) + self.client = FakeSessionMockClient(**kwargs) + + +class FakeSessionMockClient(base_client.SessionClient, FakeHTTPClient): + + def __init__(self, *args, **kwargs): + + self.callstack = [] + self.auth = mock.Mock() + self.session = mock.Mock() + self.service_type = 'service_type' + + self.auth.get_auth_ref.return_value.project_id = 'tenant_id' + + def request(self, url, method, **kwargs): + return self._cs_request(url, method, **kwargs) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_agents.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_agents.py new file mode 100644 index 0000000000..e7fa7d6edb --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_agents.py @@ -0,0 +1,104 @@ +# Copyright 2012 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import agents as data +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit import utils +from novaclient.v2 import agents + + +class AgentsTest(utils.FixturedTestCase): + + data_fixture_class = data.Fixture + + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + + def stub_hypervisors(self, hypervisor='kvm'): + get_os_agents = { + 'agents': [ + { + '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 + }, + ] + } + + headers = {'Content-Type': 'application/json'} + self.requests.register_uri('GET', self.data_fixture.url(), + json=get_os_agents, + headers=headers) + + def test_list_agents(self): + self.stub_hypervisors() + ags = self.cs.agents.list() + self.assert_called('GET', '/os-agents') + for a in ags: + self.assertIsInstance(a, agents.Agent) + self.assertEqual('kvm', a.hypervisor) + + def test_list_agents_with_hypervisor(self): + self.stub_hypervisors('xen') + ags = self.cs.agents.list('xen') + self.assert_called('GET', '/os-agents?hypervisor=xen') + for a in ags: + self.assertIsInstance(a, agents.Agent) + self.assertEqual('xen', a.hypervisor) + + def test_agents_create(self): + ag = self.cs.agents.create('win', 'x86', '7.0', + '/xxx/xxx/xxx', + 'add6bb58e139be103324d04d82d8f546', + 'xen') + body = {'agent': {'url': '/xxx/xxx/xxx', + 'hypervisor': 'xen', + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'version': '7.0', + 'architecture': 'x86', + 'os': 'win'}} + 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.assert_called('DELETE', '/os-agents/1') + + def _build_example_update_body(self): + return {"para": { + "url": "/yyy/yyyy/yyyy", + "version": "8.0", + "md5hash": "add6bb58e139be103324d04d82d8f546"}} + + def test_agents_modify(self): + ag = self.cs.agents.update('1', '8.0', + '/yyy/yyyy/yyyy', + 'add6bb58e139be103324d04d82d8f546') + body = self._build_example_update_body() + self.assert_called('PUT', '/os-agents/1', body) + self.assertEqual(1, ag.id) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_aggregates.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_aggregates.py new file mode 100644 index 0000000000..97a3ffc2bc --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_aggregates.py @@ -0,0 +1,141 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import aggregates as data +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit import utils +from novaclient.v2 import aggregates + + +class AggregatesTest(utils.FixturedTestCase): + + data_fixture_class = data.Fixture + + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + + def test_list_aggregates(self): + result = self.cs.aggregates.list() + self.assert_called('GET', '/os-aggregates') + 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.assert_called('POST', '/os-aggregates', body) + self.assertIsInstance(aggregate, aggregates.Aggregate) + + def test_get(self): + aggregate = self.cs.aggregates.get("1") + self.assert_called('GET', '/os-aggregates/1') + self.assertIsInstance(aggregate, aggregates.Aggregate) + + aggregate2 = self.cs.aggregates.get(aggregate) + self.assert_called('GET', '/os-aggregates/1') + self.assertIsInstance(aggregate2, aggregates.Aggregate) + + def test_get_details(self): + aggregate = self.cs.aggregates.get_details("1") + self.assert_called('GET', '/os-aggregates/1') + self.assertIsInstance(aggregate, aggregates.Aggregate) + + aggregate2 = self.cs.aggregates.get_details(aggregate) + self.assert_called('GET', '/os-aggregates/1') + self.assertIsInstance(aggregate2, aggregates.Aggregate) + + def test_update(self): + aggregate = self.cs.aggregates.get("1") + values = {"name": "foo"} + body = {"aggregate": values} + + result1 = aggregate.update(values) + self.assert_called('PUT', '/os-aggregates/1', body) + self.assertIsInstance(result1, aggregates.Aggregate) + + result2 = self.cs.aggregates.update(2, values) + self.assert_called('PUT', '/os-aggregates/2', body) + self.assertIsInstance(result2, aggregates.Aggregate) + + def test_update_with_availability_zone(self): + aggregate = self.cs.aggregates.get("1") + values = {"name": "foo", "availability_zone": "new_zone"} + body = {"aggregate": values} + + result3 = self.cs.aggregates.update(aggregate, values) + self.assert_called('PUT', '/os-aggregates/1', body) + self.assertIsInstance(result3, aggregates.Aggregate) + + def test_add_host(self): + aggregate = self.cs.aggregates.get("1") + host = "host1" + body = {"add_host": {"host": "host1"}} + + result1 = aggregate.add_host(host) + self.assert_called('POST', '/os-aggregates/1/action', body) + self.assertIsInstance(result1, aggregates.Aggregate) + + result2 = self.cs.aggregates.add_host("2", host) + self.assert_called('POST', '/os-aggregates/2/action', body) + self.assertIsInstance(result2, aggregates.Aggregate) + + result3 = self.cs.aggregates.add_host(aggregate, host) + self.assert_called('POST', '/os-aggregates/1/action', body) + self.assertIsInstance(result3, aggregates.Aggregate) + + def test_remove_host(self): + aggregate = self.cs.aggregates.get("1") + host = "host1" + body = {"remove_host": {"host": "host1"}} + + result1 = aggregate.remove_host(host) + self.assert_called('POST', '/os-aggregates/1/action', body) + self.assertIsInstance(result1, aggregates.Aggregate) + + result2 = self.cs.aggregates.remove_host("2", host) + self.assert_called('POST', '/os-aggregates/2/action', body) + self.assertIsInstance(result2, aggregates.Aggregate) + + result3 = self.cs.aggregates.remove_host(aggregate, host) + self.assert_called('POST', '/os-aggregates/1/action', body) + self.assertIsInstance(result3, aggregates.Aggregate) + + def test_set_metadata(self): + aggregate = self.cs.aggregates.get("1") + metadata = {"foo": "bar"} + body = {"set_metadata": {"metadata": metadata}} + + result1 = aggregate.set_metadata(metadata) + self.assert_called('POST', '/os-aggregates/1/action', body) + self.assertIsInstance(result1, aggregates.Aggregate) + + result2 = self.cs.aggregates.set_metadata(2, metadata) + self.assert_called('POST', '/os-aggregates/2/action', body) + self.assertIsInstance(result2, aggregates.Aggregate) + + result3 = self.cs.aggregates.set_metadata(aggregate, metadata) + 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.assert_called('DELETE', '/os-aggregates/1') + + self.cs.aggregates.delete('1') + self.assert_called('DELETE', '/os-aggregates/1') + + self.cs.aggregates.delete(aggregate) + self.assert_called('DELETE', '/os-aggregates/1') diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_auth.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_auth.py new file mode 100644 index 0000000000..696428a4a9 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_auth.py @@ -0,0 +1,387 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import json + +from keystoneclient import fixture +import mock +import requests + +from novaclient import exceptions +from novaclient.tests.unit import utils +from novaclient.v2 import client + + +class AuthenticateAgainstKeystoneTests(utils.TestCase): + + def get_token(self, **kwargs): + resp = fixture.V2Token(**kwargs) + resp.set_scope() + + s = resp.add_service('compute') + s.add_endpoint('http://localhost:8774/v1.1', region='RegionOne') + + return resp + + def test_authenticate_success(self): + cs = client.Client("username", "password", "project_id", + utils.AUTH_URL_V2, service_type='compute') + resp = self.get_token() + + auth_response = utils.TestResponse({ + "status_code": 200, + "text": json.dumps(resp), + }) + + mock_request = mock.Mock(return_value=(auth_response)) + + @mock.patch.object(requests, "request", mock_request) + def test_auth_call(): + cs.client.authenticate() + headers = { + 'User-Agent': cs.client.USER_AGENT, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + body = { + 'auth': { + 'passwordCredentials': { + 'username': cs.client.user, + 'password': cs.client.password, + }, + 'tenantName': cs.client.projectid, + }, + } + + token_url = cs.client.auth_url + "/tokens" + mock_request.assert_called_with( + "POST", + token_url, + headers=headers, + data=json.dumps(body), + allow_redirects=True, + **self.TEST_REQUEST_BASE) + + endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] + public_url = endpoints[0]["publicURL"].rstrip('/') + self.assertEqual(cs.client.management_url, public_url) + token_id = resp["access"]["token"]["id"] + self.assertEqual(cs.client.auth_token, token_id) + + test_auth_call() + + def test_authenticate_failure(self): + cs = client.Client("username", "password", "project_id", + utils.AUTH_URL_V2) + resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} + auth_response = utils.TestResponse({ + "status_code": 401, + "text": json.dumps(resp), + }) + + mock_request = mock.Mock(return_value=(auth_response)) + + @mock.patch.object(requests.Session, "request", mock_request) + def test_auth_call(): + self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) + + test_auth_call() + + def test_v1_auth_redirect(self): + cs = client.Client("username", "password", "project_id", + utils.AUTH_URL_V1, service_type='compute') + dict_correct_response = self.get_token() + correct_response = json.dumps(dict_correct_response) + dict_responses = [ + {"headers": {'location': 'http://127.0.0.1:5001'}, + "status_code": 305, + "text": "Use proxy"}, + # Configured on admin port, nova redirects to v2.0 port. + # When trying to connect on it, keystone auth succeed by v1.0 + # protocol (through headers) but tokens are being returned in + # body (looks like keystone bug). Leaved for compatibility. + {"headers": {}, + "status_code": 200, + "text": correct_response}, + {"headers": {}, + "status_code": 200, + "text": correct_response} + ] + + responses = [(utils.TestResponse(resp)) for resp in dict_responses] + + def side_effect(*args, **kwargs): + return responses.pop(0) + + mock_request = mock.Mock(side_effect=side_effect) + + @mock.patch.object(requests, "request", mock_request) + def test_auth_call(): + cs.client.authenticate() + headers = { + 'User-Agent': cs.client.USER_AGENT, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + body = { + 'auth': { + 'passwordCredentials': { + 'username': cs.client.user, + 'password': cs.client.password, + }, + 'tenantName': cs.client.projectid, + }, + } + + token_url = cs.client.auth_url + "/tokens" + kwargs = copy.copy(self.TEST_REQUEST_BASE) + kwargs['headers'] = headers + kwargs['data'] = json.dumps(body) + mock_request.assert_called_with( + "POST", + token_url, + allow_redirects=True, + **kwargs) + + resp = dict_correct_response + endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] + public_url = endpoints[0]["publicURL"].rstrip('/') + self.assertEqual(cs.client.management_url, public_url) + token_id = resp["access"]["token"]["id"] + self.assertEqual(cs.client.auth_token, token_id) + + test_auth_call() + + def test_v2_auth_redirect(self): + cs = client.Client("username", "password", "project_id", + utils.AUTH_URL_V2, service_type='compute') + dict_correct_response = self.get_token() + correct_response = json.dumps(dict_correct_response) + dict_responses = [ + {"headers": {'location': 'http://127.0.0.1:5001'}, + "status_code": 305, + "text": "Use proxy"}, + # Configured on admin port, nova redirects to v2.0 port. + # When trying to connect on it, keystone auth succeed by v1.0 + # protocol (through headers) but tokens are being returned in + # body (looks like keystone bug). Leaved for compatibility. + {"headers": {}, + "status_code": 200, + "text": correct_response}, + {"headers": {}, + "status_code": 200, + "text": correct_response} + ] + + responses = [(utils.TestResponse(resp)) for resp in dict_responses] + + def side_effect(*args, **kwargs): + return responses.pop(0) + + mock_request = mock.Mock(side_effect=side_effect) + + @mock.patch.object(requests, "request", mock_request) + def test_auth_call(): + cs.client.authenticate() + headers = { + 'User-Agent': cs.client.USER_AGENT, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + body = { + 'auth': { + 'passwordCredentials': { + 'username': cs.client.user, + 'password': cs.client.password, + }, + 'tenantName': cs.client.projectid, + }, + } + + token_url = cs.client.auth_url + "/tokens" + kwargs = copy.copy(self.TEST_REQUEST_BASE) + kwargs['headers'] = headers + kwargs['data'] = json.dumps(body) + mock_request.assert_called_with( + "POST", + token_url, + allow_redirects=True, + **kwargs) + + resp = dict_correct_response + endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] + public_url = endpoints[0]["publicURL"].rstrip('/') + self.assertEqual(cs.client.management_url, public_url) + token_id = resp["access"]["token"]["id"] + self.assertEqual(cs.client.auth_token, token_id) + + test_auth_call() + + def test_ambiguous_endpoints(self): + cs = client.Client("username", "password", "project_id", + utils.AUTH_URL_V2, service_type='compute') + resp = self.get_token() + + # duplicate existing service + s = resp.add_service('compute') + s.add_endpoint('http://localhost:8774/v1.1', region='RegionOne') + + auth_response = utils.TestResponse({ + "status_code": 200, + "text": json.dumps(resp), + }) + + mock_request = mock.Mock(return_value=(auth_response)) + + @mock.patch.object(requests.Session, "request", mock_request) + def test_auth_call(): + self.assertRaises(exceptions.AmbiguousEndpoints, + cs.client.authenticate) + + test_auth_call() + + def test_authenticate_with_token_success(self): + cs = client.Client("username", None, "project_id", + utils.AUTH_URL_V2, service_type='compute') + cs.client.auth_token = "FAKE_ID" + resp = self.get_token(token_id="FAKE_ID") + auth_response = utils.TestResponse({ + "status_code": 200, + "text": json.dumps(resp), + }) + + mock_request = mock.Mock(return_value=(auth_response)) + + with mock.patch.object(requests, "request", mock_request): + cs.client.authenticate() + headers = { + 'User-Agent': cs.client.USER_AGENT, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + body = { + 'auth': { + 'token': { + 'id': cs.client.auth_token, + }, + 'tenantName': cs.client.projectid, + }, + } + + token_url = cs.client.auth_url + "/tokens" + mock_request.assert_called_with( + "POST", + token_url, + headers=headers, + data=json.dumps(body), + allow_redirects=True, + **self.TEST_REQUEST_BASE) + + endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] + public_url = endpoints[0]["publicURL"].rstrip('/') + self.assertEqual(cs.client.management_url, public_url) + token_id = resp["access"]["token"]["id"] + self.assertEqual(cs.client.auth_token, token_id) + + def test_authenticate_with_token_failure(self): + cs = client.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({ + "status_code": 401, + "text": json.dumps(resp), + }) + + mock_request = mock.Mock(return_value=(auth_response)) + + with mock.patch.object(requests.Session, "request", mock_request): + self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) + + +class AuthenticationTests(utils.TestCase): + def test_authenticate_success(self): + cs = client.Client("username", "password", + "project_id", utils.AUTH_URL) + management_url = 'https://localhost/v1.1/443470' + auth_response = utils.TestResponse({ + 'status_code': 204, + 'headers': { + 'x-server-management-url': management_url, + 'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1', + }, + }) + mock_request = mock.Mock(return_value=(auth_response)) + + @mock.patch.object(requests, "request", mock_request) + def test_auth_call(): + cs.client.authenticate() + headers = { + 'Accept': 'application/json', + 'X-Auth-User': 'username', + 'X-Auth-Key': 'password', + 'X-Auth-Project-Id': 'project_id', + 'User-Agent': cs.client.USER_AGENT + } + mock_request.assert_called_with( + "GET", + cs.client.auth_url, + headers=headers, + **self.TEST_REQUEST_BASE) + + self.assertEqual(cs.client.management_url, + auth_response.headers['x-server-management-url']) + self.assertEqual(cs.client.auth_token, + auth_response.headers['x-auth-token']) + + test_auth_call() + + def test_authenticate_failure(self): + cs = client.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, "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", utils.AUTH_URL) + http_client = cs.client + http_client.management_url = '' + http_client.get_service_url = mock.Mock(return_value='') + mock_request = mock.Mock(return_value=(None, None)) + + @mock.patch.object(http_client, 'request', mock_request) + @mock.patch.object(http_client, 'authenticate') + def test_auth_call(m): + http_client.get('/') + self.assertTrue(m.called) + self.assertTrue(mock_request.called) + + test_auth_call() + + def test_auth_manual(self): + cs = client.Client("username", "password", + "project_id", utils.AUTH_URL) + + @mock.patch.object(cs.client, 'authenticate') + def test_auth_call(m): + cs.authenticate() + self.assertTrue(m.called) + + test_auth_call() diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_availability_zone.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_availability_zone.py new file mode 100644 index 0000000000..a7d6d82e28 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_availability_zone.py @@ -0,0 +1,102 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import six + +from novaclient.tests.unit.fixture_data import availability_zones as data +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit import utils +from novaclient.v2 import availability_zones + + +class AvailabilityZoneTest(utils.FixturedTestCase): + # NOTE(cyeoh): import shell here so the V3 version of + # this class can inherit off the v3 version of shell + from novaclient.v2 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.availability_zone_type = self._get_availability_zone_type() + + def _get_availability_zone_type(self): + return availability_zones.AvailabilityZone + + def _assertZone(self, zone, name, status): + self.assertEqual(zone.zoneName, name) + self.assertEqual(zone.zoneState, status) + + def test_list_availability_zone(self): + zones = self.cs.availability_zones.list(detailed=False) + self.assert_called('GET', '/os-availability-zone') + + for zone in zones: + self.assertIsInstance(zone, self.availability_zone_type) + + self.assertEqual(2, len(zones)) + + l0 = [six.u('zone-1'), six.u('available')] + l1 = [six.u('zone-2'), six.u('not available')] + + z0 = self.shell._treeizeAvailabilityZone(zones[0]) + z1 = self.shell._treeizeAvailabilityZone(zones[1]) + + self.assertEqual((1, 1), (len(z0), len(z1))) + + self._assertZone(z0[0], l0[0], l0[1]) + self._assertZone(z1[0], l1[0], l1[1]) + + def test_detail_availability_zone(self): + zones = self.cs.availability_zones.list(detailed=True) + self.assert_called('GET', '/os-availability-zone/detail') + + for zone in zones: + self.assertIsInstance(zone, self.availability_zone_type) + + self.assertEqual(3, len(zones)) + + l0 = [six.u('zone-1'), six.u('available')] + l1 = [six.u('|- fake_host-1'), six.u('')] + l2 = [six.u('| |- nova-compute'), + six.u('enabled :-) 2012-12-26 14:45:25')] + l3 = [six.u('internal'), six.u('available')] + l4 = [six.u('|- fake_host-1'), six.u('')] + l5 = [six.u('| |- nova-sched'), + six.u('enabled :-) 2012-12-26 14:45:25')] + l6 = [six.u('|- fake_host-2'), six.u('')] + l7 = [six.u('| |- nova-network'), + six.u('enabled XXX 2012-12-26 14:45:24')] + l8 = [six.u('zone-2'), six.u('not available')] + + z0 = self.shell._treeizeAvailabilityZone(zones[0]) + z1 = self.shell._treeizeAvailabilityZone(zones[1]) + z2 = self.shell._treeizeAvailabilityZone(zones[2]) + + self.assertEqual((3, 5, 1), (len(z0), len(z1), len(z2))) + + self._assertZone(z0[0], l0[0], l0[1]) + self._assertZone(z0[1], l1[0], l1[1]) + self._assertZone(z0[2], l2[0], l2[1]) + self._assertZone(z1[0], l3[0], l3[1]) + self._assertZone(z1[1], l4[0], l4[1]) + self._assertZone(z1[2], l5[0], l5[1]) + self._assertZone(z1[3], l6[0], l6[1]) + self._assertZone(z1[4], l7[0], l7[1]) + self._assertZone(z2[0], l8[0], l8[1]) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_certs.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_certs.py new file mode 100644 index 0000000000..6ec2258fc3 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_certs.py @@ -0,0 +1,36 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import certs as data +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit import utils +from novaclient.v2 import certs + + +class CertsTest(utils.FixturedTestCase): + + data_fixture_class = data.Fixture + cert_type = certs.Certificate + + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + + def test_create_cert(self): + cert = self.cs.certs.create() + self.assert_called('POST', '/os-certificates') + self.assertIsInstance(cert, self.cert_type) + + def test_get_root_cert(self): + cert = self.cs.certs.get() + self.assert_called('GET', '/os-certificates/root') + self.assertIsInstance(cert, self.cert_type) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_client.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_client.py new file mode 100644 index 0000000000..b4c59ce381 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_client.py @@ -0,0 +1,45 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneclient import session + +from novaclient.tests.unit import utils +from novaclient.v2 import client + + +class ClientTest(utils.TestCase): + + def test_adapter_properties(self): + # sample of properties, there are many more + user_agent = uuid.uuid4().hex + endpoint_override = uuid.uuid4().hex + + s = session.Session() + c = client.Client(session=s, + user_agent=user_agent, + endpoint_override=endpoint_override) + + self.assertEqual(user_agent, c.client.user_agent) + self.assertEqual(endpoint_override, c.client.endpoint_override) + + def test_passing_interface(self): + endpoint_type = uuid.uuid4().hex + interface = uuid.uuid4().hex + + s = session.Session() + c = client.Client(session=s, + interface=interface, + endpoint_type=endpoint_type) + + self.assertEqual(interface, c.client.interface) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_cloudpipe.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_cloudpipe.py new file mode 100644 index 0000000000..3ef9533cd0 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_cloudpipe.py @@ -0,0 +1,45 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import six + +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import cloudpipe as data +from novaclient.tests.unit import utils +from novaclient.v2 import cloudpipe + + +class CloudpipeTest(utils.FixturedTestCase): + + data_fixture_class = data.Fixture + + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + + def test_list_cloudpipes(self): + cp = self.cs.cloudpipe.list() + self.assert_called('GET', '/os-cloudpipe') + [self.assertIsInstance(c, cloudpipe.Cloudpipe) for c in cp] + + def test_create(self): + project = "test" + cp = self.cs.cloudpipe.create(project) + body = {'cloudpipe': {'project_id': project}} + self.assert_called('POST', '/os-cloudpipe', body) + self.assertIsInstance(cp, six.string_types) + + def test_update(self): + self.cs.cloudpipe.update("192.168.1.1", 2345) + body = {'configure_project': {'vpn_ip': "192.168.1.1", + 'vpn_port': 2345}} + self.assert_called('PUT', '/os-cloudpipe/configure-project', body) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_fixed_ips.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_fixed_ips.py new file mode 100644 index 0000000000..02ba39b6d0 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_fixed_ips.py @@ -0,0 +1,44 @@ +# Copyright 2012 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import fixedips as data +from novaclient.tests.unit import utils + + +class FixedIpsTest(utils.FixturedTestCase): + + data_fixture_class = data.Fixture + + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + + def test_get_fixed_ip(self): + info = self.cs.fixed_ips.get(fixed_ip='192.168.1.1') + self.assert_called('GET', '/os-fixed-ips/192.168.1.1') + self.assertEqual('192.168.1.0/24', info.cidr) + self.assertEqual('192.168.1.1', info.address) + self.assertEqual('foo', info.hostname) + self.assertEqual('bar', info.host) + + def test_reserve_fixed_ip(self): + body = {"reserve": None} + 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} + 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/unit/v2/test_flavor_access.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_flavor_access.py new file mode 100644 index 0000000000..109f177e66 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_flavor_access.py @@ -0,0 +1,70 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import flavor_access + + +cs = fakes.FakeClient() + + +class FlavorAccessTest(utils.TestCase): + + def test_list_access_by_flavor_private(self): + kwargs = {'flavor': cs.flavors.get(2)} + r = cs.flavor_access.list(**kwargs) + cs.assert_called('GET', '/flavors/2/os-flavor-access') + [self.assertIsInstance(a, flavor_access.FlavorAccess) for a in r] + + def test_add_tenant_access(self): + flavor = cs.flavors.get(2) + tenant = 'proj2' + r = cs.flavor_access.add_tenant_access(flavor, tenant) + + body = { + "addTenantAccess": { + "tenant": "proj2" + } + } + + cs.assert_called('POST', '/flavors/2/action', body) + [self.assertIsInstance(a, flavor_access.FlavorAccess) for a in r] + + def test_remove_tenant_access(self): + flavor = cs.flavors.get(2) + tenant = 'proj2' + r = cs.flavor_access.remove_tenant_access(flavor, tenant) + + body = { + "removeTenantAccess": { + "tenant": "proj2" + } + } + + 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/unit/v2/test_flavors.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_flavors.py new file mode 100644 index 0000000000..504590277c --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_flavors.py @@ -0,0 +1,219 @@ +# Copyright (c) 2013, OpenStack +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from novaclient import exceptions +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import flavors + + +class FlavorsTest(utils.TestCase): + def setUp(self): + super(FlavorsTest, self).setUp() + self.cs = self._get_fake_client() + self.flavor_type = self._get_flavor_type() + + def _get_fake_client(self): + return fakes.FakeClient() + + def _get_flavor_type(self): + return flavors.Flavor + + def test_list_flavors(self): + fl = self.cs.flavors.list() + self.cs.assert_called('GET', '/flavors/detail') + for flavor in fl: + self.assertIsInstance(flavor, self.flavor_type) + + def test_list_flavors_undetailed(self): + fl = self.cs.flavors.list(detailed=False) + self.cs.assert_called('GET', '/flavors') + for flavor in fl: + self.assertIsInstance(flavor, self.flavor_type) + + def test_list_flavors_is_public_none(self): + fl = self.cs.flavors.list(is_public=None) + self.cs.assert_called('GET', '/flavors/detail?is_public=None') + for flavor in fl: + self.assertIsInstance(flavor, self.flavor_type) + + def test_list_flavors_is_public_false(self): + fl = self.cs.flavors.list(is_public=False) + self.cs.assert_called('GET', '/flavors/detail?is_public=False') + for flavor in fl: + self.assertIsInstance(flavor, self.flavor_type) + + def test_list_flavors_is_public_true(self): + fl = self.cs.flavors.list(is_public=True) + self.cs.assert_called('GET', '/flavors/detail') + for flavor in fl: + self.assertIsInstance(flavor, self.flavor_type) + + def test_get_flavor_details(self): + f = self.cs.flavors.get(1) + self.cs.assert_called('GET', '/flavors/1') + self.assertIsInstance(f, self.flavor_type) + self.assertEqual(256, f.ram) + self.assertEqual(10, f.disk) + self.assertEqual(10, f.ephemeral) + self.assertTrue(f.is_public) + + def test_get_flavor_details_alphanum_id(self): + f = self.cs.flavors.get('aa1') + self.cs.assert_called('GET', '/flavors/aa1') + self.assertIsInstance(f, self.flavor_type) + self.assertEqual(128, f.ram) + self.assertEqual(0, f.disk) + self.assertEqual(0, f.ephemeral) + self.assertTrue(f.is_public) + + def test_get_flavor_details_diablo(self): + f = self.cs.flavors.get(3) + self.cs.assert_called('GET', '/flavors/3') + self.assertIsInstance(f, self.flavor_type) + self.assertEqual(256, f.ram) + self.assertEqual(10, f.disk) + self.assertEqual('N/A', f.ephemeral) + self.assertEqual('N/A', f.is_public) + + def test_find(self): + f = self.cs.flavors.find(ram=256) + self.cs.assert_called('GET', '/flavors/detail') + self.assertEqual('256 mb server', f.name) + + f = self.cs.flavors.find(disk=0) + self.assertEqual('128 mb server', f.name) + + self.assertRaises(exceptions.NotFound, self.cs.flavors.find, + disk=12345) + + def _create_body(self, name, ram, vcpus, disk, ephemeral, id, swap, + rxtx_factor, is_public): + return { + "flavor": { + "name": name, + "ram": ram, + "vcpus": vcpus, + "disk": disk, + "OS-FLV-EXT-DATA:ephemeral": ephemeral, + "id": id, + "swap": swap, + "rxtx_factor": rxtx_factor, + "os-flavor-access:is_public": is_public, + } + } + + def test_create(self): + f = self.cs.flavors.create("flavorcreate", 512, 1, 10, 1234, + ephemeral=10, is_public=False) + + body = self._create_body("flavorcreate", 512, 1, 10, 10, 1234, 0, 1.0, + False) + + self.cs.assert_called('POST', '/flavors', body) + self.assertIsInstance(f, self.flavor_type) + + def test_create_with_id_as_string(self): + flavor_id = 'foobar' + f = self.cs.flavors.create("flavorcreate", 512, + 1, 10, flavor_id, ephemeral=10, + is_public=False) + + body = self._create_body("flavorcreate", 512, 1, 10, 10, flavor_id, 0, + 1.0, False) + + self.cs.assert_called('POST', '/flavors', body) + self.assertIsInstance(f, self.flavor_type) + + def test_create_ephemeral_ispublic_defaults(self): + f = self.cs.flavors.create("flavorcreate", 512, 1, 10, 1234) + + body = self._create_body("flavorcreate", 512, 1, 10, 0, 1234, 0, + 1.0, True) + + self.cs.assert_called('POST', '/flavors', body) + self.assertIsInstance(f, self.flavor_type) + + def test_invalid_parameters_create(self): + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, + "flavorcreate", "invalid", 1, 10, 1234, swap=0, + ephemeral=0, rxtx_factor=1.0, is_public=True) + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, + "flavorcreate", 512, "invalid", 10, 1234, swap=0, + ephemeral=0, rxtx_factor=1.0, is_public=True) + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, + "flavorcreate", 512, 1, "invalid", 1234, swap=0, + ephemeral=0, rxtx_factor=1.0, is_public=True) + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, + "flavorcreate", 512, 1, 10, 1234, swap="invalid", + ephemeral=0, rxtx_factor=1.0, is_public=True) + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, + "flavorcreate", 512, 1, 10, 1234, swap=0, + ephemeral="invalid", rxtx_factor=1.0, is_public=True) + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, + "flavorcreate", 512, 1, 10, 1234, swap=0, + ephemeral=0, rxtx_factor="invalid", is_public=True) + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, + "flavorcreate", 512, 1, 10, 1234, swap=0, + ephemeral=0, rxtx_factor=1.0, is_public='invalid') + + def test_delete(self): + self.cs.flavors.delete("flavordelete") + self.cs.assert_called('DELETE', '/flavors/flavordelete') + + def test_delete_with_flavor_instance(self): + f = self.cs.flavors.get(2) + self.cs.flavors.delete(f) + self.cs.assert_called('DELETE', '/flavors/2') + + def test_delete_with_flavor_instance_method(self): + f = self.cs.flavors.get(2) + f.delete() + self.cs.assert_called('DELETE', '/flavors/2') + + def test_set_keys(self): + f = self.cs.flavors.get(1) + f.set_keys({'k1': 'v1'}) + self.cs.assert_called('POST', '/flavors/1/os-extra_specs', + {"extra_specs": {'k1': 'v1'}}) + + def test_set_with_valid_keys(self): + valid_keys = ['key4', 'month.price', 'I-Am:AK-ey.44-', + 'key with spaces and _'] + + f = self.cs.flavors.get(4) + for key in valid_keys: + f.set_keys({key: 'v4'}) + self.cs.assert_called('POST', '/flavors/4/os-extra_specs', + {"extra_specs": {key: 'v4'}}) + + def test_set_with_invalid_keys(self): + invalid_keys = ['/1', '?1', '%1', '<', '>'] + + f = self.cs.flavors.get(1) + for key in invalid_keys: + self.assertRaises(exceptions.CommandError, f.set_keys, {key: 'v1'}) + + @mock.patch.object(flavors.FlavorManager, '_delete') + def test_unset_keys(self, mock_delete): + f = self.cs.flavors.get(1) + keys = ['k1', 'k2'] + f.unset_keys(keys) + mock_delete.assert_has_calls([ + mock.call("/flavors/1/os-extra_specs/k1"), + mock.call("/flavors/1/os-extra_specs/k2") + ]) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_floating_ip_dns.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_floating_ip_dns.py new file mode 100644 index 0000000000..ec20e56c5f --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_floating_ip_dns.py @@ -0,0 +1,91 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import floatingips as data +from novaclient.tests.unit import utils +from novaclient.v2 import floating_ip_dns + + +class FloatingIPDNSDomainTest(utils.FixturedTestCase): + + testdomain = "testdomain" + client_fixture_class = client.V1 + data_fixture_class = data.DNSFixture + + def test_dns_domains(self): + domainlist = self.cs.dns_domains.domains() + self.assertEqual(2, len(domainlist)) + + for entry in domainlist: + self.assertIsInstance(entry, + floating_ip_dns.FloatingIPDNSDomain) + + self.assertEqual('example.com', domainlist[1].domain) + + def test_create_private_domain(self): + 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): + 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): + self.cs.dns_domains.delete(self.testdomain) + self.assert_called('DELETE', '/os-floating-ip-dns/%s' % + self.testdomain) + + +class FloatingIPDNSEntryTest(utils.FixturedTestCase): + + testname = "testname" + testip = "1.2.3.4" + testdomain = "testdomain" + testtype = "A" + client_fixture_class = client.V1 + data_fixture_class = data.DNSFixture + + def test_get_dns_entries_by_ip(self): + entries = self.cs.dns_entries.get_for_ip(self.testdomain, + ip=self.testip) + self.assertEqual(2, len(entries)) + + for entry in entries: + self.assertIsInstance(entry, + floating_ip_dns.FloatingIPDNSEntry) + + self.assertEqual('host2', entries[1].dns_entry['name']) + self.assertEqual(entries[1].dns_entry['ip'], self.testip) + + def test_get_dns_entry_by_name(self): + entry = self.cs.dns_entries.get(self.testdomain, + self.testname) + self.assertIsInstance(entry, floating_ip_dns.FloatingIPDNSEntry) + self.assertEqual(entry.name, self.testname) + + def test_create_entry(self): + self.cs.dns_entries.create(self.testdomain, + self.testname, + self.testip, + self.testtype) + + self.assert_called('PUT', '/os-floating-ip-dns/%s/entries/%s' % + (self.testdomain, self.testname)) + + def test_delete_entry(self): + self.cs.dns_entries.delete(self.testdomain, self.testname) + self.assert_called('DELETE', '/os-floating-ip-dns/%s/entries/%s' % + (self.testdomain, self.testname)) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_floating_ip_pools.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_floating_ip_pools.py new file mode 100644 index 0000000000..a138fc285c --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_floating_ip_pools.py @@ -0,0 +1,32 @@ +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import floatingips as data +from novaclient.tests.unit import utils +from novaclient.v2 import floating_ip_pools + + +class TestFloatingIPPools(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.PoolsFixture + + def test_list_floating_ips(self): + fl = self.cs.floating_ip_pools.list() + self.assert_called('GET', '/os-floating-ip-pools') + for f in fl: + self.assertIsInstance(f, floating_ip_pools.FloatingIPPool) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_floating_ips.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_floating_ips.py new file mode 100644 index 0000000000..2b091a516b --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_floating_ips.py @@ -0,0 +1,59 @@ +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import floatingips as data +from novaclient.tests.unit import utils +from novaclient.v2 import floating_ips + + +class FloatingIPsTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.FloatingFixture + + def test_list_floating_ips(self): + fips = self.cs.floating_ips.list() + self.assert_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 = self.cs.floating_ips.list()[0] + fl.delete() + 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 = 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 = self.cs.floating_ips.create('nova') + self.assert_called('POST', '/os-floating-ips') + self.assertEqual('nova', fl.pool) + self.assertIsInstance(fl, floating_ips.FloatingIP) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_floating_ips_bulk.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_floating_ips_bulk.py new file mode 100644 index 0000000000..69b553f24c --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_floating_ips_bulk.py @@ -0,0 +1,64 @@ +# Copyright 2012 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import floatingips as data +from novaclient.tests.unit import utils +from novaclient.v2 import floating_ips_bulk + + +class FloatingIPsBulkTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.BulkFixture + + def test_list_floating_ips_bulk(self): + fl = self.cs.floating_ips_bulk.list() + self.assert_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 = 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 = self.cs.floating_ips_bulk.create('192.168.1.0/30') + body = {'floating_ips_bulk_create': {'ip_range': '192.168.1.0/30'}} + self.assert_called('POST', '/os-floating-ips-bulk', body) + self.assertEqual(fl.ip_range, + body['floating_ips_bulk_create']['ip_range']) + + def test_create_floating_ips_bulk_with_pool_and_host(self): + fl = self.cs.floating_ips_bulk.create('192.168.1.0/30', 'poolTest', + 'interfaceTest') + body = {'floating_ips_bulk_create': { + 'ip_range': '192.168.1.0/30', 'pool': 'poolTest', + 'interface': 'interfaceTest'}} + self.assert_called('POST', '/os-floating-ips-bulk', body) + self.assertEqual(fl.ip_range, + body['floating_ips_bulk_create']['ip_range']) + self.assertEqual(fl.pool, + body['floating_ips_bulk_create']['pool']) + self.assertEqual(fl.interface, + body['floating_ips_bulk_create']['interface']) + + def test_delete_floating_ips_bulk(self): + fl = self.cs.floating_ips_bulk.delete('192.168.1.0/30') + body = {'ip_range': '192.168.1.0/30'} + self.assert_called('PUT', '/os-floating-ips-bulk/delete', body) + self.assertEqual(fl.floating_ips_bulk_delete, body['ip_range']) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_fping.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_fping.py new file mode 100644 index 0000000000..0dd7cd30d7 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_fping.py @@ -0,0 +1,62 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import fping as data +from novaclient.tests.unit import utils +from novaclient.v2 import fping + + +class FpingTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.Fixture + + def test_fping_repr(self): + r = self.cs.fping.get(1) + self.assertEqual("", repr(r)) + + def test_list_fpings(self): + fl = self.cs.fping.list() + self.assert_called('GET', '/os-fping') + for f in fl: + self.assertIsInstance(f, fping.Fping) + self.assertEqual("fake-project", f.project_id) + self.assertTrue(f.alive) + + def test_list_fpings_all_tenants(self): + fl = self.cs.fping.list(all_tenants=True) + for f in fl: + self.assertIsInstance(f, fping.Fping) + self.assert_called('GET', '/os-fping?all_tenants=1') + + def test_list_fpings_exclude(self): + fl = self.cs.fping.list(exclude=['1']) + for f in fl: + self.assertIsInstance(f, fping.Fping) + self.assert_called('GET', '/os-fping?exclude=1') + + def test_list_fpings_include(self): + fl = self.cs.fping.list(include=['1']) + for f in fl: + self.assertIsInstance(f, fping.Fping) + self.assert_called('GET', '/os-fping?include=1') + + def test_get_fping(self): + f = self.cs.fping.get(1) + self.assert_called('GET', '/os-fping/1') + self.assertIsInstance(f, fping.Fping) + self.assertEqual("fake-project", f.project_id) + self.assertTrue(f.alive) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_hosts.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_hosts.py new file mode 100644 index 0000000000..029964b6b5 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_hosts.py @@ -0,0 +1,84 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import hosts as data +from novaclient.tests.unit import utils +from novaclient.v2 import hosts + + +class HostsTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.V1 + + def test_describe_resource(self): + hs = self.cs.hosts.get('host') + self.assert_called('GET', '/os-hosts/host') + [self.assertIsInstance(h, hosts.Host) for h in hs] + + def test_list_host(self): + hs = self.cs.hosts.list() + self.assert_called('GET', '/os-hosts') + [self.assertIsInstance(h, hosts.Host) for h in hs] + [self.assertEqual(h.zone, 'nova1') for h in hs] + + def test_list_host_with_zone(self): + hs = self.cs.hosts.list('nova') + self.assert_called('GET', '/os-hosts?zone=nova') + [self.assertIsInstance(h, hosts.Host) for h in hs] + [self.assertEqual(h.zone, 'nova') for h in hs] + + def test_update_enable(self): + host = self.cs.hosts.get('sample_host')[0] + values = {"status": "enabled"} + result = host.update(values) + self.assert_called('PUT', '/os-hosts/sample_host', values) + self.assertIsInstance(result, hosts.Host) + + def test_update_maintenance(self): + host = self.cs.hosts.get('sample_host')[0] + values = {"maintenance_mode": "enable"} + result = host.update(values) + self.assert_called('PUT', '/os-hosts/sample_host', values) + self.assertIsInstance(result, hosts.Host) + + def test_update_both(self): + host = self.cs.hosts.get('sample_host')[0] + values = {"status": "enabled", + "maintenance_mode": "enable"} + result = host.update(values) + self.assert_called('PUT', '/os-hosts/sample_host', values) + self.assertIsInstance(result, hosts.Host) + + def test_host_startup(self): + host = self.cs.hosts.get('sample_host')[0] + host.startup() + self.assert_called( + 'GET', '/os-hosts/sample_host/startup') + + def test_host_reboot(self): + host = self.cs.hosts.get('sample_host')[0] + host.reboot() + self.assert_called( + 'GET', '/os-hosts/sample_host/reboot') + + def test_host_shutdown(self): + host = self.cs.hosts.get('sample_host')[0] + host.shutdown() + self.assert_called( + 'GET', '/os-hosts/sample_host/shutdown') + + def test_hosts_repr(self): + hs = self.cs.hosts.get('host') + self.assertEqual('', repr(hs[0])) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_hypervisors.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_hypervisors.py new file mode 100644 index 0000000000..b297c12470 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_hypervisors.py @@ -0,0 +1,178 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import hypervisors as data +from novaclient.tests.unit import utils + + +class HypervisorsTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.V1 + + def compare_to_expected(self, expected, hyper): + for key, value in expected.items(): + self.assertEqual(getattr(hyper, key), value) + + def test_hypervisor_index(self): + expected = [ + dict(id=1234, hypervisor_hostname='hyper1'), + dict(id=5678, hypervisor_hostname='hyper2')] + + result = self.cs.hypervisors.list(False) + self.assert_called('GET', '/os-hypervisors') + + for idx, hyper in enumerate(result): + self.compare_to_expected(expected[idx], hyper) + + def test_hypervisor_detail(self): + expected = [ + dict(id=1234, + service=dict(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), + dict(id=2, + service=dict(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)] + + result = self.cs.hypervisors.list() + self.assert_called('GET', '/os-hypervisors/detail') + + for idx, hyper in enumerate(result): + self.compare_to_expected(expected[idx], hyper) + + def test_hypervisor_search(self): + expected = [ + dict(id=1234, hypervisor_hostname='hyper1'), + dict(id=5678, hypervisor_hostname='hyper2')] + + result = self.cs.hypervisors.search('hyper') + self.assert_called('GET', '/os-hypervisors/hyper/search') + + for idx, hyper in enumerate(result): + self.compare_to_expected(expected[idx], hyper) + + def test_hypervisor_servers(self): + expected = [ + dict(id=1234, + hypervisor_hostname='hyper1', + servers=[ + dict(name='inst1', uuid='uuid1'), + dict(name='inst2', uuid='uuid2')]), + dict(id=5678, + hypervisor_hostname='hyper2', + servers=[ + dict(name='inst3', uuid='uuid3'), + dict(name='inst4', uuid='uuid4')]), + ] + + result = self.cs.hypervisors.search('hyper', True) + self.assert_called('GET', '/os-hypervisors/hyper/servers') + + for idx, hyper in enumerate(result): + self.compare_to_expected(expected[idx], hyper) + + def test_hypervisor_get(self): + expected = dict( + id=1234, + service=dict(id=1, host='compute1'), + 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) + + result = self.cs.hypervisors.get(1234) + self.assert_called('GET', '/os-hypervisors/1234') + + self.compare_to_expected(expected, result) + + def test_hypervisor_uptime(self): + expected = dict( + id=1234, + hypervisor_hostname="hyper1", + uptime="fake uptime") + + result = self.cs.hypervisors.uptime(1234) + self.assert_called('GET', '/os-hypervisors/1234/uptime') + + self.compare_to_expected(expected, result) + + def test_hypervisor_statistics(self): + expected = dict( + 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, + ) + + result = self.cs.hypervisors.statistics() + self.assert_called('GET', '/os-hypervisors/statistics') + + self.compare_to_expected(expected, result) + + def test_hypervisor_statistics_data_model(self): + result = self.cs.hypervisor_stats.statistics() + self.assert_called('GET', '/os-hypervisors/statistics') + + # Test for Bug #1370415, the line below used to raise AttributeError + self.assertEqual("", + result.__repr__()) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_images.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_images.py new file mode 100644 index 0000000000..0cd110fb53 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_images.py @@ -0,0 +1,67 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import images as data +from novaclient.tests.unit import utils +from novaclient.v2 import images + + +class ImagesTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.V1 + + def test_list_images(self): + il = self.cs.images.list() + self.assert_called('GET', '/images/detail') + [self.assertIsInstance(i, images.Image) for i in il] + self.assertEqual(2, len(il)) + + def test_list_images_undetailed(self): + il = self.cs.images.list(detailed=False) + self.assert_called('GET', '/images') + [self.assertIsInstance(i, images.Image) for i in il] + + def test_list_images_with_limit(self): + self.cs.images.list(limit=4) + self.assert_called('GET', '/images/detail?limit=4') + + def test_get_image_details(self): + i = self.cs.images.get(1) + self.assert_called('GET', '/images/1') + self.assertIsInstance(i, images.Image) + self.assertEqual(1, i.id) + self.assertEqual('CentOS 5.2', i.name) + + def test_delete_image(self): + self.cs.images.delete(1) + self.assert_called('DELETE', '/images/1') + + def test_delete_meta(self): + self.cs.images.delete_meta(1, {'test_key': 'test_value'}) + self.assert_called('DELETE', '/images/1/metadata/test_key') + + def test_set_meta(self): + self.cs.images.set_meta(1, {'test_key': 'test_value'}) + self.assert_called('POST', '/images/1/metadata', + {"metadata": {'test_key': 'test_value'}}) + + def test_find(self): + i = self.cs.images.find(name="CentOS 5.2") + self.assertEqual(1, i.id) + self.assert_called('GET', '/images/1') + + iml = self.cs.images.findall(status='SAVING') + self.assertEqual(1, len(iml)) + self.assertEqual('My Server Backup', iml[0].name) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_keypairs.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_keypairs.py new file mode 100644 index 0000000000..91c249f824 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_keypairs.py @@ -0,0 +1,64 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import keypairs as data +from novaclient.tests.unit import utils +from novaclient.v2 import keypairs + + +class KeypairsTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.V1 + + def setUp(self): + super(KeypairsTest, self).setUp() + self.keypair_type = self._get_keypair_type() + 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.assert_called('GET', '/%s/test' % self.keypair_prefix) + self.assertIsInstance(kp, keypairs.Keypair) + self.assertEqual('test', kp.name) + + def test_list_keypairs(self): + kps = self.cs.keypairs.list() + self.assert_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.assert_called('DELETE', '/%s/test' % self.keypair_prefix) + self.cs.keypairs.delete('test') + self.assert_called('DELETE', '/%s/test' % self.keypair_prefix) + self.cs.keypairs.delete(kp) + self.assert_called('DELETE', '/%s/test' % self.keypair_prefix) + + def test_create_keypair(self): + kp = self.cs.keypairs.create("foo") + 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.assert_called('POST', '/%s' % self.keypair_prefix) + self.assertIsInstance(kp, keypairs.Keypair) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_limits.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_limits.py new file mode 100644 index 0000000000..9dc9dec436 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_limits.py @@ -0,0 +1,88 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import limits as data +from novaclient.tests.unit import utils +from novaclient.v2 import limits + + +class LimitsTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.Fixture + + def test_get_limits(self): + obj = self.cs.limits.get() + self.assert_called('GET', '/limits') + self.assertIsInstance(obj, limits.Limits) + + def test_get_limits_for_a_tenant(self): + obj = self.cs.limits.get(tenant_id=1234) + self.assert_called('GET', '/limits?tenant_id=1234') + self.assertIsInstance(obj, limits.Limits) + + def test_absolute_limits(self): + obj = self.cs.limits.get() + + expected = ( + limits.AbsoluteLimit("maxTotalRAMSize", 51200), + limits.AbsoluteLimit("maxServerMeta", 5), + limits.AbsoluteLimit("maxImageMeta", 5), + limits.AbsoluteLimit("maxPersonality", 5), + limits.AbsoluteLimit("maxPersonalitySize", 10240), + ) + + abs_limits = list(obj.absolute) + self.assertEqual(len(abs_limits), len(expected)) + + for limit in abs_limits: + self.assertIn(limit, expected) + + def test_absolute_limits_reserved(self): + obj = self.cs.limits.get(reserved=True) + + expected = ( + limits.AbsoluteLimit("maxTotalRAMSize", 51200), + limits.AbsoluteLimit("maxServerMeta", 5), + limits.AbsoluteLimit("maxImageMeta", 5), + limits.AbsoluteLimit("maxPersonality", 5), + limits.AbsoluteLimit("maxPersonalitySize", 10240), + ) + + self.assert_called('GET', '/limits?reserved=1') + abs_limits = list(obj.absolute) + self.assertEqual(len(abs_limits), len(expected)) + + for limit in abs_limits: + self.assertIn(limit, expected) + + def test_rate_limits(self): + obj = self.cs.limits.get() + + expected = ( + limits.RateLimit('POST', '*', '.*', 10, 2, 'MINUTE', + '2011-12-15T22:42:45Z'), + limits.RateLimit('PUT', '*', '.*', 10, 2, 'MINUTE', + '2011-12-15T22:42:45Z'), + limits.RateLimit('DELETE', '*', '.*', 100, 100, 'MINUTE', + '2011-12-15T22:42:45Z'), + limits.RateLimit('POST', '*/servers', '^/servers', 25, 24, 'DAY', + '2011-12-15T22:42:45Z'), + ) + + rate_limits = list(obj.rate) + self.assertEqual(len(rate_limits), len(expected)) + + for limit in rate_limits: + self.assertIn(limit, expected) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_networks.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_networks.py new file mode 100644 index 0000000000..a45ba80dc9 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_networks.py @@ -0,0 +1,106 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import networks as data +from novaclient.tests.unit import utils +from novaclient.v2 import networks + + +class NetworksTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.Fixture + + def test_list_networks(self): + fl = self.cs.networks.list() + self.assert_called('GET', '/os-networks') + [self.assertIsInstance(f, networks.Network) for f in fl] + + def test_get_network(self): + f = self.cs.networks.get(1) + self.assert_called('GET', '/os-networks/1') + self.assertIsInstance(f, networks.Network) + + def test_delete(self): + self.cs.networks.delete('networkdelete') + self.assert_called('DELETE', '/os-networks/networkdelete') + + def test_create(self): + 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): + params = { + 'label': 'bar', + 'bridge': 'br0', + 'bridge_interface': 'int0', + 'cidr': '192.0.2.0/24', + 'cidr_v6': '2001:DB8::/32', + 'dns1': '1.1.1.1', + 'dns2': '1.1.1.2', + 'fixed_cidr': '198.51.100.0/24', + 'gateway': '192.0.2.1', + 'gateway_v6': '2001:DB8::1', + 'multi_host': 'T', + 'priority': '1', + 'project_id': '1', + 'vlan': 5, + 'vlan_start': 1, + 'vpn_start': 1, + 'mtu': 1500, + 'enable_dhcp': 'T', + 'dhcp_server': '1920.2.2', + 'share_address': 'T', + 'allowed_start': '192.0.2.10', + 'allowed_end': '192.0.2.20', + } + + f = self.cs.networks.create(**params) + self.assert_called('POST', '/os-networks', {'network': params}) + self.assertIsInstance(f, networks.Network) + + def test_associate_project(self): + self.cs.networks.associate_project('networktest') + self.assert_called('POST', '/os-networks/add', + {'id': 'networktest'}) + + def test_associate_host(self): + self.cs.networks.associate_host('networktest', 'testHost') + self.assert_called('POST', '/os-networks/networktest/action', + {'associate_host': 'testHost'}) + + def test_disassociate(self): + self.cs.networks.disassociate('networkdisassociate') + self.assert_called('POST', + '/os-networks/networkdisassociate/action', + {'disassociate': None}) + + def test_disassociate_host_only(self): + self.cs.networks.disassociate('networkdisassociate', True, False) + self.assert_called('POST', + '/os-networks/networkdisassociate/action', + {'disassociate_host': None}) + + def test_disassociate_project(self): + self.cs.networks.disassociate('networkdisassociate', False, True) + self.assert_called('POST', + '/os-networks/networkdisassociate/action', + {'disassociate_project': None}) + + def test_add(self): + self.cs.networks.add('networkadd') + self.assert_called('POST', '/os-networks/add', + {'id': 'networkadd'}) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_quota_classes.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_quota_classes.py new file mode 100644 index 0000000000..467ff830d8 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_quota_classes.py @@ -0,0 +1,42 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes + + +cs = fakes.FakeClient() + + +class QuotaClassSetsTest(utils.TestCase): + + def test_class_quotas_get(self): + class_name = 'test' + cs.quota_classes.get(class_name) + cs.assert_called('GET', '/os-quota-class-sets/%s' % class_name) + + def test_update_quota(self): + q = cs.quota_classes.get('test') + q.update(cores=2) + cs.assert_called('PUT', '/os-quota-class-sets/test') + + def test_refresh_quota(self): + q = cs.quota_classes.get('test') + q2 = cs.quota_classes.get('test') + self.assertEqual(q.cores, q2.cores) + q2.cores = 0 + self.assertNotEqual(q.cores, q2.cores) + q2.get() + self.assertEqual(q.cores, q2.cores) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_quotas.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_quotas.py new file mode 100644 index 0000000000..43f8ce1d8f --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_quotas.py @@ -0,0 +1,62 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import quotas as data +from novaclient.tests.unit import utils + + +class QuotaSetsTest(utils.FixturedTestCase): + + 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.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.assert_called('GET', url) + + def test_tenant_quotas_defaults(self): + tenant_id = '97f4c221bff44578b0300df4ef119353' + self.cs.quotas.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.assert_called( + 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', + {'quota_set': {'force': True, + 'cores': 2, + 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) + + def test_quotas_delete(self): + tenant_id = 'test' + self.cs.quotas.delete(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.assert_called('DELETE', url) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_security_group_rules.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_security_group_rules.py new file mode 100644 index 0000000000..8375198212 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_security_group_rules.py @@ -0,0 +1,89 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import exceptions +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import security_group_rules as data +from novaclient.tests.unit import utils +from novaclient.v2 import security_group_rules + + +class SecurityGroupRulesTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.Fixture + + def test_delete_security_group_rule(self): + self.cs.security_group_rules.delete(1) + self.assert_called('DELETE', '/os-security-group-rules/1') + + def test_create_security_group_rule(self): + sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, + "10.0.0.0/16") + + body = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": 1, + "to_port": 65535, + "cidr": "10.0.0.0/16", + "group_id": None, + "parent_group_id": 1, + } + } + + self.assert_called('POST', '/os-security-group-rules', body) + self.assertTrue(isinstance(sg, security_group_rules.SecurityGroupRule)) + + def test_create_security_group_group_rule(self): + sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, + "10.0.0.0/16", 101) + + body = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": 1, + "to_port": 65535, + "cidr": "10.0.0.0/16", + "group_id": 101, + "parent_group_id": 1, + } + } + + self.assert_called('POST', '/os-security-group-rules', body) + self.assertTrue(isinstance(sg, security_group_rules.SecurityGroupRule)) + + def test_invalid_parameters_create(self): + self.assertRaises(exceptions.CommandError, + self.cs.security_group_rules.create, + 1, "invalid_ip_protocol", 1, 65535, + "10.0.0.0/16", 101) + self.assertRaises(exceptions.CommandError, + self.cs.security_group_rules.create, + 1, "tcp", "invalid_from_port", 65535, + "10.0.0.0/16", 101) + self.assertRaises(exceptions.CommandError, + self.cs.security_group_rules.create, + 1, "tcp", 1, "invalid_to_port", + "10.0.0.0/16", 101) + + def test_security_group_rule_str(self): + sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, + "10.0.0.0/16") + self.assertEqual('1', str(sg)) + + def test_security_group_rule_del(self): + sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, + "10.0.0.0/16") + sg.delete() + self.assert_called('DELETE', '/os-security-group-rules/1') diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_security_groups.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_security_groups.py new file mode 100644 index 0000000000..d05a93aa1f --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_security_groups.py @@ -0,0 +1,76 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import security_groups as data +from novaclient.tests.unit import utils +from novaclient.v2 import security_groups + + +class SecurityGroupsTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.Fixture + + def _do_test_list_security_groups(self, search_opts, path): + sgs = self.cs.security_groups.list(search_opts=search_opts) + self.assert_called('GET', path) + for sg in sgs: + self.assertIsInstance(sg, security_groups.SecurityGroup) + + def test_list_security_groups_all_tenants_on(self): + self._do_test_list_security_groups( + None, '/os-security-groups') + + def test_list_security_groups_all_tenants_on_with_search_opts(self): + self._do_test_list_security_groups( + {'all_tenants': 1}, '/os-security-groups?all_tenants=1') + + def test_list_security_groups_all_tenants_off(self): + self._do_test_list_security_groups( + {'all_tenants': 0}, '/os-security-groups') + + def test_get_security_groups(self): + sg = self.cs.security_groups.get(1) + self.assert_called('GET', '/os-security-groups/1') + self.assertIsInstance(sg, security_groups.SecurityGroup) + self.assertEqual('1', str(sg)) + + def test_delete_security_group(self): + sg = self.cs.security_groups.list()[0] + sg.delete() + 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 = 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 = 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 = self.cs.security_groups.get(1) + sg2 = self.cs.security_groups.get(1) + self.assertEqual(sg.name, sg2.name) + sg2.name = "should be test" + self.assertNotEqual(sg.name, sg2.name) + sg2.get() + self.assertEqual(sg.name, sg2.name) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_server_groups.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_server_groups.py new file mode 100644 index 0000000000..fcda2dec7e --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_server_groups.py @@ -0,0 +1,59 @@ +# 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.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import server_groups as data +from novaclient.tests.unit import utils +from novaclient.v2 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) + + def test_delete_server_group_object(self): + id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' + server_group = self.cs.server_groups.get(id) + server_group.delete() + self.assert_called('DELETE', '/os-server-groups/%s' % id) diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_servers.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_servers.py new file mode 100644 index 0000000000..c43aed422b --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_servers.py @@ -0,0 +1,704 @@ +# -*- coding: utf-8 -*- +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from oslo.serialization import jsonutils +import six + +from novaclient import exceptions +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import floatingips +from novaclient.tests.unit.fixture_data import servers as data +from novaclient.tests.unit import utils +from novaclient.v2 import servers + + +class ServersTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.V1 + + def setUp(self): + super(ServersTest, self).setUp() + self.useFixture(floatingips.FloatingFixture(self.requests)) + + def test_list_servers(self): + sl = self.cs.servers.list() + self.assert_called('GET', '/servers/detail') + [self.assertIsInstance(s, servers.Server) for s in sl] + + def test_list_servers_undetailed(self): + sl = self.cs.servers.list(detailed=False) + self.assert_called('GET', '/servers') + [self.assertIsInstance(s, servers.Server) for s in sl] + + def test_list_servers_with_marker_limit(self): + sl = self.cs.servers.list(marker=1234, limit=2) + self.assert_called('GET', '/servers/detail?limit=2&marker=1234') + for s in sl: + self.assertIsInstance(s, servers.Server) + + def test_list_servers_sort_single(self): + sl = self.cs.servers.list(sort_keys=['display_name'], + sort_dirs=['asc']) + self.assert_called( + 'GET', + '/servers/detail?sort_dir=asc&sort_key=display_name') + for s in sl: + self.assertIsInstance(s, servers.Server) + + def test_list_servers_sort_multiple(self): + sl = self.cs.servers.list(sort_keys=['display_name', 'id'], + sort_dirs=['asc', 'desc']) + self.assert_called( + 'GET', + ('/servers/detail?sort_dir=asc&sort_dir=desc&' + 'sort_key=display_name&sort_key=id')) + for s in sl: + self.assertIsInstance(s, servers.Server) + + def test_get_server_details(self): + s = self.cs.servers.get(1234) + self.assert_called('GET', '/servers/1234') + self.assertIsInstance(s, servers.Server) + self.assertEqual(1234, s.id) + self.assertEqual('BUILD', s.status) + + def test_get_server_promote_details(self): + s1 = self.cs.servers.list(detailed=False)[0] + s2 = self.cs.servers.list(detailed=True)[0] + self.assertNotEqual(s1._info, s2._info) + s1.get() + self.assertEqual(s1._info, s2._info) + + def test_create_server(self): + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + files={ + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + } + ) + self.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + + def test_create_server_boot_from_volume_with_nics(self): + old_boot = self.cs.servers._boot + + nics = [{'net-id': '11111111-1111-1111-1111-111111111111', + 'v4-fixed-ip': '10.0.0.7'}] + bdm = {"volume_size": "1", + "volume_id": "11111111-1111-1111-1111-111111111111", + "delete_on_termination": "0", + "device_name": "vda"} + + def wrapped_boot(url, key, *boot_args, **boot_kwargs): + self.assertEqual(boot_kwargs['block_device_mapping'], bdm) + self.assertEqual(boot_kwargs['nics'], nics) + return old_boot(url, key, *boot_args, **boot_kwargs) + + @mock.patch.object(self.cs.servers, '_boot', wrapped_boot) + def test_create_server_from_volume(): + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + block_device_mapping=bdm, + nics=nics + ) + self.assert_called('POST', '/os-volumes_boot') + self.assertIsInstance(s, servers.Server) + + test_create_server_from_volume() + + def test_create_server_boot_with_nics_ipv6(self): + old_boot = self.cs.servers._boot + nics = [{'net-id': '11111111-1111-1111-1111-111111111111', + 'v6-fixed-ip': '2001:db9:0:1::10'}] + + def wrapped_boot(url, key, *boot_args, **boot_kwargs): + self.assertEqual(boot_kwargs['nics'], nics) + return old_boot(url, key, *boot_args, **boot_kwargs) + + with mock.patch.object(self.cs.servers, '_boot', wrapped_boot): + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + nics=nics + ) + self.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + + def test_create_server_userdata_file_object(self): + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata=six.StringIO('hello moto'), + files={ + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + }, + ) + self.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + + def test_create_server_userdata_unicode(self): + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata=six.u('こんにちは'), + key_name="fakekey", + files={ + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + }, + ) + self.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + + def test_create_server_userdata_utf8(self): + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata='こんにちは', + key_name="fakekey", + files={ + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + }, + ) + self.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + + def _create_disk_config(self, disk_config): + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + disk_config=disk_config + ) + self.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + + # verify disk config param was used in the request: + body = jsonutils.loads(self.requests.last_request.body) + server = body['server'] + self.assertIn('OS-DCF:diskConfig', server) + self.assertEqual(disk_config, server['OS-DCF:diskConfig']) + + def test_create_server_disk_config_auto(self): + self._create_disk_config('AUTO') + + def test_create_server_disk_config_manual(self): + self._create_disk_config('MANUAL') + + def test_update_server(self): + s = self.cs.servers.get(1234) + + # Update via instance + s.update(name='hi') + self.assert_called('PUT', '/servers/1234') + s.update(name='hi') + self.assert_called('PUT', '/servers/1234') + + # Silly, but not an error + s.update() + + # Update via manager + self.cs.servers.update(s, name='hi') + self.assert_called('PUT', '/servers/1234') + + def test_delete_server(self): + s = self.cs.servers.get(1234) + s.delete() + self.assert_called('DELETE', '/servers/1234') + self.cs.servers.delete(1234) + self.assert_called('DELETE', '/servers/1234') + self.cs.servers.delete(s) + self.assert_called('DELETE', '/servers/1234') + + def test_delete_server_meta(self): + self.cs.servers.delete_meta(1234, ['test_key']) + self.assert_called('DELETE', '/servers/1234/metadata/test_key') + + def test_set_server_meta(self): + self.cs.servers.set_meta(1234, {'test_key': 'test_value'}) + self.assert_called('POST', '/servers/1234/metadata', + {'metadata': {'test_key': 'test_value'}}) + + def test_set_server_meta_item(self): + self.cs.servers.set_meta_item(1234, 'test_key', 'test_value') + self.assert_called('PUT', '/servers/1234/metadata/test_key', + {'meta': {'test_key': 'test_value'}}) + + def test_find(self): + server = self.cs.servers.find(name='sample-server') + self.assert_called('GET', '/servers/1234') + self.assertEqual('sample-server', server.name) + + self.assertRaises(exceptions.NoUniqueMatch, self.cs.servers.find, + flavor={"id": 1, "name": "256 MB Server"}) + + sl = self.cs.servers.findall(flavor={"id": 1, "name": "256 MB Server"}) + self.assertEqual([1234, 5678, 9012], [s.id for s in sl]) + + def test_reboot_server(self): + s = self.cs.servers.get(1234) + s.reboot() + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.reboot(s, reboot_type='HARD') + self.assert_called('POST', '/servers/1234/action') + + def test_rebuild_server(self): + s = self.cs.servers.get(1234) + s.rebuild(image=1) + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.rebuild(s, image=1) + self.assert_called('POST', '/servers/1234/action') + s.rebuild(image=1, password='5678') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.rebuild(s, image=1, password='5678') + self.assert_called('POST', '/servers/1234/action') + + def _rebuild_resize_disk_config(self, disk_config, operation="rebuild"): + s = self.cs.servers.get(1234) + + if operation == "rebuild": + s.rebuild(image=1, disk_config=disk_config) + elif operation == "resize": + s.resize(flavor=1, disk_config=disk_config) + self.assert_called('POST', '/servers/1234/action') + + # verify disk config param was used in the request: + body = jsonutils.loads(self.requests.last_request.body) + + d = body[operation] + self.assertIn('OS-DCF:diskConfig', d) + self.assertEqual(disk_config, d['OS-DCF:diskConfig']) + + def test_rebuild_server_disk_config_auto(self): + self._rebuild_resize_disk_config('AUTO') + + def test_rebuild_server_disk_config_manual(self): + self._rebuild_resize_disk_config('MANUAL') + + def test_rebuild_server_preserve_ephemeral(self): + s = self.cs.servers.get(1234) + s.rebuild(image=1, preserve_ephemeral=True) + self.assert_called('POST', '/servers/1234/action') + body = jsonutils.loads(self.requests.last_request.body) + d = body['rebuild'] + self.assertIn('preserve_ephemeral', d) + self.assertTrue(d['preserve_ephemeral']) + + def test_rebuild_server_name_meta_files(self): + files = {'/etc/passwd': 'some data'} + s = self.cs.servers.get(1234) + s.rebuild(image=1, name='new', meta={'foo': 'bar'}, files=files) + body = jsonutils.loads(self.requests.last_request.body) + d = body['rebuild'] + self.assertEqual('new', d['name']) + self.assertEqual({'foo': 'bar'}, d['metadata']) + self.assertEqual('/etc/passwd', + d['personality'][0]['path']) + + def test_resize_server(self): + s = self.cs.servers.get(1234) + s.resize(flavor=1) + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.resize(s, flavor=1) + self.assert_called('POST', '/servers/1234/action') + + def test_resize_server_disk_config_auto(self): + self._rebuild_resize_disk_config('AUTO', 'resize') + + def test_resize_server_disk_config_manual(self): + self._rebuild_resize_disk_config('MANUAL', 'resize') + + def test_confirm_resized_server(self): + s = self.cs.servers.get(1234) + s.confirm_resize() + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.confirm_resize(s) + self.assert_called('POST', '/servers/1234/action') + + def test_revert_resized_server(self): + s = self.cs.servers.get(1234) + s.revert_resize() + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.revert_resize(s) + self.assert_called('POST', '/servers/1234/action') + + def test_migrate_server(self): + s = self.cs.servers.get(1234) + s.migrate() + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.migrate(s) + self.assert_called('POST', '/servers/1234/action') + + def test_add_fixed_ip(self): + s = self.cs.servers.get(1234) + s.add_fixed_ip(1) + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.add_fixed_ip(s, 1) + self.assert_called('POST', '/servers/1234/action') + + def test_remove_fixed_ip(self): + s = self.cs.servers.get(1234) + s.remove_fixed_ip('10.0.0.1') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.remove_fixed_ip(s, '10.0.0.1') + self.assert_called('POST', '/servers/1234/action') + + def test_add_floating_ip(self): + s = self.cs.servers.get(1234) + s.add_floating_ip('11.0.0.1') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.add_floating_ip(s, '11.0.0.1') + self.assert_called('POST', '/servers/1234/action') + f = self.cs.floating_ips.list()[0] + self.cs.servers.add_floating_ip(s, f) + self.assert_called('POST', '/servers/1234/action') + s.add_floating_ip(f) + self.assert_called('POST', '/servers/1234/action') + + def test_add_floating_ip_to_fixed(self): + s = self.cs.servers.get(1234) + s.add_floating_ip('11.0.0.1', fixed_address='12.0.0.1') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.add_floating_ip(s, '11.0.0.1', + fixed_address='12.0.0.1') + self.assert_called('POST', '/servers/1234/action') + f = self.cs.floating_ips.list()[0] + self.cs.servers.add_floating_ip(s, f) + self.assert_called('POST', '/servers/1234/action') + s.add_floating_ip(f) + self.assert_called('POST', '/servers/1234/action') + + def test_remove_floating_ip(self): + s = self.cs.servers.get(1234) + s.remove_floating_ip('11.0.0.1') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.remove_floating_ip(s, '11.0.0.1') + self.assert_called('POST', '/servers/1234/action') + f = self.cs.floating_ips.list()[0] + self.cs.servers.remove_floating_ip(s, f) + self.assert_called('POST', '/servers/1234/action') + s.remove_floating_ip(f) + self.assert_called('POST', '/servers/1234/action') + + def test_stop(self): + s = self.cs.servers.get(1234) + s.stop() + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.stop(s) + self.assert_called('POST', '/servers/1234/action') + + def test_force_delete(self): + s = self.cs.servers.get(1234) + s.force_delete() + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.force_delete(s) + self.assert_called('POST', '/servers/1234/action') + + def test_restore(self): + s = self.cs.servers.get(1234) + s.restore() + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.restore(s) + self.assert_called('POST', '/servers/1234/action') + + def test_start(self): + s = self.cs.servers.get(1234) + s.start() + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.start(s) + self.assert_called('POST', '/servers/1234/action') + + def test_rescue(self): + s = self.cs.servers.get(1234) + s.rescue() + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.rescue(s) + self.assert_called('POST', '/servers/1234/action') + + def test_rescue_password(self): + s = self.cs.servers.get(1234) + s.rescue(password='asdf') + self.assert_called('POST', '/servers/1234/action', + {'rescue': {'adminPass': 'asdf'}}) + self.cs.servers.rescue(s, password='asdf') + self.assert_called('POST', '/servers/1234/action', + {'rescue': {'adminPass': 'asdf'}}) + + def test_rescue_image(self): + s = self.cs.servers.get(1234) + s.rescue(image=1) + self.assert_called('POST', '/servers/1234/action', + {'rescue': {'rescue_image_ref': 1}}) + self.cs.servers.rescue(s, image=1) + self.assert_called('POST', '/servers/1234/action', + {'rescue': {'rescue_image_ref': 1}}) + + def test_unrescue(self): + s = self.cs.servers.get(1234) + s.unrescue() + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.unrescue(s) + self.assert_called('POST', '/servers/1234/action') + + def test_lock(self): + s = self.cs.servers.get(1234) + s.lock() + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.lock(s) + self.assert_called('POST', '/servers/1234/action') + + def test_unlock(self): + s = self.cs.servers.get(1234) + s.unlock() + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.unlock(s) + self.assert_called('POST', '/servers/1234/action') + + def test_backup(self): + s = self.cs.servers.get(1234) + s.backup('back1', 'daily', 1) + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.backup(s, 'back1', 'daily', 2) + self.assert_called('POST', '/servers/1234/action') + + def test_get_console_output_without_length(self): + success = 'foo' + s = self.cs.servers.get(1234) + s.get_console_output() + self.assertEqual(success, s.get_console_output()) + self.assert_called('POST', '/servers/1234/action') + + self.cs.servers.get_console_output(s) + self.assertEqual(success, self.cs.servers.get_console_output(s)) + self.assert_called('POST', '/servers/1234/action') + + def test_get_console_output_with_length(self): + success = 'foo' + + s = self.cs.servers.get(1234) + s.get_console_output(length=50) + self.assertEqual(success, s.get_console_output(length=50)) + self.assert_called('POST', '/servers/1234/action') + + self.cs.servers.get_console_output(s, length=50) + self.assertEqual(success, + self.cs.servers.get_console_output(s, length=50)) + self.assert_called('POST', '/servers/1234/action') + + # Testing password methods with the following password and key + # + # Clear password: FooBar123 + # + # RSA Private Key: novaclient/tests/unit/idfake.pem + # + # Encrypted password + # OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r + # qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho + # QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw + # /y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N + # tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk + # Hi/fmZZNQQqj1Ijq0caOIw== + + def test_get_password(self): + s = self.cs.servers.get(1234) + self.assertEqual(b'FooBar123', + s.get_password('novaclient/tests/unit/idfake.pem')) + self.assert_called('GET', '/servers/1234/os-server-password') + + def test_get_password_without_key(self): + s = self.cs.servers.get(1234) + self.assertEqual( + 'OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r' + 'qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho' + 'QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw' + '/y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N' + 'tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk' + 'Hi/fmZZNQQqj1Ijq0caOIw==', s.get_password()) + self.assert_called('GET', '/servers/1234/os-server-password') + + def test_clear_password(self): + s = self.cs.servers.get(1234) + s.clear_password() + self.assert_called('DELETE', '/servers/1234/os-server-password') + + def test_get_server_diagnostics(self): + s = self.cs.servers.get(1234) + diagnostics = s.diagnostics() + self.assertIsNotNone(diagnostics) + self.assert_called('GET', '/servers/1234/diagnostics') + + diagnostics_from_manager = self.cs.servers.diagnostics(1234) + self.assertIsNotNone(diagnostics_from_manager) + self.assert_called('GET', '/servers/1234/diagnostics') + + self.assertEqual(diagnostics[1], diagnostics_from_manager[1]) + + def test_get_vnc_console(self): + s = self.cs.servers.get(1234) + s.get_vnc_console('fake') + self.assert_called('POST', '/servers/1234/action') + + self.cs.servers.get_vnc_console(s, 'fake') + self.assert_called('POST', '/servers/1234/action') + + def test_get_spice_console(self): + s = self.cs.servers.get(1234) + s.get_spice_console('fake') + self.assert_called('POST', '/servers/1234/action') + + self.cs.servers.get_spice_console(s, 'fake') + self.assert_called('POST', '/servers/1234/action') + + def test_get_serial_console(self): + s = self.cs.servers.get(1234) + s.get_serial_console('fake') + self.assert_called('POST', '/servers/1234/action') + + self.cs.servers.get_serial_console(s, 'fake') + self.assert_called('POST', '/servers/1234/action') + + def test_get_rdp_console(self): + s = self.cs.servers.get(1234) + s.get_rdp_console('fake') + self.assert_called('POST', '/servers/1234/action') + + self.cs.servers.get_rdp_console(s, 'fake') + self.assert_called('POST', '/servers/1234/action') + + def test_create_image(self): + s = self.cs.servers.get(1234) + s.create_image('123') + self.assert_called('POST', '/servers/1234/action') + s.create_image('123', {}) + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.create_image(s, '123') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.create_image(s, '123', {}) + + def test_live_migrate_server(self): + s = self.cs.servers.get(1234) + s.live_migrate(host='hostname', block_migration=False, + disk_over_commit=False) + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.live_migrate(s, host='hostname', block_migration=False, + disk_over_commit=False) + self.assert_called('POST', '/servers/1234/action') + + def test_reset_state(self): + s = self.cs.servers.get(1234) + s.reset_state('newstate') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.reset_state(s, 'newstate') + self.assert_called('POST', '/servers/1234/action') + + def test_reset_network(self): + s = self.cs.servers.get(1234) + s.reset_network() + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.reset_network(s) + self.assert_called('POST', '/servers/1234/action') + + def test_add_security_group(self): + s = self.cs.servers.get(1234) + s.add_security_group('newsg') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.add_security_group(s, 'newsg') + self.assert_called('POST', '/servers/1234/action') + + def test_remove_security_group(self): + s = self.cs.servers.get(1234) + s.remove_security_group('oldsg') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.remove_security_group(s, 'oldsg') + self.assert_called('POST', '/servers/1234/action') + + def test_list_security_group(self): + s = self.cs.servers.get(1234) + s.list_security_group() + self.assert_called('GET', '/servers/1234/os-security-groups') + + def test_evacuate(self): + s = self.cs.servers.get(1234) + s.evacuate('fake_target_host', 'True') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.evacuate(s, 'fake_target_host', + 'False', 'NewAdminPassword') + self.assert_called('POST', '/servers/1234/action') + + def test_interface_list(self): + s = self.cs.servers.get(1234) + s.interface_list() + self.assert_called('GET', '/servers/1234/os-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 = self.cs.servers.get(1234) + s.interface_attach(None, None, None) + self.assert_called('POST', '/servers/1234/os-interface') + + def test_interface_detach(self): + s = self.cs.servers.get(1234) + s.interface_detach('port-id') + self.assert_called('DELETE', '/servers/1234/os-interface/port-id') diff --git a/awx/lib/site-packages/novaclient/tests/unit/v2/test_services.py b/awx/lib/site-packages/novaclient/tests/unit/v2/test_services.py new file mode 100644 index 0000000000..2724342d40 --- /dev/null +++ b/awx/lib/site-packages/novaclient/tests/unit/v2/test_services.py @@ -0,0 +1,99 @@ +# Copyright 2012 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import services + + +class ServicesTest(utils.TestCase): + def setUp(self): + super(ServicesTest, self).setUp() + self.cs = self._get_fake_client() + self.service_type = self._get_service_type() + + def _get_fake_client(self): + return fakes.FakeClient() + + def _get_service_type(self): + return services.Service + + def test_list_services(self): + svs = self.cs.services.list() + self.cs.assert_called('GET', '/os-services') + for s in svs: + self.assertIsInstance(s, self._get_service_type()) + self.assertEqual('nova-compute', s.binary) + self.assertEqual('host1', s.host) + self.assertTrue(str(s).startswith('" % self.agent + + def _add_details(self, info): + dico = 'resource' in info and info['resource'] or info + for (k, v) in dico.items(): + setattr(self, k, v) + + +class AgentsManager(base.ManagerWithFind): + resource_class = Agent + + def list(self, hypervisor=None): + """List all agent builds.""" + url = "/os-agents" + if hypervisor: + url = "/os-agents?hypervisor=%s" % hypervisor + return self._list(url, "agents") + + def _build_update_body(self, version, url, md5hash): + return {'para': {'version': version, + 'url': url, + 'md5hash': md5hash}} + + def update(self, id, version, + url, md5hash): + """Update an existing agent build.""" + body = self._build_update_body(version, url, md5hash) + return self._update('/os-agents/%s' % id, body, 'agent') + + def create(self, os, architecture, version, + url, md5hash, hypervisor): + """Create a new agent build.""" + body = {'agent': {'hypervisor': hypervisor, + 'os': os, + 'architecture': architecture, + 'version': version, + 'url': url, + 'md5hash': md5hash}} + return self._create('/os-agents', body, 'agent') + + def delete(self, id): + """Deletes an existing agent build.""" + self._delete('/os-agents/%s' % id) diff --git a/awx/lib/site-packages/novaclient/v2/aggregates.py b/awx/lib/site-packages/novaclient/v2/aggregates.py new file mode 100644 index 0000000000..c2df31526d --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/aggregates.py @@ -0,0 +1,95 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Aggregate interface.""" + +from novaclient import base + + +class Aggregate(base.Resource): + """An aggregates is a collection of compute hosts.""" + + def __repr__(self): + return "" % self.id + + def update(self, values): + """Update the name and/or availability zone.""" + return self.manager.update(self, values) + + def add_host(self, host): + return self.manager.add_host(self, host) + + def remove_host(self, host): + return self.manager.remove_host(self, host) + + def set_metadata(self, metadata): + return self.manager.set_metadata(self, metadata) + + def delete(self): + self.manager.delete(self) + + +class AggregateManager(base.ManagerWithFind): + resource_class = Aggregate + + def list(self): + """Get a list of os-aggregates.""" + return self._list('/os-aggregates', 'aggregates') + + def create(self, name, availability_zone): + """Create a new aggregate.""" + body = {'aggregate': {'name': name, + 'availability_zone': availability_zone}} + return self._create('/os-aggregates', body, 'aggregate') + + def get(self, aggregate): + """Get details of the specified aggregate.""" + return self._get('/os-aggregates/%s' % (base.getid(aggregate)), + "aggregate") + + # NOTE:(dtroyer): utils.find_resource() uses manager.get() but we need to + # keep the API backward compatible + def get_details(self, aggregate): + """Get details of the specified aggregate.""" + return self.get(aggregate) + + def update(self, aggregate, values): + """Update the name and/or availability zone.""" + body = {'aggregate': values} + return self._update("/os-aggregates/%s" % base.getid(aggregate), + body, + "aggregate") + + def add_host(self, aggregate, host): + """Add a host into the Host Aggregate.""" + body = {'add_host': {'host': host}} + return self._create("/os-aggregates/%s/action" % base.getid(aggregate), + body, "aggregate") + + def remove_host(self, aggregate, host): + """Remove a host from the Host Aggregate.""" + body = {'remove_host': {'host': host}} + return self._create("/os-aggregates/%s/action" % base.getid(aggregate), + body, "aggregate") + + def set_metadata(self, aggregate, metadata): + """Set a aggregate metadata, replacing the existing metadata.""" + body = {'set_metadata': {'metadata': metadata}} + return self._create("/os-aggregates/%s/action" % base.getid(aggregate), + body, "aggregate") + + def delete(self, aggregate): + """Delete the specified aggregates.""" + self._delete('/os-aggregates/%s' % (base.getid(aggregate))) diff --git a/awx/lib/site-packages/novaclient/v2/availability_zones.py b/awx/lib/site-packages/novaclient/v2/availability_zones.py new file mode 100644 index 0000000000..bf59037853 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/availability_zones.py @@ -0,0 +1,52 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Availability Zone interface (1.1 extension). +""" + +from novaclient import base + + +class AvailabilityZone(base.Resource): + """ + An availability zone object. + """ + NAME_ATTR = 'display_name' + + def __repr__(self): + return "" % self.zoneName + + +class AvailabilityZoneManager(base.ManagerWithFind): + """ + Manage :class:`AvailabilityZone` resources. + """ + resource_class = AvailabilityZone + return_parameter_name = "availabilityZoneInfo" + + def list(self, detailed=True): + """ + Get a list of all availability zones. + + :rtype: list of :class:`AvailabilityZone` + """ + if detailed is True: + return self._list("/os-availability-zone/detail", + self.return_parameter_name) + else: + return self._list("/os-availability-zone", + self.return_parameter_name) diff --git a/awx/lib/site-packages/novaclient/v2/certs.py b/awx/lib/site-packages/novaclient/v2/certs.py new file mode 100644 index 0000000000..2c3006c6f7 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/certs.py @@ -0,0 +1,48 @@ +# Copyright 2010 Jacob Kaplan-Moss + +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Certificate interface. +""" + +from novaclient import base + + +class Certificate(base.Resource): + def __repr__(self): + return ("" % + (len(self.private_key) if self.private_key else 0, + len(self.data))) + + +class CertificateManager(base.Manager): + """ + Manage :class:`Certificate` resources. + """ + resource_class = Certificate + + def create(self): + """ + Create a x509 certificate for a user in tenant. + """ + return self._create('/os-certificates', {}, 'certificate') + + def get(self): + """ + Get root certificate. + """ + return self._get("/os-certificates/root", 'certificate') diff --git a/awx/lib/site-packages/novaclient/v2/client.py b/awx/lib/site-packages/novaclient/v2/client.py new file mode 100644 index 0000000000..bbb8949d94 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/client.py @@ -0,0 +1,259 @@ +# Copyright 2012 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import client +from novaclient.v2 import agents +from novaclient.v2 import aggregates +from novaclient.v2 import availability_zones +from novaclient.v2 import certs +from novaclient.v2 import cloudpipe +from novaclient.v2 import fixed_ips +from novaclient.v2 import flavor_access +from novaclient.v2 import flavors +from novaclient.v2 import floating_ip_dns +from novaclient.v2 import floating_ip_pools +from novaclient.v2 import floating_ips +from novaclient.v2 import floating_ips_bulk +from novaclient.v2 import fping +from novaclient.v2 import hosts +from novaclient.v2 import hypervisors +from novaclient.v2 import images +from novaclient.v2 import keypairs +from novaclient.v2 import limits +from novaclient.v2 import networks +from novaclient.v2 import quota_classes +from novaclient.v2 import quotas +from novaclient.v2 import security_group_default_rules +from novaclient.v2 import security_group_rules +from novaclient.v2 import security_groups +from novaclient.v2 import server_groups +from novaclient.v2 import servers +from novaclient.v2 import services +from novaclient.v2 import usage +from novaclient.v2 import versions +from novaclient.v2 import virtual_interfaces +from novaclient.v2 import volume_snapshots +from novaclient.v2 import volume_types +from novaclient.v2 import volumes + + +class Client(object): + """ + Top-level object to access the OpenStack Compute API. + + Create an instance with your creds:: + + >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) + + Or, alternatively, you can create a client instance using the + keystoneclient.session API:: + + >>> from keystoneclient.auth.identity import v2 + >>> from keystoneclient import session + >>> from novaclient.client import Client + >>> auth = v2.Password(auth_url=AUTH_URL, + username=USERNAME, + password=PASSWORD, + tenant_name=PROJECT_ID) + >>> sess = session.Session(auth=auth) + >>> nova = client.Client(VERSION, session=sess) + + Then call methods on its managers:: + + >>> client.servers.list() + ... + >>> client.flavors.list() + ... + + It is also possible to use an instance as a context manager in which + case there will be a session kept alive for the duration of the with + statement:: + + >>> with Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) as client: + ... client.servers.list() + ... client.flavors.list() + ... + + It is also possible to have a permanent (process-long) connection pool, + by passing a connection_pool=True:: + + >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, + ... AUTH_URL, connection_pool=True) + """ + + def __init__(self, username=None, 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, + **kwargs): + """ + :param str username: Username + :param str api_key: API Key + :param str project_id: Project ID + :param str auth_url: Auth URL + :param bool insecure: Allow insecure + :param float timeout: API timeout, None or 0 disables + :param str proxy_tenant_id: Tenant ID + :param str proxy_token: Proxy Token + :param str region_name: Region Name + :param str endpoint_type: Endpoint Type + :param str extensions: Exensions + :param str service_type: Service Type + :param str service_name: Service Name + :param str volume_service_name: Volume Service Name + :param bool timings: Timings + :param str bypass_url: Bypass URL + :param bool os_cache: OS cache + :param bool no_cache: No cache + :param bool http_log_debug: Enable debugging for HTTP connections + :param str auth_system: Auth system + :param str auth_plugin: Auth plugin + :param str auth_token: Auth token + :param str cacert: cacert + :param str tenant_id: Tenant ID + :param str user_id: User ID + :param bool connection_pool: Use a connection pool + :param str session: Session + :param str auth: Auth + """ + # FIXME(comstud): Rename the api_key argument above when we + # know it's not being used as keyword argument + + # 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) + self.limits = limits.LimitsManager(self) + self.servers = servers.ServerManager(self) + self.versions = versions.VersionManager(self) + + # extensions + self.agents = agents.AgentsManager(self) + self.dns_domains = floating_ip_dns.FloatingIPDNSDomainManager(self) + self.dns_entries = floating_ip_dns.FloatingIPDNSEntryManager(self) + self.cloudpipe = cloudpipe.CloudpipeManager(self) + self.certs = certs.CertificateManager(self) + self.floating_ips = floating_ips.FloatingIPManager(self) + self.floating_ip_pools = floating_ip_pools.FloatingIPPoolManager(self) + self.fping = fping.FpingManager(self) + self.volumes = volumes.VolumeManager(self) + self.volume_snapshots = volume_snapshots.SnapshotManager(self) + self.volume_types = volume_types.VolumeTypeManager(self) + self.keypairs = keypairs.KeypairManager(self) + self.networks = networks.NetworkManager(self) + self.quota_classes = quota_classes.QuotaClassSetManager(self) + self.quotas = quotas.QuotaSetManager(self) + self.security_groups = security_groups.SecurityGroupManager(self) + self.security_group_rules = \ + security_group_rules.SecurityGroupRuleManager(self) + self.security_group_default_rules = \ + security_group_default_rules.SecurityGroupDefaultRuleManager(self) + self.usage = usage.UsageManager(self) + self.virtual_interfaces = \ + virtual_interfaces.VirtualInterfaceManager(self) + self.aggregates = aggregates.AggregateManager(self) + self.hosts = hosts.HostManager(self) + self.hypervisors = hypervisors.HypervisorManager(self) + self.hypervisor_stats = hypervisors.HypervisorStatsManager(self) + self.services = services.ServiceManager(self) + self.fixed_ips = fixed_ips.FixedIPsManager(self) + self.floating_ips_bulk = floating_ips_bulk.FloatingIPBulkManager(self) + self.os_cache = os_cache or not no_cache + self.availability_zones = \ + availability_zones.AvailabilityZoneManager(self) + self.server_groups = server_groups.ServerGroupsManager(self) + + # Add in any extensions... + if extensions: + for extension in extensions: + if extension.manager_class: + setattr(self, extension.name, + extension.manager_class(self)) + + self.client = client._construct_http_client( + username=username, + password=password, + user_id=user_id, + project_id=project_id, + tenant_id=tenant_id, + auth_url=auth_url, + auth_token=auth_token, + insecure=insecure, + timeout=timeout, + auth_system=auth_system, + auth_plugin=auth_plugin, + proxy_token=proxy_token, + proxy_tenant_id=proxy_tenant_id, + region_name=region_name, + endpoint_type=endpoint_type, + service_type=service_type, + service_name=service_name, + volume_service_name=volume_service_name, + timings=timings, + bypass_url=bypass_url, + os_cache=self.os_cache, + http_log_debug=http_log_debug, + cacert=cacert, + connection_pool=connection_pool, + session=session, + auth=auth, + **kwargs) + + @client._original_only + def __enter__(self): + self.client.open_session() + return self + + @client._original_only + def __exit__(self, t, v, tb): + self.client.close_session() + + @client._original_only + def set_management_url(self, url): + self.client.set_management_url(url) + + def get_timings(self): + return self.client.get_timings() + + def reset_timings(self): + self.client.reset_timings() + + @client._original_only + def authenticate(self): + """ + Authenticate against the server. + + Normally this is called automatically when you first access the API, + but you can call this method to force authentication right now. + + Returns on success; raises :exc:`exceptions.Unauthorized` if the + credentials are wrong. + """ + self.client.authenticate() diff --git a/awx/lib/site-packages/novaclient/v2/cloudpipe.py b/awx/lib/site-packages/novaclient/v2/cloudpipe.py new file mode 100644 index 0000000000..c0926ed405 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/cloudpipe.py @@ -0,0 +1,61 @@ +# 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. + +"""Cloudpipe interface.""" + +from novaclient import base + + +class Cloudpipe(base.Resource): + """A cloudpipe instance is a VPN attached to a project's VLAN.""" + + def __repr__(self): + return "" % self.project_id + + def delete(self): + self.manager.delete(self) + + +class CloudpipeManager(base.ManagerWithFind): + resource_class = Cloudpipe + + def create(self, project): + """ + Launch a cloudpipe instance. + + :param project: UUID of the project (tenant) for the cloudpipe + """ + body = {'cloudpipe': {'project_id': project}} + return self._create('/os-cloudpipe', body, 'instance_id', + return_raw=True) + + def list(self): + """ + Get a list of cloudpipe instances. + """ + return self._list('/os-cloudpipe', 'cloudpipes') + + def update(self, address, port): + """ + Update VPN address and port for all networks associated + with the project defined by authentication + + :param address: IP address + :param port: Port number + """ + + body = {'configure_project': {'vpn_ip': address, + 'vpn_port': port}} + self._update("/os-cloudpipe/configure-project", body) diff --git a/awx/lib/site-packages/novaclient/v2/contrib/__init__.py b/awx/lib/site-packages/novaclient/v2/contrib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/awx/lib/site-packages/novaclient/v2/contrib/assisted_volume_snapshots.py b/awx/lib/site-packages/novaclient/v2/contrib/assisted_volume_snapshots.py new file mode 100644 index 0000000000..ce1a0413c8 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/contrib/assisted_volume_snapshots.py @@ -0,0 +1,48 @@ +# Copyright (C) 2013, Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Assisted volume snapshots - to be used by Cinder and not end users. +""" + +import json + +from novaclient import base + + +class Snapshot(base.Resource): + def __repr__(self): + return "" % self.id + + def delete(self): + """ + Delete this snapshot. + """ + self.manager.delete(self) + + +class AssistedSnapshotManager(base.Manager): + resource_class = Snapshot + + def create(self, volume_id, create_info): + body = {'snapshot': {'volume_id': volume_id, + 'create_info': create_info}} + return self._create('/os-assisted-volume-snapshots', body, 'snapshot') + + def delete(self, snapshot, delete_info): + self._delete("/os-assisted-volume-snapshots/%s?delete_info=%s" % ( + base.getid(snapshot), json.dumps(delete_info))) + +manager_class = AssistedSnapshotManager +name = 'assisted_volume_snapshots' diff --git a/awx/lib/site-packages/novaclient/v2/contrib/baremetal.py b/awx/lib/site-packages/novaclient/v2/contrib/baremetal.py new file mode 100644 index 0000000000..d23a38e53c --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/contrib/baremetal.py @@ -0,0 +1,339 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Baremetal interface (v2 extension). +""" + +from novaclient import base +from novaclient.i18n import _ +from novaclient.openstack.common import cliutils +from novaclient import utils + + +class BareMetalNode(base.Resource): + """ + A baremetal node (typically a physical server or an empty VM). + """ + + def __repr__(self): + return "" % self.id + + +class BareMetalNodeInterface(base.Resource): + """ + An interface belonging to a baremetal node. + """ + + def __repr__(self): + return "" % self.id + + +class BareMetalNodeManager(base.ManagerWithFind): + """ + Manage :class:`BareMetalNode` resources. + """ + resource_class = BareMetalNode + + def create(self, + service_host, + cpus, + memory_mb, + local_gb, + prov_mac_address, + pm_address=None, + pm_user=None, + pm_password=None, + terminal_port=None): + """ + Create a baremetal node. + + :param service_host: Name of controlling compute host + :param cpus: Number of CPUs in the node + :param memory_mb: Megabytes of RAM in the node + :param local_gb: Gigabytes of local storage in the node + :param pm_address: Power management IP for the node + :param pm_user: Username for the node's power management + :param pm_password: Password for the node's power management + :param prov_mac_address: MAC address to provision the node + :param terminal_port: ShellInABox port + :rtype: :class:`BareMetalNode` + """ + body = {'node': {'service_host': service_host, + 'cpus': cpus, + 'memory_mb': memory_mb, + 'local_gb': local_gb, + 'pm_address': pm_address, + 'pm_user': pm_user, + 'pm_password': pm_password, + 'prov_mac_address': prov_mac_address, + 'terminal_port': terminal_port}} + + return self._create('/os-baremetal-nodes', body, 'node') + + def delete(self, node): + """ + Delete a baremetal node. + + :param node: The :class:`BareMetalNode` to delete. + """ + self._delete('/os-baremetal-nodes/%s' % base.getid(node)) + + def get(self, node_id): + """ + Get a baremetal node. + + :param node_id: The ID of the node to delete. + :rtype: :class:`BareMetalNode` + """ + return self._get("/os-baremetal-nodes/%s" % node_id, 'node') + + def list(self): + """ + Get a list of all baremetal nodes. + + :rtype: list of :class:`BareMetalNode` + """ + return self._list('/os-baremetal-nodes', 'nodes') + + def add_interface(self, node_id, address, datapath_id=0, port_no=0): + """ + Add an interface to a baremetal node. + + :param node_id: The ID of the node to modify. + :param address: The MAC address to add. + :param datapath_id: Datapath ID of OpenFlow switch for the interface + :param port_no: OpenFlow port number for the interface + :rtype: :class:`BareMetalNodeInterface` + """ + body = {'add_interface': {'address': address, + 'datapath_id': datapath_id, + 'port_no': port_no}} + url = '/os-baremetal-nodes/%s/action' % node_id + _resp, body = self.api.client.post(url, body=body) + return BareMetalNodeInterface(self, body['interface']) + + def remove_interface(self, node_id, address): + """ + Remove an interface from a baremetal node. + + :param node_id: The ID of the node to modify. + :param address: The MAC address to remove. + :rtype: bool + """ + req_body = {'remove_interface': {'address': address}} + url = '/os-baremetal-nodes/%s/action' % node_id + self.api.client.post(url, body=req_body) + + def list_interfaces(self, node_id): + """ + List the interfaces on a baremetal node. + + :param node_id: The ID of the node to list. + :rtype: list + """ + interfaces = [] + node = self._get("/os-baremetal-nodes/%s" % node_id, 'node') + for interface in node.interfaces: + interface_object = BareMetalNodeInterface(self, interface) + interfaces.append(interface_object) + return interfaces + + +@cliutils.arg( + 'service_host', + metavar='', + help=_('Name of nova compute host which will control this baremetal ' + 'node')) +@cliutils.arg( + 'cpus', + metavar='', + type=int, + help=_('Number of CPUs in the node')) +@cliutils.arg( + 'memory_mb', + metavar='', + type=int, + help=_('Megabytes of RAM in the node')) +@cliutils.arg( + 'local_gb', + metavar='', + type=int, + help=_('Gigabytes of local storage in the node')) +@cliutils.arg( + 'prov_mac_address', + metavar='', + help=_('MAC address to provision the node')) +@cliutils.arg( + '--pm_address', default=None, + metavar='', + help=_('Power management IP for the node')) +@cliutils.arg( + '--pm_user', default=None, + metavar='', + help=_('Username for the node\'s power management')) +@cliutils.arg( + '--pm_password', default=None, + metavar='', + help=_('Password for the node\'s power management')) +@cliutils.arg( + '--terminal_port', default=None, + metavar='', + type=int, + help=_('ShellInABox port?')) +def do_baremetal_node_create(cs, args): + """Create a baremetal node.""" + node = cs.baremetal.create(args.service_host, args.cpus, + args.memory_mb, args.local_gb, + args.prov_mac_address, + pm_address=args.pm_address, + pm_user=args.pm_user, + pm_password=args.pm_password, + terminal_port=args.terminal_port) + _print_baremetal_resource(node) + + +@cliutils.arg( + 'node', + metavar='', + help=_('ID of the node to delete.')) +def do_baremetal_node_delete(cs, args): + """Remove a baremetal node and any associated interfaces.""" + node = _find_baremetal_node(cs, args.node) + cs.baremetal.delete(node) + + +def _translate_baremetal_node_keys(collection): + convert = [('service_host', 'host'), + ('local_gb', 'disk_gb'), + ('prov_mac_address', 'mac_address'), + ('pm_address', 'pm_address'), + ('pm_user', 'pm_username'), + ('pm_password', 'pm_password'), + ('terminal_port', 'terminal_port'), + ] + for item in collection: + keys = item.__dict__.keys() + for from_key, to_key in convert: + if from_key in keys and to_key not in keys: + setattr(item, to_key, item._info[from_key]) + + +def _print_baremetal_nodes_list(nodes): + """Print the list of baremetal nodes.""" + + def _parse_address(fields): + macs = [] + for interface in fields.interfaces: + macs.append(interface['address']) + return ', '.join("%s" % i for i in macs) + + formatters = { + 'MAC Address': _parse_address + } + + _translate_baremetal_node_keys(nodes) + utils.print_list(nodes, [ + 'ID', + 'Host', + 'Task State', + 'CPUs', + 'Memory_MB', + 'Disk_GB', + 'MAC Address', + 'PM Address', + 'PM Username', + 'PM Password', + 'Terminal Port', + ], formatters=formatters) + + +def do_baremetal_node_list(cs, _args): + """Print list of available baremetal nodes.""" + nodes = cs.baremetal.list() + _print_baremetal_nodes_list(nodes) + + +def _find_baremetal_node(cs, node): + """Get a node by ID.""" + return utils.find_resource(cs.baremetal, node) + + +def _print_baremetal_resource(resource): + """Print details of a baremetal resource.""" + info = resource._info.copy() + utils.print_dict(info) + + +def _print_baremetal_node_interfaces(interfaces): + """Print interfaces of a baremetal node.""" + utils.print_list(interfaces, [ + 'ID', + 'Datapath_ID', + 'Port_No', + 'Address', + ]) + + +@cliutils.arg( + 'node', + metavar='', + help=_("ID of node")) +def do_baremetal_node_show(cs, args): + """Show information about a baremetal node.""" + node = _find_baremetal_node(cs, args.node) + _print_baremetal_resource(node) + + +@cliutils.arg( + 'node', + metavar='', + help=_("ID of node")) +@cliutils.arg( + 'address', + metavar='
', + help=_("MAC address of interface")) +@cliutils.arg( + '--datapath_id', + default=0, + metavar='', + help=_("OpenFlow Datapath ID of interface")) +@cliutils.arg( + '--port_no', + default=0, + metavar='', + help=_("OpenFlow port number of interface")) +def do_baremetal_interface_add(cs, args): + """Add a network interface to a baremetal node.""" + bmif = cs.baremetal.add_interface(args.node, args.address, + args.datapath_id, args.port_no) + _print_baremetal_resource(bmif) + + +@cliutils.arg('node', metavar='', help=_("ID of node")) +@cliutils.arg( + 'address', + metavar='
', + help=_("MAC address of interface")) +def do_baremetal_interface_remove(cs, args): + """Remove a network interface from a baremetal node.""" + cs.baremetal.remove_interface(args.node, args.address) + + +@cliutils.arg('node', metavar='', help=_("ID of node")) +def do_baremetal_interface_list(cs, args): + """List network interfaces associated with a baremetal node.""" + interfaces = cs.baremetal.list_interfaces(args.node) + _print_baremetal_node_interfaces(interfaces) diff --git a/awx/lib/site-packages/novaclient/v2/contrib/cells.py b/awx/lib/site-packages/novaclient/v2/contrib/cells.py new file mode 100644 index 0000000000..7099a5a007 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/contrib/cells.py @@ -0,0 +1,74 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base +from novaclient.i18n import _ +from novaclient.openstack.common import cliutils +from novaclient import utils + + +class Cell(base.Resource): + def __repr__(self): + return "" % self.name + + +class CellsManager(base.Manager): + resource_class = Cell + + def get(self, cell_name): + """ + Get a cell. + + :param cell_name: Name of the :class:`Cell` to get. + :rtype: :class:`Cell` + """ + return self._get("/os-cells/%s" % cell_name, "cell") + + def capacities(self, cell_name=None): + """ + Get capacities for a cell. + + :param cell_name: Name of the :class:`Cell` to get capacities for. + :rtype: :class:`Cell` + """ + path = ["%s/capacities" % cell_name, "capacities"][cell_name is None] + return self._get("/os-cells/%s" % path, "cell") + + +@cliutils.arg( + 'cell', + metavar='', + help=_('Name of the cell.')) +def do_cell_show(cs, args): + """Show details of a given cell.""" + cell = cs.cells.get(args.cell) + utils.print_dict(cell._info) + + +@cliutils.arg( + '--cell', + metavar='', + help=_("Name of the cell to get the capacities."), + default=None) +def do_cell_capacities(cs, args): + """Get cell capacities for all cells or a given cell.""" + cell = cs.cells.capacities(args.cell) + print(_("Ram Available: %s MB") % cell.capacities['ram_free']['total_mb']) + utils.print_dict(cell.capacities['ram_free']['units_by_mb'], + dict_property='Ram(MB)', dict_value="Units") + print(_("\nDisk Available: %s MB") % + cell.capacities['disk_free']['total_mb']) + utils.print_dict(cell.capacities['disk_free']['units_by_mb'], + dict_property='Disk(MB)', dict_value="Units") diff --git a/awx/lib/site-packages/novaclient/v2/contrib/deferred_delete.py b/awx/lib/site-packages/novaclient/v2/contrib/deferred_delete.py new file mode 100644 index 0000000000..5d8d7d9e39 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/contrib/deferred_delete.py @@ -0,0 +1,28 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from novaclient.openstack.common import cliutils +from novaclient import utils + + +@cliutils.arg('server', metavar='', help='Name or ID of server.') +def do_force_delete(cs, args): + """Force delete a server.""" + utils.find_resource(cs.servers, args.server).force_delete() + + +@cliutils.arg('server', metavar='', help='Name or ID of server.') +def do_restore(cs, args): + """Restore a soft-deleted server.""" + utils.find_resource(cs.servers, args.server).restore() diff --git a/awx/lib/site-packages/novaclient/v2/contrib/host_evacuate.py b/awx/lib/site-packages/novaclient/v2/contrib/host_evacuate.py new file mode 100644 index 0000000000..b08e28cd1c --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/contrib/host_evacuate.py @@ -0,0 +1,64 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base +from novaclient.i18n import _ +from novaclient.openstack.common import cliutils +from novaclient import utils + + +class EvacuateHostResponse(base.Resource): + pass + + +def _server_evacuate(cs, server, args): + success = True + error_message = "" + try: + cs.servers.evacuate(server=server['uuid'], host=args.target_host, + on_shared_storage=args.on_shared_storage) + except Exception as e: + success = False + error_message = _("Error while evacuating instance: %s") % e + return EvacuateHostResponse(base.Manager, + {"server_uuid": server['uuid'], + "evacuate_accepted": success, + "error_message": error_message}) + + +@cliutils.arg('host', metavar='', help='Name of host.') +@cliutils.arg( + '--target_host', + metavar='', + default=None, + help=_('Name of target host. If no host is specified the scheduler will ' + 'select a target.')) +@cliutils.arg( + '--on-shared-storage', + dest='on_shared_storage', + action="store_true", + default=False, + help=_('Specifies whether all instances files are on shared storage')) +def do_host_evacuate(cs, args): + """Evacuate all instances from failed host.""" + hypervisors = cs.hypervisors.search(args.host, servers=True) + response = [] + for hyper in hypervisors: + if hasattr(hyper, 'servers'): + for server in hyper.servers: + response.append(_server_evacuate(cs, server, args)) + + utils.print_list(response, + ["Server UUID", "Evacuate Accepted", "Error Message"]) diff --git a/awx/lib/site-packages/novaclient/v2/contrib/host_evacuate_live.py b/awx/lib/site-packages/novaclient/v2/contrib/host_evacuate_live.py new file mode 100644 index 0000000000..276fb2f703 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/contrib/host_evacuate_live.py @@ -0,0 +1,68 @@ +# Copyright 2014 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.i18n import _ +from novaclient.openstack.common import cliutils +from novaclient import utils + + +def _server_live_migrate(cs, server, args): + class HostEvacuateLiveResponse(object): + def __init__(self, server_uuid, live_migration_accepted, + error_message): + self.server_uuid = server_uuid + self.live_migration_accepted = live_migration_accepted + self.error_message = error_message + success = True + error_message = "" + try: + cs.servers.live_migrate(server['uuid'], args.target_host, + args.block_migrate, args.disk_over_commit) + except Exception as e: + success = False + error_message = _("Error while live migrating instance: %s") % e + return HostEvacuateLiveResponse(server['uuid'], + success, + error_message) + + +@cliutils.arg('host', metavar='', help='Name of host.') +@cliutils.arg( + '--target-host', + metavar='', + default=None, + help=_('Name of target host.')) +@cliutils.arg( + '--block-migrate', + action='store_true', + default=False, + help=_('Enable block migration.')) +@cliutils.arg( + '--disk-over-commit', + action='store_true', + default=False, + help=_('Enable disk overcommit.')) +def do_host_evacuate_live(cs, args): + """Live migrate all instances of the specified host + to other available hosts. + """ + hypervisors = cs.hypervisors.search(args.host, servers=True) + response = [] + for hyper in hypervisors: + for server in getattr(hyper, 'servers', []): + response.append(_server_live_migrate(cs, server, args)) + + utils.print_list(response, ["Server UUID", "Live Migration Accepted", + "Error Message"]) diff --git a/awx/lib/site-packages/novaclient/v2/contrib/host_servers_migrate.py b/awx/lib/site-packages/novaclient/v2/contrib/host_servers_migrate.py new file mode 100644 index 0000000000..470f161f19 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/contrib/host_servers_migrate.py @@ -0,0 +1,51 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base +from novaclient.i18n import _ +from novaclient.openstack.common import cliutils +from novaclient import utils + + +class HostServersMigrateResponse(base.Resource): + pass + + +def _server_migrate(cs, server): + success = True + error_message = "" + try: + cs.servers.migrate(server['uuid']) + except Exception as e: + success = False + error_message = _("Error while migrating instance: %s") % e + return HostServersMigrateResponse(base.Manager, + {"server_uuid": server['uuid'], + "migration_accepted": success, + "error_message": error_message}) + + +@cliutils.arg('host', metavar='', help='Name of host.') +def do_host_servers_migrate(cs, args): + """Migrate all instances of the specified host to other available hosts.""" + hypervisors = cs.hypervisors.search(args.host, servers=True) + response = [] + for hyper in hypervisors: + if hasattr(hyper, 'servers'): + for server in hyper.servers: + response.append(_server_migrate(cs, server)) + + utils.print_list(response, + ["Server UUID", "Migration Accepted", "Error Message"]) diff --git a/awx/lib/site-packages/novaclient/v2/contrib/instance_action.py b/awx/lib/site-packages/novaclient/v2/contrib/instance_action.py new file mode 100644 index 0000000000..10e518ea23 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/contrib/instance_action.py @@ -0,0 +1,72 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import pprint + +from novaclient import base +from novaclient.i18n import _ +from novaclient.openstack.common import cliutils +from novaclient import utils + + +class InstanceActionManager(base.ManagerWithFind): + resource_class = base.Resource + + def get(self, server, request_id): + """ + Get details of an action performed on an instance. + + :param request_id: The request_id of the action to get. + """ + return self._get("/servers/%s/os-instance-actions/%s" % + (base.getid(server), request_id), 'instanceAction') + + def list(self, server): + """ + Get a list of actions performed on an server. + """ + return self._list('/servers/%s/os-instance-actions' % + base.getid(server), 'instanceActions') + + +@cliutils.arg( + 'server', + metavar='', + help=_('Name or UUID of the server to show an action for.')) +@cliutils.arg( + 'request_id', + metavar='', + help=_('Request ID of the action to get.')) +def do_instance_action(cs, args): + """Show an action.""" + server = utils.find_resource(cs.servers, args.server) + action_resource = cs.instance_action.get(server, args.request_id) + action = action_resource._info + if 'events' in action: + action['events'] = pprint.pformat(action['events']) + utils.print_dict(action) + + +@cliutils.arg( + 'server', + metavar='', + help=_('Name or UUID of the server to list actions for.')) +def do_instance_action_list(cs, args): + """List actions on a server.""" + server = utils.find_resource(cs.servers, args.server) + actions = cs.instance_action.list(server) + utils.print_list(actions, + ['Action', 'Request_ID', 'Message', 'Start_Time'], + sortby_index=3) diff --git a/awx/lib/site-packages/novaclient/v2/contrib/list_extensions.py b/awx/lib/site-packages/novaclient/v2/contrib/list_extensions.py new file mode 100644 index 0000000000..7eb9f16c8a --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/contrib/list_extensions.py @@ -0,0 +1,46 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base +from novaclient import utils + + +class ListExtResource(base.Resource): + @property + def summary(self): + descr = self.description.strip() + if not descr: + return '??' + lines = descr.split("\n") + if len(lines) == 1: + return lines[0] + else: + return lines[0] + "..." + + +class ListExtManager(base.Manager): + resource_class = ListExtResource + + def show_all(self): + return self._list("/extensions", 'extensions') + + +def do_list_extensions(client, _args): + """ + List all the os-api extensions that are available. + """ + extensions = client.list_extensions.show_all() + fields = ["Name", "Summary", "Alias", "Updated"] + utils.print_list(extensions, fields) diff --git a/awx/lib/site-packages/novaclient/v2/contrib/metadata_extensions.py b/awx/lib/site-packages/novaclient/v2/contrib/metadata_extensions.py new file mode 100644 index 0000000000..ad843b7461 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/contrib/metadata_extensions.py @@ -0,0 +1,47 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.i18n import _ +from novaclient.openstack.common import cliutils +from novaclient.v2 import shell + + +@cliutils.arg( + 'host', + metavar='', + help=_('Name of host.')) +@cliutils.arg( + 'action', + metavar='', + choices=['set', 'delete'], + help=_("Actions: 'set' or 'delete'")) +@cliutils.arg( + 'metadata', + metavar='', + nargs='+', + action='append', + default=[], + help=_('Metadata to set or delete (only key is necessary on delete)')) +def do_host_meta(cs, args): + """Set or Delete metadata on all instances of a host.""" + hypervisors = cs.hypervisors.search(args.host, servers=True) + for hyper in hypervisors: + metadata = shell._extract_metadata(args) + if hasattr(hyper, 'servers'): + for server in hyper.servers: + if args.action == 'set': + cs.servers.set_meta(server['uuid'], metadata) + elif args.action == 'delete': + cs.servers.delete_meta(server['uuid'], metadata.keys()) diff --git a/awx/lib/site-packages/novaclient/v2/contrib/migrations.py b/awx/lib/site-packages/novaclient/v2/contrib/migrations.py new file mode 100644 index 0000000000..15959818a4 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/contrib/migrations.py @@ -0,0 +1,91 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +migration interface +""" + +from six.moves.urllib import parse + +from novaclient import base +from novaclient.i18n import _ +from novaclient.openstack.common import cliutils +from novaclient import utils + + +class Migration(base.Resource): + def __repr__(self): + return "" % self.id + + +class MigrationManager(base.ManagerWithFind): + resource_class = Migration + + def list(self, host=None, status=None, cell_name=None): + """ + Get a list of migrations. + :param host: (optional) filter migrations by host name. + :param status: (optional) filter migrations by status. + :param cell_name: (optional) filter migrations for a cell. + """ + opts = {} + if host: + opts['host'] = host + if status: + opts['status'] = status + if cell_name: + opts['cell_name'] = cell_name + + # Transform the dict to a sequence of two-element tuples in fixed + # order, then the encoded string will be consistent in Python 2&3. + new_opts = sorted(opts.items(), key=lambda x: x[0]) + + query_string = "?%s" % parse.urlencode(new_opts) if new_opts else "" + + return self._list("/os-migrations%s" % query_string, "migrations") + + +@cliutils.arg( + '--host', + dest='host', + metavar='', + help=_('Fetch migrations for the given host.')) +@cliutils.arg( + '--status', + dest='status', + metavar='', + help=_('Fetch migrations for the given status.')) +@cliutils.arg( + '--cell_name', + dest='cell_name', + metavar='', + help=_('Fetch migrations for the given cell_name.')) +def do_migration_list(cs, args): + """Print a list of migrations.""" + _print_migrations(cs.migrations.list(args.host, args.status, + args.cell_name)) + + +def _print_migrations(migrations): + fields = ['Source Node', 'Dest Node', 'Source Compute', 'Dest Compute', + 'Dest Host', 'Status', 'Instance UUID', 'Old Flavor', + 'New Flavor', 'Created At', 'Updated At'] + + def old_flavor(migration): + return migration.old_instance_type_id + + def new_flavor(migration): + return migration.new_instance_type_id + + formatters = {'Old Flavor': old_flavor, 'New Flavor': new_flavor} + + utils.print_list(migrations, fields, formatters) diff --git a/awx/lib/site-packages/novaclient/v2/contrib/server_external_events.py b/awx/lib/site-packages/novaclient/v2/contrib/server_external_events.py new file mode 100644 index 0000000000..a45914b55c --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/contrib/server_external_events.py @@ -0,0 +1,43 @@ +# Copyright (C) 2014, Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +External event triggering for servers, not to be used by users. +""" + +from novaclient import base + + +class Event(base.Resource): + def __repr__(self): + return "" % self.name + + +class ServerExternalEventManager(base.Manager): + resource_class = Event + + def create(self, events): + """Create one or more server events. + + :param:events: A list of dictionaries containing 'server_uuid', 'name', + 'status', and 'tag' (which may be absent) + """ + + body = {'events': events} + return self._create('/os-server-external-events', body, 'events', + return_raw=True) + + +manager_class = ServerExternalEventManager +name = 'server_external_events' diff --git a/awx/lib/site-packages/novaclient/v2/contrib/tenant_networks.py b/awx/lib/site-packages/novaclient/v2/contrib/tenant_networks.py new file mode 100644 index 0000000000..0b56ae8ccd --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/contrib/tenant_networks.py @@ -0,0 +1,120 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from novaclient import base +from novaclient.i18n import _ +from novaclient.openstack.common import cliutils +from novaclient import utils + + +class TenantNetwork(base.Resource): + def delete(self): + self.manager.delete(network=self) + + +class TenantNetworkManager(base.ManagerWithFind): + resource_class = base.Resource + + def list(self): + return self._list('/os-tenant-networks', 'networks') + + def get(self, network): + return self._get('/os-tenant-networks/%s' % base.getid(network), + 'network') + + def delete(self, network): + self._delete('/os-tenant-networks/%s' % base.getid(network)) + + def create(self, label, cidr): + body = {'network': {'label': label, 'cidr': cidr}} + return self._create('/os-tenant-networks', body, 'network') + + +@cliutils.arg('network_id', metavar='', help='ID of network') +def do_net(cs, args): + """ + DEPRECATED, Use tenant-network-show instead. + """ + do_tenant_network_show(cs, args) + + +@cliutils.arg('network_id', metavar='', help='ID of network') +def do_tenant_network_show(cs, args): + """ + Show a tenant network. + """ + network = cs.tenant_networks.get(args.network_id) + utils.print_dict(network._info) + + +def do_net_list(cs, args): + """ + DEPRECATED, use tenant-network-list instead. + """ + do_tenant_network_list(cs, args) + + +def do_tenant_network_list(cs, args): + """ + List tenant networks. + """ + networks = cs.tenant_networks.list() + utils.print_list(networks, ['ID', 'Label', 'CIDR']) + + +@cliutils.arg( + 'label', + metavar='', + help=_('Network label (ex. my_new_network)')) +@cliutils.arg( + 'cidr', + metavar='', + help=_('IP block to allocate from (ex. 172.16.0.0/24 or 2001:DB8::/64)')) +def do_net_create(cs, args): + """ + DEPRECATED, use tenant-network-create instead. + """ + do_tenant_network_create(cs, args) + + +@cliutils.arg( + 'label', + metavar='', + help=_('Network label (ex. my_new_network)')) +@cliutils.arg( + 'cidr', + metavar='', + help=_('IP block to allocate from (ex. 172.16.0.0/24 or 2001:DB8::/64)')) +def do_tenant_network_create(cs, args): + """ + Create a tenant network. + """ + network = cs.tenant_networks.create(args.label, args.cidr) + utils.print_dict(network._info) + + +@cliutils.arg('network_id', metavar='', help='ID of network') +def do_net_delete(cs, args): + """ + DEPRECATED, use tenant-network-delete instead. + """ + do_tenant_network_delete(cs, args) + + +@cliutils.arg('network_id', metavar='', help='ID of network') +def do_tenant_network_delete(cs, args): + """ + Delete a tenant network. + """ + cs.tenant_networks.delete(args.network_id) diff --git a/awx/lib/site-packages/novaclient/v2/fixed_ips.py b/awx/lib/site-packages/novaclient/v2/fixed_ips.py new file mode 100644 index 0000000000..4e7c0e9512 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/fixed_ips.py @@ -0,0 +1,56 @@ +# Copyright 2012 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Fixed IPs interface. +""" + +from novaclient import base + + +class FixedIP(base.Resource): + def __repr__(self): + return "" % self.address + + +class FixedIPsManager(base.Manager): + resource_class = FixedIP + + def get(self, fixed_ip): + """ + Show information for a Fixed IP + + :param fixed_ip: Fixed IP address to get info for + """ + return self._get('/os-fixed-ips/%s' % base.getid(fixed_ip), + "fixed_ip") + + def reserve(self, fixed_ip): + """Reserve a Fixed IP + + :param fixed_ip: Fixed IP address to reserve + """ + body = {"reserve": None} + self.api.client.post('/os-fixed-ips/%s/action' % base.getid(fixed_ip), + body=body) + + def unreserve(self, fixed_ip): + """Unreserve a Fixed IP + + :param fixed_ip: Fixed IP address to unreserve + """ + body = {"unreserve": None} + self.api.client.post('/os-fixed-ips/%s/action' % base.getid(fixed_ip), + body=body) diff --git a/awx/lib/site-packages/novaclient/v2/flavor_access.py b/awx/lib/site-packages/novaclient/v2/flavor_access.py new file mode 100644 index 0000000000..ca78bdff75 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/flavor_access.py @@ -0,0 +1,70 @@ +# 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. + +"""Flavor access interface.""" + +from novaclient import base +from novaclient.i18n import _ + + +class FlavorAccess(base.Resource): + def __repr__(self): + return ("" % + (self.flavor_id, self.tenant_id)) + + +class FlavorAccessManager(base.ManagerWithFind): + """ + Manage :class:`FlavorAccess` resources. + """ + resource_class = FlavorAccess + + def list(self, **kwargs): + if kwargs.get('flavor'): + return self._list_by_flavor(kwargs['flavor']) + elif kwargs.get('tenant'): + return self._list_by_tenant(kwargs['tenant']) + else: + raise NotImplementedError(_('Unknown list options.')) + + def _list_by_flavor(self, flavor): + return self._list('/flavors/%s/os-flavor-access' % base.getid(flavor), + 'flavor_access') + + def _list_by_tenant(self, tenant): + """Print flavor list shared with the given tenant.""" + # TODO(uni): need to figure out a proper URI for list_by_tenant + # since current API already provided current tenant_id information + raise NotImplementedError(_('Sorry, query by tenant not supported.')) + + def add_tenant_access(self, flavor, tenant): + """Add a tenant to the given flavor access list.""" + info = {'tenant': tenant} + return self._action('addTenantAccess', flavor, info) + + def remove_tenant_access(self, flavor, tenant): + """Remove a tenant from the given flavor access list.""" + info = {'tenant': tenant} + return self._action('removeTenantAccess', flavor, info) + + def _action(self, action, flavor, info, **kwargs): + """Perform a flavor action.""" + body = {action: info} + self.run_hooks('modify_body_for_action', body, **kwargs) + url = '/flavors/%s/action' % base.getid(flavor) + _resp, body = self.api.client.post(url, body=body) + + return [self.resource_class(self, res) + for res in body['flavor_access']] diff --git a/awx/lib/site-packages/novaclient/v2/flavors.py b/awx/lib/site-packages/novaclient/v2/flavors.py new file mode 100644 index 0000000000..54b0f49743 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/flavors.py @@ -0,0 +1,203 @@ +# Copyright 2010 Jacob Kaplan-Moss +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Flavor interface. +""" + +from oslo.utils import strutils +from six.moves.urllib import parse + +from novaclient import base +from novaclient import exceptions +from novaclient.i18n import _ +from novaclient import utils + + +class Flavor(base.Resource): + """ + A flavor is an available hardware configuration for a server. + """ + HUMAN_ID = True + + def __repr__(self): + return "" % self.name + + @property + def ephemeral(self): + """ + Provide a user-friendly accessor to OS-FLV-EXT-DATA:ephemeral + """ + return self._info.get("OS-FLV-EXT-DATA:ephemeral", 'N/A') + + @property + def is_public(self): + """ + Provide a user-friendly accessor to os-flavor-access:is_public + """ + return self._info.get("os-flavor-access:is_public", 'N/A') + + def get_keys(self): + """ + Get extra specs from a flavor. + """ + _resp, body = self.manager.api.client.get( + "/flavors/%s/os-extra_specs" % base.getid(self)) + return body["extra_specs"] + + def set_keys(self, metadata): + """ + Set extra specs on a flavor. + + :param metadata: A dict of key/value pairs to be set + """ + utils.validate_flavor_metadata_keys(metadata.keys()) + + body = {'extra_specs': metadata} + return self.manager._create( + "/flavors/%s/os-extra_specs" % base.getid(self), body, + "extra_specs", return_raw=True) + + def unset_keys(self, keys): + """ + Unset extra specs on a flavor. + + :param keys: A list of keys to be unset + """ + for k in keys: + self.manager._delete( + "/flavors/%s/os-extra_specs/%s" % (base.getid(self), k)) + + def delete(self): + """ + Delete this flavor. + """ + self.manager.delete(self) + + +class FlavorManager(base.ManagerWithFind): + """ + Manage :class:`Flavor` resources. + """ + resource_class = Flavor + is_alphanum_id_allowed = True + + def list(self, detailed=True, is_public=True): + """ + Get a list of all flavors. + + :rtype: list of :class:`Flavor`. + """ + qparams = {} + # is_public is ternary - None means give all flavors. + # By default Nova assumes True and gives admins public flavors + # and flavors from their own projects only. + if not is_public: + qparams['is_public'] = is_public + query_string = "?%s" % parse.urlencode(qparams) if qparams else "" + + detail = "" + if detailed: + detail = "/detail" + + return self._list("/flavors%s%s" % (detail, query_string), "flavors") + + def get(self, flavor): + """ + Get a specific flavor. + + :param flavor: The ID of the :class:`Flavor` to get. + :rtype: :class:`Flavor` + """ + return self._get("/flavors/%s" % base.getid(flavor), "flavor") + + def delete(self, flavor): + """ + Delete a specific flavor. + + :param flavor: The ID of the :class:`Flavor` to get. + """ + self._delete("/flavors/%s" % base.getid(flavor)) + + def _build_body(self, name, ram, vcpus, disk, id, swap, + ephemeral, rxtx_factor, is_public): + return { + "flavor": { + "name": name, + "ram": ram, + "vcpus": vcpus, + "disk": disk, + "id": id, + "swap": swap, + "OS-FLV-EXT-DATA:ephemeral": ephemeral, + "rxtx_factor": rxtx_factor, + "os-flavor-access:is_public": is_public, + } + } + + def create(self, name, ram, vcpus, disk, flavorid="auto", + ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): + """ + Create a flavor. + + :param name: Descriptive name of the flavor + :param ram: Memory in MB for the flavor + :param vcpus: Number of VCPUs for the flavor + :param disk: Size of local disk in GB + :param flavorid: ID for the flavor (optional). You can use the reserved + value ``"auto"`` to have Nova generate a UUID for the + flavor in cases where you cannot simply pass ``None``. + :param swap: Swap space in MB + :param rxtx_factor: RX/TX factor + :rtype: :class:`Flavor` + """ + + try: + ram = int(ram) + except (TypeError, ValueError): + raise exceptions.CommandError(_("Ram must be an integer.")) + try: + vcpus = int(vcpus) + except (TypeError, ValueError): + raise exceptions.CommandError(_("VCPUs must be an integer.")) + try: + disk = int(disk) + except (TypeError, ValueError): + raise exceptions.CommandError(_("Disk must be an integer.")) + + if flavorid == "auto": + flavorid = None + + try: + swap = int(swap) + except (TypeError, ValueError): + raise exceptions.CommandError(_("Swap must be an integer.")) + try: + ephemeral = int(ephemeral) + except (TypeError, ValueError): + raise exceptions.CommandError(_("Ephemeral must be an integer.")) + try: + rxtx_factor = float(rxtx_factor) + except (TypeError, ValueError): + raise exceptions.CommandError(_("rxtx_factor must be a float.")) + + try: + is_public = strutils.bool_from_string(is_public, True) + except Exception: + raise exceptions.CommandError(_("is_public must be a boolean.")) + + body = self._build_body(name, ram, vcpus, disk, flavorid, swap, + ephemeral, rxtx_factor, is_public) + + return self._create("/flavors", body, "flavor") diff --git a/awx/lib/site-packages/novaclient/v2/floating_ip_dns.py b/awx/lib/site-packages/novaclient/v2/floating_ip_dns.py new file mode 100644 index 0000000000..41df0f6284 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/floating_ip_dns.py @@ -0,0 +1,121 @@ +# Copyright 2011 Andrew Bogott for The Wikimedia Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from six.moves.urllib import parse + +from novaclient import base + + +def _quote_domain(domain): + """Special quoting rule for placing domain names on a url line. + + Domain names tend to have .'s in them. Urllib doesn't quote dots, + but Routes tends to choke on them, so we need an extra level of + by-hand quoting here. + """ + return parse.quote(domain.replace('.', '%2E')) + + +class FloatingIPDNSDomain(base.Resource): + def delete(self): + self.manager.delete(self.domain) + + def create(self): + if self.scope == 'public': + self.manager.create_public(self.domain, self.project) + else: + self.manager.create_private(self.domain, self.availability_zone) + + def get(self): + entries = self.manager.domains() + for entry in entries: + if entry.get('domain') == self.domain: + return entry + + return None + + +class FloatingIPDNSDomainManager(base.Manager): + resource_class = FloatingIPDNSDomain + + def domains(self): + """Return the list of available dns domains.""" + return self._list("/os-floating-ip-dns", "domain_entries") + + def create_private(self, fqdomain, availability_zone): + """Add or modify a private DNS domain.""" + body = {'domain_entry': {'scope': 'private', + 'availability_zone': availability_zone}} + return self._update('/os-floating-ip-dns/%s' % _quote_domain(fqdomain), + body, + 'domain_entry') + + def create_public(self, fqdomain, project): + """Add or modify a public DNS domain.""" + body = {'domain_entry': {'scope': 'public', 'project': project}} + + return self._update('/os-floating-ip-dns/%s' % _quote_domain(fqdomain), + body, 'domain_entry') + + def delete(self, fqdomain): + """Delete the specified domain.""" + self._delete("/os-floating-ip-dns/%s" % _quote_domain(fqdomain)) + + +class FloatingIPDNSEntry(base.Resource): + def delete(self): + self.manager.delete(self.name, self.domain) + + def create(self): + self.manager.create(self.domain, self.name, self.ip, self.dns_type) + + def get(self): + return self.manager.get(self.domain, self.name) + + +class FloatingIPDNSEntryManager(base.Manager): + resource_class = FloatingIPDNSEntry + + def get(self, domain, name): + """Return a list of entries for the given domain and IP or name.""" + return self._get("/os-floating-ip-dns/%s/entries/%s" % + (_quote_domain(domain), name), "dns_entry") + + def get_for_ip(self, domain, ip): + """Return a list of entries for the given domain and IP or name.""" + qparams = {'ip': ip} + params = "?%s" % parse.urlencode(qparams) + + return self._list("/os-floating-ip-dns/%s/entries%s" % + (_quote_domain(domain), params), "dns_entries") + + def create(self, domain, name, ip, dns_type): + """Add a new DNS entry.""" + body = {'dns_entry': {'ip': ip, 'dns_type': dns_type}} + + return self._update("/os-floating-ip-dns/%s/entries/%s" % + (_quote_domain(domain), name), body, "dns_entry") + + def modify_ip(self, domain, name, ip): + """Add a new DNS entry.""" + body = {'dns_entry': {'ip': ip, 'dns_type': 'A'}} + + return self._update("/os-floating-ip-dns/%s/entries/%s" % + (_quote_domain(domain), name), body, "dns_entry") + + def delete(self, domain, name): + """Delete entry specified by name and domain.""" + self._delete("/os-floating-ip-dns/%s/entries/%s" % + (_quote_domain(domain), name)) diff --git a/awx/lib/site-packages/novaclient/v2/floating_ip_pools.py b/awx/lib/site-packages/novaclient/v2/floating_ip_pools.py new file mode 100644 index 0000000000..7666bd57f8 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/floating_ip_pools.py @@ -0,0 +1,32 @@ +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base + + +class FloatingIPPool(base.Resource): + def __repr__(self): + return "" % self.name + + +class FloatingIPPoolManager(base.ManagerWithFind): + resource_class = FloatingIPPool + + def list(self): + """ + Retrieve a list of all floating ip pools. + """ + return self._list('/os-floating-ip-pools', 'floating_ip_pools') diff --git a/awx/lib/site-packages/novaclient/v2/floating_ips.py b/awx/lib/site-packages/novaclient/v2/floating_ips.py new file mode 100644 index 0000000000..cce92da250 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/floating_ips.py @@ -0,0 +1,59 @@ +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base + + +class FloatingIP(base.Resource): + def delete(self): + """ + Delete this floating IP + """ + self.manager.delete(self) + + +class FloatingIPManager(base.ManagerWithFind): + resource_class = FloatingIP + + def list(self, all_tenants=False): + """ + List floating IPs + """ + url = '/os-floating-ips' + if all_tenants: + url += '?all_tenants=1' + return self._list(url, "floating_ips") + + def create(self, pool=None): + """ + Create (allocate) a floating IP for a tenant + """ + return self._create("/os-floating-ips", {'pool': pool}, "floating_ip") + + def delete(self, floating_ip): + """ + Delete (deallocate) a floating IP for a tenant + + :param floating_ip: The floating IP address to delete. + """ + self._delete("/os-floating-ips/%s" % base.getid(floating_ip)) + + def get(self, floating_ip): + """ + Retrieve a floating IP + """ + return self._get("/os-floating-ips/%s" % base.getid(floating_ip), + "floating_ip") diff --git a/awx/lib/site-packages/novaclient/v2/floating_ips_bulk.py b/awx/lib/site-packages/novaclient/v2/floating_ips_bulk.py new file mode 100644 index 0000000000..fb59a19e74 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/floating_ips_bulk.py @@ -0,0 +1,58 @@ +# Copyright 2012 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Bulk Floating IPs interface +""" +from novaclient import base + + +class FloatingIP(base.Resource): + def __repr__(self): + return "" % self.address + + +class FloatingIPBulkManager(base.ManagerWithFind): + resource_class = FloatingIP + + def list(self, host=None): + """ + List all floating IPs + """ + if host is None: + return self._list('/os-floating-ips-bulk', 'floating_ip_info') + else: + return self._list('/os-floating-ips-bulk/%s' % host, + 'floating_ip_info') + + def create(self, ip_range, pool=None, interface=None): + """ + Create floating IPs by range + """ + body = {"floating_ips_bulk_create": {'ip_range': ip_range}} + if pool is not None: + body['floating_ips_bulk_create']['pool'] = pool + if interface is not None: + body['floating_ips_bulk_create']['interface'] = interface + + return self._create('/os-floating-ips-bulk', body, + 'floating_ips_bulk_create') + + def delete(self, ip_range): + """ + Delete floating IPs by range + """ + body = {"ip_range": ip_range} + return self._update('/os-floating-ips-bulk/delete', body) diff --git a/awx/lib/site-packages/novaclient/v2/fping.py b/awx/lib/site-packages/novaclient/v2/fping.py new file mode 100644 index 0000000000..5e8b74bb9b --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/fping.py @@ -0,0 +1,64 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Fping interface. +""" + +from novaclient import base + + +class Fping(base.Resource): + """ + A server to fping. + """ + HUMAN_ID = True + + def __repr__(self): + return "" % self.id + + +class FpingManager(base.ManagerWithFind): + """ + Manage :class:`Fping` resources. + """ + resource_class = Fping + + def list(self, all_tenants=False, include=[], exclude=[]): + """ + Fping all servers. + + :rtype: list of :class:`Fping`. + """ + params = [] + if all_tenants: + params.append("all_tenants=1") + if include: + params.append("include=%s" % ",".join(include)) + elif exclude: + params.append("exclude=%s" % ",".join(exclude)) + uri = "/os-fping" + if params: + uri = "%s?%s" % (uri, "&".join(params)) + return self._list(uri, "servers") + + def get(self, server): + """ + Fping a specific server. + + :param server: ID of the server to fping. + :rtype: :class:`Fping` + """ + return self._get("/os-fping/%s" % base.getid(server), "server") diff --git a/awx/lib/site-packages/novaclient/v2/hosts.py b/awx/lib/site-packages/novaclient/v2/hosts.py new file mode 100644 index 0000000000..af1756ad2c --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/hosts.py @@ -0,0 +1,70 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +host interface (1.1 extension). +""" +from novaclient import base + + +class Host(base.Resource): + def __repr__(self): + return "" % self.host + + def _add_details(self, info): + dico = 'resource' in info and info['resource'] or info + for (k, v) in dico.items(): + setattr(self, k, v) + + def update(self, values): + return self.manager.update(self.host, values) + + def startup(self): + return self.manager.host_action(self.host, 'startup') + + def shutdown(self): + return self.manager.host_action(self.host, 'shutdown') + + def reboot(self): + return self.manager.host_action(self.host, 'reboot') + + +class HostManager(base.ManagerWithFind): + resource_class = Host + + def get(self, host): + """ + Describes cpu/memory/hdd info for host. + + :param host: destination host name. + """ + return self._list("/os-hosts/%s" % host, "host") + + def update(self, host, values): + """Update status or maintenance mode for the host.""" + return self._update("/os-hosts/%s" % host, values) + + def host_action(self, host, action): + """Perform an action on a host.""" + url = '/os-hosts/{0}/{1}'.format(host, action) + return self.api.client.get(url) + + def list(self, zone=None): + url = '/os-hosts' + if zone: + url = '/os-hosts?zone=%s' % zone + return self._list(url, "hosts") + + list_all = list diff --git a/awx/lib/site-packages/novaclient/v2/hypervisors.py b/awx/lib/site-packages/novaclient/v2/hypervisors.py new file mode 100644 index 0000000000..b1bfcb4b97 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/hypervisors.py @@ -0,0 +1,92 @@ +# 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. + +""" +Hypervisors interface (1.1 extension). +""" + +from six.moves.urllib import parse + +from novaclient import base + + +class Hypervisor(base.Resource): + NAME_ATTR = 'hypervisor_hostname' + + def __repr__(self): + return "" % self.id + + +class HypervisorManager(base.ManagerWithFind): + resource_class = Hypervisor + + def list(self, detailed=True): + """ + Get a list of hypervisors. + """ + detail = "" + if detailed: + detail = "/detail" + return self._list('/os-hypervisors%s' % detail, 'hypervisors') + + def search(self, hypervisor_match, servers=False): + """ + Get a list of matching hypervisors. + + :param servers: If True, server information is also retrieved. + """ + target = 'servers' if servers else 'search' + url = ('/os-hypervisors/%s/%s' % + (parse.quote(hypervisor_match, safe=''), target)) + return self._list(url, 'hypervisors') + + def get(self, hypervisor): + """ + Get a specific hypervisor. + """ + return self._get("/os-hypervisors/%s" % base.getid(hypervisor), + "hypervisor") + + def uptime(self, hypervisor): + """ + Get the uptime for a specific hypervisor. + """ + return self._get("/os-hypervisors/%s/uptime" % base.getid(hypervisor), + "hypervisor") + + def statistics(self): + """ + Get hypervisor statistics over all compute nodes. + + Kept for backwards compatibility, new code should call + hypervisor_stats.statistics() instead of hypervisors.statistics() + """ + return self.api.hypervisor_stats.statistics() + + +class HypervisorStats(base.Resource): + def __repr__(self): + return ("" % + (self.count, "s" if self.count != 1 else "")) + + +class HypervisorStatsManager(base.Manager): + resource_class = HypervisorStats + + def statistics(self): + """ + Get hypervisor statistics over all compute nodes. + """ + return self._get("/os-hypervisors/statistics", "hypervisor_statistics") diff --git a/awx/lib/site-packages/novaclient/v2/images.py b/awx/lib/site-packages/novaclient/v2/images.py new file mode 100644 index 0000000000..2d413a6bfe --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/images.py @@ -0,0 +1,101 @@ +# Copyright 2010 Jacob Kaplan-Moss +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Image interface. +""" + +from six.moves.urllib import parse + +from novaclient import base + + +class Image(base.Resource): + """ + An image is a collection of files used to create or rebuild a server. + """ + HUMAN_ID = True + + def __repr__(self): + return "" % self.name + + def delete(self): + """ + Delete this image. + """ + self.manager.delete(self) + + +class ImageManager(base.ManagerWithFind): + """ + Manage :class:`Image` resources. + """ + resource_class = Image + + def get(self, image): + """ + Get an image. + + :param image: The ID of the image to get. + :rtype: :class:`Image` + """ + return self._get("/images/%s" % base.getid(image), "image") + + def list(self, detailed=True, limit=None): + """ + Get a list of all images. + + :rtype: list of :class:`Image` + :param limit: maximum number of images to return. + """ + params = {} + detail = '' + if detailed: + detail = '/detail' + if limit: + params['limit'] = int(limit) + query = '?%s' % parse.urlencode(params) if params else '' + return self._list('/images%s%s' % (detail, query), 'images') + + def delete(self, image): + """ + Delete an image. + + It should go without saying that you can't delete an image + that you didn't create. + + :param image: The :class:`Image` (or its ID) to delete. + """ + self._delete("/images/%s" % base.getid(image)) + + def set_meta(self, image, metadata): + """ + Set an images metadata + + :param image: The :class:`Image` to add metadata to + :param metadata: A dict of metadata to add to the image + """ + body = {'metadata': metadata} + return self._create("/images/%s/metadata" % base.getid(image), + body, "metadata") + + def delete_meta(self, image, keys): + """ + Delete metadata from an image + + :param image: The :class:`Image` to delete metadata + :param keys: A list of metadata keys to delete from the image + """ + for k in keys: + self._delete("/images/%s/metadata/%s" % (base.getid(image), k)) diff --git a/awx/lib/site-packages/novaclient/v2/keypairs.py b/awx/lib/site-packages/novaclient/v2/keypairs.py new file mode 100644 index 0000000000..96caff6189 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/keypairs.py @@ -0,0 +1,92 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Keypair interface (1.1 extension). +""" + +from novaclient import base + + +class Keypair(base.Resource): + """ + A keypair is a ssh key that can be injected into a server on launch. + """ + + def __repr__(self): + return "" % self.id + + def _add_details(self, info): + dico = 'keypair' in info and \ + info['keypair'] or info + for (k, v) in dico.items(): + # NOTE(rpodolyaka): keypair name allows us to uniquely identify + # a specific keypair, while its id attribute + # is nothing more than an implementation + # detail. We can safely omit the id attribute + # here to ensure setattr() won't raise + # AttributeError trying to set read-only + # property id + if k != 'id': + setattr(self, k, v) + + @property + def id(self): + return self.name + + def delete(self): + self.manager.delete(self) + + +class KeypairManager(base.ManagerWithFind): + resource_class = Keypair + keypair_prefix = "os-keypairs" + is_alphanum_id_allowed = True + + def get(self, keypair): + """ + Get a keypair. + + :param keypair: The ID of the keypair to get. + :rtype: :class:`Keypair` + """ + return self._get("/%s/%s" % (self.keypair_prefix, base.getid(keypair)), + "keypair") + + def create(self, name, public_key=None): + """ + Create a keypair + + :param name: name for the keypair to create + :param public_key: existing public key to import + """ + body = {'keypair': {'name': name}} + if public_key: + body['keypair']['public_key'] = public_key + return self._create('/%s' % self.keypair_prefix, body, 'keypair') + + def delete(self, key): + """ + Delete a keypair + + :param key: The :class:`Keypair` (or its ID) to delete. + """ + self._delete('/%s/%s' % (self.keypair_prefix, base.getid(key))) + + def list(self): + """ + Get a list of keypairs. + """ + return self._list('/%s' % self.keypair_prefix, 'keypairs') diff --git a/awx/lib/site-packages/novaclient/v2/limits.py b/awx/lib/site-packages/novaclient/v2/limits.py new file mode 100644 index 0000000000..46d77def81 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/limits.py @@ -0,0 +1,100 @@ +# Copyright 2011 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from six.moves.urllib import parse + +from novaclient import base + + +class Limits(base.Resource): + """A collection of RateLimit and AbsoluteLimit objects.""" + + def __repr__(self): + return "" + + @property + def absolute(self): + for (name, value) in self._info['absolute'].items(): + yield AbsoluteLimit(name, value) + + @property + def rate(self): + for group in self._info['rate']: + uri = group['uri'] + regex = group['regex'] + for rate in group['limit']: + yield RateLimit(rate['verb'], uri, regex, rate['value'], + rate['remaining'], rate['unit'], + rate['next-available']) + + +class RateLimit(object): + """Data model that represents a flattened view of a single rate limit.""" + + def __init__(self, verb, uri, regex, value, remain, + unit, next_available): + self.verb = verb + self.uri = uri + self.regex = regex + self.value = value + self.remain = remain + self.unit = unit + self.next_available = next_available + + def __eq__(self, other): + return self.uri == other.uri \ + and self.regex == other.regex \ + and self.value == other.value \ + and self.verb == other.verb \ + and self.remain == other.remain \ + and self.unit == other.unit \ + and self.next_available == other.next_available + + def __repr__(self): + return "" % (self.verb, self.uri) + + +class AbsoluteLimit(object): + """Data model that represents a single absolute limit.""" + + def __init__(self, name, value): + self.name = name + self.value = value + + def __eq__(self, other): + return self.value == other.value and self.name == other.name + + def __repr__(self): + return "" % (self.name) + + +class LimitsManager(base.Manager): + """Manager object used to interact with limits resource.""" + + resource_class = Limits + + def get(self, reserved=False, tenant_id=None): + """ + Get a specific extension. + + :rtype: :class:`Limits` + """ + opts = {} + if reserved: + opts['reserved'] = 1 + if tenant_id: + opts['tenant_id'] = tenant_id + query_string = "?%s" % parse.urlencode(opts) if opts else "" + + return self._get("/limits%s" % query_string, "limits") diff --git a/awx/lib/site-packages/novaclient/v2/networks.py b/awx/lib/site-packages/novaclient/v2/networks.py new file mode 100644 index 0000000000..06dee27047 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/networks.py @@ -0,0 +1,156 @@ +# 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 interface. +""" + +from novaclient import base +from novaclient import exceptions +from novaclient.i18n import _ + + +class Network(base.Resource): + """ + A network. + """ + HUMAN_ID = True + NAME_ATTR = "label" + + def __repr__(self): + return "" % self.label + + def delete(self): + self.manager.delete(self) + + +class NetworkManager(base.ManagerWithFind): + """ + Manage :class:`Network` resources. + """ + resource_class = Network + + def list(self): + """ + Get a list of all networks. + + :rtype: list of :class:`Network`. + """ + return self._list("/os-networks", "networks") + + def get(self, network): + """ + Get a specific network. + + :param network: The ID of the :class:`Network` to get. + :rtype: :class:`Network` + """ + return self._get("/os-networks/%s" % base.getid(network), + "network") + + def delete(self, network): + """ + Delete a specific network. + + :param network: The ID of the :class:`Network` to delete. + """ + self._delete("/os-networks/%s" % base.getid(network)) + + def create(self, **kwargs): + """ + Create (allocate) a network. The following parameters are + optional except for label; cidr or cidr_v6 must be specified, too. + + :param label: str + :param bridge: str + :param bridge_interface: str + :param cidr: str + :param cidr_v6: str + :param dns1: str + :param dns2: str + :param fixed_cidr: str + :param gateway: str + :param gateway_v6: str + :param multi_host: str + :param priority: str + :param project_id: str + :param vlan: int + :param vlan_start: int + :param vpn_start: int + :param mtu: int + :param enable_dhcp: int + :param dhcp_server: str + :param share_address: int + :param allowed_start: str + :param allowed_end: str + + :rtype: object of :class:`Network` + """ + body = {"network": kwargs} + return self._create('/os-networks', body, 'network') + + def disassociate(self, network, disassociate_host=True, + disassociate_project=True): + """ + Disassociate a specific network from project and/or host. + + :param network: The ID of the :class:`Network`. + :param disassociate_host: Whether to disassociate the host + :param disassociate_project: Whether to disassociate the project + """ + if disassociate_host and disassociate_project: + body = {"disassociate": None} + elif disassociate_project: + body = {"disassociate_project": None} + elif disassociate_host: + body = {"disassociate_host": None} + else: + raise exceptions.CommandError( + _("Must disassociate either host or project or both")) + + self.api.client.post("/os-networks/%s/action" % + base.getid(network), body=body) + + def associate_host(self, network, host): + """ + Associate a specific network with a host. + + :param network: The ID of the :class:`Network`. + :param host: The name of the host to associate the network with + """ + self.api.client.post("/os-networks/%s/action" % + base.getid(network), + body={"associate_host": host}) + + def associate_project(self, network): + """ + Associate a specific network with a project. + + The project is defined by the project authenticated against + + :param network: The ID of the :class:`Network`. + """ + self.api.client.post("/os-networks/add", body={"id": network}) + + def add(self, network=None): + """ + Associates the current project with a network. Network can be chosen + automatically or provided explicitly. + + :param network: The ID of the :class:`Network` to associate (optional). + """ + self.api.client.post( + "/os-networks/add", + body={"id": base.getid(network) if network else None}) diff --git a/awx/lib/site-packages/novaclient/v2/quota_classes.py b/awx/lib/site-packages/novaclient/v2/quota_classes.py new file mode 100644 index 0000000000..4a38a970c2 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/quota_classes.py @@ -0,0 +1,44 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base + + +class QuotaClassSet(base.Resource): + + def update(self, *args, **kwargs): + return self.manager.update(self.id, *args, **kwargs) + + +class QuotaClassSetManager(base.Manager): + resource_class = QuotaClassSet + + def get(self, class_name): + return self._get("/os-quota-class-sets/%s" % (class_name), + "quota_class_set") + + def _update_body(self, **kwargs): + return {'quota_class_set': kwargs} + + def update(self, class_name, **kwargs): + body = self._update_body(**kwargs) + + for key in list(body['quota_class_set']): + if body['quota_class_set'][key] is None: + body['quota_class_set'].pop(key) + + return self._update('/os-quota-class-sets/%s' % (class_name), + body, + 'quota_class_set') diff --git a/awx/lib/site-packages/novaclient/v2/quotas.py b/awx/lib/site-packages/novaclient/v2/quotas.py new file mode 100644 index 0000000000..cd16042b44 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/quotas.py @@ -0,0 +1,72 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base + + +class QuotaSet(base.Resource): + + @property + def id(self): + """QuotaSet does not have a 'id' attribute but base.Resource needs it + to self-refresh and QuotaSet is indexed by tenant_id. + """ + return self.tenant_id + + def update(self, *args, **kwargs): + return self.manager.update(self.tenant_id, *args, **kwargs) + + +class QuotaSetManager(base.Manager): + resource_class = QuotaSet + + def get(self, tenant_id, user_id=None): + if hasattr(tenant_id, 'tenant_id'): + tenant_id = tenant_id.tenant_id + if user_id: + url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) + else: + url = '/os-quota-sets/%s' % tenant_id + return self._get(url, "quota_set") + + def _update_body(self, tenant_id, **kwargs): + kwargs['tenant_id'] = tenant_id + return {'quota_set': kwargs} + + def update(self, tenant_id, **kwargs): + + user_id = kwargs.pop('user_id', None) + body = self._update_body(tenant_id, **kwargs) + + for key in list(body['quota_set']): + if body['quota_set'][key] is None: + body['quota_set'].pop(key) + + if user_id: + url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) + else: + url = '/os-quota-sets/%s' % tenant_id + return self._update(url, body, 'quota_set') + + def defaults(self, tenant_id): + return self._get('/os-quota-sets/%s/defaults' % tenant_id, + 'quota_set') + + def delete(self, tenant_id, user_id=None): + if user_id: + url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) + else: + url = '/os-quota-sets/%s' % tenant_id + self._delete(url) diff --git a/awx/lib/site-packages/novaclient/v2/security_group_default_rules.py b/awx/lib/site-packages/novaclient/v2/security_group_default_rules.py new file mode 100644 index 0000000000..d82a1d8180 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/security_group_default_rules.py @@ -0,0 +1,81 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Security group default rules interface. +""" + +from novaclient import base +from novaclient import exceptions +from novaclient.i18n import _ + + +class SecurityGroupDefaultRule(base.Resource): + def __str__(self): + return str(self.id) + + def delete(self): + self.manager.delete(self) + + +class SecurityGroupDefaultRuleManager(base.Manager): + resource_class = SecurityGroupDefaultRule + + def create(self, ip_protocol=None, from_port=None, to_port=None, + cidr=None): + """ + Create a security group default rule + + :param ip_protocol: IP protocol, one of 'tcp', 'udp' or 'icmp' + :param from_port: Source port + :param to_port: Destination port + :param cidr: Destination IP address(es) in CIDR notation + """ + + try: + from_port = int(from_port) + except (TypeError, ValueError): + raise exceptions.CommandError(_("From port must be an integer.")) + try: + to_port = int(to_port) + except (TypeError, ValueError): + raise exceptions.CommandError(_("To port must be an integer.")) + if ip_protocol.upper() not in ['TCP', 'UDP', 'ICMP']: + raise exceptions.CommandError(_("IP protocol must be 'tcp', 'udp'" + ", or 'icmp'.")) + + body = {"security_group_default_rule": { + "ip_protocol": ip_protocol, + "from_port": from_port, + "to_port": to_port, + "cidr": cidr}} + + return self._create('/os-security-group-default-rules', body, + 'security_group_default_rule') + + def delete(self, rule): + """ + Delete a security group default rule + + :param rule: The security group default rule to delete (ID or Class) + """ + self._delete('/os-security-group-default-rules/%s' % base.getid(rule)) + + def list(self): + """ + Get a list of all security group default rules + + :rtype: list of :class:`SecurityGroupDefaultRule` + """ + + return self._list('/os-security-group-default-rules', + 'security_group_default_rules') diff --git a/awx/lib/site-packages/novaclient/v2/security_group_rules.py b/awx/lib/site-packages/novaclient/v2/security_group_rules.py new file mode 100644 index 0000000000..4cb51b01bd --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/security_group_rules.py @@ -0,0 +1,78 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Security group rules interface (1.1 extension). +""" + +from novaclient import base +from novaclient import exceptions +from novaclient.i18n import _ + + +class SecurityGroupRule(base.Resource): + def __str__(self): + return str(self.id) + + def delete(self): + self.manager.delete(self) + + +class SecurityGroupRuleManager(base.Manager): + resource_class = SecurityGroupRule + + def create(self, parent_group_id, ip_protocol=None, from_port=None, + to_port=None, cidr=None, group_id=None): + """ + Create a security group rule + + :param ip_protocol: IP protocol, one of 'tcp', 'udp' or 'icmp' + :param from_port: Source port + :param to_port: Destination port + :param cidr: Destination IP address(es) in CIDR notation + :param group_id: Security group id (int) + :param parent_group_id: Parent security group id (int) + """ + + try: + from_port = int(from_port) + except (TypeError, ValueError): + raise exceptions.CommandError(_("From port must be an integer.")) + try: + to_port = int(to_port) + except (TypeError, ValueError): + raise exceptions.CommandError(_("To port must be an integer.")) + if ip_protocol.upper() not in ['TCP', 'UDP', 'ICMP']: + raise exceptions.CommandError(_("IP protocol must be 'tcp', 'udp'" + ", or 'icmp'.")) + + body = {"security_group_rule": { + "ip_protocol": ip_protocol, + "from_port": from_port, + "to_port": to_port, + "cidr": cidr, + "group_id": group_id, + "parent_group_id": parent_group_id}} + + return self._create('/os-security-group-rules', body, + 'security_group_rule') + + def delete(self, rule): + """ + Delete a security group rule + + :param rule: The security group rule to delete (ID or Class) + """ + self._delete('/os-security-group-rules/%s' % base.getid(rule)) diff --git a/awx/lib/site-packages/novaclient/v2/security_groups.py b/awx/lib/site-packages/novaclient/v2/security_groups.py new file mode 100644 index 0000000000..40d1e7ff72 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/security_groups.py @@ -0,0 +1,96 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Security group interface (1.1 extension). +""" + +import six +from six.moves.urllib import parse + +from novaclient import base + + +class SecurityGroup(base.Resource): + def __str__(self): + return str(self.id) + + def delete(self): + self.manager.delete(self) + + def update(self): + self.manager.update(self) + + +class SecurityGroupManager(base.ManagerWithFind): + resource_class = SecurityGroup + + def create(self, name, description): + """ + Create a security group + + :param name: name for the security group to create + :param description: description of the security group + :rtype: the security group object + """ + body = {"security_group": {"name": name, 'description': description}} + return self._create('/os-security-groups', body, 'security_group') + + def update(self, group, name, description): + """ + Update a security group + + :param group: The security group to update (group or ID) + :param name: name for the security group to update + :param description: description for the security group to update + :rtype: the security group object + """ + body = {"security_group": {"name": name, 'description': description}} + return self._update('/os-security-groups/%s' % base.getid(group), + body, 'security_group') + + def delete(self, group): + """ + Delete a security group + + :param group: The security group to delete (group or ID) + :rtype: None + """ + self._delete('/os-security-groups/%s' % base.getid(group)) + + def get(self, group_id): + """ + Get a security group + + :param group_id: The security group to get by ID + :rtype: :class:`SecurityGroup` + """ + return self._get('/os-security-groups/%s' % group_id, + 'security_group') + + def list(self, search_opts=None): + """ + Get a list of all security_groups + + :rtype: list of :class:`SecurityGroup` + """ + search_opts = search_opts or {} + + qparams = dict((k, v) for (k, v) in six.iteritems(search_opts) if v) + + query_string = '?%s' % parse.urlencode(qparams) if qparams else '' + + return self._list('/os-security-groups%s' % query_string, + 'security_groups') diff --git a/awx/lib/site-packages/novaclient/v2/server_groups.py b/awx/lib/site-packages/novaclient/v2/server_groups.py new file mode 100644 index 0000000000..a5205e7726 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/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.id) + + +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/v2/servers.py b/awx/lib/site-packages/novaclient/v2/servers.py new file mode 100644 index 0000000000..397a12ac11 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/servers.py @@ -0,0 +1,1238 @@ +# Copyright 2010 Jacob Kaplan-Moss + +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Server interface. +""" + +import base64 + +from oslo.utils import encodeutils +import six +from six.moves.urllib import parse + +from novaclient import base +from novaclient import crypto +from novaclient.i18n import _ +from novaclient.v2 import security_groups + + +REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' + + +class Server(base.Resource): + HUMAN_ID = True + + def __repr__(self): + return '' % getattr(self, 'name', 'unknown-name') + + def delete(self): + """ + Delete (i.e. shut down and delete the image) this server. + """ + self.manager.delete(self) + + def update(self, name=None): + """ + Update the name for this server. + + :param name: Update the server's name. + """ + self.manager.update(self, name=name) + + def get_console_output(self, length=None): + """ + Get text console log output from Server. + + :param length: The number of lines you would like to retrieve (as int) + """ + return self.manager.get_console_output(self, length) + + def get_vnc_console(self, console_type): + """ + Get vnc console for a Server. + + :param console_type: Type of console ('novnc' or 'xvpvnc') + """ + return self.manager.get_vnc_console(self, console_type) + + def get_spice_console(self, console_type): + """ + Get spice console for a Server. + + :param console_type: Type of console ('spice-html5') + """ + return self.manager.get_spice_console(self, console_type) + + def get_rdp_console(self, console_type): + """ + Get rdp console for a Server. + + :param console_type: Type of console ('rdp-html5') + """ + return self.manager.get_rdp_console(self, console_type) + + def get_serial_console(self, console_type): + """ + Get serial console for a Server. + + :param console_type: Type of console ('serial') + """ + return self.manager.get_serial_console(self, console_type) + + def get_password(self, private_key=None): + """ + Get password for a Server. + + Returns the clear password of an instance if private_key is + provided, returns the ciphered password otherwise. + + :param private_key: Path to private key file for decryption + (optional) + """ + return self.manager.get_password(self, private_key) + + def clear_password(self): + """ + Get password for a Server. + + """ + return self.manager.clear_password(self) + + def add_fixed_ip(self, network_id): + """ + Add an IP address on a network. + + :param network_id: The ID of the network the IP should be on. + """ + self.manager.add_fixed_ip(self, network_id) + + def add_floating_ip(self, address, fixed_address=None): + """ + Add floating IP to an instance + + :param address: The IP address or FloatingIP to add to the instance + :param fixed_address: The fixedIP address the FloatingIP is to be + associated with (optional) + """ + self.manager.add_floating_ip(self, address, fixed_address) + + def remove_floating_ip(self, address): + """ + Remove floating IP from an instance + + :param address: The IP address or FloatingIP to remove + """ + self.manager.remove_floating_ip(self, address) + + def stop(self): + """ + Stop -- Stop the running server. + """ + self.manager.stop(self) + + def force_delete(self): + """ + Force delete -- Force delete a server. + """ + self.manager.force_delete(self) + + def restore(self): + """ + Restore -- Restore a server in 'soft-deleted' state. + """ + self.manager.restore(self) + + def start(self): + """ + Start -- Start the paused server. + """ + self.manager.start(self) + + def pause(self): + """ + Pause -- Pause the running server. + """ + self.manager.pause(self) + + def unpause(self): + """ + Unpause -- Unpause the paused server. + """ + self.manager.unpause(self) + + def lock(self): + """ + Lock -- Lock the instance from certain operations. + """ + self.manager.lock(self) + + def unlock(self): + """ + Unlock -- Remove instance lock. + """ + self.manager.unlock(self) + + def suspend(self): + """ + Suspend -- Suspend the running server. + """ + self.manager.suspend(self) + + def resume(self): + """ + Resume -- Resume the suspended server. + """ + self.manager.resume(self) + + def rescue(self, password=None, image=None): + """ + Rescue -- Rescue the problematic server. + + :param password: The admin password to be set in the rescue instance. + :param image: The :class:`Image` to rescue with. + """ + return self.manager.rescue(self, password, image) + + def unrescue(self): + """ + Unrescue -- Unrescue the rescued server. + """ + self.manager.unrescue(self) + + def shelve(self): + """ + Shelve -- Shelve the server. + """ + self.manager.shelve(self) + + def shelve_offload(self): + """ + Shelve_offload -- Remove a shelved server from the compute node. + """ + self.manager.shelve_offload(self) + + def unshelve(self): + """ + Unshelve -- Unshelve the server. + """ + self.manager.unshelve(self) + + def diagnostics(self): + """Diagnostics -- Retrieve server diagnostics.""" + return self.manager.diagnostics(self) + + def migrate(self): + """ + Migrate a server to a new host. + """ + self.manager.migrate(self) + + def remove_fixed_ip(self, address): + """ + Remove an IP address. + + :param address: The IP address to remove. + """ + self.manager.remove_fixed_ip(self, address) + + def change_password(self, password): + """ + Update the admin password for a server. + + :param password: string to set as the admin password on the server + """ + self.manager.change_password(self, password) + + def reboot(self, reboot_type=REBOOT_SOFT): + """ + Reboot the server. + + :param reboot_type: either :data:`REBOOT_SOFT` for a software-level + reboot, or `REBOOT_HARD` for a virtual power cycle hard reboot. + """ + self.manager.reboot(self, reboot_type) + + def rebuild(self, image, password=None, preserve_ephemeral=False, + **kwargs): + """ + Rebuild -- shut down and then re-image -- this server. + + :param image: the :class:`Image` (or its ID) to re-image with. + :param password: string to set as the admin password on the rebuilt + server. + :param preserve_ephemeral: If True, request that any ephemeral device + be preserved when rebuilding the instance. Defaults to False. + """ + return self.manager.rebuild(self, image, password=password, + preserve_ephemeral=preserve_ephemeral, + **kwargs) + + def resize(self, flavor, **kwargs): + """ + Resize the server's resources. + + :param flavor: the :class:`Flavor` (or its ID) to resize to. + + Until a resize event is confirmed with :meth:`confirm_resize`, the old + server will be kept around and you'll be able to roll back to the old + flavor quickly with :meth:`revert_resize`. All resizes are + automatically confirmed after 24 hours. + """ + self.manager.resize(self, flavor, **kwargs) + + def create_image(self, image_name, metadata=None): + """ + Create an image based on this server. + + :param image_name: The name to assign the newly create image. + :param metadata: Metadata to assign to the image. + """ + return self.manager.create_image(self, image_name, metadata) + + def backup(self, backup_name, backup_type, rotation): + """ + Backup a server instance. + + :param backup_name: Name of the backup image + :param backup_type: The backup type, like 'daily' or 'weekly' + :param rotation: Int parameter representing how many backups to + keep around. + """ + self.manager.backup(self, backup_name, backup_type, rotation) + + def confirm_resize(self): + """ + Confirm that the resize worked, thus removing the original server. + """ + self.manager.confirm_resize(self) + + def revert_resize(self): + """ + Revert a previous resize, switching back to the old server. + """ + self.manager.revert_resize(self) + + @property + def networks(self): + """ + Generate a simplified list of addresses + """ + networks = {} + try: + for network_label, address_list in self.addresses.items(): + networks[network_label] = [a['addr'] for a in address_list] + return networks + except Exception: + return {} + + def live_migrate(self, host=None, + block_migration=False, + disk_over_commit=False): + """ + Migrates a running instance to a new machine. + """ + self.manager.live_migrate(self, host, + block_migration, + disk_over_commit) + + def reset_state(self, state='error'): + """ + Reset the state of an instance to active or error. + """ + self.manager.reset_state(self, state) + + def reset_network(self): + """ + Reset network of an instance. + """ + self.manager.reset_network(self) + + def add_security_group(self, security_group): + """ + Add a security group to an instance. + """ + self.manager.add_security_group(self, security_group) + + def remove_security_group(self, security_group): + """ + Remove a security group from an instance. + """ + self.manager.remove_security_group(self, security_group) + + def list_security_group(self): + """ + List security group(s) of an instance. + """ + return self.manager.list_security_group(self) + + def evacuate(self, host=None, on_shared_storage=True, password=None): + """ + Evacuate an instance from failed host to specified host. + + :param host: Name of the target host + :param on_shared_storage: Specifies whether instance files located + on shared storage + :param password: string to set as admin password on the evacuated + server. + """ + return self.manager.evacuate(self, host, on_shared_storage, password) + + def interface_list(self): + """ + List interfaces attached to an instance. + """ + return self.manager.interface_list(self) + + def interface_attach(self, port_id, net_id, fixed_ip): + """ + Attach a network interface to an instance. + """ + return self.manager.interface_attach(self, port_id, net_id, fixed_ip) + + def interface_detach(self, port_id): + """ + Detach a network interface from an instance. + """ + return self.manager.interface_detach(self, port_id) + + +class ServerManager(base.BootingManagerWithFind): + resource_class = Server + + def _boot(self, resource_url, response_key, name, image, flavor, + meta=None, files=None, userdata=None, + reservation_id=None, return_raw=False, min_count=None, + max_count=None, security_groups=None, key_name=None, + availability_zone=None, block_device_mapping=None, + block_device_mapping_v2=None, nics=None, scheduler_hints=None, + config_drive=None, admin_pass=None, disk_config=None, **kwargs): + """ + Create (boot) a new server. + """ + body = {"server": { + "name": name, + "imageRef": str(base.getid(image)) if image else '', + "flavorRef": str(base.getid(flavor)), + }} + if userdata: + if hasattr(userdata, 'read'): + userdata = userdata.read() + + if six.PY3: + userdata = userdata.encode("utf-8") + else: + userdata = encodeutils.safe_encode(userdata) + + userdata_b64 = base64.b64encode(userdata).decode('utf-8') + body["server"]["user_data"] = userdata_b64 + if meta: + body["server"]["metadata"] = meta + if reservation_id: + body["server"]["reservation_id"] = reservation_id + if key_name: + body["server"]["key_name"] = key_name + if scheduler_hints: + body['os:scheduler_hints'] = scheduler_hints + if config_drive: + body["server"]["config_drive"] = config_drive + if admin_pass: + body["server"]["adminPass"] = admin_pass + if not min_count: + min_count = 1 + if not max_count: + max_count = min_count + body["server"]["min_count"] = min_count + body["server"]["max_count"] = max_count + + if security_groups: + body["server"]["security_groups"] = [{'name': sg} + for sg in security_groups] + + # Files are a slight bit tricky. They're passed in a "personality" + # list to the POST. Each item is a dict giving a file name and the + # base64-encoded contents of the file. We want to allow passing + # either an open file *or* some contents as files here. + if files: + personality = body['server']['personality'] = [] + for filepath, file_or_string in sorted(files.items(), + key=lambda x: x[0]): + if hasattr(file_or_string, 'read'): + data = file_or_string.read() + else: + data = file_or_string + + if six.PY3 and isinstance(data, str): + data = data.encode('utf-8') + cont = base64.b64encode(data).decode('utf-8') + personality.append({ + 'path': filepath, + 'contents': cont, + }) + + if availability_zone: + body["server"]["availability_zone"] = availability_zone + + # Block device mappings are passed as a list of dictionaries + if block_device_mapping: + body['server']['block_device_mapping'] = \ + self._parse_block_device_mapping(block_device_mapping) + elif block_device_mapping_v2: + body['server']['block_device_mapping_v2'] = block_device_mapping_v2 + + if nics is not None: + # NOTE(tr3buchet): nics can be an empty list + all_net_data = [] + for nic_info in nics: + net_data = {} + # if value is empty string, do not send value in body + if nic_info.get('net-id'): + net_data['uuid'] = nic_info['net-id'] + if (nic_info.get('v4-fixed-ip') and + nic_info.get('v6-fixed-ip')): + raise base.exceptions.CommandError(_( + "Only one of 'v4-fixed-ip' and 'v6-fixed-ip' may be" + " provided.")) + elif nic_info.get('v4-fixed-ip'): + net_data['fixed_ip'] = nic_info['v4-fixed-ip'] + elif nic_info.get('v6-fixed-ip'): + net_data['fixed_ip'] = nic_info['v6-fixed-ip'] + if nic_info.get('port-id'): + net_data['port'] = nic_info['port-id'] + all_net_data.append(net_data) + body['server']['networks'] = all_net_data + + if disk_config is not None: + body['server']['OS-DCF:diskConfig'] = disk_config + + return self._create(resource_url, body, response_key, + return_raw=return_raw, **kwargs) + + def get(self, server): + """ + Get a server. + + :param server: ID of the :class:`Server` to get. + :rtype: :class:`Server` + """ + return self._get("/servers/%s" % base.getid(server), "server") + + def list(self, detailed=True, search_opts=None, marker=None, limit=None, + sort_keys=None, sort_dirs=None): + """ + Get a list of servers. + + :param detailed: Whether to return detailed server info (optional). + :param search_opts: Search options to filter out servers (optional). + :param marker: Begin returning servers that appear later in the server + list than that represented by this server id (optional). + :param limit: Maximum number of servers to return (optional). + :param sort_keys: List of sort keys + :param sort_dirs: List of sort directions + + :rtype: list of :class:`Server` + """ + if search_opts is None: + search_opts = {} + + qparams = {} + + for opt, val in six.iteritems(search_opts): + if val: + qparams[opt] = val + + if marker: + qparams['marker'] = marker + + if limit: + qparams['limit'] = limit + + # Transform the dict to a sequence of two-element tuples in fixed + # order, then the encoded string will be consistent in Python 2&3. + if qparams or sort_keys or sort_dirs: + # sort keys and directions are unique since the same parameter + # key is repeated for each associated value + # (ie, &sort_key=key1&sort_key=key2&sort_key=key3) + items = list(qparams.items()) + if sort_keys: + items.extend(('sort_key', sort_key) for sort_key in sort_keys) + if sort_dirs: + items.extend(('sort_dir', sort_dir) for sort_dir in sort_dirs) + new_qparams = sorted(items, key=lambda x: x[0]) + query_string = "?%s" % parse.urlencode(new_qparams) + else: + query_string = "" + + detail = "" + if detailed: + detail = "/detail" + return self._list("/servers%s%s" % (detail, query_string), "servers") + + def add_fixed_ip(self, server, network_id): + """ + Add an IP address on a network. + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param network_id: The ID of the network the IP should be on. + """ + self._action('addFixedIp', server, {'networkId': network_id}) + + def remove_fixed_ip(self, server, address): + """ + Remove an IP address. + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param address: The IP address to remove. + """ + self._action('removeFixedIp', server, {'address': address}) + + def add_floating_ip(self, server, address, fixed_address=None): + """ + Add a floating IP to an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param address: The FloatingIP or string floating address to add. + :param fixed_address: The FixedIP the floatingIP should be + associated with (optional) + """ + + address = address.ip if hasattr(address, 'ip') else address + if fixed_address: + if hasattr(fixed_address, 'ip'): + fixed_address = fixed_address.ip + self._action('addFloatingIp', server, + {'address': address, 'fixed_address': fixed_address}) + else: + self._action('addFloatingIp', server, {'address': address}) + + def remove_floating_ip(self, server, address): + """ + Remove a floating IP address. + + :param server: The :class:`Server` (or its ID) to remove an IP from. + :param address: The FloatingIP or string floating address to remove. + """ + + address = address.ip if hasattr(address, 'ip') else address + self._action('removeFloatingIp', server, {'address': address}) + + def get_vnc_console(self, server, console_type): + """ + Get a vnc console for an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param console_type: Type of vnc console to get ('novnc' or 'xvpvnc') + """ + + return self._action('os-getVNCConsole', server, + {'type': console_type})[1] + + def get_spice_console(self, server, console_type): + """ + Get a spice console for an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param console_type: Type of spice console to get ('spice-html5') + """ + + return self._action('os-getSPICEConsole', server, + {'type': console_type})[1] + + def get_rdp_console(self, server, console_type): + """ + Get a rdp console for an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param console_type: Type of rdp console to get ('rdp-html5') + """ + + return self._action('os-getRDPConsole', server, + {'type': console_type})[1] + + def get_serial_console(self, server, console_type): + """ + Get a serial console for an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param console_type: Type of serial console to get ('serial') + """ + + return self._action('os-getSerialConsole', server, + {'type': console_type})[1] + + def get_password(self, server, private_key=None): + """ + Get admin password of an instance + + Returns the admin password of an instance in the clear if private_key + is provided, returns the ciphered password otherwise. + + Requires that openssl is installed and in the path + + :param server: The :class:`Server` (or its ID) for which the admin + password is to be returned + :param private_key: The private key to decrypt password + (optional) + """ + + _resp, body = self.api.client.get("/servers/%s/os-server-password" + % base.getid(server)) + ciphered_pw = body.get('password', '') if body else '' + if private_key and ciphered_pw: + try: + return crypto.decrypt_password(private_key, ciphered_pw) + except Exception as exc: + return '%sFailed to decrypt:\n%s' % (exc, ciphered_pw) + return ciphered_pw + + def clear_password(self, server): + """ + Clear the admin password of an instance + + Remove the admin password for an instance from the metadata server. + + :param server: The :class:`Server` (or its ID) for which the admin + password is to be cleared + """ + + return self._delete("/servers/%s/os-server-password" + % base.getid(server)) + + def stop(self, server): + """ + Stop the server. + """ + return self._action('os-stop', server, None) + + def force_delete(self, server): + """ + Force delete the server. + """ + return self._action('forceDelete', server, None) + + def restore(self, server): + """ + Restore soft-deleted server. + """ + return self._action('restore', server, None) + + def start(self, server): + """ + Start the server. + """ + self._action('os-start', server, None) + + def pause(self, server): + """ + Pause the server. + """ + self._action('pause', server, None) + + def unpause(self, server): + """ + Unpause the server. + """ + self._action('unpause', server, None) + + def lock(self, server): + """ + Lock the server. + """ + self._action('lock', server, None) + + def unlock(self, server): + """ + Unlock the server. + """ + self._action('unlock', server, None) + + def suspend(self, server): + """ + Suspend the server. + """ + self._action('suspend', server, None) + + def resume(self, server): + """ + Resume the server. + """ + self._action('resume', server, None) + + def rescue(self, server, password=None, image=None): + """ + Rescue the server. + + :param server: The :class:`Server` to rescue. + :param password: The admin password to be set in the rescue instance. + :param image: The :class:`Image` to rescue with. + """ + info = {} + if password: + info['adminPass'] = password + if image: + info['rescue_image_ref'] = base.getid(image) + return self._action('rescue', server, info or None) + + def unrescue(self, server): + """ + Unrescue the server. + """ + self._action('unrescue', server, None) + + def shelve(self, server): + """ + Shelve the server. + """ + self._action('shelve', server, None) + + def shelve_offload(self, server): + """ + Remove a shelved instance from the compute node. + """ + self._action('shelveOffload', server, None) + + def unshelve(self, server): + """ + Unshelve the server. + """ + self._action('unshelve', server, None) + + def diagnostics(self, server): + """Retrieve server diagnostics.""" + return self.api.client.get("/servers/%s/diagnostics" % + base.getid(server)) + + def create(self, name, image, flavor, meta=None, files=None, + reservation_id=None, min_count=None, + max_count=None, security_groups=None, userdata=None, + key_name=None, availability_zone=None, + block_device_mapping=None, block_device_mapping_v2=None, + nics=None, scheduler_hints=None, + config_drive=None, disk_config=None, **kwargs): + # TODO(anthony): indicate in doc string if param is an extension + # and/or optional + """ + Create (boot) a new server. + + :param name: Something to name the server. + :param image: The :class:`Image` to boot with. + :param flavor: The :class:`Flavor` to boot onto. + :param meta: A dict of arbitrary key/value metadata to store for this + server. A maximum of five entries is allowed, and both + keys and values must be 255 characters or less. + :param files: A dict of files to overrwrite on the server upon boot. + Keys are file names (i.e. ``/etc/passwd``) and values + are the file contents (either as a string or as a + file-like object). A maximum of five entries is allowed, + and each file must be 10k or less. + :param reservation_id: a UUID for the set of servers being requested. + :param min_count: (optional extension) The minimum number of + servers to launch. + :param max_count: (optional extension) The maximum number of + servers to launch. + :param security_groups: A list of security group names + :param userdata: user data to pass to be exposed by the metadata + server this can be a file type object as well or a + string. + :param key_name: (optional extension) name of previously created + keypair to inject into the instance. + :param availability_zone: Name of the availability zone for instance + placement. + :param block_device_mapping: (optional extension) A dict of block + device mappings for this server. + :param block_device_mapping_v2: (optional extension) A dict of block + device mappings for this server. + :param nics: (optional extension) an ordered list of nics to be + added to this server, with information about + connected networks, fixed IPs, port etc. + :param scheduler_hints: (optional extension) arbitrary key-value pairs + specified by the client to help boot an instance + :param config_drive: (optional extension) value for config drive + either boolean, or volume-id + :param disk_config: (optional extension) control how the disk is + partitioned when the server is created. possible + values are 'AUTO' or 'MANUAL'. + """ + if not min_count: + min_count = 1 + if not max_count: + max_count = min_count + if min_count > max_count: + min_count = max_count + + boot_args = [name, image, flavor] + + boot_kwargs = dict( + meta=meta, files=files, userdata=userdata, + reservation_id=reservation_id, min_count=min_count, + max_count=max_count, security_groups=security_groups, + key_name=key_name, availability_zone=availability_zone, + scheduler_hints=scheduler_hints, config_drive=config_drive, + disk_config=disk_config, **kwargs) + + if block_device_mapping: + resource_url = "/os-volumes_boot" + boot_kwargs['block_device_mapping'] = block_device_mapping + elif block_device_mapping_v2: + resource_url = "/os-volumes_boot" + boot_kwargs['block_device_mapping_v2'] = block_device_mapping_v2 + else: + resource_url = "/servers" + if nics: + boot_kwargs['nics'] = nics + + response_key = "server" + return self._boot(resource_url, response_key, *boot_args, + **boot_kwargs) + + def update(self, server, name=None): + """ + Update the name or the password for a server. + + :param server: The :class:`Server` (or its ID) to update. + :param name: Update the server's name. + """ + if name is None: + return + + body = { + "server": { + "name": name, + }, + } + + return self._update("/servers/%s" % base.getid(server), body, "server") + + def change_password(self, server, password): + """ + Update the password for a server. + """ + self._action("changePassword", server, {"adminPass": password}) + + def delete(self, server): + """ + Delete (i.e. shut down and delete the image) this server. + """ + self._delete("/servers/%s" % base.getid(server)) + + def reboot(self, server, reboot_type=REBOOT_SOFT): + """ + Reboot a server. + + :param server: The :class:`Server` (or its ID) to share onto. + :param reboot_type: either :data:`REBOOT_SOFT` for a software-level + reboot, or `REBOOT_HARD` for a virtual power cycle hard reboot. + """ + self._action('reboot', server, {'type': reboot_type}) + + def rebuild(self, server, image, password=None, disk_config=None, + preserve_ephemeral=False, name=None, meta=None, files=None, + **kwargs): + """ + Rebuild -- shut down and then re-image -- a server. + + :param server: The :class:`Server` (or its ID) to share onto. + :param image: the :class:`Image` (or its ID) to re-image with. + :param password: string to set as password on the rebuilt server. + :param disk_config: partitioning mode to use on the rebuilt server. + Valid values are 'AUTO' or 'MANUAL' + :param preserve_ephemeral: If True, request that any ephemeral device + be preserved when rebuilding the instance. Defaults to False. + :param name: Something to name the server. + :param meta: A dict of arbitrary key/value metadata to store for this + server. A maximum of five entries is allowed, and both + keys and values must be 255 characters or less. + :param files: A dict of files to overwrite on the server upon boot. + Keys are file names (i.e. ``/etc/passwd``) and values + are the file contents (either as a string or as a + file-like object). A maximum of five entries is allowed, + and each file must be 10k or less. + """ + body = {'imageRef': base.getid(image)} + if password is not None: + body['adminPass'] = password + if disk_config is not None: + body['OS-DCF:diskConfig'] = disk_config + if preserve_ephemeral is not False: + body['preserve_ephemeral'] = True + if name is not None: + body['name'] = name + if meta: + body['metadata'] = meta + if files: + personality = body['personality'] = [] + for filepath, file_or_string in sorted(files.items(), + key=lambda x: x[0]): + if hasattr(file_or_string, 'read'): + data = file_or_string.read() + else: + data = file_or_string + + cont = base64.b64encode(data.encode('utf-8')).decode('utf-8') + personality.append({ + 'path': filepath, + 'contents': cont, + }) + + _resp, body = self._action('rebuild', server, body, **kwargs) + return Server(self, body['server']) + + def migrate(self, server): + """ + Migrate a server to a new host. + + :param server: The :class:`Server` (or its ID). + """ + self._action('migrate', server) + + def resize(self, server, flavor, disk_config=None, **kwargs): + """ + Resize a server's resources. + + :param server: The :class:`Server` (or its ID) to share onto. + :param flavor: the :class:`Flavor` (or its ID) to resize to. + :param disk_config: partitioning mode to use on the rebuilt server. + Valid values are 'AUTO' or 'MANUAL' + + Until a resize event is confirmed with :meth:`confirm_resize`, the old + server will be kept around and you'll be able to roll back to the old + flavor quickly with :meth:`revert_resize`. All resizes are + automatically confirmed after 24 hours. + """ + info = {'flavorRef': base.getid(flavor)} + if disk_config is not None: + info['OS-DCF:diskConfig'] = disk_config + + self._action('resize', server, info=info, **kwargs) + + def confirm_resize(self, server): + """ + Confirm that the resize worked, thus removing the original server. + + :param server: The :class:`Server` (or its ID) to share onto. + """ + self._action('confirmResize', server) + + def revert_resize(self, server): + """ + Revert a previous resize, switching back to the old server. + + :param server: The :class:`Server` (or its ID) to share onto. + """ + self._action('revertResize', server) + + def create_image(self, server, image_name, metadata=None): + """ + Snapshot a server. + + :param server: The :class:`Server` (or its ID) to share onto. + :param image_name: Name to give the snapshot image + :param metadata: Metadata to give newly-created image entity + """ + body = {'name': image_name, 'metadata': metadata or {}} + resp = self._action('createImage', server, body)[0] + location = resp.headers['location'] + image_uuid = location.split('/')[-1] + return image_uuid + + def backup(self, server, backup_name, backup_type, rotation): + """ + Backup a server instance. + + :param server: The :class:`Server` (or its ID) to share onto. + :param backup_name: Name of the backup image + :param backup_type: The backup type, like 'daily' or 'weekly' + :param rotation: Int parameter representing how many backups to + keep around. + """ + body = {'name': backup_name, + 'backup_type': backup_type, + 'rotation': rotation} + self._action('createBackup', server, body) + + def set_meta(self, server, metadata): + """ + Set a servers metadata + :param server: The :class:`Server` to add metadata to + :param metadata: A dict of metadata to add to the server + """ + body = {'metadata': metadata} + return self._create("/servers/%s/metadata" % base.getid(server), + body, "metadata") + + def set_meta_item(self, server, key, value): + """ + Updates an item of server metadata + :param server: The :class:`Server` to add metadata to + :param key: metadata key to update + :param value: string value + """ + body = {'meta': {key: value}} + return self._update("/servers/%s/metadata/%s" % + (base.getid(server), key), body) + + def get_console_output(self, server, length=None): + """ + Get text console log output from Server. + + :param server: The :class:`Server` (or its ID) whose console output + you would like to retrieve. + :param length: The number of tail loglines you would like to retrieve. + """ + return self._action('os-getConsoleOutput', + server, + {'length': length})[1]['output'] + + def delete_meta(self, server, keys): + """ + Delete metadata from an server + :param server: The :class:`Server` to add metadata to + :param keys: A list of metadata keys to delete from the server + """ + for k in keys: + self._delete("/servers/%s/metadata/%s" % (base.getid(server), k)) + + def live_migrate(self, server, host, block_migration, disk_over_commit): + """ + Migrates a running instance to a new machine. + + :param server: instance id which comes from nova list. + :param host: destination host name. + :param block_migration: if True, do block_migration. + :param disk_over_commit: if True, Allow overcommit. + + """ + self._action('os-migrateLive', server, + {'host': host, + 'block_migration': block_migration, + 'disk_over_commit': disk_over_commit}) + + def reset_state(self, server, state='error'): + """ + Reset the state of an instance to active or error. + + :param server: ID of the instance to reset the state of. + :param state: Desired state; either 'active' or 'error'. + Defaults to 'error'. + """ + self._action('os-resetState', server, dict(state=state)) + + def reset_network(self, server): + """ + Reset network of an instance. + """ + self._action('resetNetwork', server) + + def add_security_group(self, server, security_group): + """ + Add a Security Group to an instance + + :param server: ID of the instance. + :param security_group: The name of security group to add. + + """ + self._action('addSecurityGroup', server, {'name': security_group}) + + def remove_security_group(self, server, security_group): + """ + Add a Security Group to an instance + + :param server: ID of the instance. + :param security_group: The name of security group to remove. + + """ + self._action('removeSecurityGroup', server, {'name': security_group}) + + def list_security_group(self, server): + """ + List Security Group(s) of an instance + + :param server: ID of the instance. + + """ + return self._list('/servers/%s/os-security-groups' % + base.getid(server), 'security_groups', + security_groups.SecurityGroup) + + def evacuate(self, server, host=None, on_shared_storage=True, + password=None): + """ + Evacuate a server instance. + + :param server: The :class:`Server` (or its ID) to share onto. + :param host: Name of the target host. + :param on_shared_storage: Specifies whether instance files located + on shared storage + :param password: string to set as password on the evacuated server. + """ + + body = {'onSharedStorage': on_shared_storage} + if host is not None: + body['host'] = host + + if password is not None: + body['adminPass'] = password + + return self._action('evacuate', server, body) + + def interface_list(self, server): + """ + List attached network interfaces + + :param server: The :class:`Server` (or its ID) to query. + """ + return self._list('/servers/%s/os-interface' % base.getid(server), + 'interfaceAttachments') + + def interface_attach(self, server, port_id, net_id, fixed_ip): + """ + Attach a network_interface to an instance. + + :param server: The :class:`Server` (or its ID) to attach to. + :param port_id: The port to attach. + """ + + body = {'interfaceAttachment': {}} + if port_id: + body['interfaceAttachment']['port_id'] = port_id + if net_id: + body['interfaceAttachment']['net_id'] = net_id + if fixed_ip: + body['interfaceAttachment']['fixed_ips'] = [ + {'ip_address': fixed_ip}] + + return self._create('/servers/%s/os-interface' % base.getid(server), + body, 'interfaceAttachment') + + def interface_detach(self, server, port_id): + """ + Detach a network_interface from an instance. + + :param server: The :class:`Server` (or its ID) to detach from. + :param port_id: The port to detach. + """ + self._delete('/servers/%s/os-interface/%s' % (base.getid(server), + port_id)) + + def _action(self, action, server, info=None, **kwargs): + """ + Perform a server "action" -- reboot/rebuild/resize/etc. + """ + body = {action: info} + self.run_hooks('modify_body_for_action', body, **kwargs) + url = '/servers/%s/action' % base.getid(server) + return self.api.client.post(url, body=body) diff --git a/awx/lib/site-packages/novaclient/v2/services.py b/awx/lib/site-packages/novaclient/v2/services.py new file mode 100644 index 0000000000..d51fa3ebfd --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/services.py @@ -0,0 +1,75 @@ +# Copyright 2012 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +service interface +""" +from novaclient import base + + +class Service(base.Resource): + def __repr__(self): + return "" % self.binary + + def _add_details(self, info): + dico = 'resource' in info and info['resource'] or info + for (k, v) in dico.items(): + setattr(self, k, v) + + +class ServiceManager(base.ManagerWithFind): + resource_class = Service + + def list(self, host=None, binary=None): + """ + Describes cpu/memory/hdd info for host. + + :param host: destination host name. + """ + url = "/os-services" + filters = [] + if host: + filters.append("host=%s" % host) + if binary: + filters.append("binary=%s" % binary) + if filters: + url = "%s?%s" % (url, "&".join(filters)) + return self._list(url, "services") + + def _update_body(self, host, binary, disabled_reason=None): + body = {"host": host, + "binary": binary} + if disabled_reason is not None: + body["disabled_reason"] = disabled_reason + return body + + def enable(self, host, binary): + """Enable the service specified by hostname and binary.""" + body = self._update_body(host, binary) + return self._update("/os-services/enable", body, "service") + + def disable(self, host, binary): + """Disable the service specified by hostname and binary.""" + body = self._update_body(host, binary) + return self._update("/os-services/disable", body, "service") + + def disable_log_reason(self, host, binary, reason): + """Disable the service with reason.""" + body = 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/v2/shell.py b/awx/lib/site-packages/novaclient/v2/shell.py new file mode 100644 index 0000000000..7e3a531001 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/shell.py @@ -0,0 +1,4458 @@ +# Copyright 2010 Jacob Kaplan-Moss + +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import print_function + +import argparse +import copy +import datetime +import getpass +import locale +import logging +import os +import sys +import time + +from oslo.utils import encodeutils +from oslo.utils import strutils +from oslo.utils import timeutils +import six + +from novaclient import client +from novaclient import exceptions +from novaclient.i18n import _ +from novaclient.openstack.common import cliutils +from novaclient.openstack.common import uuidutils +from novaclient import utils +from novaclient.v2 import availability_zones +from novaclient.v2 import quotas +from novaclient.v2 import servers + + +logger = logging.getLogger(__name__) + + +CLIENT_BDM2_KEYS = { + 'id': 'uuid', + 'source': 'source_type', + 'dest': 'destination_type', + 'bus': 'disk_bus', + 'device': 'device_name', + 'size': 'volume_size', + 'format': 'guest_format', + 'bootindex': 'boot_index', + 'type': 'device_type', + 'shutdown': 'delete_on_termination', +} + + +def _key_value_pairing(text): + try: + (k, v) = text.split('=', 1) + return (k, v) + except ValueError: + msg = "%r is not in the format of key=value" % text + raise argparse.ArgumentTypeError(msg) + + +def _match_image(cs, wanted_properties): + image_list = cs.images.list() + images_matched = [] + match = set(wanted_properties) + for img in image_list: + try: + if match == match.intersection(set(img.metadata.items())): + images_matched.append(img) + except AttributeError: + pass + return images_matched + + +def _parse_block_device_mapping_v2(args, image): + bdm = [] + + if args.boot_volume: + bdm_dict = {'uuid': args.boot_volume, 'source_type': 'volume', + 'destination_type': 'volume', 'boot_index': 0, + 'delete_on_termination': False} + bdm.append(bdm_dict) + + if args.snapshot: + bdm_dict = {'uuid': args.snapshot, 'source_type': 'snapshot', + 'destination_type': 'volume', 'boot_index': 0, + 'delete_on_termination': False} + bdm.append(bdm_dict) + + for device_spec in args.block_device: + spec_dict = dict(v.split('=') for v in device_spec.split(',')) + bdm_dict = {} + + for key, value in six.iteritems(spec_dict): + bdm_dict[CLIENT_BDM2_KEYS[key]] = value + + # Convert the delete_on_termination to a boolean or set it to true by + # default for local block devices when not specified. + if 'delete_on_termination' in bdm_dict: + action = bdm_dict['delete_on_termination'] + bdm_dict['delete_on_termination'] = (action == 'remove') + elif bdm_dict.get('destination_type') == 'local': + bdm_dict['delete_on_termination'] = True + + bdm.append(bdm_dict) + + for ephemeral_spec in args.ephemeral: + bdm_dict = {'source_type': 'blank', 'destination_type': 'local', + 'boot_index': -1, 'delete_on_termination': True} + + eph_dict = dict(v.split('=') for v in ephemeral_spec.split(',')) + if 'size' in eph_dict: + bdm_dict['volume_size'] = eph_dict['size'] + if 'format' in eph_dict: + bdm_dict['guest_format'] = eph_dict['format'] + + bdm.append(bdm_dict) + + if args.swap: + bdm_dict = {'source_type': 'blank', 'destination_type': 'local', + 'boot_index': -1, 'delete_on_termination': True, + 'guest_format': 'swap', 'volume_size': args.swap} + bdm.append(bdm_dict) + + return bdm + + +def _boot(cs, args): + """Boot a new server.""" + if args.image: + image = _find_image(cs, args.image) + else: + image = None + + if not image and args.image_with: + images = _match_image(cs, args.image_with) + if images: + # TODO(harlowja): log a warning that we + # are selecting the first of many? + image = images[0] + + if not args.flavor: + raise exceptions.CommandError(_("you need to specify a Flavor ID ")) + + min_count = 1 + max_count = 1 + # Don't let user mix num_instances and max_count/min_count. + if (args.num_instances is not None and + args.min_count is None and + args.max_count is None): + if args.num_instances < 1: + raise exceptions.CommandError(_("num_instances should be >= 1")) + max_count = args.num_instances + elif (args.num_instances is not None and + (args.min_count is not None or args.max_count is not None)): + raise exceptions.CommandError(_("Don't mix num-instances and " + "max/min-count")) + if args.min_count is not None: + if args.min_count < 1: + raise exceptions.CommandError(_("min_count should be >= 1")) + min_count = args.min_count + max_count = min_count + if args.max_count is not None: + if args.max_count < 1: + raise exceptions.CommandError(_("max_count should be >= 1")) + max_count = args.max_count + if (args.min_count is not None and + args.max_count is not None and + args.min_count > args.max_count): + raise exceptions.CommandError(_("min_count should be <= max_count")) + + flavor = _find_flavor(cs, args.flavor) + + meta = dict(v.split('=', 1) for v in args.meta) + + files = {} + for f in args.files: + try: + dst, src = f.split('=', 1) + files[dst] = open(src) + except IOError as e: + raise exceptions.CommandError(_("Can't open '%(src)s': %(exc)s") % + {'src': src, 'exc': e}) + except ValueError: + raise exceptions.CommandError(_("Invalid file argument '%s'. " + "File arguments must be of the " + "form '--file " + "'") % f) + + # use the os-keypair extension + key_name = None + if args.key_name is not None: + key_name = args.key_name + + if args.user_data: + try: + userdata = open(args.user_data) + except IOError as e: + raise exceptions.CommandError(_("Can't open '%(user_data)s': " + "%(exc)s") % + {'user_data': args.user_data, + 'exc': e}) + else: + userdata = None + + if args.availability_zone: + availability_zone = args.availability_zone + else: + availability_zone = None + + if args.security_groups: + security_groups = args.security_groups.split(',') + else: + security_groups = None + + block_device_mapping = {} + for bdm in args.block_device_mapping: + device_name, mapping = bdm.split('=', 1) + block_device_mapping[device_name] = mapping + + block_device_mapping_v2 = _parse_block_device_mapping_v2(args, image) + + n_boot_args = len(list(filter( + bool, (image, args.boot_volume, args.snapshot)))) + have_bdm = block_device_mapping_v2 or block_device_mapping + + # Fail if more than one boot devices are present + # or if there is no device to boot from. + if n_boot_args > 1 or n_boot_args == 0 and not have_bdm: + raise exceptions.CommandError( + _("you need to specify at least one source ID (Image, Snapshot, " + "or Volume), a block device mapping or provide a set of " + "properties to match against an image")) + + if block_device_mapping and block_device_mapping_v2: + raise exceptions.CommandError( + _("you can't mix old block devices (--block-device-mapping) " + "with the new ones (--block-device, --boot-volume, --snapshot, " + "--ephemeral, --swap)")) + + nics = [] + for nic_str in args.nics: + err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " + "the form --nic , with at minimum " + "net-id or port-id (but not both) specified.") % nic_str) + nic_info = {"net-id": "", "v4-fixed-ip": "", "v6-fixed-ip": "", + "port-id": ""} + + for kv_str in nic_str.split(","): + try: + k, v = kv_str.split("=", 1) + except ValueError: + raise exceptions.CommandError(err_msg) + + if k in nic_info: + nic_info[k] = v + else: + raise exceptions.CommandError(err_msg) + + if bool(nic_info['net-id']) == bool(nic_info['port-id']): + raise exceptions.CommandError(err_msg) + + nics.append(nic_info) + + hints = {} + if args.scheduler_hints: + for hint in args.scheduler_hints: + key, _sep, value = hint.partition('=') + # NOTE(vish): multiple copies of the same hint will + # result in a list of values + if key in hints: + if isinstance(hints[key], six.string_types): + hints[key] = [hints[key]] + hints[key] += [value] + else: + hints[key] = value + boot_args = [args.name, image, flavor] + + if str(args.config_drive).lower() in ("true", "1"): + config_drive = True + elif str(args.config_drive).lower() in ("false", "0", "", "none"): + config_drive = None + else: + config_drive = args.config_drive + + boot_kwargs = dict( + meta=meta, + files=files, + key_name=key_name, + min_count=min_count, + max_count=max_count, + userdata=userdata, + availability_zone=availability_zone, + security_groups=security_groups, + block_device_mapping=block_device_mapping, + block_device_mapping_v2=block_device_mapping_v2, + nics=nics, + scheduler_hints=hints, + config_drive=config_drive) + + return boot_args, boot_kwargs + + +@cliutils.arg( + '--flavor', + default=None, + metavar='', + help=_("Name or ID of flavor (see 'nova flavor-list').")) +@cliutils.arg( + '--image', + default=None, + metavar='', + help=_("Name or ID of image (see 'nova image-list'). ")) +@cliutils.arg( + '--image-with', + default=[], + type=_key_value_pairing, + action='append', + metavar='', + help=_("Image metadata property (see 'nova image-show'). ")) +@cliutils.arg( + '--boot-volume', + default=None, + metavar="", + help=_("Volume ID to boot from.")) +@cliutils.arg( + '--snapshot', + default=None, + metavar="", + help=_("Snapshot ID to boot from (will create a volume).")) +@cliutils.arg( + '--num-instances', + default=None, + type=int, + metavar='', + help=argparse.SUPPRESS) +@cliutils.arg( + '--min-count', + default=None, + type=int, + metavar='', + help=_("Boot at least servers (limited by quota).")) +@cliutils.arg( + '--max-count', + default=None, + type=int, + metavar='', + help=_("Boot up to servers (limited by quota).")) +@cliutils.arg( + '--meta', + metavar="", + action='append', + default=[], + help=_("Record arbitrary key/value metadata to /meta_data.json " + "on the metadata server. Can be specified multiple times.")) +@cliutils.arg( + '--file', + metavar="", + action='append', + dest='files', + default=[], + help=_("Store arbitrary files from locally to " + "on the new server. Limited by the injected_files quota value.")) +@cliutils.arg( + '--key-name', + default=os.environ.get('NOVACLIENT_DEFAULT_KEY_NAME'), + metavar='', + help=_("Key name of keypair that should be created earlier with \ + the command keypair-add")) +@cliutils.arg( + '--key_name', + help=argparse.SUPPRESS) +@cliutils.arg('name', metavar='', help=_('Name for the new server')) +@cliutils.arg( + '--user-data', + default=None, + metavar='', + help=_("user data file to pass to be exposed by the metadata server.")) +@cliutils.arg( + '--user_data', + help=argparse.SUPPRESS) +@cliutils.arg( + '--availability-zone', + default=None, + metavar='', + help=_("The availability zone for server placement.")) +@cliutils.arg( + '--availability_zone', + help=argparse.SUPPRESS) +@cliutils.arg( + '--security-groups', + default=None, + metavar='', + help=_("Comma separated list of security group names.")) +@cliutils.arg( + '--security_groups', + help=argparse.SUPPRESS) +@cliutils.arg( + '--block-device-mapping', + metavar="", + action='append', + default=[], + help=_("Block device mapping in the format " + "=:::.")) +@cliutils.arg( + '--block_device_mapping', + action='append', + help=argparse.SUPPRESS) +@cliutils.arg( + '--block-device', + metavar="key1=value1[,key2=value2...]", + action='append', + default=[], + help=_("Block device mapping with the keys: " + "id=UUID (image_id, snapshot_id or volume_id only if using source " + "image, snapshot or volume) " + "source=source type (image, snapshot, volume or blank), " + "dest=destination type of the block device (volume or local), " + "bus=device's bus (e.g. uml, lxc, virtio, ...; if omitted, " + "hypervisor driver chooses a suitable default, " + "honoured only if device type is supplied) " + "type=device type (e.g. disk, cdrom, ...; defaults to 'disk') " + "device=name of the device (e.g. vda, xda, ...; " + "if omitted, hypervisor driver chooses suitable device " + "depending on selected bus), " + "size=size of the block device in GB (if omitted, " + "hypervisor driver calculates size), " + "format=device will be formatted (e.g. swap, ntfs, ...; optional), " + "bootindex=integer used for ordering the boot disks " + "(for image backed instances it is equal to 0, " + "for others need to be specified) and " + "shutdown=shutdown behaviour (either preserve or remove, " + "for local destination set to remove).")) +@cliutils.arg( + '--swap', + metavar="", + default=None, + help=_("Create and attach a local swap block device of MB.")) +@cliutils.arg( + '--ephemeral', + metavar="size=[,format=]", + action='append', + default=[], + help=_("Create and attach a local ephemeral block device of GB " + "and format it to .")) +@cliutils.arg( + '--hint', + action='append', + dest='scheduler_hints', + default=[], + metavar='', + help=_("Send arbitrary key/value pairs to the scheduler for custom " + "use.")) +@cliutils.arg( + '--nic', + metavar="", + action='append', + dest='nics', + default=[], + help=_("Create a NIC on the server. " + "Specify option multiple times to create multiple NICs. " + "net-id: attach NIC to network with this UUID " + "(either port-id or net-id must be provided), " + "v4-fixed-ip: IPv4 fixed address for NIC (optional), " + "v6-fixed-ip: IPv6 fixed address for NIC (optional), " + "port-id: attach NIC to port with this UUID " + "(either port-id or net-id must be provided).")) +@cliutils.arg( + '--config-drive', + metavar="", + dest='config_drive', + default=False, + help=_("Enable config drive")) +@cliutils.arg( + '--poll', + dest='poll', + action="store_true", + default=False, + help=_('Report the new server boot progress until it completes.')) +def do_boot(cs, args): + """Boot a new server.""" + boot_args, boot_kwargs = _boot(cs, args) + + extra_boot_kwargs = utils.get_resource_manager_extra_kwargs(do_boot, args) + boot_kwargs.update(extra_boot_kwargs) + + server = cs.servers.create(*boot_args, **boot_kwargs) + _print_server(cs, args, server) + + if args.poll: + _poll_for_status(cs.servers.get, server.id, 'building', ['active']) + + +def do_cloudpipe_list(cs, _args): + """Print a list of all cloudpipe instances.""" + cloudpipes = cs.cloudpipe.list() + columns = ['Project Id', "Public IP", "Public Port", "Internal IP"] + utils.print_list(cloudpipes, columns) + + +@cliutils.arg( + 'project', + metavar='', + help=_('UUID of the project to create the cloudpipe for.')) +def do_cloudpipe_create(cs, args): + """Create a cloudpipe instance for the given project.""" + cs.cloudpipe.create(args.project) + + +@cliutils.arg('address', metavar='', help=_('New IP Address.')) +@cliutils.arg('port', metavar='', help='New Port.') +def do_cloudpipe_configure(cs, args): + """Update the VPN IP/port of a cloudpipe instance.""" + cs.cloudpipe.update(args.address, args.port) + + +def _poll_for_status(poll_fn, obj_id, action, final_ok_states, + poll_period=5, show_progress=True, + status_field="status", silent=False): + """Block while an action is being performed, periodically printing + progress. + """ + def print_progress(progress): + if show_progress: + msg = (_('\rServer %(action)s... %(progress)s%% complete') + % dict(action=action, progress=progress)) + else: + msg = _('\rServer %(action)s...') % dict(action=action) + + sys.stdout.write(msg) + sys.stdout.flush() + + if not silent: + print() + + while True: + obj = poll_fn(obj_id) + + status = getattr(obj, status_field) + + if status: + status = status.lower() + + progress = getattr(obj, 'progress', None) or 0 + if status in final_ok_states: + if not silent: + print_progress(100) + print(_("\nFinished")) + break + elif status == "error": + if not silent: + print(_("\nError %s server") % action) + raise exceptions.InstanceInErrorState(obj.fault['message']) + + if not silent: + print_progress(progress) + + time.sleep(poll_period) + + +def _translate_keys(collection, convert): + for item in collection: + keys = item.__dict__.keys() + for from_key, to_key in convert: + if from_key in keys and to_key not in keys: + setattr(item, to_key, item._info[from_key]) + + +def _translate_extended_states(collection): + power_states = [ + 'NOSTATE', # 0x00 + 'Running', # 0x01 + '', # 0x02 + 'Paused', # 0x03 + 'Shutdown', # 0x04 + '', # 0x05 + 'Crashed', # 0x06 + 'Suspended' # 0x07 + ] + + for item in collection: + try: + setattr(item, 'power_state', + power_states[getattr(item, 'power_state')]) + except AttributeError: + setattr(item, 'power_state', "N/A") + try: + getattr(item, 'task_state') + except AttributeError: + setattr(item, 'task_state', "N/A") + + +def _translate_flavor_keys(collection): + _translate_keys(collection, [('ram', 'memory_mb')]) + + +def _print_flavor_extra_specs(flavor): + try: + return flavor.get_keys() + except exceptions.NotFound: + return "N/A" + + +def _print_flavor_list(flavors, show_extra_specs=False): + _translate_flavor_keys(flavors) + + headers = [ + 'ID', + 'Name', + 'Memory_MB', + 'Disk', + 'Ephemeral', + 'Swap', + 'VCPUs', + 'RXTX_Factor', + 'Is_Public', + ] + + if show_extra_specs: + formatters = {'extra_specs': _print_flavor_extra_specs} + headers.append('extra_specs') + else: + formatters = {} + + utils.print_list(flavors, headers, formatters) + + +@cliutils.arg( + '--extra-specs', + dest='extra_specs', + action='store_true', + default=False, + help=_('Get extra-specs of each flavor.')) +@cliutils.arg( + '--all', + dest='all', + action='store_true', + default=False, + help=_('Display all flavors (Admin only).')) +def do_flavor_list(cs, args): + """Print a list of available 'flavors' (sizes of servers).""" + if args.all: + flavors = cs.flavors.list(is_public=None) + else: + flavors = cs.flavors.list() + _print_flavor_list(flavors, args.extra_specs) + + +@cliutils.arg( + 'flavor', + metavar='', + help=_("Name or ID of the flavor to delete")) +def do_flavor_delete(cs, args): + """Delete a specific flavor""" + flavorid = _find_flavor(cs, args.flavor) + cs.flavors.delete(flavorid) + _print_flavor_list([flavorid]) + + +@cliutils.arg( + 'flavor', + metavar='', + help=_("Name or ID of flavor")) +def do_flavor_show(cs, args): + """Show details about the given flavor.""" + flavor = _find_flavor(cs, args.flavor) + _print_flavor(flavor) + + +@cliutils.arg( + 'name', + metavar='', + help=_("Name of the new flavor")) +@cliutils.arg( + 'id', + metavar='', + help=_("Unique ID (integer or UUID) for the new flavor." + " If specifying 'auto', a UUID will be generated as id")) +@cliutils.arg( + 'ram', + metavar='', + help=_("Memory size in MB")) +@cliutils.arg( + 'disk', + metavar='', + help=_("Disk size in GB")) +@cliutils.arg( + '--ephemeral', + metavar='', + help=_("Ephemeral space size in GB (default 0)"), + default=0) +@cliutils.arg( + 'vcpus', + metavar='', + help=_("Number of vcpus")) +@cliutils.arg( + '--swap', + metavar='', + help=_("Swap space size in MB (default 0)"), + default=0) +@cliutils.arg( + '--rxtx-factor', + metavar='', + help=_("RX/TX factor (default 1)"), + default=1.0) +@cliutils.arg( + '--is-public', + metavar='', + help=_("Make flavor accessible to the public (default true)"), + type=lambda v: strutils.bool_from_string(v, True), + default=True) +def do_flavor_create(cs, args): + """Create a new flavor""" + f = cs.flavors.create(args.name, args.ram, args.vcpus, args.disk, args.id, + args.ephemeral, args.swap, args.rxtx_factor, + args.is_public) + _print_flavor_list([f]) + + +@cliutils.arg( + 'flavor', + metavar='', + help=_("Name or ID of flavor")) +@cliutils.arg( + 'action', + metavar='', + choices=['set', 'unset'], + help=_("Actions: 'set' or 'unset'")) +@cliutils.arg( + 'metadata', + metavar='', + nargs='+', + action='append', + default=[], + help=_('Extra_specs to set/unset (only key is necessary on unset)')) +def do_flavor_key(cs, args): + """Set or unset extra_spec for a flavor.""" + flavor = _find_flavor(cs, args.flavor) + keypair = _extract_metadata(args) + + if args.action == 'set': + flavor.set_keys(keypair) + elif args.action == 'unset': + flavor.unset_keys(keypair.keys()) + + +@cliutils.arg( + '--flavor', + metavar='', + help=_("Filter results by flavor name or ID.")) +@cliutils.arg( + '--tenant', metavar='', + help=_('Filter results by tenant ID.')) +def do_flavor_access_list(cs, args): + """Print access information about the given flavor.""" + if args.flavor and args.tenant: + raise exceptions.CommandError(_("Unable to filter results by " + "both --flavor and --tenant.")) + elif args.flavor: + flavor = _find_flavor(cs, args.flavor) + if flavor.is_public: + raise exceptions.CommandError(_("Failed to get access list " + "for public flavor type.")) + kwargs = {'flavor': flavor} + elif args.tenant: + kwargs = {'tenant': args.tenant} + else: + raise exceptions.CommandError(_("Unable to get all access lists. " + "Specify --flavor or --tenant")) + + try: + access_list = cs.flavor_access.list(**kwargs) + except NotImplementedError as e: + raise exceptions.CommandError("%s" % str(e)) + + columns = ['Flavor_ID', 'Tenant_ID'] + utils.print_list(access_list, columns) + + +@cliutils.arg( + 'flavor', + metavar='', + help=_("Flavor name or ID to add access for the given tenant.")) +@cliutils.arg( + 'tenant', metavar='', + help=_('Tenant ID to add flavor access for.')) +def do_flavor_access_add(cs, args): + """Add flavor access for the given tenant.""" + flavor = _find_flavor(cs, args.flavor) + access_list = cs.flavor_access.add_tenant_access(flavor, args.tenant) + columns = ['Flavor_ID', 'Tenant_ID'] + utils.print_list(access_list, columns) + + +@cliutils.arg( + 'flavor', + metavar='', + help=_("Flavor name or ID to remove access for the given tenant.")) +@cliutils.arg( + 'tenant', metavar='', + help=_('Tenant ID to remove flavor access for.')) +def do_flavor_access_remove(cs, args): + """Remove flavor access for the given tenant.""" + flavor = _find_flavor(cs, args.flavor) + access_list = cs.flavor_access.remove_tenant_access(flavor, args.tenant) + columns = ['Flavor_ID', 'Tenant_ID'] + utils.print_list(access_list, columns) + + +@cliutils.arg( + 'project_id', metavar='', + help=_('The ID of the project.')) +def do_scrub(cs, args): + """Delete networks and security groups associated with a project.""" + networks_list = cs.networks.list() + networks_list = [network for network in networks_list + if getattr(network, 'project_id', '') == args.project_id] + search_opts = {'all_tenants': 1} + groups = cs.security_groups.list(search_opts) + groups = [group for group in groups + if group.tenant_id == args.project_id] + for network in networks_list: + cs.networks.disassociate(network) + for group in groups: + cs.security_groups.delete(group) + + +@cliutils.arg( + '--fields', + default=None, + metavar='', + help='Comma-separated list of fields to display. ' + 'Use the show command to see which fields are available.') +def do_network_list(cs, args): + """Print a list of available networks.""" + network_list = cs.networks.list() + columns = ['ID', 'Label', 'Cidr'] + + formatters = {} + field_titles = [] + if args.fields: + for field in args.fields.split(','): + field_title, formatter = utils._make_field_formatter(field, {}) + field_titles.append(field_title) + formatters[field_title] = formatter + + columns = columns + field_titles + utils.print_list(network_list, columns) + + +@cliutils.arg( + 'network', + metavar='', + help=_("uuid or label of network")) +def do_network_show(cs, args): + """Show details about the given network.""" + network = utils.find_resource(cs.networks, args.network) + utils.print_dict(network._info) + + +@cliutils.arg( + 'network', + metavar='', + help=_("uuid or label of network")) +def do_network_delete(cs, args): + """Delete network by label or id.""" + network = utils.find_resource(cs.networks, args.network) + network.delete() + + +@cliutils.arg( + '--host-only', + dest='host_only', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=0) +@cliutils.arg( + '--project-only', + dest='project_only', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=0) +@cliutils.arg( + 'network', + metavar='', + help="uuid of network") +def do_network_disassociate(cs, args): + """Disassociate host and/or project from the given network.""" + if args.host_only: + cs.networks.disassociate(args.network, True, False) + elif args.project_only: + cs.networks.disassociate(args.network, False, True) + else: + cs.networks.disassociate(args.network, True, True) + + +@cliutils.arg( + 'network', + metavar='', + help="uuid of network") +@cliutils.arg( + 'host', + metavar='', + help="Name of host") +def do_network_associate_host(cs, args): + """Associate host with network.""" + cs.networks.associate_host(args.network, args.host) + + +@cliutils.arg( + 'network', + metavar='', + help="uuid of network") +def do_network_associate_project(cs, args): + """Associate project with network.""" + cs.networks.associate_project(args.network) + + +def _filter_network_create_options(args): + valid_args = ['label', 'cidr', 'vlan_start', 'vpn_start', 'cidr_v6', + 'gateway', 'gateway_v6', 'bridge', 'bridge_interface', + 'multi_host', 'dns1', 'dns2', 'uuid', 'fixed_cidr', + 'project_id', 'priority', 'vlan', 'mtu', 'dhcp_server', + 'allowed_start', 'allowed_end'] + kwargs = {} + for k, v in args.__dict__.items(): + if k in valid_args and v is not None: + kwargs[k] = v + + return kwargs + + +@cliutils.arg( + 'label', + metavar='', + help=_("Label for network")) +@cliutils.arg( + '--fixed-range-v4', + dest='cidr', + metavar='', + help=_("IPv4 subnet (ex: 10.0.0.0/8)")) +@cliutils.arg( + '--fixed-range-v6', + dest="cidr_v6", + help=_('IPv6 subnet (ex: fe80::/64')) +@cliutils.arg( + '--vlan', + dest='vlan', + type=int, + metavar='', + help=_("The vlan ID to be assigned to the project.")) +@cliutils.arg( + '--vlan-start', + dest='vlan_start', + type=int, + metavar='', + help=_('First vlan ID to be assigned to the project. Subsequent vlan ' + 'IDs will be assigned incrementally.')) +@cliutils.arg( + '--vpn', + dest='vpn_start', + type=int, + metavar='', + help=_("vpn start")) +@cliutils.arg( + '--gateway', + dest="gateway", + help=_('gateway')) +@cliutils.arg( + '--gateway-v6', + dest="gateway_v6", + help=_('IPv6 gateway')) +@cliutils.arg( + '--bridge', + dest="bridge", + metavar='', + help=_('VIFs on this network are connected to this bridge.')) +@cliutils.arg( + '--bridge-interface', + dest="bridge_interface", + metavar='', + help=_('The bridge is connected to this interface.')) +@cliutils.arg( + '--multi-host', + dest="multi_host", + metavar="<'T'|'F'>", + help=_('Multi host')) +@cliutils.arg( + '--dns1', + dest="dns1", + metavar="", help='First DNS') +@cliutils.arg( + '--dns2', + dest="dns2", + metavar="", + help=_('Second DNS')) +@cliutils.arg( + '--uuid', + dest="uuid", + metavar="", + help=_('Network UUID')) +@cliutils.arg( + '--fixed-cidr', + dest="fixed_cidr", + metavar='', + help=_('IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)')) +@cliutils.arg( + '--project-id', + dest="project_id", + metavar="", + help=_('Project ID')) +@cliutils.arg( + '--priority', + dest="priority", + metavar="", + help=_('Network interface priority')) +@cliutils.arg( + '--mtu', + dest="mtu", + type=int, + help=_('MTU for network')) +@cliutils.arg( + '--enable-dhcp', + dest="enable_dhcp", + metavar="<'T'|'F'>", + help=_('Enable dhcp')) +@cliutils.arg( + '--dhcp-server', + dest="dhcp_server", + help=_('Dhcp-server (defaults to gateway address)')) +@cliutils.arg( + '--share-address', + dest="share_address", + metavar="<'T'|'F'>", + help=_('Share address')) +@cliutils.arg( + '--allowed-start', + dest="allowed_start", + help=_('Start of allowed addresses for instances')) +@cliutils.arg( + '--allowed-end', + dest="allowed_end", + help=_('End of allowed addresses for instances')) +def do_network_create(cs, args): + """Create a network.""" + + if not (args.cidr or args.cidr_v6): + raise exceptions.CommandError( + _("Must specify either fixed_range_v4 or fixed_range_v6")) + kwargs = _filter_network_create_options(args) + if args.multi_host is not None: + kwargs['multi_host'] = bool(args.multi_host == 'T' or + strutils.bool_from_string(args.multi_host)) + if args.enable_dhcp is not None: + kwargs['enable_dhcp'] = bool( + args.enable_dhcp == 'T' or + strutils.bool_from_string(args.enable_dhcp)) + if args.share_address is not None: + kwargs['share_address'] = bool( + args.share_address == 'T' or + strutils.bool_from_string(args.share_address)) + + cs.networks.create(**kwargs) + + +@cliutils.arg( + '--limit', + dest="limit", + metavar="", + help=_('Number of images to return per request.')) +def do_image_list(cs, _args): + """Print a list of available images to boot from.""" + limit = _args.limit + image_list = cs.images.list(limit=limit) + + def parse_server_name(image): + try: + return image.server['id'] + except (AttributeError, KeyError): + return '' + + fmts = {'Server': parse_server_name} + utils.print_list(image_list, ['ID', 'Name', 'Status', 'Server'], + fmts, sortby_index=1) + + +@cliutils.arg( + 'image', + metavar='', + help=_("Name or ID of image")) +@cliutils.arg( + 'action', + metavar='', + choices=['set', 'delete'], + help=_("Actions: 'set' or 'delete'")) +@cliutils.arg( + 'metadata', + metavar='', + nargs='+', + action='append', + default=[], + help=_('Metadata to add/update or delete (only key is necessary on ' + 'delete)')) +def do_image_meta(cs, args): + """Set or Delete metadata on an image.""" + image = _find_image(cs, args.image) + metadata = _extract_metadata(args) + + if args.action == 'set': + cs.images.set_meta(image, metadata) + elif args.action == 'delete': + cs.images.delete_meta(image, metadata.keys()) + + +def _extract_metadata(args): + metadata = {} + for metadatum in args.metadata[0]: + # Can only pass the key in on 'delete' + # So this doesn't have to have '=' + if metadatum.find('=') > -1: + (key, value) = metadatum.split('=', 1) + else: + key = metadatum + value = None + + metadata[key] = value + return metadata + + +def _print_image(image): + info = image._info.copy() + + # ignore links, we don't need to present those + info.pop('links') + + # try to replace a server entity to just an id + server = info.pop('server', None) + try: + info['server'] = server['id'] + except (KeyError, TypeError): + pass + + # break up metadata and display each on its own row + metadata = info.pop('metadata', {}) + try: + for key, value in metadata.items(): + _key = 'metadata %s' % key + info[_key] = value + except AttributeError: + pass + + utils.print_dict(info) + + +def _print_flavor(flavor): + info = flavor._info.copy() + # ignore links, we don't need to present those + info.pop('links') + info.update({"extra_specs": _print_flavor_extra_specs(flavor)}) + utils.print_dict(info) + + +@cliutils.arg( + 'image', + metavar='', + help=_("Name or ID of image")) +def do_image_show(cs, args): + """Show details about the given image.""" + image = _find_image(cs, args.image) + _print_image(image) + + +@cliutils.arg( + 'image', metavar='', nargs='+', + help=_('Name or ID of image(s).')) +def do_image_delete(cs, args): + """Delete specified image(s).""" + for image in args.image: + try: + _find_image(cs, image).delete() + except Exception as e: + print(_("Delete for image %(image)s failed: %(e)s") % + {'image': image, 'e': e}) + + +@cliutils.arg( + '--reservation-id', + dest='reservation_id', + metavar='', + default=None, + help=_('Only return servers that match reservation-id.')) +@cliutils.arg( + '--reservation_id', + help=argparse.SUPPRESS) +@cliutils.arg( + '--ip', + dest='ip', + metavar='', + default=None, + help=_('Search with regular expression match by IP address.')) +@cliutils.arg( + '--ip6', + dest='ip6', + metavar='', + default=None, + help=_('Search with regular expression match by IPv6 address.')) +@cliutils.arg( + '--name', + dest='name', + metavar='', + default=None, + help=_('Search with regular expression match by name')) +@cliutils.arg( + '--instance-name', + dest='instance_name', + metavar='', + default=None, + help=_('Search with regular expression match by server name.')) +@cliutils.arg( + '--instance_name', + help=argparse.SUPPRESS) +@cliutils.arg( + '--status', + dest='status', + metavar='', + default=None, + help=_('Search by server status')) +@cliutils.arg( + '--flavor', + dest='flavor', + metavar='', + default=None, + help=_('Search by flavor name or ID')) +@cliutils.arg( + '--image', + dest='image', + metavar='', + default=None, + help=_('Search by image name or ID')) +@cliutils.arg( + '--host', + dest='host', + metavar='', + default=None, + help=_('Search servers by hostname to which they are assigned (Admin ' + 'only).')) +@cliutils.arg( + '--all-tenants', + dest='all_tenants', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=int(strutils.bool_from_string( + os.environ.get("ALL_TENANTS", 'false'), True)), + help=_('Display information from all tenants (Admin only).')) +@cliutils.arg( + '--all_tenants', + nargs='?', + type=int, + const=1, + help=argparse.SUPPRESS) +@cliutils.arg( + '--tenant', + # nova db searches by project_id + dest='tenant', + metavar='', + nargs='?', + help=_('Display information from single tenant (Admin only). ' + 'The --all-tenants option must also be provided.')) +@cliutils.arg( + '--user', + dest='user', + metavar='', + nargs='?', + help=_('Display information from single user (Admin only).')) +@cliutils.arg( + '--deleted', + dest='deleted', + action="store_true", + default=False, + help='Only display deleted servers (Admin only).') +@cliutils.arg( + '--fields', + default=None, + metavar='', + help=_('Comma-separated list of fields to display. ' + 'Use the show command to see which fields are available.')) +@cliutils.arg( + '--minimal', + dest='minimal', + action="store_true", + default=False, + help=_('Get only uuid and name.')) +@cliutils.arg( + '--sort', + dest='sort', + metavar='[:]', + help=('Comma-separated list of sort keys and directions in the form' + ' of [:]. The direction defaults to descending if' + ' not specified.')) +def do_list(cs, args): + """List active servers.""" + imageid = None + flavorid = None + if args.image: + imageid = _find_image(cs, args.image).id + if args.flavor: + flavorid = _find_flavor(cs, args.flavor).id + # search by tenant or user only works with all_tenants + if args.tenant or args.user: + args.all_tenants = 1 + search_opts = { + 'all_tenants': args.all_tenants, + 'reservation_id': args.reservation_id, + 'ip': args.ip, + 'ip6': args.ip6, + 'name': args.name, + 'image': imageid, + 'flavor': flavorid, + 'status': args.status, + 'tenant_id': args.tenant, + 'user_id': args.user, + 'host': args.host, + 'deleted': args.deleted, + 'instance_name': args.instance_name} + + filters = {'flavor': lambda f: f['id'], + 'security_groups': utils._format_security_groups} + + formatters = {} + field_titles = [] + if args.fields: + for field in args.fields.split(','): + field_title, formatter = utils._make_field_formatter(field, + filters) + field_titles.append(field_title) + formatters[field_title] = formatter + + id_col = 'ID' + + detailed = not args.minimal + + sort_keys = [] + sort_dirs = [] + if args.sort: + for sort in args.sort.split(','): + sort_key, _sep, sort_dir = sort.partition(':') + if not sort_dir: + sort_dir = 'desc' + elif sort_dir not in ('asc', 'desc'): + raise exceptions.CommandError(_( + 'Unknown sort direction: %s') % sort_dir) + sort_keys.append(sort_key) + sort_dirs.append(sort_dir) + + servers = cs.servers.list(detailed=detailed, + search_opts=search_opts, + sort_keys=sort_keys, + sort_dirs=sort_dirs) + convert = [('OS-EXT-SRV-ATTR:host', 'host'), + ('OS-EXT-STS:task_state', 'task_state'), + ('OS-EXT-SRV-ATTR:instance_name', 'instance_name'), + ('OS-EXT-STS:power_state', 'power_state'), + ('hostId', 'host_id')] + _translate_keys(servers, convert) + _translate_extended_states(servers) + if args.minimal: + columns = [ + id_col, + 'Name'] + elif field_titles: + columns = [id_col] + field_titles + else: + columns = [ + id_col, + 'Name', + 'Status', + 'Task State', + 'Power State', + 'Networks' + ] + # If getting the data for all tenants, print + # Tenant ID as well + if search_opts['all_tenants']: + columns.insert(2, 'Tenant ID') + formatters['Networks'] = utils._format_servers_list_networks + sortby_index = 1 + if args.sort: + sortby_index = None + utils.print_list(servers, columns, + formatters, sortby_index=sortby_index) + + +@cliutils.arg( + '--hard', + dest='reboot_type', + action='store_const', + const=servers.REBOOT_HARD, + default=servers.REBOOT_SOFT, + help=_('Perform a hard reboot (instead of a soft one).')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + '--poll', + dest='poll', + action="store_true", + default=False, + help=_('Poll until reboot is complete.')) +def do_reboot(cs, args): + """Reboot a server.""" + server = _find_server(cs, args.server) + server.reboot(args.reboot_type) + + if args.poll: + _poll_for_status(cs.servers.get, server.id, 'rebooting', ['active'], + show_progress=False) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('image', metavar='', help=_("Name or ID of new image.")) +@cliutils.arg( + '--rebuild-password', + dest='rebuild_password', + metavar='', + default=False, + help=_("Set the provided admin password on the rebuilt server.")) +@cliutils.arg( + '--rebuild_password', + help=argparse.SUPPRESS) +@cliutils.arg( + '--poll', + dest='poll', + action="store_true", + default=False, + help=_('Report the server rebuild progress until it completes.')) +@cliutils.arg( + '--minimal', + dest='minimal', + action="store_true", + default=False, + help=_('Skips flavor/image lookups when showing servers')) +@cliutils.arg( + '--preserve-ephemeral', + action="store_true", + default=False, + help='Preserve the default ephemeral storage partition on rebuild.') +@cliutils.arg( + '--name', + metavar='', + default=None, + help=_('Name for the new server')) +@cliutils.arg( + '--meta', + metavar="", + action='append', + default=[], + help=_("Record arbitrary key/value metadata to /meta_data.json " + "on the metadata server. Can be specified multiple times.")) +@cliutils.arg( + '--file', + metavar="", + action='append', + dest='files', + default=[], + help=_("Store arbitrary files from locally to " + "on the new server. You may store up to 5 files.")) +def do_rebuild(cs, args): + """Shutdown, re-image, and re-boot a server.""" + server = _find_server(cs, args.server) + image = _find_image(cs, args.image) + + if args.rebuild_password is not False: + _password = args.rebuild_password + else: + _password = None + + kwargs = utils.get_resource_manager_extra_kwargs(do_rebuild, args) + kwargs['preserve_ephemeral'] = args.preserve_ephemeral + kwargs['name'] = args.name + meta = dict(v.split('=', 1) for v in args.meta) + kwargs['meta'] = meta + + files = {} + for f in args.files: + try: + dst, src = f.split('=', 1) + with open(src, 'r') as s: + files[dst] = s.read() + except IOError as e: + raise exceptions.CommandError(_("Can't open '%(src)s': %(exc)s") % + {'src': src, 'exc': e}) + except ValueError: + raise exceptions.CommandError(_("Invalid file argument '%s'. " + "File arguments must be of the " + "form '--file " + "'") % f) + kwargs['files'] = files + server = server.rebuild(image, _password, **kwargs) + _print_server(cs, args, server) + + if args.poll: + _poll_for_status(cs.servers.get, server.id, 'rebuilding', ['active']) + + +@cliutils.arg( + 'server', metavar='', + help=_('Name (old name) or ID of server.')) +@cliutils.arg('name', metavar='', help=_('New name for the server.')) +def do_rename(cs, args): + """Rename a server.""" + _find_server(cs, args.server).update(name=args.name) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + 'flavor', + metavar='', + help=_("Name or ID of new flavor.")) +@cliutils.arg( + '--poll', + dest='poll', + action="store_true", + default=False, + help=_('Report the server resize progress until it completes.')) +def do_resize(cs, args): + """Resize a server.""" + server = _find_server(cs, args.server) + flavor = _find_flavor(cs, args.flavor) + kwargs = utils.get_resource_manager_extra_kwargs(do_resize, args) + server.resize(flavor, **kwargs) + if args.poll: + _poll_for_status(cs.servers.get, server.id, 'resizing', + ['active', 'verify_resize']) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_resize_confirm(cs, args): + """Confirm a previous resize.""" + _find_server(cs, args.server).confirm_resize() + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_resize_revert(cs, args): + """Revert a previous resize (and return to the previous VM).""" + _find_server(cs, args.server).revert_resize() + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + '--poll', + dest='poll', + action="store_true", + default=False, + help=_('Report the server migration progress until it completes.')) +def do_migrate(cs, args): + """Migrate a server. The new host will be selected by the scheduler.""" + server = _find_server(cs, args.server) + server.migrate() + + if args.poll: + _poll_for_status(cs.servers.get, server.id, 'migrating', + ['active', 'verify_resize']) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_pause(cs, args): + """Pause a server.""" + _find_server(cs, args.server).pause() + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_unpause(cs, args): + """Unpause a server.""" + _find_server(cs, args.server).unpause() + + +@cliutils.arg( + 'server', + metavar='', nargs='+', + help=_('Name or ID of server(s).')) +def do_stop(cs, args): + """Stop the server(s).""" + utils.do_action_on_many( + lambda s: _find_server(cs, s).stop(), + args.server, + _("Request to stop server %s has been accepted."), + _("Unable to stop the specified server(s).")) + + +@cliutils.arg( + 'server', + metavar='', nargs='+', + help=_('Name or ID of server(s).')) +def do_start(cs, args): + """Start the server(s).""" + utils.do_action_on_many( + lambda s: _find_server(cs, s).start(), + args.server, + _("Request to start server %s has been accepted."), + _("Unable to start the specified server(s).")) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_lock(cs, args): + """Lock a server. A normal (non-admin) user will not be able to execute + actions on a locked server. + """ + _find_server(cs, args.server).lock() + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_unlock(cs, args): + """Unlock a server.""" + _find_server(cs, args.server).unlock() + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_suspend(cs, args): + """Suspend a server.""" + _find_server(cs, args.server).suspend() + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_resume(cs, args): + """Resume a server.""" + _find_server(cs, args.server).resume() + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + '--password', + metavar='', + dest='password', + help=_('The admin password to be set in the rescue environment.')) +@cliutils.arg( + '--image', + metavar='', + dest='image', + help=_('The image to rescue with.')) +def do_rescue(cs, args): + """Reboots a server into rescue mode, which starts the machine + from either the initial image or a specified image, attaching the current + boot disk as secondary. + """ + kwargs = {} + if args.image: + kwargs['image'] = _find_image(cs, args.image) + if args.password: + kwargs['password'] = args.password + utils.print_dict(_find_server(cs, args.server).rescue(**kwargs)[1]) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_unrescue(cs, args): + """Restart the server from normal boot disk again.""" + _find_server(cs, args.server).unrescue() + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_shelve(cs, args): + """Shelve a server.""" + _find_server(cs, args.server).shelve() + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_shelve_offload(cs, args): + """Remove a shelved server from the compute node.""" + _find_server(cs, args.server).shelve_offload() + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_unshelve(cs, args): + """Unshelve a server.""" + _find_server(cs, args.server).unshelve() + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_diagnostics(cs, args): + """Retrieve server diagnostics.""" + server = _find_server(cs, args.server) + utils.print_dict(cs.servers.diagnostics(server)[1], wrap=80) + + +@cliutils.arg( + 'server', metavar='', + help=_('Name or ID of a server for which the network cache should ' + 'be refreshed from neutron (Admin only).')) +def do_refresh_network(cs, args): + """Refresh server network information.""" + server = _find_server(cs, args.server) + cs.server_external_events.create([{'server_uuid': server.id, + 'name': 'network-changed'}]) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_root_password(cs, args): + """ + Change the admin password for a server. + """ + server = _find_server(cs, args.server) + p1 = getpass.getpass('New password: ') + p2 = getpass.getpass('Again: ') + if p1 != p2: + raise exceptions.CommandError(_("Passwords do not match.")) + server.change_password(p1) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('name', metavar='', help=_('Name of snapshot.')) +@cliutils.arg( + '--show', + dest='show', + action="store_true", + default=False, + help=_('Print image info.')) +@cliutils.arg( + '--poll', + dest='poll', + action="store_true", + default=False, + help=_('Report the snapshot progress and poll until image creation is ' + 'complete.')) +def do_image_create(cs, args): + """Create a new image by taking a snapshot of a running server.""" + server = _find_server(cs, args.server) + image_uuid = cs.servers.create_image(server, args.name) + + if args.poll: + _poll_for_status(cs.images.get, image_uuid, 'snapshotting', + ['active']) + + # NOTE(sirp): A race-condition exists between when the image finishes + # uploading and when the servers's `task_state` is cleared. To account + # for this, we need to poll a second time to ensure the `task_state` is + # cleared before returning, ensuring that a snapshot taken immediately + # after this function returns will succeed. + # + # A better long-term solution will be to separate 'snapshotting' and + # 'image-uploading' in Nova and clear the task-state once the VM + # snapshot is complete but before the upload begins. + task_state_field = "OS-EXT-STS:task_state" + if hasattr(server, task_state_field): + _poll_for_status(cs.servers.get, server.id, 'image_snapshot', + [None], status_field=task_state_field, + show_progress=False, silent=True) + + if args.show: + _print_image(cs.images.get(image_uuid)) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('name', metavar='', help=_('Name of the backup image.')) +@cliutils.arg( + 'backup_type', metavar='', + help=_('The backup type, like "daily" or "weekly".')) +@cliutils.arg( + 'rotation', metavar='', + help=_('Int parameter representing how many backups to keep ' + 'around.')) +def do_backup(cs, args): + """Backup a server by creating a 'backup' type snapshot.""" + _find_server(cs, args.server).backup(args.name, + args.backup_type, + args.rotation) + + +@cliutils.arg( + 'server', + metavar='', + help=_("Name or ID of server")) +@cliutils.arg( + 'action', + metavar='', + choices=['set', 'delete'], + help=_("Actions: 'set' or 'delete'")) +@cliutils.arg( + 'metadata', + metavar='', + nargs='+', + action='append', + default=[], + help=_('Metadata to set or delete (only key is necessary on delete)')) +def do_meta(cs, args): + """Set or Delete metadata on a server.""" + server = _find_server(cs, args.server) + metadata = _extract_metadata(args) + + if args.action == 'set': + cs.servers.set_meta(server, metadata) + elif args.action == 'delete': + cs.servers.delete_meta(server, sorted(metadata.keys(), reverse=True)) + + +def _print_server(cs, args, server=None): + # By default when searching via name we will do a + # findall(name=blah) and due a REST /details which is not the same + # as a .get() and doesn't get the information about flavors and + # images. This fix it as we redo the call with the id which does a + # .get() to get all informations. + if not server: + server = _find_server(cs, args.server) + + minimal = getattr(args, "minimal", False) + + networks = server.networks + info = server._info.copy() + for network_label, address_list in networks.items(): + info['%s network' % network_label] = ', '.join(address_list) + + flavor = info.get('flavor', {}) + flavor_id = flavor.get('id', '') + if minimal: + info['flavor'] = flavor_id + else: + info['flavor'] = '%s (%s)' % (_find_flavor(cs, flavor_id).name, + flavor_id) + + if 'security_groups' in info: + # when we have multiple nics the info will include the + # security groups N times where N == number of nics. Be nice + # and only display it once. + info['security_groups'] = ', '.join( + sorted(set(group['name'] for group in info['security_groups']))) + + image = info.get('image', {}) + if image: + image_id = image.get('id', '') + if minimal: + info['image'] = image_id + else: + try: + info['image'] = '%s (%s)' % (_find_image(cs, image_id).name, + image_id) + except Exception: + info['image'] = '%s (%s)' % (_("Image not found"), image_id) + else: # Booted from volume + info['image'] = _("Attempt to boot from volume - no image supplied") + + info.pop('links', None) + info.pop('addresses', None) + + utils.print_dict(info) + + +@cliutils.arg( + '--minimal', + dest='minimal', + action="store_true", + default=False, + help=_('Skips flavor/image lookups when showing servers')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_show(cs, args): + """Show details about the given server.""" + _print_server(cs, args) + + +@cliutils.arg( + 'server', metavar='', nargs='+', + help=_('Name or ID of server(s).')) +def do_delete(cs, args): + """Immediately shut down and delete specified server(s).""" + find_args = {'all_tenants': '1'} + utils.do_action_on_many( + lambda s: _find_server(cs, s, **find_args).delete(), + args.server, + _("Request to delete server %s has been accepted."), + _("Unable to delete the specified server(s).")) + + +def _find_server(cs, server, **find_args): + """Get a server by name or ID.""" + return utils.find_resource(cs.servers, server, **find_args) + + +def _find_image(cs, image): + """Get an image by name or ID.""" + return utils.find_resource(cs.images, image) + + +def _find_flavor(cs, flavor): + """Get a flavor by name, ID, or RAM size.""" + try: + # isinstance() is being used to check if flavor is an instance of + # integer. It will help us to check if the user has entered flavor + # name or flavorid. If flavor name has been entered it is being + # converted to lowercase using lower(). Incase it is an ID the user + # has passed it will not go through the "flavor = flavor.lower()" + # code.The reason for checking if it is a flavor name or flavorid is + # that int has no lower() so it will give an error. + if isinstance(flavor, six.integer_types): + pass + else: + flavor = flavor.lower() + return utils.find_resource(cs.flavors, flavor, is_public=None) + except exceptions.NotFound: + return cs.flavors.find(ram=flavor) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + 'network_id', + metavar='', + help='Network ID.') +def do_add_fixed_ip(cs, args): + """Add new IP address on a network to server.""" + server = _find_server(cs, args.server) + server.add_fixed_ip(args.network_id) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('address', metavar='
', help=_('IP Address.')) +def do_remove_fixed_ip(cs, args): + """Remove an IP address from a server.""" + server = _find_server(cs, args.server) + server.remove_fixed_ip(args.address) + + +def _find_volume(cs, volume): + """Get a volume by name or ID.""" + return utils.find_resource(cs.volumes, volume) + + +def _find_volume_snapshot(cs, snapshot): + """Get a volume snapshot by name or ID.""" + return utils.find_resource(cs.volume_snapshots, snapshot) + + +def _print_volume(volume): + utils.print_dict(volume._info) + + +def _print_volume_snapshot(snapshot): + utils.print_dict(snapshot._info) + + +def _translate_volume_keys(collection): + _translate_keys(collection, + [('displayName', 'display_name'), + ('volumeType', 'volume_type')]) + + +def _translate_volume_snapshot_keys(collection): + _translate_keys(collection, + [('displayName', 'display_name'), + ('volumeId', 'volume_id')]) + + +def _translate_availability_zone_keys(collection): + _translate_keys(collection, + [('zoneName', 'name'), ('zoneState', 'status')]) + + +@cliutils.arg( + '--all-tenants', + dest='all_tenants', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=int(strutils.bool_from_string( + os.environ.get("ALL_TENANTS", 'false'), True)), + help=_('Display information from all tenants (Admin only).')) +@cliutils.arg( + '--all_tenants', + nargs='?', + type=int, + const=1, + help=argparse.SUPPRESS) +def do_volume_list(cs, args): + """List all the volumes.""" + search_opts = {'all_tenants': args.all_tenants} + volumes = cs.volumes.list(search_opts=search_opts) + _translate_volume_keys(volumes) + + # Create a list of servers to which the volume is attached + for vol in volumes: + servers = [s.get('server_id') for s in vol.attachments] + setattr(vol, 'attached_to', ','.join(map(str, servers))) + utils.print_list(volumes, ['ID', 'Status', 'Display Name', + 'Size', 'Volume Type', 'Attached to']) + + +@cliutils.arg( + 'volume', + metavar='', + help=_('Name or ID of the volume.')) +def do_volume_show(cs, args): + """Show details about a volume.""" + volume = _find_volume(cs, args.volume) + _print_volume(volume) + + +@cliutils.arg( + 'size', + metavar='', + type=int, + help=_('Size of volume in GB')) +@cliutils.arg( + '--snapshot-id', + metavar='', + default=None, + help=_('Optional snapshot id to create the volume from. (Default=None)')) +@cliutils.arg( + '--snapshot_id', + help=argparse.SUPPRESS) +@cliutils.arg( + '--image-id', + metavar='', + help=_('Optional image id to create the volume from. (Default=None)'), + default=None) +@cliutils.arg( + '--display-name', + metavar='', + default=None, + help=_('Optional volume name. (Default=None)')) +@cliutils.arg( + '--display_name', + help=argparse.SUPPRESS) +@cliutils.arg( + '--display-description', + metavar='', + default=None, + help=_('Optional volume description. (Default=None)')) +@cliutils.arg( + '--display_description', + help=argparse.SUPPRESS) +@cliutils.arg( + '--volume-type', + metavar='', + default=None, + help=_('Optional volume type. (Default=None)')) +@cliutils.arg( + '--volume_type', + help=argparse.SUPPRESS) +@cliutils.arg( + '--availability-zone', metavar='', + help=_('Optional Availability Zone for volume. (Default=None)'), + default=None) +def do_volume_create(cs, args): + """Add a new volume.""" + volume = cs.volumes.create(args.size, + args.snapshot_id, + args.display_name, + args.display_description, + args.volume_type, + args.availability_zone, + imageRef=args.image_id) + _print_volume(volume) + + +@cliutils.arg( + 'volume', + metavar='', nargs='+', + help=_('Name or ID of the volume(s) to delete.')) +def do_volume_delete(cs, args): + """Remove volume(s).""" + for volume in args.volume: + try: + _find_volume(cs, volume).delete() + except Exception as e: + print(_("Delete for volume %(volume)s failed: %(e)s") % + {'volume': volume, 'e': e}) + + +@cliutils.arg( + 'server', + metavar='', + help=_('Name or ID of server.')) +@cliutils.arg( + 'volume', + metavar='', + help=_('ID of the volume to attach.')) +@cliutils.arg( + 'device', metavar='', default=None, nargs='?', + help=_('Name of the device e.g. /dev/vdb. ' + 'Use "auto" for autoassign (if supported)')) +def do_volume_attach(cs, args): + """Attach a volume to a server.""" + if args.device == 'auto': + args.device = None + + volume = cs.volumes.create_server_volume(_find_server(cs, args.server).id, + args.volume, + args.device) + _print_volume(volume) + + +@cliutils.arg( + 'server', + metavar='', + help=_('Name or ID of server.')) +@cliutils.arg( + 'attachment_id', + metavar='', + help=_('Attachment ID of the volume.')) +@cliutils.arg( + 'new_volume', + metavar='', + help=_('ID of the volume to attach.')) +def do_volume_update(cs, args): + """Update volume attachment.""" + cs.volumes.update_server_volume(_find_server(cs, args.server).id, + args.attachment_id, + args.new_volume) + + +@cliutils.arg( + 'server', + metavar='', + help=_('Name or ID of server.')) +@cliutils.arg( + 'attachment_id', + metavar='', + help=_('ID of the volume to detach.')) +def do_volume_detach(cs, args): + """Detach a volume from a server.""" + cs.volumes.delete_server_volume(_find_server(cs, args.server).id, + args.attachment_id) + + +def do_volume_snapshot_list(cs, _args): + """List all the snapshots.""" + snapshots = cs.volume_snapshots.list() + _translate_volume_snapshot_keys(snapshots) + utils.print_list(snapshots, ['ID', 'Volume ID', 'Status', 'Display Name', + 'Size']) + + +@cliutils.arg( + 'snapshot', + metavar='', + help=_('Name or ID of the snapshot.')) +def do_volume_snapshot_show(cs, args): + """Show details about a snapshot.""" + snapshot = _find_volume_snapshot(cs, args.snapshot) + _print_volume_snapshot(snapshot) + + +@cliutils.arg( + 'volume_id', + metavar='', + help=_('ID of the volume to snapshot')) +@cliutils.arg( + '--force', + metavar='', + help=_('Optional flag to indicate whether to snapshot a volume even if ' + 'its attached to a server. (Default=False)'), + default=False) +@cliutils.arg( + '--display-name', + metavar='', + default=None, + help=_('Optional snapshot name. (Default=None)')) +@cliutils.arg( + '--display_name', + help=argparse.SUPPRESS) +@cliutils.arg( + '--display-description', + metavar='', + default=None, + help=_('Optional snapshot description. (Default=None)')) +@cliutils.arg( + '--display_description', + help=argparse.SUPPRESS) +def do_volume_snapshot_create(cs, args): + """Add a new snapshot.""" + snapshot = cs.volume_snapshots.create(args.volume_id, + args.force, + args.display_name, + args.display_description) + _print_volume_snapshot(snapshot) + + +@cliutils.arg( + 'snapshot', + metavar='', + help=_('Name or ID of the snapshot to delete.')) +def do_volume_snapshot_delete(cs, args): + """Remove a snapshot.""" + snapshot = _find_volume_snapshot(cs, args.snapshot) + snapshot.delete() + + +def _print_volume_type_list(vtypes): + utils.print_list(vtypes, ['ID', 'Name']) + + +def do_volume_type_list(cs, args): + """Print a list of available 'volume types'.""" + vtypes = cs.volume_types.list() + _print_volume_type_list(vtypes) + + +@cliutils.arg( + 'name', + metavar='', + help=_("Name of the new volume type")) +def do_volume_type_create(cs, args): + """Create a new volume type.""" + vtype = cs.volume_types.create(args.name) + _print_volume_type_list([vtype]) + + +@cliutils.arg( + 'id', + metavar='', + help=_("Unique ID of the volume type to delete")) +def do_volume_type_delete(cs, args): + """Delete a specific volume type.""" + cs.volume_types.delete(args.id) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + 'console_type', + metavar='', + help=_('Type of vnc console ("novnc" or "xvpvnc").')) +def do_get_vnc_console(cs, args): + """Get a vnc console to a server.""" + server = _find_server(cs, args.server) + data = server.get_vnc_console(args.console_type) + + class VNCConsole(object): + def __init__(self, console_dict): + self.type = console_dict['type'] + self.url = console_dict['url'] + + utils.print_list([VNCConsole(data['console'])], ['Type', 'Url']) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + 'console_type', + metavar='', + help=_('Type of spice console ("spice-html5").')) +def do_get_spice_console(cs, args): + """Get a spice console to a server.""" + server = _find_server(cs, args.server) + data = server.get_spice_console(args.console_type) + + class SPICEConsole(object): + def __init__(self, console_dict): + self.type = console_dict['type'] + self.url = console_dict['url'] + + utils.print_list([SPICEConsole(data['console'])], ['Type', 'Url']) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + 'console_type', + metavar='', + help='Type of rdp console ("rdp-html5").') +def do_get_rdp_console(cs, args): + """Get a rdp console to a server.""" + server = _find_server(cs, args.server) + data = server.get_rdp_console(args.console_type) + + class RDPConsole(object): + def __init__(self, console_dict): + self.type = console_dict['type'] + self.url = console_dict['url'] + + utils.print_list([RDPConsole(data['console'])], ['Type', 'Url']) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + '--console_type', default='serial', + help=_('Type of serial console, default="serial".')) +def do_get_serial_console(cs, args): + """Get a serial console to a server.""" + if args.console_type not in ('serial',): + raise exceptions.CommandError( + _("Invalid parameter value for 'console_type', " + "currently supported 'serial'.")) + + server = _find_server(cs, args.server) + data = server.get_serial_console(args.console_type) + + class SerialConsole(object): + def __init__(self, console_dict): + self.type = console_dict['type'] + self.url = console_dict['url'] + + utils.print_list([SerialConsole(data['console'])], ['Type', 'Url']) + + +@cliutils.arg('server', metavar='', help='Name or ID of server.') +@cliutils.arg( + 'private_key', + metavar='', + help=_('Private key (used locally to decrypt password) (Optional). ' + 'When specified, the command displays the clear (decrypted) VM ' + 'password. When not specified, the ciphered VM password is ' + 'displayed.'), + nargs='?', + default=None) +def do_get_password(cs, args): + """Get the admin password for a server.""" + server = _find_server(cs, args.server) + data = server.get_password(args.private_key) + print(data) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_clear_password(cs, args): + """Clear the admin password for a server.""" + server = _find_server(cs, args.server) + server.clear_password() + + +def _print_floating_ip_list(floating_ips): + convert = [('instance_id', 'server_id')] + _translate_keys(floating_ips, convert) + + utils.print_list(floating_ips, + ['Id', 'IP', 'Server Id', 'Fixed IP', 'Pool']) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + '--length', + metavar='', + default=None, + help=_('Length in lines to tail.')) +def do_console_log(cs, args): + """Get console log output of a server.""" + server = _find_server(cs, args.server) + data = server.get_console_output(length=args.length) + print(data) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('address', metavar='
', help=_('IP Address.')) +@cliutils.arg( + '--fixed-address', + metavar='', + default=None, + help=_('Fixed IP Address to associate with.')) +def do_add_floating_ip(cs, args): + """DEPRECATED, use floating-ip-associate instead.""" + _associate_floating_ip(cs, args) + + +@cliutils.arg('server', metavar='', help='Name or ID of server.') +@cliutils.arg('address', metavar='
', help='IP Address.') +@cliutils.arg( + '--fixed-address', + metavar='', + default=None, + help='Fixed IP Address to associate with.') +def do_floating_ip_associate(cs, args): + """Associate a floating IP address to a server.""" + _associate_floating_ip(cs, args) + + +def _associate_floating_ip(cs, args): + server = _find_server(cs, args.server) + server.add_floating_ip(args.address, args.fixed_address) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('address', metavar='
', help=_('IP Address.')) +def do_remove_floating_ip(cs, args): + """DEPRECATED, use floating-ip-disassociate instead.""" + _disassociate_floating_ip(cs, args) + + +@cliutils.arg('server', metavar='', help='Name or ID of server.') +@cliutils.arg('address', metavar='
', help='IP Address.') +def do_floating_ip_disassociate(cs, args): + """Disassociate a floating IP address from a server.""" + _disassociate_floating_ip(cs, args) + + +def _disassociate_floating_ip(cs, args): + server = _find_server(cs, args.server) + server.remove_floating_ip(args.address) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + 'secgroup', + metavar='', + help=_('Name of Security Group.')) +def do_add_secgroup(cs, args): + """Add a Security Group to a server.""" + server = _find_server(cs, args.server) + server.add_security_group(args.secgroup) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + 'secgroup', + metavar='', + help=_('Name of Security Group.')) +def do_remove_secgroup(cs, args): + """Remove a Security Group from a server.""" + server = _find_server(cs, args.server) + server.remove_security_group(args.secgroup) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_list_secgroup(cs, args): + """List Security Group(s) of a server.""" + server = _find_server(cs, args.server) + groups = server.list_security_group() + _print_secgroups(groups) + + +@cliutils.arg( + 'pool', + metavar='', + help=_('Name of Floating IP Pool. (Optional)'), + nargs='?', + default=None) +def do_floating_ip_create(cs, args): + """Allocate a floating IP for the current tenant.""" + _print_floating_ip_list([cs.floating_ips.create(pool=args.pool)]) + + +@cliutils.arg('address', metavar='
', help=_('IP of Floating IP.')) +def do_floating_ip_delete(cs, args): + """De-allocate a floating IP.""" + floating_ips = cs.floating_ips.list() + for floating_ip in floating_ips: + if floating_ip.ip == args.address: + return cs.floating_ips.delete(floating_ip.id) + raise exceptions.CommandError(_("Floating IP %s not found.") % + args.address) + + +@cliutils.arg( + '--all-tenants', + action='store_true', + default=False, + help=_('Display floatingips from all tenants (Admin only).')) +def do_floating_ip_list(cs, args): + """List floating IPs.""" + _print_floating_ip_list(cs.floating_ips.list(args.all_tenants)) + + +def do_floating_ip_pool_list(cs, _args): + """List all floating IP pools.""" + utils.print_list(cs.floating_ip_pools.list(), ['name']) + + +@cliutils.arg( + '--host', dest='host', metavar='', default=None, + help=_('Filter by host')) +def do_floating_ip_bulk_list(cs, args): + """List all floating IPs.""" + utils.print_list(cs.floating_ips_bulk.list(args.host), ['project_id', + 'address', + 'instance_uuid', + 'pool', + 'interface']) + + +@cliutils.arg('ip_range', metavar='', help=_('Address range to create')) +@cliutils.arg( + '--pool', dest='pool', metavar='', default=None, + help=_('Pool for new Floating IPs')) +@cliutils.arg( + '--interface', metavar='', default=None, + help=_('Interface for new Floating IPs')) +def do_floating_ip_bulk_create(cs, args): + """Bulk create floating IPs by range.""" + cs.floating_ips_bulk.create(args.ip_range, args.pool, args.interface) + + +@cliutils.arg('ip_range', metavar='', help=_('Address range to delete')) +def do_floating_ip_bulk_delete(cs, args): + """Bulk delete floating IPs by range.""" + cs.floating_ips_bulk.delete(args.ip_range) + + +def _print_dns_list(dns_entries): + utils.print_list(dns_entries, ['ip', 'name', 'domain']) + + +def _print_domain_list(domain_entries): + utils.print_list(domain_entries, ['domain', 'scope', + 'project', 'availability_zone']) + + +def do_dns_domains(cs, args): + """Print a list of available dns domains.""" + domains = cs.dns_domains.domains() + _print_domain_list(domains) + + +@cliutils.arg('domain', metavar='', help=_('DNS domain')) +@cliutils.arg('--ip', metavar='', help=_('IP address'), default=None) +@cliutils.arg('--name', metavar='', help=_('DNS name'), default=None) +def do_dns_list(cs, args): + """List current DNS entries for domain and IP or domain and name.""" + if not (args.ip or args.name): + raise exceptions.CommandError( + _("You must specify either --ip or --name")) + if args.name: + entry = cs.dns_entries.get(args.domain, args.name) + _print_dns_list([entry]) + else: + entries = cs.dns_entries.get_for_ip(args.domain, + ip=args.ip) + _print_dns_list(entries) + + +@cliutils.arg('ip', metavar='', help=_('IP address')) +@cliutils.arg('name', metavar='', help=_('DNS name')) +@cliutils.arg('domain', metavar='', help=_('DNS domain')) +@cliutils.arg( + '--type', + metavar='', + help=_('dns type (e.g. "A")'), + default='A') +def do_dns_create(cs, args): + """Create a DNS entry for domain, name and IP.""" + cs.dns_entries.create(args.domain, args.name, args.ip, args.type) + + +@cliutils.arg('domain', metavar='', help=_('DNS domain')) +@cliutils.arg('name', metavar='', help=_('DNS name')) +def do_dns_delete(cs, args): + """Delete the specified DNS entry.""" + cs.dns_entries.delete(args.domain, args.name) + + +@cliutils.arg('domain', metavar='', help=_('DNS domain')) +def do_dns_delete_domain(cs, args): + """Delete the specified DNS domain.""" + cs.dns_domains.delete(args.domain) + + +@cliutils.arg('domain', metavar='', help=_('DNS domain')) +@cliutils.arg( + '--availability-zone', + metavar='', + default=None, + help=_('Limit access to this domain to servers ' + 'in the specified availability zone.')) +@cliutils.arg( + '--availability_zone', + help=argparse.SUPPRESS) +def do_dns_create_private_domain(cs, args): + """Create the specified DNS domain.""" + cs.dns_domains.create_private(args.domain, + args.availability_zone) + + +@cliutils.arg('domain', metavar='', help=_('DNS domain')) +@cliutils.arg( + '--project', metavar='', + help=_('Limit access to this domain to users ' + 'of the specified project.'), + default=None) +def do_dns_create_public_domain(cs, args): + """Create the specified DNS domain.""" + cs.dns_domains.create_public(args.domain, + args.project) + + +def _print_secgroup_rules(rules, show_source_group=True): + class FormattedRule(object): + def __init__(self, obj): + items = (obj if isinstance(obj, dict) else obj._info).items() + for k, v in items: + if k == 'ip_range': + v = v.get('cidr') + elif k == 'group': + k = 'source_group' + v = v.get('name') + if v is None: + v = '' + + setattr(self, k, v) + + rules = [FormattedRule(rule) for rule in rules] + headers = ['IP Protocol', 'From Port', 'To Port', 'IP Range'] + if show_source_group: + headers.append('Source Group') + utils.print_list(rules, headers) + + +def _print_secgroups(secgroups): + utils.print_list(secgroups, ['Id', 'Name', 'Description']) + + +def _get_secgroup(cs, secgroup): + # Check secgroup is an ID (nova-network) or UUID (neutron) + if (utils.is_integer_like(encodeutils.safe_encode(secgroup)) or + uuidutils.is_uuid_like(secgroup)): + try: + return cs.security_groups.get(secgroup) + except exceptions.NotFound: + pass + + # Check secgroup as a name + match_found = False + for s in cs.security_groups.list(): + encoding = ( + locale.getpreferredencoding() or sys.stdin.encoding or 'UTF-8') + if not six.PY3: + s.name = s.name.encode(encoding) + if secgroup == s.name: + if match_found is not False: + msg = (_("Multiple security group matches found for name '%s'" + ", use an ID to be more specific.") % secgroup) + raise exceptions.NoUniqueMatch(msg) + match_found = s + if match_found is False: + raise exceptions.CommandError(_("Secgroup ID or name '%s' not found.") + % secgroup) + return match_found + + +@cliutils.arg( + 'secgroup', + metavar='', + help=_('ID or name of security group.')) +@cliutils.arg( + 'ip_proto', + metavar='', + help=_('IP protocol (icmp, tcp, udp).')) +@cliutils.arg( + 'from_port', + metavar='', + help=_('Port at start of range.')) +@cliutils.arg( + 'to_port', + metavar='', + help=_('Port at end of range.')) +@cliutils.arg('cidr', metavar='', help=_('CIDR for address range.')) +def do_secgroup_add_rule(cs, args): + """Add a rule to a security group.""" + secgroup = _get_secgroup(cs, args.secgroup) + rule = cs.security_group_rules.create(secgroup.id, + args.ip_proto, + args.from_port, + args.to_port, + args.cidr) + _print_secgroup_rules([rule]) + + +@cliutils.arg( + 'secgroup', + metavar='', + help=_('ID or name of security group.')) +@cliutils.arg( + 'ip_proto', + metavar='', + help=_('IP protocol (icmp, tcp, udp).')) +@cliutils.arg( + 'from_port', + metavar='', + help=_('Port at start of range.')) +@cliutils.arg( + 'to_port', + metavar='', + help=_('Port at end of range.')) +@cliutils.arg('cidr', metavar='', help=_('CIDR for address range.')) +def do_secgroup_delete_rule(cs, args): + """Delete a rule from a security group.""" + secgroup = _get_secgroup(cs, args.secgroup) + for rule in secgroup.rules: + if (rule['ip_protocol'] and + rule['ip_protocol'].upper() == args.ip_proto.upper() and + rule['from_port'] == int(args.from_port) and + rule['to_port'] == int(args.to_port) and + rule['ip_range']['cidr'] == args.cidr): + _print_secgroup_rules([rule]) + return cs.security_group_rules.delete(rule['id']) + + raise exceptions.CommandError(_("Rule not found")) + + +@cliutils.arg('name', metavar='', help=_('Name of security group.')) +@cliutils.arg( + 'description', metavar='', + help=_('Description of security group.')) +def do_secgroup_create(cs, args): + """Create a security group.""" + secgroup = cs.security_groups.create(args.name, args.description) + _print_secgroups([secgroup]) + + +@cliutils.arg( + 'secgroup', + metavar='', + help=_('ID or name of security group.')) +@cliutils.arg('name', metavar='', help=_('Name of security group.')) +@cliutils.arg( + 'description', metavar='', + help=_('Description of security group.')) +def do_secgroup_update(cs, args): + """Update a security group.""" + sg = _get_secgroup(cs, args.secgroup) + secgroup = cs.security_groups.update(sg, args.name, args.description) + _print_secgroups([secgroup]) + + +@cliutils.arg( + 'secgroup', + metavar='', + help=_('ID or name of security group.')) +def do_secgroup_delete(cs, args): + """Delete a security group.""" + secgroup = _get_secgroup(cs, args.secgroup) + cs.security_groups.delete(secgroup) + _print_secgroups([secgroup]) + + +@cliutils.arg( + '--all-tenants', + dest='all_tenants', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=int(strutils.bool_from_string( + os.environ.get("ALL_TENANTS", 'false'), True)), + help=_('Display information from all tenants (Admin only).')) +@cliutils.arg( + '--all_tenants', + nargs='?', + type=int, + const=1, + help=argparse.SUPPRESS) +def do_secgroup_list(cs, args): + """List security groups for the current tenant.""" + search_opts = {'all_tenants': args.all_tenants} + columns = ['Id', 'Name', 'Description'] + if args.all_tenants: + columns.append('Tenant_ID') + groups = cs.security_groups.list(search_opts=search_opts) + utils.print_list(groups, columns) + + +@cliutils.arg( + 'secgroup', + metavar='', + help=_('ID or name of security group.')) +def do_secgroup_list_rules(cs, args): + """List rules for a security group.""" + secgroup = _get_secgroup(cs, args.secgroup) + _print_secgroup_rules(secgroup.rules) + + +@cliutils.arg( + 'secgroup', + metavar='', + help=_('ID or name of security group.')) +@cliutils.arg( + 'source_group', + metavar='', + help=_('ID or name of source group.')) +@cliutils.arg( + 'ip_proto', + metavar='', + help=_('IP protocol (icmp, tcp, udp).')) +@cliutils.arg( + 'from_port', + metavar='', + help=_('Port at start of range.')) +@cliutils.arg( + 'to_port', + metavar='', + help=_('Port at end of range.')) +def do_secgroup_add_group_rule(cs, args): + """Add a source group rule to a security group.""" + secgroup = _get_secgroup(cs, args.secgroup) + source_group = _get_secgroup(cs, args.source_group) + params = {} + params['group_id'] = source_group.id + + if args.ip_proto or args.from_port or args.to_port: + if not (args.ip_proto and args.from_port and args.to_port): + raise exceptions.CommandError(_("ip_proto, from_port, and to_port" + " must be specified together")) + params['ip_protocol'] = args.ip_proto.upper() + params['from_port'] = args.from_port + params['to_port'] = args.to_port + + rule = cs.security_group_rules.create(secgroup.id, **params) + _print_secgroup_rules([rule]) + + +@cliutils.arg( + 'secgroup', + metavar='', + help=_('ID or name of security group.')) +@cliutils.arg( + 'source_group', + metavar='', + help=_('ID or name of source group.')) +@cliutils.arg( + 'ip_proto', + metavar='', + help=_('IP protocol (icmp, tcp, udp).')) +@cliutils.arg( + 'from_port', + metavar='', + help=_('Port at start of range.')) +@cliutils.arg( + 'to_port', + metavar='', + help=_('Port at end of range.')) +def do_secgroup_delete_group_rule(cs, args): + """Delete a source group rule from a security group.""" + secgroup = _get_secgroup(cs, args.secgroup) + source_group = _get_secgroup(cs, args.source_group) + params = {} + params['group_name'] = source_group.name + + if args.ip_proto or args.from_port or args.to_port: + if not (args.ip_proto and args.from_port and args.to_port): + raise exceptions.CommandError(_("ip_proto, from_port, and to_port" + " must be specified together")) + params['ip_protocol'] = args.ip_proto.upper() + params['from_port'] = int(args.from_port) + params['to_port'] = int(args.to_port) + + for rule in secgroup.rules: + if (rule.get('ip_protocol') and + rule['ip_protocol'].upper() == params.get( + 'ip_protocol').upper() and + rule.get('from_port') == params.get('from_port') and + rule.get('to_port') == params.get('to_port') and + rule.get('group', {}).get('name') == params.get('group_name')): + return cs.security_group_rules.delete(rule['id']) + + raise exceptions.CommandError(_("Rule not found")) + + +@cliutils.arg('name', metavar='', help=_('Name of key.')) +@cliutils.arg( + '--pub-key', + metavar='', + default=None, + help=_('Path to a public ssh key.')) +@cliutils.arg( + '--pub_key', + help=argparse.SUPPRESS) +def do_keypair_add(cs, args): + """Create a new key pair for use with servers.""" + name = args.name + pub_key = args.pub_key + + if pub_key: + if pub_key == '-': + pub_key = sys.stdin.read() + else: + try: + with open(os.path.expanduser(pub_key)) as f: + pub_key = f.read() + except IOError as e: + raise exceptions.CommandError( + _("Can't open or read '%(key)s': %(exc)s") + % {'key': pub_key, 'exc': e} + ) + + keypair = cs.keypairs.create(name, pub_key) + + if not pub_key: + private_key = keypair.private_key + print(private_key) + + +@cliutils.arg('name', metavar='', help=_('Keypair name to delete.')) +def do_keypair_delete(cs, args): + """Delete keypair given by its name.""" + name = _find_keypair(cs, args.name) + cs.keypairs.delete(name) + + +def do_keypair_list(cs, args): + """Print a list of keypairs for a user""" + keypairs = cs.keypairs.list() + columns = ['Name', 'Fingerprint'] + utils.print_list(keypairs, columns) + + +def _print_keypair(keypair): + kp = keypair._info.copy() + pk = kp.pop('public_key') + utils.print_dict(kp) + print(_("Public key: %s") % pk) + + +@cliutils.arg( + 'keypair', + metavar='', + help=_("Name or ID of keypair")) +def do_keypair_show(cs, args): + """Show details about the given keypair.""" + keypair = _find_keypair(cs, args.keypair) + _print_keypair(keypair) + + +def _find_keypair(cs, keypair): + """Get a keypair by name or ID.""" + return utils.find_resource(cs.keypairs, keypair) + + +@cliutils.arg( + '--tenant', + # nova db searches by project_id + dest='tenant', + metavar='', + nargs='?', + help=_('Display information from single tenant (Admin only).')) +@cliutils.arg( + '--reserved', + dest='reserved', + action='store_true', + default=False, + help=_('Include reservations count.')) +def do_absolute_limits(cs, args): + """Print a list of absolute limits for a user""" + limits = cs.limits.get(args.reserved, args.tenant).absolute + + class Limit(object): + def __init__(self, name, used, max, other): + self.name = name + self.used = used + self.max = max + self.other = other + + limit_map = { + 'maxServerMeta': {'name': 'Server Meta', 'type': 'max'}, + 'maxPersonality': {'name': 'Personality', 'type': 'max'}, + 'maxPersonalitySize': {'name': 'Personality Size', 'type': 'max'}, + 'maxImageMeta': {'name': 'ImageMeta', 'type': 'max'}, + 'maxTotalKeypairs': {'name': 'Keypairs', 'type': 'max'}, + 'totalCoresUsed': {'name': 'Cores', 'type': 'used'}, + 'maxTotalCores': {'name': 'Cores', 'type': 'max'}, + 'totalRAMUsed': {'name': 'RAM', 'type': 'used'}, + 'maxTotalRAMSize': {'name': 'RAM', 'type': 'max'}, + 'totalInstancesUsed': {'name': 'Instances', 'type': 'used'}, + 'maxTotalInstances': {'name': 'Instances', 'type': 'max'}, + 'totalFloatingIpsUsed': {'name': 'FloatingIps', 'type': 'used'}, + 'maxTotalFloatingIps': {'name': 'FloatingIps', 'type': 'max'}, + 'totalSecurityGroupsUsed': {'name': 'SecurityGroups', 'type': 'used'}, + 'maxSecurityGroups': {'name': 'SecurityGroups', 'type': 'max'}, + 'maxSecurityGroupRules': {'name': 'SecurityGroupRules', 'type': 'max'}, + 'maxServerGroups': {'name': 'ServerGroups', 'type': 'max'}, + 'totalServerGroupsUsed': {'name': 'ServerGroups', 'type': 'used'}, + 'maxServerGroupMembers': {'name': 'ServerGroupMembers', 'type': 'max'}, + } + + max = {} + used = {} + other = {} + limit_names = [] + columns = ['Name', 'Used', 'Max'] + for l in limits: + map = limit_map.get(l.name, {'name': l.name, 'type': 'other'}) + name = map['name'] + if map['type'] == 'max': + max[name] = l.value + elif map['type'] == 'used': + used[name] = l.value + else: + other[name] = l.value + columns.append('Other') + if name not in limit_names: + limit_names.append(name) + + limit_names.sort() + + limit_list = [] + for name in limit_names: + l = Limit(name, + used.get(name, "-"), + max.get(name, "-"), + other.get(name, "-")) + limit_list.append(l) + + utils.print_list(limit_list, columns) + + +def do_rate_limits(cs, args): + """Print a list of rate limits for a user""" + limits = cs.limits.get().rate + columns = ['Verb', 'URI', 'Value', 'Remain', 'Unit', 'Next_Available'] + utils.print_list(limits, columns) + + +@cliutils.arg( + '--start', + metavar='', + help=_('Usage range start date ex 2012-01-20 (default: 4 weeks ago)'), + default=None) +@cliutils.arg( + '--end', + metavar='', + help=_('Usage range end date, ex 2012-01-20 (default: tomorrow)'), + default=None) +def do_usage_list(cs, args): + """List usage data for all tenants.""" + dateformat = "%Y-%m-%d" + rows = ["Tenant ID", "Servers", "RAM MB-Hours", "CPU Hours", + "Disk GB-Hours"] + + now = timeutils.utcnow() + + if args.start: + start = datetime.datetime.strptime(args.start, dateformat) + else: + start = now - datetime.timedelta(weeks=4) + + if args.end: + end = datetime.datetime.strptime(args.end, dateformat) + else: + end = now + datetime.timedelta(days=1) + + def simplify_usage(u): + simplerows = [x.lower().replace(" ", "_") for x in rows] + + setattr(u, simplerows[0], u.tenant_id) + setattr(u, simplerows[1], "%d" % len(u.server_usages)) + setattr(u, simplerows[2], "%.2f" % u.total_memory_mb_usage) + setattr(u, simplerows[3], "%.2f" % u.total_vcpus_usage) + setattr(u, simplerows[4], "%.2f" % u.total_local_gb_usage) + + usage_list = cs.usage.list(start, end, detailed=True) + + print(_("Usage from %(start)s to %(end)s:") % + {'start': start.strftime(dateformat), + 'end': end.strftime(dateformat)}) + + for usage in usage_list: + simplify_usage(usage) + + utils.print_list(usage_list, rows) + + +@cliutils.arg( + '--start', + metavar='', + help=_('Usage range start date ex 2012-01-20 (default: 4 weeks ago)'), + default=None) +@cliutils.arg( + '--end', metavar='', + help=_('Usage range end date, ex 2012-01-20 (default: tomorrow)'), + default=None) +@cliutils.arg( + '--tenant', + metavar='', + default=None, + help=_('UUID of tenant to get usage for.')) +def do_usage(cs, args): + """Show usage data for a single tenant.""" + dateformat = "%Y-%m-%d" + rows = ["Servers", "RAM MB-Hours", "CPU Hours", "Disk GB-Hours"] + + now = timeutils.utcnow() + + if args.start: + start = datetime.datetime.strptime(args.start, dateformat) + else: + start = now - datetime.timedelta(weeks=4) + + if args.end: + end = datetime.datetime.strptime(args.end, dateformat) + else: + end = now + datetime.timedelta(days=1) + + def simplify_usage(u): + simplerows = [x.lower().replace(" ", "_") for x in rows] + + setattr(u, simplerows[0], "%d" % len(u.server_usages)) + setattr(u, simplerows[1], "%.2f" % u.total_memory_mb_usage) + setattr(u, simplerows[2], "%.2f" % u.total_vcpus_usage) + setattr(u, simplerows[3], "%.2f" % u.total_local_gb_usage) + + if args.tenant: + usage = cs.usage.get(args.tenant, start, end) + else: + if isinstance(cs.client, client.SessionClient): + auth = cs.client.auth + project_id = auth.get_auth_ref(cs.client.session).project_id + usage = cs.usage.get(project_id, start, end) + else: + usage = cs.usage.get(cs.client.tenant_id, start, end) + + print(_("Usage from %(start)s to %(end)s:") % + {'start': start.strftime(dateformat), + 'end': end.strftime(dateformat)}) + + if getattr(usage, 'total_vcpus_usage', None): + simplify_usage(usage) + utils.print_list([usage], rows) + else: + print(_('None')) + + +@cliutils.arg( + 'pk_filename', + metavar='', + nargs='?', + default='pk.pem', + help=_('Filename for the private key [Default: pk.pem]')) +@cliutils.arg( + 'cert_filename', + metavar='', + nargs='?', + default='cert.pem', + help=_('Filename for the X.509 certificate [Default: cert.pem]')) +def do_x509_create_cert(cs, args): + """Create x509 cert for a user in tenant.""" + + if os.path.exists(args.pk_filename): + raise exceptions.CommandError(_("Unable to write privatekey - %s " + "exists.") % args.pk_filename) + if os.path.exists(args.cert_filename): + raise exceptions.CommandError(_("Unable to write x509 cert - %s " + "exists.") % args.cert_filename) + + certs = cs.certs.create() + + try: + old_umask = os.umask(0o377) + with open(args.pk_filename, 'w') as private_key: + private_key.write(certs.private_key) + print(_("Wrote private key to %s") % args.pk_filename) + finally: + os.umask(old_umask) + + with open(args.cert_filename, 'w') as cert: + cert.write(certs.data) + print(_("Wrote x509 certificate to %s") % args.cert_filename) + + +@cliutils.arg( + 'filename', + metavar='', + nargs='?', + default='cacert.pem', + help=_('Filename to write the x509 root cert.')) +def do_x509_get_root_cert(cs, args): + """Fetch the x509 root cert.""" + if os.path.exists(args.filename): + raise exceptions.CommandError(_("Unable to write x509 root cert - \ + %s exists.") % args.filename) + + with open(args.filename, 'w') as cert: + cacert = cs.certs.get() + cert.write(cacert.data) + print(_("Wrote x509 root cert to %s") % args.filename) + + +@cliutils.arg( + '--hypervisor', + metavar='', + default=None, + help=_('type of hypervisor.')) +def do_agent_list(cs, args): + """List all builds.""" + result = cs.agents.list(args.hypervisor) + columns = ["Agent_id", "Hypervisor", "OS", "Architecture", "Version", + 'Md5hash', 'Url'] + utils.print_list(result, columns) + + +@cliutils.arg('os', metavar='', help=_('type of os.')) +@cliutils.arg( + 'architecture', + metavar='', + help=_('type of architecture')) +@cliutils.arg('version', metavar='', help=_('version')) +@cliutils.arg('url', metavar='', help=_('url')) +@cliutils.arg('md5hash', metavar='', help=_('md5 hash')) +@cliutils.arg( + 'hypervisor', + metavar='', + default='xen', + help=_('type of hypervisor.')) +def do_agent_create(cs, args): + """Create new agent build.""" + result = cs.agents.create(args.os, args.architecture, + args.version, args.url, + args.md5hash, args.hypervisor) + utils.print_dict(result._info.copy()) + + +@cliutils.arg('id', metavar='', help=_('id of the agent-build')) +def do_agent_delete(cs, args): + """Delete existing agent build.""" + cs.agents.delete(args.id) + + +@cliutils.arg('id', metavar='', help=_('id of the agent-build')) +@cliutils.arg('version', metavar='', help=_('version')) +@cliutils.arg('url', metavar='', help=_('url')) +@cliutils.arg('md5hash', metavar='', help=_('md5hash')) +def do_agent_modify(cs, args): + """Modify existing agent build.""" + result = cs.agents.update(args.id, args.version, + args.url, args.md5hash) + utils.print_dict(result._info) + + +def _find_aggregate(cs, aggregate): + """Get a aggregate by name or ID.""" + return utils.find_resource(cs.aggregates, aggregate) + + +def do_aggregate_list(cs, args): + """Print a list of all aggregates.""" + aggregates = cs.aggregates.list() + columns = ['Id', 'Name', 'Availability Zone'] + utils.print_list(aggregates, columns) + + +@cliutils.arg('name', metavar='', help=_('Name of aggregate.')) +@cliutils.arg( + 'availability_zone', + metavar='', + default=None, + nargs='?', + help=_('The availability zone of the aggregate (optional).')) +def do_aggregate_create(cs, args): + """Create a new aggregate with the specified details.""" + aggregate = cs.aggregates.create(args.name, args.availability_zone) + _print_aggregate_details(aggregate) + + +@cliutils.arg( + 'aggregate', + metavar='', + help=_('Name or ID of aggregate to delete.')) +def do_aggregate_delete(cs, args): + """Delete the aggregate.""" + aggregate = _find_aggregate(cs, args.aggregate) + cs.aggregates.delete(aggregate) + print(_("Aggregate %s has been successfully deleted.") % aggregate.id) + + +@cliutils.arg( + 'aggregate', + metavar='', + help=_('Name or ID of aggregate to update.')) +@cliutils.arg('name', metavar='', help=_('Name of aggregate.')) +@cliutils.arg( + 'availability_zone', + metavar='', + nargs='?', + default=None, + help=_('The availability zone of the aggregate.')) +def do_aggregate_update(cs, args): + """Update the aggregate's name and optionally availability zone.""" + aggregate = _find_aggregate(cs, args.aggregate) + updates = {"name": args.name} + if args.availability_zone: + updates["availability_zone"] = args.availability_zone + + aggregate = cs.aggregates.update(aggregate.id, updates) + print(_("Aggregate %s has been successfully updated.") % aggregate.id) + _print_aggregate_details(aggregate) + + +@cliutils.arg( + 'aggregate', metavar='', + help=_('Name or ID of aggregate to update.')) +@cliutils.arg( + 'metadata', + metavar='', + nargs='+', + action='append', + default=[], + help=_('Metadata to add/update to aggregate. ' + 'Specify only the key to delete a metadata item.')) +def do_aggregate_set_metadata(cs, args): + """Update the metadata associated with the aggregate.""" + aggregate = _find_aggregate(cs, args.aggregate) + metadata = _extract_metadata(args) + currentmetadata = getattr(aggregate, 'metadata', {}) + if set(metadata.items()) & set(currentmetadata.items()): + raise exceptions.CommandError(_("metadata already exists")) + for key, value in metadata.items(): + if value is None and key not in currentmetadata: + raise exceptions.CommandError(_("metadata key %s does not exist" + " hence can not be deleted") + % key) + aggregate = cs.aggregates.set_metadata(aggregate.id, metadata) + print(_("Metadata has been successfully updated for aggregate %s.") % + aggregate.id) + _print_aggregate_details(aggregate) + + +@cliutils.arg( + 'aggregate', metavar='', + help=_('Name or ID of aggregate.')) +@cliutils.arg( + 'host', metavar='', + help=_('The host to add to the aggregate.')) +def do_aggregate_add_host(cs, args): + """Add the host to the specified aggregate.""" + aggregate = _find_aggregate(cs, args.aggregate) + aggregate = cs.aggregates.add_host(aggregate.id, args.host) + print(_("Host %(host)s has been successfully added for aggregate " + "%(aggregate_id)s ") % {'host': args.host, + 'aggregate_id': aggregate.id}) + _print_aggregate_details(aggregate) + + +@cliutils.arg( + 'aggregate', metavar='', + help=_('Name or ID of aggregate.')) +@cliutils.arg( + 'host', metavar='', + help=_('The host to remove from the aggregate.')) +def do_aggregate_remove_host(cs, args): + """Remove the specified host from the specified aggregate.""" + aggregate = _find_aggregate(cs, args.aggregate) + aggregate = cs.aggregates.remove_host(aggregate.id, args.host) + print(_("Host %(host)s has been successfully removed from aggregate " + "%(aggregate_id)s ") % {'host': args.host, + 'aggregate_id': aggregate.id}) + _print_aggregate_details(aggregate) + + +@cliutils.arg( + 'aggregate', metavar='', + help=_('Name or ID of aggregate.')) +def do_aggregate_details(cs, args): + """Show details of the specified aggregate.""" + aggregate = _find_aggregate(cs, args.aggregate) + _print_aggregate_details(aggregate) + + +def _print_aggregate_details(aggregate): + columns = ['Id', 'Name', 'Availability Zone', 'Hosts', 'Metadata'] + + def parser_metadata(fields): + return utils.pretty_choice_dict(getattr(fields, 'metadata', {}) or {}) + + def parser_hosts(fields): + return cliutils.pretty_choice_list(getattr(fields, 'hosts', [])) + + formatters = { + 'Metadata': parser_metadata, + 'Hosts': parser_hosts, + } + utils.print_list([aggregate], columns, formatters=formatters) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + 'host', metavar='', default=None, nargs='?', + help=_('destination host name.')) +@cliutils.arg( + '--block-migrate', + action='store_true', + dest='block_migrate', + default=False, + help=_('True in case of block_migration. (Default=False:live_migration)')) +@cliutils.arg( + '--block_migrate', + action='store_true', + help=argparse.SUPPRESS) +@cliutils.arg( + '--disk-over-commit', + action='store_true', + dest='disk_over_commit', + default=False, + help=_('Allow overcommit.(Default=False)')) +@cliutils.arg( + '--disk_over_commit', + action='store_true', + help=argparse.SUPPRESS) +def do_live_migration(cs, args): + """Migrate running server to a new machine.""" + _find_server(cs, args.server).live_migrate(args.host, + args.block_migrate, + args.disk_over_commit) + + +@cliutils.arg( + 'server', metavar='', nargs='+', + help=_('Name or ID of server(s).')) +@cliutils.arg( + '--active', action='store_const', dest='state', + default='error', const='active', + help=_('Request the server be reset to "active" state instead ' + 'of "error" state (the default).')) +def do_reset_state(cs, args): + """Reset the state of a server.""" + failure_flag = False + + for server in args.server: + try: + _find_server(cs, server).reset_state(args.state) + except Exception as e: + failure_flag = True + msg = "Reset state for server %s failed: %s" % (server, e) + print(msg) + + if failure_flag: + msg = "Unable to reset the state for the specified server(s)." + raise exceptions.CommandError(msg) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_reset_network(cs, args): + """Reset network of a server.""" + _find_server(cs, args.server).reset_network() + + +@cliutils.arg( + '--host', + metavar='', + default=None, + help=_('Name of host.')) +@cliutils.arg( + '--binary', + metavar='', + default=None, + help=_('Service binary.')) +def do_service_list(cs, args): + """Show a list of all running services. Filter by host & binary.""" + result = cs.services.list(host=args.host, binary=args.binary) + columns = ["Binary", "Host", "Zone", "Status", "State", "Updated_at"] + # NOTE(sulo): we check if the response has disabled_reason + # so as not to add the column when the extended ext is not enabled. + if result and hasattr(result[0], 'disabled_reason'): + columns.append("Disabled Reason") + + # NOTE(gtt): After https://review.openstack.org/#/c/39998/ nova will + # show id in response. + if result and hasattr(result[0], 'id'): + columns.insert(0, "Id") + + utils.print_list(result, columns) + + +@cliutils.arg('host', metavar='', help=_('Name of host.')) +@cliutils.arg('binary', metavar='', help=_('Service binary.')) +def do_service_enable(cs, args): + """Enable the service.""" + result = cs.services.enable(args.host, args.binary) + utils.print_list([result], ['Host', 'Binary', 'Status']) + + +@cliutils.arg('host', metavar='', help=_('Name of host.')) +@cliutils.arg('binary', metavar='', help=_('Service binary.')) +@cliutils.arg( + '--reason', + metavar='', + help=_('Reason for disabling service.')) +def do_service_disable(cs, args): + """Disable the service.""" + if args.reason: + result = cs.services.disable_log_reason(args.host, args.binary, + args.reason) + utils.print_list([result], ['Host', 'Binary', 'Status', + 'Disabled Reason']) + else: + result = cs.services.disable(args.host, args.binary) + utils.print_list([result], ['Host', 'Binary', 'Status']) + + +@cliutils.arg('id', metavar='', help=_('Id of service.')) +def do_service_delete(cs, args): + """Delete the service.""" + cs.services.delete(args.id) + + +@cliutils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) +def do_fixed_ip_get(cs, args): + """Retrieve info on a fixed IP.""" + result = cs.fixed_ips.get(args.fixed_ip) + utils.print_list([result], ['address', 'cidr', 'hostname', 'host']) + + +@cliutils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) +def do_fixed_ip_reserve(cs, args): + """Reserve a fixed IP.""" + cs.fixed_ips.reserve(args.fixed_ip) + + +@cliutils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) +def do_fixed_ip_unreserve(cs, args): + """Unreserve a fixed IP.""" + cs.fixed_ips.unreserve(args.fixed_ip) + + +@cliutils.arg('host', metavar='', help=_('Name of host.')) +def do_host_describe(cs, args): + """Describe a specific host.""" + result = cs.hosts.get(args.host) + columns = ["HOST", "PROJECT", "cpu", "memory_mb", "disk_gb"] + utils.print_list(result, columns) + + +@cliutils.arg( + '--zone', + metavar='', + default=None, + help=_('Filters the list, returning only those hosts in the availability ' + 'zone .')) +def do_host_list(cs, args): + """List all hosts by service.""" + columns = ["host_name", "service", "zone"] + result = cs.hosts.list(args.zone) + utils.print_list(result, columns) + + +@cliutils.arg('host', metavar='', help='Name of host.') +@cliutils.arg( + '--status', metavar='', default=None, dest='status', + help=_('Either enable or disable a host.')) +@cliutils.arg( + '--maintenance', + metavar='', + default=None, + dest='maintenance', + help=_('Either put or resume host to/from maintenance.')) +def do_host_update(cs, args): + """Update host settings.""" + updates = {} + columns = ["HOST"] + if args.status: + updates['status'] = args.status + columns.append("status") + if args.maintenance: + updates['maintenance_mode'] = args.maintenance + columns.append("maintenance_mode") + result = cs.hosts.update(args.host, updates) + utils.print_list([result], columns) + + +@cliutils.arg('host', metavar='', help='Name of host.') +@cliutils.arg( + '--action', metavar='', dest='action', + choices=['startup', 'shutdown', 'reboot'], + help=_('A power action: startup, reboot, or shutdown.')) +def do_host_action(cs, args): + """Perform a power action on a host.""" + result = cs.hosts.host_action(args.host, args.action) + utils.print_list([result], ['HOST', 'power_action']) + + +def _find_hypervisor(cs, hypervisor): + """Get a hypervisor by name or ID.""" + return utils.find_resource(cs.hypervisors, hypervisor) + + +@cliutils.arg( + '--matching', + metavar='', + default=None, + help=_('List hypervisors matching the given .')) +def do_hypervisor_list(cs, args): + """List hypervisors.""" + columns = ['ID', 'Hypervisor hostname', 'State', 'Status'] + if args.matching: + utils.print_list(cs.hypervisors.search(args.matching), columns) + else: + # Since we're not outputting detail data, choose + # detailed=False for server-side efficiency + utils.print_list(cs.hypervisors.list(False), columns) + + +@cliutils.arg( + 'hostname', + metavar='', + help=_('The hypervisor hostname (or pattern) to search for.')) +def do_hypervisor_servers(cs, args): + """List servers belonging to specific hypervisors.""" + hypers = cs.hypervisors.search(args.hostname, servers=True) + + class InstanceOnHyper(object): + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + # Massage the result into a list to be displayed + instances = [] + for hyper in hypers: + hyper_host = hyper.hypervisor_hostname + hyper_id = hyper.id + if hasattr(hyper, 'servers'): + instances.extend([InstanceOnHyper(id=serv['uuid'], + name=serv['name'], + hypervisor_hostname=hyper_host, + hypervisor_id=hyper_id) + for serv in hyper.servers]) + + # Output the data + utils.print_list(instances, ['ID', 'Name', 'Hypervisor ID', + 'Hypervisor Hostname']) + + +@cliutils.arg( + 'hypervisor', + metavar='', + help=_('Name or ID of the hypervisor to show the details of.')) +def do_hypervisor_show(cs, args): + """Display the details of the specified hypervisor.""" + hyper = _find_hypervisor(cs, args.hypervisor) + utils.print_dict(utils.flatten_dict(hyper._info)) + + +@cliutils.arg( + 'hypervisor', + metavar='', + help=_('Name or ID of the hypervisor to show the uptime of.')) +def do_hypervisor_uptime(cs, args): + """Display the uptime of the specified hypervisor.""" + hyper = _find_hypervisor(cs, args.hypervisor) + hyper = cs.hypervisors.uptime(hyper) + + # Output the uptime information + utils.print_dict(hyper._info.copy()) + + +def do_hypervisor_stats(cs, args): + """Get hypervisor statistics over all compute nodes.""" + stats = cs.hypervisor_stats.statistics() + utils.print_dict(stats._info.copy()) + + +def ensure_service_catalog_present(cs): + if not hasattr(cs.client, 'service_catalog'): + # Turn off token caching and re-auth + cs.client.unauthenticate() + cs.client.use_token_cache(False) + cs.client.authenticate() + + +def do_endpoints(cs, _args): + """Discover endpoints that get returned from the authenticate services.""" + if isinstance(cs.client, client.SessionClient): + auth = cs.client.auth + sc = auth.get_access(cs.client.session).service_catalog + for service in sc.get_data(): + _print_endpoints(service, cs.client.region_name) + else: + ensure_service_catalog_present(cs) + + catalog = cs.client.service_catalog.catalog + region = cs.client.region_name + for service in catalog['access']['serviceCatalog']: + _print_endpoints(service, region) + + +def _print_endpoints(service, region): + name, endpoints = service["name"], service["endpoints"] + + try: + endpoint = _get_first_endpoint(endpoints, region) + utils.print_dict(endpoint, name) + except LookupError: + print(_("WARNING: %(service)s has no endpoint in %(region)s! " + "Available endpoints for this service:") % + {'service': name, 'region': region}) + for other_endpoint in endpoints: + utils.print_dict(other_endpoint, name) + + +def _get_first_endpoint(endpoints, region): + """Find the first suitable endpoint in endpoints. + + If there is only one endpoint, return it. If there is more than + one endpoint, return the first one with the given region. If there + are no endpoints, or there is more than one endpoint but none of + them match the given region, raise KeyError. + + """ + if len(endpoints) == 1: + return endpoints[0] + else: + for candidate_endpoint in endpoints: + if candidate_endpoint["region"] == region: + return candidate_endpoint + + raise LookupError("No suitable endpoint found") + + +@cliutils.arg( + '--wrap', dest='wrap', metavar='', default=64, + help=_('wrap PKI tokens to a specified length, or 0 to disable')) +def do_credentials(cs, _args): + """Show user credentials returned from auth.""" + if isinstance(cs.client, client.SessionClient): + auth = cs.client.auth + sc = auth.get_access(cs.client.session).service_catalog + utils.print_dict(sc.catalog['user'], 'User Credentials', + wrap=int(_args.wrap)) + utils.print_dict(sc.get_token(), 'Token', wrap=int(_args.wrap)) + else: + ensure_service_catalog_present(cs) + catalog = cs.client.service_catalog.catalog + utils.print_dict(catalog['access']['user'], "User Credentials", + wrap=int(_args.wrap)) + utils.print_dict(catalog['access']['token'], "Token", + wrap=int(_args.wrap)) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + '--port', + dest='port', + action='store', + type=int, + default=22, + help=_('Optional flag to indicate which port to use for ssh. ' + '(Default=22)')) +@cliutils.arg( + '--private', + dest='private', + action='store_true', + default=False, + help=argparse.SUPPRESS) +@cliutils.arg( + '--address-type', + dest='address_type', + action='store', + type=str, + default='floating', + help=_('Optional flag to indicate which IP type to use. Possible values ' + 'includes fixed and floating (the Default).')) +@cliutils.arg( + '--network', metavar='', + help=_('Network to use for the ssh.'), default=None) +@cliutils.arg( + '--ipv6', + dest='ipv6', + action='store_true', + default=False, + help=_('Optional flag to indicate whether to use an IPv6 address ' + 'attached to a server. (Defaults to IPv4 address)')) +@cliutils.arg( + '--login', metavar='', help=_('Login to use.'), + default="root") +@cliutils.arg( + '-i', '--identity', + dest='identity', + help=_('Private key file, same as the -i option to the ssh command.'), + default='') +@cliutils.arg( + '--extra-opts', + dest='extra', + help=_('Extra options to pass to ssh. see: man ssh'), + default='') +def do_ssh(cs, args): + """SSH into a server.""" + if '@' in args.server: + user, server = args.server.split('@', 1) + args.login = user + args.server = server + + addresses = _find_server(cs, args.server).addresses + address_type = "fixed" if args.private else args.address_type + version = 6 if args.ipv6 else 4 + pretty_version = 'IPv%d' % version + + # Select the network to use. + if args.network: + network_addresses = addresses.get(args.network) + if not network_addresses: + msg = _("Server '%(server)s' is not attached to network " + "'%(network)s'") + raise exceptions.ResourceNotFound( + msg % {'server': args.server, 'network': args.network}) + else: + if len(addresses) > 1: + msg = _("Server '%(server)s' is attached to more than one network." + " Please pick the network to use.") + raise exceptions.CommandError(msg % {'server': args.server}) + elif not addresses: + msg = _("Server '%(server)s' is not attached to any network.") + raise exceptions.CommandError(msg % {'server': args.server}) + else: + network_addresses = list(six.itervalues(addresses))[0] + + # Select the address in the selected network. + # If the extension is not present, we assume the address to be floating. + match = lambda addr: all(( + addr.get('version') == version, + addr.get('OS-EXT-IPS:type', 'floating') == address_type)) + matching_addresses = [address.get('addr') + for address in network_addresses if match(address)] + if not any(matching_addresses): + msg = _("No address that would match network '%(network)s'" + " and type '%(address_type)s' of version %(pretty_version)s " + "has been found for server '%(server)s'.") + raise exceptions.ResourceNotFound(msg % { + 'network': args.network, 'address_type': address_type, + 'pretty_version': pretty_version, 'server': args.server}) + elif len(matching_addresses) > 1: + msg = _("More than one %(pretty_version)s %(address_type)s address" + "found.") + raise exceptions.CommandError(msg % {'pretty_version': pretty_version, + 'address_type': address_type}) + else: + ip_address = matching_addresses[0] + + identity = '-i %s' % args.identity if len(args.identity) else '' + + cmd = "ssh -%d -p%d %s %s@%s %s" % (version, args.port, identity, + args.login, ip_address, args.extra) + logger.debug("Executing cmd '%s'", cmd) + os.system(cmd) + + +_quota_resources = ['instances', 'cores', 'ram', + 'floating_ips', 'fixed_ips', 'metadata_items', + 'injected_files', 'injected_file_content_bytes', + 'injected_file_path_bytes', 'key_pairs', + 'security_groups', 'security_group_rules', + 'server_groups', 'server_group_members'] + + +def _quota_show(quotas): + class FormattedQuota(object): + def __init__(self, key, value): + setattr(self, 'quota', key) + setattr(self, 'limit', value) + + quota_list = [] + for resource in _quota_resources: + try: + quota = FormattedQuota(resource, getattr(quotas, resource)) + quota_list.append(quota) + except AttributeError: + pass + columns = ['Quota', 'Limit'] + utils.print_list(quota_list, columns) + + +def _quota_update(manager, identifier, args): + updates = {} + for resource in _quota_resources: + val = getattr(args, resource, None) + if val is not None: + updates[resource] = val + + if updates: + # default value of force is None to make sure this client + # will be compatibile with old nova server + force_update = getattr(args, 'force', None) + user_id = getattr(args, 'user', None) + if isinstance(manager, quotas.QuotaSetManager): + manager.update(identifier, force=force_update, user_id=user_id, + **updates) + else: + manager.update(identifier, **updates) + + +@cliutils.arg( + '--tenant', + metavar='', + default=None, + help=_('ID of tenant to list the quotas for.')) +@cliutils.arg( + '--user', + metavar='', + default=None, + help=_('ID of user to list the quotas for.')) +def do_quota_show(cs, args): + """List the quotas for a tenant/user.""" + + if args.tenant: + project_id = args.tenant + elif isinstance(cs.client, client.SessionClient): + auth = cs.client.auth + project_id = auth.get_auth_ref(cs.client.session).project_id + else: + project_id = cs.client.tenant_id + + _quota_show(cs.quotas.get(project_id, user_id=args.user)) + + +@cliutils.arg( + '--tenant', + metavar='', + default=None, + help=_('ID of tenant to list the default quotas for.')) +def do_quota_defaults(cs, args): + """List the default quotas for a tenant.""" + + if args.tenant: + project_id = args.tenant + elif isinstance(cs.client, client.SessionClient): + auth = cs.client.auth + project_id = auth.get_auth_ref(cs.client.session).project_id + else: + project_id = cs.client.tenant_id + + _quota_show(cs.quotas.defaults(project_id)) + + +@cliutils.arg( + 'tenant', + metavar='', + help=_('ID of tenant to set the quotas for.')) +@cliutils.arg( + '--user', + metavar='', + default=None, + help=_('ID of user to set the quotas for.')) +@cliutils.arg( + '--instances', + metavar='', + type=int, default=None, + help=_('New value for the "instances" quota.')) +@cliutils.arg( + '--cores', + metavar='', + type=int, default=None, + help=_('New value for the "cores" quota.')) +@cliutils.arg( + '--ram', + metavar='', + type=int, default=None, + help=_('New value for the "ram" quota.')) +@cliutils.arg( + '--floating-ips', + metavar='', + type=int, + default=None, + help=_('New value for the "floating-ips" quota.')) +@cliutils.arg( + '--floating_ips', + type=int, + help=argparse.SUPPRESS) +@cliutils.arg( + '--fixed-ips', + metavar='', + type=int, + default=None, + help=_('New value for the "fixed-ips" quota.')) +@cliutils.arg( + '--metadata-items', + metavar='', + type=int, + default=None, + help=_('New value for the "metadata-items" quota.')) +@cliutils.arg( + '--metadata_items', + type=int, + help=argparse.SUPPRESS) +@cliutils.arg( + '--injected-files', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-files" quota.')) +@cliutils.arg( + '--injected_files', + type=int, + help=argparse.SUPPRESS) +@cliutils.arg( + '--injected-file-content-bytes', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-file-content-bytes" quota.')) +@cliutils.arg( + '--injected_file_content_bytes', + type=int, + help=argparse.SUPPRESS) +@cliutils.arg( + '--injected-file-path-bytes', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-file-path-bytes" quota.')) +@cliutils.arg( + '--key-pairs', + metavar='', + type=int, + default=None, + help=_('New value for the "key-pairs" quota.')) +@cliutils.arg( + '--security-groups', + metavar='', + type=int, + default=None, + help=_('New value for the "security-groups" quota.')) +@cliutils.arg( + '--security-group-rules', + metavar='', + type=int, + default=None, + help=_('New value for the "security-group-rules" quota.')) +@cliutils.arg( + '--server-groups', + metavar='', + type=int, + default=None, + help=_('New value for the "server-groups" quota.')) +@cliutils.arg( + '--server-group-members', + metavar='', + type=int, + default=None, + help=_('New value for the "server-group-members" quota.')) +@cliutils.arg( + '--force', + dest='force', + action="store_true", + default=None, + help=_('Whether force update the quota even if the already used and ' + 'reserved exceeds the new quota')) +def do_quota_update(cs, args): + """Update the quotas for a tenant/user.""" + + _quota_update(cs.quotas, args.tenant, args) + + +@cliutils.arg( + '--tenant', + metavar='', + required=True, + help=_('ID of tenant to delete quota for.')) +@cliutils.arg( + '--user', + metavar='', + help=_('ID of user to delete quota for.')) +def do_quota_delete(cs, args): + """Delete quota for a tenant/user so their quota will Revert + back to default. + """ + + cs.quotas.delete(args.tenant, user_id=args.user) + + +@cliutils.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)) + + +@cliutils.arg( + 'class_name', + metavar='', + help=_('Name of quota class to set the quotas for.')) +@cliutils.arg( + '--instances', + metavar='', + type=int, default=None, + help=_('New value for the "instances" quota.')) +@cliutils.arg( + '--cores', + metavar='', + type=int, default=None, + help=_('New value for the "cores" quota.')) +@cliutils.arg( + '--ram', + metavar='', + type=int, default=None, + help=_('New value for the "ram" quota.')) +@cliutils.arg( + '--floating-ips', + metavar='', + type=int, + default=None, + help=_('New value for the "floating-ips" quota.')) +@cliutils.arg( + '--floating_ips', + type=int, + help=argparse.SUPPRESS) +@cliutils.arg( + '--fixed-ips', + metavar='', + type=int, + default=None, + help=_('New value for the "fixed-ips" quota.')) +@cliutils.arg( + '--metadata-items', + metavar='', + type=int, + default=None, + help=_('New value for the "metadata-items" quota.')) +@cliutils.arg( + '--metadata_items', + type=int, + help=argparse.SUPPRESS) +@cliutils.arg( + '--injected-files', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-files" quota.')) +@cliutils.arg( + '--injected_files', + type=int, + help=argparse.SUPPRESS) +@cliutils.arg( + '--injected-file-content-bytes', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-file-content-bytes" quota.')) +@cliutils.arg( + '--injected_file_content_bytes', + type=int, + help=argparse.SUPPRESS) +@cliutils.arg( + '--injected-file-path-bytes', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-file-path-bytes" quota.')) +@cliutils.arg( + '--key-pairs', + metavar='', + type=int, + default=None, + help=_('New value for the "key-pairs" quota.')) +@cliutils.arg( + '--security-groups', + metavar='', + type=int, + default=None, + help=_('New value for the "security-groups" quota.')) +@cliutils.arg( + '--security-group-rules', + metavar='', + type=int, + default=None, + help=_('New value for the "security-group-rules" quota.')) +@cliutils.arg( + '--server-groups', + metavar='', + type=int, + default=None, + help=_('New value for the "server-groups" quota.')) +@cliutils.arg( + '--server-group-members', + metavar='', + type=int, + default=None, + help=_('New value for the "server-group-members" quota.')) +def do_quota_class_update(cs, args): + """Update the quotas for a quota class.""" + + _quota_update(cs.quota_classes, args.class_name, args) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + 'host', metavar='', nargs='?', + help=_("Name or ID of the target host. " + "If no host is specified, the scheduler will choose one.")) +@cliutils.arg( + '--password', + dest='password', + metavar='', + help=_("Set the provided admin password on the evacuated server. Not" + " applicable with on-shared-storage flag")) +@cliutils.arg( + '--on-shared-storage', + dest='on_shared_storage', + action="store_true", + default=False, + help=_('Specifies whether server files are located on shared storage')) +def do_evacuate(cs, args): + """Evacuate server from failed host.""" + + server = _find_server(cs, args.server) + + res = server.evacuate(args.host, args.on_shared_storage, args.password)[1] + if type(res) is dict: + utils.print_dict(res) + + +def _print_interfaces(interfaces): + columns = ['Port State', 'Port ID', 'Net ID', 'IP addresses', + 'MAC Addr'] + + class FormattedInterface(object): + def __init__(self, interface): + for col in columns: + key = col.lower().replace(" ", "_") + if hasattr(interface, key): + setattr(self, key, getattr(interface, key)) + self.ip_addresses = ",".join([fip['ip_address'] + for fip in interface.fixed_ips]) + utils.print_list([FormattedInterface(i) for i in interfaces], columns) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_interface_list(cs, args): + """List interfaces attached to a server.""" + server = _find_server(cs, args.server) + + res = server.interface_list() + if type(res) is list: + _print_interfaces(res) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + '--port-id', + metavar='', + help=_('Port ID.'), + dest="port_id") +@cliutils.arg( + '--net-id', + metavar='', + help=_('Network ID'), + default=None, dest="net_id") +@cliutils.arg( + '--fixed-ip', + metavar='', + help=_('Requested fixed IP.'), + default=None, dest="fixed_ip") +def do_interface_attach(cs, args): + """Attach a network interface to a server.""" + server = _find_server(cs, args.server) + + res = server.interface_attach(args.port_id, args.net_id, args.fixed_ip) + if type(res) is dict: + utils.print_dict(res) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('port_id', metavar='', help=_('Port ID.')) +def do_interface_detach(cs, args): + """Detach a network interface from a server.""" + server = _find_server(cs, args.server) + + res = server.interface_detach(args.port_id) + if type(res) is dict: + utils.print_dict(res) + + +def _treeizeAvailabilityZone(zone): + """Build a tree view for availability zones.""" + AvailabilityZone = availability_zones.AvailabilityZone + + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + result = [] + + # Zone tree view item + az.zoneName = zone.zoneName + az.zoneState = ('available' + if zone.zoneState['available'] else 'not available') + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + + if zone.hosts is not None: + zone_hosts = sorted(zone.hosts.items(), key=lambda x: x[0]) + for (host, services) in zone_hosts: + # Host tree view item + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + az.zoneName = '|- %s' % host + az.zoneState = '' + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + + for (svc, state) in services.items(): + # Service tree view item + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + az.zoneName = '| |- %s' % svc + az.zoneState = '%s %s %s' % ( + 'enabled' if state['active'] else 'disabled', + ':-)' if state['available'] else 'XXX', + state['updated_at']) + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + return result + + +@cliutils.service_type('compute') +def do_availability_zone_list(cs, _args): + """List all the availability zones.""" + try: + availability_zones = cs.availability_zones.list() + except exceptions.Forbidden as e: # policy doesn't allow probably + try: + availability_zones = cs.availability_zones.list(detailed=False) + except Exception: + raise e + + result = [] + for zone in availability_zones: + result += _treeizeAvailabilityZone(zone) + _translate_availability_zone_keys(result) + utils.print_list(result, ['Name', 'Status'], + sortby_index=None) + + +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) + + +def do_secgroup_list_default_rules(cs, args): + """List rules that will be added to the 'default' security group for + new tenants. + """ + _print_secgroup_rules(cs.security_group_default_rules.list(), + show_source_group=False) + + +@cliutils.arg( + 'ip_proto', + metavar='', + help=_('IP protocol (icmp, tcp, udp).')) +@cliutils.arg( + 'from_port', + metavar='', + help=_('Port at start of range.')) +@cliutils.arg( + 'to_port', + metavar='', + help=_('Port at end of range.')) +@cliutils.arg('cidr', metavar='', help=_('CIDR for address range.')) +def do_secgroup_add_default_rule(cs, args): + """Add a rule to the set of rules that will be added to the 'default' + security group for new tenants. + """ + rule = cs.security_group_default_rules.create(args.ip_proto, + args.from_port, + args.to_port, + args.cidr) + _print_secgroup_rules([rule], show_source_group=False) + + +@cliutils.arg( + 'ip_proto', + metavar='', + help=_('IP protocol (icmp, tcp, udp).')) +@cliutils.arg( + 'from_port', + metavar='', + help=_('Port at start of range.')) +@cliutils.arg( + 'to_port', + metavar='', + help=_('Port at end of range.')) +@cliutils.arg('cidr', metavar='', help=_('CIDR for address range.')) +def do_secgroup_delete_default_rule(cs, args): + """Delete a rule from the set of rules that will be added to the + 'default' security group for new tenants. + """ + for rule in cs.security_group_default_rules.list(): + if (rule.ip_protocol and + rule.ip_protocol.upper() == args.ip_proto.upper() and + rule.from_port == int(args.from_port) and + rule.to_port == int(args.to_port) and + rule.ip_range['cidr'] == args.cidr): + _print_secgroup_rules([rule], show_source_group=False) + return cs.security_group_default_rules.delete(rule.id) + + raise exceptions.CommandError(_("Rule not found")) + + +@cliutils.arg('name', metavar='', help='Server group name.') +# NOTE(wingwj): The '--policy' way is still reserved here for preserving +# the backwards compatibility of CLI, even if a user won't get this usage +# in '--help' description. It will be deprecated after an suitable deprecation +# 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/. +@cliutils.arg( + 'policy', + metavar='', + default=argparse.SUPPRESS, + nargs='*', + help='Policies for the server groups ("affinity" or "anti-affinity")') +@cliutils.arg( + '--policy', + default=[], + action='append', + help=argparse.SUPPRESS) +def do_server_group_create(cs, args): + """Create a new server group with the specified details.""" + if not args.policy: + 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]) + + +@cliutils.arg( + 'id', + metavar='', + nargs='+', + help="Unique ID(s) of the server group to delete") +def do_server_group_delete(cs, args): + """Delete specific server group(s).""" + failure_count = 0 + + 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.")) + + +@cliutils.arg( + 'id', + metavar='', + help="Unique ID of the server group to get") +def do_server_group_get(cs, args): + """Get a specific server group.""" + server_group = cs.server_groups.get(args.id) + _print_server_group_details([server_group]) + + +def do_version_list(cs, args): + """List all API versions.""" + result = cs.versions.list() + columns = ["Id", "Status", "Updated"] + utils.print_list(result, columns) diff --git a/awx/lib/site-packages/novaclient/v2/usage.py b/awx/lib/site-packages/novaclient/v2/usage.py new file mode 100644 index 0000000000..585ce11653 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/usage.py @@ -0,0 +1,61 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Usage interface. +""" + +from novaclient import base + + +class Usage(base.Resource): + """ + Usage contains information about a tenant's physical resource usage + """ + def __repr__(self): + return "" + + +class UsageManager(base.ManagerWithFind): + """ + Manage :class:`Usage` resources. + """ + resource_class = Usage + + def list(self, start, end, detailed=False): + """ + Get usage for all tenants + + :param start: :class:`datetime.datetime` Start date + :param end: :class:`datetime.datetime` End date + :param detailed: Whether to include information about each + instance whose usage is part of the report + :rtype: list of :class:`Usage`. + """ + return self._list( + "/os-simple-tenant-usage?start=%s&end=%s&detailed=%s" % + (start.isoformat(), end.isoformat(), int(bool(detailed))), + "tenant_usages") + + def get(self, tenant_id, start, end): + """ + Get usage for a specific tenant. + + :param tenant_id: Tenant ID to fetch usage for + :param start: :class:`datetime.datetime` Start date + :param end: :class:`datetime.datetime` End date + :rtype: :class:`Usage` + """ + return self._get("/os-simple-tenant-usage/%s?start=%s&end=%s" % + (tenant_id, start.isoformat(), end.isoformat()), + "tenant_usage") diff --git a/awx/lib/site-packages/novaclient/v2/versions.py b/awx/lib/site-packages/novaclient/v2/versions.py new file mode 100644 index 0000000000..5ab09a103a --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/versions.py @@ -0,0 +1,35 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +version interface +""" + +from novaclient import base + + +class Version(base.Resource): + """ + Compute REST API information + """ + def __repr__(self): + return "" + + +class VersionManager(base.ManagerWithFind): + resource_class = Version + + def list(self): + """List all versions.""" + return self._list(None, "versions") diff --git a/awx/lib/site-packages/novaclient/v2/virtual_interfaces.py b/awx/lib/site-packages/novaclient/v2/virtual_interfaces.py new file mode 100644 index 0000000000..9c04e8d866 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/virtual_interfaces.py @@ -0,0 +1,33 @@ +# 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. + +""" +Virtual Interfaces (1.1 extension). +""" + +from novaclient import base + + +class VirtualInterface(base.Resource): + def __repr__(self): + pass + + +class VirtualInterfaceManager(base.ManagerWithFind): + resource_class = VirtualInterface + + def list(self, instance_id): + return self._list('/servers/%s/os-virtual-interfaces' % instance_id, + 'virtual_interfaces') diff --git a/awx/lib/site-packages/novaclient/v2/volume_snapshots.py b/awx/lib/site-packages/novaclient/v2/volume_snapshots.py new file mode 100644 index 0000000000..6ff6deb9e1 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/volume_snapshots.py @@ -0,0 +1,94 @@ +# Copyright 2011 Denali Systems, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Volume snapshot interface (1.1 extension). +""" + +from novaclient import base + + +class Snapshot(base.Resource): + """ + A Snapshot is a point-in-time snapshot of an openstack volume. + """ + NAME_ATTR = 'display_name' + + def __repr__(self): + return "" % self.id + + def delete(self): + """ + Delete this snapshot. + """ + self.manager.delete(self) + + +class SnapshotManager(base.ManagerWithFind): + """ + Manage :class:`Snapshot` resources. + """ + resource_class = Snapshot + + def create(self, volume_id, force=False, display_name=None, + display_description=None): + + """ + Create a snapshot of the given volume. + + :param volume_id: The ID of the volume to snapshot. + :param force: If force is True, create a snapshot even if the volume is + attached to an instance. Default is False. + :param display_name: Name of the snapshot + :param display_description: Description of the snapshot + :rtype: :class:`Snapshot` + """ + with self.alternate_service_type('volume'): + body = {'snapshot': {'volume_id': volume_id, + 'force': force, + 'display_name': display_name, + 'display_description': display_description}} + return self._create('/snapshots', body, 'snapshot') + + def get(self, snapshot_id): + """ + Get a snapshot. + + :param snapshot_id: The ID of the snapshot to get. + :rtype: :class:`Snapshot` + """ + with self.alternate_service_type('volume'): + return self._get("/snapshots/%s" % snapshot_id, "snapshot") + + def list(self, detailed=True): + """ + Get a list of all snapshots. + + :rtype: list of :class:`Snapshot` + """ + with self.alternate_service_type('volume'): + if detailed is True: + return self._list("/snapshots/detail", "snapshots") + else: + return self._list("/snapshots", "snapshots") + + def delete(self, snapshot): + """ + Delete a snapshot. + + :param snapshot: The :class:`Snapshot` to delete. + """ + with self.alternate_service_type('volume'): + self._delete("/snapshots/%s" % base.getid(snapshot)) diff --git a/awx/lib/site-packages/novaclient/v2/volume_types.py b/awx/lib/site-packages/novaclient/v2/volume_types.py new file mode 100644 index 0000000000..e36225b458 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/volume_types.py @@ -0,0 +1,80 @@ +# Copyright (c) 2011 Rackspace US, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +""" +Volume Type interface. +""" + +from novaclient import base + + +class VolumeType(base.Resource): + """ + A Volume Type is the type of volume to be created + """ + def __repr__(self): + return "" % self.name + + +class VolumeTypeManager(base.ManagerWithFind): + """ + Manage :class:`VolumeType` resources. + """ + resource_class = VolumeType + + def list(self): + """ + Get a list of all volume types. + + :rtype: list of :class:`VolumeType`. + """ + with self.alternate_service_type('volume'): + return self._list("/types", "volume_types") + + def get(self, volume_type): + """ + Get a specific volume type. + + :param volume_type: The ID of the :class:`VolumeType` to get. + :rtype: :class:`VolumeType` + """ + with self.alternate_service_type('volume'): + return self._get("/types/%s" % base.getid(volume_type), + "volume_type") + + def delete(self, volume_type): + """ + Delete a specific volume_type. + + :param volume_type: The ID of the :class:`VolumeType` to get. + """ + with self.alternate_service_type('volume'): + self._delete("/types/%s" % base.getid(volume_type)) + + def create(self, name): + """ + Create a volume type. + + :param name: Descriptive name of the volume type + :rtype: :class:`VolumeType` + """ + with self.alternate_service_type('volume'): + body = { + "volume_type": { + "name": name, + } + } + return self._create("/types", body, "volume_type") diff --git a/awx/lib/site-packages/novaclient/v2/volumes.py b/awx/lib/site-packages/novaclient/v2/volumes.py new file mode 100644 index 0000000000..182698ff51 --- /dev/null +++ b/awx/lib/site-packages/novaclient/v2/volumes.py @@ -0,0 +1,174 @@ +# Copyright 2011 Denali Systems, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Volume interface (1.1 extension). +""" + +import six +from six.moves.urllib import parse + +from novaclient import base + + +class Volume(base.Resource): + """ + A volume is an extra block level storage to the OpenStack instances. + """ + NAME_ATTR = 'display_name' + + def __repr__(self): + return "" % self.id + + def delete(self): + """ + Delete this volume. + """ + self.manager.delete(self) + + +class VolumeManager(base.ManagerWithFind): + """ + Manage :class:`Volume` resources. + """ + resource_class = Volume + + def create(self, size, snapshot_id=None, display_name=None, + display_description=None, volume_type=None, + availability_zone=None, imageRef=None): + """ + Create a volume. + + :param size: Size of volume in GB + :param snapshot_id: ID of the snapshot + :param display_name: Name of the volume + :param display_description: Description of the volume + :param volume_type: Type of volume + :param availability_zone: Availability Zone for volume + :rtype: :class:`Volume` + :param imageRef: reference to an image stored in glance + """ + # NOTE(melwitt): Ensure we use the volume endpoint for this call + with self.alternate_service_type('volume'): + body = {'volume': {'size': size, + 'snapshot_id': snapshot_id, + 'display_name': display_name, + 'display_description': display_description, + 'volume_type': volume_type, + 'availability_zone': availability_zone, + 'imageRef': imageRef}} + return self._create('/volumes', body, 'volume') + + def get(self, volume_id): + """ + Get a volume. + + :param volume_id: The ID of the volume to get. + :rtype: :class:`Volume` + """ + with self.alternate_service_type('volume'): + return self._get("/volumes/%s" % volume_id, "volume") + + def list(self, detailed=True, search_opts=None): + """ + Get a list of all volumes. + + :rtype: list of :class:`Volume` + """ + with self.alternate_service_type('volume'): + search_opts = search_opts or {} + + if 'name' in search_opts.keys(): + search_opts['display_name'] = search_opts.pop('name') + + qparams = dict((k, v) for (k, v) in + six.iteritems(search_opts) if v) + + query_str = '?%s' % parse.urlencode(qparams) if qparams else '' + + if detailed is True: + return self._list("/volumes/detail%s" % query_str, "volumes") + else: + return self._list("/volumes%s" % query_str, "volumes") + + def delete(self, volume): + """ + Delete a volume. + + :param volume: The :class:`Volume` to delete. + """ + with self.alternate_service_type('volume'): + self._delete("/volumes/%s" % base.getid(volume)) + + def create_server_volume(self, server_id, volume_id, device): + """ + Attach a volume identified by the volume ID to the given server ID + + :param server_id: The ID of the server + :param volume_id: The ID of the volume to attach. + :param device: The device name + :rtype: :class:`Volume` + """ + body = {'volumeAttachment': {'volumeId': volume_id, + 'device': device}} + return self._create("/servers/%s/os-volume_attachments" % server_id, + body, "volumeAttachment") + + def update_server_volume(self, server_id, attachment_id, new_volume_id): + """ + Update the volume identified by the attachment ID, that is attached to + the given server ID + + :param server_id: The ID of the server + :param attachment_id: The ID of the attachment + :param new_volume_id: The ID of the new volume to attach + :rtype: :class:`Volume` + """ + body = {'volumeAttachment': {'volumeId': new_volume_id}} + return self._update("/servers/%s/os-volume_attachments/%s" % + (server_id, attachment_id,), + body, "volumeAttachment") + + def get_server_volume(self, server_id, attachment_id): + """ + Get the volume identified by the attachment ID, that is attached to + the given server ID + + :param server_id: The ID of the server + :param attachment_id: The ID of the attachment + :rtype: :class:`Volume` + """ + return self._get("/servers/%s/os-volume_attachments/%s" % (server_id, + attachment_id,), "volumeAttachment") + + def get_server_volumes(self, server_id): + """ + Get a list of all the attached volumes for the given server ID + + :param server_id: The ID of the server + :rtype: list of :class:`Volume` + """ + return self._list("/servers/%s/os-volume_attachments" % server_id, + "volumeAttachments") + + def delete_server_volume(self, server_id, attachment_id): + """ + Detach a volume identified by the attachment ID from the given server + + :param server_id: The ID of the server + :param attachment_id: The ID of the attachment + """ + self._delete("/servers/%s/os-volume_attachments/%s" % + (server_id, attachment_id,)) diff --git a/awx/lib/site-packages/os_networksv2_python_novaclient_ext.py b/awx/lib/site-packages/os_networksv2_python_novaclient_ext.py index 0cd51aa46e..b4d61a1408 100644 --- a/awx/lib/site-packages/os_networksv2_python_novaclient_ext.py +++ b/awx/lib/site-packages/os_networksv2_python_novaclient_ext.py @@ -13,7 +13,7 @@ # limitations under the License. from novaclient import base -from novaclient import utils +from novaclient.openstack.common import cliutils as utils class Network(base.Resource): @@ -47,6 +47,9 @@ def do_network(cs, args): utils.print_dict(network._info) +do_network_show = do_network + + def do_network_list(cs, args): """ List networks @@ -65,7 +68,7 @@ def do_network_create(cs, args): Create a network """ network = cs.os_networksv2_python_novaclient_ext.create(args.label, - args.cidr) + args.cidr) utils.print_dict(network._info) diff --git a/awx/lib/site-packages/os_virtual_interfacesv2_python_novaclient_ext.py b/awx/lib/site-packages/os_virtual_interfacesv2_python_novaclient_ext.py index 712d8d315e..7707c3ec8e 100644 --- a/awx/lib/site-packages/os_virtual_interfacesv2_python_novaclient_ext.py +++ b/awx/lib/site-packages/os_virtual_interfacesv2_python_novaclient_ext.py @@ -13,6 +13,7 @@ # limitations under the License. from novaclient import base +from novaclient.openstack.common import cliutils from novaclient import utils @@ -26,7 +27,7 @@ class VirtualInterfaceManager(base.ManagerWithFind): def list(self, instance_id): return self._list("/servers/%s/os-virtual-interfacesv2" % instance_id, - "virtual_interfaces") + "virtual_interfaces") def create(self, network_id, instance_id): body = {'virtual_interface': {'network_id': network_id}} @@ -52,30 +53,30 @@ def ip_address_formatter(field): return ",".join(addresses) -@utils.arg('instance_id', metavar='', - help="ID of the instance you want to display virtual" - "interfaces for") +@cliutils.arg('instance_id', metavar='', + help="ID of the instance you want to display virtual" + "interfaces for") def do_virtual_interface_list(cs, args): """ - Lists the virtual interfaces for a specified server instance + Lists the virtual interfaces for an instance """ vifs = cs.os_virtual_interfacesv2_python_novaclient_ext.list( - args.instance_id) + args.instance_id) utils.print_list(vifs, ["id", "mac_address", "ip_addresses"], formatters={"ip_addresses": ip_address_formatter}) -@utils.arg('network_id', metavar='', - help='Network ID to connect the new virtual interface to') -@utils.arg('instance_id', metavar='', - help="Instance to attach the new virtual interface to") +@cliutils.arg('network_id', metavar='', + help='Network ID to connect the new virtual interface to') +@cliutils.arg('instance_id', metavar='', + help="Instance to attach the new virtual interface to") def do_virtual_interface_create(cs, args): """ Add a new virtual interface to an instance """ addresses = cs.os_virtual_interfacesv2_python_novaclient_ext.create( - args.network_id, - args.instance_id) + args.network_id, + args.instance_id) for address in addresses: addr_list = [ip_dict_formatter(a) for a in address["ip_addresses"]] addr_dict = {"id": address["id"], @@ -84,13 +85,13 @@ def do_virtual_interface_create(cs, args): utils.print_dict(addr_dict) -@utils.arg('instance_id', metavar='', - help="Instance to remove the virtual interface from") -@utils.arg('interface_id', metavar='', - help='ID of the virtual interface to delete') +@cliutils.arg('instance_id', metavar='', + help="Instance to remove the virtual interface from") +@cliutils.arg('interface_id', metavar='', + help='ID of the virtual interface to delete') def do_virtual_interface_delete(cs, args): """ Removes the specified virtual interface from an instance """ cs.os_virtual_interfacesv2_python_novaclient_ext.delete(args.instance_id, - args.interface_id) + args.interface_id)