started working on cron job task
This commit is contained in:
parent
0dc58ed6a8
commit
d1078b8f30
@ -6,9 +6,12 @@ from datetime import datetime
|
|||||||
import click
|
import click
|
||||||
from flask.cli import with_appcontext
|
from flask.cli import with_appcontext
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
from flask import current_app
|
||||||
from psycopg2 import ProgrammingError
|
from psycopg2 import ProgrammingError
|
||||||
|
from flask_mail import Message
|
||||||
|
|
||||||
from capsulflask.db import get_model, my_exec_info_message
|
from capsulflask.db import get_model, my_exec_info_message
|
||||||
|
from capsulflask.console import get_account_balance
|
||||||
|
|
||||||
bp = Blueprint('cli', __name__)
|
bp = Blueprint('cli', __name__)
|
||||||
|
|
||||||
@ -44,7 +47,7 @@ def sql_script(f, c):
|
|||||||
for row in model.cursor.fetchall():
|
for row in model.cursor.fetchall():
|
||||||
def format_value(x):
|
def format_value(x):
|
||||||
if isinstance(x, bool):
|
if isinstance(x, bool):
|
||||||
return "TRUE" if x else "FALSE"
|
return "true" if x else "false"
|
||||||
if not x :
|
if not x :
|
||||||
return "null"
|
return "null"
|
||||||
if isinstance(x, datetime):
|
if isinstance(x, datetime):
|
||||||
@ -60,4 +63,115 @@ def sql_script(f, c):
|
|||||||
@bp.cli.command('cron-task')
|
@bp.cli.command('cron-task')
|
||||||
@with_appcontext
|
@with_appcontext
|
||||||
def cron_task():
|
def cron_task():
|
||||||
print('a')
|
|
||||||
|
# make sure btcpay payments get completed (in case we miss a webhook), otherwise invalidate the payment
|
||||||
|
|
||||||
|
unresolved_btcpay_invoices = get_model().get_unresolved_btcpay_invoices()
|
||||||
|
for invoice in unresolved_btcpay_invoices:
|
||||||
|
invoice_id = invoice.id
|
||||||
|
invoice = current_app.config['BTCPAY_CLIENT'].get_invoice(invoice_id)
|
||||||
|
days = float((datetime.now() - invoice.created).total_seconds())/float(60*60*24)
|
||||||
|
|
||||||
|
if invoice['status'] == "complete":
|
||||||
|
get_model().btcpay_invoice_resolved(invoice_id, True)
|
||||||
|
elif days >= 1:
|
||||||
|
get_model().btcpay_invoice_resolved(invoice_id, False)
|
||||||
|
|
||||||
|
# notify when funds run out
|
||||||
|
|
||||||
|
accounts = get_model().accounts_list()
|
||||||
|
for account in accounts:
|
||||||
|
vms = get_model().list_vms_for_account(account['email'])
|
||||||
|
payments = get_model().list_payments_for_account(account['email'])
|
||||||
|
balance_1w = get_account_balance(vms, payments, datetime.utcnow() + datetime.timedelta(days=7))
|
||||||
|
balance_1d = get_account_balance(vms, payments, datetime.utcnow() + datetime.timedelta(days=1))
|
||||||
|
balance_now = get_account_balance(vms, payments, datetime.utcnow())
|
||||||
|
current_warning = account['account_balance_warning']
|
||||||
|
|
||||||
|
delete_at_account_balance = -10
|
||||||
|
|
||||||
|
pluralize_capsul = "s" if len(vms) > 1 else ""
|
||||||
|
|
||||||
|
warnings = [
|
||||||
|
dict(
|
||||||
|
id='zero_1w',
|
||||||
|
active=balance_1w < 0,
|
||||||
|
subject="Capsul One Week Payment Reminder",
|
||||||
|
body=("According to our calculations, your Capsul account will run out of funds in one week.\n\n"
|
||||||
|
f"Log in now to re-fill your account! {current_app.config["BASE_URL"]}/console/account-balance\n\n"
|
||||||
|
"If you believe you have recieved this message in error, please let us know: support@cyberia.club")
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
id='zero_1d',
|
||||||
|
active=balance_1d < 0,
|
||||||
|
subject="Capsul One Day Payment Reminder",
|
||||||
|
body=("According to our calculations, your Capsul account will run out of funds tomorrow.\n\n"
|
||||||
|
f"Log in now to re-fill your account! {current_app.config["BASE_URL"]}/console/account-balance\n\n"
|
||||||
|
"If you believe you have recieved this message in error, please let us know: support@cyberia.club")
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
id='zero_now',
|
||||||
|
active=balance_now < 0,
|
||||||
|
subject="Your Capsul Account is No Longer Funded",
|
||||||
|
body=(f"You have run out of funds! You will no longer be able to create Capsul{pluralize_capsul}.\n\n"
|
||||||
|
f"As a courtesy, we'll let your existing Capsul{pluralize_capsul} keep running until your account "
|
||||||
|
"reaches a -$10 balance, at which point they will be deleted.\n\n"
|
||||||
|
f"Log in now to re-fill your account! {current_app.config["BASE_URL"]}/console/account-balance\n\n"
|
||||||
|
f"If you need help decomissioning your Capsul{pluralize_capsul}, "
|
||||||
|
"would like to request backups, or de-activate your account, please contact: support@cyberia.club")
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
id='delete_1w',
|
||||||
|
active=balance_1w < delete_at_account_balance,
|
||||||
|
subject=f"Your Capsul{pluralize_capsul} Will be Deleted In Less Than a Week",
|
||||||
|
body=("You have run out of funds and have not refilled your account.\n\n"
|
||||||
|
f"As a courtesy, we have let your existing Capsul{pluralize_capsul} keep running. "
|
||||||
|
f"However, your account will reach a -$10 balance some time next week and your Capsul{pluralize_capsul}""
|
||||||
|
"will be deleted.\n\n"
|
||||||
|
f"Log in now to re-fill your account! {current_app.config["BASE_URL"]}/console/account-balance\n\n"
|
||||||
|
f"If you need help decomissioning your Capsul{pluralize_capsul}, "
|
||||||
|
"would like to request backups, or de-activate your account, please contact: support@cyberia.club")
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
id='delete_1d',
|
||||||
|
active=balance_1d < delete_at_account_balance,
|
||||||
|
subject=f"Last Chance to Save your Capsul{pluralize_capsul}: Gone Tomorrow",
|
||||||
|
body=("You have run out of funds and have not refilled your account.\n\n"
|
||||||
|
f"As a courtesy, we have let your existing Capsul{pluralize_capsul} keep running. "
|
||||||
|
f"However, your account will reach a -$10 balance tomorrow and your Capsul{pluralize_capsul} will be deleted.\n\n"
|
||||||
|
f"Last chance to deposit funds now and keep your Capsul{pluralize_capsul} running! {current_app.config["BASE_URL"]}/console/account-balance")
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
id='delete_now',
|
||||||
|
active=balance_now < delete_at_account_balance,
|
||||||
|
subject=f"Capsul{pluralize_capsul} Deleted",
|
||||||
|
body=(f"Your account reached a -$10 balance and your Capsul{pluralize_capsul} were deleted.")
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
current_warning_index = -1
|
||||||
|
if current_warning:
|
||||||
|
for i in range(0, len(warnings)):
|
||||||
|
if warnings[i]['id'] == current_warning:
|
||||||
|
current_warning_index = i
|
||||||
|
|
||||||
|
index_to_send = -1
|
||||||
|
for i in range(0, len(warnings)):
|
||||||
|
if i > current_warning_index and warnings[i].active:
|
||||||
|
index_to_send = i
|
||||||
|
|
||||||
|
if index_to_send > -1:
|
||||||
|
current_app.config["FLASK_MAIL_INSTANCE"].send(
|
||||||
|
Message(
|
||||||
|
warnings[index_to_send]['subject'],
|
||||||
|
body=warnings[index_to_send]['body'],
|
||||||
|
recipients=[account['email']]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
get_model().set_account_balance_warning(account['email'], warnings[index_to_send]['id'])
|
||||||
|
if index_to_send == len(warnings)-1:
|
||||||
|
print('TODO: delete capsuls')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# make sure vm system and DB are synced
|
||||||
|
@ -91,7 +91,7 @@ def create():
|
|||||||
vm_sizes = get_model().vm_sizes_dict()
|
vm_sizes = get_model().vm_sizes_dict()
|
||||||
operating_systems = get_model().operating_systems_dict()
|
operating_systems = get_model().operating_systems_dict()
|
||||||
ssh_public_keys = get_model().list_ssh_public_keys_for_account(session["account"])
|
ssh_public_keys = get_model().list_ssh_public_keys_for_account(session["account"])
|
||||||
account_balance = get_account_balance()
|
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
|
created_os = None
|
||||||
@ -246,15 +246,15 @@ def get_payments():
|
|||||||
|
|
||||||
average_number_of_days_in_a_month = 30.44
|
average_number_of_days_in_a_month = 30.44
|
||||||
|
|
||||||
def get_account_balance():
|
def get_account_balance(vms, payments, as_of):
|
||||||
|
|
||||||
vm_cost_dollars = 0.0
|
vm_cost_dollars = 0.0
|
||||||
for vm in get_vms():
|
for vm in vms:
|
||||||
end_datetime = vm["deleted"] if vm["deleted"] else datetime.utcnow()
|
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 = ( 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"], get_payments())) )
|
payment_dollars_total = float( sum(map(lambda x: 0 if x["invalidated"] else x["dollars"], payments)) )
|
||||||
|
|
||||||
return payment_dollars_total - vm_cost_dollars
|
return payment_dollars_total - vm_cost_dollars
|
||||||
|
|
||||||
@ -262,7 +262,7 @@ def get_account_balance():
|
|||||||
@account_required
|
@account_required
|
||||||
def account_balance():
|
def account_balance():
|
||||||
payments = get_payments()
|
payments = get_payments()
|
||||||
account_balance = get_account_balance()
|
account_balance = get_account_balance(get_vms(), payments, datetime.utcnow())
|
||||||
|
|
||||||
vms_billed = list()
|
vms_billed = list()
|
||||||
|
|
||||||
|
@ -24,9 +24,9 @@ class DBModel:
|
|||||||
|
|
||||||
def consume_token(self, token):
|
def consume_token(self, token):
|
||||||
self.cursor.execute("SELECT email FROM login_tokens WHERE token = %s and created > (NOW() - INTERVAL '20 min')", (token, ))
|
self.cursor.execute("SELECT email FROM login_tokens WHERE token = %s and created > (NOW() - INTERVAL '20 min')", (token, ))
|
||||||
rows = self.cursor.fetchall()
|
row = self.cursor.fetchone()
|
||||||
if len(rows) > 0:
|
if row:
|
||||||
email = rows[0][0]
|
email = row[0]
|
||||||
self.cursor.execute("DELETE FROM login_tokens WHERE email = %s", (email, ))
|
self.cursor.execute("DELETE FROM login_tokens WHERE email = %s", (email, ))
|
||||||
self.connection.commit()
|
self.connection.commit()
|
||||||
return email
|
return email
|
||||||
@ -125,14 +125,13 @@ class DBModel:
|
|||||||
WHERE vms.email = %s AND vms.id = %s""",
|
WHERE vms.email = %s AND vms.id = %s""",
|
||||||
(email, id)
|
(email, id)
|
||||||
)
|
)
|
||||||
rows = self.cursor.fetchall()
|
row = self.cursor.fetchone()
|
||||||
if len(rows) == 0:
|
if not row:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
x = rows[0]
|
|
||||||
vm = dict(
|
vm = dict(
|
||||||
id=x[0], ipv4=x[1], ipv6=x[2], os_description=x[3], created=x[4], deleted=x[5],
|
id=row[0], ipv4=row[1], ipv6=row[2], os_description=row[3], created=row[4], deleted=row[5],
|
||||||
size=x[6], dollars_per_month=x[7], vcpus=x[8], memory_mb=x[9], bandwidth_gb_per_month=x[10]
|
size=row[6], dollars_per_month=row[7], vcpus=row[8], memory_mb=row[9], bandwidth_gb_per_month=row[10]
|
||||||
)
|
)
|
||||||
|
|
||||||
self.cursor.execute("""
|
self.cursor.execute("""
|
||||||
@ -166,23 +165,62 @@ class DBModel:
|
|||||||
|
|
||||||
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))
|
||||||
rows = self.cursor.fetchall()
|
row = self.cursor.fetchone()
|
||||||
if len(rows) > 0:
|
if row:
|
||||||
if int(rows[0][1]) != int(dollars):
|
if int(row[1]) != int(dollars):
|
||||||
print(f"""
|
print(f"""
|
||||||
{payment_type} gave us a completed payment session with a different dollar amount than what we had recorded!!
|
{payment_type} gave us a completed payment session with a different dollar amount than what we had recorded!!
|
||||||
id: {id}
|
id: {id}
|
||||||
account: {rows[0][0]}
|
account: {row[0]}
|
||||||
Recorded amount: {int(rows[0][1])}
|
Recorded amount: {int(row[1])}
|
||||||
{payment_type} sent: {int(dollars)}
|
{payment_type} sent: {int(dollars)}
|
||||||
""")
|
""")
|
||||||
# not sure what to do here. For now just log and do nothing.
|
# not sure what to do here. For now just log and do nothing.
|
||||||
self.cursor.execute( "DELETE FROM payment_sessions WHERE id = %s AND type = %s", (id, payment_type) )
|
self.cursor.execute( "DELETE FROM payment_sessions WHERE id = %s AND type = %s", (id, payment_type) )
|
||||||
self.cursor.execute( "INSERT INTO payments (email, dollars) VALUES (%s, %s)", (rows[0][0], rows[0][1]) )
|
self.cursor.execute( "INSERT INTO payments (email, dollars) VALUES (%s, %s) RETURNING id", (row[0], row[1]) )
|
||||||
|
|
||||||
|
if payment_type == "btcpay":
|
||||||
|
payment_id = self.cursor.fetchone()[0]
|
||||||
|
self.cursor.execute(
|
||||||
|
"INSERT INTO unresolved_btcpay_invoices (id, email, payment_id) VALUES (%s, %s, %s)",
|
||||||
|
(id, row[0], payment_id)
|
||||||
|
)
|
||||||
|
|
||||||
self.connection.commit()
|
self.connection.commit()
|
||||||
return rows[0][0]
|
return row[0]
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def btcpay_invoice_resolved(self, id, completed):
|
||||||
|
self.cursor.execute("SELECT email, payment_id FROM unresolved_btcpay_invoices WHERE id = %s ", (id,))
|
||||||
|
row = self.cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
self.cursor.execute( "DELETE FROM unresolved_btcpay_invoices WHERE id = %s", (id,) )
|
||||||
|
if not completed:
|
||||||
|
self.cursor.execute("UPDATE payments SET invalidated = True WHERE email = %s id = %s", (row[0], row[1]))
|
||||||
|
|
||||||
|
self.connection.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def get_unresolved_btcpay_invoices(self):
|
||||||
|
self.cursor.execute("SELECT id, payments.created, email FROM unresolved_btcpay_invoices JOIN payments on payment_id = payment.id")
|
||||||
|
return list(map(lambda row: dict(id=row[0], created=row[1], email=row[2]), self.cursor.fetchall()))
|
||||||
|
|
||||||
|
def get_account_balance_warning(self, email):
|
||||||
|
self.cursor.execute("SELECT account_balance_warning FROM accounts WHERE email = %s", (email,))
|
||||||
|
return self.cursor.fetchone()[0]
|
||||||
|
|
||||||
|
def set_account_balance_warning(self, email, account_balance_warning):
|
||||||
|
self.cursor.execute("UPDATE accounts SET account_balance_warning = %s WHERE email = %s", (account_balance_warning, email))
|
||||||
|
self.connection.commit()
|
||||||
|
|
||||||
|
def all_accounts(self):
|
||||||
|
self.cursor.execute("SELECT email, account_balance_warning FROM accounts")
|
||||||
|
return list(map(lambda row: dict(row=row[0], account_balance_warning=row[1]), self.cursor.fetchall()))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -55,12 +55,13 @@ def btcpay_payment():
|
|||||||
currency="USD",
|
currency="USD",
|
||||||
itemDesc="Capsul Cloud Compute",
|
itemDesc="Capsul Cloud Compute",
|
||||||
transactionSpeed="high",
|
transactionSpeed="high",
|
||||||
redirectURL=f"{current_app.config['BASE_URL']}/account-balance"
|
redirectURL=f"{current_app.config['BASE_URL']}/account-balance",
|
||||||
|
notificationURL=f"{current_app.config['BASE_URL']}/payment/btcpay/webhook"
|
||||||
))
|
))
|
||||||
# print(invoice)
|
# print(invoice)
|
||||||
invoice_id = invoice["id"]
|
invoice_id = invoice["id"]
|
||||||
|
|
||||||
print(f"created btcpay invoice_id={invoice_id} ( {session['account']}, ${request.form['dollars']} )")
|
print(f"created btcpay invoice_id={invoice_id} ( {session['account']}, ${dollars} )")
|
||||||
|
|
||||||
get_model().create_payment_session("btcpay", invoice_id, session["account"], dollars)
|
get_model().create_payment_session("btcpay", invoice_id, session["account"], dollars)
|
||||||
|
|
||||||
@ -72,6 +73,35 @@ 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 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!
|
||||||
|
invoice = current_app.config['BTCPAY_CLIENT'].get_invoice(invoice_id)
|
||||||
|
|
||||||
|
if invoice['currency'] != "USD":
|
||||||
|
abort(400, "invalid currency")
|
||||||
|
|
||||||
|
dollars = invoice['price']
|
||||||
|
|
||||||
|
if invoice['status'] == "paid" or invoice['status'] == "confirmed":
|
||||||
|
success_account = get_model().consume_payment_session("btcpay", invoice_id, dollars)
|
||||||
|
|
||||||
|
if success_account:
|
||||||
|
print(f"{success_account} paid ${dollars} successfully (btcpay_invoice_id={invoice_id})")
|
||||||
|
|
||||||
|
elif invoice['status'] == "complete":
|
||||||
|
get_model().btcpay_invoice_resolved(invoice_id, True)
|
||||||
|
elif invoice['status'] == "expired" or invoice['status'] == "invalid":
|
||||||
|
get_model().btcpay_invoice_resolved(invoice_id, False)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/stripe", methods=("GET", "POST"))
|
@bp.route("/stripe", methods=("GET", "POST"))
|
||||||
@account_required
|
@account_required
|
||||||
def stripe_payment():
|
def stripe_payment():
|
||||||
@ -142,7 +172,7 @@ def success():
|
|||||||
|
|
||||||
#consume_payment_session deletes the checkout session row and inserts a payment row
|
#consume_payment_session deletes the checkout session row and inserts a payment row
|
||||||
# its ok to call consume_payment_session more than once because it only takes an action if the session exists
|
# its ok to call consume_payment_session more than once because it only takes an action if the session exists
|
||||||
success_account = get_model().consume_payment_session(stripe_checkout_session_id, dollars)
|
success_account = get_model().consume_payment_session("stripe", stripe_checkout_session_id, dollars)
|
||||||
|
|
||||||
if success_account:
|
if success_account:
|
||||||
print(f"{success_account} paid ${dollars} successfully (stripe_checkout_session_id={stripe_checkout_session_id})")
|
print(f"{success_account} paid ${dollars} successfully (stripe_checkout_session_id={stripe_checkout_session_id})")
|
||||||
@ -168,7 +198,7 @@ def success():
|
|||||||
|
|
||||||
# #consume_payment_session deletes the checkout session row and inserts a payment row
|
# #consume_payment_session deletes the checkout session row and inserts a payment row
|
||||||
# # its ok to call consume_payment_session more than once because it only takes an action if the session exists
|
# # its ok to call consume_payment_session more than once because it only takes an action if the session exists
|
||||||
# success_account = get_model().consume_payment_session(stripe_checkout_session_id, dollars)
|
# success_account = get_model().consume_payment_session("stripe", stripe_checkout_session_id, dollars)
|
||||||
# if success_account:
|
# if success_account:
|
||||||
# print(f"{success_account} paid ${dollars} successfully (stripe_checkout_session_id={stripe_checkout_session_id})")
|
# print(f"{success_account} paid ${dollars} successfully (stripe_checkout_session_id={stripe_checkout_session_id})")
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
CREATE TABLE accounts (
|
CREATE TABLE accounts (
|
||||||
email TEXT PRIMARY KEY NOT NULL,
|
email TEXT PRIMARY KEY NOT NULL,
|
||||||
|
account_balance_warning TEXT NULL,
|
||||||
created TIMESTAMP NOT NULL DEFAULT NOW()
|
created TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -72,13 +73,14 @@ CREATE TABLE payment_sessions (
|
|||||||
dollars NUMERIC(8, 2) NOT NULL
|
dollars NUMERIC(8, 2) NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE unconfirmed_btcpay_invoices (
|
CREATE TABLE unresolved_btcpay_invoices (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
|
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
|
||||||
payment_id INTEGER NOT NULL,
|
payment_id INTEGER NOT NULL,
|
||||||
FOREIGN KEY (email, payment_id) REFERENCES payments(email, id) ON DELETE CASCADE
|
FOREIGN KEY (email, payment_id) REFERENCES payments(email, id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
INSERT INTO os_images (id, template_image_file_name, description)
|
INSERT INTO os_images (id, template_image_file_name, description)
|
||||||
VALUES ('alpine311', 'alpine-cloud-2020-04-18.qcow2', 'Alpine Linux 3.11'),
|
VALUES ('alpine311', 'alpine-cloud-2020-04-18.qcow2', 'Alpine Linux 3.11'),
|
||||||
('ubuntu18', 'ubuntu-18.04-minimal-cloudimg-amd64.img', 'Ubuntu 18.04 LTS (Bionic)'),
|
('ubuntu18', 'ubuntu-18.04-minimal-cloudimg-amd64.img', 'Ubuntu 18.04 LTS (Bionic)'),
|
||||||
|
Loading…
Reference in New Issue
Block a user