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:
parent
b133f2d95f
commit
9955831638
@ -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()
|
||||
|
41
cloudcafe/openstackcli/common/behaviors.py
Normal file
41
cloudcafe/openstackcli/common/behaviors.py
Normal 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)
|
@ -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
|
||||
|
||||
|
30
cloudcafe/openstackcli/common/models/extensions.py
Normal file
30
cloudcafe/openstackcli/common/models/extensions.py
Normal 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
|
@ -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))
|
||||
|
15
metatests/openstackcli/__init__.py
Normal file
15
metatests/openstackcli/__init__.py
Normal 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.
|
||||
"""
|
Loading…
x
Reference in New Issue
Block a user