from typing import List, Union, Optional, Dict, Type from flask import render_template, url_for, redirect, Blueprint from flask.typing import ResponseReturnValue from flask_wtf import FlaskForm from wtforms import SelectField, StringField, SubmitField, IntegerField, BooleanField, Form, FormField 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", "help_url": "https://bypass.censorship.guide/user/cloud.html" } class NewCloudAccountForm(FlaskForm): # type: ignore provider = SelectField('Cloud Provider', validators=[InputRequired()]) submit = SubmitField('Next') class AWSAccountForm(FlaskForm): # type: ignore 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') class HcloudAccountForm(FlaskForm): # type: ignore 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') class GitlabAccountForm(FlaskForm): # type: ignore 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') 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 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') 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 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] provider_forms: Dict[str, Type[CloudAccountForm]] = { CloudProvider.AWS.name: AWSAccountForm, CloudProvider.HCLOUD.name: HcloudAccountForm, CloudProvider.GANDI.name: GandiAccountForm, CloudProvider.GITLAB.name: GitlabAccountForm, CloudProvider.OVH.name: OvhAccountForm, } def cloud_account_save(account: Optional[CloudAccount], provider: CloudProvider, form: CloudAccountForm) -> None: 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, } 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"], "ovh_cloud_application_secret": form.ovh_api.data["ovh_cloud_application_secret"], "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 elif account.provider == CloudProvider.HCLOUD and isinstance(form, HcloudAccountForm): form.hcloud_token.data = account.credentials["hcloud_token"] form.max_instances.data = account.max_instances elif account.provider == CloudProvider.GANDI and isinstance(form, GandiAccountForm): 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"] form.max_instances.data = account.max_instances elif account.provider == CloudProvider.GITLAB and isinstance(form, GitlabAccountForm): form.gitlab_token.data = account.credentials["gitlab_token"] elif account.provider == CloudProvider.OVH and isinstance(form, OvhAccountForm): 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"] form.max_instances.data = account.max_instances else: raise RuntimeError(f"Unknown provider {account.provider} or form data {type(form)} did not match provider.") @bp.route("/list") def cloud_account_list() -> ResponseReturnValue: 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']) def cloud_account_new() -> ResponseReturnValue: form = NewCloudAccountForm() form.provider.choices = sorted([ (provider.name, provider.description) for provider in CloudProvider ], key=lambda p: p[1].lower()) # type: ignore[no-any-return] if form.validate_on_submit(): 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) @bp.route("/new/", methods=['GET', 'POST']) 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")) return render_template("new.html.j2", form=form, **_SECTION_TEMPLATE_VARS) @bp.route("/edit/", methods=['GET', 'POST']) 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) return render_template("new.html.j2", form=form, **_SECTION_TEMPLATE_VARS)