diff --git a/novaclient/base.py b/novaclient/base.py index c54367c1f..6b000f8fe 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -20,11 +20,7 @@ Base utilities to build API operation managers and objects on top of. """ import abc -import contextlib -import hashlib import inspect -import os -import threading import six @@ -52,11 +48,18 @@ class Manager(utils.HookableMixin): etc.) and provide CRUD operations for them. """ resource_class = None - cache_lock = threading.RLock() def __init__(self, api): self.api = api + def _write_object_to_completion_cache(self, obj): + if hasattr(self.api, 'write_object_to_completion_cache'): + self.api.write_object_to_completion_cache(obj) + + def _clear_completion_cache_for_class(self, obj_class): + if hasattr(self.api, 'clear_completion_cache_for_class'): + self.api.clear_completion_cache_for_class(obj_class) + def _list(self, url, response_key, obj_class=None, body=None): if body: _resp, body = self.api.client.post(url, body=body) @@ -75,77 +78,22 @@ class Manager(utils.HookableMixin): except KeyError: pass - with self.completion_cache('human_id', obj_class, mode="w"): - with self.completion_cache('uuid', obj_class, mode="w"): - return [obj_class(self, res, loaded=True) - for res in data if res] + self._clear_completion_cache_for_class(obj_class) - @contextlib.contextmanager - def completion_cache(self, cache_type, obj_class, mode): - """ - The completion cache store items that can be used for bash - autocompletion, like UUIDs or human-friendly IDs. + objs = [] + for res in data: + if res: + obj = obj_class(self, res, loaded=True) + self._write_object_to_completion_cache(obj) + objs.append(obj) - A resource listing will clear and repopulate the cache. - - A resource create will append to the cache. - - Delete is not handled because listings are assumed to be performed - often enough to keep the cache reasonably up-to-date. - """ - # NOTE(wryan): This lock protects read and write access to the - # completion caches - with self.cache_lock: - base_dir = utils.env('NOVACLIENT_UUID_CACHE_DIR', - default="~/.novaclient") - - # NOTE(sirp): Keep separate UUID caches for each username + - # endpoint pair - username = utils.env('OS_USERNAME', 'NOVA_USERNAME') - url = utils.env('OS_URL', 'NOVA_URL') - uniqifier = hashlib.md5(username.encode('utf-8') + - url.encode('utf-8')).hexdigest() - - cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier)) - - try: - os.makedirs(cache_dir, 0o755) - except OSError: - # NOTE(kiall): This is typically either permission denied while - # attempting to create the directory, or the - # directory already exists. Either way, don't - # fail. - pass - - resource = obj_class.__name__.lower() - filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-')) - path = os.path.join(cache_dir, filename) - - cache_attr = "_%s_cache" % cache_type - - try: - setattr(self, cache_attr, open(path, mode)) - except IOError: - # NOTE(kiall): This is typically a permission denied while - # attempting to write the cache file. - pass - - try: - yield - finally: - cache = getattr(self, cache_attr, None) - if cache: - cache.close() - delattr(self, cache_attr) - - def write_to_completion_cache(self, cache_type, val): - cache = getattr(self, "_%s_cache" % cache_type, None) - if cache: - cache.write("%s\n" % val) + return objs def _get(self, url, response_key): _resp, body = self.api.client.get(url) - return self.resource_class(self, body[response_key], loaded=True) + obj = self.resource_class(self, body[response_key], loaded=True) + self._write_object_to_completion_cache(obj) + return obj def _create(self, url, body, response_key, return_raw=False, **kwargs): self.run_hooks('modify_body_for_create', body, **kwargs) @@ -153,9 +101,9 @@ class Manager(utils.HookableMixin): if return_raw: return body[response_key] - with self.completion_cache('human_id', self.resource_class, mode="a"): - with self.completion_cache('uuid', self.resource_class, mode="a"): - return self.resource_class(self, body[response_key]) + obj = self.resource_class(self, body[response_key]) + self._write_object_to_completion_cache(obj) + return obj def _delete(self, url): _resp, _body = self.api.client.delete(url) diff --git a/novaclient/client.py b/novaclient/client.py index eef6a6edb..8d2d3f678 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -20,8 +20,11 @@ OpenStack Client interface. Handles the REST calls and responses. """ +import errno +import glob import hashlib import logging +import os import time import requests @@ -58,6 +61,77 @@ class _ClientConnectionPool(object): return self._adapters[url] +class CompletionCache(object): + """The completion cache is how we support tab-completion with novaclient. + + The `Manager` writes object IDs and Human-IDs to the completion-cache on + object-show, object-list, and object-create calls. + + The `nova.bash_completion` script then uses these files to provide the + actual tab-completion. + + The cache directory layout is: + + ~/.novaclient/ + <hash-of-endpoint-and-username>/ + <resource>-id-cache + <resource>-human-id-cache + """ + def __init__(self, username, auth_url, attributes=('id', 'human_id')): + self.directory = self._make_directory_name(username, auth_url) + self.attributes = attributes + + def _make_directory_name(self, username, auth_url): + """Creates a unique directory name based on the auth_url and username + of the current user. + """ + uniqifier = hashlib.md5(username.encode('utf-8') + + auth_url.encode('utf-8')).hexdigest() + base_dir = utils.env('NOVACLIENT_UUID_CACHE_DIR', + default="~/.novaclient") + return os.path.expanduser(os.path.join(base_dir, uniqifier)) + + def _prepare_directory(self): + try: + os.makedirs(self.directory, 0o755) + except OSError: + # NOTE(kiall): This is typically either permission denied while + # attempting to create the directory, or the + # directory already exists. Either way, don't + # fail. + pass + + def clear_class(self, obj_class): + self._prepare_directory() + + resource = obj_class.__name__.lower() + resource_glob = os.path.join(self.directory, "%s-*-cache" % resource) + + for filename in glob.iglob(resource_glob): + try: + os.unlink(filename) + except OSError as e: + if e.errno != errno.ENOENT: + raise + + def _write_attribute(self, resource, attribute, value): + self._prepare_directory() + + filename = "%s-%s-cache" % (resource, attribute.replace('_', '-')) + path = os.path.join(self.directory, filename) + + with open(path, 'a') as f: + f.write("%s\n" % value) + + def write_object(self, obj): + resource = obj.__class__.__name__.lower() + + for attribute in self.attributes: + value = getattr(obj, attribute, None) + if value: + self._write_attribute(resource, attribute, value) + + class HTTPClient(object): USER_AGENT = 'python-novaclient' diff --git a/novaclient/shell.py b/novaclient/shell.py index 5729aa1e1..08f5e8c40 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -648,6 +648,8 @@ class OpenStackComputeShell(object): raise exc.CommandError(_("You must provide an auth url " "via either --os-auth-url or env[OS_AUTH_URL]")) + completion_cache = client.CompletionCache(os_username, os_auth_url) + self.cs = client.Client(options.os_compute_api_version, os_username, os_password, os_tenant_name, tenant_id=os_tenant_id, user_id=os_user_id, @@ -659,7 +661,8 @@ class OpenStackComputeShell(object): volume_service_name=volume_service_name, timings=args.timings, bypass_url=bypass_url, os_cache=os_cache, http_log_debug=options.debug, - cacert=cacert, timeout=timeout) + cacert=cacert, timeout=timeout, + completion_cache=completion_cache) # Now check for the password/token of which pieces of the # identifying keyring key can come from the underlying client diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index b15155094..2cf62e947 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -89,7 +89,7 @@ class Client(object): http_log_debug=False, auth_system='keystone', auth_plugin=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, - connection_pool=False): + connection_pool=False, completion_cache=None): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument password = api_key @@ -167,6 +167,16 @@ class Client(object): cacert=cacert, connection_pool=connection_pool) + self.completion_cache = completion_cache + + def write_object_to_completion_cache(self, obj): + if self.completion_cache: + self.completion_cache.write_object(obj) + + def clear_completion_cache_for_class(self, obj_class): + if self.completion_cache: + self.completion_cache.clear_class(obj_class) + def __enter__(self): self.client.open_session() return self diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 7af2e1f2b..5274c7aee 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -74,7 +74,7 @@ class Client(object): http_log_debug=False, auth_system='keystone', auth_plugin=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, - connection_pool=False): + connection_pool=False, completion_cache=None): self.projectid = project_id self.tenant_id = tenant_id self.user_id = user_id @@ -130,6 +130,16 @@ class Client(object): cacert=cacert, connection_pool=connection_pool) + self.completion_cache = completion_cache + + def write_object_to_completion_cache(self, obj): + if self.completion_cache: + self.completion_cache.write_object(obj) + + def clear_completion_cache_for_class(self, obj_class): + if self.completion_cache: + self.completion_cache.clear_class(obj_class) + def __enter__(self): self.client.open_session() return self