majuna/app/portal/cloud.py

326 lines
13 KiB
Python
Raw Permalink Normal View History

from typing import Dict, List, Optional, Type, Union
2023-02-26 12:52:08 +00:00
from flask import Blueprint, redirect, render_template, url_for
2023-02-26 12:52:08 +00:00
from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm
2024-12-06 18:15:47 +00:00
from wtforms import (
BooleanField,
Form,
FormField,
IntegerField,
SelectField,
StringField,
SubmitField,
)
2023-02-26 12:52:08 +00:00
from wtforms.validators import InputRequired
from app.extensions import db
from app.models.cloud import CloudAccount, CloudProvider
bp = Blueprint("cloud", __name__)
_SECTION_TEMPLATE_VARS = {
"section": "cloud",
2024-12-06 18:15:47 +00:00
"help_url": "https://bypass.censorship.guide/user/cloud.html",
2023-02-26 12:52:08 +00:00
}
class NewCloudAccountForm(FlaskForm): # type: ignore
2024-12-06 18:15:47 +00:00
provider = SelectField("Cloud Provider", validators=[InputRequired()])
submit = SubmitField("Next")
2023-02-26 12:52:08 +00:00
class AWSAccountForm(FlaskForm): # type: ignore
2024-12-06 18:15:47 +00:00
provider = StringField("Platform", render_kw={"disabled": ""})
description = StringField("Description", validators=[InputRequired()])
aws_access_key = StringField("AWS Access Key", validators=[InputRequired()])
aws_secret_key = StringField("AWS Secret Key", validators=[InputRequired()])
aws_region = StringField(
"AWS Region", default="us-east-2", validators=[InputRequired()]
)
max_distributions = IntegerField(
"Cloudfront Distributions Quota",
default=200,
description="This is the quota for number of distributions per account.",
validators=[InputRequired()],
)
max_instances = IntegerField(
"EC2 Instance Quota",
default=2,
description="This can be impacted by a number of quotas including instance limits "
"and IP address limits.",
validators=[InputRequired()],
)
enabled = BooleanField(
"Enable this account",
default=True,
description="New resources will not be deployed to disabled accounts, however existing "
"resources will persist until destroyed at the end of their lifecycle.",
)
submit = SubmitField("Save Changes")
2023-02-26 12:52:08 +00:00
class HcloudAccountForm(FlaskForm): # type: ignore
2024-12-06 18:15:47 +00:00
provider = StringField("Platform", render_kw={"disabled": ""})
description = StringField("Description", validators=[InputRequired()])
hcloud_token = StringField("Hetzner Cloud Token", validators=[InputRequired()])
max_instances = IntegerField(
"Server Limit", default=10, validators=[InputRequired()]
)
enabled = BooleanField(
"Enable this account",
default=True,
description="New resources will not be deployed to disabled accounts, however existing "
"resources will persist until destroyed at the end of their lifecycle.",
)
submit = SubmitField("Save Changes")
2023-02-26 12:52:08 +00:00
class GitlabAccountForm(FlaskForm): # type: ignore
2024-12-06 18:15:47 +00:00
provider = StringField("Platform", render_kw={"disabled": ""})
description = StringField("Description", validators=[InputRequired()])
gitlab_token = StringField("GitLab Access Token", validators=[InputRequired()])
enabled = BooleanField(
"Enable this account",
default=True,
description="New resources will not be deployed to disabled accounts, however existing "
"resources will persist until destroyed at the end of their lifecycle.",
)
submit = SubmitField("Save Changes")
2023-02-26 12:52:08 +00:00
class OvhHorizonForm(Form): # type: ignore[misc]
ovh_openstack_user = StringField("User")
ovh_openstack_password = StringField("Password")
ovh_openstack_tenant_id = StringField("Tenant ID")
class OvhApiForm(Form): # type: ignore[misc]
ovh_cloud_application_key = StringField("Application Key")
ovh_cloud_application_secret = StringField("Application Secret")
ovh_cloud_consumer_key = StringField("Consumer Key")
class OvhAccountForm(FlaskForm): # type: ignore
2024-12-06 18:15:47 +00:00
provider = StringField("Platform", render_kw={"disabled": ""})
description = StringField("Description", validators=[InputRequired()])
horizon = FormField(OvhHorizonForm, "OpenStack Horizon API")
ovh_api = FormField(OvhApiForm, "OVH API")
max_instances = IntegerField(
"Server Limit", default=10, validators=[InputRequired()]
)
enabled = BooleanField(
"Enable this account",
default=True,
description="New resources will not be deployed to disabled accounts, however existing "
"resources will persist until destroyed at the end of their lifecycle.",
)
submit = SubmitField("Save Changes")
2023-02-26 12:52:08 +00:00
class GandiHorizonForm(Form): # type: ignore[misc]
gandi_openstack_user = StringField("User")
gandi_openstack_password = StringField("Password")
gandi_openstack_tenant_id = StringField("Tenant ID")
class GandiAccountForm(FlaskForm): # type: ignore
2024-12-06 18:15:47 +00:00
provider = StringField("Platform", render_kw={"disabled": ""})
description = StringField("Description", validators=[InputRequired()])
horizon = FormField(GandiHorizonForm, "OpenStack Horizon API")
max_instances = IntegerField(
"Server Limit", default=10, validators=[InputRequired()]
)
enabled = BooleanField(
"Enable this account",
default=True,
description="New resources will not be deployed to disabled accounts, however existing "
"resources will persist until destroyed at the end of their lifecycle.",
)
submit = SubmitField("Save Changes")
CloudAccountForm = Union[
AWSAccountForm, HcloudAccountForm, GandiAccountForm, OvhAccountForm
]
2023-02-26 12:52:08 +00:00
provider_forms: Dict[str, Type[CloudAccountForm]] = {
CloudProvider.AWS.name: AWSAccountForm,
CloudProvider.HCLOUD.name: HcloudAccountForm,
CloudProvider.GANDI.name: GandiAccountForm,
CloudProvider.GITLAB.name: GitlabAccountForm,
2023-02-26 12:52:08 +00:00
CloudProvider.OVH.name: OvhAccountForm,
}
2024-12-06 18:15:47 +00:00
def cloud_account_save(
account: Optional[CloudAccount], provider: CloudProvider, form: CloudAccountForm
) -> None:
2023-02-26 12:52:08 +00:00
if not account:
account = CloudAccount()
account.provider = provider
db.session.add(account)
if account.provider != provider:
raise RuntimeError("Provider mismatch in saving cloud account.")
account.description = form.description.data
account.enabled = form.enabled.data
if provider == CloudProvider.AWS and isinstance(form, AWSAccountForm):
account.credentials = {
"aws_access_key": form.aws_access_key.data,
"aws_secret_key": form.aws_secret_key.data,
"aws_region": form.aws_region.data,
}
account.max_distributions = form.max_distributions.data
account.max_sub_distributions = 1
account.max_instances = form.max_instances.data
elif provider == CloudProvider.HCLOUD and isinstance(form, HcloudAccountForm):
account.credentials = {
"hcloud_token": form.hcloud_token.data,
}
account.max_distributions = 0
account.max_sub_distributions = 0
account.max_instances = form.max_instances.data
elif provider == CloudProvider.GANDI and isinstance(form, GandiAccountForm):
account.credentials = {
"gandi_openstack_user": form.horizon.data["gandi_openstack_user"],
"gandi_openstack_password": form.horizon.data["gandi_openstack_password"],
"gandi_openstack_tenant_id": form.horizon.data["gandi_openstack_tenant_id"],
}
account.max_distributions = 0
account.max_sub_distributions = 0
account.max_instances = form.max_instances.data
elif provider == CloudProvider.GITLAB and isinstance(form, GitlabAccountForm):
account.credentials = {
"gitlab_token": form.gitlab_token.data,
}
2023-02-26 12:52:08 +00:00
elif provider == CloudProvider.OVH and isinstance(form, OvhAccountForm):
account.credentials = {
"ovh_openstack_user": form.horizon.data["ovh_openstack_user"],
"ovh_openstack_password": form.horizon.data["ovh_openstack_password"],
"ovh_openstack_tenant_id": form.horizon.data["ovh_openstack_tenant_id"],
"ovh_cloud_application_key": form.ovh_api.data["ovh_cloud_application_key"],
2024-12-06 18:15:47 +00:00
"ovh_cloud_application_secret": form.ovh_api.data[
"ovh_cloud_application_secret"
],
2023-02-26 12:52:08 +00:00
"ovh_cloud_consumer_key": form.ovh_api.data["ovh_cloud_consumer_key"],
}
account.max_distributions = 0
account.max_sub_distributions = 0
account.max_instances = form.max_instances.data
else:
raise RuntimeError("Unknown provider or form data did not match provider.")
def cloud_account_populate(form: CloudAccountForm, account: CloudAccount) -> None:
form.provider.data = account.provider.description
form.description.data = account.description
form.enabled.data = account.enabled
if account.provider == CloudProvider.AWS and isinstance(form, AWSAccountForm):
form.aws_access_key.data = account.credentials["aws_access_key"]
form.aws_secret_key.data = account.credentials["aws_secret_key"]
form.aws_region.data = account.credentials["aws_region"]
form.max_distributions.data = account.max_distributions
form.max_instances.data = account.max_instances
2024-12-06 18:15:47 +00:00
elif account.provider == CloudProvider.HCLOUD and isinstance(
form, HcloudAccountForm
):
2023-02-26 12:52:08 +00:00
form.hcloud_token.data = account.credentials["hcloud_token"]
form.max_instances.data = account.max_instances
elif account.provider == CloudProvider.GANDI and isinstance(form, GandiAccountForm):
2024-12-06 18:15:47 +00:00
form.horizon.form.gandi_openstack_user.data = account.credentials[
"gandi_openstack_user"
]
form.horizon.form.gandi_openstack_password.data = account.credentials[
"gandi_openstack_password"
]
form.horizon.form.gandi_openstack_tenant_id.data = account.credentials[
"gandi_openstack_tenant_id"
]
2023-02-26 12:52:08 +00:00
form.max_instances.data = account.max_instances
2024-12-06 18:15:47 +00:00
elif account.provider == CloudProvider.GITLAB and isinstance(
form, GitlabAccountForm
):
form.gitlab_token.data = account.credentials["gitlab_token"]
2023-02-26 12:52:08 +00:00
elif account.provider == CloudProvider.OVH and isinstance(form, OvhAccountForm):
2024-12-06 18:15:47 +00:00
form.horizon.form.ovh_openstack_user.data = account.credentials[
"ovh_openstack_user"
]
form.horizon.form.ovh_openstack_password.data = account.credentials[
"ovh_openstack_password"
]
form.horizon.form.ovh_openstack_tenant_id.data = account.credentials[
"ovh_openstack_tenant_id"
]
form.ovh_api.form.ovh_cloud_application_key.data = account.credentials[
"ovh_cloud_application_key"
]
form.ovh_api.form.ovh_cloud_application_secret.data = account.credentials[
"ovh_cloud_application_secret"
]
form.ovh_api.form.ovh_cloud_consumer_key.data = account.credentials[
"ovh_cloud_consumer_key"
]
2023-02-26 12:52:08 +00:00
form.max_instances.data = account.max_instances
else:
2024-12-06 18:15:47 +00:00
raise RuntimeError(
f"Unknown provider {account.provider} or form data {type(form)} did not match provider."
)
2023-02-26 12:52:08 +00:00
@bp.route("/list")
def cloud_account_list() -> ResponseReturnValue:
2024-12-06 18:15:47 +00:00
accounts: List[CloudAccount] = CloudAccount.query.filter(
CloudAccount.destroyed.is_(None)
).all()
return render_template(
"list.html.j2",
title="Cloud Accounts",
item="cloud account",
items=accounts,
new_link=url_for("portal.cloud.cloud_account_new"),
**_SECTION_TEMPLATE_VARS,
)
@bp.route("/new", methods=["GET", "POST"])
2023-02-26 12:52:08 +00:00
def cloud_account_new() -> ResponseReturnValue:
form = NewCloudAccountForm()
2024-12-06 18:15:47 +00:00
form.provider.choices = sorted(
[(provider.name, provider.description) for provider in CloudProvider],
key=lambda p: p[1].lower(),
)
2023-02-26 12:52:08 +00:00
if form.validate_on_submit():
2024-12-06 18:15:47 +00:00
return redirect(
url_for("portal.cloud.cloud_account_new_for", provider=form.provider.data)
)
return render_template("new.html.j2", form=form, **_SECTION_TEMPLATE_VARS)
2023-02-26 12:52:08 +00:00
2024-12-06 18:15:47 +00:00
@bp.route("/new/<provider>", methods=["GET", "POST"])
2023-02-26 12:52:08 +00:00
def cloud_account_new_for(provider: str) -> ResponseReturnValue:
form = provider_forms[provider]()
form.provider.data = CloudProvider[provider].description
if form.validate_on_submit():
cloud_account_save(None, CloudProvider[provider], form)
db.session.commit()
return redirect(url_for("portal.cloud.cloud_account_list"))
2024-12-06 18:15:47 +00:00
return render_template("new.html.j2", form=form, **_SECTION_TEMPLATE_VARS)
2023-02-26 12:52:08 +00:00
2024-12-06 18:15:47 +00:00
@bp.route("/edit/<account_id>", methods=["GET", "POST"])
2023-02-26 12:52:08 +00:00
def cloud_account_edit(account_id: int) -> ResponseReturnValue:
account = CloudAccount.query.filter(
CloudAccount.id == account_id,
CloudAccount.destroyed.is_(None),
).first()
if not account:
return "Not found", 404
form = provider_forms[account.provider.name]()
if form.validate_on_submit():
cloud_account_save(account, account.provider, form)
print(account.description)
db.session.commit()
return redirect(url_for("portal.cloud.cloud_account_list"))
cloud_account_populate(form, account)
2024-12-06 18:15:47 +00:00
return render_template("new.html.j2", form=form, **_SECTION_TEMPLATE_VARS)