From 99e6e2892f0034556ac3e9323d7dacba563d3bba Mon Sep 17 00:00:00 2001 From: Arbel Nathan Date: Mon, 25 May 2020 12:26:15 +0300 Subject: [PATCH] Feature/csi 1451 create flashcopy (#7) * add first attempt for create flashcopy method add flashcopy creation options to types.py * flashcopy -> flashcopies * add cs to url * return all post command * return all post command * adding log for url * adding log for url * delete log * try to debug * try to debug * try to debug * try going against the wiki * try going against the wiki * try going against the wiki add posta to flashcopy * try going against the wiki * testing * testing without rebuild * testing with rebuild * testing cs/flashcopy * testing v1/flashcopeis * testing v1/flashcopy * testing v1/flashcopy * testing global url * testing global url * testing global url * testing global url * testing global url * testing global url * testing global url * testing set_base_url method * remove posta from volumes.py * add flashcopies.py * edit flashcopies.py * disable pprcs.py * disable volumemixin * disable pprcs.py * able pprcs.py * flashcopies.py to flashcopy * flashcopies.py to flashcopy * flashcopies.py to flashcopy * testing manager * pdb * create_flashcopy to sc_client.py * edit flashcopies.py * edit init * add posta to flashcopies.py * remove pdb and global params * add delete and unit test for create * testing * add __init__.py * write in __init__.py files * delete content in __init__.py files and add __init__.py * add __init__.py * add __init__.py * remove __init__.py * try change import relations * changed create_flashcopy input add unit tests - pass * trying options * testing * flashcopy to flashcopies * // * get and list for flashcopies.py * pdb add * test change response from flashcopies get * add if to _add_details * test get_flashcopies_by_volume * revert get_flashcopies_by_volume method * test options * options - list and singular * options - list and singular fix * options - list and singular * no singular option is allowed * options - list and singular * cleanup fo PR * check tests * check tests * check tests fix route * // * added flashcopies.py ALL ONE * get rid of bug * flake8 fixes * flake8 fixes * flake8 fixes * get rid of unnecessary code * PR fixes * fix * get rid of unnecessary imports * all flashcopy stay as it was, only cs_flashcopies change (added) * change if statement, PR * related_resource change * fix lines * get rid of unnecessary code Co-authored-by: ArbelNathan --- pyds8k/client/ds8k/v1/sc_client.py | 4 + pyds8k/resources/__init__.py | 0 pyds8k/resources/ds8k/v1/__init__.py | 3 +- pyds8k/resources/ds8k/v1/common/mixins.py | 83 +++++++-- pyds8k/resources/ds8k/v1/common/types.py | 23 +++ pyds8k/resources/ds8k/v1/cs/flashcopies.py | 70 ++++++++ pyds8k/test/__init__.py | 0 pyds8k/test/data.py | 165 ++++++++++-------- pyds8k/test/integration/__init__.py | 0 pyds8k/test/mock/flashcopies.py | 119 +++++++++++++ pyds8k/test/test_client/test_ds8k/__init__.py | 0 .../test_client/test_ds8k/test_sc_client.py | 7 + pyds8k/test/test_dataParser/__init__.py | 0 .../test_ds8k/test_flashcopies.py | 108 ++++++++++++ .../test_resources/test_ds8k/test_volume.py | 29 +-- 15 files changed, 506 insertions(+), 105 deletions(-) create mode 100644 pyds8k/resources/__init__.py create mode 100644 pyds8k/resources/ds8k/v1/cs/flashcopies.py create mode 100644 pyds8k/test/__init__.py create mode 100644 pyds8k/test/integration/__init__.py create mode 100644 pyds8k/test/mock/flashcopies.py create mode 100644 pyds8k/test/test_client/test_ds8k/__init__.py create mode 100644 pyds8k/test/test_dataParser/__init__.py create mode 100644 pyds8k/test/test_resources/test_ds8k/test_flashcopies.py diff --git a/pyds8k/client/ds8k/v1/sc_client.py b/pyds8k/client/ds8k/v1/sc_client.py index 44ea2a0..04414df 100755 --- a/pyds8k/client/ds8k/v1/sc_client.py +++ b/pyds8k/client/ds8k/v1/sc_client.py @@ -90,6 +90,10 @@ def list_extentpool_virtualpool(self, pool_id): def list_flashcopies(self): return self.client.get_flashcopies() + @dictionarize + def list_cs_flashcopies(self): + return self.client.get_cs_flashcopies() + @dictionarize def list_volume_flashcopies(self, volume_id): # two requests diff --git a/pyds8k/resources/__init__.py b/pyds8k/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyds8k/resources/ds8k/v1/__init__.py b/pyds8k/resources/ds8k/v1/__init__.py index c125f61..6ef8f0b 100755 --- a/pyds8k/resources/ds8k/v1/__init__.py +++ b/pyds8k/resources/ds8k/v1/__init__.py @@ -17,11 +17,12 @@ from . import ioports, flashcopy, events, mappings, pprc, eserep, \ users, systems, nodes, marrays, encryption_groups, io_enclosures, \ pools, tserep, lss, volumes, host_ports, hosts -from .cs import pprcs +from .cs import pprcs, flashcopies __all__ = ( 'ioports', 'flashcopy', + 'flashcopies', 'events', 'mappings', 'pprc', diff --git a/pyds8k/resources/ds8k/v1/common/mixins.py b/pyds8k/resources/ds8k/v1/common/mixins.py index 9223a55..b035795 100755 --- a/pyds8k/resources/ds8k/v1/common/mixins.py +++ b/pyds8k/resources/ds8k/v1/common/mixins.py @@ -27,6 +27,7 @@ class RootBaseMixin(object): pass + # Note: the format of all the get list methods should be: # get_{right type in types} # the format of all the get single methods should be: @@ -134,8 +135,8 @@ def update_tserep_cap_by_pool(self, pool_id, cap, captype=''): pool_id, rebuild_url=True ).all( - types.DS8K_TSEREP - ).update({'cap': cap, 'captype': captype}) + types.DS8K_TSEREP + ).update({'cap': cap, 'captype': captype}) return res def update_eserep_cap_by_pool(self, pool_id, cap, captype=''): @@ -143,8 +144,8 @@ def update_eserep_cap_by_pool(self, pool_id, cap, captype=''): pool_id, rebuild_url=True ).all( - types.DS8K_ESEREP - ).update({'cap': cap, 'captype': captype}) + types.DS8K_ESEREP + ).update({'cap': cap, 'captype': captype}) return res def update_tserep_threshold_by_pool(self, pool_id, threshold): @@ -152,8 +153,8 @@ def update_tserep_threshold_by_pool(self, pool_id, threshold): pool_id, rebuild_url=True ).all( - types.DS8K_TSEREP - ).update({'threshold': threshold}) + types.DS8K_TSEREP + ).update({'threshold': threshold}) return res def update_eserep_threshold_by_pool(self, pool_id, threshold): @@ -161,8 +162,8 @@ def update_eserep_threshold_by_pool(self, pool_id, threshold): pool_id, rebuild_url=True ).all( - types.DS8K_ESEREP - ).update({'threshold': threshold}) + types.DS8K_ESEREP + ).update({'threshold': threshold}) return res def get_volumes_by_pool(self, pool_id): @@ -272,8 +273,8 @@ def update_volume_extend(self, volume_id, new_size, captype=''): _, res = self.one(types.DS8K_VOLUME, volume_id, rebuild_url=True).update( - {'cap': new_size, 'captype': captype} - ) + {'cap': new_size, 'captype': captype} + ) return res def update_volume_move(self, volume_id, new_pool): @@ -392,8 +393,8 @@ def map_volume_to_host(self, host_name, volume_id, lunid=''): host_name, rebuild_url=True ).all(types.DS8K_VOLMAP).posta( - post_data - ) + post_data + ) return res def unmap_volume_from_host(self, host_name, lunid): @@ -414,8 +415,8 @@ def get_lss(self, lss_id=None, lss_type=''): raise ValueError( INVALID_TYPE.format( ', '.join(types.DS8K_VOLUME_TYPES) - ) ) + ) return self.all(types.DS8K_LSS, rebuild_url=True).list( params={'type': lss_type} ) @@ -444,6 +445,42 @@ def get_flashcopies_by_volume(self, volume_id): volume_id, rebuild_url=True).all(types.DS8K_FLASHCOPY).list() + def get_cs_flashcopies(self, fcid=None): + return self.get_cs_flashcopy(fcid) + + def get_cs_flashcopy(self, fcid=None): + if fcid: + return self.one('{}.{}'.format( + types.DS8K_COPY_SERVICE_PREFIX, + types.DS8K_CS_FLASHCOPY), fcid, rebuild_url=True).get() + return self.all('{}.{}'.format( + types.DS8K_COPY_SERVICE_PREFIX, + types.DS8K_CS_FLASHCOPY), rebuild_url=True).list() + + def create_cs_flashcopy(self, volume_pairs, options=[]): + """ + :param volume_pairs: [{"source_volume": 0000,"target_volume": 1100},..] + :param options: + :return: + """ + for option in options: + self._verify_type(option, types.DS8K_FC_OPTIONS) + _, res = self.all('{}.{}'.format( + types.DS8K_COPY_SERVICE_PREFIX, + types.DS8K_CS_FLASHCOPY), + rebuild_url=True).posta({"volume_pairs": volume_pairs, + "options": options + }) + return res + + def delete_cs_flashcopy(self, flashcopy_id): + _, res = self.one('{}.{}'.format( + types.DS8K_COPY_SERVICE_PREFIX, + types.DS8K_CS_FLASHCOPY), + flashcopy_id, + rebuild_url=True).delete() + return res + class RootPPRCMixin(object): def get_pprc(self, pprc_id=None): @@ -505,7 +542,7 @@ def get_events_by_filter(self, if not isinstance(v, datetime): raise InvalidArgumentError( 'before/after must be an datetime instance.' - ) + ) dttz = datetime(year=v.year, month=v.month, day=v.day, @@ -607,6 +644,24 @@ def get_flashcopy(self, fcid=None): self._stop_updating() return flashcopies + def get_cs_flashcopies(self, fcid=None): + return self.get_cs_flashcopy(fcid) + + def get_cs_flashcopy(self, fcid=None): + if not self.id: + raise IDMissingError() + if fcid: + return self.one('{}.{}'.format( + types.DS8K_COPY_SERVICE_PREFIX, + types.DS8K_CS_FLASHCOPY), fcid).get() + flashcopies = self.all('{}.{}'.format( + types.DS8K_COPY_SERVICE_PREFIX, + types.DS8K_CS_FLASHCOPY)).list() + self._start_updating() + setattr(self, types.DS8K_CS_FLASHCOPY, flashcopies) + self._stop_updating() + return flashcopies + class PPRCMixin(object): def get_pprc(self, pprc_id=None): diff --git a/pyds8k/resources/ds8k/v1/common/types.py b/pyds8k/resources/ds8k/v1/common/types.py index 387a54f..89ee87e 100755 --- a/pyds8k/resources/ds8k/v1/common/types.py +++ b/pyds8k/resources/ds8k/v1/common/types.py @@ -54,3 +54,26 @@ DS8K_COPY_SERVICE_PREFIX = 'cs' DS8K_CS_PPRC = 'pprcs' +DS8K_CS_FLASHCOPY = 'flashcopies' + +DS8K_OPTION_FRCO = "freeze_consistency" +DS8K_OPTION_ITW = "inhibit_target_writes" +DS8K_OPTION_RECH = "record_changes" +DS8K_OPTION_NBC = "no_background_copy" +DS8K_OPTION_PER = "persistent" +DS8K_OPTION_APTP = "allow_pprc_target_primary" +DS8K_OPTION_RERE = "reverse_restore" +DS8K_OPTION_FRR = "fast_reverse_restore" +DS8K_OPTION_PSET = "permit_space_efficient_target" +DS8K_OPTION_FSETOOS = "fail_space_efficient_target_out_of_space" + +DS8K_FC_OPTIONS = (DS8K_OPTION_FRCO, + DS8K_OPTION_ITW, + DS8K_OPTION_RECH, + DS8K_OPTION_NBC, + DS8K_OPTION_PER, + DS8K_OPTION_APTP, + DS8K_OPTION_RERE, + DS8K_OPTION_FRR, + DS8K_OPTION_PSET, + DS8K_OPTION_FSETOOS) diff --git a/pyds8k/resources/ds8k/v1/cs/flashcopies.py b/pyds8k/resources/ds8k/v1/cs/flashcopies.py new file mode 100644 index 0000000..0e30a2c --- /dev/null +++ b/pyds8k/resources/ds8k/v1/cs/flashcopies.py @@ -0,0 +1,70 @@ +############################################################################## +# Copyright 2019 IBM Corp. +# +# 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. +############################################################################## + +""" +advanced FlashCopies interface. +""" +from pyds8k.base import ManagerMeta, ResourceMeta +from ..common.base import Base, BaseManager +from ..common.types import DS8K_CS_FLASHCOPY, DS8K_FLASHCOPY +from ..volumes import Volume, VolumeManager + + +class FlashCopy(Base, metaclass=ResourceMeta): + resource_type = DS8K_CS_FLASHCOPY + _template = {'id': None, + 'persistent': None, + 'recording': None, + 'backgroundcopy': None, + 'state': None, + 'options': [], + 'volume_pairs': [] + } + + related_resource = {'_volume_pairs': [{ + 'source_volume': (Volume, VolumeManager), + 'target_volume': (Volume, VolumeManager) + }] + } + + def __repr__(self): + return "".format(self._get_id()) + + def _add_details(self, info, force=False): + super(FlashCopy, self)._add_details(info, force=force) + if DS8K_FLASHCOPY in info: + self._id = info[DS8K_FLASHCOPY][0]['id'] + + +class FlashCopyManager(BaseManager, metaclass=ManagerMeta): + """ + Manage advanced FlashCopies resources. + """ + resource_class = FlashCopy + resource_type = DS8K_CS_FLASHCOPY + + def get(self, resource_id='', url='', obj_class=None, **kwargs): + return self._get(resource_id=resource_id, url=url, + obj_class=obj_class, **kwargs) + + def list(self, url='', obj_class=None, body=None, **kwargs): + return self._list(url=url, obj_class=obj_class, body=body, **kwargs) + + def posta(self, url='', body=None): + return self._posta(url=url, body=body) + + def delete(self, url=''): + return self._delete(url=url) diff --git a/pyds8k/test/__init__.py b/pyds8k/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyds8k/test/data.py b/pyds8k/test/data.py index 0daa6f6..3ac779c 100755 --- a/pyds8k/test/data.py +++ b/pyds8k/test/data.py @@ -60,37 +60,37 @@ def get_request_json_body(body): # actions fail/success response action_response = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, + "server": { + "status": "ok", + "code": "", + "message": "Operation done successfully." + }, } action_response_json = json.dumps(action_response) action_response_failed = { - "server": { - "status": "failed", - "code": "888", - "message": "Operation done unsuccessfully." - }, + "server": { + "status": "failed", + "code": "888", + "message": "Operation done unsuccessfully." + }, } action_response_failed_json = json.dumps(action_response_failed) _delete_response = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, + "server": { + "status": "ok", + "code": "", + "message": "Operation done successfully." + }, } _delete_response_json = json.dumps(_delete_response) _put_post_response = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, + "server": { + "status": "ok", + "code": "", + "message": "Operation done successfully." + }, } _put_post_response_json = json.dumps(_put_post_response) @@ -105,34 +105,34 @@ def get_request_json_body(body): "status": "ok", "code": "", "message": "Operation done successfully." + }, + "data": { + "volumes": [{"name": "lou_test1", + "id": "0010" + } + ] }, - "data": { - "volumes": [{"name": "lou_test1", - "id": "0010" - } - ] - }, - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/volumes/0010" + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/volumes/0010" } - }, + }, {"server": { "status": "ok", "code": "", "message": "Operation done successfully." + }, + "data": { + "volumes": [{"name": "lou_test2", + "id": "0011" + } + ] }, - "data": { - "volumes": [{"name": "lou_test2", - "id": "0011" - } - ] - }, - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/volumes/0011" + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/volumes/0011" } - }, + }, ] } create_volumes_response_json = json.dumps(create_volumes_response) @@ -148,24 +148,24 @@ def get_request_json_body(body): "status": "ok", "code": "", "message": "Operation done successfully." + }, + "data": { + "volumes": [{"name": "lou_test1", + "id": "0010" + } + ] }, - "data": { - "volumes": [{"name": "lou_test1", - "id": "0010" - } - ] - }, - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/volumes/0010" + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/volumes/0010" } - }, + }, {"server": { "status": "failed", "code": "error_code", "message": "something wrong" - }, - }, + }, + }, ] } create_volumes_partial_failed_response_json = json.dumps( @@ -186,7 +186,7 @@ def get_request_json_body(body): "id": "0010" } ] - }, + }, "link": { "rel": "self", "href": "https://localhost:8088/api/v1/volumes/0010" @@ -205,22 +205,22 @@ def get_request_json_body(body): "status": "ok", "code": "", "message": "Operation done successfully." - }, - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/hosts/host1/mappings/00" + }, + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/hosts/host1/mappings/00" } - }, + }, {"server": { "status": "ok", "code": "", "message": "Operation done successfully." - }, - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/hosts/host1/mappings/01" + }, + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/hosts/host1/mappings/01" } - }, + }, ] } create_mappings_response_json = json.dumps(create_mappings_response) @@ -264,7 +264,6 @@ def get_request_json_body(body): } create_host_port_response_json = json.dumps(create_host_port_response) - # Templates volume_template = {'name': 'vol1', 'type': 'fb', @@ -282,22 +281,34 @@ def get_request_json_body(body): 'pool_id': 'P0', } - # fail response token_response_error = { - "server": { - "status": "failed", - "code": "NIServerException", - "message": "Operation done successfully." - } + "server": { + "status": "failed", + "code": "NIServerException", + "message": "Operation done successfully." + } } - # request default_request = { - 'request': { - 'params': { - 'param1': 'test', - } - } - } + 'request': { + 'params': { + 'param1': 'test', + } + } +} + +create_flashcopy_response = { + 'server': { + 'status': 'ok', + 'code': '', + 'message': 'Operation done successfully.' + }, + 'link': { + 'rel': 'self', + 'href': 'https:/9.151.159.203:8452/api/v1/cs/flashcopies/0000:0001' + } +} + +create_flashcopy_response_json = json.dumps(create_flashcopy_response) diff --git a/pyds8k/test/integration/__init__.py b/pyds8k/test/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyds8k/test/mock/flashcopies.py b/pyds8k/test/mock/flashcopies.py new file mode 100644 index 0000000..11a3a9c --- /dev/null +++ b/pyds8k/test/mock/flashcopies.py @@ -0,0 +1,119 @@ +############################################################################## +# Copyright 2019 IBM Corp. +# +# 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. +############################################################################## + +ALL = { + "server": { + "status": "ok", + "code": "CMUC00183I", + "message": "Operation done successfully." + }, + "counts": { + "data_counts": 32, + "total_counts": 32 + }, + 'data': { + 'flashcopy': [ + { + 'id': '0000:0001', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/cs/flashcopies/' + '0000:0001' + }, + 'source_volume': { + 'id': '0000', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/volumes/0000' + } + }, + 'target_volume': { + 'id': '0001', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/volumes/0001' + } + }, + 'out_of_sync_tracks': '0', + 'state': 'valid' + }, + { + 'id': '1000:1001', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/cs/flashcopies/' + '1000:1001' + }, + 'source_volume': { + 'id': '1000', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/volumes/1000' + } + }, + 'target_volume': { + 'id': '1001', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/volumes/1001' + } + }, + 'out_of_sync_tracks': '0', + 'state': 'valid' + }, + ] + } +} + +ONE = { + "server": { + "status": "ok", + "code": "CMUC00183I", + "message": "Operation done successfully." + }, + "counts": { + "data_counts": 1, + "total_counts": 1 + }, + 'data': { + 'flashcopy': [ + { + 'id': '0000:0001', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/cs/flashcopies/' + '0000:0001' + }, + 'source_volume': { + 'id': '0000', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/volumes/0000' + } + }, + 'target_volume': { + 'id': '0001', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/volumes/0001' + } + }, + 'out_of_sync_tracks': '0', + 'state': 'valid' + }, + ] + } +} diff --git a/pyds8k/test/test_client/test_ds8k/__init__.py b/pyds8k/test/test_client/test_ds8k/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyds8k/test/test_client/test_ds8k/test_sc_client.py b/pyds8k/test/test_client/test_ds8k/test_sc_client.py index 3556aff..677add8 100644 --- a/pyds8k/test/test_client/test_ds8k/test_sc_client.py +++ b/pyds8k/test/test_client/test_ds8k/test_sc_client.py @@ -216,6 +216,13 @@ def test_list_flashcopies(self): 'list_flashcopies' ) + def test_list_cs_flashcopies(self): + self._test_resource_list_by_route('{}.{}'.format( + types.DS8K_COPY_SERVICE_PREFIX, + types.DS8K_CS_FLASHCOPY), + 'list_cs_flashcopies' + ) + def test_list_volume_flashcopies(self): self._test_sub_resource(types.DS8K_VOLUME, types.DS8K_FLASHCOPY, diff --git a/pyds8k/test/test_dataParser/__init__.py b/pyds8k/test/test_dataParser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyds8k/test/test_resources/test_ds8k/test_flashcopies.py b/pyds8k/test/test_resources/test_ds8k/test_flashcopies.py new file mode 100644 index 0000000..e41295c --- /dev/null +++ b/pyds8k/test/test_resources/test_ds8k/test_flashcopies.py @@ -0,0 +1,108 @@ +import json + +import httpretty + +from pyds8k.dataParser.ds8k import RequestParser +from pyds8k.resources.ds8k.v1.common.types import DS8K_CS_FLASHCOPY, \ + DS8K_COPY_SERVICE_PREFIX, DS8K_FLASHCOPY +from pyds8k.resources.ds8k.v1.cs.flashcopies import FlashCopy as FlashCopies +from pyds8k.resources.ds8k.v1.flashcopy import FlashCopy +from pyds8k.test.data import get_response_json_by_type, \ + get_response_data_by_type, action_response_json, \ + create_flashcopy_response_json +from pyds8k.test.test_resources.test_ds8k.base import TestDS8KWithConnect + + +class TestFlashCopies(TestDS8KWithConnect): + + def setUp(self): + super(TestFlashCopies, self).setUp() + self.maxDiff = None + + @httpretty.activate + def test_create_cs_flashcopy(self): + url = '/cs/flashcopies' + + source_volume = '0000' + target_volume = '0001' + + def _verify_request(request, uri, headers): + self.assertEqual(uri, self.domain + self.base_url + url) + + req = RequestParser( + {"volume_pairs": [{"source_volume": source_volume, + "target_volume": target_volume + }], + "options": [] + }) + self.assertDictContainsSubset( + req.get_request_data().get('request').get('params'), + json.loads(request.body).get('request').get('params'), + ) + return (201, headers, create_flashcopy_response_json) + + httpretty.register_uri(httpretty.POST, + self.domain + self.base_url + url, + body=_verify_request, + content_type='application/json', + ) + # Way 1 + resp1 = self.system.create_cs_flashcopy( + volume_pairs=[{'source_volume': source_volume, + 'target_volume': target_volume}]) + self.assertEqual(httpretty.POST, httpretty.last_request().method) + self.assertIsInstance(resp1[0], FlashCopies) + + # Way 2 + flashcopies = self.system.all( + '{}.{}'.format(DS8K_COPY_SERVICE_PREFIX, DS8K_CS_FLASHCOPY), + rebuild_url=True) + new_fc2 = flashcopies.create(volume_pairs=[ + {'source_volume': source_volume, 'target_volume': target_volume}]) + resp2, data2 = new_fc2.posta() + self.assertEqual(httpretty.POST, httpretty.last_request().method) + self.assertIsInstance(data2[0], FlashCopies) + self.assertEqual(resp2.status_code, 201) + + # Way 3 + flashcopies = self.system.all( + '{}.{}'.format(DS8K_COPY_SERVICE_PREFIX, DS8K_CS_FLASHCOPY), + rebuild_url=True) + new_fc3 = flashcopies.create(volume_pairs=[ + {'source_volume': source_volume, 'target_volume': target_volume}]) + resp3, data3 = new_fc3.save() + self.assertEqual(httpretty.POST, httpretty.last_request().method) + self.assertIsInstance(data3[0], FlashCopies) + self.assertEqual(resp3.status_code, 201) + + @httpretty.activate + def test_delete_cs_flashcopy(self): + response_a_json = get_response_json_by_type(DS8K_CS_FLASHCOPY) + response_a = get_response_data_by_type(DS8K_CS_FLASHCOPY) + name = self._get_resource_id_from_resopnse(DS8K_FLASHCOPY, + response_a, + FlashCopy.id_field + ) + url = '/cs/flashcopies/{}'.format(name) + httpretty.register_uri(httpretty.GET, + self.domain + self.base_url + url, + body=response_a_json, + content_type='application/json', + status=200, + ) + httpretty.register_uri(httpretty.DELETE, + self.domain + self.base_url + url, + body=action_response_json, + content_type='application/json', + status=204, + ) + # Way 1 + _ = self.system.delete_cs_flashcopy(name) + self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + + # Way 2 + flashcopy = self.system.get_cs_flashcopies(name) + self.assertIsInstance(flashcopy, FlashCopies) + resp2, _ = flashcopy.delete() + self.assertEqual(resp2.status_code, 204) + self.assertEqual(httpretty.DELETE, httpretty.last_request().method) diff --git a/pyds8k/test/test_resources/test_ds8k/test_volume.py b/pyds8k/test/test_resources/test_ds8k/test_volume.py index f6fa818..75ab341 100755 --- a/pyds8k/test/test_resources/test_ds8k/test_volume.py +++ b/pyds8k/test/test_resources/test_ds8k/test_volume.py @@ -14,27 +14,30 @@ # limitations under the License. ############################################################################## -import httpretty import json + +import httpretty from nose.tools import nottest + +from pyds8k.dataParser.ds8k import RequestParser +from pyds8k.exceptions import FieldReadOnly from pyds8k.messages import INVALID_TYPE from pyds8k.resources.ds8k.v1.common import types -from pyds8k.dataParser.ds8k import RequestParser -from .base import TestDS8KWithConnect -from pyds8k.resources.ds8k.v1.volumes import Volume, \ - VolumeManager -from pyds8k.resources.ds8k.v1.pools import Pool -from pyds8k.resources.ds8k.v1.hosts import Host +from pyds8k.resources.ds8k.v1.common.types import DS8K_VOLUME from pyds8k.resources.ds8k.v1.flashcopy import FlashCopy -from pyds8k.resources.ds8k.v1.pprc import PPRC +from pyds8k.resources.ds8k.v1.hosts import Host from pyds8k.resources.ds8k.v1.lss import LSS -from pyds8k.resources.ds8k.v1.common.types import DS8K_VOLUME -from ...data import get_response_json_by_type, get_response_data_by_type -from ...data import action_response_json, action_response, \ - create_volumes_response_json, create_volume_response_json, \ +from pyds8k.resources.ds8k.v1.pools import Pool +from pyds8k.resources.ds8k.v1.pprc import PPRC +from pyds8k.resources.ds8k.v1.volumes import Volume, \ + VolumeManager +from pyds8k.test.data import get_response_json_by_type, \ + get_response_data_by_type, action_response_json, \ + action_response, create_volume_response_json, \ + create_volumes_response_json, \ create_volumes_partial_failed_response_json, \ create_volumes_partial_failed_response -from pyds8k.exceptions import FieldReadOnly +from pyds8k.test.test_resources.test_ds8k.base import TestDS8KWithConnect class TestVolume(TestDS8KWithConnect):