starting to build console controller and views

This commit is contained in:
forest 2020-05-11 01:47:14 -05:00
parent 7932db90d5
commit 18e6a1b141
18 changed files with 345 additions and 89 deletions

View File

@ -1,8 +1,10 @@
import os
from flask import Flask
from flask_mail import Mail
from flask import render_template
import os
from capsulflask import virt_model
def create_app():
app = Flask(__name__)
@ -21,6 +23,7 @@ def create_app():
)
app.config['FLASK_MAIL_INSTANCE'] = Mail(app)
app.config['VIRTUALIZATION_MODEL'] = virt_model.MockVirtualization()
from capsulflask import db
@ -29,8 +32,8 @@ def create_app():
from capsulflask import auth, landing, console
app.register_blueprint(landing.bp)
app.register_blueprint(auth.bp)
app.register_blueprint(console.bp)
app.add_url_rule("/", endpoint="index")

View File

@ -71,11 +71,11 @@ def login():
@bp.route("/magic/<string:token>", methods=("GET", ))
def magiclink(token):
email = get_model().consumeToken(token)
email = get_model().consume_token(token)
if email is not None:
session.clear()
session["account"] = email
return redirect(url_for("index"))
return redirect(url_for("console.index"))
else:
abort(404, f"Token {token} doesn't exist or has already been used.")

View File

@ -9,8 +9,69 @@ from flask import session
from flask import render_template
from flask_mail import Message
from werkzeug.exceptions import abort
from nanoid import generate
from capsulflask.auth import account_required
from capsulflask.db import get_model
bp = Blueprint("console", __name__, url_prefix="/console")
def makeCapsulId():
lettersAndNumbers = generate(alphabet="1234567890qwertyuiopasdfghjklzxcvbnm", size=10)
return f"capsul-{lettersAndNumbers}"
@bp.route("/")
@account_required
def index():
return render_template("console.html", vms=get_model().list_vms_for_account(session["account"]))
@bp.route("/create", methods=("GET", "POST"))
@account_required
def create():
db_model = get_model()
vm_sizes = db_model.vm_sizes_dict()
operating_systems = db_model.operating_systems_dict()
ssh_public_keys = db_model.list_ssh_public_keys_for_account(session["account"])
error = None
if request.method == "POST":
size = request.form["size"]
os = request.form["os"]
if not size:
error = "Size is required"
elif size not in vm_sizes:
error = f"Invalid size {size}"
if not os:
error = "OS is required"
elif os not in operating_systems:
error = f"Invalid os {os}"
if error is None:
id = makeCapsulId()
db_model.create_vm(
email=session["account"],
id=id,
size=size,
os=os
)
current_app.config["VIRTUALIZATION_MODEL"].create(
email = session["account"],
id=id,
template_image_file_name=operating_systems[os].template_image_file_name,
vcpus=vm_sizes[size].vcpus,
memory=vm_sizes[size].memory
)
return render_template(
"create.html",
ssh_public_keys=ssh_public_keys,
operating_systems=operating_systems,
vm_sizes=vm_sizes
)
@bp.route("/billing")
@account_required
def faq():
return render_template("billing.html")

View File

@ -22,7 +22,7 @@ class DBModel:
return token
def consumeToken(self, token):
def consume_token(self, token):
self.cursor.execute("SELECT email FROM login_tokens WHERE token = %s", (token, ))
rows = self.cursor.fetchall()
if len(rows) > 0:
@ -32,7 +32,52 @@ class DBModel:
return email
return None
def allVmIds(self,):
def all_vm_ids(self,):
self.cursor.execute("SELECT id FROM vms")
return map(lambda x: x[0], self.cursor.fetchall())
def operating_systems_dict(self,):
self.cursor.execute("SELECT id, template_image_file_name, description FROM os_images")
operatingSystems = dict()
for row in self.cursor.fetchall():
operatingSystems[row[0]] = dict(template_image_file_name=row[1], description=row[2])
return operatingSystems
def vm_sizes_dict(self,):
self.cursor.execute("SELECT id, dollars_per_month, vcpus, memory_mb, bandwidth_gb_per_month FROM vm_sizes")
vmSizes = dict()
for row in self.cursor.fetchall():
vmSizes[row[0]] = dict(dollars_per_month=row[1], vcpus=row[2], memory_mb=row[3], bandwidth_gb_per_month=row[4])
return vmSizes
def list_ssh_public_keys_for_account(self, email):
self.cursor.execute("SELECT name, content FROM ssh_public_keys WHERE email = %s", (email, ))
return map(
lambda x: dict(name=x[0], content=x[1]),
self.cursor.fetchall()
)
def list_vms_for_account(self, email):
self.cursor.execute("""
SELECT vms.id, vms.last_seen_ipv4, vms.last_seen_ipv6, vms.size, os_images.description, vms.created, vms.deleted
FROM vms JOIN os_images on os_images.id = vms.os
WHERE vms.email = %s""",
(email, )
)
return map(
lambda x: dict(id=x[0], ipv4=x[1], ipv6=x[2], size=x[3], os=x[4], created=x[5], deleted=x[6]),
self.cursor.fetchall()
)
def create_vm(self, email, id, size, os):
self.cursor.execute("""
INSERT INTO vms (email, id, size, os)
VALUES (%s, %s, %s, %s)
""",
(email, id, size, os)
)
self.connection.commit()

View File

@ -20,11 +20,10 @@ CREATE TABLE vm_sizes (
);
CREATE TABLE ssh_public_keys (
id SERIAL PRIMARY KEY,
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
name TEXT NOT NULL,
content TEXT NOT NULL,
UNIQUE (id, email)
PRIMARY KEY (email, name)
);
CREATE TABLE vms (
@ -40,12 +39,12 @@ CREATE TABLE vms (
);
CREATE TABLE vm_ssh_public_key (
ssh_public_key_id INTEGER NOT NULL,
ssh_public_key_name TEXT NOT NULL,
email TEXT NOT NULL,
vm_id TEXT NOT NULL,
FOREIGN KEY (email, ssh_public_key_id) REFERENCES ssh_public_keys(email, id) ON DELETE RESTRICT,
FOREIGN KEY (email, ssh_public_key_name) REFERENCES ssh_public_keys(email, name) ON DELETE RESTRICT,
FOREIGN KEY (email, vm_id) REFERENCES vms(email, id) ON DELETE RESTRICT,
PRIMARY KEY (email, vm_id, ssh_public_key_id)
PRIMARY KEY (email, vm_id, ssh_public_key_name)
);
CREATE TABLE payments (
@ -63,11 +62,11 @@ CREATE TABLE login_tokens (
);
INSERT INTO os_images (id, template_image_file_name, description)
VALUES ('debian10', 'debian-10-genericcloud-amd64-20191117-80.qcow2', 'Debian 10 (Buster)'),
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 Beaver)'),
('debian10', 'debian-10-genericcloud-amd64-20191117-80.qcow2', 'Debian 10 (Buster)'),
('centos7', 'CentOS-7-x86_64-GenericCloud.qcow2', 'CentOS 7'),
('centos8', 'CentOS-8-GenericCloud-8.1.1911-20200113.3.x86_64.qcow2', 'CentOS 8'),
('ubuntu18', 'ubuntu-18.04-minimal-cloudimg-amd64.img', 'Ubuntu 18.04 LTS (Bionic Beaver)'),
('alpine311', 'alpine-cloud-2020-04-18.qcow2', 'Alpine Linux 3.11'),
('openbsd66', 'openbsd-cloud-2020-05.qcow2', 'OpenBSD 6.6'),
('guix110', 'guixsystem-cloud-2020-05.qcow2', 'Guix System 1.1.0');

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

View File

@ -26,15 +26,14 @@ a:hover, a:active, a:visited {
color: #b5bd68;
}
nav .nav-row {
.nav-row {
display: flex;
justify-content: space-between;
margin: 2rem 0;
}
nav .nav-row:last-child {
.nav-row:last-child {
justify-content: center;
}
nav .nav-row:last-child a, nav .nav-row:last-child div {
.nav-row:last-child a, .nav-row:last-child div {
margin: 0 1em;
}
@ -52,37 +51,77 @@ main {
border: 1px dashed #bdc7b8;
padding: 1rem;
margin-bottom: 2em;
}
.hero {
display: flex;
flex-direction: column;
align-items: center;
}
.single-content {
margin: 2rem 0;
.full-margin {
margin: 3rem 0;
}
.half-margin {
margin: 1.5rem 0;
}
.third-margin {
margin: 1rem 0;
}
.row {
display: flex;
align-items: center;
justify-content: space-around;
width: 100%;
}
.row.wrap {
flex-wrap: wrap;
}
.row.justify-start {
justify-content: flex-start;
}
.row.justify-end {
justify-content: flex-end;
}
form {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-around;
flex-direction: column;
align-items: flex-start;
}
input, label {
label.align {
min-width: 10em;
}
input, select, label {
margin: 0.5em;
}
input {
background: none;
input, select {
outline: 0;
padding: 0.25em 0.5em;
border-radius: 0.5em;
color: #bdc7b8;
}
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
/*
re-generate the following line from the source image with:
echo "background-image: url(data:image/png;base64,$(cat capsulflask/static/dropdown-handle.png | base64 -w 0));"
*/
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA9hAAAPYQGoP6dpAAACfElEQVRYw+2WSWgVQRCGp3tmHmLEuKEEL3owguJBjBcD8eJJiCABT4IrOQiKC6jnoJ6CYBDiQkTxiQiC4nKIntR4E70JUQQ9KahIUAg6m1/FntBvXpYZE6PCFPxUTc90/dW19HuOU0op/4K8GnzcOMN8s8BCsbVZO8hCO1AzQN6EugJa7QCWguvgMB/4f5B8DeqO73vb0JEdwBetVYPnud3Yl0yU003egep3XbclCEInG8C3OE6cMIwc3/e383yXDWuniViDI5J2rXVTFEXpq9gO4Gu6GgSB43neOsyHbNwFpkK+AHWeU3dD3hDHsf06sQO4DZ6lUYVh6CilpEvPgTNpxxYgVmA15i3KuldObZGL8QQ8Hw2geWXbW9RWMECkv8JLEgmiQvQHeLyGw+YCMWwC98hkm5Q1Fdcd8d0POuD8LA8qE/kic+otYHQafM9zgjB8jXkIPGBzMN58o/aAExxkXiblP8ANsJ/9Q+mitr/gxSeUNOHVNBMjfUFJOM0KzJviACJvDPI5QgzOZsnJpKiLYLdNXpcBy1kF1WVOXKnZgDPKU8U8Ct6b5WWgh3q32yk38h2cAichr3upJmmmYyaQmiC4SJiW8AVmJ5Bs9DG+q2SCMjIMjkPcMx6HytHRUtPTYK69TnM6dPcHKSPNtTiK6kZsyNS7OpF/lXOsZEL6qO18u7Zpn2TXeJZe2gn5/cl8qwKzvRF12dR7InkDdkD+NI9fnTcAHD4yd8Wg9EBWzNpL+SYveaEMWJlYjqoyDBuSpGYyBmSEIX9XxJ/6zTt+CeoC2GwaTmrdCfnHor7UFH5oZqN6zd2+D/Lhv/FXbj1oKf/UllLKfy0/ATtM/c/kKrmhAAAAAElFTkSuQmCC);
background-repeat: no-repeat;
background-position: bottom 0.65em right 0.8em;
background-size: 0.5em;
}
input {
background: none;
}
input[type=text] {
font: calc(0.40rem + 1vmin) monospace;
border: 0;
@ -91,14 +130,14 @@ input[type=text] {
outline: 0;
}
input[type=submit] {
input[type=submit], select {
font: calc(0.40rem + 1vmin) monospace;
cursor: pointer;
border: 1px solid #777e73;
background: #bdc7b810;
background-color: #bdc7b810;
}
h1, h2, h3, h4, h5 {
font-size:calc(0.40rem + 1vmin);
margin: initial;

View File

@ -9,7 +9,7 @@
</head>
<body>
<nav>
<div class="nav-row">
<div class="nav-row half-margin">
<div>
<a href="/"><b>Capsul</b></a>💊
</div>
@ -21,9 +21,15 @@
{% endif %}
</div>
</div>
<div class="nav-row">
<div class="nav-row half-margin">
<a href="/faq">FAQ</a>
<a href="/changelog">Changelog</a>
{% if session["account"] %}
<a href="/console/">Console</a>
<a href="/console/billing">Billing</a>
{% endif %}
<a href="/support">Support</a>
</div>
</nav>

View File

@ -3,9 +3,7 @@
{% block title %}Changelog{% endblock %}
{% block content %}
<div class="hero">
<div class="single-content"><h1>CHANGELOG</h1></div>
</div>
<div class="full-margin"><h1>CHANGELOG</h1></div>
{% endblock %}
{% block subcontent %}
<p>

View File

@ -0,0 +1,49 @@
{% extends 'base.html' %}
{% block title %}Console{% endblock %}
{% block content %}
<div class="third-margin">
<h1>CONSOLE</h1>
</div>
<div class="third-margin">
<div class="nav-row">
<a href="/console">Capsuls</a>
<a href="/console/ssh">SSH Public Keys</a>
<a href="/console/billing">Billing</a>
</div>
</div>
<div class="third-margin">
{% if vms[0] is defined %}
<table>
<thead>
<tr>
<th>id</th>
<th>size</th>
<th>ipv4</th>
<th>os</th>
<th>created</th>
</tr>
</thead>
<tbody>
{% for vm in vms %}
<a href="/console/{{ vm['id'] }}">
<tr>
<td>{{ vm["id"] }}</td>
<td>{{ vm["size"] }}</td>
<td>{{ vm["ipv4"] }}</td>
<td>{{ vm["os"] }}</td>
<td>{{ vm["created"] }}</td>
</tr>
</a>
{% endfor %}
</tbody>
</table>
{% else %}
You don't have any Capsuls running. <a href="/console/create">Create one</a> today!
{% endif %}
</div>
{% endblock %}
{% block pagesource %}/templates/console.html{% endblock %}

View File

@ -0,0 +1,68 @@
{% extends 'base.html' %}
{% block title %}Create{% endblock %}
{% block content %}
<div class="half-margin">
<h1>CONSOLE - CREATE CAPSUL</h1>
</div>
<div class="half-margin">
{% if ssh_public_keys[0] is defined %}
<pre>
CAPSUL SIZES
============
type monthly cpus mem ssd net*
----- ------- ---- --- --- ---
f1-s $5.33 1 512M 25G .5TB
f1-m $7.16 1 1024M 25G 1TB
f1-l $8.92 1 2048M 25G 2TB
f1-x $16.16 2 4096M 25G 4TB
f1-xx $29.66 4 8096M 25G 8TB
f1-xxx $57.58 8 16G 25G 16TB
* net is calculated as a per-month average
* all VMs come standard with one public IPv4 addr</pre>
</div>
<form method="post">
<div class="row justify-start">
<label class="align" for="size">Capsul Size</label>
<select id="size" name="size">
{% for size in vm_sizes.keys() %}<option value="{{ size }}">{{ size }}</option>{% endfor %}
</select>
</div>
<div class="row justify-start">
<label class="align" for="os">Operating System</label>
<select id="os" name="os">
{% for os_id, os in operating_systems.items() %}
<option value="{{ os_id }}">{{ os.description }}</option>
{% endfor %}
</select>
</div>
<div class="row justify-end">
<input type="submit" value="Create">
</div>
</form>
{% else %}
<p>You don't have any ssh public keys yet.</p>
<p>You must <a href="/console/ssh/upload">upload one</a> before you can create a Capsul.</p>
{% endif %}
{% endblock %}
{% block subcontent %}
{% if ssh_public_keys[0] is defined %}
<p>
Using our services implies that you agree to our terms of service & privacy policy.
</p>
<ul>
<li>
<a href="https://git.cyberia.club/congress/terms-of-service/plain/README">git.cyberia.club/congress/terms-of-service</a>
</li>
<li>
<a href="https://git.cyberia.club/congress/privacy-policy/plain/README">git.cyberia.club/congress/privacy-policy</a>
</li>
</ul>
{% endif %}
{% endblock %}
{% block pagesource %}/templates/console.html{% endblock %}

View File

@ -3,9 +3,7 @@
{% block title %}FAQ{% endblock %}
{% block content %}
<div class="hero">
<div class="single-content"><h1>Frequently Asked Questions</h1></div>
</div>
<div class="full-margin"><h1>Frequently Asked Questions</h1></div>
{% endblock %}
{% block subcontent %}

View File

@ -2,7 +2,6 @@
{% block content %}
<div class="hero">
<h1>CAPSUL</h1>
<pre>
.-.
@ -13,20 +12,19 @@
\ /
`"`
</pre>
<span>Simple, fast, private compute by <a href="https://cyberia.club">https://cyberia.club</a></span>
</div>
<span>Simple, fast, private compute by <a href="https://cyberia.club">cyberia.club</a></span>
{% endblock %}
{% block subcontent %}
<p>
<ul>
<li>Simply log in with your email address </li>
<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>A Minnesota non-profit organization that will never exploit you</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>
</ul>
</p>

View File

@ -3,9 +3,7 @@
{% block title %}check your email{% endblock %}
{% block content %}
<div class="hero">
<div class="single-content">check your email. a login link has been sent to {{ email }}</div>
</div>
<div class="full-margin">Check your email. A login link has been sent to {{ email }}</div>
{% endblock %}
{% block pagesource %}/templates/login-landing.html{% endblock %}

View File

@ -3,14 +3,16 @@
{% block title %}login{% endblock %}
{% block content %}
<div class="hero">
<form method="post" class="single-content">
<label for="email">Email Address</label>
<input type="text" name="email" id="email" required>
<input type="submit" value="Log In">
<div class="half-margin">
<h1>LOGIN</h1>
</div>
<form method="post" class="half-margin">
<div class="row wrap">
<label for="email">Email Address</label>
<input type="text" name="email" id="email" required>
<input type="submit" value="Log In">
</div>
</form>
</div>
{% endblock %}
{% block pagesource %}/templates/login.html{% endblock %}

View File

@ -3,15 +3,12 @@
{% block title %}Support{% endblock %}
{% block content %}
<div class="hero">
<div class="single-content">
<div class="half-margin">
<h1>SUPPORT</h1>
</div>
<div class="single-content">
<a href="mailto:support@cyberia.club?subject=Please%20halp!">Click Here to Email Support</a> (support@cyberia.club)
<div class="half-margin">
<a href="mailto:support@cyberia.club?subject=Please%20halp!">mailto:support@cyberia.club</a>
</div>
</div>
{% endblock %}
{% block subcontent %}

View File

@ -5,15 +5,10 @@ from flask import current_app
from time import sleep
from os.path import join
from subprocess import run
from nanoid import generate
from capsulflask.db import get_model
def makeCapsulId():
lettersAndNumbers = generate(alphabet="1234567890qwertyuiopasdfghjklzxcvbnm", size=10)
return f"capsul-{lettersAndNumbers}"
def validateCapsulId(id):
def validate_capsul_id(id):
if not re.match(r"^capsul-[a-z0-9]{10}$", id):
raise ValueError(f"vm id \"{id}\" must match \"^capsul-[a-z0-9]{{10}}$\"")
@ -27,10 +22,10 @@ class VirtualizationInterface:
def get(self, id: str) -> VirtualMachine:
pass
def listIds(self) -> list:
def list_ids(self) -> list:
pass
def create(self, email: str, template_file_name: str, vcpus: int, memory: int, ssh_public_keys: list) -> VirtualMachine:
def create(self, email: str, id: str, template_image_file_name: str, vcpus: int, memory: int, ssh_public_keys: list) -> VirtualMachine:
pass
def destroy(self, email: str, id: str):
@ -38,14 +33,14 @@ class VirtualizationInterface:
class MockVirtualization(VirtualizationInterface):
def get(self, id):
validateCapsulId(id)
validate_capsul_id(id)
return VirtualMachine(id, ipv4="1.1.1.1")
def listIds(self) -> list:
return get_model().allVmIds()
def list_ids(self) -> list:
return get_model().all_vm_ids()
def create(self, email: str, template_file_name: str, vcpus: int, memory_mb: int, ssh_public_keys: list):
id = makeCapsulId()
def create(self, email: str, id: str, template_image_file_name: str, vcpus: int, memory_mb: int, ssh_public_keys: list):
validate_capsul_id(id)
print(f"mock create: {id} for {email}")
sleep(5)
return VirtualMachine(id, ipv4="1.1.1.1")
@ -56,7 +51,7 @@ class MockVirtualization(VirtualizationInterface):
class ShellScriptVirtualization(VirtualizationInterface):
def validateCompletedProcess(self, completedProcess, email=None):
def validate_completed_process(self, completedProcess, email=None):
emailPart = ""
if email != None:
emailPart = f"for {email}"
@ -70,9 +65,9 @@ class ShellScriptVirtualization(VirtualizationInterface):
""")
def get(self, id):
validateCapsulId(id)
validate_capsul_id(id)
completedProcess = run([join(current_app.root_path, 'shell_scripts/get.sh'), id], capture_output=True)
self.validateCompletedProcess(completedProcess)
self.validate_completed_process(completedProcess)
lines = completedProcess.stdout.splitlines()
if not re.match(r"^([0-9]{1,3}\.){3}[0-9]{1,3}$", lines[0]):
@ -80,16 +75,16 @@ class ShellScriptVirtualization(VirtualizationInterface):
return VirtualMachine(id, ipv4=lines[0])
def listIds(self) -> list:
completedProcess = run([join(current_app.root_path, 'shell_scripts/listIds.sh')], capture_output=True)
self.validateCompletedProcess(completedProcess)
def list_ids(self) -> list:
completedProcess = run([join(current_app.root_path, 'shell_scripts/list_ids.sh')], capture_output=True)
self.validate_completed_process(completedProcess)
return completedProcess.stdout.splitlines()
def create(self, email: str, template_file_name: str, vcpus: int, memory_mb: int, ssh_public_keys: list):
id = makeCapsulId()
def create(self, email: str, id: str, template_image_file_name: str, vcpus: int, memory_mb: int, ssh_public_keys: list):
validate_capsul_id(id)
if not re.match(r"^[a-zA-Z0-9_.-]$", template_file_name):
raise ValueError(f"template_file_name \"{template_file_name}\" must match \"^[a-zA-Z0-9_.-]$\"")
if not re.match(r"^[a-zA-Z0-9_.-]$", template_image_file_name):
raise ValueError(f"template_image_file_name \"{template_image_file_name}\" must match \"^[a-zA-Z0-9_.-]$\"")
for ssh_public_key in ssh_public_keys:
if not re.match(r"^(ssh|ecdsa)-[0-9A-Za-z+/_=@ -]+$", ssh_public_key):
@ -106,18 +101,18 @@ class ShellScriptVirtualization(VirtualizationInterface):
completedProcess = run([
join(current_app.root_path, 'shell_scripts/create.sh'),
id,
template_file_name,
template_image_file_name,
str(vcpus),
str(memory_mb),
ssh_keys_string
], capture_output=True)
self.validateCompletedProcess(completedProcess, email)
self.validate_completed_process(completedProcess, email)
lines = completedProcess.stdout.splitlines()
vmSettings = f"""
id={id}
template_file_name={template_file_name}
template_image_file_name={template_image_file_name}
vcpus={str(vcpus)}
memory={str(memory_mb)}
ssh_public_keys={ssh_keys_string}
@ -149,9 +144,9 @@ class ShellScriptVirtualization(VirtualizationInterface):
""")
def destroy(self, email: str, id: str):
validateCapsulId(id)
validate_capsul_id(id)
completedProcess = run([join(current_app.root_path, 'shell_scripts/destroy.sh'), id], capture_output=True)
self.validateCompletedProcess(completedProcess, email)
self.validate_completed_process(completedProcess, email)
lines = completedProcess.stdout.splitlines()
if not lines[len(lines)-1] == "success":