WIP: CLoudStack provider

This commit is contained in:
Ivan Gromov 2021-01-30 01:49:06 +05:00
parent 01bd17d361
commit 465b8e6e5c
3 changed files with 327 additions and 2 deletions

View file

@ -1,9 +1,14 @@
import asyncio
import base64
import concurrent.futures
import configparser
import hashlib
import hmac
import json
import os
import sys
from os.path import join, dirname, expanduser
from urllib.parse import quote, urlencode
import yaml
from aiohttp import web, ClientSession
@ -158,7 +163,8 @@ async def do_regions(request):
async def aws_config(_):
if not HAS_BOTO3:
return web.json_response({'error': 'missing_boto'}, status=400)
return web.json_response({'has_secret': 'AWS_ACCESS_KEY_ID' in os.environ and 'AWS_SECRET_ACCESS_KEY' in os.environ})
return web.json_response(
{'has_secret': 'AWS_ACCESS_KEY_ID' in os.environ and 'AWS_SECRET_ACCESS_KEY' in os.environ})
@routes.post('/lightsail_regions')
@ -307,13 +313,108 @@ async def linode_config(_):
@routes.get('/linode_regions')
async def linode_config(_):
async def linode_regions(_):
async with ClientSession() as session:
async with session.get('https://api.linode.com/v4/regions') as r:
json_body = await r.json()
return web.json_response(json_body)
@routes.get('/cloudstack_config')
async def get_cloudstack_config(_):
response = {'has_secret': False}
if 'CLOUDSTACK_CONFIG' in os.environ:
try:
open(os.environ['CLOUDSTACK_CONFIG'], 'r').read()
response['has_secret'] = True
except IOError:
pass
# check default path
default_path = expanduser(join('~', '.cloudstack.ini'))
try:
open(default_path, 'r').read()
response['has_secret'] = True
except IOError:
pass
return web.json_response(response)
@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')
async def cloudstack_regions(request):
data = {} #await request.json()
config = configparser.ConfigParser()
config.read_string(_get_cloudstack_config(data.get('cs_config')))
section = config[config.sections()[0]]
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)
app = web.Application()
app.router.add_routes(routes)
app.add_routes([web.static('/static', join(PROJECT_ROOT, 'app', 'static'))])

View file

@ -0,0 +1,118 @@
<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 token was read from the environment variable
</div>
</div>
<div class="form-group" v-else>
<label for="id_do_token">
Enter your API token. The token must have read and write permissions
<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>
</label>
<input
type="text"
class="form-control"
id="id_do_token"
name="do_token"
v-bind:disabled="ui_loading_check"
v-model="do_token"
@blur="load_regions"
/>
</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>
</template>
<script>
module.exports = {
data: function() {
return {
do_token: null,
region: null,
// helper variables
ui_loading_check: false,
ui_loading_regions: false,
ui_region_error: null,
ui_token_from_env: false,
ui_region_options: []
}
},
computed: {
is_valid() {
return (this.do_token || this.ui_token_from_env) && this.region;
}
},
created: function() {
this.check_config();
},
methods: {
check_config() {
this.ui_loading_check = true;
return fetch("/do_config")
.then(r => r.json())
.then(response => {
if (response.has_secret) {
this.ui_token_from_env = true;
this.load_regions();
}
})
.finally(() => {
this.ui_loading_check = false;
});
},
load_regions() {
if (this.ui_token_from_env || this.do_token) {
this.ui_loading_regions = true;
this.ui_region_error = null;
const payload = this.ui_token_from_env ? {} : {
token: this.do_token
};
fetch("/do_regions", {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then((r) => {
if (r.status === 200) {
return r.json();
}
throw new Error(r.status);
})
.then((data) => {
this.ui_region_options = data.regions.map(i => ({key: i.slug, value: i.name}));
})
.catch((err) => {
this.ui_region_error = err;
})
.finally(() => {
this.ui_loading_regions = false;
});
}
},
submit() {
if (this.ui_token_from_env) {
this.$emit("submit", {
region: this.region
});
} else {
this.$emit("submit", {
do_token: this.do_token,
region: this.region
});
}
}
},
components: {
"region-select": window.httpVueLoader("/static/region-select.vue"),
}
};
</script>

View file

@ -0,0 +1,106 @@
<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>
<div class="form-group" v-else>
</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>
</template>
<script>
module.exports = {
data: function() {
return {
cs_config: null,
region: null,
// helper variables
ui_loading_check: false,
ui_loading_regions: false,
ui_region_error: null,
ui_token_from_env: false,
ui_region_options: []
}
},
computed: {
is_valid() {
return (this.ui_config_uploaded || this.ui_token_from_env) && this.region;
}
},
created: function() {
this.check_config();
},
methods: {
check_config() {
this.ui_loading_check = true;
return fetch("/cloudstack_config")
.then(r => r.json())
.then(response => {
if (response.has_secret) {
this.ui_token_from_env = true;
this.load_regions();
}
})
.finally(() => {
this.ui_loading_check = false;
});
},
load_regions() {
if (this.ui_token_from_env || this.cs_config) {
this.ui_loading_regions = true;
this.ui_region_error = null;
const payload = this.ui_token_from_env ? {} : {
token: this.cs_config
};
fetch("/cloudstack_regions", {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then((r) => {
if (r.status === 200) {
return r.json();
}
throw new Error(r.status);
})
.then((data) => {
this.ui_region_options = data.regions.map(i => ({key: i.slug, value: i.name}));
})
.catch((err) => {
this.ui_region_error = err;
})
.finally(() => {
this.ui_loading_regions = false;
});
}
},
submit() {
if (this.ui_token_from_env) {
this.$emit("submit", {
region: this.region
});
} else {
this.$emit("submit", {
cs_config: this.cs_config,
region: this.region
});
}
}
},
components: {
"region-select": window.httpVueLoader("/static/region-select.vue"),
}
};
</script>