Compare commits
60 Commits
webapi
...
cfb323bf60
Author | SHA1 | Date | |
---|---|---|---|
cfb323bf60 | |||
8c7197f118 | |||
fb83f52162 | |||
982556a2c5 | |||
13646e64da | |||
67149f437a | |||
308ac05fe6 | |||
c378c2b287 | |||
5367822747 | |||
e295b4420c | |||
e4180b8306 | |||
5cd5126039 | |||
f8e9ab2482 | |||
487a1ac0a3 | |||
71344fbf4d | |||
6f3ecf991d | |||
40aa3bccd3 | |||
bcc3f20b27 | |||
8bd3971545 | |||
c639899cd0 | |||
a47c430855 | |||
d5362897df | |||
a52d183f2e | |||
73042d1705 | |||
d3078fc5ce | |||
d60238f6f0 | |||
ab55146d41 | |||
7191213646 | |||
8c802a977a | |||
929aa29663 | |||
61beee8d3b | |||
8586b85095 | |||
b2b78e0015 | |||
d6f4be8197 | |||
5d3d0a1c5e | |||
fc7e264178 | |||
157e81e6e5 | |||
97f9486ea3 | |||
4b90a6c7ae | |||
adc3342a8c | |||
f8bbdcc3c5 | |||
1133caa8a4 | |||
1f384f34b5 | |||
c25d85bbdd | |||
0f4ac8e444 | |||
3cf501a393 | |||
180efa01af | |||
f5c079ffc2 | |||
0e5dfe6bde | |||
2adbb8d94c | |||
8446d11720 | |||
a580b04659 | |||
2e6894ad14 | |||
2e6c6517f3 | |||
be6c1b38b7 | |||
aa8e129913 | |||
71e09807a7 | |||
4816170c03 | |||
6af241e8be | |||
c8ec53f207 |
13
.drone.yml
Normal file
13
.drone.yml
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: publish docker image
|
||||
steps:
|
||||
- name: build and publish
|
||||
image: plugins/docker
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_reg_username_3wc
|
||||
password:
|
||||
from_secret: docker_reg_passwd_3wc
|
||||
repo: 3wordchant/capsul-flask
|
||||
tags: ${DRONE_COMMIT_BRANCH}
|
48
Dockerfile
Normal file
48
Dockerfile
Normal file
@ -0,0 +1,48 @@
|
||||
FROM python:3.8-alpine as build
|
||||
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
gcc \
|
||||
gettext \
|
||||
git \
|
||||
jpeg-dev \
|
||||
libffi-dev \
|
||||
libjpeg \
|
||||
musl-dev \
|
||||
postgresql-dev \
|
||||
python3-dev \
|
||||
zlib-dev
|
||||
|
||||
RUN mkdir -p /app/{code,venv}
|
||||
WORKDIR /app/code
|
||||
COPY Pipfile Pipfile.lock /app/code/
|
||||
|
||||
RUN python3 -m venv /app/venv
|
||||
RUN pip install pipenv setuptools
|
||||
ENV PATH="/app/venv/bin:$PATH" VIRTUAL_ENV="/app/venv"
|
||||
RUN pip install wheel cppy
|
||||
# Install dependencies into the virtual environment with Pipenv
|
||||
RUN pipenv install --deploy --verbose
|
||||
|
||||
FROM python:3.8-alpine
|
||||
|
||||
RUN apk add --no-cache \
|
||||
cloud-utils \
|
||||
libjpeg \
|
||||
libpq \
|
||||
libstdc++ \
|
||||
libvirt-client \
|
||||
openssh-client \
|
||||
virt-install
|
||||
|
||||
COPY . /app/code/
|
||||
WORKDIR /app/code
|
||||
|
||||
COPY --from=build /app/venv /app/venv
|
||||
ENV PATH="/app/venv/bin:$PATH" VIRTUAL_ENV="/app/venv"
|
||||
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "-k", "gevent", "--worker-connections", "1000", "app:app"]
|
||||
|
||||
VOLUME /app/code
|
||||
|
||||
EXPOSE 5000
|
@ -2,6 +2,7 @@ import logging
|
||||
from logging.config import dictConfig as logging_dict_config
|
||||
|
||||
import atexit
|
||||
import jinja2
|
||||
import os
|
||||
import hashlib
|
||||
import requests
|
||||
@ -26,8 +27,24 @@ class StdoutMockFlaskMail:
|
||||
def send(self, message: Message):
|
||||
current_app.logger.info(f"Email would have been sent if configured:\n\nto: {','.join(message.recipients)}\nsubject: {message.subject}\nbody:\n\n{message.body}\n\n")
|
||||
|
||||
|
||||
load_dotenv(find_dotenv())
|
||||
|
||||
for var_name in [
|
||||
"SPOKE_HOST_TOKEN", "HUB_TOKEN", "STRIPE_SECRET_KEY",
|
||||
"BTCPAY_PRIVATE_KEY", "MAIL_PASSWORD"
|
||||
]:
|
||||
var = os.environ.get(f"{var_name}_FILE")
|
||||
if not var:
|
||||
continue
|
||||
|
||||
if not os.path.isfile(var):
|
||||
continue
|
||||
|
||||
with open(var) as secret_file:
|
||||
os.environ[var_name] = secret_file.read().rstrip('\n')
|
||||
del os.environ[f"{var_name}_FILE"]
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
app.config.from_mapping(
|
||||
@ -71,7 +88,9 @@ app.config.from_mapping(
|
||||
#STRIPE_WEBHOOK_SECRET=os.environ.get("STRIPE_WEBHOOK_SECRET", default="")
|
||||
|
||||
BTCPAY_PRIVATE_KEY=os.environ.get("BTCPAY_PRIVATE_KEY", default="").replace("\\n", "\n"),
|
||||
BTCPAY_URL=os.environ.get("BTCPAY_URL", default="")
|
||||
BTCPAY_URL=os.environ.get("BTCPAY_URL", default=""),
|
||||
|
||||
THEME=os.environ.get("THEME", default="")
|
||||
)
|
||||
|
||||
app.config['HUB_URL'] = os.environ.get("HUB_URL", default=app.config['BASE_URL'])
|
||||
@ -154,7 +173,16 @@ is_running_server = ('flask run' in command_line) or ('gunicorn' in command_line
|
||||
|
||||
app.logger.info(f"is_running_server: {is_running_server}")
|
||||
|
||||
if app.config['THEME'] != "":
|
||||
my_loader = jinja2.ChoiceLoader([
|
||||
jinja2.FileSystemLoader(
|
||||
[os.path.join('capsulflask', 'theme', app.config['THEME']),
|
||||
'capsulflask/templates']),
|
||||
])
|
||||
app.jinja_loader = my_loader
|
||||
|
||||
if app.config['HUB_MODE_ENABLED']:
|
||||
|
||||
if app.config['HUB_MODEL'] == "capsul-flask":
|
||||
app.config['HUB_MODEL'] = hub_model.CapsulFlaskHub()
|
||||
|
||||
@ -176,9 +204,7 @@ if app.config['HUB_MODE_ENABLED']:
|
||||
from capsulflask import db
|
||||
db.init_app(app, is_running_server)
|
||||
|
||||
from capsulflask import (
|
||||
auth, landing, console, payment, metrics, cli, hub_api, publicapi, admin
|
||||
)
|
||||
from capsulflask import auth, landing, console, payment, metrics, cli, hub_api, admin
|
||||
|
||||
app.register_blueprint(landing.bp)
|
||||
app.register_blueprint(auth.bp)
|
||||
@ -188,13 +214,13 @@ if app.config['HUB_MODE_ENABLED']:
|
||||
app.register_blueprint(cli.bp)
|
||||
app.register_blueprint(hub_api.bp)
|
||||
app.register_blueprint(admin.bp)
|
||||
app.register_blueprint(publicapi.bp)
|
||||
|
||||
app.add_url_rule("/", endpoint="index")
|
||||
|
||||
|
||||
|
||||
if app.config['SPOKE_MODE_ENABLED']:
|
||||
|
||||
if app.config['SPOKE_MODEL'] == "shell-scripts":
|
||||
app.config['SPOKE_MODEL'] = spoke_model.ShellScriptSpoke()
|
||||
else:
|
||||
|
@ -1,4 +1,3 @@
|
||||
from base64 import b64decode
|
||||
import functools
|
||||
import re
|
||||
|
||||
@ -25,15 +24,6 @@ def account_required(view):
|
||||
|
||||
@functools.wraps(view)
|
||||
def wrapped_view(**kwargs):
|
||||
api_token = request.headers.get('authorization', None)
|
||||
if api_token is not None:
|
||||
email = get_model().authenticate_token(b64decode(api_token).decode('utf-8'))
|
||||
|
||||
if email is not None:
|
||||
session.clear()
|
||||
session["account"] = email
|
||||
session["csrf-token"] = generate()
|
||||
|
||||
if session.get("account") is None or session.get("csrf-token") is None :
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
@ -66,7 +56,7 @@ def login():
|
||||
if not email:
|
||||
errors.append("email is required")
|
||||
elif len(email.strip()) < 6 or email.count('@') != 1 or email.count('.') == 0:
|
||||
errors.append("enter a valid email address")
|
||||
errors.append("enter a valid email address")
|
||||
|
||||
if len(errors) == 0:
|
||||
result = get_model().login(email)
|
||||
|
@ -1,9 +1,7 @@
|
||||
from base64 import b64encode
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Blueprint
|
||||
from flask import flash
|
||||
from flask import current_app
|
||||
@ -100,6 +98,7 @@ def index():
|
||||
@bp.route("/<string:id>", methods=("GET", "POST"))
|
||||
@account_required
|
||||
def detail(id):
|
||||
|
||||
duration=request.args.get('duration')
|
||||
if not duration:
|
||||
duration = "5m"
|
||||
@ -189,67 +188,6 @@ def detail(id):
|
||||
duration=duration
|
||||
)
|
||||
|
||||
def _create(vm_sizes, operating_systems, public_keys_for_account, server_data):
|
||||
errors = list()
|
||||
|
||||
size = server_data.get("size")
|
||||
os = server_data.get("os")
|
||||
posted_keys_count = int(server_data.get("ssh_authorized_key_count"))
|
||||
|
||||
if not size:
|
||||
errors.append("Size is required")
|
||||
elif size not in vm_sizes:
|
||||
errors.append(f"Invalid size {size}")
|
||||
|
||||
if not os:
|
||||
errors.append("OS is required")
|
||||
elif os not in operating_systems:
|
||||
errors.append(f"Invalid os {os}")
|
||||
|
||||
posted_keys = list()
|
||||
|
||||
if posted_keys_count > 1000:
|
||||
errors.append("something went wrong with ssh keys")
|
||||
else:
|
||||
for i in range(0, posted_keys_count):
|
||||
if f"ssh_key_{i}" in server_data:
|
||||
posted_name = server_data.get(f"ssh_key_{i}")
|
||||
key = None
|
||||
for x in public_keys_for_account:
|
||||
if x['name'] == posted_name:
|
||||
key = x
|
||||
if key:
|
||||
posted_keys.append(key)
|
||||
else:
|
||||
errors.append(f"SSH Key \"{posted_name}\" doesn't exist")
|
||||
|
||||
if len(posted_keys) == 0:
|
||||
errors.append("At least one SSH Public Key is required")
|
||||
|
||||
capacity_avaliable = current_app.config["HUB_MODEL"].capacity_avaliable(
|
||||
vm_sizes[size]['memory_mb']*1024*1024
|
||||
)
|
||||
|
||||
if not capacity_avaliable:
|
||||
errors.append("""
|
||||
host(s) at capacity. no capsuls can be created at this time. sorry.
|
||||
""")
|
||||
|
||||
if len(errors) == 0:
|
||||
id = make_capsul_id()
|
||||
current_app.config["HUB_MODEL"].create(
|
||||
email = session["account"],
|
||||
id=id,
|
||||
os=os,
|
||||
size=size,
|
||||
template_image_file_name=operating_systems[os]['template_image_file_name'],
|
||||
vcpus=vm_sizes[size]['vcpus'],
|
||||
memory_mb=vm_sizes[size]['memory_mb'],
|
||||
ssh_authorized_keys=list(map(lambda x: x["content"], posted_keys))
|
||||
)
|
||||
return id, errors
|
||||
|
||||
return None, errors
|
||||
|
||||
@bp.route("/create", methods=("GET", "POST"))
|
||||
@account_required
|
||||
@ -264,12 +202,62 @@ def create():
|
||||
if request.method == "POST":
|
||||
if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']:
|
||||
return abort(418, f"u want tea")
|
||||
id, errors = _create(
|
||||
vm_sizes,
|
||||
operating_systems,
|
||||
public_keys_for_account,
|
||||
request.form)
|
||||
|
||||
size = request.form["size"]
|
||||
os = request.form["os"]
|
||||
if not size:
|
||||
errors.append("Size is required")
|
||||
elif size not in vm_sizes:
|
||||
errors.append(f"Invalid size {size}")
|
||||
|
||||
if not os:
|
||||
errors.append("OS is required")
|
||||
elif os not in operating_systems:
|
||||
errors.append(f"Invalid os {os}")
|
||||
|
||||
posted_keys_count = int(request.form["ssh_authorized_key_count"])
|
||||
posted_keys = list()
|
||||
|
||||
if posted_keys_count > 1000:
|
||||
errors.append("something went wrong with ssh keys")
|
||||
else:
|
||||
for i in range(0, posted_keys_count):
|
||||
if f"ssh_key_{i}" in request.form:
|
||||
posted_name = request.form[f"ssh_key_{i}"]
|
||||
key = None
|
||||
for x in public_keys_for_account:
|
||||
if x['name'] == posted_name:
|
||||
key = x
|
||||
if key:
|
||||
posted_keys.append(key)
|
||||
else:
|
||||
errors.append(f"SSH Key \"{posted_name}\" doesn't exist")
|
||||
|
||||
if len(posted_keys) == 0:
|
||||
errors.append("At least one SSH Public Key is required")
|
||||
|
||||
capacity_avaliable = current_app.config["HUB_MODEL"].capacity_avaliable(vm_sizes[size]['memory_mb']*1024*1024)
|
||||
|
||||
if not capacity_avaliable:
|
||||
errors.append("""
|
||||
host(s) at capacity. no capsuls can be created at this time. sorry.
|
||||
""")
|
||||
|
||||
if len(errors) == 0:
|
||||
id = make_capsul_id()
|
||||
# we can't create the vm record in the DB yet because its IP address needs to be allocated first.
|
||||
# so it will be created when the allocation happens inside the hub_api.
|
||||
current_app.config["HUB_MODEL"].create(
|
||||
email = session["account"],
|
||||
id=id,
|
||||
os=os,
|
||||
size=size,
|
||||
template_image_file_name=operating_systems[os]['template_image_file_name'],
|
||||
vcpus=vm_sizes[size]['vcpus'],
|
||||
memory_mb=vm_sizes[size]['memory_mb'],
|
||||
ssh_authorized_keys=list(map(lambda x: dict(name=x['name'], content=x['content']), posted_keys))
|
||||
)
|
||||
|
||||
return redirect(f"{url_for('console.index')}?created={id}")
|
||||
|
||||
affordable_vm_sizes = dict()
|
||||
@ -299,25 +287,23 @@ def create():
|
||||
vm_sizes=affordable_vm_sizes
|
||||
)
|
||||
|
||||
@bp.route("/keys", methods=("GET", "POST"))
|
||||
@bp.route("/ssh", methods=("GET", "POST"))
|
||||
@account_required
|
||||
def ssh_api_keys():
|
||||
def ssh_public_keys():
|
||||
errors = list()
|
||||
|
||||
token = None
|
||||
|
||||
if request.method == "POST":
|
||||
if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']:
|
||||
return abort(418, f"u want tea")
|
||||
|
||||
action = request.form["action"]
|
||||
|
||||
if action == 'upload_ssh_key':
|
||||
content = None
|
||||
method = request.form["method"]
|
||||
content = None
|
||||
if method == "POST":
|
||||
content = request.form["content"].replace("\r", " ").replace("\n", " ").strip()
|
||||
|
||||
name = request.form["name"]
|
||||
if not name or len(name.strip()) < 1:
|
||||
name = request.form["name"]
|
||||
if not name or len(name.strip()) < 1:
|
||||
if method == "POST":
|
||||
parts = re.split(" +", content)
|
||||
if len(parts) > 2 and len(parts[2].strip()) > 0:
|
||||
name = parts[2].strip()
|
||||
@ -325,9 +311,10 @@ def ssh_api_keys():
|
||||
name = parts[0].strip()
|
||||
else:
|
||||
errors.append("Name is required")
|
||||
if not re.match(r"^[0-9A-Za-z_@:. -]+$", name):
|
||||
errors.append(f"Key name '{name}' must match \"^[0-9A-Za-z_@:. -]+$\"")
|
||||
if not re.match(r"^[0-9A-Za-z_@:. -]+$", name):
|
||||
errors.append(f"Key name '{name}' must match \"^[0-9A-Za-z_@:. -]+$\"")
|
||||
|
||||
if method == "POST":
|
||||
if not content or len(content.strip()) < 1:
|
||||
errors.append("Content is required")
|
||||
else:
|
||||
@ -340,36 +327,24 @@ def ssh_api_keys():
|
||||
if len(errors) == 0:
|
||||
get_model().create_ssh_public_key(session["account"], name, content)
|
||||
|
||||
elif action == "delete_ssh_key":
|
||||
get_model().delete_ssh_public_key(session["account"], name)
|
||||
elif method == "DELETE":
|
||||
|
||||
elif action == "generate_api_token":
|
||||
name = request.form["name"]
|
||||
if name == '':
|
||||
name = datetime.utcnow().strftime('%y-%m-%d %H:%M:%S')
|
||||
token = b64encode(
|
||||
get_model().generate_api_token(session["account"], name).encode('utf-8')
|
||||
).decode('utf-8')
|
||||
|
||||
elif action == "delete_api_token":
|
||||
get_model().delete_api_token(session["account"], request.form["id"])
|
||||
if len(errors) == 0:
|
||||
get_model().delete_ssh_public_key(session["account"], name)
|
||||
|
||||
for error in errors:
|
||||
flash(error)
|
||||
|
||||
ssh_keys_list=list(map(
|
||||
keys_list=list(map(
|
||||
lambda x: dict(name=x['name'], content=f"{x['content'][:20]}...{x['content'][len(x['content'])-20:]}"),
|
||||
get_model().list_ssh_public_keys_for_account(session["account"])
|
||||
))
|
||||
|
||||
api_tokens_list = get_model().list_api_tokens(session["account"])
|
||||
|
||||
return render_template(
|
||||
"keys.html",
|
||||
"ssh-public-keys.html",
|
||||
csrf_token = session["csrf-token"],
|
||||
api_tokens=api_tokens_list,
|
||||
ssh_public_keys=ssh_keys_list,
|
||||
generated_api_token=token,
|
||||
ssh_public_keys=keys_list,
|
||||
has_ssh_public_keys=len(keys_list) > 0
|
||||
)
|
||||
|
||||
def get_vms():
|
||||
@ -393,6 +368,7 @@ def get_vm_months_float(vm, as_of):
|
||||
return days / average_number_of_days_in_a_month
|
||||
|
||||
def get_account_balance(vms, payments, as_of):
|
||||
|
||||
vm_cost_dollars = 0.0
|
||||
for vm in vms:
|
||||
vm_months = get_vm_months_float(vm, as_of)
|
||||
@ -405,6 +381,7 @@ def get_account_balance(vms, payments, as_of):
|
||||
@bp.route("/account-balance")
|
||||
@account_required
|
||||
def account_balance():
|
||||
|
||||
payment_sessions = get_model().list_payment_sessions_for_account(session['account'])
|
||||
for payment_session in payment_sessions:
|
||||
if payment_session['type'] == 'btcpay':
|
||||
|
@ -33,7 +33,7 @@ def init_app(app, is_running_server):
|
||||
result = re.search(r"^\d+_(up|down)", filename)
|
||||
if not result:
|
||||
app.logger.error(f"schemaVersion {filename} must match ^\\d+_(up|down). exiting.")
|
||||
continue
|
||||
exit(1)
|
||||
key = result.group()
|
||||
with open(join(schemaMigrationsPath, filename), 'rb') as file:
|
||||
schemaMigrations[key] = file.read().decode("utf8")
|
||||
@ -43,7 +43,7 @@ def init_app(app, is_running_server):
|
||||
hasSchemaVersionTable = False
|
||||
actionWasTaken = False
|
||||
schemaVersion = 0
|
||||
desiredSchemaVersion = 19
|
||||
desiredSchemaVersion = 18
|
||||
|
||||
cursor = connection.cursor()
|
||||
|
||||
@ -128,3 +128,4 @@ def close_db(e=None):
|
||||
if db_model is not None:
|
||||
db_model.cursor.close()
|
||||
current_app.config['PSYCOPG2_CONNECTION_POOL'].putconn(db_model.connection)
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
|
||||
import re
|
||||
|
||||
# I was never able to get this type hinting to work correctly
|
||||
# from psycopg2.extensions import connection as Psycopg2Connection, cursor as Psycopg2Cursor
|
||||
import hashlib
|
||||
from nanoid import generate
|
||||
from flask import current_app
|
||||
from typing import List
|
||||
@ -17,6 +17,7 @@ class DBModel:
|
||||
self.cursor = cursor
|
||||
|
||||
|
||||
|
||||
# ------ LOGIN ---------
|
||||
|
||||
|
||||
@ -43,16 +44,6 @@ class DBModel:
|
||||
|
||||
return (token, ignoreCaseMatches)
|
||||
|
||||
def authenticate_token(self, token):
|
||||
m = hashlib.md5()
|
||||
m.update(token.encode('utf-8'))
|
||||
hash_token = m.hexdigest()
|
||||
self.cursor.execute("SELECT email FROM api_tokens WHERE token = %s", (hash_token, ))
|
||||
result = self.cursor.fetchall()
|
||||
if len(result) == 1:
|
||||
return result[0]
|
||||
return None
|
||||
|
||||
def consume_token(self, token):
|
||||
self.cursor.execute("SELECT email FROM login_tokens WHERE token = %s and created > (NOW() - INTERVAL '20 min')", (token, ))
|
||||
row = self.cursor.fetchone()
|
||||
@ -141,32 +132,6 @@ class DBModel:
|
||||
self.cursor.execute( "DELETE FROM ssh_public_keys where email = %s AND name = %s", (email, name) )
|
||||
self.connection.commit()
|
||||
|
||||
def list_api_tokens(self, email):
|
||||
self.cursor.execute(
|
||||
"SELECT id, token, name, created FROM api_tokens WHERE email = %s",
|
||||
(email, )
|
||||
)
|
||||
return list(map(
|
||||
lambda x: dict(id=x[0], token=x[1], name=x[2], created=x[3]),
|
||||
self.cursor.fetchall()
|
||||
))
|
||||
|
||||
def generate_api_token(self, email, name):
|
||||
token = generate()
|
||||
m = hashlib.md5()
|
||||
m.update(token.encode('utf-8'))
|
||||
hash_token = m.hexdigest()
|
||||
self.cursor.execute(
|
||||
"INSERT INTO api_tokens (email, name, token) VALUES (%s, %s, %s)",
|
||||
(email, name, hash_token)
|
||||
)
|
||||
self.connection.commit()
|
||||
return token
|
||||
|
||||
def delete_api_token(self, email, id_):
|
||||
self.cursor.execute( "DELETE FROM api_tokens where email = %s AND id = %s", (email, id_))
|
||||
self.connection.commit()
|
||||
|
||||
def list_vms_for_account(self, email):
|
||||
self.cursor.execute("""
|
||||
SELECT vms.id, vms.public_ipv4, vms.public_ipv6, vms.size, vms.os, vms.created, vms.deleted, vm_sizes.dollars_per_month
|
||||
@ -514,3 +479,8 @@ class DBModel:
|
||||
#cursor.close()
|
||||
|
||||
return to_return
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -228,11 +228,12 @@ class CapsulFlaskHub(VirtualizationInterface):
|
||||
# no need to do anything here since if it cant be parsed then generic_operation will handle it.
|
||||
pass
|
||||
|
||||
if error_message != "":
|
||||
raise ValueError(f"create capsul operation {operation_id} on {assigned_hosts} failed with {error_message}")
|
||||
|
||||
if number_of_assigned != 1:
|
||||
assigned_hosts_string = ", ".join(assigned_hosts)
|
||||
raise ValueError(f"expected create capsul operation {operation_id} to be assigned to one host, it was assigned to {number_of_assigned} ({assigned_hosts_string})")
|
||||
if error_message != "":
|
||||
raise ValueError(f"create capsul operation {operation_id} on {assigned_hosts_string} failed with {error_message}")
|
||||
|
||||
|
||||
def destroy(self, email: str, id: str):
|
||||
|
@ -1,40 +0,0 @@
|
||||
import datetime
|
||||
|
||||
from flask import Blueprint
|
||||
from flask import current_app
|
||||
from flask import jsonify
|
||||
from flask import request
|
||||
from flask import session
|
||||
from nanoid import generate
|
||||
|
||||
from capsulflask.auth import account_required
|
||||
from capsulflask.db import get_model
|
||||
|
||||
bp = Blueprint("publicapi", __name__, url_prefix="/api")
|
||||
|
||||
@bp.route("/capsul/create", methods=["POST"])
|
||||
@account_required
|
||||
def capsul_create():
|
||||
email = session["account"]
|
||||
|
||||
from .console import _create,get_account_balance, get_payments, get_vms
|
||||
|
||||
vm_sizes = get_model().vm_sizes_dict()
|
||||
operating_systems = get_model().operating_systems_dict()
|
||||
public_keys_for_account = get_model().list_ssh_public_keys_for_account(session["account"])
|
||||
account_balance = get_account_balance(get_vms(), get_payments(), datetime.datetime.utcnow())
|
||||
capacity_avaliable = current_app.config["HUB_MODEL"].capacity_avaliable(512*1024*1024)
|
||||
|
||||
request.json['ssh_authorized_key_count'] = 1
|
||||
|
||||
id, errors = _create(
|
||||
vm_sizes,
|
||||
operating_systems,
|
||||
public_keys_for_account,
|
||||
request.json)
|
||||
|
||||
if id is not None:
|
||||
return jsonify(
|
||||
id=id,
|
||||
)
|
||||
return jsonify(errors=errors)
|
@ -1,2 +0,0 @@
|
||||
DROP TABLE api_keys;
|
||||
UPDATE schemaversion SET version = 18;
|
@ -1,9 +0,0 @@
|
||||
CREATE TABLE api_tokens (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
|
||||
name TEXT NOT NULL,
|
||||
created TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
token TEXT NOT NULL
|
||||
);
|
||||
|
||||
UPDATE schemaversion SET version = 19;
|
@ -3,7 +3,7 @@
|
||||
# check available RAM and IPv4s
|
||||
|
||||
ram_bytes_to_allocate="$1"
|
||||
ram_bytes_available=$(grep -E "^(size|memory_available_bytes)" /proc/spl/kstat/zfs/arcstats | awk '{sum+=$3} END {printf "%.0f", sum}')
|
||||
ram_bytes_available="$(($(grep Available /proc/meminfo | grep -o '[0-9]*') * 1024))"
|
||||
ram_bytes_remainder="$((ram_bytes_available - ram_bytes_to_allocate))"
|
||||
|
||||
if echo "$ram_bytes_to_allocate" | grep -vqE "^[0-9]+$"; then
|
||||
@ -11,8 +11,8 @@ if echo "$ram_bytes_to_allocate" | grep -vqE "^[0-9]+$"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 20GB
|
||||
if [ "$ram_bytes_remainder" -le $((20 * 1024 * 1024 * 1024)) ]; then
|
||||
# 0.25GB
|
||||
if [ "$ram_bytes_remainder" -le $((1 * 1024 * 1024 * 1024 / 4)) ]; then
|
||||
echo "VM is requesting more RAM than $(hostname -f) has available."
|
||||
echo "Bytes requested: $ram_bytes_to_allocate"
|
||||
echo "Bytes available: $ram_bytes_available"
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
vmname="$1"
|
||||
template_file="/tank/img/$2"
|
||||
qemu_tank_dir="/tank"
|
||||
vcpus="$3"
|
||||
memory="$4"
|
||||
pubkeys="$5"
|
||||
@ -50,40 +51,40 @@ if echo "$public_ipv4" | grep -vqE "^[0-9.]+$"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
disk="/tank/vm/$vmname.qcow2"
|
||||
cdrom="/tank/vm/$vmname.iso"
|
||||
xml="/tank/vm/$vmname.xml"
|
||||
disk="$vmname.qcow2"
|
||||
cdrom="$vmname.iso"
|
||||
xml="$vmname.xml"
|
||||
|
||||
if [ -f /tank/vm/$vmname.qcow2 ]; then
|
||||
echo "Randomly generated name matched an existing VM! Odds are like one in a billion. Buy a lotto ticket."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "$template_file" "$disk"
|
||||
cp "$template_file" "/tank/vm/$disk"
|
||||
cp /tank/config/cyberia-cloudinit.yml /tmp/cloudinit.yml
|
||||
echo "$pubkeys" | while IFS= read -r line; do
|
||||
echo " - $line" >> /tmp/cloudinit.yml
|
||||
done
|
||||
|
||||
cloud-localds "$cdrom" /tmp/cloudinit.yml
|
||||
cloud-localds "/tank/vm/$cdrom" /tmp/cloudinit.yml
|
||||
|
||||
qemu-img resize "$disk" "$root_volume_size"
|
||||
qemu-img resize "/tank/vm/$disk" "$root_volume_size"
|
||||
virt-install \
|
||||
--memory "$memory" \
|
||||
--vcpus "$vcpus" \
|
||||
--name "$vmname" \
|
||||
--disk "$disk",bus=virtio \
|
||||
--disk "$cdrom",device=cdrom \
|
||||
--disk "$qemu_tank_dir/vm/$disk",bus=virtio \
|
||||
--disk "$qemu_tank_dir/vm/$cdrom",device=cdrom \
|
||||
--os-type Linux \
|
||||
--os-variant generic \
|
||||
--virt-type kvm \
|
||||
--graphics vnc,listen=127.0.0.1 \
|
||||
--network network=$network_name,filterref=clean-traffic,model=virtio \
|
||||
--network network=$network_name,model=virtio \
|
||||
--import \
|
||||
--print-xml > "$xml"
|
||||
--print-xml > "/tank/vm/$xml"
|
||||
|
||||
chmod 0600 "$xml" "$disk" "$cdrom"
|
||||
virsh define "$xml"
|
||||
chmod 0600 "/tank/vm/$xml" "/tank/vm/$disk" "/tank/vm/$cdrom"
|
||||
virsh define "/tank/vm/$xml"
|
||||
virsh start "$vmname"
|
||||
|
||||
echo "success"
|
||||
|
BIN
capsulflask/static/favicon.yolocolo.ico
Normal file
BIN
capsulflask/static/favicon.yolocolo.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
capsulflask/static/icon.yolocolo.png
Normal file
BIN
capsulflask/static/icon.yolocolo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
@ -1,8 +1,8 @@
|
||||
html {
|
||||
color: #bdc7b8;
|
||||
color: #241e1e;
|
||||
font: calc(0.40rem + 1vmin) monospace;
|
||||
overflow-y: scroll;
|
||||
background-color: #241e1e;
|
||||
background-color: #bdc7b8;
|
||||
}
|
||||
|
||||
body {
|
||||
@ -19,8 +19,8 @@ body {
|
||||
}
|
||||
|
||||
a {
|
||||
color:#6CF;
|
||||
text-shadow: 1px 1px 0px #000c;
|
||||
color:#00517a;
|
||||
text-shadow: 1px 1px 0px #eee;
|
||||
}
|
||||
|
||||
a.no-shadow {
|
||||
@ -28,7 +28,7 @@ a.no-shadow {
|
||||
}
|
||||
|
||||
a:hover, a:active, a:visited {
|
||||
color: #b5bd68;
|
||||
color: #323417;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
@ -59,11 +59,11 @@ h1, h2, h3, h4, h5 {
|
||||
margin: initial;
|
||||
padding: initial;
|
||||
text-transform: uppercase;
|
||||
text-shadow: 2px 2px 0px #0007;
|
||||
text-shadow: 2px 2px 0px #eee;
|
||||
}
|
||||
|
||||
main {
|
||||
border: 1px dashed #bdc7b8;
|
||||
border: 1px dashed #241e1e;
|
||||
padding: 1rem;
|
||||
margin-bottom: 2em;
|
||||
|
||||
@ -143,7 +143,7 @@ input, textarea, select, label {
|
||||
input, select, textarea {
|
||||
outline: 0;
|
||||
padding: 0.25em 0.5em;
|
||||
color: #bdc7b8;
|
||||
color: #241e1e;
|
||||
background-color: #bdc7b805;
|
||||
}
|
||||
|
||||
|
35
capsulflask/static/style.yolocolo.css
Normal file
35
capsulflask/static/style.yolocolo.css
Normal file
@ -0,0 +1,35 @@
|
||||
html {
|
||||
color: #241e1e !important;
|
||||
background-color: #bdc7b8 !important;
|
||||
}
|
||||
|
||||
a {
|
||||
color:#00517a !important;
|
||||
text-shadow: 1px 1px 0px #eee !important;
|
||||
}
|
||||
|
||||
a:hover, a:active, a:visited {
|
||||
color: #323417 !important;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
text-shadow: 2px 2px 0px #eee !important;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
text-shadow: 2px 2px 0px #eee;
|
||||
}
|
||||
|
||||
main {
|
||||
border: 1px dashed #241e1e !important;
|
||||
}
|
||||
|
||||
|
||||
input, select, textarea {
|
||||
color: #241e1e !important;
|
||||
}
|
||||
|
||||
th {
|
||||
border-right: 1px solid #eee !important;
|
||||
text-align: left !important;
|
||||
}
|
@ -49,8 +49,6 @@
|
||||
{% if btcpay_enabled %}
|
||||
<li><a href="/payment/btcpay">Add funds with Bitcoin/Litecoin/Monero (btcpay)</a></li>
|
||||
{% endif %}
|
||||
|
||||
<li>Cash: email <a href="mailto:treasurer@cyberia.club">treasurer@cyberia.club</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -13,7 +13,7 @@
|
||||
<nav>
|
||||
<div class="row justify-space-between half-margin">
|
||||
<div>
|
||||
<a href="/"><b>Capsul</b></a>💊
|
||||
🦉 <a href="/"><b>YOLOCOLO</b></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@ -27,11 +27,10 @@
|
||||
<div class="row justify-center half-margin wrap nav-links">
|
||||
<a href="/pricing">Pricing</a>
|
||||
<a href="/faq">FAQ</a>
|
||||
<a href="/changelog">Changelog</a>
|
||||
|
||||
{% if session["account"] %}
|
||||
<a href="/console">Capsuls</a>
|
||||
<a href="/console/keys">SSH & API Keys</a>
|
||||
<a href="/console/ssh">SSH Public Keys</a>
|
||||
<a href="/console/account-balance">Account Balance</a>
|
||||
{% endif %}
|
||||
|
||||
@ -47,11 +46,12 @@
|
||||
</main>
|
||||
{% block subcontent %}{% endblock %}
|
||||
<footer>
|
||||
(c) Attribution-ShareAlike 4.0 International <br/>
|
||||
A service by Cyberia Computer Club 2020-<span class="bigtext">∞</span> <br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<a href="https://giit.cyberia.club/~forest/capsul-flask/tree/master/capsulflask{% block pagesource %}{% endblock %}">View page source</a>
|
||||
This server runs <a
|
||||
href="https://giit.cyberia.club/~forest/capsul-flask">capsul-flask</a> by
|
||||
Cyberia Computer Club, available under the <a
|
||||
href="https://creativecommons.org/licenses/by-sa/4.0/">Attribution-ShareAlike
|
||||
4.0 International</a> licence.<br/><br/>
|
||||
<a href="https://git.autonomic.zone/3wordchant/capsul-flask/src/branch/yolocolo/capsulflask{% block pagesource %}{% endblock %}">View page source</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -101,7 +101,7 @@
|
||||
</div>
|
||||
<div class="row justify-start">
|
||||
<label class="align" for="ssh_authorized_keys">SSH Authorized Keys</label>
|
||||
<a id="ssh_authorized_keys" href="/console/keys">{{ vm['ssh_authorized_keys'] }}</a>
|
||||
<a id="ssh_authorized_keys" href="/console/ssh">{{ vm['ssh_authorized_keys'] }}</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -31,7 +31,7 @@
|
||||
<p>(At least one month of funding is required)</p>
|
||||
{% elif no_ssh_public_keys %}
|
||||
<p>You don't have any ssh public keys yet.</p>
|
||||
<p>You must <a href="/console/keys">upload one</a> before you can create a Capsul.</p>
|
||||
<p>You must <a href="/console/ssh">upload one</a> before you can create a Capsul.</p>
|
||||
{% elif not capacity_avaliable %}
|
||||
<p>Host(s) at capacity. No capsuls can be created at this time. sorry. </p>
|
||||
{% else %}
|
||||
|
@ -10,82 +10,32 @@
|
||||
<p>
|
||||
<ul>
|
||||
<li>
|
||||
Which instance type should I buy?
|
||||
<p>There are no hard rules for this sort of thing, but here are some guidelines:</p>
|
||||
<p>f1-xs: blog, vpn, bot, cgit</p>
|
||||
<p>f1-s: a bot, owncloud, gitea, popular blog</p>
|
||||
<p>f1-m: docker host, build system</p>
|
||||
<p>f1-l: large webservice, rotund java app</p>
|
||||
<p>f1-x: gitlab (wow such memory very devops</p>
|
||||
<p>f1-xx: something gargantuan</p>
|
||||
What is this?
|
||||
<p>
|
||||
This is a <strong>technical demo</strong> of <a
|
||||
href="https://giit.cyberia.club/~forest/capsul-flask">Capsul</a>, for the
|
||||
as-yet-untitled <a href="https://coops.tech">Cotech</a> server hosting
|
||||
initiative, which you can <a
|
||||
href="https://community.coops.tech/t/call-for-input-v2-co-op-vps-survey/2802/9">read
|
||||
about on the Cotech forum</a>.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
How do I log in?
|
||||
<p>ssh to the ip provided to you using the cyberian user.</p>
|
||||
<pre class='code'>$ ssh cyberian@1.2.3.4</pre>
|
||||
<p>For more information, see <a href="/about-ssh">Understanding the Secure Shell Protocol (SSH)</a>.</p>
|
||||
What do you mean, "technical demo"?
|
||||
<p>No backups</p>
|
||||
<p>No service level agreement</p>
|
||||
<p>"Best effort" support</p>
|
||||
</li>
|
||||
<li>
|
||||
How do I change to the root user?
|
||||
<p>The cyberian user has passwordless sudo access by default. This should work:</p>
|
||||
<pre class='code'>
|
||||
# Linux
|
||||
$ sudo su -
|
||||
|
||||
# OpenBSD
|
||||
$ doas su -</pre>
|
||||
Where can I get this, but, more reliable?
|
||||
<p>Cyberia, the authors of this platform, run the canonical instance, <a
|
||||
href="https://capsul.org">Capsul.org</a>, on hardware they own. Please
|
||||
send them your money! (cash, crypto, or card accepted).</p>
|
||||
</li>
|
||||
<li>
|
||||
Do you offer reverse DNS?
|
||||
<p>We do, but right now it's a manual process. Shoot us an email and we'll get it done.</p>
|
||||
</li>
|
||||
<li>
|
||||
What if I don't pay / don't maintain my payments?
|
||||
<p>Your VM will eventually be deleted.
|
||||
Capsul will send you a few inoffensive reminders as that termination date approaches.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
Besides my virtual machines and payments, what information do you keep about me?
|
||||
<p>We associate an email address with every VM so that we can track payment and respond to support requests.</p>
|
||||
<p>If you pay with a credit card, Stripe stores some additional details about you that we literally cannot delete.</p>
|
||||
</li>
|
||||
<li>
|
||||
What can I do with my VM?
|
||||
<p>Make it into a mailserver, a tor relay, a VPN host, whatever you'd like - we do have one small request, though.</p>
|
||||
<p>Crypto mining on capsul is currently considered obnoxious behavior, because the hashrates on our CPUs is so low and because mining crypto consumes entire processor cores that could have otherwise been shared between many dozens of other users.</p>
|
||||
<p>In the future, if we have plentiful CPU resources, we may come out with a tier more suitable for mining - maybe a high cpu tier or similar, where each VM gets a full dedicated core and sharing them is not anticipated.</p>
|
||||
<p>We will never snoop on your traffic or inspect what's going on inside of our customer virtual machines - we don't want to. We hope that you'll extend us a similar courtesy and try not to use too much of our shared CPU resources. Capsul is currently a shared (resource-wise) world, and we all must live in it together!</p>
|
||||
<p>Also, mandatory: our systems exist within the USA, and as such those systems are bound by US law.</p>
|
||||
</li>
|
||||
<li>
|
||||
Can you recover my passwords/insert new keys?
|
||||
<p>Can we? Technically yes. Will we? No, never. It would violate the trust that our users have in us.
|
||||
We have no interest in touching client VMs after they're running.
|
||||
We promise to keep your machines running smoothly.
|
||||
If you lose access to your VM, that's on you.</p>
|
||||
</li>
|
||||
<li>
|
||||
Do you offer refunds?
|
||||
<p>Not now, but email us and we can probably figure something out.</p>
|
||||
</li>
|
||||
<li>
|
||||
Where do the VMs run? Is it on a machine that you guys own/control?
|
||||
<p>Capsul runs on a server named Baikal which Cyberia built from scratch & mailed to a datacenter
|
||||
in Georgia called CyberWurx. CyberWurx staff installed it for us in a rack space that
|
||||
Cyberia pays for. </p>
|
||||
</li>
|
||||
<li>
|
||||
Do you offer support?
|
||||
<p>Yep, see <a href="/support">our support page</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
Do you have an SLA?
|
||||
<p>No, but we normally respond pretty quickly.</p>
|
||||
</li>
|
||||
<li>
|
||||
Will you implement feature X?
|
||||
<p>Maybe! Email <a href="mailto:ops@cyberia.club">ops@cyberia.club</a> and ask us about it.</p>
|
||||
How do I use this system?
|
||||
<p>Please see <a href="https://capsul.org/faq">the official Capsul FAQ
|
||||
page</a>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
@ -1,31 +1,26 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<h1>CAPSUL</h1>
|
||||
<h1>
|
||||
<pre>
|
||||
.-.
|
||||
/:::\
|
||||
/::::/
|
||||
/ `-:/
|
||||
/ /
|
||||
\ /
|
||||
`"`
|
||||
_ _
|
||||
_ _ ___ | | ___ ___ ___ | | ___
|
||||
| | | |/ _ \| |/ _ \ / __/ _ \| |/ _ \
|
||||
| |_| | (_) | | (_) | (_| (_) | | (_) |
|
||||
\__, |\___/|_|\___/ \___\___/|_|\___/
|
||||
|___/
|
||||
|
||||
</pre>
|
||||
<span>Simple, fast, private compute by <a href="https://cyberia.club">cyberia.club</a></span>
|
||||
<span>Co-operative hosting using <a href="https://cyberia.club">Cyberia</a>'s Capsul</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block subcontent %}
|
||||
<p>
|
||||
<ul>
|
||||
<li>Low friction: simply log in with your email address and fund your account with Credit/Debit or Cryptocurrency</li>
|
||||
<li>All root disks are backed up at no charge</li>
|
||||
<li>All storage is fast, local, and solid-state</li>
|
||||
<li>All network connections are low latency</li>
|
||||
<li>Supported by amazing volunteers from Cyberia</li>
|
||||
<li>Upfront prices, no confusing billing</li>
|
||||
<li>Operated by a Minnesota non-profit organization that will never exploit you</li>
|
||||
<li>We donate a portion of our proceeds to likeminded hacker groups around the globe</li>
|
||||
<li>Sign up for an account!</li>
|
||||
<li>Add some funds!</li>
|
||||
<li>Create a VPS!</li>
|
||||
<li>Give your feedback!</li>
|
||||
</ul>
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
@ -7,42 +7,17 @@
|
||||
<h1>CAPSUL TYPES & PRICING</h1>
|
||||
</div>
|
||||
<div class="row half-margin">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>type</th>
|
||||
<th>monthly*</th>
|
||||
<th>cpus</th>
|
||||
<th>mem</th>
|
||||
<th>ssd</th>
|
||||
<th>net</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for vm_size_key, vm_size in vm_sizes.items() %}
|
||||
<tr>
|
||||
<td>{{ vm_size_key }}</td>
|
||||
<td>${{ vm_size['dollars_per_month'] }}</td>
|
||||
<td>{{ vm_size['vcpus'] }}</td>
|
||||
<td>{{ vm_size['memory_mb'] }}</td>
|
||||
<td>25G</td>
|
||||
<td>{{ vm_size['bandwidth_gb_per_month'] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
Rates for this service aren't set yet. You can see Cyberia's Capsul pricing
|
||||
on <a href="https://capsul.org/pricing">their website</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="row half-margin">
|
||||
<div>
|
||||
<pre>
|
||||
* net is calculated as a per-month average
|
||||
* vms are billed for a minimum of 24 hours upon creation
|
||||
* all VMs come standard with one public IPv4 address
|
||||
SUPPORTED OPERATING SYSTEMS:
|
||||
|
||||
SUPPORTED OPERATING SYSTEMS:
|
||||
|
||||
|
||||
{% for os_id, os in operating_systems.items() %} - {{ os.description }}
|
||||
{% endfor %}
|
||||
</pre>
|
||||
{% for os_id, os in operating_systems.items() %} - {{ os.description }}
|
||||
{% endfor %}
|
||||
</pre>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,18 +1,17 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}SSH & API Keys{% endblock %}
|
||||
{% block title %}SSH Public Keys{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row third-margin">
|
||||
<h1>SSH PUBLIC KEYS</h1>
|
||||
</div>
|
||||
<div class="row third-margin"><div>
|
||||
{% if ssh_public_keys|length > 0 %} <hr/> {% endif %}
|
||||
{% if has_ssh_public_keys %} <hr/> {% endif %}
|
||||
|
||||
{% for ssh_public_key in ssh_public_keys %}
|
||||
<form method="post">
|
||||
<input type="hidden" name="method" value="DELETE"></input>
|
||||
<input type="hidden" name="action" value="delete_ssh_key"></input>
|
||||
<input type="hidden" name="name" value="{{ ssh_public_key['name'] }}"></input>
|
||||
<input type="hidden" name="csrf-token" value="{{ csrf_token }}"/>
|
||||
<div class="row">
|
||||
@ -23,14 +22,13 @@
|
||||
</form>
|
||||
{% endfor %}
|
||||
|
||||
{% if ssh_public_keys|length > 0 %} <hr/> {% endif %}
|
||||
{% if has_ssh_public_keys %} <hr/> {% endif %}
|
||||
|
||||
<div class="third-margin">
|
||||
<h1>UPLOAD A NEW SSH PUBLIC KEY</h1>
|
||||
</div>
|
||||
<form method="post">
|
||||
<input type="hidden" name="method" value="POST"></input>
|
||||
<input type="hidden" name="action" value="upload_ssh_key"></input>
|
||||
<input type="hidden" name="csrf-token" value="{{ csrf_token }}"/>
|
||||
<div class="row justify-start">
|
||||
<label class="align" for="content">File Contents</label>
|
||||
@ -56,51 +54,6 @@
|
||||
</div>
|
||||
</form>
|
||||
</div></div>
|
||||
<hr/>
|
||||
<div class="row third-margin">
|
||||
<h1>API KEYS</h1>
|
||||
</div>
|
||||
<div class="row third-margin"><div>
|
||||
{% if generated_api_token %}
|
||||
<hr/>
|
||||
Generated key:
|
||||
<span class="code">{{ generated_api_token }}</span>
|
||||
{% endif %}
|
||||
{% if api_tokens|length >0 %} <hr/>{% endif %}
|
||||
{% for api_token in api_tokens %}
|
||||
<form method="post">
|
||||
<input type="hidden" name="method" value="DELETE"></input>
|
||||
<input type="hidden" name="action" value="delete_api_token"></input>
|
||||
<input type="hidden" name="id" value="{{ api_token['id'] }}"></input>
|
||||
<input type="hidden" name="csrf-token" value="{{ csrf_token }}"/>
|
||||
<div class="row">
|
||||
<span class="code">{{ api_token['name'] }}</span>
|
||||
created {{ api_token['created'].strftime("%b %d %Y") }}
|
||||
<input type="submit" value="Delete">
|
||||
</div>
|
||||
</form>
|
||||
{% endfor %}
|
||||
{% if api_tokens|length >0 %} <hr/>{% endif %}
|
||||
|
||||
<div class="third-margin">
|
||||
<h1>GENERATE A NEW API KEY</h1>
|
||||
</div>
|
||||
<form method="post">
|
||||
<input type="hidden" name="method" value="POST"></input>
|
||||
<input type="hidden" name="action" value="generate_api_token"></input>
|
||||
<input type="hidden" name="csrf-token" value="{{ csrf_token }}"/>
|
||||
<div class="smalltext">
|
||||
<p>Generate a new API key, to integrate with other systems.</p>
|
||||
</div>
|
||||
<div class="row justify-start">
|
||||
<label class="align" for="name">Key Name</label>
|
||||
<input type="text" id="name" name="name"></input> (defaults to creation time)
|
||||
</div>
|
||||
<div class="row justify-end">
|
||||
<input type="submit" value="Generate">
|
||||
</div>
|
||||
</form>
|
||||
</div></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block pagesource %}/templates/ssh-public-keys.html{% endblock %}
|
@ -7,20 +7,14 @@
|
||||
<h1>SUPPORT</h1>
|
||||
</div>
|
||||
<div class="row half-margin">
|
||||
<a href="mailto:support@cyberia.club?subject=Please%20help!">support@cyberia.club</a>
|
||||
<a href="mailto:yolocolo@doesthisthing.work?subject=Please%20help!">yolocolo@doesthisthing.work</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block subcontent %}
|
||||
<p>
|
||||
Note: We maintain a searchable archive of all support emails at
|
||||
<a href="https://lists.cyberia.club/~cyberia/support">https://lists.cyberia.club/~cyberia/support</a>
|
||||
</p>
|
||||
<p>
|
||||
If you do not want your mail to appear in a public archive, email <a href="mailto:capsul@cyberia.club?subject=Please%20help!">capsul@cyberia.club</a> instead.
|
||||
</p>
|
||||
<p>
|
||||
Please describe your problem or feature request, and we will do our best to get back to you promptly. Thank you very much.
|
||||
You can also find us on Matrix: <a
|
||||
href="https://matrix.to/#/#untitled-hosting.public:autonomic.zone">#untitled-hosting.public:autonomic.zone</a>.
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
|
94
capsulflask/theme/yolocolo/account-balance.html
Normal file
94
capsulflask/theme/yolocolo/account-balance.html
Normal file
@ -0,0 +1,94 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Account Balance{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row third-margin">
|
||||
<h1>Account Balance: ${{ account_balance }}</h1>
|
||||
</div>
|
||||
<div class="half-margin">
|
||||
|
||||
{% if has_vms and has_payments and warning_text != "" %}
|
||||
<div class="row">
|
||||
<pre class="wrap">{{ warning_text }}</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
{% if has_payments %}
|
||||
<div>
|
||||
<div class="row third-margin">
|
||||
<h1>Payments</h1>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>amount</th>
|
||||
<th>date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for payment in payments %}
|
||||
<tr>
|
||||
<td class="{{ payment['class_name'] }}">${{ payment["dollars"] }}</td>
|
||||
<td class="{{ payment['class_name'] }}">{{ payment["created"] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
<ul>
|
||||
<li>
|
||||
<h1>PAYMENT OPTIONS</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/payment/stripe">Add funds with Credit/Debit (stripe)</a>
|
||||
<ul><li>notice: stripe will load nonfree javascript </li></ul>
|
||||
</li>
|
||||
{% if btcpay_enabled %}
|
||||
<li><a href="/payment/btcpay">Add funds with Bitcoin/Litecoin/Monero (btcpay)</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{% if has_vms %}
|
||||
<div class="row third-margin">
|
||||
<h1>Capsuls Billed</h1>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="small">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>id</th>
|
||||
<th>created</th>
|
||||
<th>deleted</th>
|
||||
<th>$/month</th>
|
||||
<th>months</th>
|
||||
<th>$ billed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for vm in vms_billed %}
|
||||
<tr>
|
||||
<td>{{ vm["id"] }}</td>
|
||||
<td>{{ vm["created"] }}</td>
|
||||
<td>{{ vm["deleted"] }}</td>
|
||||
<td>${{ vm["dollars_per_month"] }}</td>
|
||||
<td>{{ vm["months"] }}</td>
|
||||
<td>${{ vm["dollars"] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block pagesource %}/templates/create-capsul.html{% endblock %}
|
60
capsulflask/theme/yolocolo/base.html
Normal file
60
capsulflask/theme/yolocolo/base.html
Normal file
@ -0,0 +1,60 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Namecoin Address: N2aVL6pHtBp7EtNGb3jpsL2L2NyjBNbiB1 -->
|
||||
<link href="{{ url_for('static', filename='favicon.yolocolo.ico') }}" rel="icon">
|
||||
<title>{% block title %}{% endblock %}{% if self.title() %} - {% endif %}Capsul</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="Description" content="Cyberia Capsul">
|
||||
{% block head %}{% endblock %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.yolocolo.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<div class="row justify-space-between half-margin">
|
||||
<div>
|
||||
🦉 <a href="/"><b>YOLOCOLO</b></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
{% if session["account"] %}
|
||||
{ {{ session["account"] }} <a href="{{ url_for('auth.logout') }}">Log Out</a> }
|
||||
{% else %}
|
||||
<a href="{{ url_for('auth.login') }}">Login</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-center half-margin wrap nav-links">
|
||||
<a href="/pricing">Pricing</a>
|
||||
<a href="/faq">FAQ</a>
|
||||
|
||||
{% if session["account"] %}
|
||||
<a href="/console">Capsuls</a>
|
||||
<a href="/console/ssh">SSH Public Keys</a>
|
||||
<a href="/console/account-balance">Account Balance</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="/support">Support</a>
|
||||
</div>
|
||||
</nav>
|
||||
{% for message in get_flashed_messages() %}
|
||||
<div class="flash">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% block custom_flash %}{% endblock %}
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
{% block subcontent %}{% endblock %}
|
||||
<footer>
|
||||
This server runs <a
|
||||
href="https://giit.cyberia.club/~forest/capsul-flask">capsul-flask</a> by
|
||||
Cyberia Computer Club, available under the <a
|
||||
href="https://creativecommons.org/licenses/by-sa/4.0/">Attribution-ShareAlike
|
||||
4.0 International</a> licence.<br/><br/>
|
||||
<a href="https://git.autonomic.zone/3wordchant/capsul-flask/src/branch/yolocolo/capsulflask{% block pagesource %}{% endblock %}">View page source</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
68
capsulflask/theme/yolocolo/capsuls.html
Normal file
68
capsulflask/theme/yolocolo/capsuls.html
Normal file
@ -0,0 +1,68 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Capsuls{% endblock %}
|
||||
|
||||
{% block custom_flash %}
|
||||
{% if created %}
|
||||
<div class="flash green">{{ created }} successfully created!</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row third-margin">
|
||||
<h1>Capsuls</h1>
|
||||
</div>
|
||||
<div class="third-margin">
|
||||
{% if has_vms %}
|
||||
<div class="row third-margin justify-end">
|
||||
<a href="/console/create">Create Capsul</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="heart-icon">❦</th>
|
||||
<th>id</th>
|
||||
<th>size</th>
|
||||
<th>cpu</th>
|
||||
<th>mem</th>
|
||||
<th>ipv4</th>
|
||||
<th>os</th>
|
||||
<th>created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for vm in vms %}
|
||||
<tr>
|
||||
{% if vm['state'] == 'starting' or vm['state'] == 'stopping' %}
|
||||
<td class="capsul-status waiting-pulse">•</td>
|
||||
{% elif vm['state'] == 'crashed' or vm['state'] == 'blocked' or vm['state'] == 'stopped' %}
|
||||
<td class="capsul-status red">•</td>
|
||||
{% elif vm['state'] == 'unknown' %}
|
||||
<td class="capsul-status-questionmark">?</td>
|
||||
{% else %}
|
||||
<td class="capsul-status green">•</td>
|
||||
{% endif %}
|
||||
|
||||
<td><a class="no-shadow" href="/console/{{ vm['id'] }}">{{ vm["id"] }}</a></td>
|
||||
<td>{{ vm["size"] }}</td>
|
||||
<td class="metrics"><img src="/metrics/cpu/{{ vm['id'] }}/5m/s"/></td>
|
||||
<td class="metrics"><img src="/metrics/memory/{{ vm['id'] }}/5m/s"/></td>
|
||||
<td class="{{ vm['ipv4_status'] }}">{{ vm["ipv4"] }}</td>
|
||||
<td>{{ vm["os"] }}</td>
|
||||
<td>{{ vm["created"] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="row">
|
||||
<p>You don't have any Capsuls running. <a href="/console/create">Create one</a> today!</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block pagesource %}/templates/capsuls.html{% endblock %}
|
46
capsulflask/theme/yolocolo/faq.html
Normal file
46
capsulflask/theme/yolocolo/faq.html
Normal file
@ -0,0 +1,46 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}FAQ{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row full-margin"><h1>Frequently Asked Questions</h1></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block subcontent %}
|
||||
<p>
|
||||
<ul>
|
||||
<li>
|
||||
What is this?
|
||||
<p>
|
||||
This is a <strong>technical demo</strong> of <a
|
||||
href="https://giit.cyberia.club/~forest/capsul-flask">Capsul</a>, for the
|
||||
as-yet-untitled <a href="https://coops.tech">Cotech</a> server hosting
|
||||
initiative, which you can <a
|
||||
href="https://community.coops.tech/t/call-for-input-v2-co-op-vps-survey/2802/9">read
|
||||
about on the Cotech forum</a>.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
What do you mean, "technical demo"?
|
||||
<p>No backups</p>
|
||||
<p>No service level agreement</p>
|
||||
<p>"Best effort" support</p>
|
||||
</li>
|
||||
<li>
|
||||
Where can I get this, but, more reliable?
|
||||
<p>Cyberia, the authors of this platform, run the canonical instance, <a
|
||||
href="https://capsul.org">Capsul.org</a>, on hardware they own. Please
|
||||
send them your money! (cash, crypto, or card accepted).</p>
|
||||
</li>
|
||||
<li>
|
||||
How do I use this system?
|
||||
<p>Please see <a href="https://capsul.org/faq">the official Capsul FAQ
|
||||
page</a>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block pagesource %}/templates/faq.html{% endblock %}
|
||||
|
28
capsulflask/theme/yolocolo/index.html
Normal file
28
capsulflask/theme/yolocolo/index.html
Normal file
@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>
|
||||
<pre>
|
||||
_ _
|
||||
_ _ ___ | | ___ ___ ___ | | ___
|
||||
| | | |/ _ \| |/ _ \ / __/ _ \| |/ _ \
|
||||
| |_| | (_) | | (_) | (_| (_) | | (_) |
|
||||
\__, |\___/|_|\___/ \___\___/|_|\___/
|
||||
|___/
|
||||
|
||||
</pre>
|
||||
<span>Co-operative hosting using <a href="https://cyberia.club">Cyberia</a>'s Capsul</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block subcontent %}
|
||||
<p>
|
||||
<ul>
|
||||
<li>Sign up for an account!</li>
|
||||
<li>Add some funds!</li>
|
||||
<li>Create a VPS!</li>
|
||||
<li>Give your feedback!</li>
|
||||
</ul>
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block pagesource %}/templates/index.html{% endblock %}
|
23
capsulflask/theme/yolocolo/pricing.html
Normal file
23
capsulflask/theme/yolocolo/pricing.html
Normal file
@ -0,0 +1,23 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Pricing{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row third-margin">
|
||||
<h1>CAPSUL TYPES & PRICING</h1>
|
||||
</div>
|
||||
<div class="row half-margin">
|
||||
<p>
|
||||
Rates for this service aren't set yet. You can see Cyberia's Capsul pricing
|
||||
on <a href="https://capsul.org/pricing">their website</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<pre>
|
||||
SUPPORTED OPERATING SYSTEMS:
|
||||
|
||||
{% for os_id, os in operating_systems.items() %} - {{ os.description }}
|
||||
{% endfor %}
|
||||
</pre>
|
||||
</div>
|
||||
{% endblock %}
|
21
capsulflask/theme/yolocolo/support.html
Normal file
21
capsulflask/theme/yolocolo/support.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Support{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row half-margin">
|
||||
<h1>SUPPORT</h1>
|
||||
</div>
|
||||
<div class="row half-margin">
|
||||
<a href="mailto:yolocolo@doesthisthing.work?subject=Please%20help!">yolocolo@doesthisthing.work</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block subcontent %}
|
||||
<p>
|
||||
You can also find us on Matrix: <a
|
||||
href="https://matrix.to/#/#untitled-hosting.public:autonomic.zone">#untitled-hosting.public:autonomic.zone</a>.
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block pagesource %}/templates/support.html{% endblock %}
|
36
docker-compose.yml
Normal file
36
docker-compose.yml
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: 3wordchant/capsul-flask:latest
|
||||
build: .
|
||||
volumes:
|
||||
- "./:/app/code"
|
||||
- "../tank:/tank"
|
||||
# - "/var/run/libvirt/libvirt-sock:/var/run/libvirt/libvirt-sock"
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- "POSTGRES_CONNECTION_PARAMETERS=host=db port=5432 user=capsul password=capsul dbname=capsul"
|
||||
- SPOKE_MODEL=shell-scripts
|
||||
#- FLASK_DEBUG=1
|
||||
- BASE_URL=http://localhost:5000
|
||||
- ADMIN_PANEL_ALLOW_EMAIL_ADDRESSES=3wc.capsul@doesthisthing.work
|
||||
- VIRSH_DEFAULT_CONNECT_URI=qemu:///system
|
||||
# The image uses gunicorn by default, let's override it with Flask's
|
||||
# built-in development server
|
||||
command: ["flask", "run", "-h", "0.0.0.0", "-p", "5000"]
|
||||
db:
|
||||
image: "postgres:9.6.5-alpine"
|
||||
volumes:
|
||||
- "postgres:/var/lib/postgresql/data"
|
||||
environment:
|
||||
POSTGRES_USER: capsul
|
||||
POSTGRES_PASSWORD: capsul
|
||||
POSTGRES_DB: capsul
|
||||
|
||||
volumes:
|
||||
postgres:
|
Reference in New Issue
Block a user