From e67e37dc480747537434d7ac9d62ead2b18e365e Mon Sep 17 00:00:00 2001 From: Tom Lohmuller Date: Wed, 4 Jan 2023 12:12:04 -0800 Subject: [PATCH] api module: Add support for PUT methods This reverts commit 6e326f7d7228e684bb77aa1f30a0f11a942f1b84 and allows for both json (virtual disk upload)and non-json (ISO upload) responses to be returned. --- examples/virtual_disk.yml | 66 ++++++++++++++++++++++++++ plugins/module_utils/rest_client.py | 12 +++-- plugins/modules/api.py | 38 ++++++++++++++- tests/unit/plugins/modules/test_api.py | 22 +++++++++ 4 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 examples/virtual_disk.yml diff --git a/examples/virtual_disk.yml b/examples/virtual_disk.yml new file mode 100644 index 000000000..87ba90249 --- /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..e884f8c66 100644 --- a/plugins/module_utils/rest_client.py +++ b/plugins/module_utils/rest_client.py @@ -7,6 +7,7 @@ from . import errors from . import utils +import json __metaclass__ = type @@ -84,6 +85,7 @@ def put_record( endpoint, payload, check_mode, + query=None, timeout=None, binary_data=None, headers=None, @@ -91,20 +93,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 as e: + return response class CachedRestClient(RestClient): diff --git a/plugins/modules/api.py b/plugins/modules/api.py index 4f252f7d4..5b5e8c35b 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: @@ -239,6 +240,34 @@ def delete_record(module, rest_client): return True, task_tag 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( @@ -258,6 +287,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 +302,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/modules/test_api.py b/tests/unit/plugins/modules/test_api.py index 4b0bbbf5c..4663010b0 100644 --- a/tests/unit/plugins/modules/test_api.py +++ b/tests/unit/plugins/modules/test_api.py @@ -75,6 +75,28 @@ 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): + # 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", + data=dict(), + ) + ) + + result = api.put_record(module, rest_client) + assert result == (-1, -1, -1) + + class TestDeleteRecord: def test_delete_method_record_present(self, create_module, rest_client, task_wait): module = create_module(