Skip to content

Commit 1db0705

Browse files
committed
WIP: CLoudStack provider
1 parent 1bf3a80 commit 1db0705

File tree

3 files changed

+327
-2
lines changed

3 files changed

+327
-2
lines changed

app/server.py

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import asyncio
2+
import base64
23
import concurrent.futures
4+
import configparser
5+
import hashlib
6+
import hmac
37
import json
48
import os
59
import sys
610
from os.path import join, dirname, expanduser
11+
from urllib.parse import quote, urlencode
712

813
import yaml
914
from aiohttp import web, ClientSession
@@ -158,7 +163,8 @@ async def do_regions(request):
158163
async def aws_config(_):
159164
if not HAS_BOTO3:
160165
return web.json_response({'error': 'missing_boto'}, status=400)
161-
return web.json_response({'has_secret': 'AWS_ACCESS_KEY_ID' in os.environ and 'AWS_SECRET_ACCESS_KEY' in os.environ})
166+
return web.json_response(
167+
{'has_secret': 'AWS_ACCESS_KEY_ID' in os.environ and 'AWS_SECRET_ACCESS_KEY' in os.environ})
162168

163169

164170
@routes.post('/lightsail_regions')
@@ -307,13 +313,108 @@ async def linode_config(_):
307313

308314

309315
@routes.get('/linode_regions')
310-
async def linode_config(_):
316+
async def linode_regions(_):
311317
async with ClientSession() as session:
312318
async with session.get('https://api.linode.com/v4/regions') as r:
313319
json_body = await r.json()
314320
return web.json_response(json_body)
315321

316322

323+
@routes.get('/cloudstack_config')
324+
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
339+
return web.json_response(response)
340+
341+
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+
365+
if 'CLOUDSTACK_CONFIG' in os.environ:
366+
try:
367+
return open(os.environ['CLOUDSTACK_CONFIG'], 'r').read()
368+
except IOError:
369+
pass
370+
371+
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)
395+
396+
397+
@routes.get('/cloudstack_regions')
398+
async def cloudstack_regions(request):
399+
data = {} #await request.json()
400+
config = configparser.ConfigParser()
401+
config.read_string(_get_cloudstack_config(data.get('cs_config')))
402+
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)
416+
417+
317418
app = web.Application()
318419
app.router.add_routes(routes)
319420
app.add_routes([web.static('/static', join(PROJECT_ROOT, 'app', 'static'))])

app/static/provider-blank.vue

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<template>
2+
<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 token was read from the environment variable
6+
</div>
7+
</div>
8+
<div class="form-group" v-else>
9+
<label for="id_do_token">
10+
Enter your API token. The token must have read and write permissions
11+
<a href="https://cloud.digitalocean.com/settings/api/tokens" title="https://cloud.digitalocean.com/settings/api/tokens" class="badge bagde-pill badge-primary" target="_blank" rel="noopener noreferrer">?</a>
12+
</label>
13+
<input
14+
type="text"
15+
class="form-control"
16+
id="id_do_token"
17+
name="do_token"
18+
v-bind:disabled="ui_loading_check"
19+
v-model="do_token"
20+
@blur="load_regions"
21+
/>
22+
</div>
23+
<region-select v-model="region"
24+
v-bind:options="ui_region_options"
25+
v-bind:loading="ui_loading_check || ui_loading_regions"
26+
v-bind:error="ui_region_error">
27+
</region-select>
28+
<button v-on:click="submit"
29+
v-bind:disabled="!is_valid" class="btn btn-primary" type="button">Next</button>
30+
</div>
31+
</template>
32+
33+
<script>
34+
module.exports = {
35+
data: function() {
36+
return {
37+
do_token: null,
38+
region: null,
39+
// helper variables
40+
ui_loading_check: false,
41+
ui_loading_regions: false,
42+
ui_region_error: null,
43+
ui_token_from_env: false,
44+
ui_region_options: []
45+
}
46+
},
47+
computed: {
48+
is_valid() {
49+
return (this.do_token || this.ui_token_from_env) && this.region;
50+
}
51+
},
52+
created: function() {
53+
this.check_config();
54+
},
55+
methods: {
56+
check_config() {
57+
this.ui_loading_check = true;
58+
return fetch("/do_config")
59+
.then(r => r.json())
60+
.then(response => {
61+
if (response.has_secret) {
62+
this.ui_token_from_env = true;
63+
this.load_regions();
64+
}
65+
})
66+
.finally(() => {
67+
this.ui_loading_check = false;
68+
});
69+
},
70+
load_regions() {
71+
if (this.ui_token_from_env || this.do_token) {
72+
this.ui_loading_regions = true;
73+
this.ui_region_error = null;
74+
const payload = this.ui_token_from_env ? {} : {
75+
token: this.do_token
76+
};
77+
fetch("/do_regions", {
78+
method: 'post',
79+
headers: {
80+
'Content-Type': 'application/json'
81+
},
82+
body: JSON.stringify(payload)
83+
})
84+
.then((r) => {
85+
if (r.status === 200) {
86+
return r.json();
87+
}
88+
throw new Error(r.status);
89+
})
90+
.then((data) => {
91+
this.ui_region_options = data.regions.map(i => ({key: i.slug, value: i.name}));
92+
})
93+
.catch((err) => {
94+
this.ui_region_error = err;
95+
})
96+
.finally(() => {
97+
this.ui_loading_regions = false;
98+
});
99+
}
100+
},
101+
submit() {
102+
if (this.ui_token_from_env) {
103+
this.$emit("submit", {
104+
region: this.region
105+
});
106+
} else {
107+
this.$emit("submit", {
108+
do_token: this.do_token,
109+
region: this.region
110+
});
111+
}
112+
}
113+
},
114+
components: {
115+
"region-select": window.httpVueLoader("/static/region-select.vue"),
116+
}
117+
};
118+
</script>

app/static/provider-cloudstack.vue

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<template>
2+
<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>
7+
</div>
8+
<div class="form-group" v-else>
9+
10+
</div>
11+
<region-select v-model="region"
12+
v-bind:options="ui_region_options"
13+
v-bind:loading="ui_loading_check || ui_loading_regions"
14+
v-bind:error="ui_region_error">
15+
</region-select>
16+
<button v-on:click="submit"
17+
v-bind:disabled="!is_valid" class="btn btn-primary" type="button">Next</button>
18+
</div>
19+
</template>
20+
21+
<script>
22+
module.exports = {
23+
data: function() {
24+
return {
25+
cs_config: null,
26+
region: null,
27+
// helper variables
28+
ui_loading_check: false,
29+
ui_loading_regions: false,
30+
ui_region_error: null,
31+
ui_token_from_env: false,
32+
ui_region_options: []
33+
}
34+
},
35+
computed: {
36+
is_valid() {
37+
return (this.ui_config_uploaded || this.ui_token_from_env) && this.region;
38+
}
39+
},
40+
created: function() {
41+
this.check_config();
42+
},
43+
methods: {
44+
check_config() {
45+
this.ui_loading_check = true;
46+
return fetch("/cloudstack_config")
47+
.then(r => r.json())
48+
.then(response => {
49+
if (response.has_secret) {
50+
this.ui_token_from_env = true;
51+
this.load_regions();
52+
}
53+
})
54+
.finally(() => {
55+
this.ui_loading_check = false;
56+
});
57+
},
58+
load_regions() {
59+
if (this.ui_token_from_env || this.cs_config) {
60+
this.ui_loading_regions = true;
61+
this.ui_region_error = null;
62+
const payload = this.ui_token_from_env ? {} : {
63+
token: this.cs_config
64+
};
65+
fetch("/cloudstack_regions", {
66+
method: 'post',
67+
headers: {
68+
'Content-Type': 'application/json'
69+
},
70+
body: JSON.stringify(payload)
71+
})
72+
.then((r) => {
73+
if (r.status === 200) {
74+
return r.json();
75+
}
76+
throw new Error(r.status);
77+
})
78+
.then((data) => {
79+
this.ui_region_options = data.regions.map(i => ({key: i.slug, value: i.name}));
80+
})
81+
.catch((err) => {
82+
this.ui_region_error = err;
83+
})
84+
.finally(() => {
85+
this.ui_loading_regions = false;
86+
});
87+
}
88+
},
89+
submit() {
90+
if (this.ui_token_from_env) {
91+
this.$emit("submit", {
92+
region: this.region
93+
});
94+
} else {
95+
this.$emit("submit", {
96+
cs_config: this.cs_config,
97+
region: this.region
98+
});
99+
}
100+
}
101+
},
102+
components: {
103+
"region-select": window.httpVueLoader("/static/region-select.vue"),
104+
}
105+
};
106+
</script>

0 commit comments

Comments
 (0)