Add human-friendly ID support.
Allows a user to interact with certain models (image, flavors, and servers currently) using a human-friendly identifier which is a slugified form of the model name. Example: nova boot --image debian-6-squeeze --flavor 256mb-instance myinst Change-Id: I43dbedac3493d010c1ec9ba8b8bb1007ff7ac499
This commit is contained in:
parent
0528368fb9
commit
b22ec22336
@ -82,25 +82,23 @@ class Manager(utils.HookableMixin):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
with self.uuid_cache(obj_class, mode="w"):
|
with self.completion_cache('human_id', obj_class, mode="w"):
|
||||||
return [obj_class(self, res, loaded=True) for res in data if res]
|
with self.completion_cache('uuid', obj_class, mode="w"):
|
||||||
|
return [obj_class(self, res, loaded=True)
|
||||||
|
for res in data if res]
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def uuid_cache(self, obj_class, mode):
|
def completion_cache(self, cache_type, obj_class, mode):
|
||||||
"""
|
"""
|
||||||
Cache UUIDs for bash autocompletion.
|
The completion cache store items that can be used for bash
|
||||||
|
autocompletion, like UUIDs or human-friendly IDs.
|
||||||
|
|
||||||
The UUID cache works by checking to see whether an ID is UUID-like when
|
A resource listing will clear and repopulate the cache.
|
||||||
we create a resource object (e.g. a Image or a Server), and if it is,
|
|
||||||
we add it to a local cache file. We maintain one cache file per
|
|
||||||
resource type so that we can refresh them independently.
|
|
||||||
|
|
||||||
A resource listing will clear and repopulate the UUID cache.
|
A resource create will append to the cache.
|
||||||
|
|
||||||
A resource create will append to the UUID cache.
|
|
||||||
|
|
||||||
Delete is not handled because listings are assumed to be performed
|
Delete is not handled because listings are assumed to be performed
|
||||||
often enough to keep the UUID cache reasonably up-to-date.
|
often enough to keep the cache reasonably up-to-date.
|
||||||
"""
|
"""
|
||||||
base_dir = utils.env('NOVACLIENT_UUID_CACHE_DIR',
|
base_dir = utils.env('NOVACLIENT_UUID_CACHE_DIR',
|
||||||
default="~/.novaclient")
|
default="~/.novaclient")
|
||||||
@ -111,10 +109,10 @@ class Manager(utils.HookableMixin):
|
|||||||
url = utils.env('OS_URL', 'NOVA_URL')
|
url = utils.env('OS_URL', 'NOVA_URL')
|
||||||
uniqifier = hashlib.md5(username + url).hexdigest()
|
uniqifier = hashlib.md5(username + url).hexdigest()
|
||||||
|
|
||||||
uuid_cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier))
|
cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.makedirs(uuid_cache_dir, 0755)
|
os.makedirs(cache_dir, 0755)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if e.errno == errno.EEXIST:
|
if e.errno == errno.EEXIST:
|
||||||
pass
|
pass
|
||||||
@ -122,10 +120,13 @@ class Manager(utils.HookableMixin):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
resource = obj_class.__name__.lower()
|
resource = obj_class.__name__.lower()
|
||||||
filename = os.path.join(uuid_cache_dir, "%s-uuid-cache" % resource)
|
filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-'))
|
||||||
|
path = os.path.join(cache_dir, filename)
|
||||||
|
|
||||||
|
cache_attr = "_%s_cache" % cache_type
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._uuid_cache = open(filename, mode)
|
setattr(self, cache_attr, open(path, mode))
|
||||||
except IOError:
|
except IOError:
|
||||||
# NOTE(kiall): This is typicaly a permission denied while
|
# NOTE(kiall): This is typicaly a permission denied while
|
||||||
# attempting to write the cache file.
|
# attempting to write the cache file.
|
||||||
@ -134,13 +135,15 @@ class Manager(utils.HookableMixin):
|
|||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
if hasattr(self, '_uuid_cache'):
|
cache = getattr(self, cache_attr, None)
|
||||||
self._uuid_cache.close()
|
if cache:
|
||||||
del self._uuid_cache
|
cache.close()
|
||||||
|
delattr(self, cache_attr)
|
||||||
|
|
||||||
def write_uuid_to_cache(self, uuid):
|
def write_to_completion_cache(self, cache_type, val):
|
||||||
if hasattr(self, '_uuid_cache'):
|
cache = getattr(self, "_%s_cache" % cache_type, None)
|
||||||
self._uuid_cache.write("%s\n" % uuid)
|
if cache:
|
||||||
|
cache.write("%s\n" % val)
|
||||||
|
|
||||||
def _get(self, url, response_key=None):
|
def _get(self, url, response_key=None):
|
||||||
resp, body = self.api.client.get(url)
|
resp, body = self.api.client.get(url)
|
||||||
@ -155,7 +158,8 @@ class Manager(utils.HookableMixin):
|
|||||||
if return_raw:
|
if return_raw:
|
||||||
return body[response_key]
|
return body[response_key]
|
||||||
|
|
||||||
with self.uuid_cache(self.resource_class, mode="a"):
|
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])
|
return self.resource_class(self, body[response_key])
|
||||||
|
|
||||||
def _delete(self, url):
|
def _delete(self, url):
|
||||||
@ -282,6 +286,8 @@ class Resource(object):
|
|||||||
:param info: dictionary representing resource attributes
|
:param info: dictionary representing resource attributes
|
||||||
:param loaded: prevent lazy-loading if set to True
|
:param loaded: prevent lazy-loading if set to True
|
||||||
"""
|
"""
|
||||||
|
HUMAN_ID = False
|
||||||
|
|
||||||
def __init__(self, manager, info, loaded=False):
|
def __init__(self, manager, info, loaded=False):
|
||||||
self.manager = manager
|
self.manager = manager
|
||||||
self._info = info
|
self._info = info
|
||||||
@ -292,7 +298,20 @@ class Resource(object):
|
|||||||
# enter an infinite loop of __getattr__ -> get -> __init__ ->
|
# enter an infinite loop of __getattr__ -> get -> __init__ ->
|
||||||
# __getattr__ -> ...
|
# __getattr__ -> ...
|
||||||
if 'id' in self.__dict__ and len(str(self.id)) == 36:
|
if 'id' in self.__dict__ and len(str(self.id)) == 36:
|
||||||
self.manager.write_uuid_to_cache(self.id)
|
self.manager.write_to_completion_cache('uuid', self.id)
|
||||||
|
|
||||||
|
human_id = self.human_id
|
||||||
|
if human_id:
|
||||||
|
self.manager.write_to_completion_cache('human_id', human_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def human_id(self):
|
||||||
|
"""Subclasses may override this provide a pretty ID which can be used
|
||||||
|
for bash completion.
|
||||||
|
"""
|
||||||
|
if 'name' in self.__dict__ and self.HUMAN_ID:
|
||||||
|
return utils.slugify(self.name)
|
||||||
|
return None
|
||||||
|
|
||||||
def _add_details(self, info):
|
def _add_details(self, info):
|
||||||
for (k, v) in info.iteritems():
|
for (k, v) in info.iteritems():
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import os
|
import os
|
||||||
import prettytable
|
import re
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
import prettytable
|
||||||
|
|
||||||
from novaclient import exceptions
|
from novaclient import exceptions
|
||||||
|
|
||||||
|
|
||||||
@ -166,6 +168,11 @@ def find_resource(manager, name_or_id):
|
|||||||
except (ValueError, exceptions.NotFound):
|
except (ValueError, exceptions.NotFound):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
return manager.find(human_id=name_or_id)
|
||||||
|
except exceptions.NotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
# finally try to find entity by name
|
# finally try to find entity by name
|
||||||
try:
|
try:
|
||||||
return manager.find(name=name_or_id)
|
return manager.find(name=name_or_id)
|
||||||
@ -226,3 +233,23 @@ def import_class(import_str):
|
|||||||
mod_str, _sep, class_str = import_str.rpartition('.')
|
mod_str, _sep, class_str = import_str.rpartition('.')
|
||||||
__import__(mod_str)
|
__import__(mod_str)
|
||||||
return getattr(sys.modules[mod_str], class_str)
|
return getattr(sys.modules[mod_str], class_str)
|
||||||
|
|
||||||
|
_slugify_strip_re = re.compile(r'[^\w\s-]')
|
||||||
|
_slugify_hyphenate_re = re.compile(r'[-\s]+')
|
||||||
|
|
||||||
|
|
||||||
|
# http://code.activestate.com/recipes/
|
||||||
|
# 577257-slugify-make-a-string-usable-in-a-url-or-filename/
|
||||||
|
def slugify(value):
|
||||||
|
"""
|
||||||
|
Normalizes string, converts to lowercase, removes non-alpha characters,
|
||||||
|
and converts spaces to hyphens.
|
||||||
|
|
||||||
|
From Django's "django/template/defaultfilters.py".
|
||||||
|
"""
|
||||||
|
import unicodedata
|
||||||
|
if not isinstance(value, unicode):
|
||||||
|
value = unicode(value)
|
||||||
|
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
|
||||||
|
value = unicode(_slugify_strip_re.sub('', value).strip().lower())
|
||||||
|
return _slugify_hyphenate_re.sub('-', value)
|
||||||
|
@ -64,8 +64,8 @@ class BootingManagerWithFind(base.ManagerWithFind):
|
|||||||
"""
|
"""
|
||||||
body = {"server": {
|
body = {"server": {
|
||||||
"name": name,
|
"name": name,
|
||||||
"imageRef": base.getid(image),
|
"imageRef": str(base.getid(image)),
|
||||||
"flavorRef": base.getid(flavor),
|
"flavorRef": str(base.getid(flavor)),
|
||||||
}}
|
}}
|
||||||
if userdata:
|
if userdata:
|
||||||
if hasattr(userdata, 'read'):
|
if hasattr(userdata, 'read'):
|
||||||
|
@ -10,6 +10,8 @@ class Flavor(base.Resource):
|
|||||||
"""
|
"""
|
||||||
A flavor is an available hardware configuration for a server.
|
A flavor is an available hardware configuration for a server.
|
||||||
"""
|
"""
|
||||||
|
HUMAN_ID = True
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Flavor: %s>" % self.name
|
return "<Flavor: %s>" % self.name
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@ class Image(base.Resource):
|
|||||||
"""
|
"""
|
||||||
An image is a collection of files used to create or rebuild a server.
|
An image is a collection of files used to create or rebuild a server.
|
||||||
"""
|
"""
|
||||||
|
HUMAN_ID = True
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Image: %s>" % self.name
|
return "<Image: %s>" % self.name
|
||||||
|
|
||||||
|
@ -29,6 +29,8 @@ REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD'
|
|||||||
|
|
||||||
|
|
||||||
class Server(base.Resource):
|
class Server(base.Resource):
|
||||||
|
HUMAN_ID = True
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Server: %s>" % self.name
|
return "<Server: %s>" % self.name
|
||||||
|
|
||||||
|
@ -45,8 +45,8 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None):
|
|||||||
if not args.flavor:
|
if not args.flavor:
|
||||||
raise exceptions.CommandError("you need to specify a Flavor ID ")
|
raise exceptions.CommandError("you need to specify a Flavor ID ")
|
||||||
|
|
||||||
flavor = args.flavor
|
flavor = _find_flavor(cs, args.flavor)
|
||||||
image = args.image
|
image = _find_image(cs, args.image)
|
||||||
|
|
||||||
meta = dict(v.split('=', 1) for v in args.meta)
|
meta = dict(v.split('=', 1) for v in args.meta)
|
||||||
|
|
||||||
|
@ -7,8 +7,8 @@ _nova()
|
|||||||
|
|
||||||
opts="$(nova bash_completion)"
|
opts="$(nova bash_completion)"
|
||||||
|
|
||||||
UUID_CACHE=~/.novaclient/*/*-uuid-cache
|
COMPLETION_CACHE=~/.novaclient/*/*-cache
|
||||||
opts+=" "$(cat $UUID_CACHE 2> /dev/null | tr '\n' ' ')
|
opts+=" "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ')
|
||||||
|
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user