Skip to content

Commit f098937

Browse files
committed
release 1.3.0
2 parents 4d53970 + 69e198a commit f098937

File tree

6 files changed

+220
-9
lines changed

6 files changed

+220
-9
lines changed

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
Changelog
22
=========
33

4+
v1.3.0 (2023-05-25)
5+
-------------------
6+
7+
* Added support for volume cloning
8+
49
v1.2.0 (2023-04-24)
510
-------------------
611

datacrunch/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = '1.2.0'
1+
VERSION = '1.3.0'

datacrunch/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class VolumeActions:
1515
RENAME = 'rename'
1616
INCREASE_SIZE = 'increase-size'
1717
DELETE = 'delete'
18+
CLONE = 'clone'
1819

1920
def __init__(self):
2021
return
@@ -40,6 +41,7 @@ class VolumeStatus:
4041
DETACHED = "detached"
4142
DELETING = "deleting"
4243
DELETED = "deleted"
44+
CLONING = 'cloning'
4345

4446
def __init__(self):
4547
return

datacrunch/volumes/volumes.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ def __init__(self,
4646
:param ssh_key_ids: list of ssh keys ids
4747
:type ssh_key_ids: List[str]
4848
"""
49-
5049
self._id = id
5150
self._status = status
5251
self._name = name
@@ -166,6 +165,7 @@ def __str__(self) -> str:
166165
"""
167166
return stringify_class_object_properties(self)
168167

168+
169169
class VolumesService:
170170
"""A service for interacting with the volumes endpoint"""
171171

@@ -193,7 +193,8 @@ def get(self, status: str = None) -> List[Volume]:
193193
target=volume_dict['target'] if 'target' in volume_dict else None,
194194
location=volume_dict['location'],
195195
instance_id=volume_dict['instance_id'] if 'instance_id' in volume_dict else None,
196-
ssh_key_ids=volume_dict['ssh_key_ids'] if 'ssh_key_ids' in volume_dict else [],
196+
ssh_key_ids=volume_dict['ssh_key_ids'] if 'ssh_key_ids' in volume_dict else [
197+
],
197198
), volumes_dict))
198199
return volumes
199200

@@ -218,7 +219,8 @@ def get_by_id(self, id: str) -> Volume:
218219
target=volume_dict['target'] if 'target' in volume_dict else None,
219220
location=volume_dict['location'],
220221
instance_id=volume_dict['instance_id'] if 'instance_id' in volume_dict else None,
221-
ssh_key_ids=volume_dict['ssh_key_ids'] if 'ssh_key_ids' in volume_dict else [],
222+
ssh_key_ids=volume_dict['ssh_key_ids'] if 'ssh_key_ids' in volume_dict else [
223+
],
222224
)
223225
return volume
224226

@@ -288,6 +290,40 @@ def detach(self, id_list: Union[List[str], str]) -> None:
288290
self._http_client.put(VOLUMES_ENDPOINT, json=payload)
289291
return
290292

293+
def clone(self, id: str, name: str = None, type: str = None) -> Volume:
294+
"""Clone a volume or multiple volumes
295+
296+
:param id: volume id or list of volume ids
297+
:type id: str or List[str]
298+
:param name: new volume name
299+
:type name: str
300+
:param type: volume type
301+
:type type: str, optional
302+
:return: the new volume object, or a list of volume objects if cloned mutliple volumes
303+
:rtype: Volume or List[Volume]
304+
"""
305+
payload = {
306+
"id": id,
307+
"action": VolumeActions.CLONE,
308+
"name": name,
309+
"type": type
310+
}
311+
312+
# clone volume(s)
313+
volume_ids_array = self._http_client.put(
314+
VOLUMES_ENDPOINT, json=payload).json()
315+
316+
# map the IDs into Volume objects
317+
volumes_array = list(
318+
map(lambda volume_id: self.get_by_id(volume_id), volume_ids_array))
319+
320+
# if the array has only one element, return that element
321+
if len(volumes_array) == 1:
322+
return volumes_array[0]
323+
324+
# otherwise return the volumes array
325+
return volumes_array
326+
291327
def rename(self, id_list: Union[List[str], str], name: str) -> None:
292328
"""Rename multiple volumes or single volume
293329

examples/storage_volumes.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
all_volumes = datacrunch.volumes.get()
2020

2121
# Get all attached volumes
22-
all_attached_volumes = datacrunch.volumes.get(status=datacrunch.constants.volume_status.ATTACHED)
22+
all_attached_volumes = datacrunch.volumes.get(
23+
status=datacrunch.constants.volume_status.ATTACHED)
2324

2425
# Get volume by id
25-
random_volume = datacrunch.volumes.get_by_id("0c41e387-3dd8-495f-a285-e861527f2f3d")
26+
random_volume = datacrunch.volumes.get_by_id(
27+
"0c41e387-3dd8-495f-a285-e861527f2f3d")
2628

2729
# Create a 200 GB detached NVMe volume
2830
nvme_volume = datacrunch.volumes.create(type=NVMe,
@@ -51,5 +53,14 @@
5153
# increase volume size
5254
datacrunch.volumes.increase_size(nvme_volume_id, 300)
5355

56+
# clone volume
57+
datacrunch.volumes.clone(nvme_volume_id)
58+
59+
# clone volume and give it a new name and storage type (from NVMe to HDD)
60+
datacrunch.volumes.clone(nvme_volume_id, name="my-cloned-volume", type=HDD)
61+
62+
# clone multiple volumes at once
63+
datacrunch.volumes.clone([nvme_volume_id, hdd_volume_id])
64+
5465
# delete volumes
5566
datacrunch.volumes.delete([nvme_volume_id, hdd_volume_id])

tests/unit_tests/volumes/test_volumes.py

Lines changed: 160 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
HDD_VOL_SIZE = 100
3030
HDD_VOL_CREATED_AT = "2021-06-02T12:56:49.582Z"
3131

32+
RANDOM_VOL_ID = '07d864ee-ba86-451e-85b3-34ef551bd4a2'
33+
RANDOM_VOL2_ID = '72c5c082-7fe7-4d13-bd9e-f529c97d63b3'
34+
3235
NVME_VOLUME = {
3336
"id": NVME_VOL_ID,
3437
"status": NVME_VOL_STATUS,
@@ -69,7 +72,23 @@ def volumes_service(self, http_client):
6972
def endpoint(self, http_client):
7073
return http_client._base_url + "/volumes"
7174

72-
def test_get_instances(self, volumes_service, endpoint):
75+
def test_initialize_a_volume(self):
76+
volume = Volume(RANDOM_VOL_ID, VolumeStatus.DETACHED, HDD_VOL_NAME, HDD_VOL_SIZE,
77+
HDD, False, HDD_VOL_CREATED_AT)
78+
79+
assert volume.id == RANDOM_VOL_ID
80+
assert volume.status == VolumeStatus.DETACHED
81+
assert volume.instance_id == None
82+
assert volume.name == HDD_VOL_NAME
83+
assert volume.size == HDD_VOL_SIZE
84+
assert volume.type == HDD
85+
assert volume.location == FIN1
86+
assert volume.is_os_volume == False
87+
assert volume.created_at == HDD_VOL_CREATED_AT
88+
assert volume.target == None
89+
assert volume.ssh_key_ids == []
90+
91+
def test_get_volumes(self, volumes_service, endpoint):
7392
# arrange - add response mock
7493
responses.add(
7594
responses.GET,
@@ -219,7 +238,8 @@ def test_create_volume_successful(self, volumes_service, endpoint):
219238
)
220239

221240
# act
222-
volume = volumes_service.create(VolumeTypes.NVMe, NVME_VOL_NAME, NVME_VOL_SIZE)
241+
volume = volumes_service.create(
242+
VolumeTypes.NVMe, NVME_VOL_NAME, NVME_VOL_SIZE)
223243

224244
# assert
225245
assert volume.id == NVME_VOL_ID
@@ -236,7 +256,8 @@ def test_create_volume_failed(self, volumes_service, endpoint):
236256

237257
# act
238258
with pytest.raises(APIException) as excinfo:
239-
volumes_service.create(VolumeTypes.NVMe, NVME_VOL_NAME, 100000000000000000000000)
259+
volumes_service.create(
260+
VolumeTypes.NVMe, NVME_VOL_NAME, 100000000000000000000000)
240261

241262
# assert
242263
assert excinfo.value.code == INVALID_REQUEST
@@ -481,3 +502,139 @@ def test_delete_volume_failed(self, volumes_service, endpoint):
481502
assert excinfo.value.code == INVALID_REQUEST
482503
assert excinfo.value.message == INVALID_REQUEST_MESSAGE
483504
assert responses.assert_call_count(endpoint, 1) is True
505+
506+
def test_clone_volume_with_input_name_successful(self, volumes_service, endpoint):
507+
# arrange
508+
CLONED_VOLUME_NAME = "cloned-volume"
509+
510+
# mock response for cloning the volume
511+
responses.add(
512+
responses.PUT,
513+
endpoint,
514+
status=202,
515+
json=[RANDOM_VOL_ID],
516+
match=[
517+
responses.json_params_matcher({
518+
"id": NVME_VOL_ID,
519+
"action": VolumeActions.CLONE,
520+
"name": CLONED_VOLUME_NAME,
521+
"type": None
522+
})
523+
]
524+
)
525+
526+
# mock object for the cloned volume
527+
CLONED_VOL_GET_MOCK = NVME_VOLUME
528+
CLONED_VOL_GET_MOCK['id'] = RANDOM_VOL_ID
529+
CLONED_VOL_GET_MOCK['name'] = CLONED_VOLUME_NAME
530+
CLONED_VOL_GET_MOCK['status'] = VolumeStatus.CLONING
531+
532+
# mock response for getting the cloned volume
533+
responses.add(
534+
responses.GET,
535+
endpoint + "/" + RANDOM_VOL_ID,
536+
status=200,
537+
json=CLONED_VOL_GET_MOCK,
538+
)
539+
540+
# act
541+
cloned_volume = volumes_service.clone(NVME_VOL_ID, CLONED_VOLUME_NAME)
542+
543+
# assert
544+
assert responses.assert_call_count(endpoint, 1) is True
545+
assert cloned_volume.name == CLONED_VOLUME_NAME
546+
547+
def test_clone_volume_without_input_name_successful(self, volumes_service: VolumesService, endpoint):
548+
# arrange
549+
CLONED_VOLUME_NAME = "CLONE-" + NVME_VOL_NAME
550+
551+
# mock response for cloning the volume
552+
responses.add(
553+
responses.PUT,
554+
endpoint,
555+
status=202,
556+
json=[RANDOM_VOL_ID],
557+
match=[
558+
responses.json_params_matcher({
559+
"id": NVME_VOL_ID,
560+
"action": VolumeActions.CLONE,
561+
"name": None,
562+
"type": None
563+
})
564+
]
565+
)
566+
567+
# mock object for the cloned volume
568+
CLONED_VOL_GET_MOCK = NVME_VOLUME
569+
CLONED_VOL_GET_MOCK['id'] = RANDOM_VOL_ID
570+
CLONED_VOL_GET_MOCK['name'] = CLONED_VOLUME_NAME
571+
CLONED_VOL_GET_MOCK['status'] = VolumeStatus.CLONING
572+
573+
# mock response for getting the cloned volume
574+
responses.add(
575+
responses.GET,
576+
endpoint + "/" + RANDOM_VOL_ID,
577+
status=200,
578+
json=CLONED_VOL_GET_MOCK,
579+
)
580+
581+
# act
582+
cloned_volume = volumes_service.clone(NVME_VOL_ID)
583+
584+
# assert
585+
assert responses.assert_call_count(endpoint, 1) is True
586+
assert cloned_volume.name == CLONED_VOLUME_NAME
587+
588+
def test_clone_two_volumes_successful(self, volumes_service: VolumesService, endpoint):
589+
# arrange
590+
CLONED_VOL1_NAME = "CLONE-" + NVME_VOL_NAME
591+
CLONED_VOL2_NAME = "CLONE-" + HDD_VOL_NAME
592+
593+
# mock response for cloning the volumes
594+
responses.add(
595+
responses.PUT,
596+
endpoint,
597+
status=202,
598+
json=[RANDOM_VOL_ID, RANDOM_VOL2_ID],
599+
match=[
600+
responses.json_params_matcher({
601+
"id": [NVME_VOL_ID, HDD_VOL_ID],
602+
"action": VolumeActions.CLONE,
603+
"name": None,
604+
"type": None
605+
})
606+
]
607+
)
608+
609+
# mock object for the cloned volumes
610+
CLONED_VOL1_GET_MOCK = NVME_VOLUME
611+
CLONED_VOL1_GET_MOCK['id'] = RANDOM_VOL_ID
612+
CLONED_VOL1_GET_MOCK['name'] = CLONED_VOL1_NAME
613+
CLONED_VOL1_GET_MOCK['status'] = VolumeStatus.CLONING
614+
615+
CLONED_VOL2_GET_MOCK = HDD_VOLUME
616+
CLONED_VOL2_GET_MOCK['id'] = RANDOM_VOL2_ID
617+
CLONED_VOL2_GET_MOCK['name'] = CLONED_VOL2_NAME
618+
CLONED_VOL2_GET_MOCK['status'] = VolumeStatus.CLONING
619+
620+
# mock response for getting the cloned volumes
621+
responses.add(
622+
responses.GET,
623+
endpoint + "/" + RANDOM_VOL_ID,
624+
status=200,
625+
json=CLONED_VOL1_GET_MOCK,
626+
)
627+
responses.add(
628+
responses.GET,
629+
endpoint + "/" + RANDOM_VOL2_ID,
630+
status=200,
631+
json=CLONED_VOL2_GET_MOCK,
632+
)
633+
634+
# act
635+
cloned_volume = volumes_service.clone([NVME_VOL_ID, HDD_VOL_ID])
636+
637+
# assert
638+
assert responses.assert_call_count(endpoint, 1) is True
639+
assert cloned_volume[0].name == CLONED_VOL1_NAME
640+
assert cloned_volume[1].name == CLONED_VOL2_NAME

0 commit comments

Comments
 (0)