From d09c8dab8c5a946aec7fa7f6a0f800733e6ebebb Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 16 Apr 2020 17:38:54 +0200 Subject: [PATCH 01/24] rework queue, group, message --- otcextensions/sdk/dms/v1/_base.py | 53 +++++- otcextensions/sdk/dms/v1/_proxy.py | 138 ++++++++++----- otcextensions/sdk/dms/v1/group.py | 39 +++-- otcextensions/sdk/dms/v1/message.py | 99 ++++++++++- otcextensions/sdk/dms/v1/queue.py | 47 +++-- otcextensions/sdk/dms/v1/quota.py | 2 +- .../tests/unit/sdk/dms/v1/test_group.py | 105 +++++++++++ .../tests/unit/sdk/dms/v1/test_message.py | 96 +++++++++++ .../tests/unit/sdk/dms/v1/test_proxy.py | 163 ++++++++++++------ .../tests/unit/sdk/dms/v1/test_queue.py | 113 +----------- .../tests/unit/sdk/dms/v1/test_quota.py | 2 +- 11 files changed, 622 insertions(+), 235 deletions(-) create mode 100644 otcextensions/tests/unit/sdk/dms/v1/test_group.py create mode 100644 otcextensions/tests/unit/sdk/dms/v1/test_message.py diff --git a/otcextensions/sdk/dms/v1/_base.py b/otcextensions/sdk/dms/v1/_base.py index 18c2fef69..1c8112550 100644 --- a/otcextensions/sdk/dms/v1/_base.py +++ b/otcextensions/sdk/dms/v1/_base.py @@ -9,10 +9,55 @@ # 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 otcextensions.sdk import sdk_resource +from openstack import exceptions +from openstack import resource -class Resource(sdk_resource.Resource): - base_path = '' +class Resource(resource.Resource): - service_expectes_json_type = True + @classmethod + def find(cls, session, name_or_id, ignore_missing=True, **params): + """Find a resource by its name or id. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param name_or_id: This resource's identifier, if needed by + the request. The default is ``None``. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict params: Any additional parameters to be passed into + underlying methods, such as to + :meth:`~openstack.resource.Resource.existing` + in order to pass on URI parameters. + + :return: The :class:`Resource` object matching the given name or id + or None if nothing matches. + :raises: :class:`openstack.exceptions.DuplicateResource` if more + than one resource is found for this request. + :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing + is found and ignore_missing is ``False``. + """ + session = cls._get_session(session) + # Try to short-circuit by looking directly for a matching ID. + try: + match = cls.existing( + id=name_or_id, + connection=session._get_connection(), + **params) + return match.fetch(session, **params) + except exceptions.SDKException: + pass + + data = cls.list(session, **params) + + result = cls._get_one_match(name_or_id, data) + if result is not None: + return result + + if ignore_missing: + return None + raise exceptions.ResourceNotFound( + "No %s found for %s" % (cls.__name__, name_or_id)) diff --git a/otcextensions/sdk/dms/v1/_proxy.py b/otcextensions/sdk/dms/v1/_proxy.py index bf9ebe118..c01a94ff9 100644 --- a/otcextensions/sdk/dms/v1/_proxy.py +++ b/otcextensions/sdk/dms/v1/_proxy.py @@ -10,18 +10,23 @@ # License for the specific language governing permissions and limitations # under the License. -from otcextensions.sdk import sdk_proxy +from openstack import proxy from otcextensions.sdk.dms.v1 import queue as _queue from otcextensions.sdk.dms.v1 import quota as _quota from otcextensions.sdk.dms.v1 import group as _group from otcextensions.sdk.dms.v1 import message as _message -from otcextensions.sdk.dms.v1 import group_message as _group_message -class Proxy(sdk_proxy.Proxy): +class Proxy(proxy.Proxy): skip_discovery = True + def __init__(self, session, *args, **kwargs): + super(Proxy, self).__init__(session=session, *args, **kwargs) + self.additional_headers = { + 'Content-Type': 'application/json', + } + # ======== Queues ======== def create_queue(self, **kwargs): """Create a queue @@ -31,13 +36,15 @@ def create_queue(self, **kwargs): """ return self._create(_queue.Queue, **kwargs) - def queues(self): + def queues(self, **kwargs): """List all queues + :param dict kwargs: List of query parameters + :returns: A generator of Queue object of :class:`~otcextensions.sdk.dms.v1.queue.Queue` """ - return self._list(_queue.Queue, paginated=False) + return self._list(_queue.Queue, paginated=False, **kwargs) def find_queue(self, name_or_id): """Find queue by name or id @@ -72,44 +79,51 @@ def delete_queue(self, queue, ignore_missing=True): self._delete(_queue.Queue, queue, ignore_missing=ignore_missing) # ======== Groups ======== - def create_group(self, queue, group): + def create_group(self, queue, name): """Create a list consume groups for a queue :param queue: The queue id or an instance of :class:`~otcextensions.sdk.dms.v1.queue.Queue` - :param dict kwargs: Keyword arguments which will be used to overwrite a - :class:`~otcextensions.sdk.dms.v1.queue.Group` + :param str name: Group name to create :returns: A list of object :class:`~otcextensions.sdk.dms.v1.queue.Group` """ + queue_obj = self._get_resource(_queue.Queue, queue) - queue_id = queue - if isinstance(queue, _queue.Queue): - queue_id = queue.id - - # Use a dummy group first to have control over create request - res = _group.Group.new(queue_id=queue_id) + return self._create(_group.Group, queue_id=queue_obj.id, name=name) - return res.create(self, group=group) - - def groups(self, queue, include_deadletter=False): + def groups(self, queue, **kwargs): """List all groups for a given queue :param queue: The queue id or an instance of :class:`~otcextensions.sdk.dms.v1.queue.Queue` + :param dict kwargs: Query parameters + :returns: A generator of Group object :rtype: :class:`~otcextensions.sdk.dms.v1.queue.Group` """ - queue_id = queue - if isinstance(queue, _queue.Queue): - queue_id = queue.id + queue_obj = self._get_resource(_queue.Queue, queue) return self._list(_group.Group, - queue_id=queue_id, - include_deadletter=include_deadletter, - paginated=False) + queue_id=queue_obj.id, + paginated=False, + **kwargs) + + def find_group(self, queue, name_or_id, ignore_missing=False): + """Find group by name or id + + :param queue: Queue name or object + :param name_or_id: Name or ID + :param ignore_missing: + :returns: one object of class + :class:`~otcextensions.sdk.dms.v1.queue.Queue` + """ + queue_obj = self._get_resource(_queue.Queue, queue) + return self._find(_group.Group, name_or_id, + ignore_missing=ignore_missing, + queue_id=queue_obj.id) - def delete_group(self, queue, group): + def delete_group(self, queue, group, ignore_missing=True): """Delete a consume on the queue :param queue: The queue id or an instance of @@ -118,28 +132,58 @@ def delete_group(self, queue, group): :class:`~otcextensions.sdk.dms.v1.group.Group` :returns: ``None`` """ - queue_id = queue - if isinstance(queue, _queue.Queue): - queue_id = queue.id + queue_obj = self._get_resource(_queue.Queue, queue) - self._delete(_group.Group, group, queue_id=queue_id) + self._delete(_group.Group, group, queue_id=queue_obj.id, + ignore_missing=ignore_missing) # ======== Messages ======== - def send_messages(self, queue, **kwargs): + def send_messages(self, queue, messages, return_id=False, **kwargs): """Send messages for a given queue :param queue: The queue id or an instance of :class:`~otcextensions.sdk.dms.v1.queue.Queue` - :param dict kwargs: Keyword to create messages - :returns: dict + :param list messages: A list of message dictionaries + :returns: Messages holder instance + :class:`otcextensions.sdk.dms.v1.message.Messages` """ - queue_id = queue - if isinstance(queue, _queue.Queue): - queue_id = queue.id + queue_obj = self._get_resource(_queue.Queue, queue) + messages_list = list() + for msg in messages: + if not isinstance(msg, _message.Message): + obj = _message.Message(**msg) + else: + obj = msg + messages_list.append(obj.to_dict(computed=False, ignore_none=True)) + + return self._create(_message.Messages, + base_path='/queues/%(queue_id)s/messages', + queue_id=queue_obj.id, + messages=messages_list, + return_id=return_id) + + def send_message(self, queue, return_id=True, body=None, **attrs): + """Send single message into a given queue - return self._create(_message.Message, queue_id=queue_id, **kwargs) + :param queue: The queue id or an instance of + :class:`~otcextensions.sdk.dms.v1.queue.Queue` + :param bool return_id: Whether response should contain message id + :param body: A str/json object representing message body + :param dict attr: Additional message attributes + :returns: Messages holder instance + :class:`otcextensions.sdk.dms.v1.message.Message` + """ + queue_obj = self._get_resource(_queue.Queue, queue) + msg = _message.Message(body=body, **attrs) - def consume_message(self, queue, consume_group, **query): + return self._create(_message.Messages, + base_path='/queues/%(queue_id)s/messages', + queue_id=queue_obj.id, + messages=[msg.to_dict(computed=False, + ignore_none=True)], + return_id=return_id).messages[0] + + def consume_message(self, queue, group, **query): """Consume queue's message :param queue: The queue id or an instance of @@ -151,20 +195,17 @@ def consume_message(self, queue, consume_group, **query): :returns: A list of object :class:`~otcextensions.sdk.dms.v1.group_message.GroupMessage` """ - queue_id = queue - if isinstance(queue, _queue.Queue): - queue_id = queue.id - consumer_group_id = consume_group - if isinstance(consume_group, _group.Group): - consumer_group_id = consume_group.id + queue_obj = self._get_resource(_queue.Queue, queue) + group_obj = self._get_resource(_group.Group, group) return self._list( - _group_message.GroupMessage, - queue_id=queue_id, - consumer_group_id=consumer_group_id, + _message.Message, + base_path='/queues/%(queue_id)s/groups/%(group_id)s/messages', + queue_id=queue_obj.id, + group_id=group_obj.id, **query) - def ack_consumed_message(self, consumed_message, status='success'): + def ack_message(self, queue, group, messages, status='success'): """Confirm consumed message :param consumed_message: An object of an instance of @@ -173,7 +214,10 @@ def ack_consumed_message(self, consumed_message, status='success'): :returns: An object of an instance of :class:`~otcextensions.sdk.dms.v1.group_message.GroupMessage` """ - return consumed_message.ack(self.session, status=status) + queue_obj = self._get_resource(_queue.Queue, queue) + group_obj = self._get_resource(_group.Group, group) + + return group_obj.ack(self, queue_obj, messages, status=status) def quotas(self): """List quota diff --git a/otcextensions/sdk/dms/v1/group.py b/otcextensions/sdk/dms/v1/group.py index 9290c33f2..893ddde7b 100644 --- a/otcextensions/sdk/dms/v1/group.py +++ b/otcextensions/sdk/dms/v1/group.py @@ -9,28 +9,27 @@ # 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 openstack import _log +from openstack import exceptions from openstack import resource -# from openstack import utils +from openstack import utils from otcextensions.sdk.dms.v1 import _base -_logger = _log.setup_logging('openstack') - class Group(_base.Resource): # NOTE: we are not interested in the also returned short queue info resources_key = 'groups' - base_path = 'queues/%(queue_id)s/groups' + # capabilities allow_create = True allow_list = True allow_delete = True _query_mapping = resource.QueryParameters( - 'include_deadletter' + 'include_deadletter', 'page_size', 'current_page' + # 'include_messages_num' ) # Properties @@ -56,17 +55,17 @@ class Group(_base.Resource): #: *Type: int* available_deadletters = resource.Body('available_deadletters', type=int) - def create(self, session, group): + def create(self, session, *args, **kwargs): """create group""" - body = {"groups": [{'name': group}]} + body = {"groups": [{'name': self.name}]} request = self._prepare_request(requires_id=False, prepend_key=False) response = session.post( request.url, - json=body, - headers={'Content-Length': str(len(str(body)))}) + json=body + ) # Squize groups into single response entity resp = response.json() @@ -78,3 +77,23 @@ def create(self, session, group): self._body.clean() return self + + def ack(self, session, queue_obj, ids, status='success'): + uri = utils.urljoin(self.base_path, self.id, 'ack') + self.queue_id = queue_obj.id + + uri = uri % self._uri.attributes + + ack_list = list() + for msg in ids: + ack_list.append( + {"handler": msg, + "status": status} + ) + + response = session.post( + uri, json={'message': ack_list} + ) + + exceptions.raise_from_response(response) + return diff --git a/otcextensions/sdk/dms/v1/message.py b/otcextensions/sdk/dms/v1/message.py index b2d18608a..2caef1f04 100644 --- a/otcextensions/sdk/dms/v1/message.py +++ b/otcextensions/sdk/dms/v1/message.py @@ -9,23 +9,112 @@ # 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 openstack import exceptions from openstack import resource -from openstack import _log -from otcextensions.sdk.dms.v1 import _base -_logger = _log.setup_logging('openstack') +class Message(resource.Resource): + resource_key = 'message' + base_path = '/queues/%(queue_id)s/groups/%(group_id)s/messages' -class Message(_base.Resource): + allow_list = True + + _query_mapping = resource.QueryParameters( + 'max_msgs', 'time_wait', 'ack_wait' + ) + + #: Queue id + queue_id = resource.URI('queue_id') + #: Group ID (part of URL for listing) + group_id = resource.URI('group_id') + + # Properties + #: Attributes + attributes = resource.Body('attributes', type=dict) + #: Message body + body = resource.Body('body') + #: Error info + error = resource.Body('error') + #: Error code + error_code = resource.Body('error_code') + #: Message handler (ID) + handler = resource.Body('handler', alternate_id=True) + #: State (0 - success) + state = resource.Body('state', type=int) + + def _collect_attrs(self, attrs): + """ Save remaining attributes under "attributes" attribute + """ + body = self._consume_body_attrs(attrs) + header = self._consume_body_attrs(attrs) + uri = self._consume_body_attrs(attrs) + body['attributes'] = attrs + computed = self._consume_attrs(self._computed_mapping(), attrs) + + return body, header, uri, computed + + @classmethod + def list(cls, session, paginated=True, base_path=None, + allow_unknown_params=False, **params): + + microversion = cls._get_microversion_for_list(session) + + if base_path is None: + base_path = cls.base_path + params = cls._query_mapping._validate( + params, base_path=base_path, + allow_unknown_params=allow_unknown_params) + query_params = cls._query_mapping._transpose(params, cls) + uri = base_path % params + + params = cls._query_mapping._validate( + params, base_path=base_path, + allow_unknown_params=allow_unknown_params) + query_params = cls._query_mapping._transpose(params, cls) + uri = base_path % params + + while uri: + # Copy query_params due to weird mock unittest interactions + response = session.get( + uri, + headers={"Accept": "application/json"}, + params=query_params.copy(), + microversion=microversion) + exceptions.raise_from_response(response) + data = response.json() + resources = data + + if not isinstance(resources, list): + resources = [resources] + + for raw_resource in resources: + attrs = raw_resource.pop('message') + handler = raw_resource.pop('handler') + value = cls.existing( + microversion=microversion, + connection=session._get_connection(), + id=handler, + **attrs) + yield value + + if not resources: + return + + +class Messages(resource.Resource): resources_key = 'messages' base_path = '/queues/%(queue_id)s/messages' # capabilities allow_create = True + allow_list = True # Properties #: Queue id queue_id = resource.URI('queue_id') - messages = resource.Body('messages', type=list) + #: Messages + messages = resource.Body('messages', type=list, list_type=Message) + #: Whether message ID should be returned + return_id = resource.Body('returnId', type=bool) diff --git a/otcextensions/sdk/dms/v1/queue.py b/otcextensions/sdk/dms/v1/queue.py index 08c0bbb25..25a075fb4 100644 --- a/otcextensions/sdk/dms/v1/queue.py +++ b/otcextensions/sdk/dms/v1/queue.py @@ -10,12 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. from openstack import resource -from openstack import _log from otcextensions.sdk.dms.v1 import _base -_logger = _log.setup_logging('openstack') - class Queue(_base.Resource): @@ -26,26 +23,46 @@ class Queue(_base.Resource): # capabilities allow_create = True allow_list = True - allow_get = True + allow_fetch = True allow_delete = True + _query_mapping = resource.QueryParameters( + 'include_deadletter', + 'include_messages_num' + ) + # Properties + #: Created time + #: *Type: int* + created = resource.Body('created', type=int) + #: Description for the queue. + #: The value is a string of a maximum of 160 characters and cannot + #: contain the angle brackets (<>). + description = resource.Body('description') #: Queue Id id = resource.Body('id') + #: Max consume count number + #: *Type: int* + #: Value range: 1–100. + max_consume_count = resource.Body('max_consume_count', type=int) #: Queue name name = resource.Body('name') - #: Queue mode + #: Queue mode: + #: `NORMAL`: Standard queue, which supports high concurrency performance + #: but cannot guarantee that messages are retrieved in the exact + #: sequence as how they are received. + #: `FIFO`: First-in-first-out (FIFO) queue, which guarantees that messages + #: are retrieved in the exact sequence as how they are received. + #: `KAFKA_HA`: High-reliability Kafka queue. All message replicas are + #: flushed to a disk synchronously, ensuring message reliability. + #: `KAFKA_HT`: High-throughput Kafka queue. All message replicas are + #: flushed to a disk asynchronously, ensuring high performance. queue_mode = resource.Body('queue_mode') - #: Description for the queue - description = resource.Body('description') - #: Redrive policy + #: Redrive policy. + #: Supported values: `enable`, `disable`. Default: `disable redrive_policy = resource.Body('redrive_policy') - #: Max consume count number - #: *Type: int* - max_consume_count = resource.Body('max_consume_count', type=int) - #: Retentions hours + #: Indicates the hours of storing messages in the Kafka queue. + #: This parameter is valid only when queue_mode is set to KAFKA_HA or + #: KAFKA_HT. Value range: 1–72. #: *Type: int* retention_hours = resource.Body('retention_hours', type=int) - #: Created time - #: *Type: int* - created = resource.Body('created', type=int) diff --git a/otcextensions/sdk/dms/v1/quota.py b/otcextensions/sdk/dms/v1/quota.py index 3854277e4..9be46c82d 100644 --- a/otcextensions/sdk/dms/v1/quota.py +++ b/otcextensions/sdk/dms/v1/quota.py @@ -19,7 +19,7 @@ class Quota(_base.Resource): - """AutoScaling Quota resource""" + """DMS Quota resource""" resources_key = 'quotas.resources' base_path = '/quotas/dms' diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_group.py b/otcextensions/tests/unit/sdk/dms/v1/test_group.py new file mode 100644 index 000000000..53d85e62c --- /dev/null +++ b/otcextensions/tests/unit/sdk/dms/v1/test_group.py @@ -0,0 +1,105 @@ +# 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 keystoneauth1 import adapter + +from unittest import mock + +from openstack.tests.unit import base + +from otcextensions.sdk.dms.v1 import group + +GROUP_EXAMPLE = { + "queue_name": "queue_001", + "groups": [{ + "id": "g-5ec247fd-d4a2-4d4f-9876-e4ff3280c461", + "name": "abcDffD", + "produced_messages": 0, + "consumed_messages": 0, + "available_messages": 0 + }], + "redrive_policy": "enable", + "queue_id": "9bf46390-38a2-462d-b392-4d5b2d519c55" +} + + +class TestGroup(base.TestCase): + + example = GROUP_EXAMPLE['groups'][0] + objcls = group.Group + + def setUp(self): + super(TestGroup, self).setUp() + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = None + + def test_basic(self): + sot = self.objcls() + + self.assertEqual('queues/%(queue_id)s/groups', sot.base_path) + + self.assertDictEqual({ + 'current_page': 'current_page', + 'include_deadletter': 'include_deadletter', + 'limit': 'limit', + 'marker': 'marker', + 'page_size': 'page_size'}, + sot._query_mapping._mapping + ) + + def test_make_it(self): + + sot = self.objcls(**self.example) + self.assertEqual(self.example['id'], sot.id) + self.assertEqual(self.example['name'], sot.name) + self.assertEqual( + self.example['produced_messages'], + sot.produced_messages) + self.assertEqual( + self.example['consumed_messages'], + sot.consumed_messages) + self.assertEqual( + self.example['available_messages'], + sot.available_messages) + + def test_create(self): + sot = group.Group(queue_id='qid', name='grp') + + response = mock.Mock() + response.body = {'groups': [{'id': '1', 'name': 'grp'}]} + response.json = mock.Mock(return_value=response.body) + self.sess.post = mock.Mock(return_value=response) + + obj = sot.create(self.sess) + self.sess.post.assert_called_with( + 'queues/qid/groups', + json={'groups': [{'name': 'grp'}]} + ) + self.assertEqual('grp', obj.name) + self.assertEqual('1', obj.id) + + def test_ack(self): + sot = group.Group(queue_id='qid', id='gid') + + response = mock.Mock() + response.json = mock.Mock(return_value={}) + response.status_code = 200 + self.sess.post = mock.Mock(return_value=response) + + sot.ack(self.sess, mock.Mock(id='qid'), ['1', '2'], 'failure') + + self.sess.post.assert_called_with( + 'queues/qid/groups/gid/ack', + json={'message': [ + {'handler': '1', 'status': 'failure'}, + {'handler': '2', 'status': 'failure'} + ]} + ) diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_message.py b/otcextensions/tests/unit/sdk/dms/v1/test_message.py new file mode 100644 index 000000000..30fa4dd95 --- /dev/null +++ b/otcextensions/tests/unit/sdk/dms/v1/test_message.py @@ -0,0 +1,96 @@ +# 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 openstack.tests.unit import base + +from otcextensions.sdk.dms.v1 import message + + +MESSAGES_EXAMPLE = { + "messages": [ + { + "body": "TEST11", + "attributes": { + "attribute1": "value1", + "attribute2": "value2" + }, + }, { + "body": { + "foo": "test02" + }, + "attributes": { + "attribute1": "value1", + "attribute2": "value2" + }, + } + ] +} + + +class TestMessage(base.TestCase): + + def test_basic(self): + sot = message.Message() + + self.assertEqual( + '/queues/%(queue_id)s/groups/%(group_id)s/messages', + sot.base_path) + self.assertEqual('message', sot.resource_key) + self.assertTrue(sot.allow_list) + + self.assertDictEqual({ + 'ack_wait': 'ack_wait', + 'limit': 'limit', + 'marker': 'marker', + 'max_msgs': 'max_msgs', + 'time_wait': 'time_wait'}, + sot._query_mapping._mapping + ) + + def test_make_it(self): + + sot = message.Message() + self.assertIsNone(sot.queue_id) + # TODO() + + def test__collect_attrs(self): + # TODO() + pass + + def test_list(self): + # TODO() + pass + + +class TestMessages(base.TestCase): + + def test_basic(self): + sot = message.Messages() + + self.assertEqual('/queues/%(queue_id)s/messages', sot.base_path) + self.assertEqual('messages', sot.resources_key) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_create) + + self.assertDictEqual( + { + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping + ) + + def test_make_it(self): + + sot = message.Messages(**MESSAGES_EXAMPLE) + self.assertEqual(2, len(sot.messages)) + # TODO() diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py b/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py index e60da5dd7..a6cdcd318 100644 --- a/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py +++ b/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. -import mock - from otcextensions.sdk.dms.v1 import _proxy +from otcextensions.sdk.dms.v1 import group as _group +from otcextensions.sdk.dms.v1 import message as _message from otcextensions.sdk.dms.v1 import queue as _queue from openstack.tests.unit import test_proxy_base @@ -29,21 +29,26 @@ def test_create_queue(self): self.verify_create( self.proxy.create_queue, _queue.Queue, - mock_method='otcextensions.sdk.sdk_proxy.Proxy._create', - method_kwargs={ - 'name': 'queue_001' - }, + ) + + def test_queues(self): + self.verify_list( + self.proxy.queues, + _queue.Queue, expected_kwargs={ - 'name': 'queue_001' + 'paginated': False } ) - def test_queues(self): + def test_queues_qp(self): self.verify_list( self.proxy.queues, _queue.Queue, - mock_method='otcextensions.sdk.sdk_proxy.Proxy._list', + method_kwargs={ + 'include_deadletter': True, + }, expected_kwargs={ + 'include_deadletter': True, 'paginated': False } ) @@ -51,68 +56,130 @@ def test_queues(self): def test_get_queue(self): self.verify_get( self.proxy.get_queue, - _queue.Queue, - mock_method='otcextensions.sdk.sdk_proxy.Proxy._get', - expected_kwargs={} + _queue.Queue ) def test_delete_queue(self): self.verify_delete( self.proxy.delete_queue, _queue.Queue, - True, - mock_method='otcextensions.sdk.sdk_proxy.Proxy._delete', - expected_kwargs={} + False + ) + + def test_delete_queue_ignore(self): + self.verify_delete( + self.proxy.delete_queue, + _queue.Queue, + True ) def test_create_group(self): - self._verify2( - 'otcextensions.sdk.dms.v1.group.Group.create', + self.verify_create( self.proxy.create_group, - method_args=['queue', 'name'], - expected_args=[mock.ANY], - expected_kwargs={'group': 'name'}) + _group.Group, + method_kwargs={'queue': 'qip', 'name': 'grp'}, + expected_kwargs={'queue_id': 'qip', 'name': 'grp'}) def test_groups(self): - self._verify2( - 'otcextensions.sdk.sdk_proxy.Proxy._list', + self.verify_list( self.proxy.groups, - method_args=['queue'], - expected_args=[mock.ANY], + _group.Group, + method_kwargs={'queue': 'qid'}, expected_kwargs={ - 'queue_id': 'queue', - 'paginated': False, - 'include_deadletter': False}) + 'queue_id': 'qid', + 'paginated': False} + ) def test_delete_group(self): - self._verify2( - 'otcextensions.sdk.sdk_proxy.Proxy._delete', + self.verify_delete( self.proxy.delete_group, - method_args=['queue', 'group'], - expected_args=[mock.ANY, 'group'], - expected_kwargs={'queue_id': 'queue'}) + _group.Group, + False, + input_path_args=['QID', "resource_or_id"], + expected_path_args={'queue_id': 'QID'} + ) - def test_send_messages(self): - self._verify2( - 'otcextensions.sdk.sdk_proxy.Proxy._create', + def test_delete_group_ignore(self): + self.verify_delete( + self.proxy.delete_group, + _group.Group, + True, + input_path_args=['QID', "resource_or_id"], + expected_path_args={'queue_id': 'QID'} + ) + + ###### + # Messages + + def test_send_messages_dict(self): + self.verify_create( + self.proxy.send_messages, + _message.Messages, + method_kwargs={ + 'queue': 'qid', + 'messages': [{'body': 'b1'}] + }, + expected_kwargs={ + 'queue_id': 'qid', + 'messages': [ + {'attributes': {}, 'body': 'b1'} + ], + 'return_id': False + } + ) + + def test_send_messages_msg(self): + self.verify_create( self.proxy.send_messages, - method_args=['queue'], - expected_args=[mock.ANY], - expected_kwargs={'queue_id': 'queue'}) + _message.Messages, + method_kwargs={ + 'queue': 'qid', + 'messages': [_message.Message(body='b1')] + }, + expected_kwargs={ + 'queue_id': 'qid', + 'messages': [ + {'attributes': {}, 'body': 'b1'} + ], + 'return_id': False + } + ) + + def test_send_message(self): + self.verify_create( + self.proxy.send_message, + _message.Messages, + method_kwargs={ + 'queue': 'qid', + 'body': 'b1', + 'p1': 'v1' + }, + expected_kwargs={ + 'queue_id': 'qid', + 'messages': [ + {'attributes': {'p1': 'v1'}, 'body': 'b1'} + ], + 'return_id': True + }, + method_result=_message.Message(id='1'), + expected_result=_message.Messages( + messages=[_message.Message(id='1')]) + ) def test_consume_message(self): - self._verify2( - 'otcextensions.sdk.dms.v1.group_message.GroupMessage.list', + self.verify_list( self.proxy.consume_message, - method_args=['queue', 'group'], - expected_args=[mock.ANY], + _message.Message, + method_kwargs={ + 'queue': 'qid', + 'group': 'gid' + }, expected_kwargs={ - 'queue_id': 'queue', - 'consumer_group_id': 'group', - 'endpoint_override': None, - 'headers': None, - 'paginated': False, - 'requests_auth': None}) + 'queue_id': 'qid', + 'group_id': 'gid' + }, + base_path='/queues/%(queue_id)s/groups/%(group_id)s/messages' + ) def test_ack_consumed_message(self): pass diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_queue.py b/otcextensions/tests/unit/sdk/dms/v1/test_queue.py index 40558b362..552ba1db3 100644 --- a/otcextensions/tests/unit/sdk/dms/v1/test_queue.py +++ b/otcextensions/tests/unit/sdk/dms/v1/test_queue.py @@ -12,9 +12,6 @@ from openstack.tests.unit import base -from otcextensions.sdk.dms.v1 import group -from otcextensions.sdk.dms.v1 import group_message -from otcextensions.sdk.dms.v1 import message from otcextensions.sdk.dms.v1 import queue EXAMPLE = { @@ -26,45 +23,6 @@ "retention_hours": 7 } -GROUP_EXAMPLE = { - "queue_name": "queue_001", - "groups": [{ - "id": "g-5ec247fd-d4a2-4d4f-9876-e4ff3280c461", - "name": "abcDffD", - "produced_messages": 0, - "consumed_messages": 0, - "available_messages": 0 - }], - "redrive_policy": "enable", - "queue_id": "9bf46390-38a2-462d-b392-4d5b2d519c55" -} - -MSG_GROUP_EXAMPLE = { - "message": { - "body": { - "foo": "123=" - }, - "attributes": { - "attribute1": "value1", - "attribute2": "value2" - } - }, - "handler": "eyJjZyI6Im15X2pzb25fZ3JvdXAiLCJjaSI6InJlc3QtY29uc3VtZXItYz", -} - -MSG_EXAMPLE = { - "messages": - [ - { - "body": "TEST11", - "attributes": { - "attribute1": "value1", - "attribute2": "value2" - } - } - ] -} - class TestQueue(base.TestCase): @@ -79,9 +37,17 @@ def test_basic(self): self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_commit) self.assertTrue(sot.allow_delete) + self.assertDictEqual({ + 'include_deadletter': 'include_deadletter', + 'include_messages_num': 'include_messages_num', + 'limit': 'limit', + 'marker': 'marker'}, + sot._query_mapping._mapping + ) + def test_make_it(self): sot = self.objcls(**self.example) @@ -93,64 +59,3 @@ def test_make_it(self): self.assertEqual(self.example['redrive_policy'], sot.redrive_policy) self.assertEqual(self.example['retention_hours'], sot.retention_hours) - - -class TestGroup(base.TestCase): - - example = GROUP_EXAMPLE['groups'][0] - objcls = group.Group - - def test_basic(self): - sot = self.objcls() - - self.assertEqual('queues/%(queue_id)s/groups', sot.base_path) - - def test_make_it(self): - - sot = self.objcls(**self.example) - self.assertEqual(self.example['id'], sot.id) - self.assertEqual(self.example['name'], sot.name) - self.assertEqual( - self.example['produced_messages'], - sot.produced_messages) - self.assertEqual( - self.example['consumed_messages'], - sot.consumed_messages) - self.assertEqual( - self.example['available_messages'], - sot.available_messages) - - -class TestMessage(base.TestCase): - - example = MSG_EXAMPLE - objcls = message.Message - - def test_basic(self): - sot = self.objcls() - - self.assertEqual('/queues/%(queue_id)s/messages', sot.base_path) - - def test_make_it(self): - - sot = self.objcls(**self.example) - self.assertEqual(self.example['messages'], sot.messages) - - -class TestGroupMessage(base.TestCase): - - example = MSG_GROUP_EXAMPLE - objcls = group_message.GroupMessage - - def test_basic(self): - sot = self.objcls() - - self.assertEqual( - '/queues/%(queue_id)s/groups/%(consumer_group_id)s/messages', - sot.base_path) - - def test_make_it(self): - - sot = self.objcls(**self.example) - self.assertEqual(self.example['message'], sot.message) - self.assertEqual(self.example['handler'], sot.handler) diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_quota.py b/otcextensions/tests/unit/sdk/dms/v1/test_quota.py index 58d5a61a8..4bc0e085f 100644 --- a/otcextensions/tests/unit/sdk/dms/v1/test_quota.py +++ b/otcextensions/tests/unit/sdk/dms/v1/test_quota.py @@ -50,7 +50,7 @@ def test_basic(self): self.assertEqual('/quotas/dms', sot.base_path) self.assertTrue(sot.allow_list) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) + self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_update) self.assertFalse(sot.allow_delete) From b427b5333c73ebeebdd952e78572f165050b0aed Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 16 Apr 2020 17:40:56 +0200 Subject: [PATCH 02/24] pin sdk version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1e9fdb979..831e89211 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -openstacksdk>=0.36.0 # Apache-2.0 +openstacksdk>=0.36.0,<0.43.0 # Apache-2.0 oslo.i18n>3.15.3 # Apache-2.0 From 860c7b42d66e096565bfb595693b44cb8dec2261 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 17 Apr 2020 16:15:27 +0200 Subject: [PATCH 03/24] add instance resource with proxies --- otcextensions/sdk/dms/v1/_proxy.py | 78 +++++++++- otcextensions/sdk/dms/v1/instance.py | 118 ++++++++++++++ .../tests/unit/sdk/dms/v1/test_instance.py | 147 ++++++++++++++++++ .../tests/unit/sdk/dms/v1/test_proxy.py | 50 ++++++ 4 files changed, 391 insertions(+), 2 deletions(-) create mode 100644 otcextensions/sdk/dms/v1/instance.py create mode 100644 otcextensions/tests/unit/sdk/dms/v1/test_instance.py diff --git a/otcextensions/sdk/dms/v1/_proxy.py b/otcextensions/sdk/dms/v1/_proxy.py index c01a94ff9..6247eb181 100644 --- a/otcextensions/sdk/dms/v1/_proxy.py +++ b/otcextensions/sdk/dms/v1/_proxy.py @@ -11,10 +11,12 @@ # under the License. from openstack import proxy -from otcextensions.sdk.dms.v1 import queue as _queue -from otcextensions.sdk.dms.v1 import quota as _quota + from otcextensions.sdk.dms.v1 import group as _group +from otcextensions.sdk.dms.v1 import instance as _instance from otcextensions.sdk.dms.v1 import message as _message +from otcextensions.sdk.dms.v1 import queue as _queue +from otcextensions.sdk.dms.v1 import quota as _quota class Proxy(proxy.Proxy): @@ -219,6 +221,78 @@ def ack_message(self, queue, group, messages, status='success'): return group_obj.ack(self, queue_obj, messages, status=status) + # ======== Instances ======= + def instances(self, **kwargs): + """List all DMS Instances + + :param dict kwargs: List of query parameters + + :returns: A generator of Instance object of + :class:`~otcextensions.sdk.dms.v1.instance.Instance` + """ + return self._list(_instance.Instance, paginated=False, **kwargs) + + def create_instance(self, **attrs): + """Create an DMS instance + + :param dict attrs: instance attributes + :class:`~otcextensions.sdk.dms.v1.instance.Instance` + :returns: An instance class object + :class:`~otcextensions.sdk.dms.v1.instance.Instance` + """ + return self._create(_instance.Instance, **attrs) + + def delete_instance(self, instance, ignore_missing=True): + """Delete DMS Instance + + :param instance: The instance id or an object instance of + :class:`~otcextensions.sdk.dms.v1.instance.Instance` + :param bool ignore_missing: When set to ``False`` + :class:`~otcextensions.sdk.exceptions.ResourceNotFound` will be + raised when the queue does not exist. + :returns: `None` + """ + self._delete(_instance.Instance, instance, + ignore_missing=ignore_missing) + + def find_instance(self, name_or_id, ignore_missing=False): + """Find DMS Instance by name or id + + :param name_or_id: Name or ID + :param bool ignore_missing: When set to ``False`` + :class:`~otcextensions.sdk.exceptions.ResourceNotFound` will be + raised when the instance does not exist. + + :returns: one object of class + :class:`~otcextensions.sdk.dms.v1.instance.Instance` + """ + return self._find(_instance.Instance, name_or_id, + ignore_missing=ignore_missing) + + def get_instance(self, instance): + """Get detail about a given instance id + + :param instance: The instance id or an instance of + :class:`~otcextensions.sdk.dms.v1.instance.Instance` + :returns: one object of class + :class:`~otcextensions.sdk.dms.v1.instance.Instance` + """ + return self._get(_instance.Instance, instance) + + def update_instance(self, instance, **attrs): + """Update an Instance + + :param instance: Either the ID of an instance or a + :class:`~otcextensions.sdk.dms.v1.instance.Instance` instance. + :param dict attrs: The attributes to update on the instance + represented by ``value``. + + :returns: The updated instance + :rtype: :class:`~otcextensions.sdk.dms.v1.instance.Instance` + """ + return self._update(_instance.Instance, instance, + **attrs) + def quotas(self): """List quota diff --git a/otcextensions/sdk/dms/v1/instance.py b/otcextensions/sdk/dms/v1/instance.py new file mode 100644 index 000000000..64dabc6fe --- /dev/null +++ b/otcextensions/sdk/dms/v1/instance.py @@ -0,0 +1,118 @@ +# 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 openstack import resource + +from otcextensions.sdk.dms.v1 import _base + + +class Instance(_base.Resource): + """DMS Instance resource""" + resources_key = 'instances' + base_path = '/instances' + + _query_mapping = resource.QueryParameters( + 'engine', 'name', 'status', 'include_failure', + 'exact_match_name', + include_failure='includeFailure', + exact_match_name='exactMatchName' + ) + + # capabilities + allow_list = True + allow_fetch = True + allow_create = True + allow_delete = True + allow_commit = True + + #: Properties + #: The username of an instance. + access_user = resource.Body('access_user') + #: List of availability zones the instance belongs to + availability_zones = resource.Body('available_zones', type=list) + #: Billing mode + charging_mode = resource.Body('charging_mode') + #: IP address of the instance + connect_address = resource.Body('connect_address') + #: Instance creation time + created_at = resource.Body('created_at') + #: Instance description + description = resource.Body('description') + #: Message engine. + engine = resource.Body('engine') + #: Engine version + engine_version = resource.Body('engine_version') + #: Instance id + instance_id = resource.Body('instance_id', alternate_id=True) + #: Associate floating IP + is_public = resource.Body('enable_publicip', type=bool) + #: Enable SSL + is_ssl = resource.Body('ssl_enable', type=bool) + #: Kafka public status + kafka_public_status = resource.Body('kafka_public_status') + #: End time of the maintenance window + maintenance_end = resource.Body('maintain_end') + #: Beginning of the maintenance window + maintenance_start = resource.Body('maintain_begin') + #: Maximum number of partitions + max_partitions = resource.Body('partition_num', type=int) + #: User password + password = resource.Body('password') + #: Port number of the instance + port = resource.Body('port', type=int) + #: Product ID + product_id = resource.Body('product_id') + #: Bandwidth of the public access + public_bandwidth = resource.Body('public_bandwidth', type=int) + #: Retention policy + retention_policy = resource.Body('retention_policy') + #: Router ID + router_id = resource.Body('vpc_id') + #: Router name + router_name = resource.Body('vpc_name') + #: Security group ID + security_group_id = resource.Body('security_group_id') + #: Security group Name + security_group_name = resource.Body('security_group_name') + #: Service type + service_type = resource.Body('service_type') + #: specification of the instance + spec = resource.Body('specification') + #: Specification code of the instance: + #: `dms.instance.kafka.cluster.c3.min` + #: `dms.instance.kafka.cluster.c3.small.2` + #: `dms.instance.kafka.cluster.c3.middle.2` + #: `dms.instance.kafka.cluster.c3.high.2` + spec_code = resource.Body('resource_spec_code') + #: Instance status + #: CREATING, CREATEFAILED, RUNNING, ERROR, STARTING, + #: RESTARTING, CLOSING, FROZEN + status = resource.Body('status') + #: Storage resource ID + storage_resource_id = resource.Body('storage_resource_id') + #: Storage I/O specification code + storage_spec_code = resource.Body('storage_spec_code') + #: Storage type + storage_type = resource.Body('storage_type') + #: Storage space GB + storage = resource.Body('storage_space', type=int) + #: Subnet ID + subnet_id = resource.Body('subnet_id') + #: Total storage space GB + total_storage = resource.Body('total_storage_space', type=int) + #: Instance type + type = resource.Body('type') + #: Used storage GB + used_storage = resource.Body('used_storage_space', type=int) + #: User ID + user_id = resource.Body('user_id') + #: User name + user_name = resource.Body('user_name') diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_instance.py b/otcextensions/tests/unit/sdk/dms/v1/test_instance.py new file mode 100644 index 000000000..8dfe9523a --- /dev/null +++ b/otcextensions/tests/unit/sdk/dms/v1/test_instance.py @@ -0,0 +1,147 @@ +# 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 openstack.tests.unit import base + +from otcextensions.sdk.dms.v1 import instance + + +JSON_DATA = { + "name": "kafka-test", + "description": "", + "engine": "kafka", + "engine_version": "2.3.0", + "storage_space": 600, + "access_user": "", + "password": "", + "vpc_id": "1e93f86e-13af-46c8-97d6-d40fa62b76c2", + "security_group_id": "0aaa0033-bf7f-4c41-a6c2-18cd04cad2c8", + "subnet_id": "b5fa806c-35e7-4299-b659-b39398dd4718", + "available_zones": ["d573142f24894ef3bd3664de068b44b0"], + "product_id": "00300-30308-0--0", + "maintain_begin": "22:00", + "maintain_end": "02:00", + "ssl_enable": False, + "enable_publicip": False, + "publicip_id": "", + "enterprise_project_id": "0", + "specification": "100MB", + "partition_num": "300", + "retention_policy": "produce_reject", + "connector_enable": False, + "storage_spec_code": "dms.physical.storage.ultra" +} + + +class TestInstance(base.TestCase): + + def test_basic(self): + sot = instance.Instance() + + self.assertEqual('/instances', sot.base_path) + self.assertEqual('instances', sot.resources_key) + + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + + self.assertDictEqual({ + 'engine': 'engine', + 'exact_match_name': 'exactMatchName', + 'include_failure': 'includeFailure', + 'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'status': 'status'}, + sot._query_mapping._mapping + ) + + def test_make_it(self): + + sot = instance.Instance(**JSON_DATA) + self.assertEqual(JSON_DATA['name'], sot.name) + self.assertEqual(JSON_DATA['access_user'], sot.access_user) + self.assertEqual(JSON_DATA['available_zones'], sot.availability_zones) + self.assertEqual(JSON_DATA.get('charging_mode', None), + sot.charging_mode) + self.assertEqual(JSON_DATA.get('connect_address', None), + sot.connect_address) + self.assertEqual(JSON_DATA.get('created_at', None), + sot.created_at) + self.assertEqual(JSON_DATA.get('description', None), + sot.description) + self.assertEqual(JSON_DATA.get('engine', None), + sot.engine) + self.assertEqual(JSON_DATA.get('engine_version', None), + sot.engine_version) + self.assertEqual(JSON_DATA.get('instance_id', None), + sot.id) + self.assertEqual(JSON_DATA.get('enable_publicip', False), + sot.is_public) + self.assertEqual(JSON_DATA.get('ssl_enable', None), + sot.is_ssl) + self.assertEqual(JSON_DATA.get('kafka_public_status', None), + sot.kafka_public_status) + self.assertEqual(JSON_DATA.get('maintain_end', None), + sot.maintenance_end) + self.assertEqual(JSON_DATA.get('maintain_begin', None), + sot.maintenance_start) + self.assertEqual(int(JSON_DATA.get('partition_num', None)), + sot.max_partitions) + self.assertEqual(JSON_DATA.get('password', None), + sot.password) + self.assertEqual(JSON_DATA.get('port', None), + sot.port) + self.assertEqual(JSON_DATA.get('product_id', None), + sot.product_id) + self.assertEqual(JSON_DATA.get('public_bandwidth', None), + sot.public_bandwidth) + self.assertEqual(JSON_DATA.get('retention_policy', None), + sot.retention_policy) + self.assertEqual(JSON_DATA.get('vpc_id', None), + sot.router_id) + self.assertEqual(JSON_DATA.get('vpc_name', None), + sot.router_name) + self.assertEqual(JSON_DATA.get('security_group_id', None), + sot.security_group_id) + self.assertEqual(JSON_DATA.get('security_group_name', None), + sot.security_group_name) + self.assertEqual(JSON_DATA.get('service_type', None), + sot.service_type) + self.assertEqual(JSON_DATA.get('specification', None), + sot.spec) + self.assertEqual(JSON_DATA.get('resource_spec_code', None), + sot.spec_code) + self.assertEqual(JSON_DATA.get('status', None), + sot.status) + self.assertEqual(JSON_DATA.get('storage_resource_id', None), + sot.storage_resource_id) + self.assertEqual(JSON_DATA.get('storage_spec_code', None), + sot.storage_spec_code) + self.assertEqual(JSON_DATA.get('storage_type', None), + sot.storage_type) + self.assertEqual(JSON_DATA.get('storage_space', None), + sot.storage) + self.assertEqual(JSON_DATA.get('subnet_id', None), + sot.subnet_id) + self.assertEqual(JSON_DATA.get('total_storage_space', None), + sot.total_storage) + self.assertEqual(JSON_DATA.get('type', None), + sot.type) + self.assertEqual(JSON_DATA.get('used_storage_space', None), + sot.storage_type) + self.assertEqual(JSON_DATA.get('user_id', None), + sot.user_id) + self.assertEqual(JSON_DATA.get('user_name', None), + sot.user_name) diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py b/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py index a6cdcd318..4acf78714 100644 --- a/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py +++ b/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py @@ -12,6 +12,7 @@ from otcextensions.sdk.dms.v1 import _proxy from otcextensions.sdk.dms.v1 import group as _group +from otcextensions.sdk.dms.v1 import instance as _instance from otcextensions.sdk.dms.v1 import message as _message from otcextensions.sdk.dms.v1 import queue as _queue @@ -186,3 +187,52 @@ def test_ack_consumed_message(self): def test_quotas(self): pass + + ###### + # Instances + def test_instances(self): + self.verify_list( + self.proxy.instances, + _instance.Instance, + expected_kwargs={ + 'paginated': False + } + ) + + def test_create_instance(self): + self.verify_create( + self.proxy.create_instance, + _instance.Instance + ) + + def test_delete_instance(self): + self.verify_delete( + self.proxy.delete_instance, + _instance.Instance, + False, + ) + + def test_delete_instance_ignore(self): + self.verify_delete( + self.proxy.delete_instance, + _instance.Instance, + True, + ) + + def test_find_instance(self): + self.verify_find( + self.proxy.find_instance, + _instance.Instance + ) + + def test_get_instance(self): + self.verify_get( + self.proxy.get_instance, + _instance.Instance + ) + + def test_update_instance(self): + self.verify_update( + self.proxy.update_instance, + _instance.Instance + ) From 4a1eec0ab65b590acb56edebe4ccbb9cb8d30814 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 17 Apr 2020 16:20:06 +0200 Subject: [PATCH 04/24] disable queue for now --- otcextensions/sdk/dms/v1/_proxy.py | 14 +++--- .../tests/unit/sdk/dms/v1/test_quota.py | 44 ++++++++++--------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/otcextensions/sdk/dms/v1/_proxy.py b/otcextensions/sdk/dms/v1/_proxy.py index 6247eb181..90f2a8e98 100644 --- a/otcextensions/sdk/dms/v1/_proxy.py +++ b/otcextensions/sdk/dms/v1/_proxy.py @@ -293,10 +293,10 @@ def update_instance(self, instance, **attrs): return self._update(_instance.Instance, instance, **attrs) - def quotas(self): - """List quota - - :returns: A generator of Quota object - :rtype: :class:`~otcextensions.sdk.dms.v1.quota.Quota` - """ - return self._list(_quota.Quota) +# def quotas(self): +# """List quota +# +# :returns: A generator of Quota object +# :rtype: :class:`~otcextensions.sdk.dms.v1.quota.Quota` +# """ +# return self._list(_quota.Quota) diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_quota.py b/otcextensions/tests/unit/sdk/dms/v1/test_quota.py index 4bc0e085f..eb1ec0092 100644 --- a/otcextensions/tests/unit/sdk/dms/v1/test_quota.py +++ b/otcextensions/tests/unit/sdk/dms/v1/test_quota.py @@ -44,6 +44,7 @@ def setUp(self): self.sot = quota.Quota() def test_basic(self): + pass sot = quota.Quota() self.assertEqual(None, sot.resource_key) self.assertEqual('quotas.resources', sot.resources_key) @@ -51,7 +52,7 @@ def test_basic(self): self.assertTrue(sot.allow_list) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) def test_make_it(self): @@ -64,23 +65,24 @@ def test_make_it(self): self.assertEqual(obj['max'], sot.max) def test_list(self): - sot = quota.Quota() - mock_response = mock.Mock() - mock_response.status_code = 200 - mock_response.json.return_value = copy.deepcopy(EXAMPLE_LIST) - - self.sess.get.return_value = mock_response - - result = list(sot.list(self.sess)) - - self.sess.get.assert_called_once_with( - '/quotas/dms', - headers={'Content-Type': 'application/json'}, - params={} - ) - - expected_list = [ - quota.Quota.existing( - **EXAMPLE_LIST['quotas']['resources'][0])] - - self.assertEqual(expected_list, result) + pass +# sot = quota.Quota() +# mock_response = mock.Mock() +# mock_response.status_code = 200 +# mock_response.json.return_value = copy.deepcopy(EXAMPLE_LIST) +# +# self.sess.get.return_value = mock_response +# +# result = list(sot.list(self.sess)) +# +# self.sess.get.assert_called_once_with( +# '/quotas/dms', +# headers={'Content-Type': 'application/json'}, +# params={} +# ) +# +# expected_list = [ +# quota.Quota.existing( +# **EXAMPLE_LIST['quotas']['resources'][0])] +# +# self.assertEqual(expected_list, result) From aa3afe6381fccc8da1aaa4e9b323634fa361bb10 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 17 Apr 2020 17:05:32 +0200 Subject: [PATCH 05/24] repair docs --- otcextensions/sdk/dms/v1/_proxy.py | 1 - otcextensions/sdk/dms/v1/queue.py | 2 +- otcextensions/tests/unit/sdk/dms/v1/test_quota.py | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/otcextensions/sdk/dms/v1/_proxy.py b/otcextensions/sdk/dms/v1/_proxy.py index 90f2a8e98..3dc976aaf 100644 --- a/otcextensions/sdk/dms/v1/_proxy.py +++ b/otcextensions/sdk/dms/v1/_proxy.py @@ -16,7 +16,6 @@ from otcextensions.sdk.dms.v1 import instance as _instance from otcextensions.sdk.dms.v1 import message as _message from otcextensions.sdk.dms.v1 import queue as _queue -from otcextensions.sdk.dms.v1 import quota as _quota class Proxy(proxy.Proxy): diff --git a/otcextensions/sdk/dms/v1/queue.py b/otcextensions/sdk/dms/v1/queue.py index 25a075fb4..9ba733fa4 100644 --- a/otcextensions/sdk/dms/v1/queue.py +++ b/otcextensions/sdk/dms/v1/queue.py @@ -59,7 +59,7 @@ class Queue(_base.Resource): #: flushed to a disk asynchronously, ensuring high performance. queue_mode = resource.Body('queue_mode') #: Redrive policy. - #: Supported values: `enable`, `disable`. Default: `disable + #: Supported values: `enable`, `disable`. Default: `disable` redrive_policy = resource.Body('redrive_policy') #: Indicates the hours of storing messages in the Kafka queue. #: This parameter is valid only when queue_mode is set to KAFKA_HA or diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_quota.py b/otcextensions/tests/unit/sdk/dms/v1/test_quota.py index eb1ec0092..ab2a1d2ad 100644 --- a/otcextensions/tests/unit/sdk/dms/v1/test_quota.py +++ b/otcextensions/tests/unit/sdk/dms/v1/test_quota.py @@ -9,8 +9,6 @@ # 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 copy - from keystoneauth1 import adapter import mock From 7346c29d68fb8a076c697d2b9e38d05a3a197984 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 17 Apr 2020 17:12:37 +0200 Subject: [PATCH 06/24] really repair docs --- doc/source/sdk/proxies/dms.rst | 7 ------- doc/source/sdk/resources/dms/index.rst | 2 -- doc/source/sdk/resources/dms/v1/group_message.rst | 13 ------------- doc/source/sdk/resources/dms/v1/quota.rst | 13 ------------- 4 files changed, 35 deletions(-) delete mode 100644 doc/source/sdk/resources/dms/v1/group_message.rst delete mode 100644 doc/source/sdk/resources/dms/v1/quota.rst diff --git a/doc/source/sdk/proxies/dms.rst b/doc/source/sdk/proxies/dms.rst index b9ca1bac2..c793c28f5 100644 --- a/doc/source/sdk/proxies/dms.rst +++ b/doc/source/sdk/proxies/dms.rst @@ -24,10 +24,3 @@ Message Group Operations .. autoclass:: otcextensions.sdk.dms.v1._proxy.Proxy :noindex: :members: groups, create_group, delete_group - -DMS Quota Operations -^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: otcextensions.sdk.dms.v1._proxy.Proxy - :noindex: - :members: quotas diff --git a/doc/source/sdk/resources/dms/index.rst b/doc/source/sdk/resources/dms/index.rst index 210555c88..8236b0b78 100644 --- a/doc/source/sdk/resources/dms/index.rst +++ b/doc/source/sdk/resources/dms/index.rst @@ -5,7 +5,5 @@ DMS Resources :maxdepth: 1 v1/group - v1/group_message v1/message v1/queue - v1/quota diff --git a/doc/source/sdk/resources/dms/v1/group_message.rst b/doc/source/sdk/resources/dms/v1/group_message.rst deleted file mode 100644 index 94090ce49..000000000 --- a/doc/source/sdk/resources/dms/v1/group_message.rst +++ /dev/null @@ -1,13 +0,0 @@ -otcextensions.sdk.dcs.v1.group_message -====================================== - -.. automodule:: otcextensions.sdk.dms.v1.group_message - -The DMS GroupMessage Class --------------------------- - -The ``GroupMessage`` class inherits from -:class:`~otcextensions.sdk.sdk_resource.Resource`. - -.. autoclass:: otcextensions.sdk.dms.v1.group_message.GroupMessage - :members: diff --git a/doc/source/sdk/resources/dms/v1/quota.rst b/doc/source/sdk/resources/dms/v1/quota.rst deleted file mode 100644 index 5faa27fd5..000000000 --- a/doc/source/sdk/resources/dms/v1/quota.rst +++ /dev/null @@ -1,13 +0,0 @@ -otcextensions.sdk.dcs.v1.quota -============================== - -.. automodule:: otcextensions.sdk.dms.v1.quota - -The DMS Quota Class -------------------- - -The ``Quota`` class inherits from -:class:`~otcextensions.sdk.sdk_resource.Resource`. - -.. autoclass:: otcextensions.sdk.dms.v1.quota.Quota - :members: From 747dda0c99da62c7d16c0909b13daca8adf221bc Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 17 Apr 2020 17:17:27 +0200 Subject: [PATCH 07/24] fixes for dms sdk functional testing (#77) Co-authored-by: Vladimir Hasko --- .../tests/functional/sdk/dms/v1/test_message.py | 14 +++++++------- .../tests/functional/sdk/dms/v1/test_queue.py | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/otcextensions/tests/functional/sdk/dms/v1/test_message.py b/otcextensions/tests/functional/sdk/dms/v1/test_message.py index 06f9fce0e..2c802f6a3 100644 --- a/otcextensions/tests/functional/sdk/dms/v1/test_message.py +++ b/otcextensions/tests/functional/sdk/dms/v1/test_message.py @@ -35,13 +35,13 @@ def setUp(self): ) except openstack.exceptions.BadRequestException: - self.queue = self.conn.dms.get_queue(TestMessage.QUEUE_ALIAS) + self.queue = self.conn.dms.find_queue(TestMessage.QUEUE_ALIAS) self.queues.append(self.queue) try: self.group = self.conn.dms.create_group( - self.queue, {"name": "test_group"} + self.queue, "test_group" ) except openstack.exceptions.DuplicateResource: @@ -50,6 +50,7 @@ def setUp(self): self.groups.append(self.group) def tearDown(self): + super(TestMessage, self).tearDown() try: for queue in self.queues: if queue.id: @@ -75,7 +76,7 @@ def test_group(self): # self.assertIsNotNone(q) try: self.group = self.conn.dms.create_group( - self.queue, {"name": "test_group"} + self.queue, "test_group2" ) except openstack.exceptions.BadRequestException: @@ -84,7 +85,6 @@ def test_group(self): self.groups.append(self.group) # OS_TEST_TIMEOUT=60 is needed due to testbed slowness - @classmethod def test_message(self): self.queues = list(self.conn.dms.queues()) # self.assertGreaterEqual(len(self.queues), 0) @@ -112,7 +112,7 @@ def test_message(self): self.messages.append(self.message) try: self.group = self.conn.dms.create_group( - self.queue, {"name": "test_group2"} + self.queue, "test_group3" ) except openstack.exceptions.BadRequestException: @@ -120,8 +120,8 @@ def test_message(self): self.groups.append(self.group) - self.received_messages = self.dms.consume_message( + self.received_messages = self.conn.dms.consume_message( self.queue, self.group ) - self.assertGreaterEqual(len(self.received_messages), 0) + self.assertGreaterEqual(len(list(self.received_messages)), 0) diff --git a/otcextensions/tests/functional/sdk/dms/v1/test_queue.py b/otcextensions/tests/functional/sdk/dms/v1/test_queue.py index ece366cfe..db8276d6a 100644 --- a/otcextensions/tests/functional/sdk/dms/v1/test_queue.py +++ b/otcextensions/tests/functional/sdk/dms/v1/test_queue.py @@ -33,6 +33,7 @@ def setUp(self): self.queues.append(self.queue) def tearDown(self): + super(TestQueue, self).tearDown() try: for queue in self.queues: if queue.id: From 63ea36195f038c486f8f9cf623f2e3e74d99263d Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 20 Apr 2020 09:19:40 +0200 Subject: [PATCH 08/24] add instance batch operations --- otcextensions/sdk/dms/v1/_proxy.py | 29 ++++++++++- otcextensions/sdk/dms/v1/instance.py | 42 ++++++++++++++++ .../tests/unit/sdk/dms/v1/test_instance.py | 50 +++++++++++++++++++ .../tests/unit/sdk/dms/v1/test_proxy.py | 31 ++++++++++++ 4 files changed, 151 insertions(+), 1 deletion(-) diff --git a/otcextensions/sdk/dms/v1/_proxy.py b/otcextensions/sdk/dms/v1/_proxy.py index 3dc976aaf..a5b0b94ef 100644 --- a/otcextensions/sdk/dms/v1/_proxy.py +++ b/otcextensions/sdk/dms/v1/_proxy.py @@ -248,7 +248,7 @@ def delete_instance(self, instance, ignore_missing=True): :class:`~otcextensions.sdk.dms.v1.instance.Instance` :param bool ignore_missing: When set to ``False`` :class:`~otcextensions.sdk.exceptions.ResourceNotFound` will be - raised when the queue does not exist. + raised when the instance does not exist. :returns: `None` """ self._delete(_instance.Instance, instance, @@ -292,6 +292,33 @@ def update_instance(self, instance, **attrs): return self._update(_instance.Instance, instance, **attrs) + def restart_instance(self, instance): + """Restart instance + + :param instance: Either the ID of an instance or a + :class:`~otcextensions.sdk.dms.v1.instance.Instance` instance. + """ + instance = self._get_resource(_instance.Instance, instance) + return instance.restart(self) + + def restart_instances(self, instances_list): + """Restart multiple instances + """ + dummy_instance = _instance.Instance() + return dummy_instance.restart_batch(self, instances_list) + + def delete_batch(self, instances_list): + """Delete multiple instances + """ + dummy_instance = _instance.Instance() + return dummy_instance.delete_batch(self, instances_list) + + def delete_failed(self): + """Delete failed Kafka instances + """ + dummy_instance = _instance.Instance() + return dummy_instance.delete_failed(self) + # def quotas(self): # """List quota # diff --git a/otcextensions/sdk/dms/v1/instance.py b/otcextensions/sdk/dms/v1/instance.py index 64dabc6fe..f4524975a 100644 --- a/otcextensions/sdk/dms/v1/instance.py +++ b/otcextensions/sdk/dms/v1/instance.py @@ -9,6 +9,7 @@ # 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 openstack import exceptions from openstack import resource from otcextensions.sdk.dms.v1 import _base @@ -116,3 +117,44 @@ class Instance(_base.Resource): user_id = resource.Body('user_id') #: User name user_name = resource.Body('user_name') + + def _action(self, session, action, id_list): + body = { + 'action': action, + 'instances': id_list + } + + response = session.post( + '/instances/action', + body + ) + + exceptions.raise_from_response(response) + + return + + def restart(self, session): + """Restart specified instances + """ + return self._action(session, 'restart', [self.id]) + + def restart_batch(self, session, id_list): + return self._action(session, 'restart', id_list) + + def delete_batch(self, session, id_list): + """Delete batch of instances + """ + return self._action(session, 'delete', id_list) + + def delete_failed(self, session): + body = { + 'action': 'delete', + 'allFailure': 'kafka' + } + + response = session.post( + '/instances/action', + body + ) + exceptions.raise_from_response(response) + return diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_instance.py b/otcextensions/tests/unit/sdk/dms/v1/test_instance.py index 8dfe9523a..89a444102 100644 --- a/otcextensions/tests/unit/sdk/dms/v1/test_instance.py +++ b/otcextensions/tests/unit/sdk/dms/v1/test_instance.py @@ -9,6 +9,9 @@ # 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 keystoneauth1 import adapter + +from unittest import mock from openstack.tests.unit import base @@ -44,6 +47,11 @@ class TestInstance(base.TestCase): + def setUp(self): + super(TestInstance, self).setUp() + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.post = mock.Mock() + def test_basic(self): sot = instance.Instance() @@ -145,3 +153,45 @@ def test_make_it(self): sot.user_id) self.assertEqual(JSON_DATA.get('user_name', None), sot.user_name) + + def test_restart(self): + sot = instance.Instance(id='1') + response = mock.Mock() + response.status_code = 200 + + self.sess.post.return_value = response + + sot.restart(self.sess) + + self.sess.post.assert_called_with( + '/instances/action', + {'action': 'restart', 'instances': ['1']} + ) + + sot.restart_batch(self.sess, ['1', '2']) + + self.sess.post.assert_called_with( + '/instances/action', + {'action': 'restart', 'instances': ['1', '2']} + ) + + def test_delete(self): + sot = instance.Instance() + response = mock.Mock() + response.status_code = 200 + + self.sess.post.return_value = response + + sot.delete_batch(self.sess, ['1', '2']) + + self.sess.post.assert_called_with( + '/instances/action', + {'action': 'delete', 'instances': ['1', '2']} + ) + + sot.delete_failed(self.sess) + + self.sess.post.assert_called_with( + '/instances/action', + {'action': 'delete', 'allFailure': 'kafka'} + ) diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py b/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py index 4acf78714..29d2316fb 100644 --- a/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py +++ b/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py @@ -236,3 +236,34 @@ def test_update_instance(self): self.proxy.update_instance, _instance.Instance ) + + def test_restart_instance(self): + self._verify( + 'otcextensions.sdk.dms.v1.instance.Instance._action', + self.proxy.restart_instance, + method_args=['value'], + expected_args=['restart', ['value']] + ) + + def test_restart_instances(self): + self._verify( + 'otcextensions.sdk.dms.v1.instance.Instance._action', + self.proxy.restart_instances, + method_args=[['1', '2']], + expected_args=['restart', ['1', '2']] + ) + + def test_delete_failed(self): + self._verify( + 'otcextensions.sdk.dms.v1.instance.Instance.delete_failed', + self.proxy.delete_failed, + method_args=[] + ) + + def test_delete_batch(self): + self._verify( + 'otcextensions.sdk.dms.v1.instance.Instance._action', + self.proxy.delete_batch, + method_args=[['1', '2']], + expected_args=['delete', ['1', '2']] + ) From bf6e023b49742fb9678ca12f39d1fee12ff7c492 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 20 Apr 2020 10:13:04 +0200 Subject: [PATCH 09/24] add instance topic support --- otcextensions/sdk/dms/v1/_proxy.py | 73 ++++++++++++++++--- otcextensions/sdk/dms/v1/topic.py | 40 ++++++++++ .../tests/unit/sdk/dms/v1/test_proxy.py | 27 +++++++ .../tests/unit/sdk/dms/v1/test_topic.py | 57 +++++++++++++++ 4 files changed, 188 insertions(+), 9 deletions(-) create mode 100644 otcextensions/sdk/dms/v1/topic.py create mode 100644 otcextensions/tests/unit/sdk/dms/v1/test_topic.py diff --git a/otcextensions/sdk/dms/v1/_proxy.py b/otcextensions/sdk/dms/v1/_proxy.py index a5b0b94ef..ca19320cb 100644 --- a/otcextensions/sdk/dms/v1/_proxy.py +++ b/otcextensions/sdk/dms/v1/_proxy.py @@ -10,12 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import proxy from otcextensions.sdk.dms.v1 import group as _group from otcextensions.sdk.dms.v1 import instance as _instance from otcextensions.sdk.dms.v1 import message as _message from otcextensions.sdk.dms.v1 import queue as _queue +from otcextensions.sdk.dms.v1 import topic as _topic class Proxy(proxy.Proxy): @@ -209,8 +211,10 @@ def consume_message(self, queue, group, **query): def ack_message(self, queue, group, messages, status='success'): """Confirm consumed message - :param consumed_message: An object of an instance of - :class:`~otcextensions.sdk.dms.v1.group_message.GroupMessage + :param queue: An queue object + :param group: A Queue group object + :param messages: List of messages to be ACKed of + :class:`~otcextensions.sdk.dms.v1.message.Messages :param status: The expeced status of the consumed message :returns: An object of an instance of :class:`~otcextensions.sdk.dms.v1.group_message.GroupMessage` @@ -319,10 +323,61 @@ def delete_failed(self): dummy_instance = _instance.Instance() return dummy_instance.delete_failed(self) -# def quotas(self): -# """List quota -# -# :returns: A generator of Quota object -# :rtype: :class:`~otcextensions.sdk.dms.v1.quota.Quota` -# """ -# return self._list(_quota.Quota) + # ======== Topics ======= + def topics(self, instance, **kwargs): + """List all DMS Instance topics + + :param instance: Either the ID of an instance or a + :class:`~otcextensions.sdk.dms.v1.instance.Instance` instance. + :param dict kwargs: List of query parameters + + :returns: A generator of Instance object of + :class:`~otcextensions.sdk.dms.v1.topic.Topic` + """ + instance_obj = self._get_resource(_instance.Instance, instance) + return self._list(_instance.Instance, paginated=False, + instance_id=instance_obj.id, **kwargs) + + def create_topic(self, instance, **attrs): + """Create a topic on DMS Instance + + :param instance: instance id or + :class:`~otcextensions.sdk.dms.v1.instance.Instance` + :param dict attrs: Attributes of the topic + :class:`otcextensions.sdk.dms.v1.topic.Topic` + :returns: An topic class object + :class:`~otcextensions.sdk.dms.v1.topic.Topic` + """ + instance_obj = self._get_resource(_instance.Instance, instance) + return self._create(_topic.Topic, + instance_id=instance_obj.id, + **attrs) + + def delete_topic(self, instance, topics, ignore_missing=True): + """Delete topic on DMS instance + + :param instance: The instance id or an object instance of + :class:`~otcextensions.sdk.dms.v1.instance.Instance` + :param list topics: List of topic IDs + :param bool ignore_missing: When set to ``False`` + :class:`~otcextensions.sdk.exceptions.ResourceNotFound` will be + raised when the instance does not exist. + :returns: `None` + """ + instance_obj = self._get_resource(_instance.Instance, instance) + + topics_list = [] + if isinstance(topics, str): + topics_list.append(topics) + elif isinstance(topics, list): + for i in topics: + if isinstance(i, str): + topics_list.append(i) + elif isinstance(i, _topic.Topic): + topics_list.append(i.id) + + response = self.post( + '/instances/%s/topics/delete' % (instance_obj.id), + {'topics': topics_list} + ) + exceptions.raise_from_response(response) diff --git a/otcextensions/sdk/dms/v1/topic.py b/otcextensions/sdk/dms/v1/topic.py new file mode 100644 index 000000000..55265e28a --- /dev/null +++ b/otcextensions/sdk/dms/v1/topic.py @@ -0,0 +1,40 @@ +# 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 openstack import resource + +from otcextensions.sdk.dms.v1 import _base + + +class Topic(_base.Resource): + """DMS Topic resource""" + resources_key = 'topics' + base_path = '/instances/%(instance_id)s/topics' + + # capabilities + allow_list = True + allow_create = True + allow_delete = True + + instance_id = resource.URI('instance_id') + + #: Properties + #: Synchronous flushing. Default=false + is_sync_flush = resource.Body('sync_message_flush', type=bool) + #: Synchronous replication. Default=false. With replication=1 can be only + #: false + is_sync_replication = resource.Body('sync_replication', type=bool) + #: Number of partitions. Default=3 + partition = resource.Body('partition', type=int) + #: Replication factor. Default=3 + replication = resource.Body('replication', type=int) + #: Retention time in hours. Default=72 + retention_time = resource.Body('retention_time', type=int) diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py b/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py index 29d2316fb..e2ee4acbf 100644 --- a/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py +++ b/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py @@ -9,12 +9,14 @@ # 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 unittest import mock from otcextensions.sdk.dms.v1 import _proxy from otcextensions.sdk.dms.v1 import group as _group from otcextensions.sdk.dms.v1 import instance as _instance from otcextensions.sdk.dms.v1 import message as _message from otcextensions.sdk.dms.v1 import queue as _queue +from otcextensions.sdk.dms.v1 import topic as _topic from openstack.tests.unit import test_proxy_base @@ -267,3 +269,28 @@ def test_delete_batch(self): method_args=[['1', '2']], expected_args=['delete', ['1', '2']] ) + + def test_create_topic(self): + self.verify_create( + self.proxy.create_topic, + _topic.Topic, + method_args=['iid'], + expected_kwargs={'instance_id': 'iid', 'x': 1, 'y': 2, 'z': 3} + ) + + @mock.patch('otcextensions.sdk.dms.v1._proxy.Proxy.post') + def test_delete_topics(self, post_mock): + response = mock.Mock() + response.status_code = 200 + post_mock.return_value = response + self.proxy.delete_topic('instance', ['t1', 't2']) + + post_mock.assert_called_with( + '/instances/instance/topics/delete', + {'topics': ['t1', 't2']}) + + self.proxy.delete_topic('instance', 't1') + + post_mock.assert_called_with( + '/instances/instance/topics/delete', + {'topics': ['t1']}) diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_topic.py b/otcextensions/tests/unit/sdk/dms/v1/test_topic.py new file mode 100644 index 000000000..abd2107d5 --- /dev/null +++ b/otcextensions/tests/unit/sdk/dms/v1/test_topic.py @@ -0,0 +1,57 @@ +# 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 keystoneauth1 import adapter + +from unittest import mock + +from openstack.tests.unit import base + +from otcextensions.sdk.dms.v1 import topic + + +JSON_DATA = { + "id": "haha", + "partition": 3, + "replication": 3, + "sync_replication": True, + "retention_time": 10, + "sync_message_flush": True +} + + +class TestTopic(base.TestCase): + + def setUp(self): + super(TestTopic, self).setUp() + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.post = mock.Mock() + + def test_basic(self): + sot = topic.Topic() + + self.assertEqual('/instances/%(instance_id)s/topics', sot.base_path) + self.assertEqual('topics', sot.resources_key) + + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_delete) + + def test_make_it(self): + + sot = topic.Topic(**JSON_DATA) + self.assertEqual(JSON_DATA['id'], sot.id) + self.assertEqual(JSON_DATA['partition'], sot.partition) + self.assertEqual(JSON_DATA['replication'], sot.replication) + self.assertEqual(JSON_DATA['retention_time'], sot.retention_time) + self.assertEqual(JSON_DATA['sync_message_flush'], sot.is_sync_flush) + self.assertEqual(JSON_DATA['sync_replication'], + sot.is_sync_replication) From 0e96cfeb6d83c12d21011d73058c6b2e2caa5066 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 20 Apr 2020 11:03:10 +0200 Subject: [PATCH 10/24] add az support --- otcextensions/sdk/dms/v1/_base.py | 45 +++++++++ otcextensions/sdk/dms/v1/_proxy.py | 10 ++ otcextensions/sdk/dms/v1/az.py | 37 ++++++++ .../tests/unit/sdk/dms/v1/test_az.py | 92 +++++++++++++++++++ .../tests/unit/sdk/dms/v1/test_proxy.py | 8 ++ .../tests/unit/sdk/dms/v1/test_topic.py | 12 +-- 6 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 otcextensions/sdk/dms/v1/az.py create mode 100644 otcextensions/tests/unit/sdk/dms/v1/test_az.py diff --git a/otcextensions/sdk/dms/v1/_base.py b/otcextensions/sdk/dms/v1/_base.py index 1c8112550..d8f83592c 100644 --- a/otcextensions/sdk/dms/v1/_base.py +++ b/otcextensions/sdk/dms/v1/_base.py @@ -61,3 +61,48 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): return None raise exceptions.ResourceNotFound( "No %s found for %s" % (cls.__name__, name_or_id)) + + @classmethod + def list_override(cls, session, **params): + # Some really stupid APIs are not following any guidelines + # and are under other endpoint (without project id), + # so we need to hack on the endpoint_override yet again + session = cls._get_session(session) + + base_path = cls.base_path + params.pop('paginated', None) + params.pop('base_path', None) + params = cls._query_mapping._validate( + params, base_path=base_path, + allow_unknown_params=False) + query_params = cls._query_mapping._transpose(params, cls) + uri = base_path % params + region_id = None + + # Copy query_params due to weird mock unittest interactions + response = session.get( + uri, + endpoint_override=session.endpoint_override.replace( + '%(project_id)s', ''), + headers={"Accept": "application/json"}, + params=query_params.copy()) + exceptions.raise_from_response(response) + data = response.json() + if 'regionId' in data: + region_id = data['regionId'] + + if cls.resources_key: + resources = data[cls.resources_key] + else: + resources = data + + if not isinstance(resources, list): + resources = [resources] + + for raw_resource in resources: + if region_id: + raw_resource['region_id'] = region_id + value = cls.existing( + connection=session._get_connection(), + **raw_resource) + yield value diff --git a/otcextensions/sdk/dms/v1/_proxy.py b/otcextensions/sdk/dms/v1/_proxy.py index ca19320cb..c46fd101f 100644 --- a/otcextensions/sdk/dms/v1/_proxy.py +++ b/otcextensions/sdk/dms/v1/_proxy.py @@ -13,6 +13,7 @@ from openstack import exceptions from openstack import proxy +from otcextensions.sdk.dms.v1 import az as _az from otcextensions.sdk.dms.v1 import group as _group from otcextensions.sdk.dms.v1 import instance as _instance from otcextensions.sdk.dms.v1 import message as _message @@ -381,3 +382,12 @@ def delete_topic(self, instance, topics, ignore_missing=True): {'topics': topics_list} ) exceptions.raise_from_response(response) + + # ======== Misc ======= + def availability_zones(self, **kwargs): + """List all supported DMS Instance availability zones + + :returns: A generator of Instance object of AvailabilityZone + :class:`~otcextensions.sdk.dms.v1.az.AvailabilityZone` + """ + return self._list(_az.AvailabilityZone, **kwargs) diff --git a/otcextensions/sdk/dms/v1/az.py b/otcextensions/sdk/dms/v1/az.py new file mode 100644 index 000000000..587756083 --- /dev/null +++ b/otcextensions/sdk/dms/v1/az.py @@ -0,0 +1,37 @@ +# 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 openstack import resource + +from otcextensions.sdk.dms.v1 import _base + + +class AvailabilityZone(_base.Resource): + """DMS AZ resource""" + resources_key = 'available_zones' + base_path = '/availableZones' + + # capabilities + allow_list = True + + #: Properties + #: AZ code + code = resource.Body('code') + #: Has free resources + has_available_resources = resource.Body('resource_availability', type=bool) + #: Port number + port = resource.Body('port') + #: Region ID + region_id = resource.Body('region_id') + + @classmethod + def list(cls, session, **params): + return super(AvailabilityZone, cls).list_override(session, **params) diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_az.py b/otcextensions/tests/unit/sdk/dms/v1/test_az.py new file mode 100644 index 000000000..675966a25 --- /dev/null +++ b/otcextensions/tests/unit/sdk/dms/v1/test_az.py @@ -0,0 +1,92 @@ +# 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 keystoneauth1 import adapter + +from unittest import mock + +from openstack.tests.unit import base + +from otcextensions.sdk.dms.v1 import az + + +JSON_DATA = { + 'id': '1d7b939b382c4c3bb3481a8ca10da768', + 'name': 'az10.dc1', + 'code': 'az10.dc1', + 'port': '8002', + 'resource_availability': True +} + +JSON_LIST = { + 'regionId': 'fake_region', + 'available_zones': [{ + 'id': '1d7b939b382c4c3bb3481a8ca10da768', + 'name': 'az10.dc1', + 'code': 'az10.dc1', + 'port': '8002', + 'resource_availability': 'true' + }, { + 'id': '1d7b939b382c4c3bb3481a8ca10da769', + 'name': 'az10.dc2', + 'code': 'az10.dc2', + 'port': '8002', + 'resource_availability': 'true' + }] +} + + +class TestAZ(base.TestCase): + + def setUp(self): + super(TestAZ, self).setUp() + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.get = mock.Mock() + self.sess.endpoint_override = 'fake/%(project_id)s' + self.sess._get_connection = mock.Mock() + + def test_basic(self): + sot = az.AvailabilityZone() + + self.assertEqual('/availableZones', sot.base_path) + self.assertEqual('available_zones', sot.resources_key) + + self.assertTrue(sot.allow_list) + + def test_make_it(self): + + sot = az.AvailabilityZone(**JSON_DATA) + self.assertEqual(JSON_DATA['id'], sot.id) + self.assertEqual(JSON_DATA['name'], sot.name) + self.assertEqual(JSON_DATA['code'], sot.code) + self.assertEqual(JSON_DATA['port'], sot.port) + self.assertEqual(JSON_DATA['resource_availability'], + sot.has_available_resources) + + def test_list(self): + sot = az.AvailabilityZone() + + response = mock.Mock() + response.status_code = 200 + response.json = mock.Mock(return_value=JSON_LIST) + self.sess.get.return_value = response + + rsp = list(sot.list(self.sess)) + + self.sess.get.assert_called_with( + '/availableZones', + endpoint_override='fake/', + headers={'Accept': 'application/json'}, + params={} + ) + + self.assertIsInstance(rsp[0], az.AvailabilityZone) + self.assertEqual(JSON_LIST['regionId'], rsp[0].region_id) diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py b/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py index e2ee4acbf..e5bd393d3 100644 --- a/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py +++ b/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py @@ -12,6 +12,7 @@ from unittest import mock from otcextensions.sdk.dms.v1 import _proxy +from otcextensions.sdk.dms.v1 import az as _az from otcextensions.sdk.dms.v1 import group as _group from otcextensions.sdk.dms.v1 import instance as _instance from otcextensions.sdk.dms.v1 import message as _message @@ -294,3 +295,10 @@ def test_delete_topics(self, post_mock): post_mock.assert_called_with( '/instances/instance/topics/delete', {'topics': ['t1']}) + + # Misc + def test_az(self): + self.verify_list( + self.proxy.availability_zones, + _az.AvailabilityZone, + ) diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_topic.py b/otcextensions/tests/unit/sdk/dms/v1/test_topic.py index abd2107d5..b641704c3 100644 --- a/otcextensions/tests/unit/sdk/dms/v1/test_topic.py +++ b/otcextensions/tests/unit/sdk/dms/v1/test_topic.py @@ -19,12 +19,12 @@ JSON_DATA = { - "id": "haha", - "partition": 3, - "replication": 3, - "sync_replication": True, - "retention_time": 10, - "sync_message_flush": True + 'id': 'haha', + 'partition': 3, + 'replication': 3, + 'sync_replication': True, + 'retention_time': 10, + 'sync_message_flush': True } From 79ea78ec1815a3e1c563c5c93140917409e91fab Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 20 Apr 2020 12:32:05 +0200 Subject: [PATCH 11/24] add product and maintenance_windows --- otcextensions/sdk/dms/v1/_proxy.py | 18 ++++ .../sdk/dms/v1/maintenance_window.py | 38 ++++++++ otcextensions/sdk/dms/v1/product.py | 90 +++++++++++++++++++ .../tests/unit/sdk/dms/v1/test_mw.py | 41 +++++++++ .../unit/sdk/dms/v1/test_product_spec.py | 54 +++++++++++ .../tests/unit/sdk/dms/v1/test_proxy.py | 14 +++ 6 files changed, 255 insertions(+) create mode 100644 otcextensions/sdk/dms/v1/maintenance_window.py create mode 100644 otcextensions/sdk/dms/v1/product.py create mode 100644 otcextensions/tests/unit/sdk/dms/v1/test_mw.py create mode 100644 otcextensions/tests/unit/sdk/dms/v1/test_product_spec.py diff --git a/otcextensions/sdk/dms/v1/_proxy.py b/otcextensions/sdk/dms/v1/_proxy.py index c46fd101f..6da18d918 100644 --- a/otcextensions/sdk/dms/v1/_proxy.py +++ b/otcextensions/sdk/dms/v1/_proxy.py @@ -16,7 +16,9 @@ from otcextensions.sdk.dms.v1 import az as _az from otcextensions.sdk.dms.v1 import group as _group from otcextensions.sdk.dms.v1 import instance as _instance +from otcextensions.sdk.dms.v1 import maintenance_window as _mw from otcextensions.sdk.dms.v1 import message as _message +from otcextensions.sdk.dms.v1 import product as _product from otcextensions.sdk.dms.v1 import queue as _queue from otcextensions.sdk.dms.v1 import topic as _topic @@ -391,3 +393,19 @@ def availability_zones(self, **kwargs): :class:`~otcextensions.sdk.dms.v1.az.AvailabilityZone` """ return self._list(_az.AvailabilityZone, **kwargs) + + def products(self, **kwargs): + """List all supported DMS products + + :returns: A generator of Product object + :class:`~otcextensions.sdk.dms.v1.product.Product` + """ + return self._list(_product.Product, **kwargs) + + def maintenance_windows(self, **kwargs): + """List all DMS maintenance windows + + :returns: A generator of MaintenanceWindow object + :class:`~otcextensions.sdk.dms.v1.maintenance_window.MaintenanceWindow` + """ + return self._list(_mw.MaintenanceWindow, **kwargs) diff --git a/otcextensions/sdk/dms/v1/maintenance_window.py b/otcextensions/sdk/dms/v1/maintenance_window.py new file mode 100644 index 000000000..e99721e47 --- /dev/null +++ b/otcextensions/sdk/dms/v1/maintenance_window.py @@ -0,0 +1,38 @@ +# 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 openstack import resource + +from otcextensions.sdk.dms.v1 import _base + + +class MaintenanceWindow(_base.Resource): + """DMS Maintenance window resource""" + resources_key = 'maintain_windows' + base_path = '/instances/maintain-windows' + + # capabilities + allow_list = True + + #: Properties + #: Indicates the sequential number of a maintenance time window. + seq = resource.Body('seq', type=int) + #: Indicates the time at which a maintenance time window starts. + begin = resource.Body('begin') + #: Indicates the time at which a maintenance time window ends. + end = resource.Body('end') + #: Indicates whether a maintenance time window is set to the + #: default time segment. + is_default = resource.Body('default', type=bool) + + @classmethod + def list(cls, session, **params): + return super(MaintenanceWindow, cls).list_override(session, **params) diff --git a/otcextensions/sdk/dms/v1/product.py b/otcextensions/sdk/dms/v1/product.py new file mode 100644 index 000000000..8ce8fa1d0 --- /dev/null +++ b/otcextensions/sdk/dms/v1/product.py @@ -0,0 +1,90 @@ +# 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 openstack import exceptions +from openstack import resource + +from otcextensions.sdk.dms.v1 import _base + + +class Product(_base.Resource): + """DMS Product resource""" + base_path = '/products' + resources_key = 'Hourly' + + # capabilities + allow_list = True + + #: Properties + engine_name = resource.Body('engine_name') + engine_version = resource.Body('engine_version') + #: Indicates the maximum number of messages per unit time. + tps = resource.Body('tps') + #: Indicates the message storage space. + storage = resource.Body('storage') + #: Indicates the maximum number of topics in a Kafka instance. + partition_num = resource.Body('partition_num') + #: Indicates the product ID. + product_id = resource.Body('product_id') + #: Indicates the specification ID. + spec_code = resource.Body('spec_code') + #: Indicates the I/O information. + io = resource.Body('io', type=list, list_type=dict) + #: Indicates the bandwidth of a Kafka instance. + bandwidth = resource.Body('bandwidth') + #: Indicates AZs where there are available resources. + availability_zones = resource.Body('available_zones', type=list) + #: Indicated AZs where it is not available + unavailable_zones = resource.Body('unavailable_zones', type=list) + #: Indicates the VM specifications of the instance resources. + vm_flavor_id = resource.Body('ecs_flavor_id') + #: Indicates the instance architecture type. + #: Currently, only x86 is supported. + arch_type = resource.Body('arch_type') + + @classmethod + def list(cls, session, **params): + # This API is a total disaster (b*****it) + # Show me who was so smart to develop it... + session = cls._get_session(session) + + base_path = cls.base_path + params.pop('paginated', None) + params.pop('base_path', None) + params = cls._query_mapping._validate( + params, base_path=base_path, + allow_unknown_params=False) + query_params = cls._query_mapping._transpose(params, cls) + uri = base_path % params + + # Copy query_params due to weird mock unittest interactions + response = session.get( + uri, + endpoint_override=session.endpoint_override.replace( + '%(project_id)s', ''), + headers={"Accept": "application/json"}, + params=query_params.copy()) + exceptions.raise_from_response(response) + data = response.json() + + for k, v in data.items(): + for rec in v: + engine = rec + engine_name = engine.get('name') + engine_version = engine.get('version') + for madness in engine.get('values', []): + for spec in madness.get('detail', []): + value = cls.existing( + connection=session._get_connection(), + engine_name=engine_name, + engine_version=engine_version, + **spec) + yield value diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_mw.py b/otcextensions/tests/unit/sdk/dms/v1/test_mw.py new file mode 100644 index 000000000..de6d22389 --- /dev/null +++ b/otcextensions/tests/unit/sdk/dms/v1/test_mw.py @@ -0,0 +1,41 @@ +# 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 openstack.tests.unit import base + +from otcextensions.sdk.dms.v1 import maintenance_window as mw + + +JSON_DATA = { + "default": False, + "seq": 1, + "begin": "22", + "end": "02" +} + + +class TestMW(base.TestCase): + + def test_basic(self): + sot = mw.MaintenanceWindow() + + self.assertEqual('/instances/maintain-windows', sot.base_path) + self.assertEqual('maintain_windows', sot.resources_key) + + self.assertTrue(sot.allow_list) + + def test_make_it(self): + + sot = mw.MaintenanceWindow(**JSON_DATA) + self.assertEqual(JSON_DATA['default'], sot.is_default) + self.assertEqual(int(JSON_DATA['seq']), sot.seq) + self.assertEqual(JSON_DATA['begin'], sot.begin) + self.assertEqual(JSON_DATA['end'], sot.end) diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_product_spec.py b/otcextensions/tests/unit/sdk/dms/v1/test_product_spec.py new file mode 100644 index 000000000..07742cace --- /dev/null +++ b/otcextensions/tests/unit/sdk/dms/v1/test_product_spec.py @@ -0,0 +1,54 @@ +# 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 openstack.tests.unit import base + +from otcextensions.sdk.dms.v1 import product + + +JSON_DATA = { + 'tps': '50000', + 'storage': '600', + 'partition_num': '300', + 'product_id': '00300-30308-0--0', + 'spec_code': 'dms.instance.kafka.cluster.c3.mini', + 'io': [{ + 'io_type': 'high', + 'storage_spec_code': 'dms.physical.storage.high', + 'available_zones': ['eu-de-02', 'eu-de-01'], + 'volume_type': 'SAS' + }, { + 'io_type': 'ultra', + 'storage_spec_code': 'dms.physical.storage.ultra', + 'available_zones': ['eu-de-02', 'eu-de-01'], + 'volume_type': 'SSD' + }], + 'bandwidth': '100MB', + 'unavailable_zones': ['eu-de-02'], + 'available_zones': ['eu-de-01'], + 'ecs_flavor_id': 'c4.large.2', + 'arch_type': 'X86' +} + + +class TestProduct(base.TestCase): + + def test_basic(self): + sot = product.Product() + + self.assertEqual('/products', sot.base_path) + + self.assertTrue(sot.allow_list) + + def test_make_it(self): + + sot = product.Product(**JSON_DATA) + self.assertEqual(JSON_DATA['tps'], sot.tps) diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py b/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py index e5bd393d3..724c3d9ff 100644 --- a/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py +++ b/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py @@ -15,7 +15,9 @@ from otcextensions.sdk.dms.v1 import az as _az from otcextensions.sdk.dms.v1 import group as _group from otcextensions.sdk.dms.v1 import instance as _instance +from otcextensions.sdk.dms.v1 import maintenance_window as _mw from otcextensions.sdk.dms.v1 import message as _message +from otcextensions.sdk.dms.v1 import product as _product from otcextensions.sdk.dms.v1 import queue as _queue from otcextensions.sdk.dms.v1 import topic as _topic @@ -302,3 +304,15 @@ def test_az(self): self.proxy.availability_zones, _az.AvailabilityZone, ) + + def test_products(self): + self.verify_list( + self.proxy.products, + _product.Product + ) + + def test_mws(self): + self.verify_list( + self.proxy.maintenance_windows, + _mw.MaintenanceWindow + ) From 2a0a95298a5e234b2f3963dce561370c87f39d9f Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 20 Apr 2020 16:20:24 +0200 Subject: [PATCH 12/24] drop wrongly distributed header --- .../tests/unit/osclient/anti_ddos/v1/fakes.py | 2 - .../unit/osclient/anti_ddos/v1/test_config.py | 2 - .../osclient/anti_ddos/v1/test_floating_ip.py | 2 - .../unit/osclient/anti_ddos/v1/test_status.py | 2 - .../unit/osclient/auto_scaling/v1/fakes.py | 2 - .../osclient/auto_scaling/v1/test_activity.py | 2 - .../osclient/auto_scaling/v1/test_config.py | 2 - .../osclient/auto_scaling/v1/test_group.py | 2 - .../osclient/auto_scaling/v1/test_instance.py | 2 - .../osclient/auto_scaling/v1/test_policy.py | 2 - .../osclient/auto_scaling/v1/test_quota.py | 2 - .../tests/unit/osclient/cce/v1/fakes.py | 2 - .../unit/osclient/cce/v1/test_cluster.py | 2 - .../unit/osclient/cce/v1/test_cluster_node.py | 2 - .../tests/unit/osclient/cce/v2/fakes.py | 2 - .../unit/osclient/cce/v2/test_cluster.py | 2 - .../unit/osclient/cce/v2/test_cluster_node.py | 2 - .../tests/unit/osclient/cts/v1/fakes.py | 2 - .../tests/unit/osclient/cts/v1/test_trace.py | 2 - .../unit/osclient/cts/v1/test_tracker.py | 2 - .../tests/unit/osclient/dcs/v1/fakes.py | 2 - .../tests/unit/osclient/dcs/v1/test_backup.py | 2 - .../tests/unit/osclient/dcs/v1/test_config.py | 2 - .../unit/osclient/dcs/v1/test_instance.py | 2 - .../unit/osclient/dcs/v1/test_restore.py | 2 - .../unit/osclient/dcs/v1/test_statistic.py | 2 - .../tests/unit/osclient/deh/v1/fakes.py | 2 - .../tests/unit/osclient/deh/v1/test_host.py | 2 - .../tests/unit/osclient/dms/v1/fakes.py | 2 - .../tests/unit/osclient/dms/v1/test_group.py | 2 - .../unit/osclient/dms/v1/test_instance.py | 238 ++++++++++++++++++ .../tests/unit/osclient/dms/v1/test_queue.py | 20 +- .../tests/unit/osclient/dms/v1/test_quota.py | 2 - .../tests/unit/osclient/dns/v2/test_ptr.py | 2 - .../unit/osclient/dns/v2/test_recordset.py | 2 - .../tests/unit/osclient/dns/v2/test_zone.py | 2 - .../tests/unit/osclient/kms/v1/fakes.py | 2 - .../tests/unit/osclient/kms/v1/test_cmk.py | 2 - .../unit/osclient/load_balancer/v1/fakes.py | 2 - .../load_balancer/v1/test_health_monitor.py | 2 - .../load_balancer/v1/test_listener.py | 2 - .../load_balancer/v1/test_load_balancer.py | 2 - .../osclient/load_balancer/v1/test_pool.py | 2 - .../load_balancer/v1/test_pool_member.py | 2 - .../tests/unit/osclient/obs/v1/fakes.py | 2 - .../tests/unit/osclient/rds/v1/fakes.py | 2 - .../tests/unit/osclient/rds/v1/test_backup.py | 2 - .../osclient/rds/v1/test_configuration.py | 2 - .../unit/osclient/rds/v1/test_datastore.py | 2 - .../tests/unit/osclient/rds/v1/test_flavor.py | 2 - .../unit/osclient/rds/v1/test_instance.py | 2 - .../tests/unit/osclient/test_base.py | 3 - 52 files changed, 247 insertions(+), 112 deletions(-) create mode 100644 otcextensions/tests/unit/osclient/dms/v1/test_instance.py diff --git a/otcextensions/tests/unit/osclient/anti_ddos/v1/fakes.py b/otcextensions/tests/unit/osclient/anti_ddos/v1/fakes.py index 58a0320db..08ca91950 100644 --- a/otcextensions/tests/unit/osclient/anti_ddos/v1/fakes.py +++ b/otcextensions/tests/unit/osclient/anti_ddos/v1/fakes.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/anti_ddos/v1/test_config.py b/otcextensions/tests/unit/osclient/anti_ddos/v1/test_config.py index 6b4893602..6ea446bf0 100644 --- a/otcextensions/tests/unit/osclient/anti_ddos/v1/test_config.py +++ b/otcextensions/tests/unit/osclient/anti_ddos/v1/test_config.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/anti_ddos/v1/test_floating_ip.py b/otcextensions/tests/unit/osclient/anti_ddos/v1/test_floating_ip.py index 2923fa1f8..2dc652ee8 100644 --- a/otcextensions/tests/unit/osclient/anti_ddos/v1/test_floating_ip.py +++ b/otcextensions/tests/unit/osclient/anti_ddos/v1/test_floating_ip.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/anti_ddos/v1/test_status.py b/otcextensions/tests/unit/osclient/anti_ddos/v1/test_status.py index 764549b93..85a8fe8af 100644 --- a/otcextensions/tests/unit/osclient/anti_ddos/v1/test_status.py +++ b/otcextensions/tests/unit/osclient/anti_ddos/v1/test_status.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/auto_scaling/v1/fakes.py b/otcextensions/tests/unit/osclient/auto_scaling/v1/fakes.py index 8a8dc006e..1350684b0 100644 --- a/otcextensions/tests/unit/osclient/auto_scaling/v1/fakes.py +++ b/otcextensions/tests/unit/osclient/auto_scaling/v1/fakes.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/auto_scaling/v1/test_activity.py b/otcextensions/tests/unit/osclient/auto_scaling/v1/test_activity.py index 63132fc85..98cc0aad6 100644 --- a/otcextensions/tests/unit/osclient/auto_scaling/v1/test_activity.py +++ b/otcextensions/tests/unit/osclient/auto_scaling/v1/test_activity.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/auto_scaling/v1/test_config.py b/otcextensions/tests/unit/osclient/auto_scaling/v1/test_config.py index 295e900ff..7c8b62862 100644 --- a/otcextensions/tests/unit/osclient/auto_scaling/v1/test_config.py +++ b/otcextensions/tests/unit/osclient/auto_scaling/v1/test_config.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/auto_scaling/v1/test_group.py b/otcextensions/tests/unit/osclient/auto_scaling/v1/test_group.py index 95e3472fb..276064caa 100644 --- a/otcextensions/tests/unit/osclient/auto_scaling/v1/test_group.py +++ b/otcextensions/tests/unit/osclient/auto_scaling/v1/test_group.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/auto_scaling/v1/test_instance.py b/otcextensions/tests/unit/osclient/auto_scaling/v1/test_instance.py index 9b94ea5d0..d2c0e549c 100644 --- a/otcextensions/tests/unit/osclient/auto_scaling/v1/test_instance.py +++ b/otcextensions/tests/unit/osclient/auto_scaling/v1/test_instance.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/auto_scaling/v1/test_policy.py b/otcextensions/tests/unit/osclient/auto_scaling/v1/test_policy.py index feab2f710..838f2932a 100644 --- a/otcextensions/tests/unit/osclient/auto_scaling/v1/test_policy.py +++ b/otcextensions/tests/unit/osclient/auto_scaling/v1/test_policy.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/auto_scaling/v1/test_quota.py b/otcextensions/tests/unit/osclient/auto_scaling/v1/test_quota.py index 3afe0f49e..968af7c5c 100644 --- a/otcextensions/tests/unit/osclient/auto_scaling/v1/test_quota.py +++ b/otcextensions/tests/unit/osclient/auto_scaling/v1/test_quota.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/cce/v1/fakes.py b/otcextensions/tests/unit/osclient/cce/v1/fakes.py index 2010494c3..d64cea861 100644 --- a/otcextensions/tests/unit/osclient/cce/v1/fakes.py +++ b/otcextensions/tests/unit/osclient/cce/v1/fakes.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/cce/v1/test_cluster.py b/otcextensions/tests/unit/osclient/cce/v1/test_cluster.py index 435eae2cb..1b30b276c 100644 --- a/otcextensions/tests/unit/osclient/cce/v1/test_cluster.py +++ b/otcextensions/tests/unit/osclient/cce/v1/test_cluster.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/cce/v1/test_cluster_node.py b/otcextensions/tests/unit/osclient/cce/v1/test_cluster_node.py index c3fb1a880..6e023e61b 100644 --- a/otcextensions/tests/unit/osclient/cce/v1/test_cluster_node.py +++ b/otcextensions/tests/unit/osclient/cce/v1/test_cluster_node.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/cce/v2/fakes.py b/otcextensions/tests/unit/osclient/cce/v2/fakes.py index 06e9668eb..151e71d96 100644 --- a/otcextensions/tests/unit/osclient/cce/v2/fakes.py +++ b/otcextensions/tests/unit/osclient/cce/v2/fakes.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/cce/v2/test_cluster.py b/otcextensions/tests/unit/osclient/cce/v2/test_cluster.py index 520f2fc86..c80d96b19 100644 --- a/otcextensions/tests/unit/osclient/cce/v2/test_cluster.py +++ b/otcextensions/tests/unit/osclient/cce/v2/test_cluster.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/cce/v2/test_cluster_node.py b/otcextensions/tests/unit/osclient/cce/v2/test_cluster_node.py index 16fbf8cc1..081808607 100644 --- a/otcextensions/tests/unit/osclient/cce/v2/test_cluster_node.py +++ b/otcextensions/tests/unit/osclient/cce/v2/test_cluster_node.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/cts/v1/fakes.py b/otcextensions/tests/unit/osclient/cts/v1/fakes.py index c524ff9ba..221c736ac 100644 --- a/otcextensions/tests/unit/osclient/cts/v1/fakes.py +++ b/otcextensions/tests/unit/osclient/cts/v1/fakes.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/cts/v1/test_trace.py b/otcextensions/tests/unit/osclient/cts/v1/test_trace.py index 6dc390eb1..55d5d4552 100644 --- a/otcextensions/tests/unit/osclient/cts/v1/test_trace.py +++ b/otcextensions/tests/unit/osclient/cts/v1/test_trace.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/cts/v1/test_tracker.py b/otcextensions/tests/unit/osclient/cts/v1/test_tracker.py index c77aee5ba..40c70049b 100644 --- a/otcextensions/tests/unit/osclient/cts/v1/test_tracker.py +++ b/otcextensions/tests/unit/osclient/cts/v1/test_tracker.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/dcs/v1/fakes.py b/otcextensions/tests/unit/osclient/dcs/v1/fakes.py index ef695edf4..92df29892 100644 --- a/otcextensions/tests/unit/osclient/dcs/v1/fakes.py +++ b/otcextensions/tests/unit/osclient/dcs/v1/fakes.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/dcs/v1/test_backup.py b/otcextensions/tests/unit/osclient/dcs/v1/test_backup.py index 909263386..e92b21b16 100644 --- a/otcextensions/tests/unit/osclient/dcs/v1/test_backup.py +++ b/otcextensions/tests/unit/osclient/dcs/v1/test_backup.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/dcs/v1/test_config.py b/otcextensions/tests/unit/osclient/dcs/v1/test_config.py index ef5fe17d2..ebed795e3 100644 --- a/otcextensions/tests/unit/osclient/dcs/v1/test_config.py +++ b/otcextensions/tests/unit/osclient/dcs/v1/test_config.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/dcs/v1/test_instance.py b/otcextensions/tests/unit/osclient/dcs/v1/test_instance.py index 8285d9cea..54c52394a 100644 --- a/otcextensions/tests/unit/osclient/dcs/v1/test_instance.py +++ b/otcextensions/tests/unit/osclient/dcs/v1/test_instance.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/dcs/v1/test_restore.py b/otcextensions/tests/unit/osclient/dcs/v1/test_restore.py index 33a611249..f190b254f 100644 --- a/otcextensions/tests/unit/osclient/dcs/v1/test_restore.py +++ b/otcextensions/tests/unit/osclient/dcs/v1/test_restore.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/dcs/v1/test_statistic.py b/otcextensions/tests/unit/osclient/dcs/v1/test_statistic.py index aa7be28ab..dad2f0cea 100644 --- a/otcextensions/tests/unit/osclient/dcs/v1/test_statistic.py +++ b/otcextensions/tests/unit/osclient/dcs/v1/test_statistic.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/deh/v1/fakes.py b/otcextensions/tests/unit/osclient/deh/v1/fakes.py index bd872d0eb..7934db30e 100644 --- a/otcextensions/tests/unit/osclient/deh/v1/fakes.py +++ b/otcextensions/tests/unit/osclient/deh/v1/fakes.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/deh/v1/test_host.py b/otcextensions/tests/unit/osclient/deh/v1/test_host.py index a7c6ae52e..868948f44 100644 --- a/otcextensions/tests/unit/osclient/deh/v1/test_host.py +++ b/otcextensions/tests/unit/osclient/deh/v1/test_host.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/dms/v1/fakes.py b/otcextensions/tests/unit/osclient/dms/v1/fakes.py index 98cdc66b4..c69f5aaaa 100644 --- a/otcextensions/tests/unit/osclient/dms/v1/fakes.py +++ b/otcextensions/tests/unit/osclient/dms/v1/fakes.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/dms/v1/test_group.py b/otcextensions/tests/unit/osclient/dms/v1/test_group.py index 2539188b8..19daa5d85 100644 --- a/otcextensions/tests/unit/osclient/dms/v1/test_group.py +++ b/otcextensions/tests/unit/osclient/dms/v1/test_group.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/dms/v1/test_instance.py b/otcextensions/tests/unit/osclient/dms/v1/test_instance.py new file mode 100644 index 000000000..e8ca7b5a4 --- /dev/null +++ b/otcextensions/tests/unit/osclient/dms/v1/test_instance.py @@ -0,0 +1,238 @@ +# 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 mock + +from otcextensions.osclient.dms.v1 import queue +from otcextensions.tests.unit.osclient.dms.v1 import fakes + + +class TestDMSQueue(fakes.TestDMS): + + def setUp(self): + super(TestDMSQueue, self).setUp() + self.client = self.app.client_manager.dms + + +class TestListDMSQueue(TestDMSQueue): + + queues = fakes.FakeQueue.create_multiple(3) + + columns = ('ID', 'name', 'queue_mode', 'description', 'redrive_policy', + 'max_consume_count', 'retention_hours') + + data = [] + + for s in queues: + data.append(( + s.id, + s.name, + s.queue_mode, + s.description, + s.redrive_policy, + s.max_consume_count, + s.retention_hours + )) + + def setUp(self): + super(TestListDMSQueue, self).setUp() + + self.cmd = queue.ListDMSQueue(self.app, None) + + self.client.queues = mock.Mock() + + def test_list_queue(self): + arglist = [ + ] + + verifylist = [ + # ('group', None), + ] + + # Verify cm is triggereg with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.queues.side_effect = [ + self.queues + ] + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + + self.client.queues.assert_called_once_with() + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestShowDMSQueue(TestDMSQueue): + + _data = fakes.FakeQueue.create_one() + + columns = ('ID', 'name', 'queue_mode', 'description', 'redrive_policy', + 'max_consume_count', 'retention_hours') + + data = ( + _data.id, + _data.name, + _data.queue_mode, + _data.description, + _data.redrive_policy, + _data.max_consume_count, + _data.retention_hours + ) + + def setUp(self): + super(TestShowDMSQueue, self).setUp() + + self.cmd = queue.ShowDMSQueue(self.app, None) + + self.client.show_queue = mock.Mock() + + def test_show_default(self): + arglist = [ + 'test_queue' + ] + verifylist = [ + ('queue', 'test_queue') + ] + # Verify cm is triggereg with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.get_queue.side_effect = [ + self._data + ] + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + + self.client.get_queue.assert_called() + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestDeleteDMSQueue(TestDMSQueue): + + def setUp(self): + super(TestDeleteDMSQueue, self).setUp() + + self.cmd = queue.DeleteDMSQueue(self.app, None) + + self.client.delete_queue = mock.Mock() + + def test_delete(self): + arglist = ['t1'] + verifylist = [ + ('queue', ['t1']) + ] + # Verify cm is triggereg with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.delete_queue.side_effect = [{}] + + # Trigger the action + self.cmd.take_action(parsed_args) + + calls = [mock.call('t1')] + + self.client.delete_queue.assert_has_calls(calls) + self.assertEqual(1, self.client.delete_queue.call_count) + + def test_delete_multiple(self): + arglist = [ + 't1', + 't2', + ] + verifylist = [ + ('queue', ['t1', 't2']) + ] + # Verify cm is triggereg with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.delete_queue.side_effect = [{}, {}] + + # Trigger the action + self.cmd.take_action(parsed_args) + + calls = [mock.call('t1'), mock.call('t2')] + + self.client.delete_queue.assert_has_calls(calls) + self.assertEqual(2, self.client.delete_queue.call_count) + + +class TestCreateDMSQueue(TestDMSQueue): + + _data = fakes.FakeQueue.create_one() + + columns = ('ID', 'name', 'queue_mode', 'description', 'redrive_policy', + 'max_consume_count', 'retention_hours') + + data = ( + _data.id, + _data.name, + _data.queue_mode, + _data.description, + _data.redrive_policy, + _data.max_consume_count, + _data.retention_hours + ) + + def setUp(self): + super(TestCreateDMSQueue, self).setUp() + + self.cmd = queue.CreateDMSQueue(self.app, None) + + self.client.create_queue = mock.Mock() + + def test_show_default(self): + arglist = [ + 'name', + 'NORMAL', + '--description', 'descr', + '--redrive_policy', 'enable', + '--max_consume_count', '1', + '--retention_hours', '2' + ] + verifylist = [ + ('name', 'name'), + ('queue_mode', 'NORMAL'), + ('description', 'descr'), + ('redrive_policy', 'enable'), + ('max_consume_count', 1), + ('retention_hours', 2) + ] + # Verify cm is triggereg with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.create_queue.side_effect = [ + self._data + ] + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + + self.client.create_queue.assert_called_with( + description='descr', + max_consume_count=1, + name='name', + queue_mode='NORMAL', + redrive_policy='enable', + retention_hours=2 + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/otcextensions/tests/unit/osclient/dms/v1/test_queue.py b/otcextensions/tests/unit/osclient/dms/v1/test_queue.py index 9c5f8c2ca..e8ca7b5a4 100644 --- a/otcextensions/tests/unit/osclient/dms/v1/test_queue.py +++ b/otcextensions/tests/unit/osclient/dms/v1/test_queue.py @@ -1,16 +1,14 @@ -# Copyright 2013 Nebula Inc. +# 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 # -# 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 # -# 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. +# 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 mock from otcextensions.osclient.dms.v1 import queue diff --git a/otcextensions/tests/unit/osclient/dms/v1/test_quota.py b/otcextensions/tests/unit/osclient/dms/v1/test_quota.py index 0ee2a92e8..961bdaaab 100644 --- a/otcextensions/tests/unit/osclient/dms/v1/test_quota.py +++ b/otcextensions/tests/unit/osclient/dms/v1/test_quota.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/dns/v2/test_ptr.py b/otcextensions/tests/unit/osclient/dns/v2/test_ptr.py index 72154fa26..5e45317fe 100644 --- a/otcextensions/tests/unit/osclient/dns/v2/test_ptr.py +++ b/otcextensions/tests/unit/osclient/dns/v2/test_ptr.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/dns/v2/test_recordset.py b/otcextensions/tests/unit/osclient/dns/v2/test_recordset.py index 30603b4ef..17ce97412 100644 --- a/otcextensions/tests/unit/osclient/dns/v2/test_recordset.py +++ b/otcextensions/tests/unit/osclient/dns/v2/test_recordset.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/dns/v2/test_zone.py b/otcextensions/tests/unit/osclient/dns/v2/test_zone.py index 7fddab12c..0e3c70260 100644 --- a/otcextensions/tests/unit/osclient/dns/v2/test_zone.py +++ b/otcextensions/tests/unit/osclient/dns/v2/test_zone.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/kms/v1/fakes.py b/otcextensions/tests/unit/osclient/kms/v1/fakes.py index 7fcc15626..7bd9685f0 100644 --- a/otcextensions/tests/unit/osclient/kms/v1/fakes.py +++ b/otcextensions/tests/unit/osclient/kms/v1/fakes.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/kms/v1/test_cmk.py b/otcextensions/tests/unit/osclient/kms/v1/test_cmk.py index 0e1eebd29..f54e4f794 100644 --- a/otcextensions/tests/unit/osclient/kms/v1/test_cmk.py +++ b/otcextensions/tests/unit/osclient/kms/v1/test_cmk.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/load_balancer/v1/fakes.py b/otcextensions/tests/unit/osclient/load_balancer/v1/fakes.py index e232e9717..04203b43e 100644 --- a/otcextensions/tests/unit/osclient/load_balancer/v1/fakes.py +++ b/otcextensions/tests/unit/osclient/load_balancer/v1/fakes.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/load_balancer/v1/test_health_monitor.py b/otcextensions/tests/unit/osclient/load_balancer/v1/test_health_monitor.py index 19550e127..06286ad8e 100644 --- a/otcextensions/tests/unit/osclient/load_balancer/v1/test_health_monitor.py +++ b/otcextensions/tests/unit/osclient/load_balancer/v1/test_health_monitor.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/load_balancer/v1/test_listener.py b/otcextensions/tests/unit/osclient/load_balancer/v1/test_listener.py index 291c39b98..3dee1d0e9 100644 --- a/otcextensions/tests/unit/osclient/load_balancer/v1/test_listener.py +++ b/otcextensions/tests/unit/osclient/load_balancer/v1/test_listener.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/load_balancer/v1/test_load_balancer.py b/otcextensions/tests/unit/osclient/load_balancer/v1/test_load_balancer.py index f7449b435..e3e252fd3 100644 --- a/otcextensions/tests/unit/osclient/load_balancer/v1/test_load_balancer.py +++ b/otcextensions/tests/unit/osclient/load_balancer/v1/test_load_balancer.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/load_balancer/v1/test_pool.py b/otcextensions/tests/unit/osclient/load_balancer/v1/test_pool.py index 41cd76a99..7d2c2d859 100644 --- a/otcextensions/tests/unit/osclient/load_balancer/v1/test_pool.py +++ b/otcextensions/tests/unit/osclient/load_balancer/v1/test_pool.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/load_balancer/v1/test_pool_member.py b/otcextensions/tests/unit/osclient/load_balancer/v1/test_pool_member.py index 6cc11270d..4ae9ce7f2 100644 --- a/otcextensions/tests/unit/osclient/load_balancer/v1/test_pool_member.py +++ b/otcextensions/tests/unit/osclient/load_balancer/v1/test_pool_member.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/obs/v1/fakes.py b/otcextensions/tests/unit/osclient/obs/v1/fakes.py index 9748c41da..7858fc90b 100644 --- a/otcextensions/tests/unit/osclient/obs/v1/fakes.py +++ b/otcextensions/tests/unit/osclient/obs/v1/fakes.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/rds/v1/fakes.py b/otcextensions/tests/unit/osclient/rds/v1/fakes.py index a0ecae7b6..89f5b6d7f 100644 --- a/otcextensions/tests/unit/osclient/rds/v1/fakes.py +++ b/otcextensions/tests/unit/osclient/rds/v1/fakes.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/rds/v1/test_backup.py b/otcextensions/tests/unit/osclient/rds/v1/test_backup.py index 90f6e4490..f1c0a9afd 100644 --- a/otcextensions/tests/unit/osclient/rds/v1/test_backup.py +++ b/otcextensions/tests/unit/osclient/rds/v1/test_backup.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/rds/v1/test_configuration.py b/otcextensions/tests/unit/osclient/rds/v1/test_configuration.py index 2819845a6..dd17e1598 100644 --- a/otcextensions/tests/unit/osclient/rds/v1/test_configuration.py +++ b/otcextensions/tests/unit/osclient/rds/v1/test_configuration.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/rds/v1/test_datastore.py b/otcextensions/tests/unit/osclient/rds/v1/test_datastore.py index 7feb851e4..3ee5a06e0 100644 --- a/otcextensions/tests/unit/osclient/rds/v1/test_datastore.py +++ b/otcextensions/tests/unit/osclient/rds/v1/test_datastore.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/rds/v1/test_flavor.py b/otcextensions/tests/unit/osclient/rds/v1/test_flavor.py index 5116d158e..ff4106723 100644 --- a/otcextensions/tests/unit/osclient/rds/v1/test_flavor.py +++ b/otcextensions/tests/unit/osclient/rds/v1/test_flavor.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/rds/v1/test_instance.py b/otcextensions/tests/unit/osclient/rds/v1/test_instance.py index 9fa0c88c3..b05745cf3 100644 --- a/otcextensions/tests/unit/osclient/rds/v1/test_instance.py +++ b/otcextensions/tests/unit/osclient/rds/v1/test_instance.py @@ -1,5 +1,3 @@ -# Copyright 2013 Nebula Inc. -# # 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 diff --git a/otcextensions/tests/unit/osclient/test_base.py b/otcextensions/tests/unit/osclient/test_base.py index 12dee68ea..e944614e0 100644 --- a/otcextensions/tests/unit/osclient/test_base.py +++ b/otcextensions/tests/unit/osclient/test_base.py @@ -1,6 +1,3 @@ -# Copyright 2012-2013 OpenStack Foundation -# Copyright 2013 Nebula Inc. -# # 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 From 7b15b291bfc3d572eda5c8a5ec79ab19f32c4c3d Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 20 Apr 2020 17:10:00 +0200 Subject: [PATCH 13/24] complete param renaming --- otcextensions/tests/unit/sdk/dms/v1/test_instance.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_instance.py b/otcextensions/tests/unit/sdk/dms/v1/test_instance.py index 89a444102..88a156695 100644 --- a/otcextensions/tests/unit/sdk/dms/v1/test_instance.py +++ b/otcextensions/tests/unit/sdk/dms/v1/test_instance.py @@ -76,7 +76,6 @@ def test_basic(self): ) def test_make_it(self): - sot = instance.Instance(**JSON_DATA) self.assertEqual(JSON_DATA['name'], sot.name) self.assertEqual(JSON_DATA['access_user'], sot.access_user) @@ -90,7 +89,7 @@ def test_make_it(self): self.assertEqual(JSON_DATA.get('description', None), sot.description) self.assertEqual(JSON_DATA.get('engine', None), - sot.engine) + sot.engine_name) self.assertEqual(JSON_DATA.get('engine_version', None), sot.engine_version) self.assertEqual(JSON_DATA.get('instance_id', None), From 7157f58a25aba21fbe62736584206a50825ce13d Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 20 Apr 2020 17:40:09 +0200 Subject: [PATCH 14/24] add basic instance ops --- otcextensions/osclient/dms/v1/az.py | 38 ++ otcextensions/osclient/dms/v1/instance.py | 335 ++++++++++++++++++ .../osclient/dms/v1/maintenance_window.py | 38 ++ otcextensions/osclient/dms/v1/product.py | 41 +++ otcextensions/sdk/dms/v1/instance.py | 5 +- .../tests/unit/osclient/dms/v1/fakes.py | 47 +++ .../unit/osclient/dms/v1/test_instance.py | 251 ++++++++----- .../tests/unit/sdk/dms/v1/test_instance.py | 2 +- setup.cfg | 9 +- 9 files changed, 679 insertions(+), 87 deletions(-) create mode 100644 otcextensions/osclient/dms/v1/az.py create mode 100644 otcextensions/osclient/dms/v1/instance.py create mode 100644 otcextensions/osclient/dms/v1/maintenance_window.py create mode 100644 otcextensions/osclient/dms/v1/product.py diff --git a/otcextensions/osclient/dms/v1/az.py b/otcextensions/osclient/dms/v1/az.py new file mode 100644 index 000000000..22cf5a843 --- /dev/null +++ b/otcextensions/osclient/dms/v1/az.py @@ -0,0 +1,38 @@ +# 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. +# +'''DMS Instance AZ action implementations''' +from osc_lib import utils +from osc_lib.command import command + +from otcextensions.i18n import _ + + +class ListAZ(command.Lister): + _description = _('List Availability zones') + columns = ('ID', 'name', 'code', 'port', 'has_available_resources') + + def get_parser(self, prog_name): + parser = super(ListAZ, self).get_parser(prog_name) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.dms + + data = client.availability_zones() + + table = (self.columns, + (utils.get_item_properties( + s, self.columns, + ) for s in data)) + return table diff --git a/otcextensions/osclient/dms/v1/instance.py b/otcextensions/osclient/dms/v1/instance.py new file mode 100644 index 000000000..b824aed90 --- /dev/null +++ b/otcextensions/osclient/dms/v1/instance.py @@ -0,0 +1,335 @@ +# 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. +# +'''DMS Instance v1 action implementations''' +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.command import command + +from otcextensions.common import sdk_utils +from otcextensions.i18n import _ + + +INSTANCE_STATUS_CHOICES = ['CREATING', 'CREATEFAILED', 'RUNNING', 'ERROR', + 'STARTING', 'RESTARTING', 'CLOSING', 'FROZEN'] +RETENTION_POLICY_CHOICES = ['produce_reject', 'time_base'] +STORAGE_SPEC_CHOICES = ['dms.physical.storage.high', + 'dms.physical.storage.ultra'] + + +def _get_columns(item): + column_map = {} + hidden = [] + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map, + hidden) + + +class ListDMSInstance(command.Lister): + _description = _('List DMS Instances') + columns = ('ID', 'name', 'engine_name', 'engine_version', + 'storage_spec_code', 'status', 'connect_address', 'router_id', + 'security_group_id', 'subnet_id', 'user_name', 'storage', + 'total_storage', 'used_storage') + + def get_parser(self, prog_name): + parser = super(ListDMSInstance, self).get_parser(prog_name) + + parser.add_argument( + '--engine-name', + metavar='', + help=_('Engine name') + ) + + parser.add_argument( + '--status', + metavar='{' + ','.join(INSTANCE_STATUS_CHOICES) + '}', + type=lambda s: s.upper(), + choices=INSTANCE_STATUS_CHOICES, + help=_('Instance status') + ) + + parser.add_argument( + '--include-failure', + action='store_true', + help=_('Include instances failed to create') + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.dms + + query_params = {} + for param in ['engine_name', 'status', 'include_failure']: + val = getattr(parsed_args, param) + if val is not None: + query_params[param] = val + + data = client.instances(**query_params) + + table = (self.columns, + (utils.get_item_properties( + s, self.columns, + ) for s in data)) + return table + + +class ShowDMSInstance(command.ShowOne): + _description = _('Show single Instance details') + + def get_parser(self, prog_name): + parser = super(ShowDMSInstance, self).get_parser(prog_name) + parser.add_argument( + 'instance', + metavar='', + help=_('ID of the instance') + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.dms + + obj = client.find_instance(parsed_args.instance) + + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + + return (display_columns, data) + + +class DeleteDMSInstance(command.Command): + _description = _('Delete DMS Instance') + + def get_parser(self, prog_name): + parser = super(DeleteDMSInstance, self).get_parser(prog_name) + parser.add_argument( + 'instance', + metavar='', + nargs='+', + help=_('ID of the Instance') + ) + return parser + + def take_action(self, parsed_args): + + if parsed_args.instance: + client = self.app.client_manager.dms + for instance in parsed_args.instance: + client.delete_instance(instance) + + +class CreateDMSInstance(command.ShowOne): + _description = _('Create DMS Instance') + + def get_parser(self, prog_name): + parser = super(CreateDMSInstance, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_('Name of the instance.') + ) + + parser.add_argument( + '--description', + metavar='', + help=_('Description of the instance.') + ) + parser.add_argument( + '--engine-name', + metavar='', + help=_('Engine name. Currently only Kafka is supported.') + ) + parser.add_argument( + '--engine-version', + metavar='', + help=_('Engine version. Currently only "2.3.0" is supported.') + ) + parser.add_argument( + '--storage', + metavar='', + type=int, + required=True, + help=_('Indicates the message storage space with increments ' + 'of 100 GB:\n' + 'Instance with specification being 100MB: 600–90,000 GB\n' + 'Instance with specification being 300MB: 1,200–90,000 GB\n' + 'Instance with specification being 600MB: 2,400–90,000 GB\n' + 'Instance with specification being 1200MB: 4,800–90,000 GB') + ) + parser.add_argument( + '--access-user', + metavar='', + help=_( + 'This parameter is mandatory when engine is set to kafka and ' + 'ssl_enable is set to true. This parameter is invalid when ' + 'ssl_enable is set to false.\n' + 'Indicates a username. A username consists of 4 to 64 ' + 'characters and supports only letters, digits, hyphens (-), ' + 'and underscores (_).') + ) + parser.add_argument( + '--password', + metavar='', + help=_( + 'This parameter is mandatory when engine is set to kafka and ' + 'ssl_enable is set to true. This parameter is invalid when ' + 'ssl_enable is set to false.\n' + 'An instance password must meet the following complexity ' + 'requirements: \n' + ' - Must be a string consisting of 8 to 32 characters.\n' + ' - Must contain at least two of the following character ' + 'types: \n' + ' - Lowercase letters\n' + ' - Uppercase letters\n' + ' - Digits\n' + ' - Special characters' + ) + ) + parser.add_argument( + '--router', + metavar='', + required=True, + help=_('Router ID or Name') + ) + parser.add_argument( + '--security-group', + metavar='', + required=True, + help=_('Security group ID or Name') + ) + parser.add_argument( + '--subnet', + metavar='', + required=True, + help=_('Subnet ID or Name') + ) + parser.add_argument( + '--availability-zone', + metavar='', + required=True, + action='append', + help=_('List of availability zones') + ) + parser.add_argument( + '--product-id', + metavar='', + required=True, + help=_('Product ID of the DMS instance') + ) + parser.add_argument( + '--maintenance-begin', + metavar='', + help=_('Start of the instance maintenance window') + ) + parser.add_argument( + '--maintenance-end', + metavar='', + help=_('End of the instance maintenance window') + ) + parser.add_argument( + '--enable-public-access', + action='store_true', + help=_('Assign public ip to the instance') + ) + parser.add_argument( + '--enable-ssl', + action='store_true', + help=_('Enable SSL for the public access') + ) + parser.add_argument( + '--public-bandwidth', + metavar='', + type=int, + help=_('Public network bandwidth in Mbit/s:\n' + 'When specification 100MB: 3-900\n' + 'When 300MB: 3-900\n' + 'When 600MB: 4-1200\n' + 'When 1200MB: 8-2400') + ) + parser.add_argument( + '--retention-policy', + metavar='{' + ','.join(RETENTION_POLICY_CHOICES) + '}', + type=lambda s: s.lower(), + choices=RETENTION_POLICY_CHOICES, + help=_('Action to be taken when the memory usage reaches the ' + 'disk capacity threshold. Options:\n' + ' `produce_reject`: New messages cannot be created.\n' + ' `time_base`: The earliest messages are deleted.') + ) + parser.add_argument( + '--storage-spec-code', + metavar='{' + ','.join(STORAGE_SPEC_CHOICES) + '}', + type=lambda s: s.lower(), + choices=STORAGE_SPEC_CHOICES, + help=_('The storage I/O specification of a Kafka instance.\n' + 'When specification is 100MB, the storage I/O can be:' + '[`dms.physical.storage.high`, ' + '`dms.physical.storage.ultra`]\n' + 'When specification is 300MB, the storage I/O can be:' + '[`dms.physical.storage.high`, ' + '`dms.physical.storage.ultra`]\n' + 'When specification is 600MB, the storage I/O is ' + '`dms.physical.storage.ultra`.\n' + 'When specification is 1200MB, the storage I/O is ' + '`dms.physical.storage.ultra`.') + ) + + return parser + + def take_action(self, parsed_args): + + attrs = {} + + attrs['name'] = parsed_args.name + for attr in ['description', 'engine_name', 'engine_version', 'storage', + 'access_user', 'password', 'product_id', + 'maintenance_begin', 'maintenance_end', + 'public_bandwidth', + 'retention_policy', 'storage_spec_code']: + val = getattr(parsed_args, attr) + if val is not None: + attrs[attr] = val + + network_client = self.app.client_manager.network + + router_obj = network_client.find_router(parsed_args.router, + ignore_missing=False) + attrs['router_id'] = router_obj.id + subnet_obj = network_client.find_subnet(parsed_args.subnet, + ignore_missing=False) + attrs['subnet_id'] = subnet_obj.id + sg_obj = self.app.client_manager.compute.find_security_group( + parsed_args.security_group, ignore_missing=False) + attrs['security_group_id'] = sg_obj.id + + if parsed_args.availability_zone: + attrs['availability_zone'] = parsed_args.availability_zone + + if parsed_args.maintenance_begin and parsed_args.maintenance_end: + attrs['maintenance_begin'] = parsed_args.maintenance_begin + attrs['maintenance_end'] = parsed_args.maintenance_end + elif parsed_args.maintenance_begin or parsed_args.maintenance_end: + raise exceptions.CommandException(_( + '`maintenance_start` and `maintenance_end` can be set only' + 'together')) + if parsed_args.enable_public_access: + attrs['is_public'] = True + if parsed_args.enable_ssl: + attrs['is_ssl'] = True + + client = self.app.client_manager.dms + + obj = client.create_instance(**attrs) + + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + + return (display_columns, data) diff --git a/otcextensions/osclient/dms/v1/maintenance_window.py b/otcextensions/osclient/dms/v1/maintenance_window.py new file mode 100644 index 000000000..1426d0ad0 --- /dev/null +++ b/otcextensions/osclient/dms/v1/maintenance_window.py @@ -0,0 +1,38 @@ +# 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. +# +'''DMS Instance Maintenance Window action implementations''' +from osc_lib import utils +from osc_lib.command import command + +from otcextensions.i18n import _ + + +class ListMW(command.Lister): + _description = _('List Maintenance Windows') + columns = ('seq', 'begin', 'end', 'is_default') + + def get_parser(self, prog_name): + parser = super(ListMW, self).get_parser(prog_name) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.dms + + data = client.maintenance_windows() + + table = (self.columns, + (utils.get_item_properties( + s, self.columns, + ) for s in data)) + return table diff --git a/otcextensions/osclient/dms/v1/product.py b/otcextensions/osclient/dms/v1/product.py new file mode 100644 index 000000000..ed8d55302 --- /dev/null +++ b/otcextensions/osclient/dms/v1/product.py @@ -0,0 +1,41 @@ +# 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. +# +'''DMS Product specification action implementations''' + +from osc_lib import utils +from osc_lib.command import command + +from otcextensions.i18n import _ + + +class ListProduct(command.Lister): + _description = _('List Product specs') + columns = ('spec_code', 'engine_name', 'engine_version', 'tps', 'storage', + 'partition_num', 'product_id', 'availability_zones', + 'unavailable_zones') + + def get_parser(self, prog_name): + parser = super(ListProduct, self).get_parser(prog_name) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.dms + + data = client.products() + + table = (self.columns, + (utils.get_item_properties( + s, self.columns, + ) for s in data)) + return table diff --git a/otcextensions/sdk/dms/v1/instance.py b/otcextensions/sdk/dms/v1/instance.py index f4524975a..bbfe5e202 100644 --- a/otcextensions/sdk/dms/v1/instance.py +++ b/otcextensions/sdk/dms/v1/instance.py @@ -21,8 +21,9 @@ class Instance(_base.Resource): base_path = '/instances' _query_mapping = resource.QueryParameters( - 'engine', 'name', 'status', 'include_failure', + 'engine_name', 'name', 'status', 'include_failure', 'exact_match_name', + engine_name='engine', include_failure='includeFailure', exact_match_name='exactMatchName' ) @@ -48,7 +49,7 @@ class Instance(_base.Resource): #: Instance description description = resource.Body('description') #: Message engine. - engine = resource.Body('engine') + engine_name = resource.Body('engine') #: Engine version engine_version = resource.Body('engine_version') #: Instance id diff --git a/otcextensions/tests/unit/osclient/dms/v1/fakes.py b/otcextensions/tests/unit/osclient/dms/v1/fakes.py index c69f5aaaa..c39c547c9 100644 --- a/otcextensions/tests/unit/osclient/dms/v1/fakes.py +++ b/otcextensions/tests/unit/osclient/dms/v1/fakes.py @@ -19,10 +19,23 @@ from otcextensions.tests.unit.osclient import test_base from otcextensions.sdk.dms.v1 import group +from otcextensions.sdk.dms.v1 import instance from otcextensions.sdk.dms.v1 import queue from otcextensions.sdk.dms.v1 import quota +def gen_data(data, columns): + """Fill expected data tuple based on columns list + """ + return tuple(getattr(data, attr, '') for attr in columns) + + +def gen_data_dict(data, columns): + """Fill expected data tuple based on columns list + """ + return tuple(data.get(attr, '') for attr in columns) + + class TestDMS(utils.TestCommand): def setUp(self): @@ -82,3 +95,37 @@ def generate(cls): } obj = quota.Quota.existing(**object_info) return obj + + +class FakeInstance(test_base.Fake): + @classmethod + def generate(cls): + object_info = { + 'name': 'name-' + uuid.uuid4().hex, + 'description': 'name-' + uuid.uuid4().hex, + 'engine_name': 'engine-' + uuid.uuid4().hex, + 'engine_version': 'ver-' + uuid.uuid4().hex, + 'storage': random.randint(1, 100), + 'access_user': 'user-' + uuid.uuid4().hex, + 'password': uuid.uuid4().hex, + 'router_id': 'router_id-' + uuid.uuid4().hex, + 'router_name': 'router_name-' + uuid.uuid4().hex, + 'subnet_id': 'subnet_id-' + uuid.uuid4().hex, + 'subnet_name': 'subnet_name-' + uuid.uuid4().hex, + 'security_group_id': 'security_group_id-' + uuid.uuid4().hex, + 'security_group_name': 'security_group_name-' + uuid.uuid4().hex, + 'availability_zones': ['az' + uuid.uuid4().hex], + 'product_id': 'product-' + uuid.uuid4().hex, + 'maintenance_begin': 'mb-' + uuid.uuid4().hex, + 'maintenance_end': 'me-' + uuid.uuid4().hex, + 'is_public': random.choice([True, False]), + 'is_ssl': random.choice([True, False]), + 'kafka_public_status': 'kps-' + uuid.uuid4().hex, + 'public_bandwidth': random.randint(1, 100), + 'retention_policy': random.choice(['produce_reject', 'time_base']), + 'storage_spec_code': random.choice(['dms.physical.storage.high', + 'dms.physical.storage.ultra']) + } + + obj = instance.Instance.existing(**object_info) + return obj diff --git a/otcextensions/tests/unit/osclient/dms/v1/test_instance.py b/otcextensions/tests/unit/osclient/dms/v1/test_instance.py index e8ca7b5a4..1e9db6a76 100644 --- a/otcextensions/tests/unit/osclient/dms/v1/test_instance.py +++ b/otcextensions/tests/unit/osclient/dms/v1/test_instance.py @@ -11,144 +11,181 @@ # under the License. import mock -from otcextensions.osclient.dms.v1 import queue +from otcextensions.osclient.dms.v1 import instance from otcextensions.tests.unit.osclient.dms.v1 import fakes -class TestDMSQueue(fakes.TestDMS): +class TestDMSInstance(fakes.TestDMS): def setUp(self): - super(TestDMSQueue, self).setUp() + super(TestDMSInstance, self).setUp() self.client = self.app.client_manager.dms -class TestListDMSQueue(TestDMSQueue): +class TestListDMSInstance(TestDMSInstance): - queues = fakes.FakeQueue.create_multiple(3) + instances = fakes.FakeInstance.create_multiple(3) - columns = ('ID', 'name', 'queue_mode', 'description', 'redrive_policy', - 'max_consume_count', 'retention_hours') + columns = ('ID', 'name', 'engine_name', 'engine_version', + 'storage_spec_code', 'status', 'connect_address', 'router_id', + 'security_group_id', 'subnet_id', 'user_name', 'storage', + 'total_storage', 'used_storage') data = [] - for s in queues: + for s in instances: data.append(( s.id, s.name, - s.queue_mode, - s.description, - s.redrive_policy, - s.max_consume_count, - s.retention_hours + s.engine_name, + s.engine_version, + s.storage_spec_code, + s.status, + s.connect_address, + s.router_id, + s.security_group_id, + s.subnet_id, + s.user_name, + s.storage, + s.total_storage, + s.used_storage )) def setUp(self): - super(TestListDMSQueue, self).setUp() + super(TestListDMSInstance, self).setUp() - self.cmd = queue.ListDMSQueue(self.app, None) + self.cmd = instance.ListDMSInstance(self.app, None) - self.client.queues = mock.Mock() + self.client.instances = mock.Mock() def test_list_queue(self): arglist = [ ] verifylist = [ - # ('group', None), ] # Verify cm is triggereg with default parameters parsed_args = self.check_parser(self.cmd, arglist, verifylist) # Set the response - self.client.queues.side_effect = [ - self.queues + self.client.instances.side_effect = [ + self.instances ] # Trigger the action columns, data = self.cmd.take_action(parsed_args) - self.client.queues.assert_called_once_with() + self.client.instances.assert_called_once_with(include_failure=False) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_list_queue_args(self): + arglist = [ + '--engine-name', 'engine', + '--status', 'Creating', + '--include-failure' + ] -class TestShowDMSQueue(TestDMSQueue): + verifylist = [ + ('engine_name', 'engine'), + ('status', 'CREATING'), + ('include_failure', True) + ] - _data = fakes.FakeQueue.create_one() + # Verify cm is triggereg with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns = ('ID', 'name', 'queue_mode', 'description', 'redrive_policy', - 'max_consume_count', 'retention_hours') + # Set the response + self.client.instances.side_effect = [ + self.instances + ] + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) - data = ( - _data.id, - _data.name, - _data.queue_mode, - _data.description, - _data.redrive_policy, - _data.max_consume_count, - _data.retention_hours - ) + self.client.instances.assert_called_once_with( + engine_name='engine', + status='CREATING', + include_failure=True) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestShowDMSInstance(TestDMSInstance): + + _data = fakes.FakeInstance.create_one() + + columns = ('access_user', 'availability_zones', 'description', + 'engine_name', 'engine_version', 'is_public', 'is_ssl', + 'kafka_public_status', 'maintenance_end', 'name', 'password', + 'product_id', 'public_bandwidth', 'retention_policy', + 'router_id', 'router_name', 'security_group_id', + 'security_group_name', 'storage', 'storage_spec_code', + 'subnet_id') + + data = fakes.gen_data(_data, columns) def setUp(self): - super(TestShowDMSQueue, self).setUp() + super(TestShowDMSInstance, self).setUp() - self.cmd = queue.ShowDMSQueue(self.app, None) + self.cmd = instance.ShowDMSInstance(self.app, None) - self.client.show_queue = mock.Mock() + self.client.find_instance = mock.Mock() def test_show_default(self): arglist = [ - 'test_queue' + 'test_instance' ] verifylist = [ - ('queue', 'test_queue') + ('instance', 'test_instance') ] # Verify cm is triggereg with default parameters parsed_args = self.check_parser(self.cmd, arglist, verifylist) # Set the response - self.client.get_queue.side_effect = [ + self.client.find_instance.side_effect = [ self._data ] # Trigger the action columns, data = self.cmd.take_action(parsed_args) - self.client.get_queue.assert_called() + self.client.find_instance.assert_called() self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) -class TestDeleteDMSQueue(TestDMSQueue): +class TestDeleteDMSInstance(TestDMSInstance): def setUp(self): - super(TestDeleteDMSQueue, self).setUp() + super(TestDeleteDMSInstance, self).setUp() - self.cmd = queue.DeleteDMSQueue(self.app, None) + self.cmd = instance.DeleteDMSInstance(self.app, None) - self.client.delete_queue = mock.Mock() + self.client.delete_instance = mock.Mock() def test_delete(self): arglist = ['t1'] verifylist = [ - ('queue', ['t1']) + ('instance', ['t1']) ] # Verify cm is triggereg with default parameters parsed_args = self.check_parser(self.cmd, arglist, verifylist) # Set the response - self.client.delete_queue.side_effect = [{}] + self.client.delete_instance.side_effect = [{}] # Trigger the action self.cmd.take_action(parsed_args) calls = [mock.call('t1')] - self.client.delete_queue.assert_has_calls(calls) - self.assertEqual(1, self.client.delete_queue.call_count) + self.client.delete_instance.assert_has_calls(calls) + self.assertEqual(1, self.client.delete_instance.call_count) def test_delete_multiple(self): arglist = [ @@ -156,82 +193,130 @@ def test_delete_multiple(self): 't2', ] verifylist = [ - ('queue', ['t1', 't2']) + ('instance', ['t1', 't2']) ] # Verify cm is triggereg with default parameters parsed_args = self.check_parser(self.cmd, arglist, verifylist) # Set the response - self.client.delete_queue.side_effect = [{}, {}] + self.client.delete_instance.side_effect = [{}, {}] # Trigger the action self.cmd.take_action(parsed_args) calls = [mock.call('t1'), mock.call('t2')] - self.client.delete_queue.assert_has_calls(calls) - self.assertEqual(2, self.client.delete_queue.call_count) + self.client.delete_instance.assert_has_calls(calls) + self.assertEqual(2, self.client.delete_instance.call_count) -class TestCreateDMSQueue(TestDMSQueue): +class TestCreateDMSInstance(TestDMSInstance): - _data = fakes.FakeQueue.create_one() + _data = fakes.FakeInstance.create_one() - columns = ('ID', 'name', 'queue_mode', 'description', 'redrive_policy', - 'max_consume_count', 'retention_hours') + columns = ('access_user', 'availability_zones', 'description', + 'engine_name', 'engine_version', 'is_public', 'is_ssl', + 'kafka_public_status', 'maintenance_end', 'name', 'password', + 'product_id', 'public_bandwidth', 'retention_policy', + 'router_id', 'router_name', 'security_group_id', + 'security_group_name', 'storage', 'storage_spec_code', + 'subnet_id') - data = ( - _data.id, - _data.name, - _data.queue_mode, - _data.description, - _data.redrive_policy, - _data.max_consume_count, - _data.retention_hours - ) + data = fakes.gen_data(_data, columns) def setUp(self): - super(TestCreateDMSQueue, self).setUp() + super(TestCreateDMSInstance, self).setUp() - self.cmd = queue.CreateDMSQueue(self.app, None) + self.cmd = instance.CreateDMSInstance(self.app, None) - self.client.create_queue = mock.Mock() + self.client.create_instance = mock.Mock() + self.app.client_manager.network = mock.Mock() + self.app.client_manager.network.find_router = mock.Mock() + self.app.client_manager.network.find_subnet = mock.Mock() + self.app.client_manager.compute = mock.Mock() - def test_show_default(self): + def test_create_default(self): arglist = [ 'name', - 'NORMAL', '--description', 'descr', - '--redrive_policy', 'enable', - '--max_consume_count', '1', - '--retention_hours', '2' + '--engine-name', 'kafka', + '--engine-version', '2.1.0', + '--storage', '15', + '--access-user', 'u1', + '--password', 'pwd', + '--router', 'router_id', + '--security-group', 'sg_id', + '--subnet', 'subnet_id', + '--availability-zone', 'az1', + '--availability-zone', 'az2', + '--product-id', 'pid', + '--maintenance-begin', 'mwb', + '--maintenance-end', 'mwe', + '--enable-public-access', + '--enable-ssl', + '--public-bandwidth', '14', + '--retention-policy', 'produce_reject', + '--storage-spec-code', 'dms.physical.storage.high' ] verifylist = [ ('name', 'name'), - ('queue_mode', 'NORMAL'), ('description', 'descr'), - ('redrive_policy', 'enable'), - ('max_consume_count', 1), - ('retention_hours', 2) + ('engine_name', 'kafka'), + ('engine_version', '2.1.0'), + ('storage', 15), + ('access_user', 'u1'), + ('password', 'pwd'), + ('router', 'router_id'), + ('security_group', 'sg_id'), + ('subnet', 'subnet_id'), + ('availability_zone', ['az1', 'az2']), + ('product_id', 'pid'), + ('maintenance_begin', 'mwb'), + ('maintenance_end', 'mwe'), + ('enable_public_access', True), + ('enable_ssl', True), + ('public_bandwidth', 14), + ('retention_policy', 'produce_reject'), + ('storage_spec_code', 'dms.physical.storage.high') ] # Verify cm is triggereg with default parameters parsed_args = self.check_parser(self.cmd, arglist, verifylist) # Set the response - self.client.create_queue.side_effect = [ + self.client.create_instance.side_effect = [ self._data ] # Trigger the action columns, data = self.cmd.take_action(parsed_args) - self.client.create_queue.assert_called_with( + self.app.client_manager.network.find_router.assert_called_with( + 'router_id', ignore_missing=False) + self.app.client_manager.network.find_subnet.assert_called_with( + 'subnet_id', ignore_missing=False) + self.app.client_manager.compute.find_security_group.assert_called_with( + 'sg_id', ignore_missing=False) + + self.client.create_instance.assert_called_with( + access_user='u1', + availability_zone=['az1', 'az2'], description='descr', - max_consume_count=1, + engine_name='kafka', + engine_version='2.1.0', + is_public=True, + is_ssl=True, + maintenance_begin='mwb', + maintenance_end='mwe', name='name', - queue_mode='NORMAL', - redrive_policy='enable', - retention_hours=2 + password='pwd', + product_id='pid', + public_bandwidth=14, + retention_policy='produce_reject', + router_id=mock.ANY, + security_group_id=mock.ANY, + storage=15, + storage_spec_code='dms.physical.storage.high', + subnet_id=mock.ANY ) self.assertEqual(self.columns, columns) diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_instance.py b/otcextensions/tests/unit/sdk/dms/v1/test_instance.py index 88a156695..8e794c586 100644 --- a/otcextensions/tests/unit/sdk/dms/v1/test_instance.py +++ b/otcextensions/tests/unit/sdk/dms/v1/test_instance.py @@ -65,7 +65,7 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertDictEqual({ - 'engine': 'engine', + 'engine_name': 'engine', 'exact_match_name': 'exactMatchName', 'include_failure': 'includeFailure', 'limit': 'limit', diff --git a/setup.cfg b/setup.cfg index 6f69f6bdd..010bb4ba8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -152,7 +152,14 @@ openstack.dms.v1 = dms_group_list = otcextensions.osclient.dms.v1.group:ListGroup dms_group_delete = otcextensions.osclient.dms.v1.group:DeleteGroup dms_group_create = otcextensions.osclient.dms.v1.group:CreateGroup - dms_quota_list = otcextensions.osclient.dms.v1.quota:ListQuota + dms_instance_list = otcextensions.osclient.dms.v1.instance:ListDMSInstance + dms_instance_show = otcextensions.osclient.dms.v1.instance:ShowDMSInstance + dms_instance_create = otcextensions.osclient.dms.v1.instance:CreateDMSInstance + dms_instance_update = otcextensions.osclient.dms.v1.instance:UpdateDMSInstance + dms_instance_restart = otcextensions.osclient.dms.v1.instance:RestartDMSInstance + dms_az_list = otcextensions.osclient.dms.v1.az:ListAZ + dms_maintenance_window_list = otcextensions.osclient.dms.v1.maintenance_window:ListMW + dms_product_list = otcextensions.osclient.dms.v1.product:ListProduct openstack.dcs.v1 = dcs_instance_list = otcextensions.osclient.dcs.v1.instance:ListInstance From 37823e2c3946d7dadb58cd8a60cbb51d15f1690d Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 21 Apr 2020 08:51:24 +0200 Subject: [PATCH 15/24] some review comments --- otcextensions/sdk/dms/v1/instance.py | 2 +- otcextensions/sdk/dms/v1/message.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/otcextensions/sdk/dms/v1/instance.py b/otcextensions/sdk/dms/v1/instance.py index bbfe5e202..9a83190f3 100644 --- a/otcextensions/sdk/dms/v1/instance.py +++ b/otcextensions/sdk/dms/v1/instance.py @@ -41,7 +41,7 @@ class Instance(_base.Resource): #: List of availability zones the instance belongs to availability_zones = resource.Body('available_zones', type=list) #: Billing mode - charging_mode = resource.Body('charging_mode') + charging_mode = resource.Body('charging_mode', type=int) #: IP address of the instance connect_address = resource.Body('connect_address') #: Instance creation time diff --git a/otcextensions/sdk/dms/v1/message.py b/otcextensions/sdk/dms/v1/message.py index 2caef1f04..318f06d82 100644 --- a/otcextensions/sdk/dms/v1/message.py +++ b/otcextensions/sdk/dms/v1/message.py @@ -47,8 +47,8 @@ def _collect_attrs(self, attrs): """ Save remaining attributes under "attributes" attribute """ body = self._consume_body_attrs(attrs) - header = self._consume_body_attrs(attrs) - uri = self._consume_body_attrs(attrs) + header = self._consume_header_attrs(attrs) + uri = self._consume_uri_attrs(attrs) body['attributes'] = attrs computed = self._consume_attrs(self._computed_mapping(), attrs) From 20df7dd097b990f95a5dca59b74d900e2cfad095 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 21 Apr 2020 09:15:03 +0200 Subject: [PATCH 16/24] drop DMS quota cli doc --- doc/source/cli/dms.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/cli/dms.rst b/doc/source/cli/dms.rst index ce3370d93..532a0f4b4 100644 --- a/doc/source/cli/dms.rst +++ b/doc/source/cli/dms.rst @@ -28,8 +28,8 @@ Group operations .. _dms_quota: -Quota operations ----------------- +Instance operations +------------------- .. autoprogram-cliff:: openstack.dms.v1 - :command: dms quota * + :command: dms instance * From 71ec14d7facacf486b9f67cf48627e7351f4a218 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 21 Apr 2020 09:15:24 +0200 Subject: [PATCH 17/24] add instance restart CLI and fix SDK for it --- otcextensions/osclient/dms/v1/instance.py | 24 ++++++++++++++++++- otcextensions/sdk/dms/v1/instance.py | 4 ++-- .../tests/unit/sdk/dms/v1/test_instance.py | 8 +++---- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/otcextensions/osclient/dms/v1/instance.py b/otcextensions/osclient/dms/v1/instance.py index b824aed90..fc75961ca 100644 --- a/otcextensions/osclient/dms/v1/instance.py +++ b/otcextensions/osclient/dms/v1/instance.py @@ -28,7 +28,7 @@ def _get_columns(item): column_map = {} - hidden = [] + hidden = ['location'] return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map, hidden) @@ -333,3 +333,25 @@ def take_action(self, parsed_args): data = utils.get_item_properties(obj, columns) return (display_columns, data) + + +class RestartDMSInstance(command.Command): + _description = _('Restart single Instance') + + def get_parser(self, prog_name): + parser = super(RestartDMSInstance, self).get_parser(prog_name) + parser.add_argument( + 'instance', + metavar='', + help=_('ID of the instance') + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.dms + + obj = client.find_instance(parsed_args.instance, ignore_missing=False) + + client.restart_instance(obj) + + return diff --git a/otcextensions/sdk/dms/v1/instance.py b/otcextensions/sdk/dms/v1/instance.py index 9a83190f3..05e0a3929 100644 --- a/otcextensions/sdk/dms/v1/instance.py +++ b/otcextensions/sdk/dms/v1/instance.py @@ -127,7 +127,7 @@ def _action(self, session, action, id_list): response = session.post( '/instances/action', - body + json=body ) exceptions.raise_from_response(response) @@ -155,7 +155,7 @@ def delete_failed(self, session): response = session.post( '/instances/action', - body + json=body ) exceptions.raise_from_response(response) return diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_instance.py b/otcextensions/tests/unit/sdk/dms/v1/test_instance.py index 8e794c586..b55b913b9 100644 --- a/otcextensions/tests/unit/sdk/dms/v1/test_instance.py +++ b/otcextensions/tests/unit/sdk/dms/v1/test_instance.py @@ -164,14 +164,14 @@ def test_restart(self): self.sess.post.assert_called_with( '/instances/action', - {'action': 'restart', 'instances': ['1']} + json={'action': 'restart', 'instances': ['1']} ) sot.restart_batch(self.sess, ['1', '2']) self.sess.post.assert_called_with( '/instances/action', - {'action': 'restart', 'instances': ['1', '2']} + json={'action': 'restart', 'instances': ['1', '2']} ) def test_delete(self): @@ -185,12 +185,12 @@ def test_delete(self): self.sess.post.assert_called_with( '/instances/action', - {'action': 'delete', 'instances': ['1', '2']} + json={'action': 'delete', 'instances': ['1', '2']} ) sot.delete_failed(self.sess) self.sess.post.assert_called_with( '/instances/action', - {'action': 'delete', 'allFailure': 'kafka'} + json={'action': 'delete', 'allFailure': 'kafka'} ) From 16fd5ddaf3fb0363aad62cbcecd5d02d035857ec Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 21 Apr 2020 09:19:32 +0200 Subject: [PATCH 18/24] add unit test for osc restart instance --- .../unit/osclient/dms/v1/test_instance.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/otcextensions/tests/unit/osclient/dms/v1/test_instance.py b/otcextensions/tests/unit/osclient/dms/v1/test_instance.py index 1e9db6a76..52912d618 100644 --- a/otcextensions/tests/unit/osclient/dms/v1/test_instance.py +++ b/otcextensions/tests/unit/osclient/dms/v1/test_instance.py @@ -321,3 +321,38 @@ def test_create_default(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + + +class TestRestartDMSInstance(TestDMSInstance): + + _data = fakes.FakeInstance.create_one() + + def setUp(self): + super(TestRestartDMSInstance, self).setUp() + + self.cmd = instance.RestartDMSInstance(self.app, None) + + self.client.find_instance = mock.Mock() + self.client.restart_instance = mock.Mock() + + def test_show_default(self): + arglist = [ + 'test_instance' + ] + verifylist = [ + ('instance', 'test_instance') + ] + # Verify cm is triggereg with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.find_instance.side_effect = [ + self._data + ] + + # Trigger the action + self.cmd.take_action(parsed_args) + + self.client.find_instance.assert_called_with('test_instance', + ignore_missing=False) + self.client.restart_instance.assert_called_with(self._data) From c108242d4ad78d909a2a30dbb4a262af03794cd8 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 21 Apr 2020 13:14:41 +0200 Subject: [PATCH 19/24] add further OSC stuff --- doc/source/cli/dms.rst | 16 +- doc/source/sdk/proxies/dms.rst | 11 + doc/source/sdk/resources/dms/index.rst | 3 + doc/source/sdk/resources/dms/v1/instance.rst | 13 ++ doc/source/sdk/resources/dms/v1/misc | 41 ++++ doc/source/sdk/resources/dms/v1/misc.rst | 41 ++++ doc/source/sdk/resources/dms/v1/topic.rst | 13 ++ otcextensions/osclient/dms/v1/instance.py | 12 +- otcextensions/osclient/dms/v1/topic.py | 172 +++++++++++++++ otcextensions/sdk/dms/v1/_proxy.py | 9 +- otcextensions/sdk/dms/v1/topic.py | 2 + .../tests/unit/osclient/dms/v1/fakes.py | 18 ++ .../unit/osclient/dms/v1/test_instance.py | 4 +- .../tests/unit/osclient/dms/v1/test_topic.py | 196 ++++++++++++++++++ .../tests/unit/sdk/dms/v1/test_proxy.py | 14 +- setup.cfg | 5 +- 16 files changed, 554 insertions(+), 16 deletions(-) create mode 100644 doc/source/sdk/resources/dms/v1/instance.rst create mode 100644 doc/source/sdk/resources/dms/v1/misc create mode 100644 doc/source/sdk/resources/dms/v1/misc.rst create mode 100644 doc/source/sdk/resources/dms/v1/topic.rst create mode 100644 otcextensions/osclient/dms/v1/topic.py create mode 100644 otcextensions/tests/unit/osclient/dms/v1/test_topic.py diff --git a/doc/source/cli/dms.rst b/doc/source/cli/dms.rst index 532a0f4b4..e28453920 100644 --- a/doc/source/cli/dms.rst +++ b/doc/source/cli/dms.rst @@ -26,10 +26,24 @@ Group operations .. autoprogram-cliff:: openstack.dms.v1 :command: dms group * -.. _dms_quota: +.. _dms_instance: Instance operations ------------------- .. autoprogram-cliff:: openstack.dms.v1 :command: dms instance * + +.. _dms_misq: + +Misc operations +--------------- + +.. autoprogram-cliff:: openstack.dms.v1 + :command: dms az list + +.. autoprogram-cliff:: openstack.dms.v1 + :command: dms maintenance window list + +.. autoprogram-cliff:: openstack.dms.v1 + :command: dms product list diff --git a/doc/source/sdk/proxies/dms.rst b/doc/source/sdk/proxies/dms.rst index c793c28f5..a4d301f06 100644 --- a/doc/source/sdk/proxies/dms.rst +++ b/doc/source/sdk/proxies/dms.rst @@ -24,3 +24,14 @@ Message Group Operations .. autoclass:: otcextensions.sdk.dms.v1._proxy.Proxy :noindex: :members: groups, create_group, delete_group + +Instance Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: otcextensions.sdk.dms.v1._proxy.Proxy + :noindex: + :members: instances, find_instance, get_instance, + create_instance, update_instance, delete_instance, + delete_batch, restart_instance, delete_failed, + topics, create_topic, delete_topic, + availability_zones, products, maintenance_windows diff --git a/doc/source/sdk/resources/dms/index.rst b/doc/source/sdk/resources/dms/index.rst index 8236b0b78..3837c3064 100644 --- a/doc/source/sdk/resources/dms/index.rst +++ b/doc/source/sdk/resources/dms/index.rst @@ -7,3 +7,6 @@ DMS Resources v1/group v1/message v1/queue + v1/instance + v1/topic + v1/misc diff --git a/doc/source/sdk/resources/dms/v1/instance.rst b/doc/source/sdk/resources/dms/v1/instance.rst new file mode 100644 index 000000000..fad2b31fe --- /dev/null +++ b/doc/source/sdk/resources/dms/v1/instance.rst @@ -0,0 +1,13 @@ +otcextensions.sdk.dcs.v1.instance +================================= + +.. automodule:: otcextensions.sdk.dms.v1.instance + +The DMS Instance Class +---------------------- + +The ``Instance`` class inherits from +:class:`~otcextensions.sdk.sdk_resource.Resource`. + +.. autoclass:: otcextensions.sdk.dms.v1.instance.Instance + :members: diff --git a/doc/source/sdk/resources/dms/v1/misc b/doc/source/sdk/resources/dms/v1/misc new file mode 100644 index 000000000..027026683 --- /dev/null +++ b/doc/source/sdk/resources/dms/v1/misc @@ -0,0 +1,41 @@ +otcextensions.sdk.dcs.v1.az +=========================== + +.. automodule:: otcextensions.sdk.dms.v1.az + +The DMS Availability Zone Class +------------------------------- + +The ``AvailabilityZone`` class inherits from +:class:`~otcextensions.sdk.sdk_resource.Resource`. + +.. autoclass:: otcextensions.sdk.dms.v1.az.AvailabilityZone + :members: + +otcextensions.sdk.dcs.v1.product +================================ + +.. automodule:: otcextensions.sdk.dms.v1.product + +The DMS Product Spec Class +-------------------------- + +The ``Product`` class inherits from +:class:`~otcextensions.sdk.sdk_resource.Resource`. + +.. autoclass:: otcextensions.sdk.dms.v1.product.Product + :members: + +otcextensions.sdk.dcs.v1.maintenance_window +=========================================== + +.. automodule:: otcextensions.sdk.dms.v1.maintenance_window + +The DMS Maintenance window Class +-------------------------------- + +The ``MaintenanceWindow`` class inherits from +:class:`~otcextensions.sdk.sdk_resource.Resource`. + +.. autoclass:: otcextensions.sdk.dms.v1.maintenance_window.MaintenanceWindow + :members: diff --git a/doc/source/sdk/resources/dms/v1/misc.rst b/doc/source/sdk/resources/dms/v1/misc.rst new file mode 100644 index 000000000..027026683 --- /dev/null +++ b/doc/source/sdk/resources/dms/v1/misc.rst @@ -0,0 +1,41 @@ +otcextensions.sdk.dcs.v1.az +=========================== + +.. automodule:: otcextensions.sdk.dms.v1.az + +The DMS Availability Zone Class +------------------------------- + +The ``AvailabilityZone`` class inherits from +:class:`~otcextensions.sdk.sdk_resource.Resource`. + +.. autoclass:: otcextensions.sdk.dms.v1.az.AvailabilityZone + :members: + +otcextensions.sdk.dcs.v1.product +================================ + +.. automodule:: otcextensions.sdk.dms.v1.product + +The DMS Product Spec Class +-------------------------- + +The ``Product`` class inherits from +:class:`~otcextensions.sdk.sdk_resource.Resource`. + +.. autoclass:: otcextensions.sdk.dms.v1.product.Product + :members: + +otcextensions.sdk.dcs.v1.maintenance_window +=========================================== + +.. automodule:: otcextensions.sdk.dms.v1.maintenance_window + +The DMS Maintenance window Class +-------------------------------- + +The ``MaintenanceWindow`` class inherits from +:class:`~otcextensions.sdk.sdk_resource.Resource`. + +.. autoclass:: otcextensions.sdk.dms.v1.maintenance_window.MaintenanceWindow + :members: diff --git a/doc/source/sdk/resources/dms/v1/topic.rst b/doc/source/sdk/resources/dms/v1/topic.rst new file mode 100644 index 000000000..46a04e733 --- /dev/null +++ b/doc/source/sdk/resources/dms/v1/topic.rst @@ -0,0 +1,13 @@ +otcextensions.sdk.dcs.v1.topic +============================== + +.. automodule:: otcextensions.sdk.dms.v1.topic + +The DMS Instance topic Class +---------------------------- + +The ``Topic`` class inherits from +:class:`~otcextensions.sdk.sdk_resource.Resource`. + +.. autoclass:: otcextensions.sdk.dms.v1.topic.Topic + :members: diff --git a/otcextensions/osclient/dms/v1/instance.py b/otcextensions/osclient/dms/v1/instance.py index fc75961ca..a403370be 100644 --- a/otcextensions/osclient/dms/v1/instance.py +++ b/otcextensions/osclient/dms/v1/instance.py @@ -184,13 +184,13 @@ def get_parser(self, prog_name): 'ssl_enable is set to false.\n' 'An instance password must meet the following complexity ' 'requirements: \n' - ' - Must be a string consisting of 8 to 32 characters.\n' - ' - Must contain at least two of the following character ' + '- Must be a string consisting of 8 to 32 characters.\n' + '- Must contain at least two of the following character ' 'types: \n' - ' - Lowercase letters\n' - ' - Uppercase letters\n' - ' - Digits\n' - ' - Special characters' + '-- Lowercase letters\n' + '-- Uppercase letters\n' + '-- Digits\n' + '-- Special characters' ) ) parser.add_argument( diff --git a/otcextensions/osclient/dms/v1/topic.py b/otcextensions/osclient/dms/v1/topic.py new file mode 100644 index 000000000..220e358f3 --- /dev/null +++ b/otcextensions/osclient/dms/v1/topic.py @@ -0,0 +1,172 @@ +# 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. +# +'''DMS Topic v1 action implementations''' +from osc_lib import utils +from osc_lib.command import command + +from otcextensions.common import sdk_utils +from otcextensions.i18n import _ + + +def _get_columns(item): + column_map = {} + hidden = ['location'] + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map, + hidden) + + +class ListDMSInstanceTopic(command.Lister): + _description = _('List DMS Instance topics') + columns = ('ID', 'replication', 'partition', 'retention_time', + 'is_sync_flush', 'is_sync_replication') + + def get_parser(self, prog_name): + parser = super(ListDMSInstanceTopic, self).get_parser(prog_name) + + parser.add_argument( + 'instance', + metavar='', + help=_('DMS Instance name or ID') + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.dms + + instance_obj = client.find_instance(parsed_args.instance, + ignore_missing=False) + data = client.topics(instance=instance_obj) + + table = (self.columns, + (utils.get_item_properties( + s, self.columns, + ) for s in data)) + return table + + +class DeleteDMSInstanceTopic(command.Command): + _description = _('Delete DMS Instance Topic') + + def get_parser(self, prog_name): + parser = super(DeleteDMSInstanceTopic, self).get_parser(prog_name) + parser.add_argument( + 'instance', + metavar='', + help=_('ID or name of the Instance') + ) + parser.add_argument( + 'topic', + metavar='', + nargs='+', + help=_('Topic ID') + ) + return parser + + def take_action(self, parsed_args): + + if parsed_args.instance: + client = self.app.client_manager.dms + instance = client.find_instance( + parsed_args.instance, + ignore_missing=False) + client.delete_topic(instance=instance, topics=parsed_args.topic) + + +class CreateDMSInstanceTopic(command.ShowOne): + _description = _('Create DMS Instance Topic') + + def get_parser(self, prog_name): + parser = super(CreateDMSInstanceTopic, self).get_parser(prog_name) + parser.add_argument( + 'instance', + metavar='', + help=_('Instance ID or Name.') + ) + parser.add_argument( + 'id', + metavar='', + help=_('Name/ID of the topic.') + ) + parser.add_argument( + '--partition', + metavar='', + type=int, + choices=range(1, 21), + help=_( + 'The number of topic partitions, which is used to set the ' + 'number of concurrently consumed messages. ' + 'Value range: 1–20. Default value: 3.') + ) + parser.add_argument( + '--replication', + metavar='', + type=int, + choices=range(1, 4), + help=_( + 'The number of replicas, which is configured to ensure data ' + 'reliability. Value range: 1–3. Default value: 3.') + ) + parser.add_argument( + '--retention-time', + metavar='', + type=int, + choices=range(1, 169), + default=72, + help=_( + 'The retention period of a message. Its default value is ' + '72. Value range: 1–168. Default value: 72. Unit: hour.') + ) + parser.add_argument( + '--enable-sync-flush', + action='store_true', + help=_( + 'Whether to enable synchronous flushing. ' + 'Default value: false. Synchronous flushing compromises ' + 'performance.') + ) + parser.add_argument( + '--enable-sync-replication', + action='store_true', + help=_( + 'Whether to enable synchronous replication. After this ' + 'function is enabled, the acks parameter on the producer ' + 'client must be set to –1. Otherwise, this parameter does ' + 'not take effect.') + ) + return parser + + def take_action(self, parsed_args): + + attrs = {} + + attrs['id'] = parsed_args.id + for attr in ['partition', 'replication', 'retention_time']: + val = getattr(parsed_args, attr) + if val is not None: + attrs[attr] = val + if parsed_args.enable_sync_flush: + attrs['is_sync_flush'] = True + if parsed_args.enable_sync_replication: + attrs['is_sync_replication'] = True + + client = self.app.client_manager.dms + + instance = client.find_instance(parsed_args.instance, + ignore_missing=False) + + obj = client.create_topic(instance=instance, **attrs) + + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + + return (display_columns, data) diff --git a/otcextensions/sdk/dms/v1/_proxy.py b/otcextensions/sdk/dms/v1/_proxy.py index 6da18d918..a614784cb 100644 --- a/otcextensions/sdk/dms/v1/_proxy.py +++ b/otcextensions/sdk/dms/v1/_proxy.py @@ -327,7 +327,7 @@ def delete_failed(self): return dummy_instance.delete_failed(self) # ======== Topics ======= - def topics(self, instance, **kwargs): + def topics(self, instance): """List all DMS Instance topics :param instance: Either the ID of an instance or a @@ -338,8 +338,9 @@ def topics(self, instance, **kwargs): :class:`~otcextensions.sdk.dms.v1.topic.Topic` """ instance_obj = self._get_resource(_instance.Instance, instance) - return self._list(_instance.Instance, paginated=False, - instance_id=instance_obj.id, **kwargs) + return self._list(_topic.Topic, + instance_id=instance_obj.id + ) def create_topic(self, instance, **attrs): """Create a topic on DMS Instance @@ -381,7 +382,7 @@ def delete_topic(self, instance, topics, ignore_missing=True): response = self.post( '/instances/%s/topics/delete' % (instance_obj.id), - {'topics': topics_list} + json={'topics': topics_list} ) exceptions.raise_from_response(response) diff --git a/otcextensions/sdk/dms/v1/topic.py b/otcextensions/sdk/dms/v1/topic.py index 55265e28a..bf84a50cb 100644 --- a/otcextensions/sdk/dms/v1/topic.py +++ b/otcextensions/sdk/dms/v1/topic.py @@ -24,6 +24,8 @@ class Topic(_base.Resource): allow_create = True allow_delete = True +# _query_mapping = resource.QueryParameters('instance_id') + instance_id = resource.URI('instance_id') #: Properties diff --git a/otcextensions/tests/unit/osclient/dms/v1/fakes.py b/otcextensions/tests/unit/osclient/dms/v1/fakes.py index c39c547c9..5c659d6a4 100644 --- a/otcextensions/tests/unit/osclient/dms/v1/fakes.py +++ b/otcextensions/tests/unit/osclient/dms/v1/fakes.py @@ -22,6 +22,7 @@ from otcextensions.sdk.dms.v1 import instance from otcextensions.sdk.dms.v1 import queue from otcextensions.sdk.dms.v1 import quota +from otcextensions.sdk.dms.v1 import topic def gen_data(data, columns): @@ -129,3 +130,20 @@ def generate(cls): obj = instance.Instance.existing(**object_info) return obj + + +class FakeTopic(test_base.Fake): + @classmethod + def generate(cls): + object_info = { + 'id': 'id-' + uuid.uuid4().hex, + 'replication': random.randint(1, 3), + 'partition': random.randint(1, 21), + 'retention_time': random.randint(1, 169), + 'is_sync_replication': random.choice([True, False]), + 'is_sync_flush': random.choice([True, False]), + 'instance_id': 'iid-' + uuid.uuid4().hex + } + + obj = topic.Topic.existing(**object_info) + return obj diff --git a/otcextensions/tests/unit/osclient/dms/v1/test_instance.py b/otcextensions/tests/unit/osclient/dms/v1/test_instance.py index 52912d618..7e5f4d83e 100644 --- a/otcextensions/tests/unit/osclient/dms/v1/test_instance.py +++ b/otcextensions/tests/unit/osclient/dms/v1/test_instance.py @@ -58,7 +58,7 @@ def setUp(self): self.client.instances = mock.Mock() - def test_list_queue(self): + def test_list(self): arglist = [ ] @@ -81,7 +81,7 @@ def test_list_queue(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) - def test_list_queue_args(self): + def test_list_args(self): arglist = [ '--engine-name', 'engine', '--status', 'Creating', diff --git a/otcextensions/tests/unit/osclient/dms/v1/test_topic.py b/otcextensions/tests/unit/osclient/dms/v1/test_topic.py new file mode 100644 index 000000000..5efe5ae40 --- /dev/null +++ b/otcextensions/tests/unit/osclient/dms/v1/test_topic.py @@ -0,0 +1,196 @@ +# 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 mock + +from otcextensions.osclient.dms.v1 import topic +from otcextensions.tests.unit.osclient.dms.v1 import fakes + + +class TestDMSInstanceTopic(fakes.TestDMS): + + def setUp(self): + super(TestDMSInstanceTopic, self).setUp() + self.client = self.app.client_manager.dms + self.instance = fakes.FakeInstance.create_one() + self.client.find_instance = mock.Mock(return_value=self.instance) + + +class TestListDMSInstanceTopic(TestDMSInstanceTopic): + + topics = fakes.FakeTopic.create_multiple(3) + + columns = ('ID', 'replication', 'partition', 'retention_time', + 'is_sync_flush', 'is_sync_replication') + + data = [] + + for s in topics: + data.append(( + s.id, + s.replication, + s.partition, + s.retention_time, + s.is_sync_flush, + s.is_sync_replication + )) + + def setUp(self): + super(TestListDMSInstanceTopic, self).setUp() + + self.cmd = topic.ListDMSInstanceTopic(self.app, None) + + self.client.topics = mock.Mock() + + def test_list_topics(self): + arglist = [ + 'inst' + ] + + verifylist = [ + ('instance', 'inst') + ] + + # Verify cm is triggereg with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.topics.side_effect = [ + self.topics + ] + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + + self.client.find_instance.assert_called_once_with( + 'inst', ignore_missing=False) + self.client.topics.assert_called_once_with(instance=self.instance) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestDeleteDMSInstanceTopic(TestDMSInstanceTopic): + + def setUp(self): + super(TestDeleteDMSInstanceTopic, self).setUp() + + self.cmd = topic.DeleteDMSInstanceTopic(self.app, None) + + self.client.delete_topic = mock.Mock() + + def test_delete(self): + arglist = ['inst', 't1'] + verifylist = [ + ('instance', 'inst'), + ('topic', ['t1']) + ] + # Verify cm is triggereg with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.delete_instance.side_effect = [{}] + + # Trigger the action + self.cmd.take_action(parsed_args) + + calls = [mock.call(instance=self.instance, topics=['t1'])] + + self.client.find_instance.assert_called_with( + 'inst', ignore_missing=False) + self.client.delete_topic.assert_has_calls(calls) + self.assertEqual(1, self.client.delete_topic.call_count) + + def test_delete_multiple(self): + arglist = [ + 'inst', + 't1', + 't2', + ] + verifylist = [ + ('instance', 'inst'), + ('topic', ['t1', 't2']) + ] + # Verify cm is triggereg with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.delete_instance.side_effect = [{}, {}] + + # Trigger the action + self.cmd.take_action(parsed_args) + + calls = [mock.call(instance=self.instance, topics=['t1', 't2'])] + + self.client.find_instance.assert_called_with( + 'inst', ignore_missing=False) + self.client.delete_topic.assert_has_calls(calls) + self.assertEqual(1, self.client.delete_topic.call_count) + + +class TestCreateDMSInstanceTopic(TestDMSInstanceTopic): + + _data = fakes.FakeTopic.create_one() + + columns = ('id', 'is_sync_flush', 'is_sync_replication', 'partition', + 'replication', 'retention_time') + + data = fakes.gen_data(_data, columns) + + def setUp(self): + super(TestCreateDMSInstanceTopic, self).setUp() + + self.cmd = topic.CreateDMSInstanceTopic(self.app, None) + + self.client.create_topic = mock.Mock() + + def test_create_default(self): + arglist = [ + 'inst', + 'topic', + '--partition', '5', + '--replication', '3', + '--retention-time', '8', + '--enable-sync-flush', + '--enable-sync-replication' + ] + verifylist = [ + ('instance', 'inst'), + ('id', 'topic'), + ('partition', 5), + ('replication', 3), + ('retention_time', 8), + ('enable_sync_flush', True), + ('enable_sync_replication', True), + ] + # Verify cm is triggereg with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.create_topic.side_effect = [ + self._data + ] + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + + self.client.create_topic.assert_called_with( + instance=self.instance, + id='topic', + is_sync_flush=True, + is_sync_replication=True, + partition=5, + replication=3, + retention_time=8 + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py b/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py index 724c3d9ff..b255140dd 100644 --- a/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py +++ b/otcextensions/tests/unit/sdk/dms/v1/test_proxy.py @@ -290,13 +290,23 @@ def test_delete_topics(self, post_mock): post_mock.assert_called_with( '/instances/instance/topics/delete', - {'topics': ['t1', 't2']}) + json={'topics': ['t1', 't2']}) self.proxy.delete_topic('instance', 't1') post_mock.assert_called_with( '/instances/instance/topics/delete', - {'topics': ['t1']}) + json={'topics': ['t1']}) + + def test_topics(self): + self.verify_list( + self.proxy.topics, + _topic.Topic, + method_args=['iid'], + expected_kwargs={ + 'instance_id': 'iid' + } + ) # Misc def test_az(self): diff --git a/setup.cfg b/setup.cfg index 010bb4ba8..52ee7e8a6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -155,8 +155,11 @@ openstack.dms.v1 = dms_instance_list = otcextensions.osclient.dms.v1.instance:ListDMSInstance dms_instance_show = otcextensions.osclient.dms.v1.instance:ShowDMSInstance dms_instance_create = otcextensions.osclient.dms.v1.instance:CreateDMSInstance - dms_instance_update = otcextensions.osclient.dms.v1.instance:UpdateDMSInstance +# dms_instance_update = otcextensions.osclient.dms.v1.instance:UpdateDMSInstance dms_instance_restart = otcextensions.osclient.dms.v1.instance:RestartDMSInstance + dms_instance_topic_list = otcextensions.osclient.dms.v1.topic:ListDMSInstanceTopic + dms_instance_topic_create = otcextensions.osclient.dms.v1.topic:CreateDMSInstanceTopic + dms_instance_topic_delete = otcextensions.osclient.dms.v1.topic:DeleteDMSInstanceTopic dms_az_list = otcextensions.osclient.dms.v1.az:ListAZ dms_maintenance_window_list = otcextensions.osclient.dms.v1.maintenance_window:ListMW dms_product_list = otcextensions.osclient.dms.v1.product:ListProduct From 7a94e7b4c3ba3166fe65061152a1cd2e80c89aba Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 22 Apr 2020 17:20:28 +0200 Subject: [PATCH 20/24] add instance update osc --- otcextensions/osclient/dms/v1/instance.py | 73 +++++++++++++++++++ .../unit/osclient/dms/v1/test_instance.py | 69 ++++++++++++++++++ setup.cfg | 2 +- 3 files changed, 143 insertions(+), 1 deletion(-) diff --git a/otcextensions/osclient/dms/v1/instance.py b/otcextensions/osclient/dms/v1/instance.py index a403370be..152e78f42 100644 --- a/otcextensions/osclient/dms/v1/instance.py +++ b/otcextensions/osclient/dms/v1/instance.py @@ -335,6 +335,79 @@ def take_action(self, parsed_args): return (display_columns, data) +class UpdateDMSInstance(command.ShowOne): + _description = _('Update DMS Instance') + + def get_parser(self, prog_name): + parser = super(UpdateDMSInstance, self).get_parser(prog_name) + parser.add_argument( + 'instance', + metavar='', + help=_('Name or ID of the DMS instance') + ) + parser.add_argument( + '--name', + metavar='', + help=_('New name of the instance.') + ) + parser.add_argument( + '--description', + metavar='', + help=_('New description of the instance.') + ) + parser.add_argument( + '--security-group', + metavar='', + required=True, + help=_('Security group ID or Name') + ) + parser.add_argument( + '--maintenance-begin', + metavar='', + help=_('Start of the instance maintenance window') + ) + parser.add_argument( + '--maintenance-end', + metavar='', + help=_('End of the instance maintenance window') + ) + return parser + + def take_action(self, parsed_args): + + attrs = {} + + attrs['name'] = parsed_args.name + for attr in ['description', 'maintenance_begin', 'maintenance_end']: + val = getattr(parsed_args, attr) + if val is not None: + attrs[attr] = val + + sg_obj = self.app.client_manager.compute.find_security_group( + parsed_args.security_group, ignore_missing=False) + attrs['security_group_id'] = sg_obj.id + + if parsed_args.maintenance_begin and parsed_args.maintenance_end: + attrs['maintenance_begin'] = parsed_args.maintenance_begin + attrs['maintenance_end'] = parsed_args.maintenance_end + elif parsed_args.maintenance_begin or parsed_args.maintenance_end: + raise exceptions.CommandException(_( + '`maintenance_start` and `maintenance_end` can be set only' + 'together')) + + client = self.app.client_manager.dms + + instance = client.find_instance(parsed_args.instance, + ignore_missing=False) + + obj = client.update_instance(instance, **attrs) + + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + + return (display_columns, data) + + class RestartDMSInstance(command.Command): _description = _('Restart single Instance') diff --git a/otcextensions/tests/unit/osclient/dms/v1/test_instance.py b/otcextensions/tests/unit/osclient/dms/v1/test_instance.py index 7e5f4d83e..449337048 100644 --- a/otcextensions/tests/unit/osclient/dms/v1/test_instance.py +++ b/otcextensions/tests/unit/osclient/dms/v1/test_instance.py @@ -323,6 +323,75 @@ def test_create_default(self): self.assertEqual(self.data, data) +class TestUpdateDMSInstance(TestDMSInstance): + + _data = fakes.FakeInstance.create_one() + + columns = ('access_user', 'availability_zones', 'description', + 'engine_name', 'engine_version', 'is_public', 'is_ssl', + 'kafka_public_status', 'maintenance_end', 'name', 'password', + 'product_id', 'public_bandwidth', 'retention_policy', + 'router_id', 'router_name', 'security_group_id', + 'security_group_name', 'storage', 'storage_spec_code', + 'subnet_id') + + data = fakes.gen_data(_data, columns) + + def setUp(self): + super(TestUpdateDMSInstance, self).setUp() + + self.cmd = instance.UpdateDMSInstance(self.app, None) + + self.client.update_instance = mock.Mock() + self.client.find_instance = mock.Mock(return_value=self._data) + self.app.client_manager.compute = mock.Mock() + + def test_update_default(self): + arglist = [ + 'inst', + '--name', 'new_name', + '--description', 'descr', + '--security-group', 'sg_id', + '--maintenance-begin', 'mwb', + '--maintenance-end', 'mwe' + ] + verifylist = [ + ('instance', 'inst'), + ('name', 'new_name'), + ('description', 'descr'), + ('security_group', 'sg_id'), + ('maintenance_begin', 'mwb'), + ('maintenance_end', 'mwe'), + ] + # Verify cm is triggereg with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.update_instance.side_effect = [ + self._data + ] + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + + self.client.find_instance.assert_called_with( + 'inst', ignore_missing=False) + self.app.client_manager.compute.find_security_group.assert_called_with( + 'sg_id', ignore_missing=False) + + self.client.update_instance.assert_called_with( + self._data, + description='descr', + maintenance_begin='mwb', + maintenance_end='mwe', + name='new_name', + security_group_id=mock.ANY + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + class TestRestartDMSInstance(TestDMSInstance): _data = fakes.FakeInstance.create_one() diff --git a/setup.cfg b/setup.cfg index 52ee7e8a6..6f6c55d1d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -155,7 +155,7 @@ openstack.dms.v1 = dms_instance_list = otcextensions.osclient.dms.v1.instance:ListDMSInstance dms_instance_show = otcextensions.osclient.dms.v1.instance:ShowDMSInstance dms_instance_create = otcextensions.osclient.dms.v1.instance:CreateDMSInstance -# dms_instance_update = otcextensions.osclient.dms.v1.instance:UpdateDMSInstance + dms_instance_update = otcextensions.osclient.dms.v1.instance:UpdateDMSInstance dms_instance_restart = otcextensions.osclient.dms.v1.instance:RestartDMSInstance dms_instance_topic_list = otcextensions.osclient.dms.v1.topic:ListDMSInstanceTopic dms_instance_topic_create = otcextensions.osclient.dms.v1.topic:CreateDMSInstanceTopic From bb5c64b3d973bf0a95797912504a165bb54603ca Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 22 Apr 2020 17:21:56 +0200 Subject: [PATCH 21/24] rename to "dms instance set" --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 6f6c55d1d..7f4c4705e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -155,7 +155,7 @@ openstack.dms.v1 = dms_instance_list = otcextensions.osclient.dms.v1.instance:ListDMSInstance dms_instance_show = otcextensions.osclient.dms.v1.instance:ShowDMSInstance dms_instance_create = otcextensions.osclient.dms.v1.instance:CreateDMSInstance - dms_instance_update = otcextensions.osclient.dms.v1.instance:UpdateDMSInstance + dms_instance_set = otcextensions.osclient.dms.v1.instance:UpdateDMSInstance dms_instance_restart = otcextensions.osclient.dms.v1.instance:RestartDMSInstance dms_instance_topic_list = otcextensions.osclient.dms.v1.topic:ListDMSInstanceTopic dms_instance_topic_create = otcextensions.osclient.dms.v1.topic:CreateDMSInstanceTopic From 5942613bfed3e804bd195c5fb576bd213361136e Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 24 Apr 2020 09:52:51 +0200 Subject: [PATCH 22/24] rename attr to be what it really is --- otcextensions/osclient/dms/v1/instance.py | 14 +++---- otcextensions/sdk/dms/v1/instance.py | 4 +- .../tests/unit/osclient/dms/v1/fakes.py | 2 +- .../unit/osclient/dms/v1/test_instance.py | 42 +++++++++---------- .../tests/unit/sdk/dms/v1/test_instance.py | 2 +- 5 files changed, 32 insertions(+), 32 deletions(-) diff --git a/otcextensions/osclient/dms/v1/instance.py b/otcextensions/osclient/dms/v1/instance.py index 152e78f42..f60ca19fe 100644 --- a/otcextensions/osclient/dms/v1/instance.py +++ b/otcextensions/osclient/dms/v1/instance.py @@ -37,7 +37,7 @@ class ListDMSInstance(command.Lister): _description = _('List DMS Instances') columns = ('ID', 'name', 'engine_name', 'engine_version', 'storage_spec_code', 'status', 'connect_address', 'router_id', - 'security_group_id', 'subnet_id', 'user_name', 'storage', + 'network_id', 'security_group_id', 'user_name', 'storage', 'total_storage', 'used_storage') def get_parser(self, prog_name): @@ -206,10 +206,10 @@ def get_parser(self, prog_name): help=_('Security group ID or Name') ) parser.add_argument( - '--subnet', - metavar='', + '--network', + metavar='', required=True, - help=_('Subnet ID or Name') + help=_('Neutron network ID or Name') ) parser.add_argument( '--availability-zone', @@ -303,9 +303,9 @@ def take_action(self, parsed_args): router_obj = network_client.find_router(parsed_args.router, ignore_missing=False) attrs['router_id'] = router_obj.id - subnet_obj = network_client.find_subnet(parsed_args.subnet, - ignore_missing=False) - attrs['subnet_id'] = subnet_obj.id + net_obj = network_client.find_network(parsed_args.network, + ignore_missing=False) + attrs['network_id'] = net_obj.id sg_obj = self.app.client_manager.compute.find_security_group( parsed_args.security_group, ignore_missing=False) attrs['security_group_id'] = sg_obj.id diff --git a/otcextensions/sdk/dms/v1/instance.py b/otcextensions/sdk/dms/v1/instance.py index 05e0a3929..5045b5481 100644 --- a/otcextensions/sdk/dms/v1/instance.py +++ b/otcextensions/sdk/dms/v1/instance.py @@ -66,6 +66,8 @@ class Instance(_base.Resource): maintenance_start = resource.Body('maintain_begin') #: Maximum number of partitions max_partitions = resource.Body('partition_num', type=int) + #: Neutron network ID + network_id = resource.Body('subnet_id') #: User password password = resource.Body('password') #: Port number of the instance @@ -106,8 +108,6 @@ class Instance(_base.Resource): storage_type = resource.Body('storage_type') #: Storage space GB storage = resource.Body('storage_space', type=int) - #: Subnet ID - subnet_id = resource.Body('subnet_id') #: Total storage space GB total_storage = resource.Body('total_storage_space', type=int) #: Instance type diff --git a/otcextensions/tests/unit/osclient/dms/v1/fakes.py b/otcextensions/tests/unit/osclient/dms/v1/fakes.py index 5c659d6a4..dff892245 100644 --- a/otcextensions/tests/unit/osclient/dms/v1/fakes.py +++ b/otcextensions/tests/unit/osclient/dms/v1/fakes.py @@ -111,7 +111,7 @@ def generate(cls): 'password': uuid.uuid4().hex, 'router_id': 'router_id-' + uuid.uuid4().hex, 'router_name': 'router_name-' + uuid.uuid4().hex, - 'subnet_id': 'subnet_id-' + uuid.uuid4().hex, + 'network_id': 'net_id-' + uuid.uuid4().hex, 'subnet_name': 'subnet_name-' + uuid.uuid4().hex, 'security_group_id': 'security_group_id-' + uuid.uuid4().hex, 'security_group_name': 'security_group_name-' + uuid.uuid4().hex, diff --git a/otcextensions/tests/unit/osclient/dms/v1/test_instance.py b/otcextensions/tests/unit/osclient/dms/v1/test_instance.py index 449337048..f7f061099 100644 --- a/otcextensions/tests/unit/osclient/dms/v1/test_instance.py +++ b/otcextensions/tests/unit/osclient/dms/v1/test_instance.py @@ -27,9 +27,9 @@ class TestListDMSInstance(TestDMSInstance): instances = fakes.FakeInstance.create_multiple(3) columns = ('ID', 'name', 'engine_name', 'engine_version', - 'storage_spec_code', 'status', 'connect_address', 'router_id', - 'security_group_id', 'subnet_id', 'user_name', 'storage', - 'total_storage', 'used_storage') + 'storage_spec_code', 'status', 'connect_address', + 'router_id', 'network_id', 'security_group_id', + 'user_name', 'storage', 'total_storage', 'used_storage') data = [] @@ -43,8 +43,8 @@ class TestListDMSInstance(TestDMSInstance): s.status, s.connect_address, s.router_id, + s.network_id, s.security_group_id, - s.subnet_id, s.user_name, s.storage, s.total_storage, @@ -120,11 +120,11 @@ class TestShowDMSInstance(TestDMSInstance): columns = ('access_user', 'availability_zones', 'description', 'engine_name', 'engine_version', 'is_public', 'is_ssl', - 'kafka_public_status', 'maintenance_end', 'name', 'password', - 'product_id', 'public_bandwidth', 'retention_policy', - 'router_id', 'router_name', 'security_group_id', - 'security_group_name', 'storage', 'storage_spec_code', - 'subnet_id') + 'kafka_public_status', 'maintenance_end', 'name', 'network_id', + 'password', 'product_id', 'public_bandwidth', + 'retention_policy', 'router_id', 'router_name', + 'security_group_id', + 'security_group_name', 'storage', 'storage_spec_code') data = fakes.gen_data(_data, columns) @@ -216,11 +216,11 @@ class TestCreateDMSInstance(TestDMSInstance): columns = ('access_user', 'availability_zones', 'description', 'engine_name', 'engine_version', 'is_public', 'is_ssl', - 'kafka_public_status', 'maintenance_end', 'name', 'password', + 'kafka_public_status', 'maintenance_end', 'name', 'network_id', + 'password', 'product_id', 'public_bandwidth', 'retention_policy', 'router_id', 'router_name', 'security_group_id', - 'security_group_name', 'storage', 'storage_spec_code', - 'subnet_id') + 'security_group_name', 'storage', 'storage_spec_code') data = fakes.gen_data(_data, columns) @@ -232,7 +232,7 @@ def setUp(self): self.client.create_instance = mock.Mock() self.app.client_manager.network = mock.Mock() self.app.client_manager.network.find_router = mock.Mock() - self.app.client_manager.network.find_subnet = mock.Mock() + self.app.client_manager.network.find_network = mock.Mock() self.app.client_manager.compute = mock.Mock() def test_create_default(self): @@ -246,7 +246,7 @@ def test_create_default(self): '--password', 'pwd', '--router', 'router_id', '--security-group', 'sg_id', - '--subnet', 'subnet_id', + '--network', 'net_id', '--availability-zone', 'az1', '--availability-zone', 'az2', '--product-id', 'pid', @@ -268,7 +268,7 @@ def test_create_default(self): ('password', 'pwd'), ('router', 'router_id'), ('security_group', 'sg_id'), - ('subnet', 'subnet_id'), + ('network', 'net_id'), ('availability_zone', ['az1', 'az2']), ('product_id', 'pid'), ('maintenance_begin', 'mwb'), @@ -292,8 +292,8 @@ def test_create_default(self): self.app.client_manager.network.find_router.assert_called_with( 'router_id', ignore_missing=False) - self.app.client_manager.network.find_subnet.assert_called_with( - 'subnet_id', ignore_missing=False) + self.app.client_manager.network.find_network.assert_called_with( + 'net_id', ignore_missing=False) self.app.client_manager.compute.find_security_group.assert_called_with( 'sg_id', ignore_missing=False) @@ -316,7 +316,7 @@ def test_create_default(self): security_group_id=mock.ANY, storage=15, storage_spec_code='dms.physical.storage.high', - subnet_id=mock.ANY + network_id=mock.ANY ) self.assertEqual(self.columns, columns) @@ -329,11 +329,11 @@ class TestUpdateDMSInstance(TestDMSInstance): columns = ('access_user', 'availability_zones', 'description', 'engine_name', 'engine_version', 'is_public', 'is_ssl', - 'kafka_public_status', 'maintenance_end', 'name', 'password', + 'kafka_public_status', 'maintenance_end', 'name', 'network_id', + 'password', 'product_id', 'public_bandwidth', 'retention_policy', 'router_id', 'router_name', 'security_group_id', - 'security_group_name', 'storage', 'storage_spec_code', - 'subnet_id') + 'security_group_name', 'storage', 'storage_spec_code') data = fakes.gen_data(_data, columns) diff --git a/otcextensions/tests/unit/sdk/dms/v1/test_instance.py b/otcextensions/tests/unit/sdk/dms/v1/test_instance.py index b55b913b9..2b5ac40b5 100644 --- a/otcextensions/tests/unit/sdk/dms/v1/test_instance.py +++ b/otcextensions/tests/unit/sdk/dms/v1/test_instance.py @@ -141,7 +141,7 @@ def test_make_it(self): self.assertEqual(JSON_DATA.get('storage_space', None), sot.storage) self.assertEqual(JSON_DATA.get('subnet_id', None), - sot.subnet_id) + sot.network_id) self.assertEqual(JSON_DATA.get('total_storage_space', None), sot.total_storage) self.assertEqual(JSON_DATA.get('type', None), From afb1424a91f9e2415e558b00870fa741c71054d1 Mon Sep 17 00:00:00 2001 From: Vladimir Hasko Date: Mon, 27 Apr 2020 14:00:27 +0200 Subject: [PATCH 23/24] Adding basic functional test for DMS instance (#80) Adding basic functional test for DMS instance Reviewed-by: https://github.com/apps/otc-zuul --- .../functional/sdk/dms/v1/test_instance.py | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 otcextensions/tests/functional/sdk/dms/v1/test_instance.py diff --git a/otcextensions/tests/functional/sdk/dms/v1/test_instance.py b/otcextensions/tests/functional/sdk/dms/v1/test_instance.py new file mode 100644 index 000000000..524f5a54a --- /dev/null +++ b/otcextensions/tests/functional/sdk/dms/v1/test_instance.py @@ -0,0 +1,160 @@ +# 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 openstack +from openstack import exceptions +from openstack import resource + + +from otcextensions.tests.functional import base +import uuid +import random + +_logger = openstack._log.setup_logging('openstack') + + +class TestInstance(base.BaseFunctionalTest): + UUID = uuid.uuid4().hex[:8] + INSTANCE_NAME = 'sdk-test-dms-' + UUID + ROUTER_NAME = 'sdk-test-router-' + UUID + NET_NAME = 'sdk-test-net-' + UUID + SUBNET_NAME = 'sdk-test-subnet-' + UUID + SG_NAME = 'sdk-test-sg-' + UUID + ROUTER_ID = None + NET_ID = None + SUBNET_ID = None + SG_ID = None + instances = [] + + def setUp(self): + super(TestInstance, self).setUp() + self._initialize_network() + products = list(self.conn.dms.products()) + products_ids = [product.product_id for product in products] + product_id = random.choice(products_ids) + availability_zone_names = [ + product.availability_zones for product in products + if product_id == product.product_id + ] + availability_zone_name = random.choice( + sum(availability_zone_names, []) + ) + availability_zone_id = [ + availability_zone.id + for availability_zone in self.conn.dms.availability_zones() + if availability_zone_name == availability_zone.code + ] + try: + self.instance = self.conn.dms.create_instance( + name=self.INSTANCE_NAME, + engine="kafka", engine_version="2.3.0", storage=4800, + router_id=self.ROUTER_ID, network_id=self.NET_ID, + security_group_id=self.SG_ID, + availability_zones=availability_zone_id, + product_id=product_id, + storage_spec_code="dms.physical.storage.ultra" + ) + except exceptions.DuplicateResource: + self.instance = self.conn.dms.find_instance( + alias=self.INSTANCE_NAME + ) + resource.wait_for_status( + session=self.conn.dms, + resource=self.instance, + status='RUNNING', + failures=['CREATEFAILED'], + interval=5, + wait=900) + self.assertIn('instance_id', self.instance) + self.instances.append(self.instance) + + def tearDown(self): + super(TestInstance, self).tearDown() + try: + for instance in self.instances: + if instance.id: + self.conn.dms.delete_instance(instance) + resource.wait_for_delete( + session=self.conn.dms, + resource=instance, + interval=2, + wait=60) + except openstack.exceptions.SDKException as e: + _logger.warning('Got exception during clearing resources %s' + % e.message) + self._deinitialize_network() + + def test_list(self): + self.all_instances = list(self.conn.dms.instances()) + self.assertGreaterEqual(len(self.all_instances), 0) + if len(self.all_instances) > 0: + instance = self.all_instances[0] + instance = self.conn.dms.get_instance(instance=instance.id) + self.assertIsNotNone(instance) + + def _initialize_network(self): + self.cidr = '192.168.0.0/16' + self.ipv4 = 4 + + sg = self.conn.network.create_security_group(name=self.SG_NAME) + self.assertEqual(self.SG_NAME, sg.name) + TestInstance.SG_ID = sg.id + + network = self.conn.network.create_network(name=self.NET_NAME) + self.assertEqual(self.NET_NAME, network.name) + TestInstance.NET_ID = network.id + + subnet = self.conn.network.create_subnet( + name=self.SUBNET_NAME, + ip_version=self.ipv4, + network_id=TestInstance.NET_ID, + cidr=self.cidr + ) + self.assertEqual(self.SUBNET_NAME, subnet.name) + TestInstance.SUBNET_ID = subnet.id + + router = self.conn.network.create_router(name=self.ROUTER_NAME) + self.assertEqual(self.ROUTER_NAME, router.name) + TestInstance.ROUTER_ID = router.id + interface = router.add_interface(self.conn.network, + subnet_id=TestInstance.SUBNET_ID) + self.assertEqual(interface['subnet_id'], TestInstance.SUBNET_ID) + self.assertIn('port_id', interface) + # add CIDR to Router for compatibility + service = self.conn.identity.find_service('vpc') + endpoints = list(self.conn.identity.endpoints(service_id=service.id)) + endpoint_url = endpoints[0].url + endpoint = endpoint_url[:endpoint_url.rfind("/") + 1] + cidr = self.conn.compute.put( + endpoint + self.conn.session.get_project_id() + '/vpcs/' + + self.ROUTER_ID, data='{"vpc": {"cidr": "192.168.0.0/16"}}', + headers={'content-type': 'application/json'}) + self.assertIn('cidr', cidr.json()['vpc']) + self.assertIn('192.168.0.0/16', cidr.json()['vpc']['cidr']) + + def _deinitialize_network(self): + router = self.conn.network.get_router(TestInstance.ROUTER_ID) + interface = router.remove_interface(self.conn.network, + subnet_id=TestInstance.SUBNET_ID) + self.assertEqual(interface['subnet_id'], TestInstance.SUBNET_ID) + self.assertIn('port_id', interface) + sot = self.conn.network.delete_router( + TestInstance.ROUTER_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_subnet( + TestInstance.SUBNET_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_network( + TestInstance.NET_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_security_group( + TestInstance.SG_ID, ignore_missing=False) + self.assertIsNone(sot) From b2da5d5d6fb3e699c6889becacf8e4a9c2a4d05a Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 4 May 2020 08:30:32 +0200 Subject: [PATCH 24/24] fix missed review comments --- otcextensions/sdk/dms/v1/_proxy.py | 2 +- otcextensions/sdk/dms/v1/message.py | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/otcextensions/sdk/dms/v1/_proxy.py b/otcextensions/sdk/dms/v1/_proxy.py index a614784cb..601257f8c 100644 --- a/otcextensions/sdk/dms/v1/_proxy.py +++ b/otcextensions/sdk/dms/v1/_proxy.py @@ -194,7 +194,7 @@ def consume_message(self, queue, group, **query): :param queue: The queue id or an instance of :class:`~otcextensions.sdk.dms.v1.queue.Queue` - :param consume_group: The consume group id or an instance of + :param group: The consume group id or an instance of :class:`~otcextensions.sdk.dms.v1.group.Group` :param kwargs query: Optional query parameters to be sent to limit the resources being returned. diff --git a/otcextensions/sdk/dms/v1/message.py b/otcextensions/sdk/dms/v1/message.py index 318f06d82..eaa7b3d48 100644 --- a/otcextensions/sdk/dms/v1/message.py +++ b/otcextensions/sdk/dms/v1/message.py @@ -68,12 +68,6 @@ def list(cls, session, paginated=True, base_path=None, query_params = cls._query_mapping._transpose(params, cls) uri = base_path % params - params = cls._query_mapping._validate( - params, base_path=base_path, - allow_unknown_params=allow_unknown_params) - query_params = cls._query_mapping._transpose(params, cls) - uri = base_path % params - while uri: # Copy query_params due to weird mock unittest interactions response = session.get(