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 = "*"
|
matplotlib = "*"
|
||||||
requests = "*"
|
requests = "*"
|
||||||
python-dotenv = "*"
|
python-dotenv = "*"
|
||||||
|
ecdsa = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
|
|
||||||
|
10
Pipfile.lock
generated
10
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "4f61804f9405ff8f66e41739aeac24a2577f7b64f7d08b93459b568528c4f494"
|
"sha256": "89f5b91fef8e0029cbecf7f83ce1fdc64a64f295161c8c63bfe5940e31d466e1"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -60,6 +60,14 @@
|
|||||||
],
|
],
|
||||||
"version": "==0.10.0"
|
"version": "==0.10.0"
|
||||||
},
|
},
|
||||||
|
"ecdsa": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:867ec9cf6df0b03addc8ef66b56359643cb5d0c1dc329df76ba7ecfe256c8061",
|
||||||
|
"sha256:8f12ac317f8a1318efa75757ef0a651abe12e51fc1af8838fb91079445227277"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.15"
|
||||||
|
},
|
||||||
"flask": {
|
"flask": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060",
|
"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`
|
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-----
|
BTCPAY_PRIVATE_KEY='-----BEGIN EC PRIVATE KEY-----\nEXAMPLEIArx/EXAMPLEKH23EXAMPLEsYXEXAMPLE5qdEXAMPLEcFHoAcEXAMPLEK\noUQDQgAEnWs47PT8+ihhzyvXX6/yYMAWWODluRTR2Ix6ZY7Z+MV7v0W1maJzqeqq\nNQ+cpBvPDbyrDk9+Uf/sEaRCma094g==\n-----END EC PRIVATE KEY-----'
|
||||||
EXAMPLEIArx/EXAMPLEKH23EXAMPLEsYXEXAMPLE5qdEXAMPLEcFHoAcEXAMPLEK
|
|
||||||
oUQDQgAEnWs47PT8+ihhzyvXX6/yYMAWWODluRTR2Ix6ZY7Z+MV7v0W1maJzqeqq
|
|
||||||
NQ+cpBvPDbyrDk9+Uf/sEaRCma094g==
|
|
||||||
-----END EC PRIVATE KEY-----'
|
|
||||||
```
|
```
|
@ -71,6 +71,10 @@ def magiclink(token):
|
|||||||
session["account"] = email
|
session["account"] = email
|
||||||
return redirect(url_for("console.index"))
|
return redirect(url_for("console.index"))
|
||||||
else:
|
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.")
|
abort(404, f"Token {token} doesn't exist or has already been used.")
|
||||||
|
|
||||||
@bp.route("/logout")
|
@bp.route("/logout")
|
||||||
|
@ -8,6 +8,8 @@ from flask import g
|
|||||||
from flask import request
|
from flask import request
|
||||||
from flask import session
|
from flask import session
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
|
from flask import redirect
|
||||||
|
from flask import url_for
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
from werkzeug.exceptions import abort
|
from werkzeug.exceptions import abort
|
||||||
from nanoid import generate
|
from nanoid import generate
|
||||||
@ -15,6 +17,7 @@ from nanoid import generate
|
|||||||
from capsulflask.metrics import durations as metric_durations
|
from capsulflask.metrics import durations as metric_durations
|
||||||
from capsulflask.auth import account_required
|
from capsulflask.auth import account_required
|
||||||
from capsulflask.db import get_model, my_exec_info_message
|
from capsulflask.db import get_model, my_exec_info_message
|
||||||
|
from capsulflask.payment import poll_btcpay_session
|
||||||
from capsulflask import cli
|
from capsulflask import cli
|
||||||
|
|
||||||
bp = Blueprint("console", __name__, url_prefix="/console")
|
bp = Blueprint("console", __name__, url_prefix="/console")
|
||||||
@ -41,6 +44,11 @@ def double_check_capsul_address(id, ipv4):
|
|||||||
@account_required
|
@account_required
|
||||||
def index():
|
def index():
|
||||||
vms = get_vms()
|
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
|
# 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...
|
# 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))
|
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"))
|
@bp.route("/<string:id>", methods=("GET", "POST"))
|
||||||
@account_required
|
@account_required
|
||||||
@ -110,7 +118,6 @@ def create():
|
|||||||
account_balance = get_account_balance(get_vms(), get_payments(), datetime.utcnow())
|
account_balance = get_account_balance(get_vms(), get_payments(), datetime.utcnow())
|
||||||
capacity_avaliable = current_app.config["VIRTUALIZATION_MODEL"].capacity_avaliable(512*1024*1024)
|
capacity_avaliable = current_app.config["VIRTUALIZATION_MODEL"].capacity_avaliable(512*1024*1024)
|
||||||
errors = list()
|
errors = list()
|
||||||
created_os = None
|
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
|
||||||
@ -171,7 +178,8 @@ def create():
|
|||||||
memory_mb=vm_sizes[size]['memory_mb'],
|
memory_mb=vm_sizes[size]['memory_mb'],
|
||||||
ssh_public_keys=list(map(lambda x: x["content"], posted_keys))
|
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()
|
affordable_vm_sizes = dict()
|
||||||
for key, vm_size in vm_sizes.items():
|
for key, vm_size in vm_sizes.items():
|
||||||
@ -186,7 +194,6 @@ def create():
|
|||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"create-capsul.html",
|
"create-capsul.html",
|
||||||
created_os=created_os,
|
|
||||||
capacity_avaliable=capacity_avaliable,
|
capacity_avaliable=capacity_avaliable,
|
||||||
account_balance=format(account_balance, '.2f'),
|
account_balance=format(account_balance, '.2f'),
|
||||||
ssh_public_keys=ssh_public_keys,
|
ssh_public_keys=ssh_public_keys,
|
||||||
@ -262,12 +269,18 @@ def get_payments():
|
|||||||
|
|
||||||
average_number_of_days_in_a_month = 30.44
|
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):
|
def get_account_balance(vms, payments, as_of):
|
||||||
|
|
||||||
vm_cost_dollars = 0.0
|
vm_cost_dollars = 0.0
|
||||||
for vm in vms:
|
for vm in vms:
|
||||||
end_datetime = vm["deleted"] if vm["deleted"] else as_of
|
vm_months = get_vm_months_float(vm, as_of)
|
||||||
vm_months = ( end_datetime - vm["created"] ).days / average_number_of_days_in_a_month
|
|
||||||
vm_cost_dollars += vm_months * float(vm["dollars_per_month"])
|
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)) )
|
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")
|
@bp.route("/account-balance")
|
||||||
@account_required
|
@account_required
|
||||||
def account_balance():
|
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()
|
payments = get_payments()
|
||||||
vms = get_vms()
|
vms = get_vms()
|
||||||
balance_1w = get_account_balance(vms, payments, datetime.utcnow() + timedelta(days=7))
|
balance_1w = get_account_balance(vms, payments, datetime.utcnow() + timedelta(days=7))
|
||||||
@ -298,9 +317,7 @@ def account_balance():
|
|||||||
vms_billed = list()
|
vms_billed = list()
|
||||||
|
|
||||||
for vm in get_vms():
|
for vm in get_vms():
|
||||||
end_datetime = vm["deleted"] if vm["deleted"] else datetime.utcnow()
|
vm_months = get_vm_months_float(vm, datetime.utcnow())
|
||||||
|
|
||||||
vm_months = (end_datetime - vm["created"]).days / average_number_of_days_in_a_month
|
|
||||||
vms_billed.append(dict(
|
vms_billed.append(dict(
|
||||||
id=vm["id"],
|
id=vm["id"],
|
||||||
dollars_per_month=vm["dollars_per_month"],
|
dollars_per_month=vm["dollars_per_month"],
|
||||||
|
@ -164,6 +164,17 @@ class DBModel:
|
|||||||
)
|
)
|
||||||
self.connection.commit()
|
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):
|
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))
|
self.cursor.execute("SELECT email, dollars FROM payment_sessions WHERE id = %s AND type = %s", (id, payment_type))
|
||||||
row = self.cursor.fetchone()
|
row = self.cursor.fetchone()
|
||||||
|
@ -86,7 +86,7 @@ def get_plot_bytes(metric, capsulid, duration, size):
|
|||||||
# Prometheus queries to pull metrics for VMs
|
# Prometheus queries to pull metrics for VMs
|
||||||
metric_queries = dict(
|
metric_queries = dict(
|
||||||
cpu=f"irate(libvirtd_domain_info_cpu_time_seconds_total{{domain='{capsulid}'}}[30s])",
|
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_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])",
|
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])",
|
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",
|
redirectURL=f"{current_app.config['BASE_URL']}/console/account-balance",
|
||||||
notificationURL=f"{current_app.config['BASE_URL']}/payment/btcpay/webhook"
|
notificationURL=f"{current_app.config['BASE_URL']}/payment/btcpay/webhook"
|
||||||
))
|
))
|
||||||
|
|
||||||
|
current_app.logger.info(f"created btcpay invoice: {invoice}")
|
||||||
|
|
||||||
# print(invoice)
|
# print(invoice)
|
||||||
invoice_id = invoice["id"]
|
invoice_id = invoice["id"]
|
||||||
|
|
||||||
@ -74,22 +77,17 @@ def btcpay_payment():
|
|||||||
return render_template("btcpay.html", invoice_id=invoice_id)
|
return render_template("btcpay.html", invoice_id=invoice_id)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/btcpay/webhook", methods=("POST",))
|
def poll_btcpay_session(invoice_id):
|
||||||
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']
|
|
||||||
|
|
||||||
# so you better make sure to get the invoice directly from the horses mouth!
|
# so you better make sure to get the invoice directly from the horses mouth!
|
||||||
invoice = current_app.config['BTCPAY_CLIENT'].get_invoice(invoice_id)
|
invoice = current_app.config['BTCPAY_CLIENT'].get_invoice(invoice_id)
|
||||||
|
|
||||||
if invoice['currency'] != "USD":
|
if invoice['currency'] != "USD":
|
||||||
abort(400, "invalid currency")
|
return [400, "invalid currency"]
|
||||||
|
|
||||||
dollars = invoice['price']
|
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":
|
if invoice['status'] == "paid" or invoice['status'] == "confirmed" or invoice['status'] == "complete":
|
||||||
success_account = get_model().consume_payment_session("btcpay", invoice_id, dollars)
|
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":
|
elif invoice['status'] == "expired" or invoice['status'] == "invalid":
|
||||||
get_model().btcpay_invoice_resolved(invoice_id, False)
|
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 payments;
|
||||||
|
|
||||||
DROP TABLE login_tokens;
|
DROP TABLE login_tokens;
|
||||||
|
@ -98,14 +98,4 @@ VALUES ('f1-s', 5.33, 512, 1, 500),
|
|||||||
('f1-xx', 29.66, 8192, 4, 8000),
|
('f1-xx', 29.66, 8192, 4, 8000),
|
||||||
('f1-xxx', 57.58, 16384, 8, 16000);
|
('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;
|
UPDATE schemaversion SET version = 2;
|
@ -45,6 +45,15 @@ a:hover, a:active, a:visited {
|
|||||||
padding: 1em;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flash.green {
|
||||||
|
color: rgb(8, 173, 137);
|
||||||
|
border-color: rgb(8, 173, 137);
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-none {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5 {
|
h1, h2, h3, h4, h5 {
|
||||||
font-size:calc(0.40rem + 1vmin);
|
font-size:calc(0.40rem + 1vmin);
|
||||||
margin: initial;
|
margin: initial;
|
||||||
@ -63,6 +72,8 @@ main {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.full-margin {
|
.full-margin {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 3rem 0;
|
margin: 3rem 0;
|
||||||
|
@ -39,6 +39,7 @@
|
|||||||
{% for message in get_flashed_messages() %}
|
{% for message in get_flashed_messages() %}
|
||||||
<div class="flash">{{ message }}</div>
|
<div class="flash">{{ message }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% block custom_flash %}{% endblock %}
|
||||||
<main>
|
<main>
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
{% block title %}Capsuls{% endblock %}
|
{% block title %}Capsuls{% endblock %}
|
||||||
|
|
||||||
|
{% block custom_flash %}
|
||||||
|
{% if created %}
|
||||||
|
<div class="flash green">{{ created }} successfully created!</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row third-margin">
|
<div class="row third-margin">
|
||||||
<h1>Capsuls</h1>
|
<h1>Capsuls</h1>
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
<li>2020-04-17: OpenBSD support added</li>
|
<li>2020-04-17: OpenBSD support added</li>
|
||||||
<li>2020-04-26: Support link added</li>
|
<li>2020-04-26: Support link added</li>
|
||||||
<li>2020-05-04: Simplified payment page</li>
|
<li>2020-05-04: Simplified payment page</li>
|
||||||
|
<li>2020-05-16: Beta version of new Capsul web application</li>
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -6,21 +6,10 @@
|
|||||||
<div class="row third-margin">
|
<div class="row third-margin">
|
||||||
<h1>Create Capsul</h1>
|
<h1>Create Capsul</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="row third-margin"><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 %}
|
|
||||||
{% if cant_afford %}
|
{% if cant_afford %}
|
||||||
<p>
|
<p>
|
||||||
Your account does not have sufficient funds to create a Capsul.
|
Your account does not have sufficient funds to create a Capsul.
|
||||||
@ -36,7 +25,7 @@
|
|||||||
<pre>
|
<pre>
|
||||||
CAPSUL SIZES
|
CAPSUL SIZES
|
||||||
============
|
============
|
||||||
type monthly cpus mem ssd net*
|
type monthly* cpus mem ssd net*
|
||||||
----- ------- ---- --- --- ---
|
----- ------- ---- --- --- ---
|
||||||
f1-s $5.33 1 512M 25G .5TB
|
f1-s $5.33 1 512M 25G .5TB
|
||||||
f1-m $7.16 1 1024M 25G 1TB
|
f1-m $7.16 1 1024M 25G 1TB
|
||||||
@ -46,6 +35,7 @@
|
|||||||
f1-xxx $57.58 8 16G 25G 16TB
|
f1-xxx $57.58 8 16G 25G 16TB
|
||||||
|
|
||||||
* net is calculated as a per-month average
|
* 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>
|
* all VMs come standard with one public IPv4 addr</pre>
|
||||||
<pre>
|
<pre>
|
||||||
Your <a href="/console/account-balance">account balance</a>: ${{ account_balance }}
|
Your <a href="/console/account-balance">account balance</a>: ${{ account_balance }}
|
||||||
@ -79,14 +69,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row justify-end">
|
<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>
|
</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>
|
</form>
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endif %}
|
</div>
|
||||||
</div></div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block subcontent %}
|
{% block subcontent %}
|
||||||
|
Loading…
Reference in New Issue
Block a user