Skip to content

Commit 8b312e7

Browse files
authored
Implement custom image support (#41)
1 parent a6ec98d commit 8b312e7

6 files changed

Lines changed: 426 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## v1.3.0 (2020-12-13)
6+
7+
### Minor changes
8+
9+
- Implemented custom image support.
10+
- cloudscale-sdk updated to 0.6.1.
11+
512
## v1.2.1 (2020-11-25)
613

714
### Bug fixes

cloudscale_cli/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .commands.subnet import subnet
1313
from .commands.volume import volume
1414
from .commands.objects_user import objects_user
15+
from .commands.custom_image import custom_image
1516

1617
@click.group(cls=OrderedGroup, context_settings={
1718
'help_option_names': ['-h', '--help'],
@@ -43,3 +44,4 @@ def cli(ctx, profile, api_token, debug, output, verbose):
4344
cli.add_command(subnet)
4445
cli.add_command(volume)
4546
cli.add_command(objects_user)
47+
cli.add_command(custom_image)

cloudscale_cli/commands/__init__.py

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ def __init__(self, cloud_resource_name=None, api_token=None, profile=None, debug
3030
sys.exit(1)
3131

3232
self._output = output
33-
self._retries = 30
3433

3534
self.cloud_resource_name = cloud_resource_name
3635
self.verbose = verbose
@@ -50,6 +49,14 @@ def __init__(self, cloud_resource_name=None, api_token=None, profile=None, debug
5049
def get_client_resource(self):
5150
return getattr(self._client, self.cloud_resource_name)
5251

52+
def _handle_tags(self, tags):
53+
try:
54+
tags = tags_to_dict(tags)
55+
return tags
56+
except ValueError as e:
57+
click.echo(e, err=True)
58+
sys.exit(1)
59+
5360
def _format_output(self, response):
5461
if self._output == "json":
5562
return to_pretty_json(response)
@@ -73,7 +80,7 @@ def cmd_list(self, filter_tag=None, filter_json=None, action=None, delete=False,
7380
sys.exit(1)
7481
try:
7582
with Spinner(text="Querying"):
76-
response = self.get_client_resource().get_all(filter_tag)
83+
response = self.get_client_resource().get_all(filter_tag=filter_tag)
7784
if filter_json:
7885
try:
7986
response = jmespath.search(filter_json, response)
@@ -104,13 +111,13 @@ def cmd_list(self, filter_tag=None, filter_json=None, action=None, delete=False,
104111
getattr(self.get_client_resource(), action)(uuid)
105112

106113
if wait:
107-
response = self.get_client_resource().get_all(filter_tag)
114+
response = self.get_client_resource().get_all(filter_tag=filter_tag)
108115
for r in response:
109116
uuid = r['href'].split('/')[-1]
110117
self.wait_for_status(uuid=uuid)
111118

112119
with Spinner(text="Querying"):
113-
response = self.get_client_resource().get_all(filter_tag)
120+
response = self.get_client_resource().get_all(filter_tag=filter_tag)
114121
click.echo(self._format_output(response))
115122
except Exception as e:
116123
click.echo(e, err=True)
@@ -133,7 +140,7 @@ def cmd_get_by_name(self, name):
133140
def cmd_show(self, uuid):
134141
try:
135142
with Spinner(text=f"Querying by {self.resource_uuid_name} {uuid}"):
136-
response = self.get_client_resource().get_by_uuid(uuid)
143+
response = self.get_client_resource().get_by_uuid(uuid=uuid)
137144
click.echo(self._format_output(response))
138145
except CloudscaleApiException as e:
139146
results = self.cmd_get_by_name(name=uuid)
@@ -152,11 +159,7 @@ def cmd_show(self, uuid):
152159
def cmd_create(self, silent=False, **kwargs):
153160
try:
154161
if 'tags' in kwargs:
155-
try:
156-
kwargs['tags'] = tags_to_dict(kwargs['tags'])
157-
except ValueError as e:
158-
click.echo(e, err=True)
159-
sys.exit(1)
162+
kwargs['tags'] = self._handle_tags(kwargs['tags'])
160163

161164
name = kwargs.get(self.resource_name_key, '')
162165
with Spinner(text=f"Creating {name}"):
@@ -172,7 +175,7 @@ def cmd_create(self, silent=False, **kwargs):
172175
def cmd_update(self, uuid, tags=None, clear_tags=None, clear_all_tags=False, wait=False, **kwargs):
173176
try:
174177
with Spinner(text=f"Querying by {self.resource_uuid_name} {uuid}"):
175-
self.get_client_resource().get_by_uuid(uuid)
178+
self.get_client_resource().get_by_uuid(uuid=uuid)
176179

177180
except CloudscaleApiException as e:
178181
results = self.cmd_get_by_name(name=uuid)
@@ -222,7 +225,7 @@ def cmd_update(self, uuid, tags=None, clear_tags=None, clear_all_tags=False, wai
222225
response = self.wait_for_status(uuid=uuid)
223226
else:
224227
with Spinner(text=f"Querying {uuid}"):
225-
response = self.get_client_resource().get_by_uuid(uuid)
228+
response = self.get_client_resource().get_by_uuid(uuid=uuid)
226229

227230
click.echo(self._format_output(response))
228231
except Exception as e:
@@ -234,7 +237,7 @@ def cmd_delete(self, uuid, force=False, skip_query=False):
234237
if not skip_query:
235238
try:
236239
with Spinner(text=f"Querying by {self.resource_uuid_name} {uuid}"):
237-
response = self.get_client_resource().get_by_uuid(uuid)
240+
response = self.get_client_resource().get_by_uuid(uuid=uuid)
238241
except CloudscaleApiException as e:
239242
results = self.cmd_get_by_name(name=uuid)
240243
if not results:
@@ -268,7 +271,7 @@ def cmd_act(self, action, uuid, wait=False):
268271
with Spinner(text=f"Processing"):
269272
try:
270273
with Spinner(text=f"Querying by {self.resource_uuid_name} {uuid}"):
271-
self.get_client_resource().get_by_uuid(uuid)
274+
self.get_client_resource().get_by_uuid(uuid=uuid)
272275

273276
except CloudscaleApiException as e:
274277
results = self.cmd_get_by_name(name=uuid)
@@ -295,20 +298,25 @@ def cmd_act(self, action, uuid, wait=False):
295298
response = self.wait_for_status(uuid=uuid)
296299
else:
297300
with Spinner(text=f"Querying {uuid}"):
298-
response = self.get_client_resource().get_by_uuid(uuid)
301+
response = self.get_client_resource().get_by_uuid(uuid=uuid)
299302
click.echo(self._format_output(response))
300303
except Exception as e:
301304
click.echo(e, err=True)
302305
sys.exit(1)
303306

304-
def wait_for_status(self, uuid):
307+
def wait_for_status(self, uuid, status = "changing", max_sleep = 4, retries = 30, path = ""):
305308
with Spinner(text=f"Waiting for status {uuid}: ...") as sp:
306-
for retry in range(1, self._retries):
307-
response = self.get_client_resource().get_by_uuid(uuid)
308-
if response['status'] != 'changing':
309+
for retry in range(0, retries):
310+
response = self.get_client_resource().get_by_uuid(uuid=uuid, path=path)
311+
if response['status'] != status:
309312
break
310-
sp.text = f"Waiting for status {uuid}: {response['status']}"
311-
time.sleep(1)
313+
sp.text = f"Waiting for {uuid} to finish, current status: {response['status']} { retry * '.'}"
314+
315+
# Exponential wait...
316+
sleep = 2 ** retry
317+
if sleep > max_sleep:
318+
sleep = max_sleep
319+
time.sleep(sleep)
312320
else:
313321
sp.text = f"Waiting for status {uuid} timed out."
314322
time.sleep(1)
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import sys
2+
import time
3+
import click
4+
from . import Spinner
5+
6+
@click.group()
7+
@click.pass_context
8+
def custom_image(ctx):
9+
headers = [
10+
'name',
11+
'slug',
12+
'created_at',
13+
'zones',
14+
'tags',
15+
'uuid',
16+
]
17+
verbose_headers = [
18+
'name',
19+
'slug',
20+
'created_at',
21+
'zones',
22+
'user_data_handling',
23+
'tags',
24+
'checksum_md5',
25+
'checksum_sha256',
26+
'uuid',
27+
]
28+
29+
ctx.obj.cloud_resource_name = "custom_image"
30+
ctx.obj.headers = verbose_headers if ctx.obj.verbose else headers
31+
ctx.obj.response_transform_json = '''
32+
[].{
33+
"name": name,
34+
"slug": slug,
35+
"user_data_handling": user_data_handling,
36+
"zones": zones[].slug,
37+
"tags": tags,
38+
"uuid": uuid,
39+
"created_at": created_at,
40+
"checksum_sha256": checksums.sha256,
41+
"checksum_md5": checksums.md5
42+
}
43+
'''
44+
45+
@click.option('--filter-tag')
46+
@click.option('--filter-json')
47+
@click.option('--delete', is_flag=True)
48+
@click.option('--force', is_flag=True)
49+
@custom_image.command("list")
50+
@click.pass_obj
51+
def cmd_list(cloudscale, filter_tag, filter_json, delete, force):
52+
cloudscale.cmd_list(
53+
filter_tag=filter_tag,
54+
filter_json=filter_json,
55+
delete=delete,
56+
force=force,
57+
)
58+
59+
@click.argument('uuid', required=True)
60+
@custom_image.command("show")
61+
@click.pass_obj
62+
def cmd_show(cloudscale, uuid):
63+
cloudscale.cmd_show(
64+
uuid=uuid,
65+
)
66+
67+
@click.option('--url', required=True)
68+
@click.option('--name', required=True)
69+
@click.option('--slug', required=True)
70+
@click.option('--user-data-handling', type=click.Choice(['pass-through', 'extend-cloud-config']), required=True)
71+
@click.option('--zone', 'zones', multiple=True, required=True)
72+
@click.option('--source-format', type=click.Choice(['raw']), default="raw", show_default=True)
73+
@click.option('--tag', 'tags', multiple=True)
74+
@click.option('--wait', is_flag=True)
75+
@custom_image.command("import")
76+
@click.pass_obj
77+
def cmd_import(cloudscale, url, name, slug, user_data_handling, zones, source_format, tags, wait):
78+
try:
79+
tags = cloudscale._handle_tags(tags)
80+
zones_string = ', '.join(zones)
81+
msg = f"{url} as {name} into zone: {zones_string}. "
82+
83+
with Spinner(text="Importing " + msg):
84+
response = cloudscale.get_client_resource().import_by_url(
85+
url=url,
86+
name=name,
87+
slug=slug,
88+
user_data_handling=user_data_handling,
89+
zones=zones,
90+
source_format=source_format,
91+
tags=tags,
92+
)
93+
94+
if not wait:
95+
click.echo("Import continues in the background and may take a while.")
96+
sys.exit(0)
97+
98+
response = cloudscale.wait_for_status(uuid=response['uuid'],
99+
status = "in_progress",
100+
max_sleep = 8,
101+
retries = 120,
102+
path = "/import"
103+
)
104+
105+
if response.get('status') == "failed":
106+
click.echo("Import failed.")
107+
sys.exit(1)
108+
109+
response = cloudscale.get_client_resource().get_by_uuid(uuid=response['uuid'])
110+
click.echo(cloudscale._format_output(response))
111+
112+
except Exception as e:
113+
click.echo(e, err=True)
114+
sys.exit(1)
115+
116+
@click.argument('uuid', required=True)
117+
@click.option('--name')
118+
@click.option('--slug')
119+
@click.option('--user-data-handling', type=click.Choice(['pass-through', 'extend-cloud-config']))
120+
@click.option('--tag', 'tags', multiple=True)
121+
@click.option('--clear-tag', 'clear_tags', multiple=True)
122+
@click.option('--clear-all-tags', is_flag=True)
123+
@custom_image.command("update")
124+
@click.pass_obj
125+
def cmd_update(cloudscale, uuid, name, slug, user_data_handling, tags, clear_tags, clear_all_tags):
126+
cloudscale.cmd_update(
127+
uuid=uuid,
128+
tags=tags,
129+
clear_tags=clear_tags,
130+
clear_all_tags=clear_all_tags,
131+
name=name,
132+
slug=slug,
133+
user_data_handling=user_data_handling,
134+
)
135+
136+
@click.argument('uuid', required=True)
137+
@click.option('--force', is_flag=True)
138+
@custom_image.command("delete")
139+
@click.pass_obj
140+
def cmd_delete(cloudscale, uuid, force):
141+
cloudscale.cmd_delete(
142+
uuid=uuid,
143+
force=force,
144+
)

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
cloudscale-sdk>=0.5.0
1+
cloudscale-sdk>=0.6.1
22
click>=7.0.0
33
tabulate
44
pygments

0 commit comments

Comments
 (0)