Added CloudStack (exoscale) provider setup

This commit is contained in:
Ivan Gromov 2021-05-30 00:34:01 +05:00
parent 465b8e6e5c
commit 27d21db3c9
3 changed files with 70 additions and 85 deletions

View file

@ -45,6 +45,12 @@ try:
except ImportError:
HAS_AZURE_LIBRARIES = False
try:
from cs import AIOCloudStack, CloudStackApiException
HAS_CS_LIBRARIES = True
except ImportError:
HAS_CS_LIBRARIES = False
routes = web.RouteTableDef()
PROJECT_ROOT = dirname(dirname(__file__))
pool = None
@ -322,97 +328,53 @@ async def linode_regions(_):
@routes.get('/cloudstack_config')
async def get_cloudstack_config(_):
response = {'has_secret': False}
if not HAS_REQUESTS:
return web.json_response({'error': 'missing_requests'}, status=400)
if not HAS_CS_LIBRARIES:
return web.json_response({'error': 'missing_cloudstack'}, status=400)
response = {'has_secret': _read_cloudstack_config() is not None}
return web.json_response(response)
def _read_cloudstack_config():
if 'CLOUDSTACK_CONFIG' in os.environ:
try:
open(os.environ['CLOUDSTACK_CONFIG'], 'r').read()
response['has_secret'] = True
return open(os.environ['CLOUDSTACK_CONFIG'], 'r').read()
except IOError:
pass
# check default path
default_path = expanduser(join('~', '.cloudstack.ini'))
try:
open(default_path, 'r').read()
response['has_secret'] = True
return open(default_path, 'r').read()
except IOError:
pass
return web.json_response(response)
return None
@routes.post('/cloudstack_config')
async def post_cloudstack_config(request):
data = await request.json()
with open(join(PROJECT_ROOT, 'cloudstack.ini'), 'w') as f:
try:
config = data.config_text
except Exception as e:
return web.json_response({'error': {
'code': type(e).__name__,
'message': e,
}}, status=400)
else:
f.write(config)
return web.json_response({'ok': True})
def _get_cloudstack_config(path=None):
if path:
try:
return open(os.environ['CLOUDSTACK_CONFIG'], 'r').read()
except IOError:
pass
if 'CLOUDSTACK_CONFIG' in os.environ:
try:
return open(os.environ['CLOUDSTACK_CONFIG'], 'r').read()
except IOError:
pass
default_path = expanduser(join('~', '.cloudstack.ini'))
return open(default_path, 'r').read()
def _sign(command, secret):
"""Adds the signature bit to a command expressed as a dict"""
# order matters
arguments = sorted(command.items())
# urllib.parse.urlencode is not good enough here.
# key contains should only contain safe content already.
# safe="*" is required when producing the signature.
query_string = "&".join("=".join((key, quote(value, safe="*")))
for key, value in arguments)
# Signing using HMAC-SHA1
digest = hmac.new(
secret.encode("utf-8"),
msg=query_string.lower().encode("utf-8"),
digestmod=hashlib.sha1).digest()
signature = base64.b64encode(digest).decode("utf-8")
return dict(command, signature=signature)
@routes.get('/cloudstack_regions')
@routes.post('/cloudstack_regions')
async def cloudstack_regions(request):
data = {} #await request.json()
data = await request.json()
client_config = data.get('token')
config = configparser.ConfigParser()
config.read_string(_get_cloudstack_config(data.get('cs_config')))
config.read_string(_read_cloudstack_config() or client_config)
section = config[config.sections()[0]]
client = AIOCloudStack(**section)
try:
zones = await client.listZones(fetch_list=True)
except CloudStackApiException as resp:
return web.json_response({
'cloud_stack_error': resp.error
}, status=400)
# if config was passed from client, save it after successful zone retrieval
if _read_cloudstack_config() is None:
path = os.environ['CLOUDSTACK_CONFIG'] or expanduser(join('~', '.cloudstack.ini'))
with open(path, 'w') as f:
try:
f.write(client_config)
except IOError as e:
return web.json_response({'error': 'can not save config file'}, status=400)
compute_endpoint = section.get('endpoint', '')
api_key = section.get('key', '')
api_secret = section.get('secret', '')
params = _sign({
"command": "listZones",
"apikey": api_key}, api_secret)
query_string = urlencode(params)
async with ClientSession() as session:
async with session.get(f'{compute_endpoint}?{query_string}') as r:
json_body = await r.json()
return web.json_response(json_body)
return web.json_response(zones)
app = web.Application()

View file

@ -1,18 +1,29 @@
<template>
<div>
<div v-if="ui_token_from_env">
<div v-if="ui_token_from_env" class="form-text alert alert-success" role="alert">
The config file was found on your system
</div>
<div v-if="ui_token_from_env" class="form-text alert alert-success" role="alert">
The config file was found on your system
</div>
<div class="form-group" v-else>
<div v-else class="form-group">
<label>
Enter your cloudstack.ini file contents below, it will be saved to your system.
</label>
<p>Example config file format (clickable):</p>
<pre class="example" v-on:click="cs_config = ui_example_cfg">{{ ui_example_cfg }}</pre>
<textarea v-model="cs_config"
v-bind:disabled="ui_loading_check"
v-on:blur="load_regions"
class="form-control"
rows="5"></textarea>
<div v-if="ui_region_options.length > 0 && !ui_token_from_env" class="form-text alert alert-success" role="alert">
The config file was saved on your system
</div>
</div>
<region-select v-model="region"
v-bind:options="ui_region_options"
v-bind:loading="ui_loading_check || ui_loading_regions"
v-bind:error="ui_region_error">
</region-select>
<button v-on:click="submit"
v-bind:disabled="!is_valid" class="btn btn-primary" type="button">Next</button>
</div>
@ -25,6 +36,11 @@ module.exports = {
cs_config: null,
region: null,
// helper variables
ui_example_cfg: '[exoscale]\n' +
'endpoint = https://api.exoscale.com/compute\n' +
'key = API Key here\n' +
'secret = Secret key here\n' +
'timeout = 30',
ui_loading_check: false,
ui_loading_regions: false,
ui_region_error: null,
@ -34,7 +50,7 @@ module.exports = {
},
computed: {
is_valid() {
return (this.ui_config_uploaded || this.ui_token_from_env) && this.region;
return (this.cs_config || this.ui_token_from_env) && this.region;
}
},
created: function() {
@ -76,7 +92,7 @@ module.exports = {
throw new Error(r.status);
})
.then((data) => {
this.ui_region_options = data.regions.map(i => ({key: i.slug, value: i.name}));
this.ui_region_options = data.map(i => ({key: i.name, value: i.name}));
})
.catch((err) => {
this.ui_region_error = err;
@ -104,3 +120,9 @@ module.exports = {
}
};
</script>
<style scoped>
.example {
cursor: pointer;
}
</style>

View file

@ -66,7 +66,8 @@ module.exports = {
'scaleway': window.httpVueLoader('/static/provider-scaleway.vue'),
'hetzner': window.httpVueLoader('/static/provider-hetzner.vue'),
'azure': window.httpVueLoader('/static/provider-azure.vue'),
'linode': window.httpVueLoader('/static/provider-linode.vue')
'linode': window.httpVueLoader('/static/provider-linode.vue'),
'cloudstack': window.httpVueLoader('/static/provider-cloudstack.vue')
}
};
</script>