Skip to content

Commit 610b7a1

Browse files
authored
Merge pull request #103 from UpCloudLtd/storage-uploads-and-defaults
Storage uploads and defaults
2 parents 4d14bbe + ec44467 commit 610b7a1

File tree

5 files changed

+100
-33
lines changed

5 files changed

+100
-33
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ docs/html/
1111
.tox
1212
.cache
1313
.vscode/
14+
.idea/
1415

1516
# coverage
1617
.coverage

docs/Storage.md

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@ Storage can be created with the CloudManager's `.create_storage(size=10, tier="m
5050

5151
```python
5252

53-
storage1 = manager.create_storage( size=10,
54-
tier="maxiops",
55-
title="my storage disk",
56-
zone='fi-hel1' )
53+
storage1 = manager.create_storage(
54+
zone='fi-hel1',
55+
size=10,
56+
tier="maxiops",
57+
title="my storage disk"
58+
)
5759

58-
storage2 = manager.create_storage(100, zone='fi-hel1')
60+
storage2 = manager.create_storage(zone='de-fra1', size=100)
5961

6062
```
6163

@@ -81,6 +83,58 @@ storage.destroy()
8183

8284
```
8385

86+
## Import
87+
88+
Storages can be imported either by passing a URL or by uploading the file. Currently .iso, .raw and .img formats
89+
are supported. Other formats like QCOW2 or VMDK should be converted before uploading
90+
(with e.g. [`qemu-img convert`](https://linux.die.net/man/1/qemu-img)).
91+
92+
Uploaded storage is expected to be uncompressed. It is possible to upload gzip (`application/gzip`)
93+
or LZMA2 (`application/x-xz`) compressed files, but you need to specify a separate `content_type`
94+
when calling the `upload_file_for_storage_import` function.
95+
96+
Warning: the size of the import cannot exceed the size of the storage.
97+
The data will be written starting from the beginning of the storage,
98+
and the storage will not be truncated before starting to write.
99+
100+
Storages can be uploaded by providing a URL. Note that the upload is not
101+
done by the time `create_storage_import` returns, and you need to poll its
102+
status with `get_storage_import_details`.
103+
```python
104+
105+
new_storage = manager.create_storage(size=20, zone='nl-ams1')
106+
storage_import = manager.create_storage_import(
107+
storage=new_storage.uuid,
108+
source='http_import',
109+
source_location='https://username:[email protected]/path/to/data.raw',
110+
)
111+
112+
import_details = manager.get_storage_import_details(new_storage.uuid)
113+
114+
```
115+
116+
Other way is to upload a storage directly. After finishing, you should confirm
117+
that the storage has been processed with `get_storage_import_details`.
118+
```python
119+
120+
new_storage = manager.create_storage(size=20, zone='de-fra1', title='New imported storage')
121+
storage_import = manager.create_storage_import(storage=new_storage.uuid, source='direct_upload')
122+
123+
manager.upload_file_for_storage_import(
124+
storage_import=storage_import,
125+
file='/path/to/your/storage.img',
126+
)
127+
128+
import_details = manager.get_storage_import_details(new_storage.uuid)
129+
130+
```
131+
132+
Ongoing imports can also be cancelled:
133+
```python
134+
135+
manager.cancel_storage_import(new_storage.uuid)
136+
137+
```
84138

85139
## Clone
86140

test/test_storage.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ def test_get_templates(self, manager):
3030
@responses.activate
3131
def test_storage_create(self, manager):
3232
Mock.mock_post("storage")
33-
storage = manager.create_storage(666, "maxiops", "My data collection", "fi-hel1")
33+
storage = manager.create_storage(
34+
zone="fi-hel1", size=666, tier="maxiops", title="My data collection"
35+
)
3436
assert type(storage).__name__ == "Storage"
3537
assert storage.size == 666
3638
assert storage.tier == "maxiops"

upcloud_api/cloud_manager/storage_mixin.py

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
from typing import Optional, Union
1+
from os import PathLike
2+
from typing import BinaryIO, Optional, Union
23

34
from upcloud_api.api import API
45
from upcloud_api.storage import Storage
56
from upcloud_api.storage_import import StorageImport
6-
from upcloud_api.utils import get_raw_data_from_file
77

88

99
class StorageManager:
@@ -41,10 +41,11 @@ def get_storage(self, storage: str) -> Storage:
4141

4242
def create_storage(
4343
self,
44+
zone: str,
4445
size: int = 10,
4546
tier: str = 'maxiops',
4647
title: str = 'Storage disk',
47-
zone: str = 'fi-hel1',
48+
*,
4849
backup_rule: Optional[dict] = None,
4950
) -> Storage:
5051
"""
@@ -184,33 +185,52 @@ def create_storage_import(
184185
Creates an import task to import data into an existing storage.
185186
Source types: http_import or direct_upload.
186187
"""
188+
if source not in ("http_import", "direct_upload"):
189+
raise Exception(f"invalid storage import source: {source}")
190+
187191
url = f'/storage/{storage}/import'
188192
body = {'storage_import': {'source': source}}
189193
if source_location:
190194
body['storage_import']['source_location'] = source_location
191195
res = self.api.post_request(url, body)
192196
return StorageImport(**res['storage_import'])
193197

194-
def upload_file_for_storage_import(self, storage_import, file):
198+
def upload_file_for_storage_import(
199+
self,
200+
storage_import: StorageImport,
201+
file: Union[str, PathLike, BinaryIO],
202+
timeout: int = 30,
203+
content_type: str = 'application/octet-stream',
204+
):
195205
"""
196206
Uploads a file directly to UpCloud's uploader session.
197207
"""
198-
# TODO: this should not buffer the entire `file` into memory
199-
200-
# This is importing and using `requests` directly since there doesn't
201-
# seem to be a point in adding a `.api.raw_request()` call to the `API` class.
202-
# That could be changed.
203208

204209
import requests
205210

206-
resp = requests.put(
207-
url=storage_import.direct_upload_url,
208-
data=get_raw_data_from_file(file),
209-
headers={'Content-type': 'application/octet-stream'},
210-
timeout=600,
211-
)
212-
resp.raise_for_status()
213-
return resp.json()
211+
# This is importing and using `requests` directly since there doesn't
212+
# seem to be a point in adding a `.api.raw_request()` call to the `API` class.
213+
# That could be changed if there starts to be more of these cases.
214+
215+
f = file
216+
needs_closing = False
217+
if not hasattr(file, 'read'):
218+
f = open(file, 'rb')
219+
needs_closing = True
220+
221+
try:
222+
resp = requests.put(
223+
url=storage_import.direct_upload_url,
224+
data=f,
225+
headers={'Content-type': content_type},
226+
timeout=timeout,
227+
)
228+
229+
resp.raise_for_status()
230+
return resp.json()
231+
finally:
232+
if needs_closing:
233+
f.close()
214234

215235
def get_storage_import_details(self, storage: str) -> StorageImport:
216236
"""

upcloud_api/utils.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,3 @@ def try_it_n_times(operation, expected_error_codes, custom_error='operation fail
2222
sleep(3)
2323
if i >= n - 1:
2424
raise UpCloudClientError(custom_error)
25-
26-
27-
def get_raw_data_from_file(file: str) -> bytes:
28-
"""
29-
Helper function to get raw file data for uploading.
30-
"""
31-
with open(file, 'rb') as file:
32-
data = file.read()
33-
file.close()
34-
return data

0 commit comments

Comments
 (0)