diff --git a/examples/virtual_disk.yml b/examples/virtual_disk.yml new file mode 100644 index 000000000..824c39098 --- /dev/null +++ b/examples/virtual_disk.yml @@ -0,0 +1,66 @@ +--- +- name: Upload a virtual disk image from http link to HyperCore + hosts: localhost + connection: local + gather_facts: false + vars: + image_url: https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img + # image_url: https://github.com/ddemlow/RestAPIExamples/blob/master/ubuntu18_04-cloud-init/ubuntu18cloudimage.qcow2 + image_filename: "{{ image_url | split('/') | last }}" + image_remove_old: false + sc_host: https://10.100.20.38 # TODO + sc_username: admin + sc_password: admin + + tasks: + # # ------------------------------------------------------ + - name: Download Virtual Disk {{ image_filename }} from URL + ansible.builtin.get_url: # TODO: what if file doesn't download completely? + url: "{{ image_url }}" + dest: /tmp/{{ image_filename }} + + - name: Get the Virtual Disk size + ansible.builtin.stat: + path: /tmp/{{ image_filename }} + register: disk_file_info + + # TODO + # - name: (Optionally) remove existing Virtual Disk {{ image_filename }} from HyperCore + # scale_computing.hypercore.api: + # action: get + # cluster_instance: + # host: "{{ sc_host }}" + # username: "{{ sc_username }}" + # password: "{{ sc_password }}" + # endpoint: "/rest/v1/VirtualDisk" + # register: virtualDiskResult + + # ------------------------------------------------------ + - name: Upload Virtual Disk {{ image_filename }} to HyperCore + scale_computing.hypercore.api: + action: put + cluster_instance: + host: "{{ sc_host }}" + username: "{{ sc_username }}" + password: "{{ sc_password }}" + endpoint: /rest/v1/VirtualDisk/upload + data: + filename: "{{ image_filename }}" + filesize: "{{ disk_file_info.stat.size }}" + source: /tmp/{{ image_filename }} + register: uploadResult + + # ------------------------------------------------------ + - name: Get Information About the uploaded Virtual Disk in HyperCore + scale_computing.hypercore.api: + action: get + cluster_instance: + host: "{{ sc_host }}" + username: "{{ sc_username }}" + password: "{{ sc_password }}" + endpoint: /rest/v1/VirtualDisk/{{ uploadResult.record.createdUUID }} + register: result + + - name: Show uploaded disk info + debug: + var: result.record[0] diff --git a/plugins/module_utils/rest_client.py b/plugins/module_utils/rest_client.py index 7de82c881..7e7304819 100644 --- a/plugins/module_utils/rest_client.py +++ b/plugins/module_utils/rest_client.py @@ -84,6 +84,7 @@ def put_record( endpoint, payload, check_mode, + query=None, timeout=None, binary_data=None, headers=None, @@ -91,20 +92,22 @@ def put_record( # Method put doesn't support check mode # IT ACTUALLY DOES if check_mode: return None - # Only /rest/v1/ISO/[uuid}/data is using put, which doesn't return anything. - # self.client.put on this endpoint returns None. try: response = self.client.put( endpoint, data=payload, - query=_query(), + query=query, timeout=timeout, binary_data=binary_data, headers=headers, ) except TimeoutError as e: raise errors.ScaleComputingError(f"Request timed out: {e}") - return response + + try: + return response.json + except errors.ScaleComputingError: + return response class CachedRestClient(RestClient): diff --git a/plugins/modules/api.py b/plugins/modules/api.py index 4f252f7d4..83994809e 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -16,7 +16,7 @@ - Tjaž Eržen (@tjazsch) short_description: API interaction with Scale Computing HyperCore description: - - Perform a C(GET), C(POST), C(PATCH) or C(DELETE) request on resource(s) from the given endpoint. + - Perform a C(GET), C(POST), C(PATCH), C(DELETE), or C(PUT) request on resource(s) from the given endpoint. The api module can be used to perform raw API calls whenever there is no suitable concrete module or role implementation for a specific task. version_added: 1.0.0 @@ -36,6 +36,7 @@ - delete - get - post_list + - put data: type: dict description: @@ -50,6 +51,11 @@ - The raw endpoint that we want to perform post, patch or delete operation on. type: str required: true + source: + description: + - Source of the file to upload. + type: str + version_added: 1.1.0 notes: - C(check_mode) is not supported. @@ -240,6 +246,36 @@ def delete_record(module, rest_client): return False, dict() +""" +PUT_TIMEOUT_TIME was copied from the iso module for ISO data upload. +Currently, assume we have 4.7 GB ISO and speed 1 MB/s -> 4700 seconds. +Rounded to 3600. + +TODO: compute it from expected min upload speed and file size. +Even better, try to detect stalled uploads and terminate if no data was transmitted for more than N seconds. +Yum/dnf complain with error "Operation too slow. Less than 1000 bytes/sec transferred the last 30 seconds" +in such case. +""" +PUT_TIMEOUT_TIME = 3600 + + +def put_record(module, rest_client): + with open(module.params["source"], "rb") as source_file: + result = rest_client.put_record( + endpoint=module.params["endpoint"], + payload=None, + check_mode=module.check_mode, + query=module.params["data"], + timeout=PUT_TIMEOUT_TIME, + binary_data=source_file, + headers={ + "Content-Type": "application/octet-stream", + "Accept": "application/json", + }, + ) + return True, result + + def get_records(module, rest_client): records = rest_client.list_records( query=module.params["data"], @@ -258,6 +294,8 @@ def run(module, rest_client): return post_list_record(module, rest_client) elif action == "get": # GET method return get_records(module, rest_client) + elif action == "put": # PUT method + return put_record(module, rest_client) return delete_record(module, rest_client) # DELETE methodx @@ -271,13 +309,16 @@ def main(): ), action=dict( type="str", - choices=["post", "patch", "delete", "get", "post_list"], + choices=["post", "patch", "delete", "get", "post_list", "put"], required=True, ), endpoint=dict( type="str", required=True, ), + source=dict( + type="str", + ), ), ) diff --git a/tests/unit/plugins/module_utils/test_rest_client.py b/tests/unit/plugins/module_utils/test_rest_client.py index f158aadfc..62bc0771a 100644 --- a/tests/unit/plugins/module_utils/test_rest_client.py +++ b/tests/unit/plugins/module_utils/test_rest_client.py @@ -167,7 +167,7 @@ def test_normal_mode(self, client): client.put.assert_called_with( "my_table/id", data=None, - query=dict(), + query=None, timeout=None, binary_data=None, headers=None, diff --git a/tests/unit/plugins/modules/test_api.py b/tests/unit/plugins/modules/test_api.py index 4b0bbbf5c..5f5281969 100644 --- a/tests/unit/plugins/modules/test_api.py +++ b/tests/unit/plugins/modules/test_api.py @@ -75,6 +75,31 @@ def test_get_method_record_absent(self, create_module, rest_client): assert result == (False, []) +class TestPutMethod: + def test_put_method(self, create_module, rest_client, mocker): + # TODO: Put method hasn't been implemented yet, so tests still have to be written. + # Harcoding value for now. + module = create_module( + params=dict( + cluster_instance=dict( + host="https://0.0.0.0", + username="admin", + password="admin", + ), + action="put", + endpoint="/rest/v1/VirDomain", + unique_id="id", + source="this-source", + data=dict(), + ) + ) + mocker.patch("builtins.open", mocker.mock_open(read_data="this-data")) + rest_client.put_record.return_value = "this-value" + result = api.put_record(module, rest_client) + print(result) + assert result == (True, "this-value") + + class TestDeleteRecord: def test_delete_method_record_present(self, create_module, rest_client, task_wait): module = create_module(