Skip to content

Commit 162246d

Browse files
committed
Added CloudStack (exoscale) provider setup
1 parent 1db0705 commit 162246d

File tree

3 files changed

+71
-86
lines changed

3 files changed

+71
-86
lines changed

app/server.py

Lines changed: 39 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@
4545
except ImportError:
4646
HAS_AZURE_LIBRARIES = False
4747

48+
try:
49+
from cs import AIOCloudStack, CloudStackApiException
50+
HAS_CS_LIBRARIES = True
51+
except ImportError:
52+
HAS_CS_LIBRARIES = False
53+
4854
routes = web.RouteTableDef()
4955
PROJECT_ROOT = dirname(dirname(__file__))
5056
pool = None
@@ -322,97 +328,53 @@ async def linode_regions(_):
322328

323329
@routes.get('/cloudstack_config')
324330
async def get_cloudstack_config(_):
325-
response = {'has_secret': False}
326-
if 'CLOUDSTACK_CONFIG' in os.environ:
327-
try:
328-
open(os.environ['CLOUDSTACK_CONFIG'], 'r').read()
329-
response['has_secret'] = True
330-
except IOError:
331-
pass
332-
# check default path
333-
default_path = expanduser(join('~', '.cloudstack.ini'))
334-
try:
335-
open(default_path, 'r').read()
336-
response['has_secret'] = True
337-
except IOError:
338-
pass
331+
if not HAS_REQUESTS:
332+
return web.json_response({'error': 'missing_requests'}, status=400)
333+
if not HAS_CS_LIBRARIES:
334+
return web.json_response({'error': 'missing_cloudstack'}, status=400)
335+
response = {'has_secret': _read_cloudstack_config() is not None}
339336
return web.json_response(response)
340337

341338

342-
@routes.post('/cloudstack_config')
343-
async def post_cloudstack_config(request):
344-
data = await request.json()
345-
with open(join(PROJECT_ROOT, 'cloudstack.ini'), 'w') as f:
346-
try:
347-
config = data.config_text
348-
except Exception as e:
349-
return web.json_response({'error': {
350-
'code': type(e).__name__,
351-
'message': e,
352-
}}, status=400)
353-
else:
354-
f.write(config)
355-
return web.json_response({'ok': True})
356-
357-
358-
def _get_cloudstack_config(path=None):
359-
if path:
360-
try:
361-
return open(os.environ['CLOUDSTACK_CONFIG'], 'r').read()
362-
except IOError:
363-
pass
364-
339+
def _read_cloudstack_config():
365340
if 'CLOUDSTACK_CONFIG' in os.environ:
366341
try:
367342
return open(os.environ['CLOUDSTACK_CONFIG'], 'r').read()
368343
except IOError:
369344
pass
370-
345+
# check default path
371346
default_path = expanduser(join('~', '.cloudstack.ini'))
372-
return open(default_path, 'r').read()
373-
374-
375-
def _sign(command, secret):
376-
"""Adds the signature bit to a command expressed as a dict"""
377-
# order matters
378-
arguments = sorted(command.items())
379-
380-
# urllib.parse.urlencode is not good enough here.
381-
# key contains should only contain safe content already.
382-
# safe="*" is required when producing the signature.
383-
query_string = "&".join("=".join((key, quote(value, safe="*")))
384-
for key, value in arguments)
385-
386-
# Signing using HMAC-SHA1
387-
digest = hmac.new(
388-
secret.encode("utf-8"),
389-
msg=query_string.lower().encode("utf-8"),
390-
digestmod=hashlib.sha1).digest()
391-
392-
signature = base64.b64encode(digest).decode("utf-8")
393-
394-
return dict(command, signature=signature)
347+
try:
348+
return open(default_path, 'r').read()
349+
except IOError:
350+
pass
351+
return None
395352

396353

397-
@routes.get('/cloudstack_regions')
354+
@routes.post('/cloudstack_regions')
398355
async def cloudstack_regions(request):
399-
data = {} #await request.json()
356+
data = await request.json()
357+
client_config = data.get('token')
400358
config = configparser.ConfigParser()
401-
config.read_string(_get_cloudstack_config(data.get('cs_config')))
359+
config.read_string(_read_cloudstack_config() or client_config)
402360
section = config[config.sections()[0]]
403-
404-
compute_endpoint = section.get('endpoint', '')
405-
api_key = section.get('key', '')
406-
api_secret = section.get('secret', '')
407-
params = _sign({
408-
"command": "listZones",
409-
"apikey": api_key}, api_secret)
410-
query_string = urlencode(params)
411-
412-
async with ClientSession() as session:
413-
async with session.get(f'{compute_endpoint}?{query_string}') as r:
414-
json_body = await r.json()
415-
return web.json_response(json_body)
361+
client = AIOCloudStack(**section)
362+
try:
363+
zones = await client.listZones(fetch_list=True)
364+
except CloudStackApiException as resp:
365+
return web.json_response({
366+
'cloud_stack_error': resp.error
367+
}, status=400)
368+
# if config was passed from client, save it after successful zone retrieval
369+
if _read_cloudstack_config() is None:
370+
path = os.environ['CLOUDSTACK_CONFIG'] or expanduser(join('~', '.cloudstack.ini'))
371+
with open(path, 'w') as f:
372+
try:
373+
f.write(client_config)
374+
except IOError as e:
375+
return web.json_response({'error': 'can not save config file'}, status=400)
376+
377+
return web.json_response(zones)
416378

417379

418380
app = web.Application()

app/static/provider-cloudstack.vue

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
<template>
22
<div>
3-
<div v-if="ui_token_from_env">
4-
<div v-if="ui_token_from_env" class="form-text alert alert-success" role="alert">
5-
The config file was found on your system
6-
</div>
3+
<div v-if="ui_token_from_env" class="form-text alert alert-success" role="alert">
4+
The config file was found on your system
75
</div>
8-
<div class="form-group" v-else>
9-
6+
<div v-else class="form-group">
7+
<label>
8+
Enter your cloudstack.ini file contents below, it will be saved to your system.
9+
</label>
10+
<p>Example config file format (clickable):</p>
11+
<pre class="example" v-on:click="cs_config = ui_example_cfg">{{ ui_example_cfg }}</pre>
12+
<textarea v-model="cs_config"
13+
v-bind:disabled="ui_loading_check"
14+
v-on:blur="load_regions"
15+
class="form-control"
16+
rows="5"></textarea>
17+
<div v-if="ui_region_options.length > 0 && !ui_token_from_env" class="form-text alert alert-success" role="alert">
18+
The config file was saved on your system
19+
</div>
1020
</div>
1121
<region-select v-model="region"
1222
v-bind:options="ui_region_options"
1323
v-bind:loading="ui_loading_check || ui_loading_regions"
1424
v-bind:error="ui_region_error">
1525
</region-select>
26+
1627
<button v-on:click="submit"
1728
v-bind:disabled="!is_valid" class="btn btn-primary" type="button">Next</button>
1829
</div>
@@ -25,6 +36,11 @@ module.exports = {
2536
cs_config: null,
2637
region: null,
2738
// helper variables
39+
ui_example_cfg: '[exoscale]\n' +
40+
'endpoint = https://api.exoscale.com/compute\n' +
41+
'key = API Key here\n' +
42+
'secret = Secret key here\n' +
43+
'timeout = 30',
2844
ui_loading_check: false,
2945
ui_loading_regions: false,
3046
ui_region_error: null,
@@ -34,7 +50,7 @@ module.exports = {
3450
},
3551
computed: {
3652
is_valid() {
37-
return (this.ui_config_uploaded || this.ui_token_from_env) && this.region;
53+
return (this.cs_config || this.ui_token_from_env) && this.region;
3854
}
3955
},
4056
created: function() {
@@ -76,7 +92,7 @@ module.exports = {
7692
throw new Error(r.status);
7793
})
7894
.then((data) => {
79-
this.ui_region_options = data.regions.map(i => ({key: i.slug, value: i.name}));
95+
this.ui_region_options = data.map(i => ({key: i.name, value: i.name}));
8096
})
8197
.catch((err) => {
8298
this.ui_region_error = err;
@@ -104,3 +120,9 @@ module.exports = {
104120
}
105121
};
106122
</script>
123+
124+
<style scoped>
125+
.example {
126+
cursor: pointer;
127+
}
128+
</style>

app/static/provider-setup.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ module.exports = {
6666
'scaleway': window.httpVueLoader('/static/provider-scaleway.vue'),
6767
'hetzner': window.httpVueLoader('/static/provider-hetzner.vue'),
6868
'azure': window.httpVueLoader('/static/provider-azure.vue'),
69-
'linode': window.httpVueLoader('/static/provider-linode.vue')
69+
'linode': window.httpVueLoader('/static/provider-linode.vue'),
70+
'cloudstack': window.httpVueLoader('/static/provider-cloudstack.vue')
7071
}
7172
};
7273
</script>

0 commit comments

Comments
 (0)