Merge branch 'master' of giit.cyberia.club:~forest/capsul-flask
This commit is contained in:
commit
8962345fd2
1
Pipfile
1
Pipfile
@ -28,6 +28,7 @@ stripe = "*"
|
||||
matplotlib = "*"
|
||||
requests = "*"
|
||||
python-dotenv = "*"
|
||||
ecdsa = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
|
10
Pipfile.lock
generated
10
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "4f61804f9405ff8f66e41739aeac24a2577f7b64f7d08b93459b568528c4f494"
|
||||
"sha256": "89f5b91fef8e0029cbecf7f83ce1fdc64a64f295161c8c63bfe5940e31d466e1"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -60,6 +60,14 @@
|
||||
],
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"ecdsa": {
|
||||
"hashes": [
|
||||
"sha256:867ec9cf6df0b03addc8ef66b56359643cb5d0c1dc329df76ba7ecfe256c8061",
|
||||
"sha256:8f12ac317f8a1318efa75757ef0a651abe12e51fc1af8838fb91079445227277"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.15"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
"sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060",
|
||||
|
@ -152,10 +152,8 @@ And you should see the token in the btcpay server UI:
|
||||
|
||||
Now simply set your `BTCPAY_PRIVATE_KEY` variable in `.env`
|
||||
|
||||
NOTE: make sure to use single quotes and replace the new lines with \n.
|
||||
|
||||
```
|
||||
BTCPAY_PRIVATE_KEY='-----BEGIN EC PRIVATE KEY-----
|
||||
EXAMPLEIArx/EXAMPLEKH23EXAMPLEsYXEXAMPLE5qdEXAMPLEcFHoAcEXAMPLEK
|
||||
oUQDQgAEnWs47PT8+ihhzyvXX6/yYMAWWODluRTR2Ix6ZY7Z+MV7v0W1maJzqeqq
|
||||
NQ+cpBvPDbyrDk9+Uf/sEaRCma094g==
|
||||
-----END EC PRIVATE KEY-----'
|
||||
BTCPAY_PRIVATE_KEY='-----BEGIN EC PRIVATE KEY-----\nEXAMPLEIArx/EXAMPLEKH23EXAMPLEsYXEXAMPLE5qdEXAMPLEcFHoAcEXAMPLEK\noUQDQgAEnWs47PT8+ihhzyvXX6/yYMAWWODluRTR2Ix6ZY7Z+MV7v0W1maJzqeqq\nNQ+cpBvPDbyrDk9+Uf/sEaRCma094g==\n-----END EC PRIVATE KEY-----'
|
||||
```
|
@ -71,6 +71,10 @@ def magiclink(token):
|
||||
session["account"] = email
|
||||
return redirect(url_for("console.index"))
|
||||
else:
|
||||
# this is here to prevent xss
|
||||
if token and not re.match(r"^[a-zA-Z0-9_-]+$", token):
|
||||
token = '___________'
|
||||
|
||||
abort(404, f"Token {token} doesn't exist or has already been used.")
|
||||
|
||||
@bp.route("/logout")
|
||||
|
@ -8,6 +8,8 @@ from flask import g
|
||||
from flask import request
|
||||
from flask import session
|
||||
from flask import render_template
|
||||
from flask import redirect
|
||||
from flask import url_for
|
||||
from flask_mail import Message
|
||||
from werkzeug.exceptions import abort
|
||||
from nanoid import generate
|
||||
@ -15,6 +17,7 @@ from nanoid import generate
|
||||
from capsulflask.metrics import durations as metric_durations
|
||||
from capsulflask.auth import account_required
|
||||
from capsulflask.db import get_model, my_exec_info_message
|
||||
from capsulflask.payment import poll_btcpay_session
|
||||
from capsulflask import cli
|
||||
|
||||
bp = Blueprint("console", __name__, url_prefix="/console")
|
||||
@ -41,6 +44,11 @@ def double_check_capsul_address(id, ipv4):
|
||||
@account_required
|
||||
def index():
|
||||
vms = get_vms()
|
||||
created = request.args.get('created')
|
||||
|
||||
# this is here to prevent xss
|
||||
if created and not re.match(r"^(cvm|capsul)-[a-z0-9]{10}$", created):
|
||||
created = '___________'
|
||||
|
||||
# for now we are going to check the IP according to the virt model
|
||||
# on every request. this could be done by a background job and cached later on...
|
||||
@ -59,7 +67,7 @@ def index():
|
||||
list(filter(lambda x: not x['deleted'], vms))
|
||||
))
|
||||
|
||||
return render_template("capsuls.html", vms=vms, has_vms=len(vms) > 0)
|
||||
return render_template("capsuls.html", vms=vms, has_vms=len(vms) > 0, created=created)
|
||||
|
||||
@bp.route("/<string:id>", methods=("GET", "POST"))
|
||||
@account_required
|
||||
@ -110,7 +118,6 @@ def create():
|
||||
account_balance = get_account_balance(get_vms(), get_payments(), datetime.utcnow())
|
||||
capacity_avaliable = current_app.config["VIRTUALIZATION_MODEL"].capacity_avaliable(512*1024*1024)
|
||||
errors = list()
|
||||
created_os = None
|
||||
|
||||
if request.method == "POST":
|
||||
|
||||
@ -171,7 +178,8 @@ def create():
|
||||
memory_mb=vm_sizes[size]['memory_mb'],
|
||||
ssh_public_keys=list(map(lambda x: x["content"], posted_keys))
|
||||
)
|
||||
created_os = os
|
||||
|
||||
return redirect(f"{url_for('console.index')}?created={id}")
|
||||
|
||||
affordable_vm_sizes = dict()
|
||||
for key, vm_size in vm_sizes.items():
|
||||
@ -186,7 +194,6 @@ def create():
|
||||
|
||||
return render_template(
|
||||
"create-capsul.html",
|
||||
created_os=created_os,
|
||||
capacity_avaliable=capacity_avaliable,
|
||||
account_balance=format(account_balance, '.2f'),
|
||||
ssh_public_keys=ssh_public_keys,
|
||||
@ -262,12 +269,18 @@ def get_payments():
|
||||
|
||||
average_number_of_days_in_a_month = 30.44
|
||||
|
||||
def get_vm_months_float(vm, as_of):
|
||||
end_datetime = vm["deleted"] if vm["deleted"] else as_of
|
||||
days = float((end_datetime - vm["created"]).total_seconds())/float(60*60*24)
|
||||
if days < 1:
|
||||
days = float(1)
|
||||
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:
|
||||
end_datetime = vm["deleted"] if vm["deleted"] else as_of
|
||||
vm_months = ( end_datetime - vm["created"] ).days / average_number_of_days_in_a_month
|
||||
vm_months = get_vm_months_float(vm, as_of)
|
||||
vm_cost_dollars += vm_months * float(vm["dollars_per_month"])
|
||||
|
||||
payment_dollars_total = float( sum(map(lambda x: 0 if x["invalidated"] else x["dollars"], payments)) )
|
||||
@ -277,6 +290,12 @@ 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':
|
||||
poll_btcpay_session(payment_session['id'])
|
||||
|
||||
payments = get_payments()
|
||||
vms = get_vms()
|
||||
balance_1w = get_account_balance(vms, payments, datetime.utcnow() + timedelta(days=7))
|
||||
@ -298,9 +317,7 @@ def account_balance():
|
||||
vms_billed = list()
|
||||
|
||||
for vm in get_vms():
|
||||
end_datetime = vm["deleted"] if vm["deleted"] else datetime.utcnow()
|
||||
|
||||
vm_months = (end_datetime - vm["created"]).days / average_number_of_days_in_a_month
|
||||
vm_months = get_vm_months_float(vm, datetime.utcnow())
|
||||
vms_billed.append(dict(
|
||||
id=vm["id"],
|
||||
dollars_per_month=vm["dollars_per_month"],
|
||||
|
@ -164,6 +164,17 @@ class DBModel:
|
||||
)
|
||||
self.connection.commit()
|
||||
|
||||
def list_payment_sessions_for_account(self, email):
|
||||
self.cursor.execute("""
|
||||
SELECT id, type, dollars, created
|
||||
FROM payment_sessions WHERE email = %s""",
|
||||
(email, )
|
||||
)
|
||||
return list(map(
|
||||
lambda x: dict(id=x[0], type=x[1], dollars=x[2], created=x[3]),
|
||||
self.cursor.fetchall()
|
||||
))
|
||||
|
||||
def consume_payment_session(self, payment_type, id, dollars):
|
||||
self.cursor.execute("SELECT email, dollars FROM payment_sessions WHERE id = %s AND type = %s", (id, payment_type))
|
||||
row = self.cursor.fetchone()
|
||||
|
@ -86,7 +86,7 @@ def get_plot_bytes(metric, capsulid, duration, size):
|
||||
# Prometheus queries to pull metrics for VMs
|
||||
metric_queries = dict(
|
||||
cpu=f"irate(libvirtd_domain_info_cpu_time_seconds_total{{domain='{capsulid}'}}[30s])",
|
||||
memory=f"libvirtd_domain_info_memory_usage_bytes{{domain='{capsulid}'}}",
|
||||
memory=f"libvirtd_domain_info_maximum_memory_bytes{{domain='{capsulid}'}}-libvirtd_domain_info_memory_unused_bytes{{domain='{capsulid}'}}",
|
||||
network_in=f"rate(libvirtd_domain_interface_stats_receive_bytes_total{{domain='{capsulid}'}}[{interval_seconds}s])",
|
||||
network_out=f"rate(libvirtd_domain_interface_stats_transmit_bytes_total{{domain='{capsulid}'}}[{interval_seconds}s])",
|
||||
disk=f"rate(libvirtd_domain_block_stats_read_bytes_total{{domain='{capsulid}'}}[{interval_seconds}s])%2Brate(libvirtd_domain_block_stats_write_bytes_total{{domain='{capsulid}'}}[{interval_seconds}s])",
|
||||
|
@ -59,6 +59,9 @@ def btcpay_payment():
|
||||
redirectURL=f"{current_app.config['BASE_URL']}/console/account-balance",
|
||||
notificationURL=f"{current_app.config['BASE_URL']}/payment/btcpay/webhook"
|
||||
))
|
||||
|
||||
current_app.logger.info(f"created btcpay invoice: {invoice}")
|
||||
|
||||
# print(invoice)
|
||||
invoice_id = invoice["id"]
|
||||
|
||||
@ -74,22 +77,17 @@ def btcpay_payment():
|
||||
return render_template("btcpay.html", invoice_id=invoice_id)
|
||||
|
||||
|
||||
@bp.route("/btcpay/webhook", methods=("POST",))
|
||||
def btcpay_webhook():
|
||||
|
||||
# IMPORTANT! there is no signature or credential for the data sent into this webhook :facepalm:
|
||||
# its just a notification, thats all.
|
||||
request_data = json.loads(request.data)
|
||||
invoice_id = request_data['id']
|
||||
|
||||
def poll_btcpay_session(invoice_id):
|
||||
# so you better make sure to get the invoice directly from the horses mouth!
|
||||
invoice = current_app.config['BTCPAY_CLIENT'].get_invoice(invoice_id)
|
||||
|
||||
if invoice['currency'] != "USD":
|
||||
abort(400, "invalid currency")
|
||||
return [400, "invalid currency"]
|
||||
|
||||
dollars = invoice['price']
|
||||
|
||||
current_app.logger.info(f"got btcpay webhook with invoice_id={invoice_id}, status={invoice['status']} dollars={dollars}")
|
||||
|
||||
if invoice['status'] == "paid" or invoice['status'] == "confirmed" or invoice['status'] == "complete":
|
||||
success_account = get_model().consume_payment_session("btcpay", invoice_id, dollars)
|
||||
|
||||
@ -101,7 +99,21 @@ def btcpay_webhook():
|
||||
elif invoice['status'] == "expired" or invoice['status'] == "invalid":
|
||||
get_model().btcpay_invoice_resolved(invoice_id, False)
|
||||
|
||||
return {"msg": "ok"}, 200
|
||||
return [200, "ok"]
|
||||
|
||||
@bp.route("/btcpay/webhook", methods=("POST",))
|
||||
def btcpay_webhook():
|
||||
|
||||
current_app.logger.info(f"got btcpay webhook")
|
||||
|
||||
# IMPORTANT! there is no signature or credential for the data sent into this webhook :facepalm:
|
||||
# its just a notification, thats all.
|
||||
request_data = json.loads(request.data)
|
||||
invoice_id = request_data['id']
|
||||
|
||||
result = poll_btcpay_session(invoice_id)
|
||||
|
||||
abort(result[0], result[1])
|
||||
|
||||
|
||||
|
||||
|
@ -1,3 +1,7 @@
|
||||
DROP TABLE unresolved_btcpay_invoices;
|
||||
|
||||
DROP TABLE payment_sessions;
|
||||
|
||||
DROP TABLE payments;
|
||||
|
||||
DROP TABLE login_tokens;
|
||||
|
@ -98,14 +98,4 @@ VALUES ('f1-s', 5.33, 512, 1, 500),
|
||||
('f1-xx', 29.66, 8192, 4, 8000),
|
||||
('f1-xxx', 57.58, 16384, 8, 16000);
|
||||
|
||||
-- this is test data to be removed later
|
||||
INSERT INTO accounts (email)
|
||||
VALUES ('forest.n.johnson@gmail.com');
|
||||
|
||||
INSERT INTO payments (email, dollars, created)
|
||||
VALUES ('forest.n.johnson@gmail.com', 20.00, TO_TIMESTAMP('2020-04-05','YYYY-MM-DD'));
|
||||
|
||||
INSERT INTO vms (id, email, os, size, created)
|
||||
VALUES ('capsul-yi9ffqbjly', 'forest.n.johnson@gmail.com', 'alpine311', 'f1-xx', TO_TIMESTAMP('2020-04-19','YYYY-MM-DD'));
|
||||
|
||||
UPDATE schemaversion SET version = 2;
|
@ -45,6 +45,15 @@ a:hover, a:active, a:visited {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.flash.green {
|
||||
color: rgb(8, 173, 137);
|
||||
border-color: rgb(8, 173, 137);
|
||||
}
|
||||
|
||||
.display-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
font-size:calc(0.40rem + 1vmin);
|
||||
margin: initial;
|
||||
@ -63,6 +72,8 @@ main {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.full-margin {
|
||||
width: 100%;
|
||||
margin: 3rem 0;
|
||||
|
@ -39,6 +39,7 @@
|
||||
{% for message in get_flashed_messages() %}
|
||||
<div class="flash">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% block custom_flash %}{% endblock %}
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
@ -2,6 +2,12 @@
|
||||
|
||||
{% 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>
|
||||
|
@ -19,6 +19,7 @@
|
||||
<li>2020-04-17: OpenBSD support added</li>
|
||||
<li>2020-04-26: Support link added</li>
|
||||
<li>2020-05-04: Simplified payment page</li>
|
||||
<li>2020-05-16: Beta version of new Capsul web application</li>
|
||||
</ul>
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
@ -6,21 +6,10 @@
|
||||
<div class="row third-margin">
|
||||
<h1>Create Capsul</h1>
|
||||
</div>
|
||||
<div class="row third-margin"><div>
|
||||
|
||||
{% if created_os %}
|
||||
<p>
|
||||
Your Capsul was successfully created! You should already see it listed on the
|
||||
<a href="/console/">Capsuls page</a>, but it may not have obtained an IP address yet.
|
||||
Its IP address should become visible once the machine has booted and taken a DHCP lease.
|
||||
</p>
|
||||
{% if created_os == 'debian10' %}
|
||||
<p>
|
||||
Note: because Debian delays fully booting until after entropy has been generated, Debian Capsuls
|
||||
may take an extra-long time to obtain an IP address, like up to 10 minutes. Be patient.
|
||||
</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="row third-margin">
|
||||
<div>
|
||||
|
||||
|
||||
{% if cant_afford %}
|
||||
<p>
|
||||
Your account does not have sufficient funds to create a Capsul.
|
||||
@ -36,7 +25,7 @@
|
||||
<pre>
|
||||
CAPSUL SIZES
|
||||
============
|
||||
type monthly cpus mem ssd net*
|
||||
type monthly* cpus mem ssd net*
|
||||
----- ------- ---- --- --- ---
|
||||
f1-s $5.33 1 512M 25G .5TB
|
||||
f1-m $7.16 1 1024M 25G 1TB
|
||||
@ -46,6 +35,7 @@
|
||||
f1-xxx $57.58 8 16G 25G 16TB
|
||||
|
||||
* 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 addr</pre>
|
||||
<pre>
|
||||
Your <a href="/console/account-balance">account balance</a>: ${{ account_balance }}
|
||||
@ -79,14 +69,24 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-end">
|
||||
<input type="submit" value="Create">
|
||||
<input id="submit-button" type="submit" value="Create">
|
||||
<span id="submit-button-clicked" class="display-none">..Creating...</span>
|
||||
</div>
|
||||
<script>
|
||||
window.addEventListener('DOMContentLoaded', function(event) {
|
||||
var submitButton = document.getElementById('submit-button');
|
||||
var submitButtonClicked = document.getElementById('submit-button-clicked');
|
||||
document.getElementById('submit-button').onclick = function() {
|
||||
submitButton.className = "display-none";
|
||||
submitButtonClicked.className = "waiting-pulse";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block subcontent %}
|
||||
|
Loading…
Reference in New Issue
Block a user