Response extension support for openstackcli common

* Supports adding deserialization support for response properties added
  by optional api 'request extensions' as plugins to the openstackcli
  base response deserializers.
* Made openstackcli behaviors extensible by default.
* Added some helper methods to openstackcli client.
* Changed name of _dict_to_metadata_cmd_string method to
  _dict_to_string
* Added base openstackcli behaviors

Change-Id: I4ed8cdbf888385e8798b292f1932d3f68394f292
This commit is contained in:
Jose Idar 2014-01-03 13:08:07 -06:00
parent b133f2d95f
commit 9955831638
6 changed files with 157 additions and 19 deletions

View File

@ -29,7 +29,7 @@ class CinderCLI(BaseOpenstackPythonCLI_Client):
display_name=None, display_description=None, volume_type=None,
availability_zone=None, metadata=None):
metadata = self._dict_to_metadata_cmd_string(metadata)
metadata = self._dict_to_string(metadata)
_response_type = CinderResponses.VolumeResponse
_cmd = 'create'
@ -56,7 +56,7 @@ class CinderCLI(BaseOpenstackPythonCLI_Client):
def list_volumes(self, display_name=None, status=None, all_tenants=False):
all_tenants = 1 if all_tenants is True else 0
_response_type=CinderResponses.VolumeListResponse
_response_type = CinderResponses.VolumeListResponse
_cmd = 'list'
_kwmap = {
'display_name': 'display-name',
@ -75,21 +75,21 @@ class CinderCLI(BaseOpenstackPythonCLI_Client):
'display_name': 'display-name',
'display_description': 'display-description'}
_cmd = 'snapshot-create'
_response_type=CinderResponses.SnapshotResponse
_response_type = CinderResponses.SnapshotResponse
return self._process_command()
def list_snapshots(self):
_cmd = 'snapshot-list'
_response_type=CinderResponses.SnapshotListResponse
_response_type = CinderResponses.SnapshotListResponse
return self._process_command()
def show_snapshot(self, snapshot_id):
_cmd = 'snapshot-show'
_response_type=CinderResponses.SnapshotResponse
_response_type = CinderResponses.SnapshotResponse
return self._process_command()
# Volume Types
def list_volume_types(self):
_cmd = 'type-list'
_response_type=CinderResponses.VolumeTypeListResponse
return self._process_command()
_response_type = CinderResponses.VolumeTypeListResponse
return self._process_command()

View File

@ -0,0 +1,41 @@
from cafe.engine.behaviors import BaseBehavior
class OpenstackCLI_BehaviorError(Exception):
pass
class OpenstackCLI_BaseBehavior(BaseBehavior):
_default_exception = OpenstackCLI_BehaviorError
@staticmethod
def is_parse_error(resp):
if resp.entity is None:
return "Unable to parse CLI response"
@staticmethod
def is_cli_error(resp):
if resp.standard_error[-1].startswith("ERROR"):
return "CLI returned an error message"
@staticmethod
def is_process_error(resp):
if resp.return_code is not 0:
return "CLI process returned an error code"
@classmethod
def raise_if(cls, check, msg):
if check:
raise cls._default_exception(msg)
@classmethod
def raise_on_error(cls, resp, msg=None):
errors = [
cls.process_error(resp),
cls.cli_error(resp),
cls.parse_error(resp)]
errors = [e for e in errors if e is not None]
default_message = "ERROR: {0}".format(
" : ".join(["{0}".format(e) for e in errors]))
cls.raise_if(errors, msg or default_message)

View File

@ -53,10 +53,23 @@ class BaseOpenstackPythonCLI_Client(BaseCommandLineClient):
return " --{0}{1}".format(
flag, "".join([" {0}".format(v) for v in values]))
def _dict_to_metadata_cmd_string(self, metadata):
if isinstance(metadata, dict):
return " ".join(
["{0}={1}".format(k, v) for k, v in metadata.iteritems()])
@staticmethod
def _multiplicable_flag_data_to_string(flag, data):
"""returns a string: '--flag key1=value1 --flag key2-value2...'"""
if flag is None or data is None:
return None
return " --{0} ".format(flag).join(
["'{0}'='{1}'".format(k, v) for k, v in data.items()])
@staticmethod
def _dict_to_string(data, seperator=' '):
"""returns a string of the form "key1=value1 key2=value2 ..."
Seperator between key=value pairs is a single space by default
"""
if data is None:
return None
return "{0}".format(seperator).join(
["'{0}'='{1}'".format(k, v) for k, v in data.items()])
def _process_boolean_flag_value(self, value):
if isinstance(value, bool):
@ -76,25 +89,32 @@ class BaseOpenstackPythonCLI_Client(BaseCommandLineClient):
func_args.remove('self')
func_locals.pop('self', None)
kwmap = func_locals.pop('_kwmap', dict())
positional_args = func_locals.pop('_positional_args', list())
sub_command = func_locals.pop('_cmd', str())
response_type = func_locals.pop('_response_type', None)
# Assume every remaining non-private function local is a pythonified
# version of a command flag's name.
# version of a command flag's name, or a required argument
pythonified_flag_names = [
attr for attr in func_locals.keys() if not attr.startswith('_')]
# Assume that the name of every required function arg is the
# name of a required (positional) command arg.
# name of a required positional command arg, unless positional_args
# is defined.
# Extract required values (and their names) from the function locals
req_arg_names = [name for name in func_args if name not in kwmap]
req_arg_values = [func_locals[name] for name in req_arg_names]
positional_args = positional_args or [
name for name in func_args if name not in kwmap]
positional_arg_values = [func_locals[name] for name in positional_args]
# Build a dictionary of optional flag names mapped to the values passed
# into the function via those flag's pythonified flag names.
optional_flags_dict = dict(
(kwmap[name], func_locals[name])
for name in pythonified_flag_names if name not in req_arg_names)
for name in pythonified_flag_names if name not in positional_args)
#Build a string of all positional argument values
positional_arguments_string = ' '.join(
[str(value) for value in positional_arg_values])
# Build a string of all the optional flags and their values
optional_flags_string = ""
@ -106,10 +126,10 @@ class BaseOpenstackPythonCLI_Client(BaseCommandLineClient):
optional_flags_string, self._generate_cmd(flag, value))
# Build the final command string
cmd = "{base_cmd} {sub_cmd} {req_arg_values} {optional_flags}".format(
cmd = "{base_cmd} {sub_cmd} {pos_arg_values} {optional_flags}".format(
base_cmd=self.base_cmd(),
sub_cmd=sub_command,
req_arg_values=' '.join([str(value) for value in req_arg_values]),
pos_arg_values=positional_arguments_string,
optional_flags=optional_flags_string)
# Execute the command and attach an entity object to the response, if
@ -117,6 +137,7 @@ class BaseOpenstackPythonCLI_Client(BaseCommandLineClient):
response = self.run_command(cmd)
response_body_string = '\n'.join(response.standard_out)
setattr(response, 'entity', None)
if response_type is None:
return response

View File

@ -0,0 +1,30 @@
# Stores all registered extensions in this module
extensions = []
class ResponseExtensionType(type):
"""Metaclass for auto-registering extensions. Any new extension should
use this as it's __metaclass__"""
global extensions
def __new__(cls, class_name, bases, attrs):
extension = super(ResponseExtensionType, cls).__new__(
cls, class_name, bases, attrs)
if extension.__extends__:
extensions.append(extension)
return extension
class SingleAttributeResponseExtension(object):
"""Simple extension that can be inherited and used to support a single
response property"""
__metaclass__ = ResponseExtensionType
__extends__ = []
key_name = None
attr_name = None
def extend(cls, obj, **kwargs):
value = kwargs.get(cls.key_name)
setattr(obj, cls.attr_name, value)
return obj

View File

@ -1,5 +1,6 @@
from cafe.engine.models.base import BaseModel
from cafe.common.reporting import cclogging
from cloudcafe.openstackcli.common.models.extensions import extensions
class PRETTYTABLE_FRAME:
@ -12,7 +13,21 @@ class PrettyTableDeserializationError(Exception):
pass
class BasePrettyTableResponseModel(BaseModel):
class BaseExtensibleModel(BaseModel):
def __init__(self, **kwargs):
super(BaseExtensibleModel, self).__init__()
global extensions
for ext in extensions:
if self.__class__.__name__ in ext.__extends__:
self = ext().extend(self, **kwargs)
class BaseExtensibleListModel(list, BaseExtensibleModel):
pass
class BasePrettyTableResponseModel(BaseExtensibleModel):
_log = cclogging.getLogger(__name__)
@classmethod
@ -103,6 +118,22 @@ class BasePrettyTableResponseModel(BaseModel):
return tuple(final_list)
@staticmethod
def _apply_kwmap(kwmap, kwdict):
for local_attr, response_attr in kwmap.items():
kwdict[local_attr] = kwdict.pop(response_attr, None)
return kwdict
@classmethod
def _property_value_table_to_dict(cls, prettytable_string):
datatuple = cls._load_prettytable_string(prettytable_string)
kwdict = {}
for datadict in datatuple:
kwdict[datadict['Property']] = datadict['Value'].strip() or None
return kwdict
@classmethod
def deserialize(cls, serialized_str):
cls._log = cclogging.getLogger(cclogging.get_object_namespace(cls))

View File

@ -0,0 +1,15 @@
"""
Copyright 2013 Rackspace
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.
"""