mirror of https://github.com/trailofbits/algo
Merge aa0fff068e
into 6ce6f5c81e
commit
ccd243bbfa
@ -0,0 +1,6 @@
|
||||
*
|
||||
!requirements.txt
|
||||
!playbook.py
|
||||
!server.py
|
||||
!run.sh
|
||||
!static
|
@ -0,0 +1,10 @@
|
||||
FROM algo
|
||||
ARG BUILD_PACKAGES="gcc libffi-dev linux-headers make musl-dev openssl-dev"
|
||||
COPY requirements.txt /app/requirements.txt
|
||||
RUN apk --no-cache add ${BUILD_PACKAGES} && \
|
||||
source .env/bin/activate && \
|
||||
python3 -m pip --no-cache-dir install -r app/requirements.txt && \
|
||||
apk del ${BUILD_PACKAGES}
|
||||
COPY . app
|
||||
RUN chmod 0755 /algo/app/run.sh
|
||||
CMD [ "/algo/app/run.sh" ]
|
@ -0,0 +1,39 @@
|
||||
# Algo web app UI
|
||||
|
||||
## Abstract
|
||||
|
||||
[A short description of what project does]
|
||||
|
||||
## Background
|
||||
|
||||
VUE docs,
|
||||
asyncio docs
|
||||
|
||||
## Rationale
|
||||
|
||||
Why A not B
|
||||
(vue is modern and doesn't require build system and depoendency)
|
||||
asyncio - same
|
||||
PBEX is patched because shell access considered insecure
|
||||
|
||||
## Implementation
|
||||
|
||||
app/server.py management, threading
|
||||
config yaml writer
|
||||
generic approach to provider UI (set required fields, validation, inherit ENV, try to detect)
|
||||
how progress displayed
|
||||
|
||||
## testing
|
||||
testing js: vue-test-library + loader
|
||||
testing python: pytests
|
||||
testing pbex compatibility: demo yaml
|
||||
|
||||
## Compatibility (if applicable)
|
||||
|
||||
[A discussion of the change with regard to the compatibility.]
|
||||
Due to ansible doesn't have API, have to manually check if custom PBEX would still work
|
||||
|
||||
## Open issues (if applicable)
|
||||
|
||||
Still requires pip install, consider py2exe, pyinstaller
|
||||
No task progress displayed, require callback module
|
@ -0,0 +1,3 @@
|
||||
aiohttp==3.8.1
|
||||
ansible-runner==2.2.1
|
||||
boto3
|
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
ALGO_DIR="/algo"
|
||||
DATA_DIR="/data"
|
||||
|
||||
if [ -z ${VIRTUAL_ENV+x} ]
|
||||
then
|
||||
ACTIVATE_SCRIPT="/algo/.env/bin/activate"
|
||||
if [ -f "$ACTIVATE_SCRIPT" ]
|
||||
then
|
||||
# shellcheck source=/dev/null
|
||||
source "$ACTIVATE_SCRIPT"
|
||||
else
|
||||
echo "$ACTIVATE_SCRIPT not found. Did you follow documentation to install dependencies?"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
tr -d '\r' < "${DATA_DIR}"/config.cfg > "${ALGO_DIR}"/config.cfg
|
||||
test -d "${DATA_DIR}"/configs && rsync -qLktr --delete "${DATA_DIR}"/configs "${ALGO_DIR}"/
|
||||
python app/server.py
|
||||
rsync -qLktr --delete "${ALGO_DIR}"/configs "${DATA_DIR}"/
|
||||
exit ${retcode}
|
@ -0,0 +1,462 @@
|
||||
import configparser
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from os.path import join, dirname, expanduser
|
||||
from functools import reduce
|
||||
|
||||
import ansible_runner
|
||||
import yaml
|
||||
from aiohttp import web, ClientSession
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
task_future = None
|
||||
task_program = ''
|
||||
|
||||
|
||||
class Status:
|
||||
RUNNING = 'running'
|
||||
ERROR = 'failed'
|
||||
TIMEOUT = 'timeout'
|
||||
CANCELLED = 'canceled'
|
||||
DONE = 'successful'
|
||||
NEW = None
|
||||
|
||||
|
||||
def by_path(data: dict, path: str):
|
||||
def get(obj, attr):
|
||||
if type(obj) is dict:
|
||||
return obj.get(attr, None)
|
||||
elif type(obj) is list:
|
||||
try:
|
||||
return obj[int(attr)]
|
||||
except ValueError:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
return reduce(get, path.split('.'), data)
|
||||
|
||||
class Playbook:
|
||||
|
||||
def __init__(self):
|
||||
self.status = Status.NEW
|
||||
self.want_cancel = False
|
||||
self.events = []
|
||||
self.config_vars = {}
|
||||
self._runner = None
|
||||
|
||||
def parse(self, event: dict):
|
||||
data = {}
|
||||
if by_path(event, 'event_data.task') == 'Set subjectAltName as a fact':
|
||||
ansible_ssh_host = by_path(event, 'event_data.res.ansible_facts.IP_subject_alt_name')
|
||||
if ansible_ssh_host:
|
||||
data['ansible_ssh_host'] = ansible_ssh_host
|
||||
|
||||
if by_path(event, 'event_data.play') == 'Configure the server and install required software':
|
||||
local_service_ip = by_path(event, 'event_data.res.ansible_facts.ansible_lo.ipv4_secondaries.0.address')
|
||||
ipv6 = by_path(event, 'event_data.res.ansible_facts.ansible_lo.ipv6.0.address')
|
||||
p12_export_password = by_path(event, 'event_data.res.ansible_facts.p12_export_password')
|
||||
if local_service_ip:
|
||||
data['local_service_ip'] = local_service_ip
|
||||
if ipv6:
|
||||
data['ipv6'] = ipv6
|
||||
if p12_export_password:
|
||||
data['p12_export_password'] = p12_export_password
|
||||
|
||||
if by_path(event, 'event_data.play') == 'Provision the server':
|
||||
host_name = by_path(event, 'event_data.res.add_host.host_name')
|
||||
if host_name:
|
||||
data['host_name'] = host_name
|
||||
|
||||
return data if data else None
|
||||
|
||||
def event_handler(self, data: dict) -> None:
|
||||
if self.parse(data):
|
||||
self.config_vars.update(self.parse(data))
|
||||
|
||||
self.events.append(data)
|
||||
|
||||
def status_handler(self, status_data: dict, *args, **kwargs) -> None:
|
||||
self.status = status_data.get('status')
|
||||
|
||||
def cancel_handler(self) -> bool:
|
||||
if self.want_cancel:
|
||||
self.status = Status.CANCELLED
|
||||
return self.want_cancel
|
||||
|
||||
def cancel(self) -> None:
|
||||
self.want_cancel = True
|
||||
|
||||
def run(self, extra_vars: dict) -> None:
|
||||
self.want_cancel = False
|
||||
self.status = Status.RUNNING
|
||||
_, runner = ansible_runner.run_async(
|
||||
private_data_dir='.',
|
||||
playbook='main.yml',
|
||||
extravars=extra_vars,
|
||||
status_handler=self.status_handler,
|
||||
cancel_callback=self.cancel_handler,
|
||||
event_handler=self.event_handler
|
||||
)
|
||||
self._runner = runner
|
||||
|
||||
|
||||
playbook = Playbook()
|
||||
|
||||
|
||||
def run_playbook(data: dict):
|
||||
return playbook.run(data)
|
||||
|
||||
|
||||
@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(_):
|
||||
return web.json_response({
|
||||
'status': playbook.status,
|
||||
'result': playbook.config_vars if playbook.status == Status.DONE else {},
|
||||
'events': playbook.events,
|
||||
})
|
||||
|
||||
|
||||
@routes.post('/playbook')
|
||||
async def playbook_post_handler(request):
|
||||
data = await request.json()
|
||||
run_playbook(data)
|
||||
return web.json_response({'ok': True})
|
||||
|
||||
|
||||
@routes.delete('/playbook')
|
||||
async def playbook_delete_handler(_):
|
||||
playbook.cancel()
|
||||
return web.json_response({'ok': True})
|
||||
|
||||
|
||||
@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):
|
||||
response = {'has_secret': False}
|
||||
if 'VULTR_API_CONFIG' in os.environ:
|
||||
try:
|
||||
open(os.environ['VULTR_API_CONFIG'], 'r').read()
|
||||
response['has_secret'] = True
|
||||
response['saved_to'] = os.environ.get('VULTR_API_CONFIG')
|
||||
except IOError:
|
||||
pass
|
||||
try:
|
||||
default_path = expanduser(join('~', '.vultr.ini'))
|
||||
open(default_path, 'r').read()
|
||||
response['has_secret'] = True
|
||||
response['saved_to'] = default_path
|
||||
except IOError:
|
||||
pass
|
||||
return web.json_response(response)
|
||||
|
||||
|
||||
@routes.post('/vultr_config')
|
||||
async def save_vultr_config(request):
|
||||
data = await request.json()
|
||||
token = data.get('token')
|
||||
path = os.environ.get('VULTR_API_CONFIG') or expanduser(join('~', '.vultr.ini'))
|
||||
with open(path, 'w') as f:
|
||||
try:
|
||||
f.write('[default]\nkey = {0}'.format(token))
|
||||
except IOError:
|
||||
return web.json_response({'error': 'can not save config file'}, status=400)
|
||||
return web.json_response({'saved_to': path})
|
||||
|
||||
|
||||
@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 check_cloudstack_config(_):
|
||||
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:
|
||||
return open(os.environ['CLOUDSTACK_CONFIG'], 'r').read()
|
||||
except IOError:
|
||||
pass
|
||||
# check default path
|
||||
default_path = expanduser(join('~', '.cloudstack.ini'))
|
||||
try:
|
||||
return open(default_path, 'r').read()
|
||||
except IOError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
@routes.post('/cloudstack_regions')
|
||||
async def cloudstack_regions(request):
|
||||
data = await request.json()
|
||||
client_config = data.get('token')
|
||||
config = configparser.ConfigParser()
|
||||
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.get('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)
|
||||
|
||||
return web.json_response(zones)
|
||||
|
||||
|
||||
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)
|
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<section class="my-3">
|
||||
<pre class="code"><code>
|
||||
{{cli_preview}}
|
||||
</code></pre>
|
||||
<button v-on:click="$emit('submit')" class="btn btn-primary" type="button">Run!</button>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
props: ['extra_args'],
|
||||
computed: {
|
||||
cli_preview: function() {
|
||||
let args = "";
|
||||
for (arg in this.extra_args) {
|
||||
args += `${arg}=${this.extra_args[arg]} `;
|
||||
}
|
||||
return `ansible-playbook main.yml --extra-vars ${args}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.code {
|
||||
white-space: normal;
|
||||
background: black;
|
||||
color: lightgrey;
|
||||
padding: 1em;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="ui_config_error && ui_config_error === 'missing_requests'" class="form-text alert alert-danger" role="alert">
|
||||
Python module "requests" is missing, please install it to proceed
|
||||
</div>
|
||||
<div v-if="ui_config_error && ui_config_error === 'missing_azure'" class="form-text alert alert-danger" role="alert">
|
||||
Python Azure SDK libraries are missing, please install it to proceed
|
||||
</div>
|
||||
<div class="form-text alert alert-info" role="alert">
|
||||
<strong>Prerequisites:</strong> <a href="https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md"
|
||||
target="_blank" rel="noopener noreferrer">Install azure-cli</a>
|
||||
</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 {
|
||||
region: null,
|
||||
// helper variables
|
||||
ui_loading_check: false,
|
||||
ui_config_error: false,
|
||||
ui_loading_regions: false,
|
||||
ui_region_error: null,
|
||||
ui_region_options: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
is_valid() {
|
||||
return !!this.region;
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.check_config();
|
||||
},
|
||||
methods: {
|
||||
check_config() {
|
||||
this.ui_loading_check = true;
|
||||
fetch("/azure_config")
|
||||
.then(r => {
|
||||
if (r.status === 200 || r.status === 400) {
|
||||
return r.json();
|
||||
}
|
||||
throw new Error(r.status);
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 'ok') {
|
||||
this.load_regions();
|
||||
} else if (response.error) {
|
||||
this.ui_config_error = response.error;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.ui_loading_check = false;
|
||||
});
|
||||
},
|
||||
load_regions() {
|
||||
this.ui_loading_regions = true;
|
||||
this.ui_region_error = null;
|
||||
fetch("/azure_regions")
|
||||
.then((r) => {
|
||||
if (r.status === 200) {
|
||||
return r.json();
|
||||
}
|
||||
throw new Error(r.status);
|
||||
})
|
||||
.then((data) => {
|
||||
this.ui_region_options = data.map(i => ({key: i.name, value: i.displayName}));
|
||||
})
|
||||
.catch((err) => {
|
||||
this.ui_region_error = err;
|
||||
})
|
||||
.finally(() => {
|
||||
this.ui_loading_regions = false;
|
||||
});
|
||||
},
|
||||
submit() {
|
||||
this.$emit("submit", {
|
||||
region: this.region
|
||||
});
|
||||
}
|
||||
},
|
||||
components: {
|
||||
"region-select": window.httpVueLoader("/static/region-select.vue"),
|
||||
}
|
||||
};
|
||||
</script>
|
@ -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>
|
@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<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 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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
data: function() {
|
||||
return {
|
||||
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,
|
||||
ui_token_from_env: false,
|
||||
ui_region_options: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
is_valid() {
|
||||
return (this.cs_config || 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.map(i => ({key: i.name, 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>
|
||||
|
||||
<style scoped>
|
||||
.example {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
@ -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"
|
||||
v-on: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>
|
@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="ui_config_error && ui_config_error === 'missing_boto'" class="form-text alert alert-danger" role="alert">
|
||||
Python module "boto3" is missing, please install it to proceed
|
||||
</div>
|
||||
<div v-if="ui_env_secrets" class="form-text alert alert-success" role="alert">
|
||||
AWS credentials were read from the environment variables
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Enter your AWS Access Key
|
||||
<a
|
||||
href="http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html"
|
||||
title="http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
class="badge badge-pill badge-primary"
|
||||
>?</a
|
||||
>
|
||||
<br />
|
||||
Note: Make sure to use an IAM user with an acceptable policy attached
|
||||
(see
|
||||
<a
|
||||
href="https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>docs</a
|
||||
>)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="aws_access_key"
|
||||
v-on:blur="load_regions"
|
||||
v-model="aws_access_key"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label
|
||||
>Enter your AWS Secret Key
|
||||
<a
|
||||
href="http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html"
|
||||
title="http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
class="badge bagde-pill badge-primary"
|
||||
>?</a
|
||||
></label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
name="aws_secret_key"
|
||||
v-on:blur="load_regions"
|
||||
v-model="aws_secret_key"
|
||||
/>
|
||||
</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
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
v-on:click="submit"
|
||||
v-bind:disabled="!is_valid"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
data: function() {
|
||||
return {
|
||||
aws_access_key: null,
|
||||
aws_secret_key: null,
|
||||
region: null,
|
||||
// ui helper variables
|
||||
ui_region_options: [],
|
||||
ui_env_secrets: null,
|
||||
ui_loading_check: false,
|
||||
ui_loading_regions: false,
|
||||
ui_config_error: null,
|
||||
ui_region_error: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
is_valid() {
|
||||
return this.has_secrets && this.region;
|
||||
},
|
||||
has_secrets() {
|
||||
return this.ui_env_secrets || (this.aws_access_key && this.aws_secret_key);
|
||||
},
|
||||
},
|
||||
created: function() {
|
||||
this.check_config();
|
||||
},
|
||||
methods: {
|
||||
check_config() {
|
||||
this.ui_loading_check = true;
|
||||
fetch("/aws_config")
|
||||
.then(r => {
|
||||
if (r.status === 200 || r.status === 400) {
|
||||
return r.json();
|
||||
}
|
||||
throw new Error(r.status);
|
||||
})
|
||||
.then(response => {
|
||||
if (response.has_secret) {
|
||||
this.ui_env_secrets = true;
|
||||
this.load_regions();
|
||||
} else if (response.error) {
|
||||
this.ui_config_error = response.error;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.ui_loading_check = false;
|
||||
});
|
||||
},
|
||||
load_regions() {
|
||||
if (this.has_secrets && this.ui_region_options.length === 0) {
|
||||
this.ui_loading_regions = true;
|
||||
this.ui_region_error = false;
|
||||
const payload = this.ui_env_secrets ? {} : {
|
||||
aws_access_key: this.aws_access_key,
|
||||
aws_secret_key: this.aws_secret_key
|
||||
}
|
||||
fetch('/ec2_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.map(i => ({key: i.RegionName, value: i.RegionName}));
|
||||
})
|
||||
.catch((err) => {
|
||||
this.ui_region_error = err;
|
||||
})
|
||||
.finally(() => {
|
||||
this.ui_loading_regions = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
submit() {
|
||||
let submit_value = {
|
||||
region: this.region
|
||||
}
|
||||
if (!this.ui_env_secrets) {
|
||||
submit_value['aws_access_key'] = this.aws_access_key;
|
||||
submit_value['aws_secret_key'] = this.aws_secret_key;
|
||||
}
|
||||
this.$emit('submit', submit_value);
|
||||
}
|
||||
},
|
||||
components: {
|
||||
"region-select": window.httpVueLoader("/static/region-select.vue"),
|
||||
}
|
||||
};
|
||||
</script>
|
@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="ui_config_error && ui_config_error === 'missing_google'" class="form-text alert alert-danger" role="alert">
|
||||
Python module "google-auth" is missing, please install it to proceed
|
||||
</div>
|
||||
<div v-if="ui_config_error && ui_config_error === 'missing_requests'" class="form-text alert alert-danger" role="alert">
|
||||
Python module "requests" is missing, please install it to proceed
|
||||
</div>
|
||||
<div
|
||||
class="form-group dropzone"
|
||||
v-if="ui_needs_upload"
|
||||
v-on:dragover.prevent="dragover_handler"
|
||||
v-on:dragleave.prevent="dragleave_handler"
|
||||
v-on:drop.prevent="drop_handler"
|
||||
v-on:click="show_file_select"
|
||||
v-bind:class="{
|
||||
'dropzone--can-drop': ui_can_drop,
|
||||
'dropzone--error': ui_drop_error,
|
||||
'dropzone--success': ui_drop_success,
|
||||
}"
|
||||
>
|
||||
<strong>Drag your GCE config file or click here</strong>
|
||||
<p>
|
||||
File <code>configs/gce.json</code> was not found. Please create it (<a
|
||||
href="https://github.com/trailofbits/algo/blob/master/docs/cloud-gce.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>how?</a
|
||||
>) or upload.
|
||||
</p>
|
||||
<p>After upload it <strong>will be saved</strong> in the configs folder.</p>
|
||||
<div v-if="ui_drop_error" class="alert alert-warning" role="alert">
|
||||
<strong>Error:</strong> {{ ui_drop_error }}.
|
||||
</div>
|
||||
|
||||
<div v-if="ui_drop_success" class="alert alert-success" role="alert">
|
||||
<strong>{{ ui_drop_filename }} loaded successfully</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="form-text alert alert-success" role="alert">
|
||||
Google Compute Engine credentials were found in the project
|
||||
</div>
|
||||
<input type="file" accept=".json,applciation/json" v-on:change="filechange_handler" />
|
||||
<region-select v-model="region" v-bind:options="ui_region_options" v-bind:loading="ui_loading_check || ui_loading_regions">
|
||||
<label v-if="ui_needs_upload">Please select region</label>
|
||||
<label v-else>Please specify <code>gce.json</code> credentials file to select region</label>
|
||||
</region-select>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
v-on:click="submit"
|
||||
v-bind:disabled="!is_valid"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
data: function () {
|
||||
return {
|
||||
drop_error: null,
|
||||
gce_credentials_file: null,
|
||||
region: null,
|
||||
// helper variables
|
||||
ui_can_drop: false,
|
||||
ui_drop_error: null,
|
||||
ui_drop_success: null,
|
||||
ui_drop_filename: null,
|
||||
ui_config_error: null,
|
||||
ui_needs_upload: null,
|
||||
ui_loading_regions: false,
|
||||
ui_loading_check: false,
|
||||
ui_region_options: []
|
||||
};
|
||||
},
|
||||
created: function() {
|
||||
this.check_config();
|
||||
},
|
||||
computed: {
|
||||
is_valid() {
|
||||
return this.gce_credentials_file && this.region;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show_file_select(e) {
|
||||
if (e.target.tagName === 'A') {
|
||||
return;
|
||||
}
|
||||
const input = this.$el.querySelector(['input[type=file]']);
|
||||
const event = new MouseEvent('click', {
|
||||
'view': window,
|
||||
'bubbles': true,
|
||||
'cancelable': true
|
||||
});
|
||||
input.dispatchEvent(event);
|
||||
},
|
||||
dragover_handler(e) {
|
||||
this.ui_can_drop = true;
|
||||
this.ui_drop_success = false;
|
||||
this.ui_drop_error = false;
|
||||
this.ui_drop_filename = null;
|
||||
},
|
||||
dragleave_handler() {
|
||||
this.ui_can_drop = false;
|
||||
},
|
||||
drop_handler(e) {
|
||||
try {
|
||||
const droppedFiles = e.dataTransfer.files;
|
||||
if (droppedFiles.length !== 1) {
|
||||
this.ui_drop_error = 'Please upload GCE config as single file';
|
||||
}
|
||||
this.read_file(droppedFiles[0]);
|
||||
} catch (e) {
|
||||
this.ui_drop_error = 'Unhandled error while trying to read GCE config';
|
||||
}
|
||||
},
|
||||
filechange_handler(e) {
|
||||
if (e.target.files.length) {
|
||||
this.read_file(e.target.files[0]);
|
||||
}
|
||||
},
|
||||
read_file(file) {
|
||||
if (file.type !== 'application/json') {
|
||||
this.ui_drop_error = 'Incorrect file type';
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
let gce_config_content = null;
|
||||
try {
|
||||
gce_config_content = JSON.parse(e.target.result);
|
||||
this.ui_drop_success = true;
|
||||
this.ui_drop_filename = file.name;
|
||||
this.gce_credentials_file = 'configs/gce.json';
|
||||
} catch (e) {
|
||||
this.ui_drop_error = 'JSON format error';
|
||||
}
|
||||
gce_config_content && this.load_regions(gce_config_content);
|
||||
}
|
||||
reader.onerror = e => {
|
||||
this.ui_drop_error = 'Error while reading file';
|
||||
}
|
||||
reader.readAsText(file);
|
||||
|
||||
},
|
||||
check_config() {
|
||||
this.ui_loading_check = true;
|
||||
fetch("/gce_config")
|
||||
.then(r => {
|
||||
if (r.status === 200 || r.status === 400) {
|
||||
return r.json();
|
||||
}
|
||||
throw new Error(r.status);
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 'ok') {
|
||||
this.gce_credentials_file = 'configs/gce.json';
|
||||
this.load_regions();
|
||||
this.ui_needs_upload = false;
|
||||
} else if (response.error) {
|
||||
this.ui_config_error = response.error;
|
||||
} else {
|
||||
this.ui_needs_upload = true;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.ui_loading_check = false;
|
||||
});
|
||||
},
|
||||
load_regions(gce_config_content) {
|
||||
if (this.gce_credentials_file && this.ui_region_options.length === 0) {
|
||||
this.ui_loading_regions = true;
|
||||
fetch("/gce_regions", {
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: gce_config_content ? JSON.stringify(gce_config_content) : '{}',
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
this.ui_region_options = data.items.map(i => ({
|
||||
value: i.name,
|
||||
key: i.name
|
||||
}));
|
||||
})
|
||||
.finally(() => {
|
||||
this.ui_loading_regions = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
submit() {
|
||||
this.$emit("submit", {
|
||||
gce_credentials_file: this.gce_credentials_file,
|
||||
region: this.region,
|
||||
});
|
||||
},
|
||||
},
|
||||
components: {
|
||||
"region-select": window.httpVueLoader("/static/region-select.vue"),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.dropzone {
|
||||
padding: 2em;
|
||||
border: 5px dotted #ccc;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type=file] {
|
||||
visibility: hidden;
|
||||
}
|
||||
.dropzone--can-drop {
|
||||
border-color: var(--blue);
|
||||
}
|
||||
.dropzone--error {
|
||||
border-color: var(--red);
|
||||
}
|
||||
.dropzone--success {
|
||||
border-color: var(--green);
|
||||
}
|
||||
</style>
|
@ -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_hcloud_token">
|
||||
Enter your API token. The token must have read and write permissions
|
||||
<a href="https://github.com/trailofbits/algo/blob/master/docs/cloud-hetzner.md" title="https://github.com/trailofbits/algo/blob/master/docs/cloud-hetzner.md" class="badge bagde-pill badge-primary" target="_blank" rel="noopener noreferrer">?</a>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="id_hcloud_token"
|
||||
name="hcloud_token"
|
||||
v-bind:disabled="ui_loading_check"
|
||||
v-model="hcloud_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 {
|
||||
hcloud_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.hcloud_config || this.ui_token_from_env) && this.region;
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.check_config();
|
||||
},
|
||||
methods: {
|
||||
check_config() {
|
||||
this.ui_loading_check = true;
|
||||
return fetch("/hetzner_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.hcloud_token) {
|
||||
this.ui_loading_regions = true;
|
||||
this.ui_region_error = null;
|
||||
const payload = this.ui_token_from_env ? {} : {
|
||||
token: this.hcloud_token
|
||||
};
|
||||
fetch("/hetzner_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.datacenters.map(i => ({key: i.location.name, value: i.location.city}));
|
||||
})
|
||||
.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", {
|
||||
hcloud_token: this.hcloud_token,
|
||||
region: this.region
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
"region-select": window.httpVueLoader("/static/region-select.vue"),
|
||||
}
|
||||
};
|
||||
</script>
|
@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="ui_config_error && ui_config_error === 'missing_boto'" class="form-text alert alert-danger" role="alert">
|
||||
Python module "boto3" is missing, please install it to proceed
|
||||
</div>
|
||||
<div v-if="ui_env_secrets" class="form-text alert alert-success" role="alert">
|
||||
AWS credentials were read from the environment variables
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Enter your AWS Access Key <a href="http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html" title="http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html" target="_blank" rel="noreferrer noopener" class="badge bagde-pill badge-primary">?</a>
|
||||
<br>
|
||||
Note: Make sure to use an IAM user with an acceptable policy attached (see <a
|
||||
href="https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md" target="_blank" rel="noreferrer noopener" >docs</a>)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="aws_access_key"
|
||||
v-on:blur="load_regions"
|
||||
v-model="aws_access_key"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Enter your AWS Secret Key <a
|
||||
href="http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html" title="http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html" target="_blank" rel="noreferrer noopener" class="badge bagde-pill badge-primary">?</a></label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
name="aws_secret_key"
|
||||
v-on:blur="load_regions"
|
||||
v-model="aws_secret_key">
|
||||
</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 class="btn btn-primary"
|
||||
type="button"
|
||||
v-on:click="submit"
|
||||
v-bind:disabled="!is_valid">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
data: function() {
|
||||
return {
|
||||
aws_access_key: null,
|
||||
aws_secret_key: null,
|
||||
region: null,
|
||||
// ui helpoer variables
|
||||
ui_region_options: [],
|
||||
ui_env_secrets: null,
|
||||
ui_loading_check: false,
|
||||
ui_loading_regions: false,
|
||||
ui_config_error: null,
|
||||
ui_region_error: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
has_secrets() {
|
||||
return this.ui_env_secrets || (this.aws_access_key && this.aws_secret_key);
|
||||
},
|
||||
is_valid() {
|
||||
return this.has_secrets && this.region;
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.check_config();
|
||||
},
|
||||
methods: {
|
||||
check_config() {
|
||||
this.ui_loading_check = true;
|
||||
fetch("/aws_config")
|
||||
.then(r => {
|
||||
if (r.status === 200 || r.status === 400) {
|
||||
return r.json();
|
||||
}
|
||||
throw new Error(r.status);
|
||||
})
|
||||
.then(response => {
|
||||
if (response.has_secret) {
|
||||
this.ui_env_secrets = true;
|
||||
this.load_regions();
|
||||
} else if (response.error) {
|
||||
this.ui_config_error = response.error;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.ui_loading_check = false;
|
||||
});
|
||||
},
|
||||
load_regions() {
|
||||
if (this.has_secrets && this.ui_region_options.length === 0) {
|
||||
this.ui_loading_regions = true;
|
||||
this.ui_region_error = false;
|
||||
const payload = this.ui_env_secrets ? {} : {
|
||||
aws_access_key: this.aws_access_key,
|
||||
aws_secret_key: this.aws_secret_key
|
||||
}
|
||||
fetch('/lightsail_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.name, value: i.displayName}));
|
||||
})
|
||||
.catch((err) => {
|
||||
this.ui_region_error = err;
|
||||
})
|
||||
.finally(() => {
|
||||
this.ui_loading_regions = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
submit() {
|
||||
let submit_value = {
|
||||
region: this.region
|
||||
}
|
||||
if (!this.ui_env_secrets) {
|
||||
submit_value['aws_access_key'] = this.aws_access_key;
|
||||
submit_value['aws_secret_key'] = this.aws_secret_key;
|
||||
}
|
||||
this.$emit('submit', submit_value);
|
||||
}
|
||||
},
|
||||
components: {
|
||||
"region-select": window.httpVueLoader("/static/region-select.vue"),
|
||||
}
|
||||
};
|
||||
</script>
|
@ -0,0 +1,109 @@
|
||||
<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_token">
|
||||
Enter your API token. The token must have read and write permissions
|
||||
<a href="https://github.com/trailofbits/algo/blob/master/docs/cloud-linode.md" title="https://github.com/trailofbits/algo/blob/master/docs/cloud-linode.md" class="badge bagde-pill badge-primary" target="_blank" rel="noopener noreferrer">?</a>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="id_token"
|
||||
name="linode_token"
|
||||
v-bind:disabled="ui_loading_check"
|
||||
v-model="linode_token"
|
||||
/>
|
||||
</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 {
|
||||
linode_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.linode_token || this.ui_token_from_env) && this.region;
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.check_config();
|
||||
this.load_regions();
|
||||
},
|
||||
methods: {
|
||||
check_config() {
|
||||
this.ui_loading_check = true;
|
||||
return fetch("/linode_config")
|
||||
.then(r => r.json())
|
||||
.then(response => {
|
||||
if (response.has_secret) {
|
||||
this.ui_token_from_env = true;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.ui_loading_check = false;
|
||||
});
|
||||
},
|
||||
load_regions() {
|
||||
this.ui_loading_regions = true;
|
||||
this.ui_region_error = null;
|
||||
const payload = this.ui_token_from_env ? {} : {
|
||||
token: this.hcloud_token
|
||||
};
|
||||
fetch("/linode_regions")
|
||||
.then((r) => {
|
||||
if (r.status === 200) {
|
||||
return r.json();
|
||||
}
|
||||
throw new Error(r.status);
|
||||
})
|
||||
.then((data) => {
|
||||
this.ui_region_options = data.data.map(i => ({key: i.id, value: i.id}));
|
||||
})
|
||||
.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", {
|
||||
linode_token: this.linode_token,
|
||||
region: this.region
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
"region-select": window.httpVueLoader("/static/region-select.vue"),
|
||||
}
|
||||
};
|
||||
</script>
|
@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<label for="id_server">
|
||||
Enter the IP address of your server: (or use localhost for local installation):
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="id_server"
|
||||
name="server"
|
||||
v-model="server"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="id_ssh_user">
|
||||
What user should we use to login on the server? (note: passwordless login required, or ignore if you're deploying to localhost)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="id_ssh_user"
|
||||
name="ssh_user"
|
||||
v-model="ssh_user"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="id_endpoint">
|
||||
Enter the public IP address or domain name of your server: (IMPORTANT! This is used to verify the certificate)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="id_endpoint"
|
||||
name="endpoint"
|
||||
v-model="endpoint"
|
||||
/>
|
||||
</div>
|
||||
<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 {
|
||||
server: null,
|
||||
ssh_user: null,
|
||||
endpoint: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
is_valid() {
|
||||
return this.server && this.ssh_user && this.endpoint;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
this.$emit("submit", {
|
||||
server: this.server,
|
||||
ssh_user: this.ssh_user,
|
||||
endpoint: this.endpoint
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div>
|
||||
<p>
|
||||
Download OpenStack credentials from the OpenStack dashboard->Compute->API Access and source it in the shell (eg:
|
||||
source /tmp/dhc-openrc.sh)
|
||||
</p>
|
||||
<button v-on:click="submit" class="btn btn-primary" type="button">Next</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
methods: {
|
||||
submit() {
|
||||
this.$emit("submit");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<div v-if="ui_env_secrets" class="form-text alert alert-success" role="alert">
|
||||
Token was read from the environment variable
|
||||
</div>
|
||||
<div v-else class="form-group">
|
||||
<label
|
||||
>Enter your auth token
|
||||
<a
|
||||
href="https://trailofbits.github.io/algo/cloud-scaleway.html"
|
||||
title="https://trailofbits.github.io/algo/cloud-scaleway.html"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
class="badge bagde-pill badge-primary"
|
||||
>?</a
|
||||
></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="scaleway_token"
|
||||
v-bind:disabled="ui_loading_check"
|
||||
v-model="scaleway_token"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<region-select v-model="region" v-bind:options="ui_region_options">
|
||||
</region-select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
v-on:click="submit"
|
||||
v-bind:disabled="!is_valid"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
data: function () {
|
||||
return {
|
||||
scaleway_token: null,
|
||||
region: null,
|
||||
// helper variables
|
||||
ui_env_secrets: false,
|
||||
ui_loading_check: false,
|
||||
ui_region_options: [
|
||||
{value: 'Paris 1', key: 'par1'},
|
||||
{value: 'Amsterdam 1', key: 'ams1'}
|
||||
]
|
||||
};
|
||||
},
|
||||
created: function() {
|
||||
this.check_config();
|
||||
},
|
||||
computed: {
|
||||
is_valid() {
|
||||
return this.region && (this.scaleway_token || this.ui_env_secrets);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
check_config() {
|
||||
this.ui_loading_check = true;
|
||||
fetch("/scaleway_config")
|
||||
.then(r => {
|
||||
if (r.status === 200 || r.status === 400) {
|
||||
return r.json();
|
||||
}
|
||||
throw new Error(r.status);
|
||||
})
|
||||
.then(response => {
|
||||
if (response.has_secret) {
|
||||
this.ui_env_secrets = true;
|
||||
} else if (response.error) {
|
||||
this.ui_config_error = response.error;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.ui_loading_check = false;
|
||||
});
|
||||
},
|
||||
submit() {
|
||||
if (this.ui_env_secrets) {
|
||||
this.$emit("submit", {
|
||||
region: this.region
|
||||
});
|
||||
} else {
|
||||
this.$emit("submit", {
|
||||
scaleway_token: this.scaleway_token,
|
||||
region: this.region
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
"region-select": window.httpVueLoader("/static/region-select.vue")
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<ul class="nav flex-column nav-pills">
|
||||
<li class="nav-item"
|
||||
v-for="item in providers_map"
|
||||
v-bind:key="item.alias">
|
||||
<a
|
||||
class="nav-link"
|
||||
href="#"
|
||||
v-bind:class="{ active: item.alias === (provider && provider.alias) }"
|
||||
v-on:click="set_provider(item)"
|
||||
>{{item.name}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<component v-if="provider" v-bind:is="provider.alias" v-on:submit="on_provider_submit"></component>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
data: function() {
|
||||
return {
|
||||
loading: false,
|
||||
provider: null,
|
||||
providers_map: [
|
||||
{ name: "DigitalOcean", alias: "digitalocean" },
|
||||
{ name: "Amazon Lightsail", alias: "lightsail" },
|
||||
{ name: "Amazon EC2", alias: "ec2" },
|
||||
{ name: "Microsoft Azure", alias: "azure" },
|
||||
{ name: "Google Compute Engine", alias: "gce" },
|
||||
{ name: "Hetzner Cloud", alias: "hetzner" },
|
||||
{ name: "Vultr", alias: "vultr" },
|
||||
{ name: "Linode", alias: "linode" },
|
||||
{ name: "Scaleway", alias: "scaleway" },
|
||||
{ name: "OpenStack (DreamCompute optimised)", alias: "openstack" },
|
||||
{ name: "CloudStack (Exoscale optimised)", alias: "cloudstack" },
|
||||
{
|
||||
name: "Install to existing Ubuntu 18.04 or 19.04 server (Advanced)",
|
||||
alias: "local"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
// Warning: Mutable Object to edit parent props
|
||||
props: ['extra_args'],
|
||||
methods: {
|
||||
set_provider(provider) {
|
||||
this.provider = provider;
|
||||
this.extra_args.provider = provider.alias;
|
||||
},
|
||||
on_provider_submit(extra_args) {
|
||||
Object.assign(this.extra_args, extra_args);
|
||||
this.$emit('submit');
|
||||
}
|
||||
},
|
||||
components: {
|
||||
'digitalocean': window.httpVueLoader('/static/provider-do.vue'),
|
||||
'lightsail': window.httpVueLoader('/static/provider-lightsail.vue'),
|
||||
'ec2': window.httpVueLoader('/static/provider-ec2.vue'),
|
||||
'gce': window.httpVueLoader('/static/provider-gce.vue'),
|
||||
'vultr': window.httpVueLoader('/static/provider-vultr.vue'),
|
||||
'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'),
|
||||
'cloudstack': window.httpVueLoader('/static/provider-cloudstack.vue'),
|
||||
'openstack': window.httpVueLoader('/static/provider-openstack.vue'),
|
||||
'local': window.httpVueLoader('/static/provider-local.vue')
|
||||
}
|
||||
};
|
||||
</script>
|
@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="ui_token_from_env" class="form-text alert alert-success" role="alert">
|
||||
Vultr config file was found in your system
|
||||
</div>
|
||||
<div v-else class="form-group">
|
||||
<label
|
||||
>Enter Vultr API Token, it will be saved in your system
|
||||
<a
|
||||
href="https://trailofbits.github.io/algo/cloud-vultr.html"
|
||||
title="https://trailofbits.github.io/algo/cloud-vultr.html"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
class="badge bagde-pill badge-primary"
|
||||
>?</a
|
||||
></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="vultr_token"
|
||||
v-bind:disabled="ui_loading_check"
|
||||
v-model="ui_token"
|
||||
v-on:blur="save_config"
|
||||
/>
|
||||
<div v-if="vultr_config" class="form-text alert alert-success" role="alert">
|
||||
The config file was saved on your system
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<region-select v-model="region"
|
||||
v-bind:options="ui_region_options"
|
||||
v-bind:loading="ui_loading_check || ui_loading_regions">
|
||||
</region-select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
v-on:click="submit"
|
||||
v-bind:disabled="!is_valid"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
data: function () {
|
||||
return {
|
||||
vultr_config: null,
|
||||
region: null,
|
||||
// helper variables
|
||||
ui_token: null,
|
||||
ui_token_from_env: false,
|
||||
ui_loading_check: false,
|
||||
ui_loading_regions: false,
|
||||
ui_region_options: []
|
||||
};
|
||||
},
|
||||
created: function() {
|
||||
this.check_config();
|
||||
this.load_regions();
|
||||
},
|
||||
computed: {
|
||||
has_secrets() {
|
||||
return this.ui_token_from_env || this.vultr_config;
|
||||
},
|
||||
is_valid() {
|
||||
return this.has_secrets && this.region;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
check_config() {
|
||||
this.ui_loading_check = true;
|
||||
fetch("/vultr_config")
|
||||
.then(r => {
|
||||
if (r.status === 200 || r.status === 400) {
|
||||
return r.json();
|
||||
}
|
||||
throw new Error(r.status);
|
||||
})
|
||||
.then(response => {
|
||||
if (response.has_secret) {
|
||||
this.ui_token_from_env = true;
|
||||
this.vultr_config = response.saved_to;
|
||||
this.load_regions();
|
||||
} else if (response.error) {
|
||||
this.ui_config_error = response.error;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.ui_loading_check = false;
|
||||
});
|
||||
},
|
||||
save_config() {
|
||||
this.ui_loading_check = true;
|
||||
fetch("/vultr_config", {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: this.ui_token
|
||||
})
|
||||
})
|
||||
.then(r => {
|
||||
if (r.status === 200 || r.status === 400) {
|
||||
return r.json();
|
||||
}
|
||||
throw new Error(r.status);
|
||||
})
|
||||
.then(response => {
|
||||
if ('saved_to' in response) {
|
||||
this.vultr_config = response.saved_to;
|
||||
} else if (response.error) {
|
||||
this.ui_config_error = response.error;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.ui_loading_check = false;
|
||||
});
|
||||
},
|
||||
load_regions() {
|
||||
this.ui_loading_regions = true;
|
||||
fetch("/vultr_regions")
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
this.ui_region_options = Object.keys(data).map(k => ({
|
||||
value: data[k].name,
|
||||
key: data[k].DCID
|
||||
}));
|
||||
})
|
||||
.finally(() => {
|
||||
this.ui_loading_regions = false;
|
||||
});
|
||||
},
|
||||
submit() {
|
||||
let submit_value = {
|
||||
region: this.region,
|
||||
vultr_config: this.vultr_config
|
||||
}
|
||||
this.$emit("submit", submit_value);
|
||||
},
|
||||
},
|
||||
components: {
|
||||
"region-select": window.httpVueLoader("/static/region-select.vue"),
|
||||
}
|
||||
};
|
||||
</script>
|
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="form-group">
|
||||
<label v-if="ui_show_slot"><slot></slot></label>
|
||||
<label v-if="ui_show_loading">Loading regions...</label>
|
||||
<label v-if="ui_show_select_prompt"
|
||||
>What region should the server be located in?</label
|
||||
>
|
||||
<select
|
||||
name="region"
|
||||
class="form-control"
|
||||
v-bind:class="{ 'is-invalid': has_error }"
|
||||
v-bind:value="value"
|
||||
v-bind:disabled="ui_disabled"
|
||||
v-on:input="$emit('input', $event.target.value)"
|
||||
>
|
||||
<option value disabled>Select region</option>
|
||||
<option
|
||||
v-for="(region, i) in options"
|
||||
v-bind:key="i"
|
||||
v-bind:value="region.key"
|
||||
>{{ region.value }}</option
|
||||
>
|
||||
</select>
|
||||
<div v-if="has_error" class="invalid-feedback">
|
||||
There was an error during fetching regions
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
props: ["value", "options", "loading", "error"],
|
||||
computed: {
|
||||
has_options: function() {
|
||||
return this.options && this.options.length;
|
||||
},
|
||||
ui_disabled: function() {
|
||||
return !this.has_options || this.loading;
|
||||
},
|
||||
ui_show_slot: function() {
|
||||
return !this.loading && !this.has_options
|
||||
},
|
||||
ui_show_loading: function() {
|
||||
return this.loading;
|
||||
},
|
||||
ui_show_select_prompt: function() {
|
||||
return this.has_options;
|
||||
},
|
||||
has_error: function() {
|
||||
return !!this.error;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div>
|
||||
<section>
|
||||
<p>Config files and certificates are in the ./configs/ directory.</p>
|
||||
<p>Go to <a href="https://whoer.net/" target="_blank" rel="noopener noopener">https://whoer.net/</a>
|
||||
after connecting and ensure that all your traffic passes through the VPN.</p>
|
||||
<p v-if="result.local_service_ip">Local DNS resolver {{result.local_service_ip}}</p>
|
||||
<p v-if="result.p12_export_password">The p12 and SSH keys password for new users is <code>{{result.p12_export_password}}</code></p>
|
||||
<p v-if="result.CA_password">The CA key password is <code>{{result.CA_password}}</code></p>
|
||||
<p v-if="result.ansible_ssh_host">Shell access: <code>ssh -F configs/{{result.ansible_ssh_host}}/ssh_config {{config.server_name}}</code></p>
|
||||
<p>Read more on how to set up clients at the <a href="https://github.com/trailofbits/algo" target="_blank" rel="noopener noopener">Algo home page</a></p>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="text-center">Client configuration files</h2>
|
||||
<div v-for="user in config.users" :key="user">
|
||||
<h3>👤 {{user}}</h3>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div v-if="config.wireguard_enabled">
|
||||
<p><strong>WireGuard</strong></p>
|
||||
<p>
|
||||
<img v-bind:src="`/results/${result.ansible_ssh_host}/wireguard/${user}.png`" alt="QR Code">
|
||||
</p>
|
||||
<p><a v-bind:href="`/results/${result.ansible_ssh_host}/wireguard/${user}.conf`">{{user}}.conf</a></p>
|
||||
<p>iOS <a v-bind:href="`/results/${result.ansible_ssh_host}/wireguard/apple/ios/${user}.mobileconfig`"> .mobileconfig</a>,
|
||||
Mac OS <a v-bind:href="`/results/${result.ansible_ssh_host}/wireguard/apple/macos/${user}.mobileconfig`"> .mobileconfig</a></p>
|
||||
</div>
|
||||
<div v-if="config.ipsec_enabled">
|
||||
<p><strong>IPSec</strong></p>
|
||||
<p>Apple's <a v-bind:href="`/results/${result.ansible_ssh_host}/ipsec/apple/${user}.mobileconfig`"> .mobileconfig</a></p>
|
||||
<p>Manual configuration:
|
||||
<a v-bind:href="`/results/${result.ansible_ssh_host}/ipsec/manual/${user}.conf`">{{user}}.conf</a>,
|
||||
<a v-bind:href="`/results/${result.ansible_ssh_host}/ipsec/manual/${user}.p12`">{{user}}.p12</a>,
|
||||
<a v-bind:href="`/results/${result.ansible_ssh_host}/ipsec/manual/${user}.secrets`">{{user}}.secrets</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="text-center">Finish setup and save configs</h2>
|
||||
<p class="text-center">
|
||||
<button v-on:click="$emit('submit')" class="btn btn-primary btn-lg" type="button">Save & Exit</button>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
data: function() {
|
||||
return {
|
||||
result: {},
|
||||
config: { users: [] }
|
||||
};
|
||||
},
|
||||
created() {
|
||||
fetch("/playbook")
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
this.result = data.result;
|
||||
});
|
||||
fetch("/config")
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
this.config = data;
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Users</h2>
|
||||
<section class="my-3">
|
||||
<h4>Set up user list</h4>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" v-for="(user, index) in config.users" :key="user">
|
||||
{{ user }}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm float-right"
|
||||
v-on:click="remove_user(index)"
|
||||
>Remove</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="my-3 form-group">
|
||||
<label for="id_new_user">Add new user</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
id="id_new_user"
|
||||
class="form-control"
|
||||
placeholder="username"
|
||||
v-model="new_user"
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<button
|
||||
v-on:click="add_user"
|
||||
class="btn btn-outline-primary"
|
||||
type="button"
|
||||
id="button-addon2"
|
||||
>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div>
|
||||
<span
|
||||
v-if="save_config_message"
|
||||
v-bind:class="{ 'text-success': ok, 'text-danged': !ok }"
|
||||
>{{save_config_message}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
data: function() {
|
||||
return {
|
||||
config: {},
|
||||
loading: false,
|
||||
new_user: "",
|
||||
save_config_message: ""
|
||||
};
|
||||
},
|
||||
created: function() {
|
||||
this.load_config();
|
||||
},
|
||||
methods: {
|
||||
add_user: function() {
|
||||
this.config.users.push(this.new_user);
|
||||
this.new_user = "";
|
||||
this.save_config();
|
||||
},
|
||||
remove_user: function(index) {
|
||||
this.config.users.splice(index, 1);
|
||||
this.save_config();
|
||||
},
|
||||
save_config: function() {
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
fetch("/config", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(this.config),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (result.ok) {
|
||||
this.ok = true;
|
||||
this.save_config_message = "Saved!";
|
||||
setTimeout(() => {
|
||||
this.save_config_message = "";
|
||||
}, 1000);
|
||||
} else {
|
||||
this.ok = false;
|
||||
this.save_config_message = "Not Saved!";
|
||||
setTimeout(() => {
|
||||
this.save_config_message = "";
|
||||
}, 1000);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
load_config: function() {
|
||||
this.loading = true;
|
||||
fetch("/config")
|
||||
.then(r => r.json())
|
||||
.then(config => {
|
||||
this.config = config;
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>VPN Options</h2>
|
||||
<section class="my-3">
|
||||
<div class="form-group">
|
||||
<label>Name the vpn server</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="server name"
|
||||
v-model="extra_args.server_name"
|
||||
/>
|
||||
</div>
|
||||
<label>MacOS/iOS IPsec clients to enable Connect On Demand:</label>
|
||||
<div class="form-check">
|
||||
<label
|
||||
title="MacOS/iOS IPsec clients to enable Connect On Demand when connected to cellular
|
||||
networks?"
|
||||
>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
name="ondemand_cellular"
|
||||
v-model="extra_args.ondemand_cellular"
|
||||
/>
|
||||
when connected to cellular networks
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label title="MacOS/iOS IPsec clients to enable Connect On Demand when connected to Wi-Fi?">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
name="ondemand_wifi"
|
||||
v-model="extra_args.ondemand_wifi"
|
||||
/>
|
||||
when connected to WiFi
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_ondemand_wifi_exclude">Trusted Wi-Fi networks</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="id_ondemand_wifi_exclude"
|
||||
name="ondemand_wifi_exclude"
|
||||
placeholder="HomeNet,OfficeWifi,AlgoWiFi"
|
||||
v-model="extra_args.ondemand_wifi_exclude"
|
||||
/>
|
||||
<small class="form-text text-muted">
|
||||
List the names of any trusted Wi-Fi networks where
|
||||
macOS/iOS
|
||||
IPsec clients should not use "Connect On Demand"
|
||||
(e.g., your home network. Comma-separated value, e.g.,
|
||||
HomeNet,OfficeWifi,AlgoWiFi)
|
||||
</small>
|
||||
</div>
|
||||
<label>Retain the PKI</label>
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
name="store_pki"
|
||||
v-model="extra_args.store_pki"
|
||||
/>
|
||||
Do you want to retain the keys (PKI)?
|
||||
<small class="form-text text-muted">
|
||||
required to add users in the future, but less
|
||||
secure
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
<label>DNS adblocking</label>
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
name="dns_adblocking"
|
||||
v-model="extra_args.dns_adblocking"
|
||||
/>
|
||||
Enable DNS ad blocking on this VPN server
|
||||
</label>
|
||||
</div>
|
||||
<label>SSH tunneling</label>
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
name="ssh_tunneling"
|
||||
v-model="extra_args.ssh_tunneling"
|
||||
/>
|
||||
Each user will have their own account for SSH tunneling
|
||||
</label>
|
||||
</div>
|
||||
<button v-on:click="$emit('submit')" class="btn btn-primary" type="button">Select Cloud Provider</button>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
module.exports = {
|
||||
// Warning: Mutable Object to edit partent props
|
||||
props: ['extra_args']
|
||||
}
|
||||
</script>
|
Loading…
Reference in New Issue