summaryrefslogtreecommitdiff
path: root/saharaclient/nova
diff options
context:
space:
mode:
Diffstat (limited to 'saharaclient/nova')
-rw-r--r--saharaclient/nova/__init__.py0
-rw-r--r--saharaclient/nova/auth_plugin.py143
-rw-r--r--saharaclient/nova/base.py487
-rw-r--r--saharaclient/nova/extension.py39
-rw-r--r--saharaclient/nova/utils.py424
5 files changed, 1093 insertions, 0 deletions
diff --git a/saharaclient/nova/__init__.py b/saharaclient/nova/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/saharaclient/nova/__init__.py
diff --git a/saharaclient/nova/auth_plugin.py b/saharaclient/nova/auth_plugin.py
new file mode 100644
index 0000000..aa0dca3
--- /dev/null
+++ b/saharaclient/nova/auth_plugin.py
@@ -0,0 +1,143 @@
+# Copyright 2013 OpenStack Foundation
+# Copyright 2013 Spanish National Research Council.
+# 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 logging
+import pkg_resources
+
+import six
+
+from saharaclient.nova import utils
+from saharaclient.openstack.common.apiclient import exceptions
+
+
+logger = logging.getLogger(__name__)
+
+
+_discovered_plugins = {}
+
+
+def discover_auth_systems():
+ """Discover the available auth-systems.
+
+ This won't take into account the old style auth-systems.
+ """
+ ep_name = 'openstack.client.auth_plugin'
+ for ep in pkg_resources.iter_entry_points(ep_name):
+ try:
+ auth_plugin = ep.load()
+ except (ImportError, pkg_resources.UnknownExtra, AttributeError) as e:
+ logger.debug("ERROR: Cannot load auth plugin %s" % ep.name)
+ logger.debug(e, exc_info=1)
+ else:
+ _discovered_plugins[ep.name] = auth_plugin
+
+
+def load_auth_system_opts(parser):
+ """Load options needed by the available auth-systems into a parser.
+
+ This function will try to populate the parser with options from the
+ available plugins.
+ """
+ for name, auth_plugin in six.iteritems(_discovered_plugins):
+ add_opts_fn = getattr(auth_plugin, "add_opts", None)
+ if add_opts_fn:
+ group = parser.add_argument_group("Auth-system '%s' options" %
+ name)
+ add_opts_fn(group)
+
+
+def load_plugin(auth_system):
+ if auth_system in _discovered_plugins:
+ return _discovered_plugins[auth_system]()
+
+ # NOTE(aloga): If we arrive here, the plugin will be an old-style one,
+ # so we have to create a fake AuthPlugin for it.
+ return DeprecatedAuthPlugin(auth_system)
+
+
+class BaseAuthPlugin(object):
+ """Base class for authentication plugins.
+
+ An authentication plugin needs to override at least the authenticate
+ method to be a valid plugin.
+ """
+ def __init__(self):
+ self.opts = {}
+
+ def get_auth_url(self):
+ """Return the auth url for the plugin (if any)."""
+ return None
+
+ @staticmethod
+ def add_opts(parser):
+ """Populate and return the parser with the options for this plugin.
+
+ If the plugin does not need any options, it should return the same
+ parser untouched.
+ """
+ return parser
+
+ def parse_opts(self, args):
+ """Parse the actual auth-system options if any.
+
+ This method is expected to populate the attribute self.opts with a
+ dict containing the options and values needed to make authentication.
+ If the dict is empty, the client should assume that it needs the same
+ options as the 'keystone' auth system (i.e. os_username and
+ os_password).
+
+ Returns the self.opts dict.
+ """
+ return self.opts
+
+ def authenticate(self, cls, auth_url):
+ """Authenticate using plugin defined method."""
+ raise exceptions.AuthSystemNotFound(self.auth_system)
+
+
+class DeprecatedAuthPlugin(object):
+ """Class to mimic the AuthPlugin class for deprecated auth systems.
+
+ Old auth systems only define two entry points: openstack.client.auth_url
+ and openstack.client.authenticate. This class will load those entry points
+ into a class similar to a valid AuthPlugin.
+ """
+ def __init__(self, auth_system):
+ self.auth_system = auth_system
+
+ def authenticate(cls, auth_url):
+ raise exceptions.AuthSystemNotFound(self.auth_system)
+
+ self.opts = {}
+
+ self.get_auth_url = lambda: None
+ self.authenticate = authenticate
+
+ self._load_endpoints()
+
+ def _load_endpoints(self):
+ ep_name = 'openstack.client.auth_url'
+ fn = utils._load_entry_point(ep_name, name=self.auth_system)
+ if fn:
+ self.get_auth_url = fn
+
+ ep_name = 'openstack.client.authenticate'
+ fn = utils._load_entry_point(ep_name, name=self.auth_system)
+ if fn:
+ self.authenticate = fn
+
+ def parse_opts(self, args):
+ return self.opts
diff --git a/saharaclient/nova/base.py b/saharaclient/nova/base.py
new file mode 100644
index 0000000..9667b8d
--- /dev/null
+++ b/saharaclient/nova/base.py
@@ -0,0 +1,487 @@
+# 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.
+
+"""
+Base utilities to build API operation managers and objects on top of.
+"""
+
+import abc
+import base64
+import contextlib
+import hashlib
+import inspect
+import os
+import threading
+
+import six
+
+from saharaclient.nova import utils
+from saharaclient.openstack.common.apiclient import exceptions
+from saharaclient.openstack.common import strutils
+
+
+def getid(obj):
+ """Abstracts the common pattern of allowing both an object or an object's
+ ID as a parameter when dealing with relationships.
+ """
+ try:
+ return obj.id
+ except AttributeError:
+ return obj
+
+
+class Manager(utils.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 _list(self, url, response_key, obj_class=None, body=None):
+ if body:
+ _resp, body = self.api.client.post(url, body=body)
+ else:
+ _resp, body = self.api.client.get(url)
+
+ if obj_class is None:
+ obj_class = self.resource_class
+
+ data = body[response_key]
+ # NOTE(ja): keystone returns values as list as {'values': [ ... ]}
+ # unlike other services which just return the list...
+ if isinstance(data, dict):
+ try:
+ data = data['values']
+ 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]
+
+ @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 = 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)
+
+ def _get(self, url, response_key):
+ _resp, body = self.api.client.get(url)
+ 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)
+ _resp, body = self.api.client.post(url, body=body)
+ 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])
+
+ def _delete(self, url):
+ _resp, _body = self.api.client.delete(url)
+
+ def _update(self, url, body, response_key=None, **kwargs):
+ self.run_hooks('modify_body_for_update', body, **kwargs)
+ _resp, body = self.api.client.put(url, body=body)
+ if body:
+ if response_key:
+ return self.resource_class(self, body[response_key])
+ else:
+ return self.resource_class(self, body)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class ManagerWithFind(Manager):
+ """Like a `Manager`, but with additional `find()`/`findall()` methods.
+ """
+
+ @abc.abstractmethod
+ def list(self):
+ pass
+
+ 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)
+ if num_matches == 0:
+ msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
+ raise exceptions.NotFound(404, msg)
+ elif num_matches > 1:
+ raise exceptions.NoUniqueMatch
+ else:
+ return matches[0]
+
+ 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()
+
+ detailed = True
+ list_kwargs = {}
+
+ list_argspec = inspect.getargspec(self.list)
+ if 'detailed' in list_argspec.args:
+ detailed = ("human_id" not in kwargs and
+ "name" not in kwargs and
+ "display_name" not in kwargs)
+ list_kwargs['detailed'] = detailed
+
+ if 'is_public' in list_argspec.args and 'is_public' in kwargs:
+ is_public = kwargs['is_public']
+ list_kwargs['is_public'] = is_public
+ if is_public is None:
+ tmp_kwargs = kwargs.copy()
+ del tmp_kwargs['is_public']
+ searches = tmp_kwargs.items()
+
+ listing = self.list(**list_kwargs)
+
+ for obj in listing:
+ try:
+ if all(getattr(obj, attr) == value
+ for (attr, value) in searches):
+ if detailed:
+ found.append(obj)
+ else:
+ found.append(self.get(obj.id))
+ except AttributeError:
+ continue
+
+ return found
+
+
+class BootingManagerWithFind(ManagerWithFind):
+ """Like a `ManagerWithFind`, but has the ability to boot servers."""
+
+ def _parse_block_device_mapping(self, block_device_mapping):
+ bdm = []
+
+ for device_name, mapping in six.iteritems(block_device_mapping):
+ #
+ # The mapping is in the format:
+ # <id>:[<type>]:[<size(GB)>]:[<delete_on_terminate>]
+ #
+ bdm_dict = {'device_name': device_name}
+
+ mapping_parts = mapping.split(':')
+ source_id = mapping_parts[0]
+ if len(mapping_parts) == 1:
+ bdm_dict['volume_id'] = source_id
+
+ elif len(mapping_parts) > 1:
+ source_type = mapping_parts[1]
+ if source_type.startswith('snap'):
+ bdm_dict['snapshot_id'] = source_id
+ else:
+ bdm_dict['volume_id'] = source_id
+
+ if len(mapping_parts) > 2 and mapping_parts[2]:
+ bdm_dict['volume_size'] = str(int(mapping_parts[2]))
+
+ if len(mapping_parts) > 3:
+ bdm_dict['delete_on_termination'] = mapping_parts[3]
+
+ bdm.append(bdm_dict)
+ return bdm
+
+ 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.
+
+ :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 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.
+ :param reservation_id: a UUID for the set of servers being requested.
+ :param return_raw: If True, don't try to coearse the result into
+ a Resource object.
+ :param security_groups: list of security group names
+ :param key_name: (optional extension) name of keypair to inject into
+ the instance
+ :param availability_zone: Name of the availability zone for instance
+ placement.
+ :param block_device_mapping: A dict of block device mappings for this
+ server.
+ :param block_device_mapping_v2: A dict of block device mappings V2 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, 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 admin_pass: admin password for the server.
+ :param disk_config: (optional extension) control how the disk is
+ partitioned when the server is created.
+ """
+ body = {"server": {
+ "name": name,
+ "imageRef": str(getid(image)) if image else '',
+ "flavorRef": str(getid(flavor)),
+ }}
+ if userdata:
+ if hasattr(userdata, 'read'):
+ userdata = userdata.read()
+
+ userdata = strutils.safe_encode(userdata)
+ body["server"]["user_data"] = base64.b64encode(userdata)
+ 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
+ personality.append({
+ 'path': filepath,
+ 'contents': base64.b64encode(data.encode('utf-8')),
+ })
+
+ 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:
+ # Append the image to the list only if we have new style BDMs
+ if image:
+ bdm_dict = {'uuid': image.id, 'source_type': 'image',
+ 'destination_type': 'local', 'boot_index': 0,
+ 'delete_on_termination': True}
+ block_device_mapping_v2.insert(0, bdm_dict)
+
+ 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'):
+ net_data['fixed_ip'] = nic_info['v4-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)
+
+
+class Resource(object):
+ """A resource represents a particular instance of an object (server,
+ flavor, etc). This is pretty much just a bag for attributes.
+
+ :param manager: Manager object
+ :param info: dictionary representing resource attributes
+ :param loaded: prevent lazy-loading if set to True
+ """
+ HUMAN_ID = False
+ NAME_ATTR = 'name'
+
+ def __init__(self, manager, info, loaded=False):
+ self.manager = manager
+ self._info = info
+ self._add_details(info)
+ self._loaded = loaded
+
+ # NOTE(sirp): ensure `id` is already present because if it isn't we'll
+ # enter an infinite loop of __getattr__ -> get -> __init__ ->
+ # __getattr__ -> ...
+ if 'id' in self.__dict__ and len(str(self.id)) == 36:
+ 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 self.NAME_ATTR in self.__dict__ and self.HUMAN_ID:
+ return utils.slugify(getattr(self, self.NAME_ATTR))
+ return None
+
+ def _add_details(self, info):
+ for (k, v) in six.iteritems(info):
+ try:
+ setattr(self, k, v)
+ self._info[k] = v
+ except AttributeError:
+ # In this case we already defined the attribute on the class
+ pass
+
+ def __getattr__(self, k):
+ if k not in self.__dict__:
+ #NOTE(bcwaldon): disallow lazy-loading if already loaded once
+ if not self.is_loaded():
+ self.get()
+ return self.__getattr__(k)
+
+ raise AttributeError(k)
+ else:
+ return self.__dict__[k]
+
+ def __repr__(self):
+ reprkeys = sorted(k for k in self.__dict__.keys()
+ if k[0] != '_' and k != 'manager')
+ info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
+ return "<%s %s>" % (self.__class__.__name__, info)
+
+ def get(self):
+ # set_loaded() first ... so if we have to bail, we know we tried.
+ self.set_loaded(True)
+ if not hasattr(self.manager, 'get'):
+ return
+
+ new = self.manager.get(self.id)
+ if new:
+ self._add_details(new._info)
+
+ def __eq__(self, other):
+ if not isinstance(other, self.__class__):
+ return False
+ if hasattr(self, 'id') and hasattr(other, 'id'):
+ return self.id == other.id
+ return self._info == other._info
+
+ def is_loaded(self):
+ return self._loaded
+
+ def set_loaded(self, val):
+ self._loaded = val
diff --git a/saharaclient/nova/extension.py b/saharaclient/nova/extension.py
new file mode 100644
index 0000000..9a292dc
--- /dev/null
+++ b/saharaclient/nova/extension.py
@@ -0,0 +1,39 @@
+# 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 saharaclient.nova import base
+from saharaclient.nova import utils
+
+
+class Extension(utils.HookableMixin):
+ """Extension descriptor."""
+
+ SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
+
+ def __init__(self, name, module):
+ self.name = name
+ self.module = module
+ self._parse_extension_module()
+
+ def _parse_extension_module(self):
+ self.manager_class = None
+ for attr_name, attr_value in self.module.__dict__.items():
+ if attr_name in self.SUPPORTED_HOOKS:
+ self.add_hook(attr_name, attr_value)
+ elif utils.safe_issubclass(attr_value, base.Manager):
+ self.manager_class = attr_value
+
+ def __repr__(self):
+ return "<Extension '%s'>" % self.name
diff --git a/saharaclient/nova/utils.py b/saharaclient/nova/utils.py
new file mode 100644
index 0000000..8630704
--- /dev/null
+++ b/saharaclient/nova/utils.py
@@ -0,0 +1,424 @@
+#
+# 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 os
+import pkg_resources
+import re
+import sys
+import textwrap
+import uuid
+
+import prettytable
+import six
+
+from saharaclient.openstack.common.apiclient import exceptions
+from saharaclient.openstack.common import strutils
+
+
+def arg(*args, **kwargs):
+ """Decorator for CLI args."""
+ def _decorator(func):
+ add_arg(func, *args, **kwargs)
+ return func
+ return _decorator
+
+
+def env(*args, **kwargs):
+ """returns the first environment variable set
+ if none are non-empty, defaults to '' or keyword arg default
+ """
+ for arg in args:
+ value = os.environ.get(arg, None)
+ if value:
+ return value
+ return kwargs.get('default', '')
+
+
+def add_arg(f, *args, **kwargs):
+ """Bind CLI arguments to a shell.py `do_foo` function."""
+
+ if not hasattr(f, 'arguments'):
+ f.arguments = []
+
+ # NOTE(sirp): avoid dups that can occur when the module is shared across
+ # tests.
+ if (args, kwargs) not in f.arguments:
+ # Because of the semantics of decorator composition if we just append
+ # to the options list positional options will appear to be backwards.
+ f.arguments.insert(0, (args, kwargs))
+
+
+def bool_from_str(val):
+ """Convert a string representation of a bool into a bool value."""
+
+ if not val:
+ return False
+ try:
+ return bool(int(val))
+ except ValueError:
+ if val.lower() in ['true', 'yes', 'y']:
+ return True
+ if val.lower() in ['false', 'no', 'n']:
+ return False
+ raise
+
+
+def add_resource_manager_extra_kwargs_hook(f, hook):
+ """Add hook to bind CLI arguments to ResourceManager calls.
+
+ The `do_foo` calls in shell.py will receive CLI args and then in turn pass
+ them through to the ResourceManager. Before passing through the args, the
+ hooks registered here will be called, giving us a chance to add extra
+ kwargs (taken from the command-line) to what's passed to the
+ ResourceManager.
+ """
+ if not hasattr(f, 'resource_manager_kwargs_hooks'):
+ f.resource_manager_kwargs_hooks = []
+
+ names = [h.__name__ for h in f.resource_manager_kwargs_hooks]
+ if hook.__name__ not in names:
+ f.resource_manager_kwargs_hooks.append(hook)
+
+
+# Unused and does not pass pep8 (F821)
+#def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False):
+# """Return extra_kwargs by calling resource manager kwargs hooks."""
+# hooks = getattr(f, "resource_manager_kwargs_hooks", [])
+# extra_kwargs = {}
+# for hook in hooks:
+# hook_kwargs = hook(args)
+#
+# conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys())
+# if conflicting_keys and not allow_conflicts:
+# raise Exception("Hook '%(hook_name)s' is attempting to redefine"
+# " attributes '%(conflicting_keys)s'" %
+# {'hook_name': hook_name,
+# 'conflicting_keys': conflicting_keys})
+#
+# extra_kwargs.update(hook_kwargs)
+#
+# return extra_kwargs
+
+
+def unauthenticated(f):
+ """Adds 'unauthenticated' attribute to decorated function.
+ Usage:
+ @unauthenticated
+ def mymethod(f):
+ ...
+ """
+ f.unauthenticated = True
+ return f
+
+
+def isunauthenticated(f):
+ """Checks to see if the function is marked as not requiring authentication
+ with the @unauthenticated decorator. Returns True if decorator is
+ set to True, False otherwise.
+ """
+ return getattr(f, 'unauthenticated', False)
+
+
+def service_type(stype):
+ """Adds 'service_type' attribute to decorated function.
+ Usage:
+ @service_type('volume')
+ def mymethod(f):
+ ...
+ """
+ def inner(f):
+ f.service_type = stype
+ return f
+ return inner
+
+
+def get_service_type(f):
+ """Retrieves service type from function
+ """
+ return getattr(f, 'service_type', None)
+
+
+def pretty_choice_list(l):
+ return ', '.join("'%s'" % i for i in l)
+
+
+def print_list(objs, fields, formatters={}, sortby_index=None):
+ if sortby_index is None:
+ sortby = None
+ else:
+ sortby = fields[sortby_index]
+ mixed_case_fields = ['serverId']
+ pt = prettytable.PrettyTable([f for f in fields], caching=False)
+ pt.align = 'l'
+
+ for o in objs:
+ row = []
+ for field in fields:
+ if field in formatters:
+ row.append(formatters[field](o))
+ else:
+ if field in mixed_case_fields:
+ field_name = field.replace(' ', '_')
+ else:
+ field_name = field.lower().replace(' ', '_')
+ data = getattr(o, field_name, '')
+ row.append(data)
+ pt.add_row(row)
+
+ if sortby is not None:
+ result = strutils.safe_encode(pt.get_string(sortby=sortby))
+ else:
+ result = strutils.safe_encode(pt.get_string())
+
+ print(result)
+
+
+def _flatten(data, prefix=None):
+ """Flatten a dict, using name as a prefix for the keys of dict.
+
+ >>> _flatten('cpu_info', {'arch':'x86_64'})
+ [('cpu_info_arch': 'x86_64')]
+
+ """
+ if isinstance(data, dict):
+ for key, value in six.iteritems(data):
+ new_key = '%s_%s' % (prefix, key) if prefix else key
+ if isinstance(value, (dict, list)):
+ for item in _flatten(value, new_key):
+ yield item
+ else:
+ yield new_key, value
+ else:
+ yield prefix, data
+
+
+def flatten_dict(data):
+ """Return a new dict whose sub-dicts have been merged into the
+ original. Each of the parents keys are prepended to the child's
+ to prevent collisions. Any string elements will be JSON parsed
+ before flattening.
+
+ >>> flatten_dict({'service': {'host':'cloud9@compute-068', 'id': 143}})
+ {'service_host': colud9@compute-068', 'service_id': 143}
+
+ """
+ data = data.copy()
+ # Try and decode any nested JSON structures.
+ for key, value in six.iteritems(data):
+ if isinstance(value, six.string_types):
+ try:
+ data[key] = json.loads(value)
+ except ValueError:
+ pass
+
+ return dict(_flatten(data))
+
+
+def print_dict(d, dict_property="Property", dict_value="Value", wrap=0):
+ pt = prettytable.PrettyTable([dict_property, dict_value], caching=False)
+ pt.align = 'l'
+ for k, v in sorted(d.items()):
+ # convert dict to str to check length
+ if isinstance(v, dict):
+ v = str(v)
+ if wrap > 0:
+ v = textwrap.fill(str(v), wrap)
+ # if value has a newline, add in multiple rows
+ # e.g. fault with stacktrace
+ if v and isinstance(v, six.string_types) and r'\n' in v:
+ lines = v.strip().split(r'\n')
+ col1 = k
+ for line in lines:
+ pt.add_row([col1, line])
+ col1 = ''
+ else:
+ pt.add_row([k, v])
+ print(strutils.safe_encode(pt.get_string()))
+
+
+def find_resource(manager, name_or_id, **find_args):
+ """Helper for the _find_* methods."""
+ # 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 six.PY3:
+ tmp_id = tmp_id.decode()
+ uuid.UUID(tmp_id)
+ return manager.get(tmp_id)
+ except (TypeError, ValueError, exceptions.NotFound):
+ pass
+
+ # for str id which is not uuid (for Flavor search currently)
+ 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 %s with a name or ID of '%s' exists." % \
+ (manager.resource_class.__name__.lower(), name_or_id)
+ raise exceptions.CommandError(msg)
+ except exceptions.NoUniqueMatch:
+ msg = ("Multiple %s matches found for '%s', use an ID to be more"
+ " specific." % (manager.resource_class.__name__.lower(),
+ name_or_id))
+ raise exceptions.CommandError(msg)
+
+
+def _format_servers_list_networks(server):
+ output = []
+ for (network, addresses) in server.networks.items():
+ if len(addresses) == 0:
+ continue
+ addresses_csv = ', '.join(addresses)
+ group = "%s=%s" % (network, addresses_csv)
+ output.append(group)
+
+ return '; '.join(output)
+
+
+def _format_security_groups(groups):
+ return ', '.join(group['name'] for group in groups)
+
+
+def _format_field_name(attr):
+ """Format an object attribute in a human-friendly way."""
+ # Split at ':' and leave the extension name as-is.
+ parts = attr.rsplit(':', 1)
+ name = parts[-1].replace('_', ' ')
+ # Don't title() on mixed case
+ if name.isupper() or name.islower():
+ name = name.title()
+ parts[-1] = name
+ return ': '.join(parts)
+
+
+def _make_field_formatter(attr, filters=None):
+ """Given an object attribute, return a formatted field name and a
+ formatter suitable for passing to print_list.
+
+ Optionally pass a dict mapping attribute names to a function. The function
+ will be passed the value of the attribute and should return the string to
+ display.
+ """
+ filter_ = None
+ if filters:
+ filter_ = filters.get(attr)
+
+ def get_field(obj):
+ field = getattr(obj, attr, '')
+ if field and filter_:
+ field = filter_(field)
+ return field
+
+ name = _format_field_name(attr)
+ formatter = get_field
+ return name, formatter
+
+
+class HookableMixin(object):
+ """Mixin so classes can register and run hooks."""
+ _hooks_map = {}
+
+ @classmethod
+ def add_hook(cls, hook_type, hook_func):
+ if hook_type not in cls._hooks_map:
+ cls._hooks_map[hook_type] = []
+
+ cls._hooks_map[hook_type].append(hook_func)
+
+ @classmethod
+ def run_hooks(cls, hook_type, *args, **kwargs):
+ hook_funcs = cls._hooks_map.get(hook_type) or []
+ for hook_func in hook_funcs:
+ hook_func(*args, **kwargs)
+
+
+def safe_issubclass(*args):
+ """Like issubclass, but will just return False if not a class."""
+
+ try:
+ if issubclass(*args):
+ return True
+ except TypeError:
+ pass
+
+ return False
+
+
+def import_class(import_str):
+ """Returns a class from a string including module and class."""
+ 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, six.text_type):
+ value = six.text_type(value)
+ value = unicodedata.normalize('NFKD',
+ value).encode('ascii',
+ 'ignore').decode("ascii")
+ value = six.text_type(_slugify_strip_re.sub('', value).strip().lower())
+ return _slugify_hyphenate_re.sub('-', value)
+
+
+def _load_entry_point(ep_name, name=None):
+ """Try to load the entry point ep_name that matches name."""
+ for ep in pkg_resources.iter_entry_points(ep_name, name=name):
+ try:
+ return ep.load()
+ except (ImportError, pkg_resources.UnknownExtra, AttributeError):
+ continue
+
+
+# Unused and doesn't pass pep8 (F841)
+#def is_integer_like(val):
+# """Returns validation of a value as an integer."""
+# try:
+# value = int(val)
+# return True
+# except (TypeError, ValueError, AttributeError):
+# return False