Skip to content

Commit 1530a27

Browse files
M3 - Virtual disk - US02 - US03 - Create / Delete / List (#153)
Add virtual_disk module Fixes #165
1 parent 5a3aa1d commit 1530a27

File tree

20 files changed

+2997
-58
lines changed

20 files changed

+2997
-58
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
major_changes:
3+
- Added virtual_disk module. (https://github.com/ScaleComputing/HyperCoreAnsibleCollection/pull/153)

plugins/module_utils/client.py

+15-10
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@
1111
import json
1212
import ssl
1313
from typing import Any, Optional, Union
14+
from io import BufferedReader
1415

1516
from ansible.module_utils.urls import Request, basic_auth_header
1617

17-
from .errors import AuthError, ScaleComputingError, UnexpectedAPIResponse
18+
from .errors import (
19+
AuthError,
20+
ScaleComputingError,
21+
UnexpectedAPIResponse,
22+
ApiResponseNotJson,
23+
)
1824
from ..module_utils.typed_classes import TypedClusterInstance
1925

2026
from ansible.module_utils.six.moves.urllib.error import HTTPError, URLError
@@ -47,9 +53,7 @@ def json(self) -> Any:
4753
try:
4854
self._json = json.loads(self.data)
4955
except ValueError:
50-
raise ScaleComputingError(
51-
"Received invalid JSON response: {0}".format(self.data)
52-
)
56+
raise ApiResponseNotJson(self.data)
5357
return self._json
5458

5559

@@ -100,7 +104,7 @@ def _request(
100104
self,
101105
method: str,
102106
path: str,
103-
data: Optional[Union[dict[Any, Any], bytes, str]] = None,
107+
data: Optional[Union[dict[Any, Any], bytes, str, BufferedReader]] = None,
104108
headers: Optional[dict[Any, Any]] = None,
105109
timeout: Optional[float] = None,
106110
) -> Response:
@@ -135,13 +139,13 @@ def _request(
135139
and isinstance(e.args, tuple)
136140
and type(e.args[0]) == ConnectionRefusedError
137141
):
138-
raise ConnectionRefusedError(e)
142+
raise ConnectionRefusedError(e.reason)
139143
elif (
140144
e.args
141145
and isinstance(e.args, tuple)
142146
and type(e.args[0]) == ConnectionResetError
143147
):
144-
raise ConnectionResetError(e)
148+
raise ConnectionResetError(e.reason)
145149
elif (
146150
e.args
147151
and isinstance(e.args, tuple)
@@ -159,7 +163,7 @@ def request(
159163
query: Optional[dict[Any, Any]] = None,
160164
data: Optional[dict[Any, Any]] = None,
161165
headers: Optional[dict[Any, Any]] = None,
162-
binary_data: Optional[bytes] = None,
166+
binary_data: Optional[Union[bytes, BufferedReader]] = None,
163167
timeout: Optional[float] = None,
164168
) -> Response:
165169
# Make sure we only have one kind of payload
@@ -184,6 +188,7 @@ def request(
184188
timeout=timeout,
185189
)
186190
elif binary_data is not None:
191+
headers["Content-type"] = "application/octet-stream"
187192
return self._request(
188193
method, url, data=binary_data, headers=headers, timeout=timeout
189194
)
@@ -227,10 +232,10 @@ def patch(
227232
def put(
228233
self,
229234
path: str,
230-
data: dict[Any, Any],
235+
data: Optional[dict[Any, Any]],
231236
query: Optional[dict[Any, Any]] = None,
232237
timeout: Optional[float] = None,
233-
binary_data: Optional[bytes] = None,
238+
binary_data: Optional[Union[bytes, BufferedReader]] = None,
234239
headers: Optional[dict[Any, Any]] = None,
235240
) -> Request:
236241
resp = self.request(

plugins/module_utils/errors.py

+6
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,9 @@ class SupportTunnelError(ScaleComputingError):
9999
def __init__(self, data: Union[str, Exception]):
100100
self.message = "{0}".format(data)
101101
super(SupportTunnelError, self).__init__(self.message)
102+
103+
104+
class ApiResponseNotJson(ScaleComputingError):
105+
def __init__(self, data: Union[str, Exception]):
106+
self.message = f"From API expected json got {data}."
107+
super(ApiResponseNotJson, self).__init__(self.message)

plugins/module_utils/hypercore_version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
import operator
1212
import re
1313
from functools import total_ordering
14+
from ansible.module_utils.basic import AnsibleModule
1415
from typing import List
1516
from ..module_utils.utils import PayloadMapper
16-
from ansible.module_utils.basic import AnsibleModule
1717
from ..module_utils.rest_client import RestClient
1818
from ..module_utils.typed_classes import (
1919
TypedUpdateToAnsible,

plugins/module_utils/rest_client.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313

1414
__metaclass__ = type
1515

16-
from typing import Any, Optional
16+
from typing import Any, Optional, Union
17+
from io import BufferedReader
18+
import json
1719

1820

1921
def _query(original: Optional[dict[Any, Any]] = None) -> dict[Any, Any]:
@@ -112,14 +114,13 @@ def delete_record(
112114
def put_record(
113115
self,
114116
endpoint: str,
115-
payload: dict[Any, Any],
117+
payload: Optional[dict[Any, Any]],
116118
check_mode: bool,
117119
query: Optional[dict[Any, Any]] = None,
118120
timeout: Optional[float] = None,
119-
binary_data: Optional[bytes] = None,
121+
binary_data: Optional[Union[bytes, BufferedReader]] = None,
120122
headers: Optional[dict[Any, Any]] = None,
121123
) -> TypedTaskTag:
122-
# Method put doesn't support check mode # IT ACTUALLY DOES
123124
if check_mode:
124125
return utils.MOCKED_TASK_TAG
125126
try:
@@ -133,7 +134,8 @@ def put_record(
133134
).json
134135
except TimeoutError as e:
135136
raise errors.ScaleComputingError(f"Request timed out: {e}")
136-
137+
except (json.JSONDecodeError, json.decoder.JSONDecodeError) as e:
138+
raise json.JSONDecodeError(e.msg, e.doc, e.pos)
137139
return response
138140

139141

plugins/module_utils/task_tag.py

+17-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66

77
from __future__ import absolute_import, division, print_function
8+
from __future__ import annotations
89

910
__metaclass__ = type
1011

@@ -13,15 +14,21 @@
1314
from ..module_utils import errors
1415
from ..module_utils.rest_client import RestClient
1516
from ..module_utils.typed_classes import TypedTaskTag
17+
from typing import Optional, Dict, Any
1618

1719

1820
class TaskTag:
1921
@classmethod
2022
def wait_task(
21-
cls, rest_client: RestClient, task: TypedTaskTag, check_mode: bool = False
22-
):
23+
cls,
24+
rest_client: RestClient,
25+
task: Optional[TypedTaskTag],
26+
check_mode: bool = False,
27+
) -> None:
2328
if check_mode:
2429
return
30+
if task is None:
31+
return
2532
if type(task) != dict:
2633
raise errors.ScaleComputingError("task should be dictionary.")
2734
if "taskTag" not in task.keys():
@@ -50,14 +57,18 @@ def wait_task(
5057
sleep(1)
5158

5259
@staticmethod
53-
def get_task_status(rest_client, task):
60+
def get_task_status(
61+
rest_client: RestClient, task: Optional[TypedTaskTag]
62+
) -> Optional[Dict[Any, Any]]:
63+
if not task:
64+
return None
5465
if type(task) != dict:
5566
raise errors.ScaleComputingError("task should be dictionary.")
5667
if "taskTag" not in task.keys():
5768
raise errors.ScaleComputingError("taskTag is not in task dictionary.")
5869
if not task["taskTag"]:
59-
return
60-
task_status = rest_client.get_record(
70+
return None
71+
task_status: Optional[Dict[Any, Any]] = rest_client.get_record(
6172
"{0}/{1}".format("/rest/v1/TaskTag", task["taskTag"]), query={}
6273
)
63-
return task_status if task_status else {}
74+
return task_status if task_status else None

plugins/module_utils/typed_classes.py

+2
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ class TypedDiff(TypedDict):
133133
TypedCertificateToAnsible,
134134
TypedSyslogServerToAnsible,
135135
TypedUpdateToAnsible,
136+
TypedVirtualDiskToAnsible,
136137
None,
137138
dict[None, None],
138139
]
@@ -144,6 +145,7 @@ class TypedDiff(TypedDict):
144145
TypedClusterToAnsible,
145146
TypedCertificateToAnsible,
146147
TypedSyslogServerToAnsible,
148+
TypedVirtualDiskToAnsible,
147149
None,
148150
dict[None, None],
149151
]

plugins/module_utils/utils.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
TypedRegistrationToAnsible,
1919
TypedOidcToAnsible,
2020
TypedCertificateToAnsible,
21+
TypedVirtualDiskToAnsible,
2122
)
2223

2324

@@ -132,10 +133,18 @@ def filter_results(results, filter_data) -> list[Any]:
132133

133134
def is_changed(
134135
before: Union[
135-
TypedCertificateToAnsible, TypedOidcToAnsible, TypedRegistrationToAnsible, None
136+
TypedCertificateToAnsible,
137+
TypedOidcToAnsible,
138+
TypedRegistrationToAnsible,
139+
TypedVirtualDiskToAnsible,
140+
None,
136141
],
137142
after: Union[
138-
TypedCertificateToAnsible, TypedOidcToAnsible, TypedRegistrationToAnsible, None
143+
TypedCertificateToAnsible,
144+
TypedOidcToAnsible,
145+
TypedRegistrationToAnsible,
146+
TypedVirtualDiskToAnsible,
147+
None,
139148
],
140149
) -> bool:
141150
return not before == after

plugins/module_utils/virtual_disk.py

+82-13
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,19 @@
88

99
__metaclass__ = type
1010

11+
from ansible.module_utils.basic import AnsibleModule
1112
from ..module_utils.typed_classes import (
1213
TypedVirtualDiskFromAnsible,
1314
TypedVirtualDiskToAnsible,
15+
TypedTaskTag,
1416
)
1517
from typing import Dict, List, Any, Optional
1618

1719
from .rest_client import RestClient
1820
from ..module_utils.utils import PayloadMapper
21+
from ..module_utils import errors
22+
23+
REQUEST_TIMEOUT_TIME = 3600
1924

2025

2126
class VirtualDisk(PayloadMapper):
@@ -43,21 +48,20 @@ def from_ansible(cls, ansible_data: TypedVirtualDiskFromAnsible) -> VirtualDisk:
4348

4449
@classmethod
4550
def from_hypercore(cls, hypercore_data: Dict[Any, Any]) -> VirtualDisk:
46-
return cls(
47-
name=hypercore_data["name"],
48-
uuid=hypercore_data["uuid"],
49-
block_size=hypercore_data["blockSize"],
50-
size=hypercore_data["capacityBytes"],
51-
# allocated_size=hypercore_data["totalAllocationBytes"],
52-
replication_factor=hypercore_data["replicationFactor"],
53-
)
51+
try:
52+
return cls(
53+
name=hypercore_data["name"],
54+
uuid=hypercore_data["uuid"],
55+
block_size=hypercore_data["blockSize"],
56+
size=hypercore_data["capacityBytes"],
57+
# allocated_size=hypercore_data["totalAllocationBytes"],
58+
replication_factor=hypercore_data["replicationFactor"],
59+
)
60+
except KeyError as e:
61+
raise errors.MissingValueHypercore(e)
5462

5563
def to_hypercore(self) -> Dict[Any, Any]:
5664
raise NotImplementedError()
57-
# return dict(
58-
# searchDomains=self.search_domains,
59-
# serverIPs=self.server_ips,
60-
# )
6165

6266
def to_ansible(self) -> TypedVirtualDiskToAnsible:
6367
return dict(
@@ -93,14 +97,79 @@ def __eq__(self, other: object) -> bool:
9397
# virtual_disk = VirtualDisk.from_hypercore(hypercore_dict)
9498
# return virtual_disk
9599

100+
@classmethod
101+
def get_by_name(
102+
cls, rest_client: RestClient, name: str, must_exist: bool = False
103+
) -> Optional[VirtualDisk]:
104+
result = rest_client.list_records("/rest/v1/VirtualDisk", query=dict(name=name))
105+
if not isinstance(result, list):
106+
raise errors.ScaleComputingError(
107+
"Virtual disk API return value is not a list."
108+
)
109+
elif must_exist and (not result or not result[0]):
110+
raise errors.ScaleComputingError(
111+
f"Virtual disk with name {name} does not exist."
112+
)
113+
elif not result or not result[0]:
114+
return None
115+
elif len(result) > 1:
116+
raise errors.ScaleComputingError(
117+
f"Virtual disk {name} has multiple instances and is not unique."
118+
)
119+
return cls.from_hypercore(result[0])
120+
96121
@classmethod
97122
def get_state(
98123
cls, rest_client: RestClient, query: Dict[Any, Any]
99124
) -> List[TypedVirtualDiskToAnsible]:
100125
state = [
101-
VirtualDisk.from_hypercore(hypercore_data=hypercore_dict).to_ansible()
126+
cls.from_hypercore(hypercore_data=hypercore_dict).to_ansible()
102127
for hypercore_dict in rest_client.list_records(
103128
"/rest/v1/VirtualDisk", query
104129
)
105130
]
106131
return state
132+
133+
# Uploads a disk file (qcow2, vmdk, vhd); Hypercore creates virtual disk from uploaded file.
134+
# Filename and filesize need to be send as parameters in PUT request.
135+
@staticmethod
136+
def send_upload_request(
137+
rest_client: RestClient, file_size: int, module: AnsibleModule
138+
) -> TypedTaskTag:
139+
if (
140+
file_size is None
141+
or not module.params["name"]
142+
or not module.params["source"]
143+
):
144+
raise errors.ScaleComputingError(
145+
"Missing some virtual disk file values inside upload request."
146+
)
147+
try:
148+
with open(module.params["source"], "rb") as source_file:
149+
task = rest_client.put_record(
150+
endpoint="/rest/v1/VirtualDisk/upload",
151+
payload=None,
152+
check_mode=False,
153+
query=dict(filename=module.params["name"], filesize=file_size),
154+
timeout=REQUEST_TIMEOUT_TIME,
155+
binary_data=source_file,
156+
headers={
157+
"Content-Type": "application/octet-stream",
158+
"Accept": "application/json",
159+
"Content-Length": file_size,
160+
},
161+
)
162+
except FileNotFoundError:
163+
raise errors.ScaleComputingError(
164+
f"Disk file {module.params['source']} not found."
165+
)
166+
return task
167+
168+
def send_delete_request(self, rest_client: RestClient) -> TypedTaskTag:
169+
if not self.uuid:
170+
raise errors.ScaleComputingError(
171+
"Missing virtual disk UUID inside delete request."
172+
)
173+
return rest_client.delete_record(
174+
f"/rest/v1/VirtualDisk/{self.uuid}", check_mode=False
175+
)

0 commit comments

Comments
 (0)