algo/app/server.py
2021-07-06 22:53:25 +03:00

422 lines
12 KiB
Python

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
from playbook import PlaybookCLI
try:
import boto3
HAS_BOTO3 = True
except ImportError:
HAS_BOTO3 = False
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
try:
from google.auth.transport.requests import AuthorizedSession
from google.oauth2 import service_account
HAS_GOOGLE_LIBRARIES = True
except ImportError:
HAS_GOOGLE_LIBRARIES = False
try:
from azure.mgmt.automation import AutomationClient
import azure.mgmt.automation.models as AutomationModel
HAS_AZURE_LIBRARIES = True
except ImportError:
HAS_AZURE_LIBRARIES = False
routes = web.RouteTableDef()
PROJECT_ROOT = dirname(dirname(__file__))
pool = None
task_future = None
task_program = ''
def run_playbook(data):
global task_program
extra_vars = ' '.join(['{0}={1}'.format(key, data[key])
for key in data.keys()])
task_program = ['ansible-playbook', 'main.yml', '--extra-vars', extra_vars]
return PlaybookCLI(task_program).run()
@routes.get('/')
async def handle_index(_):
with open(join(PROJECT_ROOT, 'app', 'static', 'index.html'), 'r') as f:
return web.Response(body=f.read(), content_type='text/html')
@routes.get('/playbook')
async def playbook_get_handler(request):
if not task_future:
return web.json_response({'status': None})
if task_future.done():
try:
return web.json_response({'status': 'done', 'program': task_program, 'result': task_future.result()})
except ValueError as e:
return web.json_response({'status': 'error', 'program': task_program, 'result': str(e)})
elif task_future.cancelled():
return web.json_response({'status': 'cancelled', 'program': task_program})
else:
return web.json_response({'status': 'running', 'program': task_program})
@routes.post('/playbook')
async def playbook_post_handler(request):
global task_future
global pool
data = await request.json()
loop = asyncio.get_running_loop()
pool = concurrent.futures.ThreadPoolExecutor()
task_future = loop.run_in_executor(pool, run_playbook, data)
return web.json_response({'ok': True})
@routes.delete('/playbook')
async def playbook_delete_handler(_):
global task_future
if not task_future:
return web.json_response({'ok': False})
cancelled = task_future.cancel()
pool.shutdown(wait=False)
task_future = None
return web.json_response({'ok': cancelled})
@routes.get('/config')
async def get_config(_):
with open(join(PROJECT_ROOT, 'config.cfg'), 'r') as f:
config = yaml.safe_load(f.read())
return web.json_response(config)
@routes.post('/config')
async def post_config(request):
data = await request.json()
with open(join(PROJECT_ROOT, 'config.cfg'), 'w') as f:
try:
config = yaml.safe_dump(data)
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})
@routes.post('/exit')
async def post_exit(_):
if task_future and task_future.done():
sys.exit(0)
else:
sys.exit(1)
@routes.get('/do_config')
async def check_do_config(_):
return web.json_response({'has_secret': 'DO_API_TOKEN' in os.environ})
@routes.post('/do_regions')
async def do_regions(request):
data = await request.json()
token = data.get('token', os.environ.get('DO_API_TOKEN'))
if not token:
return web.json_response({'error': 'no token provided'}, status=400)
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer {0}'.format(token),
}
async with ClientSession(headers=headers) as session:
async with session.get('https://api.digitalocean.com/v2/regions') as r:
json_body = await r.json()
return web.json_response(json_body, status=r.status)
@routes.get('/aws_config')
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})
@routes.post('/lightsail_regions')
async def lightsail_regions(request):
data = await request.json()
client = boto3.client(
'lightsail',
aws_access_key_id=data.get('aws_access_key'),
aws_secret_access_key=data.get('aws_secret_key')
)
response = client.get_regions(
includeAvailabilityZones=False
)
return web.json_response(response)
@routes.post('/ec2_regions')
async def ec2_regions(request):
data = await request.json()
client = boto3.client(
'ec2',
aws_access_key_id=data.get('aws_access_key'),
aws_secret_access_key=data.get('aws_secret_key')
)
response = client.describe_regions()['Regions']
return web.json_response(response)
@routes.get('/gce_config')
async def check_gce_config(_):
if not HAS_REQUESTS:
return web.json_response({'error': 'missing_requests'}, status=400)
if not HAS_GOOGLE_LIBRARIES:
return web.json_response({'error': 'missing_google'}, status=400)
gce_file = join(PROJECT_ROOT, 'configs', 'gce.json')
response = {}
try:
json.loads(open(gce_file, 'r').read())['project_id']
response['status'] = 'ok'
except IOError:
response['status'] = 'not_available'
except ValueError:
response['status'] = 'wrong_format'
return web.json_response(response)
@routes.post('/gce_regions')
async def gce_regions(request):
data = await request.json()
gce_file_name = join(PROJECT_ROOT, 'configs', 'gce.json')
if data.get('project_id'):
# File is missing, save it. We can't get file path from browser :(
with open(gce_file_name, 'w') as f:
f.write(json.dumps(data))
else:
with open(gce_file_name, 'r') as f:
data = json.loads(f.read())
response = AuthorizedSession(
service_account.Credentials.from_service_account_info(
data).with_scopes(
['https://www.googleapis.com/auth/compute'])).get(
'https://www.googleapis.com/compute/v1/projects/{project_id}/regions'.format(
project_id=data['project_id'])
)
return web.json_response(json.loads(response.content))
@routes.get('/vultr_config')
async def check_vultr_config(request):
default_path = expanduser(join('~', '.vultr.ini'))
response = {'has_secret': False}
try:
open(default_path, 'r').read()
response['has_secret'] = True
except IOError:
pass
if 'VULTR_API_CONFIG' in os.environ:
try:
open(os.environ['VULTR_API_CONFIG'], 'r').read()
response['has_secret'] = True
except IOError:
pass
return web.json_response(response)
@routes.get('/vultr_regions')
async def vultr_regions(_):
async with ClientSession() as session:
async with session.get('https://api.vultr.com/v1/regions/list') as r:
json_body = await r.json()
return web.json_response(json_body)
@routes.get('/scaleway_config')
async def check_scaleway_config(_):
return web.json_response({"has_secret": 'SCW_TOKEN' in os.environ})
@routes.get('/hetzner_config')
async def check_hetzner_config(_):
return web.json_response({"has_secret": 'HCLOUD_TOKEN' in os.environ})
@routes.post('/hetzner_regions')
async def hetzner_regions(request):
data = await request.json()
token = data.get('token', os.environ.get('HCLOUD_TOKEN'))
if not token:
return web.json_response({'error': 'no token provided'}, status=400)
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer {0}'.format(token),
}
async with ClientSession(headers=headers) as session:
async with session.get('https://api.hetzner.cloud/v1/datacenters') as r:
json_body = await r.json()
return web.json_response(json_body)
@routes.get('/azure_config')
async def azure_config(_):
if not HAS_REQUESTS:
return web.json_response({'error': 'missing_requests'}, status=400)
if not HAS_AZURE_LIBRARIES:
return web.json_response({'error': 'missing_azure'}, status=400)
response = {'status': 'ok'}
return web.json_response(response)
@routes.get('/azure_regions')
async def azure_regions(_):
with open(join(PROJECT_ROOT, 'roles', 'cloud-azure', 'defaults', 'main.yml'), 'r') as f:
regions_json = yaml.safe_load(f.read())
regions = json.loads(regions_json['_azure_regions'])
return web.json_response(regions)
@routes.get('/linode_config')
async def linode_config(_):
return web.json_response({"has_secret": 'LINODE_API_TOKEN' in os.environ})
@routes.get('/linode_regions')
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'))])
app.add_routes([web.static('/results', join(PROJECT_ROOT, 'configs'))])
web.run_app(app, port=9000)