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:
Rick Harris 2012-03-06 00:33:37 +00:00
parent 0528368fb9
commit b22ec22336
8 changed files with 84 additions and 32 deletions

View File

@ -82,25 +82,23 @@ class Manager(utils.HookableMixin):
except KeyError:
pass
with self.uuid_cache(obj_class, mode="w"):
return [obj_class(self, res, loaded=True) for res in data if res]
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]
@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
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 cache.
A resource listing will clear and repopulate the UUID cache.
A resource create will append to the UUID 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 UUID cache reasonably up-to-date.
often enough to keep the cache reasonably up-to-date.
"""
base_dir = utils.env('NOVACLIENT_UUID_CACHE_DIR',
default="~/.novaclient")
@ -111,10 +109,10 @@ class Manager(utils.HookableMixin):
url = utils.env('OS_URL', 'NOVA_URL')
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:
os.makedirs(uuid_cache_dir, 0755)
os.makedirs(cache_dir, 0755)
except OSError as e:
if e.errno == errno.EEXIST:
pass
@ -122,10 +120,13 @@ class Manager(utils.HookableMixin):
raise
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:
self._uuid_cache = open(filename, mode)
setattr(self, cache_attr, open(path, mode))
except IOError:
# NOTE(kiall): This is typicaly a permission denied while
# attempting to write the cache file.
@ -134,13 +135,15 @@ class Manager(utils.HookableMixin):
try:
yield
finally:
if hasattr(self, '_uuid_cache'):
self._uuid_cache.close()
del self._uuid_cache
cache = getattr(self, cache_attr, None)
if cache:
cache.close()
delattr(self, cache_attr)
def write_uuid_to_cache(self, uuid):
if hasattr(self, '_uuid_cache'):
self._uuid_cache.write("%s\n" % uuid)
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=None):
resp, body = self.api.client.get(url)
@ -155,8 +158,9 @@ class Manager(utils.HookableMixin):
if return_raw:
return body[response_key]
with self.uuid_cache(self.resource_class, mode="a"):
return self.resource_class(self, 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])
def _delete(self, url):
resp, body = self.api.client.delete(url)
@ -282,6 +286,8 @@ class Resource(object):
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
HUMAN_ID = False
def __init__(self, manager, info, loaded=False):
self.manager = manager
self._info = info
@ -292,7 +298,20 @@ class Resource(object):
# enter an infinite loop of __getattr__ -> get -> __init__ ->
# __getattr__ -> ...
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):
for (k, v) in info.iteritems():

View File

@ -1,8 +1,10 @@
import os
import prettytable
import re
import sys
import uuid
import prettytable
from novaclient import exceptions
@ -166,6 +168,11 @@ def find_resource(manager, name_or_id):
except (ValueError, exceptions.NotFound):
pass
try:
return manager.find(human_id=name_or_id)
except exceptions.NotFound:
pass
# finally try to find entity by name
try:
return manager.find(name=name_or_id)
@ -226,3 +233,23 @@ def import_class(import_str):
mod_str, _sep, class_str = import_str.rpartition('.')
__import__(mod_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)

View File

@ -64,8 +64,8 @@ class BootingManagerWithFind(base.ManagerWithFind):
"""
body = {"server": {
"name": name,
"imageRef": base.getid(image),
"flavorRef": base.getid(flavor),
"imageRef": str(base.getid(image)),
"flavorRef": str(base.getid(flavor)),
}}
if userdata:
if hasattr(userdata, 'read'):

View File

@ -10,6 +10,8 @@ class Flavor(base.Resource):
"""
A flavor is an available hardware configuration for a server.
"""
HUMAN_ID = True
def __repr__(self):
return "<Flavor: %s>" % self.name

View File

@ -10,6 +10,8 @@ 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 "<Image: %s>" % self.name

View File

@ -29,6 +29,8 @@ REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD'
class Server(base.Resource):
HUMAN_ID = True
def __repr__(self):
return "<Server: %s>" % self.name

View File

@ -45,8 +45,8 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None):
if not args.flavor:
raise exceptions.CommandError("you need to specify a Flavor ID ")
flavor = args.flavor
image = args.image
flavor = _find_flavor(cs, args.flavor)
image = _find_image(cs, args.image)
meta = dict(v.split('=', 1) for v in args.meta)

View File

@ -7,8 +7,8 @@ _nova()
opts="$(nova bash_completion)"
UUID_CACHE=~/.novaclient/*/*-uuid-cache
opts+=" "$(cat $UUID_CACHE 2> /dev/null | tr '\n' ' ')
COMPLETION_CACHE=~/.novaclient/*/*-cache
opts+=" "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ')
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
}