Merge pull request #6 from alexcoman/feature/base-hnv-model
Add base HNV model
This commit is contained in:
commit
52f8d3e54e
189
hnv_client/client.py
Normal file
189
hnv_client/client.py
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
# Copyright 2017 Cloudbase Solutions Srl
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""This module contains all the available HNV resources."""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
from hnv_client.common import constant
|
||||||
|
from hnv_client.common import exception
|
||||||
|
from hnv_client.common import model
|
||||||
|
from hnv_client.common import utils
|
||||||
|
from hnv_client import config as hnv_config
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
CONFIG = hnv_config.CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
class _BaseHNVModel(model.Model):
|
||||||
|
|
||||||
|
_endpoint = CONFIG.HNV.url
|
||||||
|
|
||||||
|
resource_ref = model.Field(name="resource_ref", key="resourceRef",
|
||||||
|
is_property=False)
|
||||||
|
"""A relative URI to an associated resource."""
|
||||||
|
|
||||||
|
resource_id = model.Field(name="resource_id", key="resourceId",
|
||||||
|
is_property=False,
|
||||||
|
default=lambda: str(uuid.uuid1()))
|
||||||
|
"""The resource ID for the resource. The value MUST be unique in
|
||||||
|
the context of the resource if it is a top-level resource, or in the
|
||||||
|
context of the direct parent resource if it is a child resource."""
|
||||||
|
|
||||||
|
instance_id = model.Field(name="instance_id", key="instanceId",
|
||||||
|
is_property=False)
|
||||||
|
"""The globally unique Id generated and used internally by the Network
|
||||||
|
Controller. The mapping resource that enables the client to map between
|
||||||
|
the instanceId and the resourceId."""
|
||||||
|
|
||||||
|
etag = model.Field(name="etag", key="etag", is_property=False)
|
||||||
|
"""An opaque string representing the state of the resource at the
|
||||||
|
time the response was generated."""
|
||||||
|
|
||||||
|
tags = model.Field(name="tags", key="tags", is_property=False,
|
||||||
|
is_required=False)
|
||||||
|
|
||||||
|
provisioning_state = model.Field(name="provisioning_state",
|
||||||
|
key="provisioningState",
|
||||||
|
is_read_only=True, is_required=False)
|
||||||
|
"""Indicates the various states of the resource. Valid values are
|
||||||
|
Deleting, Failed, Succeeded, and Updating."""
|
||||||
|
|
||||||
|
def __init__(self, **fields):
|
||||||
|
self._parent_id = fields.pop("parent_id", None)
|
||||||
|
super(_BaseHNVModel, self).__init__(**fields)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_client():
|
||||||
|
"""Create a new client for the HNV REST API."""
|
||||||
|
return utils.get_client(url=CONFIG.HNV.url,
|
||||||
|
username=CONFIG.HNV.username,
|
||||||
|
password=CONFIG.HNV.password,
|
||||||
|
allow_insecure=CONFIG.HNV.https_allow_insecure,
|
||||||
|
ca_bundle=CONFIG.HNV.https_ca_bundle)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent_id(self):
|
||||||
|
"""The identifier for the specific ancestor resource."""
|
||||||
|
return self._parent_id
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, resource_id=None, parent_id=None):
|
||||||
|
"""Retrieves the required resources.
|
||||||
|
|
||||||
|
:param resource_id: The identifier for the specific resource
|
||||||
|
within the resource type.
|
||||||
|
:param parent_id: The identifier for the specific ancestor
|
||||||
|
resource within the resource type.
|
||||||
|
"""
|
||||||
|
client = cls._get_client()
|
||||||
|
endpoint = cls._endpoint.format(resource_id=resource_id or "",
|
||||||
|
parent_id=parent_id or "")
|
||||||
|
raw_data = client.get_resource(endpoint)
|
||||||
|
if resource_id is None:
|
||||||
|
return [cls.from_raw_data(item) for item in raw_data["value"]]
|
||||||
|
else:
|
||||||
|
return cls.from_raw_data(raw_data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def remove(cls, resource_id, parent_id=None, wait=True, timeout=None):
|
||||||
|
"""Delete the required resource.
|
||||||
|
|
||||||
|
:param resource_id: The identifier for the specific resource
|
||||||
|
within the resource type.
|
||||||
|
:param parent_id: The identifier for the specific ancestor
|
||||||
|
resource within the resource type.
|
||||||
|
:param wait: Whether to wait until the operation is completed
|
||||||
|
:param timeout: The maximum amount of time required for this
|
||||||
|
operation to be completed.
|
||||||
|
|
||||||
|
If optional :param wait: is True and timeout is None (the default),
|
||||||
|
block if necessary until the resource is available. If timeout is a
|
||||||
|
positive number, it blocks at most timeout seconds and raises the
|
||||||
|
`TimeOut` exception if no item was available within that time.
|
||||||
|
|
||||||
|
Otherwise (block is false), return a resource if one is immediately
|
||||||
|
available, else raise the `NotFound` exception (timeout is ignored
|
||||||
|
in that case).
|
||||||
|
"""
|
||||||
|
client = cls._get_client()
|
||||||
|
endpoint = cls._endpoint.format(resource_id=resource_id or "",
|
||||||
|
parent_id=parent_id or "")
|
||||||
|
client.remove_resource(endpoint)
|
||||||
|
|
||||||
|
elapsed_time = 0
|
||||||
|
while wait:
|
||||||
|
try:
|
||||||
|
client.get_resource(endpoint)
|
||||||
|
except exception.NotFound:
|
||||||
|
break
|
||||||
|
|
||||||
|
elapsed_time += CONFIG.HNV.retry_interval
|
||||||
|
if timeout and elapsed_time > timeout:
|
||||||
|
raise exception.TimeOut("The request timed out.")
|
||||||
|
time.sleep(CONFIG.HNV.retry_interval)
|
||||||
|
|
||||||
|
def commit(self, wait=True, timeout=None):
|
||||||
|
"""Apply all the changes on the current model.
|
||||||
|
|
||||||
|
:param wait: Whether to wait until the operation is completed
|
||||||
|
:param timeout: The maximum amount of time required for this
|
||||||
|
operation to be completed.
|
||||||
|
|
||||||
|
If optional :param wait: is True and timeout is None (the default),
|
||||||
|
block if necessary until the resource is available. If timeout is a
|
||||||
|
positive number, it blocks at most timeout seconds and raises the
|
||||||
|
`TimeOut` exception if no item was available within that time.
|
||||||
|
|
||||||
|
Otherwise (block is false), return a resource if one is immediately
|
||||||
|
available, else raise the `NotFound` exception (timeout is ignored
|
||||||
|
in that case).
|
||||||
|
"""
|
||||||
|
super(_BaseHNVModel, self).commit(wait=wait, timeout=timeout)
|
||||||
|
client = self._get_client()
|
||||||
|
endpoint = self._endpoint.format(resource_id=self.resource_id or "",
|
||||||
|
parent_id=self.parent_id or "")
|
||||||
|
request_body = self.dump(include_read_only=False)
|
||||||
|
response = client.update_resource(endpoint, data=request_body)
|
||||||
|
|
||||||
|
elapsed_time = 0
|
||||||
|
while wait:
|
||||||
|
response = client.get_resource(endpoint)
|
||||||
|
properties = response.get("properties", {})
|
||||||
|
provisioning_state = properties.get("provisioningState", None)
|
||||||
|
if not provisioning_state:
|
||||||
|
raise exception.ServiceException("The object doesn't contain "
|
||||||
|
"`provisioningState`.")
|
||||||
|
if provisioning_state == constant.FAILED:
|
||||||
|
raise exception.ServiceException(
|
||||||
|
"Failed to complete the required operation.")
|
||||||
|
elif provisioning_state == constant.SUCCEEDED:
|
||||||
|
break
|
||||||
|
|
||||||
|
elapsed_time += CONFIG.HNV.retry_interval
|
||||||
|
if timeout and elapsed_time > timeout:
|
||||||
|
raise exception.TimeOut("The request timed out.")
|
||||||
|
time.sleep(CONFIG.HNV.retry_interval)
|
||||||
|
|
||||||
|
# Process the raw data from the update response
|
||||||
|
fields = self.process_raw_data(response)
|
||||||
|
# Set back the provision flag
|
||||||
|
self._provision_done = False
|
||||||
|
# Update the current model representation
|
||||||
|
self._set_fields(fields)
|
||||||
|
# Lock the current model
|
||||||
|
self._provision_done = True
|
@ -79,13 +79,14 @@ class Field(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name, key, default=None, is_required=False,
|
def __init__(self, name, key, default=None, is_required=False,
|
||||||
is_property=True, is_read_only=False):
|
is_property=True, is_read_only=False, is_static=False):
|
||||||
self._name = name
|
self._name = name
|
||||||
self._key = key
|
self._key = key
|
||||||
self._default = default
|
self._default = default
|
||||||
self._is_required = is_required
|
self._is_required = is_required
|
||||||
self._is_property = is_property
|
self._is_property = is_property
|
||||||
self._is_read_only = is_read_only
|
self._is_read_only = is_read_only
|
||||||
|
self._is_static = is_static
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -117,6 +118,11 @@ class Field(object):
|
|||||||
"""Whether the current field can be updated."""
|
"""Whether the current field can be updated."""
|
||||||
return self._is_read_only
|
return self._is_read_only
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_static(self):
|
||||||
|
"""Whether the value of the current field can be changed."""
|
||||||
|
return self._is_static
|
||||||
|
|
||||||
def add_to_class(self, model_class):
|
def add_to_class(self, model_class):
|
||||||
"""Replace the `Field` attribute with a named `_FieldDescriptor`.
|
"""Replace the `Field` attribute with a named `_FieldDescriptor`.
|
||||||
|
|
||||||
@ -164,15 +170,15 @@ class _ModelOptions(object):
|
|||||||
field = self._fields.pop(field_name, None)
|
field = self._fields.pop(field_name, None)
|
||||||
if field is not None and field.default is not None:
|
if field is not None and field.default is not None:
|
||||||
if six.callable(field.default):
|
if six.callable(field.default):
|
||||||
self._default_callables.pop(field.name, None)
|
self._default_callables.pop(field.key, None)
|
||||||
else:
|
else:
|
||||||
self._defaults.pop(field.name, None)
|
self._defaults.pop(field.key, None)
|
||||||
|
|
||||||
def get_defaults(self):
|
def get_defaults(self):
|
||||||
"""Get a dictionary that contains all the available defaults."""
|
"""Get a dictionary that contains all the available defaults."""
|
||||||
defaults = self._defaults.copy()
|
defaults = self._defaults.copy()
|
||||||
for field_name, default in self._default_callables.items():
|
for field_key, default in self._default_callables.items():
|
||||||
defaults[field_name] = default()
|
defaults[field_key] = default()
|
||||||
return defaults
|
return defaults
|
||||||
|
|
||||||
|
|
||||||
@ -197,7 +203,7 @@ class _BaseModel(type):
|
|||||||
|
|
||||||
# Get all the available fields for the current model.
|
# Get all the available fields for the current model.
|
||||||
for name, field in list(cls.__dict__.items()):
|
for name, field in list(cls.__dict__.items()):
|
||||||
if isinstance(field, Field) and not name.startswith("_"):
|
if not name.startswith("_") and isinstance(field, Field):
|
||||||
field.add_to_class(cls)
|
field.add_to_class(cls)
|
||||||
|
|
||||||
# Create string representation for the current model before finalizing
|
# Create string representation for the current model before finalizing
|
||||||
@ -213,16 +219,9 @@ class Model(object):
|
|||||||
def __init__(self, **fields):
|
def __init__(self, **fields):
|
||||||
self._data = self._meta.get_defaults()
|
self._data = self._meta.get_defaults()
|
||||||
self._changes = {}
|
self._changes = {}
|
||||||
|
|
||||||
self._provision_done = False
|
self._provision_done = False
|
||||||
|
self._set_fields(fields)
|
||||||
for field in self._meta.fields.values():
|
|
||||||
value = fields.pop(field.name, None)
|
|
||||||
if field.key not in self._data or value:
|
|
||||||
setattr(self, field.name, value)
|
|
||||||
|
|
||||||
if fields:
|
|
||||||
LOG.debug("Unrecognized fields: %r", fields)
|
|
||||||
|
|
||||||
self._provision_done = True
|
self._provision_done = True
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
@ -231,14 +230,46 @@ class Model(object):
|
|||||||
|
|
||||||
my_properties = self.dump().get("properties", {})
|
my_properties = self.dump().get("properties", {})
|
||||||
other_properties = other.dump().get("properties", {})
|
other_properties = other.dump().get("properties", {})
|
||||||
|
for properties in (my_properties, other_properties):
|
||||||
|
for ignore_key in ("provisioningState", ):
|
||||||
|
properties.pop(ignore_key, None)
|
||||||
return my_properties == other_properties
|
return my_properties == other_properties
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def _unpack(self, value):
|
||||||
|
"""Obtain the raw representation of the received object."""
|
||||||
|
if isinstance(value, Model):
|
||||||
|
return value.dump()
|
||||||
|
|
||||||
|
if isinstance(value, list):
|
||||||
|
container_list = []
|
||||||
|
for item in value:
|
||||||
|
container_list.append(self._unpack(item))
|
||||||
|
return container_list
|
||||||
|
|
||||||
|
if isinstance(value, dict):
|
||||||
|
container_dict = {}
|
||||||
|
for key, item in value.items():
|
||||||
|
container_dict[key] = self._unpack(item)
|
||||||
|
return container_dict
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _set_fields(self, fields):
|
||||||
|
"""Set or update the fields value."""
|
||||||
|
for field in self._meta.fields.values():
|
||||||
|
value = fields.pop(field.name, None)
|
||||||
|
if field.key not in self._data or value:
|
||||||
|
setattr(self, field.name, value)
|
||||||
|
|
||||||
|
if fields:
|
||||||
|
LOG.debug("Unrecognized fields: %r", fields)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_raw_data(cls, raw_data):
|
def process_raw_data(cls, raw_data):
|
||||||
"""Create a new model using raw API response."""
|
"""Process the received data in order to be understood by the model."""
|
||||||
content = {}
|
content = {}
|
||||||
properties = raw_data.pop("properties", {})
|
properties = raw_data.pop("properties", {})
|
||||||
for field_name, field in cls._meta.fields.items():
|
for field_name, field in cls._meta.fields.items():
|
||||||
@ -253,6 +284,12 @@ class Model(object):
|
|||||||
if properties:
|
if properties:
|
||||||
LOG.debug("Unrecognized properties: %r", properties)
|
LOG.debug("Unrecognized properties: %r", properties)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_raw_data(cls, raw_data):
|
||||||
|
"""Create a new model using raw API response."""
|
||||||
|
content = cls.process_raw_data(raw_data)
|
||||||
return cls(**content)
|
return cls(**content)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -270,9 +307,10 @@ class Model(object):
|
|||||||
|
|
||||||
def update(self, fields=None):
|
def update(self, fields=None):
|
||||||
"""Update the value of one or more fields."""
|
"""Update the value of one or more fields."""
|
||||||
for field_name, field in self._meta.fields.items():
|
if fields and isinstance(fields, dict):
|
||||||
if field_name in fields:
|
for field_name, field in self._meta.fields.items():
|
||||||
self._changes[field.key] = fields[field_name]
|
if field_name in fields:
|
||||||
|
self._changes[field.key] = fields[field_name]
|
||||||
self._data.update(self._changes)
|
self._data.update(self._changes)
|
||||||
|
|
||||||
def commit(self, wait=False, timeout=None):
|
def commit(self, wait=False, timeout=None):
|
||||||
@ -281,18 +319,17 @@ class Model(object):
|
|||||||
self._data.update(self._changes)
|
self._data.update(self._changes)
|
||||||
self._changes.clear()
|
self._changes.clear()
|
||||||
|
|
||||||
def dump(self, include_read_only=True):
|
def dump(self, include_read_only=True, include_static=False):
|
||||||
"""Create a dictionary with the content of the current model."""
|
"""Create a dictionary with the content of the current model."""
|
||||||
content = {}
|
content = {}
|
||||||
for field in self._meta.fields.values():
|
for field in self._meta.fields.values():
|
||||||
if field.is_read_only and not include_read_only:
|
if field.is_read_only and not include_read_only:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
value = self._data.get(field.key)
|
if field.is_static and not include_static:
|
||||||
if isinstance(value, Model):
|
continue
|
||||||
# The raw content of the model is required
|
|
||||||
value = value.dump()
|
|
||||||
|
|
||||||
|
value = self._unpack(self._data.get(field.key))
|
||||||
if not field.is_required and value is None:
|
if not field.is_required and value is None:
|
||||||
# The value of this field is not relevant
|
# The value of this field is not relevant
|
||||||
continue
|
continue
|
||||||
|
165
hnv_client/tests/test_client.py
Normal file
165
hnv_client/tests/test_client.py
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
# Copyright 2017 Cloudbase Solutions Srl
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
try:
|
||||||
|
import unittest.mock as mock
|
||||||
|
except ImportError:
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from hnv_client import client
|
||||||
|
from hnv_client.common import constant
|
||||||
|
from hnv_client.common import exception
|
||||||
|
from hnv_client import config as hnv_config
|
||||||
|
|
||||||
|
CONFIG = hnv_config.CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
class TestBaseHNVModel(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
client._BaseHNVModel._endpoint = "{parent_id}/{resource_id}"
|
||||||
|
|
||||||
|
@mock.patch("hnv_client.client._BaseHNVModel.from_raw_data")
|
||||||
|
@mock.patch("hnv_client.client._BaseHNVModel._get_client")
|
||||||
|
def test_get(self, mock_get_client, mock_from_raw_data):
|
||||||
|
mock_from_raw_data.return_value = mock.sentinel.resource
|
||||||
|
http_client = mock_get_client.return_value = mock.Mock()
|
||||||
|
get_resource = http_client.get_resource = mock.Mock()
|
||||||
|
|
||||||
|
resource = client._BaseHNVModel.get(resource_id="hnv-client-test")
|
||||||
|
|
||||||
|
get_resource.assert_called_once_with("/hnv-client-test")
|
||||||
|
self.assertIs(resource, mock.sentinel.resource)
|
||||||
|
|
||||||
|
@mock.patch("hnv_client.client._BaseHNVModel.from_raw_data")
|
||||||
|
@mock.patch("hnv_client.client._BaseHNVModel._get_client")
|
||||||
|
def test_get_all(self, mock_get_client, mock_from_raw_data):
|
||||||
|
mock_from_raw_data.side_effect = range(10)
|
||||||
|
|
||||||
|
http_client = mock_get_client.return_value = mock.Mock()
|
||||||
|
get_resource = http_client.get_resource = mock.Mock()
|
||||||
|
get_resource.return_value = {"value": range(10)}
|
||||||
|
|
||||||
|
resources = client._BaseHNVModel.get()
|
||||||
|
|
||||||
|
get_resource.assert_called_once_with("/")
|
||||||
|
self.assertEqual(resources, range(10))
|
||||||
|
|
||||||
|
@mock.patch("time.sleep")
|
||||||
|
@mock.patch("hnv_client.client._BaseHNVModel._get_client")
|
||||||
|
def _test_remove(self, mock_get_client, mock_sleep,
|
||||||
|
loop_count, timeout):
|
||||||
|
http_client = mock_get_client.return_value = mock.Mock()
|
||||||
|
remove_resource = http_client.remove_resource = mock.Mock()
|
||||||
|
get_resource = http_client.get_resource = mock.Mock()
|
||||||
|
side_effect = [None for _ in range(loop_count)]
|
||||||
|
side_effect.append(exception.NotFound if not timeout else None)
|
||||||
|
get_resource.side_effect = side_effect
|
||||||
|
|
||||||
|
request_timeout = CONFIG.HNV.retry_interval * loop_count
|
||||||
|
request_wait = True if loop_count > 0 else False
|
||||||
|
|
||||||
|
if timeout:
|
||||||
|
self.assertRaises(exception.TimeOut, client._BaseHNVModel.remove,
|
||||||
|
"hnv-client-test", wait=request_wait,
|
||||||
|
timeout=request_timeout)
|
||||||
|
else:
|
||||||
|
client._BaseHNVModel.remove("hnv-client-test",
|
||||||
|
wait=request_wait,
|
||||||
|
timeout=request_timeout)
|
||||||
|
|
||||||
|
remove_resource.assert_called_once_with("/hnv-client-test")
|
||||||
|
|
||||||
|
def test_remove(self):
|
||||||
|
self._test_remove(loop_count=0, timeout=False)
|
||||||
|
|
||||||
|
def test_remove_with_wait(self):
|
||||||
|
self._test_remove(loop_count=3, timeout=False)
|
||||||
|
|
||||||
|
def test_remove_timeout(self):
|
||||||
|
self._test_remove(loop_count=1, timeout=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_provisioning(provisioning_state):
|
||||||
|
return {"properties": {"provisioningState": provisioning_state}}
|
||||||
|
|
||||||
|
@mock.patch("time.sleep")
|
||||||
|
@mock.patch("hnv_client.client._BaseHNVModel.process_raw_data")
|
||||||
|
@mock.patch("hnv_client.client._BaseHNVModel.dump")
|
||||||
|
@mock.patch("hnv_client.client._BaseHNVModel._get_client")
|
||||||
|
def _test_commit(self, mock_get_client, mock_dump, mock_process,
|
||||||
|
mock_sleep,
|
||||||
|
loop_count, timeout, failed, invalid_response):
|
||||||
|
http_client = mock_get_client.return_value = mock.Mock()
|
||||||
|
update_resource = http_client.update_resource = mock.Mock()
|
||||||
|
mock_dump.return_value = mock.sentinel.request_body
|
||||||
|
mock_process.return_value = {}
|
||||||
|
|
||||||
|
get_resource = http_client.get_resource = mock.Mock()
|
||||||
|
side_effect = [self._get_provisioning(constant.UPDATING)
|
||||||
|
for _ in range(loop_count)]
|
||||||
|
if timeout:
|
||||||
|
side_effect.append(self._get_provisioning(constant.UPDATING))
|
||||||
|
elif failed:
|
||||||
|
side_effect.append(self._get_provisioning(constant.FAILED))
|
||||||
|
elif invalid_response:
|
||||||
|
side_effect.append(self._get_provisioning(None))
|
||||||
|
else:
|
||||||
|
side_effect.append(self._get_provisioning(constant.SUCCEEDED))
|
||||||
|
get_resource.side_effect = side_effect
|
||||||
|
|
||||||
|
request_timeout = CONFIG.HNV.retry_interval * loop_count
|
||||||
|
request_wait = True if loop_count > 0 else False
|
||||||
|
|
||||||
|
model = client._BaseHNVModel(resource_id="hnv-client",
|
||||||
|
parent_id="test")
|
||||||
|
|
||||||
|
if invalid_response or failed:
|
||||||
|
self.assertRaises(exception.ServiceException, model.commit,
|
||||||
|
wait=request_wait, timeout=request_timeout)
|
||||||
|
elif timeout:
|
||||||
|
self.assertRaises(exception.TimeOut, model.commit,
|
||||||
|
wait=request_wait, timeout=request_timeout)
|
||||||
|
else:
|
||||||
|
model.commit(wait=request_wait, timeout=request_timeout)
|
||||||
|
|
||||||
|
mock_dump.assert_called_once_with(include_read_only=False)
|
||||||
|
update_resource.assert_called_once_with(
|
||||||
|
"test/hnv-client", data=mock.sentinel.request_body)
|
||||||
|
|
||||||
|
if request_wait:
|
||||||
|
self.assertEqual(get_resource.call_count, loop_count + 1)
|
||||||
|
|
||||||
|
def test_commit(self):
|
||||||
|
self._test_commit(loop_count=0, timeout=False,
|
||||||
|
failed=False, invalid_response=False)
|
||||||
|
|
||||||
|
def test_commit_with_wait(self):
|
||||||
|
self._test_commit(loop_count=3, timeout=False,
|
||||||
|
failed=False, invalid_response=False)
|
||||||
|
|
||||||
|
def test_commit_timeout(self):
|
||||||
|
self._test_commit(loop_count=1, timeout=True,
|
||||||
|
failed=False, invalid_response=False)
|
||||||
|
|
||||||
|
def test_commit_failed(self):
|
||||||
|
self._test_commit(loop_count=1, timeout=False,
|
||||||
|
failed=True, invalid_response=False)
|
||||||
|
|
||||||
|
def test_commit_invalid_response(self):
|
||||||
|
self._test_commit(loop_count=1, timeout=False,
|
||||||
|
failed=False, invalid_response=True)
|
Loading…
x
Reference in New Issue
Block a user