feat: abstracting cloud providers

This commit is contained in:
Iain Learmonth 2023-02-26 12:52:08 +00:00
parent af36a545a1
commit 0a72aeed96
18 changed files with 629 additions and 181 deletions

View file

@ -50,7 +50,7 @@ class BridgeConf(AbstractConfiguration):
class Bridge(AbstractResource): class Bridge(AbstractResource):
conf_id = db.Column(db.Integer, db.ForeignKey("bridge_conf.id"), nullable=False) conf_id = db.Column(db.Integer, db.ForeignKey("bridge_conf.id"), nullable=False)
provider = db.Column(db.String(), nullable=False) cloud_account_id = db.Column(db.Integer, db.ForeignKey("cloud_account.id"))
terraform_updated = db.Column(db.DateTime(), nullable=True) terraform_updated = db.Column(db.DateTime(), nullable=True)
nickname = db.Column(db.String(255), nullable=True) nickname = db.Column(db.String(255), nullable=True)
fingerprint = db.Column(db.String(255), nullable=True) fingerprint = db.Column(db.String(255), nullable=True)
@ -58,13 +58,14 @@ class Bridge(AbstractResource):
bridgeline = db.Column(db.String(255), nullable=True) bridgeline = db.Column(db.String(255), nullable=True)
conf = db.relationship("BridgeConf", back_populates="bridges") conf = db.relationship("BridgeConf", back_populates="bridges")
cloud_account = db.relationship("CloudAccount", back_populates="bridges")
@property @property
def brn(self) -> BRN: def brn(self) -> BRN:
return BRN( return BRN(
group_id=0, group_id=0,
product="bridge", product="bridge",
provider=self.provider, provider=self.cloud_account.provider.key,
resource_type="bridge", resource_type="bridge",
resource_id=str(self.id) resource_id=str(self.id)
) )

View file

@ -0,0 +1,42 @@
import enum
from app.brm.brn import BRN
from app.extensions import db
from app.models import AbstractConfiguration
class CloudProvider(enum.Enum):
AWS = ("aws", "Amazon Web Services")
AZURE = ("azure", "Microsoft Azure")
BUNNY = ("bunny", "bunny.net")
CLOUDFLARE = ("cloudflare", "Cloudflare")
FASTLY = ("fastly", "Fastly")
HTTP = ("http", "HTTP")
GANDI = ("gandi", "Gandi")
GITHUB = ("github", "GitHub")
GITLAB = ("gitlab", "GitLab")
HCLOUD = ("hcloud", "Hetzner Cloud")
MAXMIND = ("maxmind", "MaxMind")
OVH = ("ovh", "OVH")
RFC2136 = ("rfc2136", "RFC2136 DNS Server")
def __init__(self, key: str, description: str):
self.key = key
self.description = description
class CloudAccount(AbstractConfiguration):
provider = db.Column(db.Enum(CloudProvider))
credentials = db.Column(db.JSON())
enabled = db.Column(db.Boolean())
# CDN Quotas
max_distributions = db.Column(db.Integer())
max_sub_distributions = db.Column(db.Integer())
# Compute Quotas
max_instances = db.Column(db.Integer())
bridges = db.relationship("Bridge", back_populates="cloud_account")
@property
def brn(self) -> BRN:
raise NotImplementedError("No BRN for cloud accounts")

View file

@ -14,6 +14,7 @@ from app.models.bridges import Bridge
from app.models.mirrors import Origin, Proxy from app.models.mirrors import Origin, Proxy
from app.models.base import Group from app.models.base import Group
from app.models.onions import Eotk from app.models.onions import Eotk
from app.portal.cloud import bp as cloud
from app.portal.automation import bp as automation from app.portal.automation import bp as automation
from app.portal.bridgeconf import bp as bridgeconf from app.portal.bridgeconf import bp as bridgeconf
from app.portal.bridge import bp as bridge from app.portal.bridge import bp as bridge
@ -32,6 +33,7 @@ portal = Blueprint("portal", __name__, template_folder="templates", static_folde
portal.register_blueprint(automation, url_prefix="/automation") portal.register_blueprint(automation, url_prefix="/automation")
portal.register_blueprint(bridgeconf, url_prefix="/bridgeconf") portal.register_blueprint(bridgeconf, url_prefix="/bridgeconf")
portal.register_blueprint(bridge, url_prefix="/bridge") portal.register_blueprint(bridge, url_prefix="/bridge")
portal.register_blueprint(cloud, url_prefix="/cloud")
portal.register_blueprint(eotk, url_prefix="/eotk") portal.register_blueprint(eotk, url_prefix="/eotk")
portal.register_blueprint(group, url_prefix="/group") portal.register_blueprint(group, url_prefix="/group")
portal.register_blueprint(list_, url_prefix="/list") portal.register_blueprint(list_, url_prefix="/list")

View file

@ -0,0 +1,243 @@
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 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.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.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.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/<provider>", 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/<account_id>", 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)

View file

@ -81,6 +81,12 @@
<span>Configuration</span> <span>Configuration</span>
</h6> </h6>
<ul class="nav flex-column"> <ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link{% if section == "cloud" %} active{% else %} disabled text-secondary{% endif %}"
href="{{ url_for("portal.cloud.cloud_account_list") }}">
{{ icon("cloud") }} Cloud Accounts
</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link{% if section == "pool" %} active{% endif %}" <a class="nav-link{% if section == "pool" %} active{% endif %}"
href="{{ url_for("portal.pool.pool_list") }}"> href="{{ url_for("portal.pool.pool_list") }}">

View file

@ -16,6 +16,10 @@
viewBox="0 0 16 16"> viewBox="0 0 16 16">
<path d="M8 16a2 2 0 0 0 2-2H6a2 2 0 0 0 2 2zM8 1.918l-.797.161A4.002 4.002 0 0 0 4 6c0 .628-.134 2.197-.459 3.742-.16.767-.376 1.566-.663 2.258h10.244c-.287-.692-.502-1.49-.663-2.258C12.134 8.197 12 6.628 12 6a4.002 4.002 0 0 0-3.203-3.92L8 1.917zM14.22 12c.223.447.481.801.78 1H1c.299-.199.557-.553.78-1C2.68 10.2 3 6.88 3 6c0-2.42 1.72-4.44 4.005-4.901a1 1 0 1 1 1.99 0A5.002 5.002 0 0 1 13 6c0 .88.32 4.2 1.22 6z"/> <path d="M8 16a2 2 0 0 0 2-2H6a2 2 0 0 0 2 2zM8 1.918l-.797.161A4.002 4.002 0 0 0 4 6c0 .628-.134 2.197-.459 3.742-.16.767-.376 1.566-.663 2.258h10.244c-.287-.692-.502-1.49-.663-2.258C12.134 8.197 12 6.628 12 6a4.002 4.002 0 0 0-3.203-3.92L8 1.917zM14.22 12c.223.447.481.801.78 1H1c.299-.199.557-.553.78-1C2.68 10.2 3 6.88 3 6c0-2.42 1.72-4.44 4.005-4.901a1 1 0 1 1 1.99 0A5.002 5.002 0 0 1 13 6c0 .88.32 4.2 1.22 6z"/>
</svg> </svg>
{% elif i == "cloud" %}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cloud" viewBox="0 0 16 16">
<path d="M4.406 3.342A5.53 5.53 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773 16 11.569 14.502 13 12.687 13H3.781C1.708 13 0 11.366 0 9.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383zm.653.757c-.757.653-1.153 1.44-1.153 2.056v.448l-.445.049C2.064 6.805 1 7.952 1 9.318 1 10.785 2.23 12 3.781 12h8.906C13.98 12 15 10.988 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3a4.53 4.53 0 0 0-2.941 1.1z"/>
</svg>
{% elif i == "collection" %} {% elif i == "collection" %}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-collection" <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-collection"
viewBox="0 0 16 16"> viewBox="0 0 16 16">

View file

@ -1,7 +1,7 @@
{% extends "base.html.j2" %} {% extends "base.html.j2" %}
{% from "tables.html.j2" import alarms_table, automations_table, bridgeconfs_table, bridges_table, eotk_table, {% from "tables.html.j2" import alarms_table, automations_table, bridgeconfs_table, bridges_table, cloud_accounts_table,
groups_table, instances_table, mirrorlists_table, origins_table, origin_onion_table, onions_table, pools_table, eotk_table, groups_table, instances_table, mirrorlists_table, origins_table, origin_onion_table, onions_table,
proxies_table, webhook_table %} pools_table, proxies_table, webhook_table %}
{% block content %} {% block content %}
<h1 class="h2 mt-3">{{ title }}</h1> <h1 class="h2 mt-3">{{ title }}</h1>
@ -19,6 +19,8 @@
{{ bridgeconfs_table(items) }} {{ bridgeconfs_table(items) }}
{% elif item == "bridge" %} {% elif item == "bridge" %}
{{ bridges_table(items) }} {{ bridges_table(items) }}
{% elif item == "cloud account" %}
{{ cloud_accounts_table(items) }}
{% elif item == "eotk" %} {% elif item == "eotk" %}
{{ eotk_table(items) }} {{ eotk_table(items) }}
{% elif item == "group" %} {% elif item == "group" %}

View file

@ -185,6 +185,34 @@
</div> </div>
{% endmacro %} {% endmacro %}
{% macro cloud_accounts_table(accounts) %}
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Provider</th>
<th scope="col">Description</th>
<th scope="col">Enabled</th>
</tr>
</thead>
<tbody>
{% for account in accounts %}
{% if not account.destroyed %}
<tr class="align-middle">
<td>{{ account.provider.description }}</td>
<td>{{ account.description }}</td>
<td>{% if account.enabled %}✅{% else %}❌{% endif %}</td>
<td>
<a href="{{ url_for("portal.cloud.cloud_account_edit", account_id=account.id) }}" class="btn btn-primary btn-sm">View/Edit</a>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
{% endmacro %}
{% macro groups_table(groups, pool=None) %} {% macro groups_table(groups, pool=None) %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-sm"> <table class="table table-striped table-sm">
@ -491,10 +519,11 @@
<table class="table table-striped table-sm"> <table class="table table-striped table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">Pool</th>
<th scope="col">Configuration</th>
<th scope="col">Nickname</th> <th scope="col">Nickname</th>
<th scope="col">Hashed Fingerprint</th> <th scope="col">Hashed Fingerprint</th>
<th scope="col">Pool</th>
<th scope="col">Configuration</th>
<th scope="col">Cloud Account</th>
<th scope="col">Alarms</th> <th scope="col">Alarms</th>
<th scope="col">Actions</th> <th scope="col">Actions</th>
</tr> </tr>
@ -503,10 +532,6 @@
{% for bridge in bridges %} {% for bridge in bridges %}
{% if not bridge.destroyed %} {% if not bridge.destroyed %}
<tr class="align-middle{% if bridge.deprecated %} bg-warning{% endif %}"> <tr class="align-middle{% if bridge.deprecated %} bg-warning{% endif %}">
<td>
<a href="{{ url_for("portal.pool.pool_edit", pool_id=bridge.conf.pool_id) }}">{{ bridge.conf.pool.pool_name }}</a>
</td>
<td>{{ bridge.conf.description }} ({{ bridge.provider }}/{{ bridge.conf.method }})</td>
<td> <td>
<a href="https://metrics.torproject.org/rs.html#details/{{ bridge.hashed_fingerprint }}"> <a href="https://metrics.torproject.org/rs.html#details/{{ bridge.hashed_fingerprint }}">
{{ bridge.nickname }} {{ bridge.nickname }}
@ -515,6 +540,15 @@
<td> <td>
<code>{{ bridge.hashed_fingerprint }}</code> <code>{{ bridge.hashed_fingerprint }}</code>
</td> </td>
<td>
<a href="{{ url_for("portal.pool.pool_edit", pool_id=bridge.conf.pool_id) }}">{{ bridge.conf.pool.pool_name }}</a>
</td>
<td>{{ bridge.conf.description }} ({{ bridge.conf.method }})</td>
<td>
<a href="{{ url_for("portal.cloud.cloud_account_edit", account_id=bridge.cloud_account_id) }}">
{{ bridge.cloud_account.provider.description }} ({{ bridge.cloud_account.description }})
</a>
</td>
<td> <td>
{% for alarm in bridge.alarms %} {% for alarm in bridge.alarms %}
<span title="{{ alarm.alarm_type }}"> <span title="{{ alarm.alarm_type }}">

View file

@ -1,14 +1,38 @@
import datetime import datetime
import os import os
import sys import sys
from typing import Optional, Any, List from typing import Optional, Any, List, Tuple
from sqlalchemy import select
from app import app from app import app
from app.extensions import db from app.extensions import db
from app.models.bridges import BridgeConf, Bridge from app.models.bridges import Bridge, BridgeConf
from app.models.base import Group from app.models.cloud import CloudAccount, CloudProvider
from app.terraform.terraform import TerraformAutomation from app.terraform.terraform import TerraformAutomation
BridgeResourceRow = List[Tuple[Bridge, BridgeConf, CloudAccount]]
def active_bridges_by_provider(provider: CloudProvider) -> List[BridgeResourceRow]:
stmt = select(Bridge, BridgeConf, CloudAccount).join_from(Bridge, BridgeConf).join_from(Bridge, CloudAccount).where(
CloudAccount.provider == provider,
Bridge.destroyed.is_(None),
)
bridges: List[BridgeResourceRow] = db.session.execute(stmt).all()
return bridges
def recently_destroyed_bridges_by_provider(provider: CloudProvider) -> List[BridgeResourceRow]:
cutoff = datetime.datetime.utcnow() - datetime.timedelta(hours=72)
stmt = select(Bridge, BridgeConf, CloudAccount).join_from(Bridge, BridgeConf).join_from(Bridge, CloudAccount).where(
CloudAccount.provider == provider,
Bridge.destroyed.is_not(None),
Bridge.destroyed >= cutoff,
)
bridges: List[BridgeResourceRow] = db.session.execute(stmt).all()
return bridges
class BridgeAutomation(TerraformAutomation): class BridgeAutomation(TerraformAutomation):
template: str template: str
@ -33,11 +57,8 @@ class BridgeAutomation(TerraformAutomation):
def tf_generate(self) -> None: def tf_generate(self) -> None:
self.tf_write( self.tf_write(
self.template, self.template,
groups=Group.query.all(), active_resources=active_bridges_by_provider(self.provider),
bridgeconfs=BridgeConf.query.filter( destroyed_resources=recently_destroyed_bridges_by_provider(self.provider),
BridgeConf.destroyed.is_(None),
BridgeConf.provider == self.provider
).all(),
global_namespace=app.config['GLOBAL_NAMESPACE'], global_namespace=app.config['GLOBAL_NAMESPACE'],
terraform_modules_path=os.path.join(*list(os.path.split(app.root_path))[:-1], 'terraform-modules'), terraform_modules_path=os.path.join(*list(os.path.split(app.root_path))[:-1], 'terraform-modules'),
backend_config=f"""backend "http" {{ backend_config=f"""backend "http" {{

View file

@ -1,15 +1,15 @@
from app.models.cloud import CloudProvider
from app.terraform.bridge import BridgeAutomation from app.terraform.bridge import BridgeAutomation
class BridgeAWSAutomation(BridgeAutomation): class BridgeAWSAutomation(BridgeAutomation):
short_name = "bridge_aws" short_name = "bridge_aws"
description = "Deploy Tor bridges on AWS Lightsail" description = "Deploy Tor bridges on AWS EC2"
provider = "aws" provider = CloudProvider.AWS
template_parameters = [ template_parameters = [
"aws_access_key", "ssh_public_key_path",
"aws_secret_key", "ssh_private_key_path",
"ssh_public_key_path"
] ]
template = """ template = """
@ -22,37 +22,42 @@ class BridgeAWSAutomation(BridgeAutomation):
} }
} }
provider "aws" {
access_key = "{{ aws_access_key }}"
secret_key = "{{ aws_secret_key }}"
region = "us-east-1"
}
locals { locals {
ssh_key = file("{{ ssh_public_key_path }}") ssh_public_key = "{{ ssh_public_key_path }}"
ssh_private_key = "{{ ssh_public_key_path }}"
} }
{% for group in groups %} {% for resource in destroyed_resources %}
module "label_{{ group.id }}" { {% set bridge, bridgeconf, account = resource %}
source = "cloudposse/label/null" provider "aws" {
version = "0.25.0" access_key = "{{ account.credentials['aws_access_key'] }}"
namespace = "{{ global_namespace }}" secret_key = "{{ account.credentials['aws_secret_key'] }}"
tenant = "{{ group.group_name }}" region = "{{ account.credentials['aws_region'] }}"
label_order = ["namespace", "tenant", "name", "attributes"] alias = "account_{{ bridge.id }}"
} }
{% endfor %} {% endfor %}
{% for bridgeconf in bridgeconfs %} {% for resource in resources %}
{% for bridge in bridgeconf.bridges %} {% set bridge, bridgeconf, account = resource %}
{% if not bridge.destroyed %} provider "aws" {
access_key = "{{ account.credentials['aws_access_key'] }}"
secret_key = "{{ account.credentials['aws_secret_key'] }}"
region = "{{ account.credentials['aws_region'] }}"
alias = "account_{{ bridge.id }}"
}
module "bridge_{{ bridge.id }}" { module "bridge_{{ bridge.id }}" {
source = "{{ terraform_modules_path }}/terraform-aws-tor-bridge" providers = {
ssh_key = local.ssh_key aws = aws.account_{{ bridge.id }}
contact_info = "hi" }
context = module.label_{{ bridgeconf.group.id }}.context source = "{{ terraform_modules_path }}/terraform-aws-tor-bridge"
name = "br" ssh_public_key = local.ssh_public_key
attributes = ["{{ bridge.id }}"] ssh_private_key = local.ssh_private_key
distribution_method = "{{ bridge.conf.method }}" contact_info = "hi"
namespace = "{{ global_namespace }}"
name = "bridge"
attributes = ["{{ bridge.id }}"]
distribution_method = "{{ bridgeconf.method }}"
} }
output "bridge_hashed_fingerprint_{{ bridge.id }}" { output "bridge_hashed_fingerprint_{{ bridge.id }}" {
@ -63,7 +68,5 @@ class BridgeAWSAutomation(BridgeAutomation):
value = module.bridge_{{ bridge.id }}.bridgeline value = module.bridge_{{ bridge.id }}.bridgeline
sensitive = true sensitive = true
} }
{% endif %}
{% endfor %}
{% endfor %} {% endfor %}
""" """

View file

@ -1,15 +1,13 @@
from app.models.cloud import CloudProvider
from app.terraform.bridge import BridgeAutomation from app.terraform.bridge import BridgeAutomation
class BridgeGandiAutomation(BridgeAutomation): class BridgeGandiAutomation(BridgeAutomation):
short_name = "bridge_gandi" short_name = "bridge_gandi"
description = "Deploy Tor bridges on GandiCloud VPS" description = "Deploy Tor bridges on GandiCloud VPS"
provider = "gandi" provider = CloudProvider.GANDI
template_parameters = [ template_parameters = [
"gandi_openstack_user",
"gandi_openstack_password",
"gandi_openstack_tenant_name",
"ssh_public_key_path", "ssh_public_key_path",
"ssh_private_key_path" "ssh_private_key_path"
] ]
@ -25,43 +23,50 @@ class BridgeGandiAutomation(BridgeAutomation):
} }
} }
locals {
public_ssh_key = "{{ ssh_public_key_path }}"
private_ssh_key = "{{ ssh_private_key_path }}"
}
{% for resource in destroyed_resources %}
{% set bridge, bridgeconf, account = resource %}
provider "openstack" { provider "openstack" {
auth_url = "https://keystone.sd6.api.gandi.net:5000/v3" auth_url = "https://keystone.sd6.api.gandi.net:5000/v3"
user_domain_name = "public" user_domain_name = "public"
project_domain_name = "public" project_domain_name = "public"
user_name = "{{ gandi_openstack_user }}" user_name = "{{ account.credentials["gandi_openstack_user"] }}"
password = "{{ gandi_openstack_password }}" password = "{{ account.credentials["gandi_openstack_password"] }}"
tenant_name = "{{ gandi_openstack_tenant_name }}" tenant_name = "{{ account.credentials["gandi_openstack_tenant_id"] }}"
region = "FR-SD6" region = "FR-SD6"
} alias = "account_{{ bridge.id }}"
locals {
public_ssh_key = file("{{ ssh_public_key_path }}")
private_ssh_key = file("{{ ssh_private_key_path }}")
}
{% for group in groups %}
module "label_{{ group.id }}" {
source = "cloudposse/label/null"
version = "0.25.0"
namespace = "{{ global_namespace }}"
tenant = "{{ group.group_name }}"
label_order = ["namespace", "tenant", "name", "attributes"]
} }
{% endfor %} {% endfor %}
{% for bridgeconf in bridgeconfs %} {% for resource in active_resources %}
{% for bridge in bridgeconf.bridges %} {% set bridge, bridgeconf, account = resource %}
{% if not bridge.destroyed %} provider "openstack" {
auth_url = "https://keystone.sd6.api.gandi.net:5000/v3"
user_domain_name = "public"
project_domain_name = "public"
user_name = "{{ account.credentials["gandi_openstack_user"] }}"
password = "{{ account.credentials["gandi_openstack_password"] }}"
tenant_name = "{{ account.credentials["gandi_openstack_tenant_id"] }}"
region = "FR-SD6"
alias = "account_{{ bridge.id }}"
}
module "bridge_{{ bridge.id }}" { module "bridge_{{ bridge.id }}" {
providers = {
openstack = openstack.account_{{ bridge.id }}
}
source = "{{ terraform_modules_path }}/terraform-openstack-tor-bridge" source = "{{ terraform_modules_path }}/terraform-openstack-tor-bridge"
context = module.label_{{ bridgeconf.group.id }}.context namespace = "{{ global_namespace }}"
name = "br" name = "bridge"
attributes = ["{{ bridge.id }}"] attributes = ["{{ bridge.id }}"]
ssh_key = local.public_ssh_key ssh_key = local.public_ssh_key
ssh_private_key = local.private_ssh_key ssh_private_key = local.private_ssh_key
contact_info = "hi" contact_info = "did not write the code to populate yet"
distribution_method = "{{ bridge.conf.method }}" distribution_method = "{{ bridgeconf.method }}"
image_name = "Debian 11 Bullseye" image_name = "Debian 11 Bullseye"
flavor_name = "V-R1" flavor_name = "V-R1"
@ -77,7 +82,5 @@ class BridgeGandiAutomation(BridgeAutomation):
value = module.bridge_{{ bridge.id }}.bridgeline value = module.bridge_{{ bridge.id }}.bridgeline
sensitive = true sensitive = true
} }
{% endif %}
{% endfor %}
{% endfor %} {% endfor %}
""" """

View file

@ -1,14 +1,15 @@
from app.models.cloud import CloudProvider
from app.terraform.bridge import BridgeAutomation from app.terraform.bridge import BridgeAutomation
class BridgeHcloudAutomation(BridgeAutomation): class BridgeHcloudAutomation(BridgeAutomation):
short_name = "bridge_hcloud" short_name = "bridge_hcloud"
description = "Deploy Tor bridges on Hetzner Cloud" description = "Deploy Tor bridges on Hetzner Cloud"
provider = "hcloud" provider = CloudProvider.HCLOUD
template_parameters = [ template_parameters = [
"hcloud_token", "ssh_private_key_path",
"ssh_private_key_path" "ssh_public_key_path"
] ]
template = """ template = """
@ -26,36 +27,36 @@ class BridgeHcloudAutomation(BridgeAutomation):
} }
} }
provider "hcloud" {
token = "{{ hcloud_token }}"
}
locals { locals {
ssh_private_key = file("{{ ssh_private_key_path }}") ssh_private_key = "{{ ssh_private_key_path }}"
} }
data "hcloud_datacenters" "ds" { {% for resource in destroyed_resources %}
} {% set bridge, bridgeconf, account = resource %}
provider "hcloud" {
data "hcloud_server_type" "cx11" { token = "{{ account.credentials["hcloud_token"] }}"
name = "cx11" alias = "account_{{ bridge.id }}"
}
{% for group in groups %}
module "label_{{ group.id }}" {
source = "cloudposse/label/null"
version = "0.25.0"
namespace = "{{ global_namespace }}"
tenant = "{{ group.group_name }}"
label_order = ["namespace", "tenant", "name", "attributes"]
} }
{% endfor %} {% endfor %}
{% for bridgeconf in bridgeconfs %} {% for resource in active_resources %}
{% for bridge in bridgeconf.bridges %} {% set bridge, bridgeconf, account = resource %}
{% if not bridge.destroyed %} provider "hcloud" {
token = "{{ account.credentials["hcloud_token"] }}"
alias = "account_{{ bridge.id }}"
}
data "hcloud_datacenters" "ds_{{ bridge.id }}" {
provider = hcloud.account_{{ bridge.id }}
}
data "hcloud_server_type" "cx11_{{ bridge.id }}" {
provider = hcloud.account_{{ bridge.id }}
name = "cx11"
}
resource "random_shuffle" "datacenter_{{ bridge.id }}" { resource "random_shuffle" "datacenter_{{ bridge.id }}" {
input = [for s in data.hcloud_datacenters.ds.datacenters : s.name if contains(s.available_server_type_ids, data.hcloud_server_type.cx11.id)] input = [for s in data.hcloud_datacenters.ds_{{ bridge.id }}.datacenters : s.name if contains(s.available_server_type_ids, data.hcloud_server_type.cx11_{{ bridge.id }}.id)]
result_count = 1 result_count = 1
lifecycle { lifecycle {
@ -64,15 +65,17 @@ class BridgeHcloudAutomation(BridgeAutomation):
} }
module "bridge_{{ bridge.id }}" { module "bridge_{{ bridge.id }}" {
providers = {
hcloud = hcloud.account_{{ bridge.id }}
}
source = "{{ terraform_modules_path }}/terraform-hcloud-tor-bridge" source = "{{ terraform_modules_path }}/terraform-hcloud-tor-bridge"
datacenter = one(random_shuffle.datacenter_{{ bridge.id }}.result) datacenter = one(random_shuffle.datacenter_{{ bridge.id }}.result)
context = module.label_{{ bridgeconf.group.id }}.context namespace = "{{ global_namespace }}"
name = "br" name = "bridge"
attributes = ["{{ bridge.id }}"] attributes = ["{{ bridge.id }}"]
ssh_key_name = "bc"
ssh_private_key = local.ssh_private_key ssh_private_key = local.ssh_private_key
contact_info = "hi" contact_info = "this used to be sanitised and I did not write the code to populate it yet"
distribution_method = "{{ bridge.conf.method }}" distribution_method = "{{ bridgeconf.method }}"
} }
output "bridge_hashed_fingerprint_{{ bridge.id }}" { output "bridge_hashed_fingerprint_{{ bridge.id }}" {
@ -83,7 +86,5 @@ class BridgeHcloudAutomation(BridgeAutomation):
value = module.bridge_{{ bridge.id }}.bridgeline value = module.bridge_{{ bridge.id }}.bridgeline
sensitive = true sensitive = true
} }
{% endif %}
{% endfor %}
{% endfor %} {% endfor %}
""" """

View file

@ -4,18 +4,40 @@ from typing import Tuple, List
from app import db from app import db
from app.models.bridges import BridgeConf, Bridge from app.models.bridges import BridgeConf, Bridge
from app.models.cloud import CloudProvider, CloudAccount
from app.terraform import BaseAutomation from app.terraform import BaseAutomation
from app.terraform.bridge.gandi import BridgeGandiAutomation
from app.terraform.bridge.hcloud import BridgeHcloudAutomation
from app.terraform.bridge.ovh import BridgeOvhAutomation
BRIDGE_PROVIDERS = {p.provider: p for p in [ BRIDGE_PROVIDERS = [
# In order of cost # In order of cost
BridgeHcloudAutomation, CloudProvider.HCLOUD,
BridgeGandiAutomation, CloudProvider.GANDI,
BridgeOvhAutomation, CloudProvider.OVH,
# BridgeAWSAutomation, TODO: This module is broken right now CloudProvider.AWS,
] if p.enabled} ]
def active_bridges_in_account(account: CloudAccount) -> List[Bridge]:
bridges: List[Bridge] = Bridge.query.filter(
Bridge.cloud_account_id == account.id,
Bridge.destroyed.is_(None),
).all()
return bridges
def create_bridges_in_account(bridgeconf: BridgeConf, account: CloudAccount, count: int) -> int:
created = 0
while created < count and len(active_bridges_in_account(account)) < account.max_instances:
logging.debug("Creating bridge for configuration %s in account %s", bridgeconf.id, account)
bridge = Bridge()
bridge.pool_id = bridgeconf.pool.id
bridge.conf_id = bridgeconf.id
bridge.cloud_account = account
bridge.added = datetime.datetime.utcnow()
bridge.updated = datetime.datetime.utcnow()
logging.debug("Creating bridge %s", bridge)
db.session.add(bridge)
created += 1
return created
def create_bridges(bridgeconf: BridgeConf, count: int) -> int: def create_bridges(bridgeconf: BridgeConf, count: int) -> int:
@ -24,24 +46,17 @@ def create_bridges(bridgeconf: BridgeConf, count: int) -> int:
""" """
logging.debug("Creating %s bridges for configuration %s", count, bridgeconf.id) logging.debug("Creating %s bridges for configuration %s", count, bridgeconf.id)
created = 0 created = 0
# TODO: deal with the fact that I created a dictionary and then forgot it wasn't ordered for provider in BRIDGE_PROVIDERS:
while created < count: if created >= count:
for provider in BRIDGE_PROVIDERS: break
if BRIDGE_PROVIDERS[provider].max_bridges > BRIDGE_PROVIDERS[provider].active_bridges_count(): logging.info("Creating bridges in %s accounts", provider.description)
logging.debug("Creating bridge for configuration %s with provider %s", bridgeconf.id, provider) for account in CloudAccount.query.filter(
bridge = Bridge() CloudAccount.destroyed.is_(None),
bridge.pool_id = bridgeconf.pool.id CloudAccount.enabled.is_(True),
bridge.conf_id = bridgeconf.id CloudAccount.provider == provider,
bridge.provider = provider ):
bridge.added = datetime.datetime.utcnow() logging.info("Creating bridges in %s", account)
bridge.updated = datetime.datetime.utcnow() created += create_bridges_in_account(bridgeconf, account, count - created)
logging.debug("Creating bridge %s", bridge)
db.session.add(bridge)
created += 1
break
else:
logging.debug("No provider has available quota to create missing bridge for configuration %s",
bridgeconf.id)
logging.debug("Created %s bridges", created) logging.debug("Created %s bridges", created)
return created return created

View file

@ -1,18 +1,13 @@
from app.models.cloud import CloudProvider
from app.terraform.bridge import BridgeAutomation from app.terraform.bridge import BridgeAutomation
class BridgeOvhAutomation(BridgeAutomation): class BridgeOvhAutomation(BridgeAutomation):
short_name = "bridge_ovh" short_name = "bridge_ovh"
description = "Deploy Tor bridges on OVH Public Cloud" description = "Deploy Tor bridges on OVH Public Cloud"
provider = "ovh" provider = CloudProvider.OVH
template_parameters = [ template_parameters = [
"ovh_cloud_application_key",
"ovh_cloud_application_secret",
"ovh_cloud_consumer_key",
"ovh_openstack_user",
"ovh_openstack_password",
"ovh_openstack_tenant_id",
"ssh_public_key_path", "ssh_public_key_path",
"ssh_private_key_path" "ssh_private_key_path"
] ]
@ -36,46 +31,50 @@ class BridgeOvhAutomation(BridgeAutomation):
} }
} }
locals {
public_ssh_key = "{{ ssh_public_key_path }}"
private_ssh_key = "{{ ssh_private_key_path }}"
}
{% for resource in destroyed_resources %}
{% set bridge, bridgeconf, account = resource %}
provider "openstack" { provider "openstack" {
auth_url = "https://auth.cloud.ovh.net/v3/" auth_url = "https://auth.cloud.ovh.net/v3/"
domain_name = "Default" # Domain name - Always at 'default' for OVHcloud domain_name = "Default" # Domain name - Always at 'default' for OVHcloud
user_name = "{{ ovh_openstack_user }}" user_name = "{{ account.credentials["ovh_openstack_user"] }}"
password = "{{ ovh_openstack_password }}" password = "{{ account.credentials["ovh_openstack_password"] }}"
tenant_id = "{{ ovh_openstack_tenant_id }}" tenant_id = "{{ account.credentials["ovh_openstack_tenant_id"] }}"
alias = "account_{{ bridge.id }}"
}
{% endfor %}
{% for resource in active_resources %}
{% set bridge, bridgeconf, account = resource %}
provider "openstack" {
auth_url = "https://auth.cloud.ovh.net/v3/"
domain_name = "Default" # Domain name - Always at 'default' for OVHcloud
user_name = "{{ account.credentials["ovh_openstack_user"] }}"
password = "{{ account.credentials["ovh_openstack_password"] }}"
tenant_id = "{{ account.credentials["ovh_openstack_tenant_id"] }}"
alias = "account_{{ bridge.id }}"
} }
provider "ovh" { provider "ovh" {
endpoint = "ovh-eu" endpoint = "ovh-eu"
application_key = "{{ ovh_cloud_application_key }}" application_key = "{{ account.credentials["ovh_cloud_application_key"] }}"
application_secret = "{{ ovh_cloud_application_secret }}" application_secret = "{{ account.credentials["ovh_cloud_application_secret"] }}"
consumer_key = "{{ ovh_cloud_consumer_key }}" consumer_key = "{{ account.credentials["ovh_cloud_consumer_key"] }}"
alias = "account_{{ bridge.id }}"
} }
locals { data "ovh_cloud_project_regions" "regions_{{ bridge.id }}" {
public_ssh_key = file("{{ ssh_public_key_path }}") provider = ovh.account_{{ bridge.id }}
private_ssh_key = file("{{ ssh_private_key_path }}") service_name = "{{ account.credentials["ovh_openstack_tenant_id"] }}"
}
data "ovh_cloud_project_regions" "regions" {
service_name = "{{ ovh_openstack_tenant_id }}"
has_services_up = ["instance"] has_services_up = ["instance"]
} }
{% for group in groups %}
module "label_{{ group.id }}" {
source = "cloudposse/label/null"
version = "0.25.0"
namespace = "{{ global_namespace }}"
tenant = "{{ group.group_name }}"
label_order = ["namespace", "tenant", "name", "attributes"]
}
{% endfor %}
{% for bridgeconf in bridgeconfs %}
{% for bridge in bridgeconf.bridges %}
{% if not bridge.destroyed %}
resource "random_shuffle" "region_{{ bridge.id }}" { resource "random_shuffle" "region_{{ bridge.id }}" {
input = data.ovh_cloud_project_regions.regions.names input = data.ovh_cloud_project_regions.regions_{{ bridge.id }}.names
result_count = 1 result_count = 1
lifecycle { lifecycle {
@ -84,15 +83,18 @@ class BridgeOvhAutomation(BridgeAutomation):
} }
module "bridge_{{ bridge.id }}" { module "bridge_{{ bridge.id }}" {
providers = {
openstack = openstack.account_{{ bridge.id }}
}
source = "{{ terraform_modules_path }}/terraform-openstack-tor-bridge" source = "{{ terraform_modules_path }}/terraform-openstack-tor-bridge"
region = one(random_shuffle.region_{{ bridge.id }}.result) region = one(random_shuffle.region_{{ bridge.id }}.result)
context = module.label_{{ bridgeconf.group.id }}.context namespace = "{{ global_namespace }}"
name = "br" name = "bridge"
attributes = ["{{ bridge.id }}"] attributes = ["{{ bridge.id }}"]
ssh_key = local.public_ssh_key ssh_key = local.public_ssh_key
ssh_private_key = local.private_ssh_key ssh_private_key = local.private_ssh_key
contact_info = "hi" contact_info = "hello from onionland"
distribution_method = "{{ bridge.conf.method }}" distribution_method = "{{ bridgeconf.method }}"
} }
output "bridge_hashed_fingerprint_{{ bridge.id }}" { output "bridge_hashed_fingerprint_{{ bridge.id }}" {
@ -103,7 +105,5 @@ class BridgeOvhAutomation(BridgeAutomation):
value = module.bridge_{{ bridge.id }}.bridgeline value = module.bridge_{{ bridge.id }}.bridgeline
sensitive = true sensitive = true
} }
{% endif %}
{% endfor %}
{% endfor %} {% endfor %}
""" """

View file

@ -43,6 +43,9 @@ class ListAutomation(TerraformAutomation):
in the templating of the Terraform configuration. in the templating of the Terraform configuration.
""" """
provider: str # type: ignore[assignment]
# TODO: remove temporary override
def tf_generate(self) -> None: def tf_generate(self) -> None:
if not self.working_dir: if not self.working_dir:
raise RuntimeError("No working directory specified.") raise RuntimeError("No working directory specified.")

View file

@ -71,6 +71,9 @@ class ProxyAutomation(TerraformAutomation):
The name of the cloud provider used by this proxy provider. Used to determine if this provider can be enabled. The name of the cloud provider used by this proxy provider. Used to determine if this provider can be enabled.
""" """
provider: str # type: ignore[assignment]
# TODO: Temporary override
@abstractmethod @abstractmethod
def import_state(self, state: Any) -> None: def import_state(self, state: Any) -> None:
raise NotImplementedError() raise NotImplementedError()

View file

@ -3,6 +3,7 @@ import subprocess # nosec
from abc import abstractmethod from abc import abstractmethod
from typing import Any, Optional, Tuple from typing import Any, Optional, Tuple
from app.models.cloud import CloudProvider
from app.terraform import BaseAutomation from app.terraform import BaseAutomation
@ -22,7 +23,7 @@ class TerraformAutomation(BaseAutomation):
Default parallelism for remote API calls. Default parallelism for remote API calls.
""" """
provider: str provider: CloudProvider
""" """
Short name for the provider used by this module. Short name for the provider used by this module.
""" """

View file

@ -0,0 +1,64 @@
"""Abstracting cloud providers
Revision ID: 431704bac57a
Revises: 89a74e347d85
Create Date: 2023-01-27 13:32:39.933555
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '431704bac57a'
down_revision = '89a74e347d85'
branch_labels = None
depends_on = None
def upgrade():
op.create_table('cloud_account',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('description', sa.String(length=255), nullable=False),
sa.Column('added', sa.DateTime(), nullable=False),
sa.Column('updated', sa.DateTime(), nullable=False),
sa.Column('destroyed', sa.DateTime(), nullable=True),
sa.Column('provider',
sa.Enum('AWS', 'AZURE', 'BUNNY', 'CLOUDFLARE', 'FASTLY', 'HTTP', 'GANDI', 'GITHUB',
'GITLAB', 'HCLOUD', 'MAXMIND', 'OVH', 'RFC2136', name='cloudprovider'),
nullable=True),
sa.Column('credentials', sa.JSON(), nullable=True),
sa.Column('enabled', sa.Boolean(), nullable=True),
sa.Column('max_distributions', sa.Integer(), nullable=True),
sa.Column('max_sub_distributions', sa.Integer(), nullable=True),
sa.Column('max_instances', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('id', name=op.f('pk_cloud_account'))
)
with op.batch_alter_table('bridge', schema=None) as batch_op:
batch_op.add_column(sa.Column('cloud_account_id', sa.Integer(), nullable=True))
batch_op.create_foreign_key(batch_op.f('fk_bridge_cloud_account_id_cloud_account'), 'cloud_account',
['cloud_account_id'], ['id'])
batch_op.drop_column('provider')
def downgrade():
op.drop_table('bridge') # We can't guess at what the old providers would have been easily
op.create_table('bridge',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('added', sa.DateTime(), nullable=False),
sa.Column('updated', sa.DateTime(), nullable=False),
sa.Column('deprecated', sa.DateTime(), nullable=True),
sa.Column('deprecation_reason', sa.String(), nullable=True),
sa.Column('destroyed', sa.DateTime(), nullable=True),
sa.Column('conf_id', sa.Integer(), nullable=False),
sa.Column('provider', sa.String(), nullable=False),
sa.Column('terraform_updated', sa.DateTime(), nullable=True),
sa.Column('nickname', sa.String(length=255), nullable=True),
sa.Column('fingerprint', sa.String(length=255), nullable=True),
sa.Column('hashed_fingerprint', sa.String(length=255), nullable=True),
sa.Column('bridgeline', sa.String(length=255), nullable=True),
sa.ForeignKeyConstraint(['conf_id'], ['bridge_conf.id'],
name=op.f('fk_bridge_conf_id_bridge_conf')),
sa.PrimaryKeyConstraint('id', name=op.f('pk_bridge'))
)
op.drop_table('cloud_account')