Skip to content

Commit d5bc5a1

Browse files
Merge pull request #71 from ScaleComputing/api-put-json-validation
Api put json validation
2 parents 4d7d436 + 85dccac commit d5bc5a1

File tree

5 files changed

+142
-7
lines changed

5 files changed

+142
-7
lines changed

examples/virtual_disk.yml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
- name: Upload a virtual disk image from http link to HyperCore
3+
hosts: localhost
4+
connection: local
5+
gather_facts: false
6+
vars:
7+
image_url: https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img
8+
# image_url: https://github.com/ddemlow/RestAPIExamples/blob/master/ubuntu18_04-cloud-init/ubuntu18cloudimage.qcow2
9+
image_filename: "{{ image_url | split('/') | last }}"
10+
image_remove_old: false
11+
sc_host: https://10.100.20.38 # TODO
12+
sc_username: admin
13+
sc_password: admin
14+
15+
tasks:
16+
# # ------------------------------------------------------
17+
- name: Download Virtual Disk {{ image_filename }} from URL
18+
ansible.builtin.get_url: # TODO: what if file doesn't download completely?
19+
url: "{{ image_url }}"
20+
dest: /tmp/{{ image_filename }}
21+
22+
- name: Get the Virtual Disk size
23+
ansible.builtin.stat:
24+
path: /tmp/{{ image_filename }}
25+
register: disk_file_info
26+
27+
# TODO
28+
# - name: (Optionally) remove existing Virtual Disk {{ image_filename }} from HyperCore
29+
# scale_computing.hypercore.api:
30+
# action: get
31+
# cluster_instance:
32+
# host: "{{ sc_host }}"
33+
# username: "{{ sc_username }}"
34+
# password: "{{ sc_password }}"
35+
# endpoint: "/rest/v1/VirtualDisk"
36+
# register: virtualDiskResult
37+
38+
# ------------------------------------------------------
39+
- name: Upload Virtual Disk {{ image_filename }} to HyperCore
40+
scale_computing.hypercore.api:
41+
action: put
42+
cluster_instance:
43+
host: "{{ sc_host }}"
44+
username: "{{ sc_username }}"
45+
password: "{{ sc_password }}"
46+
endpoint: /rest/v1/VirtualDisk/upload
47+
data:
48+
filename: "{{ image_filename }}"
49+
filesize: "{{ disk_file_info.stat.size }}"
50+
source: /tmp/{{ image_filename }}
51+
register: uploadResult
52+
53+
# ------------------------------------------------------
54+
- name: Get Information About the uploaded Virtual Disk in HyperCore
55+
scale_computing.hypercore.api:
56+
action: get
57+
cluster_instance:
58+
host: "{{ sc_host }}"
59+
username: "{{ sc_username }}"
60+
password: "{{ sc_password }}"
61+
endpoint: /rest/v1/VirtualDisk/{{ uploadResult.record.createdUUID }}
62+
register: result
63+
64+
- name: Show uploaded disk info
65+
debug:
66+
var: result.record[0]

plugins/module_utils/rest_client.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,27 +84,30 @@ def put_record(
8484
endpoint,
8585
payload,
8686
check_mode,
87+
query=None,
8788
timeout=None,
8889
binary_data=None,
8990
headers=None,
9091
):
9192
# Method put doesn't support check mode # IT ACTUALLY DOES
9293
if check_mode:
9394
return None
94-
# Only /rest/v1/ISO/[uuid}/data is using put, which doesn't return anything.
95-
# self.client.put on this endpoint returns None.
9695
try:
9796
response = self.client.put(
9897
endpoint,
9998
data=payload,
100-
query=_query(),
99+
query=query,
101100
timeout=timeout,
102101
binary_data=binary_data,
103102
headers=headers,
104103
)
105104
except TimeoutError as e:
106105
raise errors.ScaleComputingError(f"Request timed out: {e}")
107-
return response
106+
107+
try:
108+
return response.json
109+
except errors.ScaleComputingError:
110+
return response
108111

109112

110113
class CachedRestClient(RestClient):

plugins/modules/api.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
- Tjaž Eržen (@tjazsch)
1717
short_description: API interaction with Scale Computing HyperCore
1818
description:
19-
- Perform a C(GET), C(POST), C(PATCH) or C(DELETE) request on resource(s) from the given endpoint.
19+
- Perform a C(GET), C(POST), C(PATCH), C(DELETE), or C(PUT) request on resource(s) from the given endpoint.
2020
The api module can be used to perform raw API calls whenever there is no
2121
suitable concrete module or role implementation for a specific task.
2222
version_added: 1.0.0
@@ -36,6 +36,7 @@
3636
- delete
3737
- get
3838
- post_list
39+
- put
3940
data:
4041
type: dict
4142
description:
@@ -50,6 +51,11 @@
5051
- The raw endpoint that we want to perform post, patch or delete operation on.
5152
type: str
5253
required: true
54+
source:
55+
description:
56+
- Source of the file to upload.
57+
type: str
58+
version_added: 1.1.0
5359
notes:
5460
- C(check_mode) is not supported.
5561
@@ -240,6 +246,36 @@ def delete_record(module, rest_client):
240246
return False, dict()
241247

242248

249+
"""
250+
PUT_TIMEOUT_TIME was copied from the iso module for ISO data upload.
251+
Currently, assume we have 4.7 GB ISO and speed 1 MB/s -> 4700 seconds.
252+
Rounded to 3600.
253+
254+
TODO: compute it from expected min upload speed and file size.
255+
Even better, try to detect stalled uploads and terminate if no data was transmitted for more than N seconds.
256+
Yum/dnf complain with error "Operation too slow. Less than 1000 bytes/sec transferred the last 30 seconds"
257+
in such case.
258+
"""
259+
PUT_TIMEOUT_TIME = 3600
260+
261+
262+
def put_record(module, rest_client):
263+
with open(module.params["source"], "rb") as source_file:
264+
result = rest_client.put_record(
265+
endpoint=module.params["endpoint"],
266+
payload=None,
267+
check_mode=module.check_mode,
268+
query=module.params["data"],
269+
timeout=PUT_TIMEOUT_TIME,
270+
binary_data=source_file,
271+
headers={
272+
"Content-Type": "application/octet-stream",
273+
"Accept": "application/json",
274+
},
275+
)
276+
return True, result
277+
278+
243279
def get_records(module, rest_client):
244280
records = rest_client.list_records(
245281
query=module.params["data"],
@@ -258,6 +294,8 @@ def run(module, rest_client):
258294
return post_list_record(module, rest_client)
259295
elif action == "get": # GET method
260296
return get_records(module, rest_client)
297+
elif action == "put": # PUT method
298+
return put_record(module, rest_client)
261299
return delete_record(module, rest_client) # DELETE methodx
262300

263301

@@ -271,13 +309,16 @@ def main():
271309
),
272310
action=dict(
273311
type="str",
274-
choices=["post", "patch", "delete", "get", "post_list"],
312+
choices=["post", "patch", "delete", "get", "post_list", "put"],
275313
required=True,
276314
),
277315
endpoint=dict(
278316
type="str",
279317
required=True,
280318
),
319+
source=dict(
320+
type="str",
321+
),
281322
),
282323
)
283324

tests/unit/plugins/module_utils/test_rest_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def test_normal_mode(self, client):
167167
client.put.assert_called_with(
168168
"my_table/id",
169169
data=None,
170-
query=dict(),
170+
query=None,
171171
timeout=None,
172172
binary_data=None,
173173
headers=None,

tests/unit/plugins/modules/test_api.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,31 @@ def test_get_method_record_absent(self, create_module, rest_client):
7575
assert result == (False, [])
7676

7777

78+
class TestPutMethod:
79+
def test_put_method(self, create_module, rest_client, mocker):
80+
# TODO: Put method hasn't been implemented yet, so tests still have to be written.
81+
# Harcoding value for now.
82+
module = create_module(
83+
params=dict(
84+
cluster_instance=dict(
85+
host="https://0.0.0.0",
86+
username="admin",
87+
password="admin",
88+
),
89+
action="put",
90+
endpoint="/rest/v1/VirDomain",
91+
unique_id="id",
92+
source="this-source",
93+
data=dict(),
94+
)
95+
)
96+
mocker.patch("builtins.open", mocker.mock_open(read_data="this-data"))
97+
rest_client.put_record.return_value = "this-value"
98+
result = api.put_record(module, rest_client)
99+
print(result)
100+
assert result == (True, "this-value")
101+
102+
78103
class TestDeleteRecord:
79104
def test_delete_method_record_present(self, create_module, rest_client, task_wait):
80105
module = create_module(

0 commit comments

Comments
 (0)