feat: abstracting cloud providers
This commit is contained in:
parent
af36a545a1
commit
0a72aeed96
18 changed files with 629 additions and 181 deletions
|
@ -50,7 +50,7 @@ class BridgeConf(AbstractConfiguration):
|
|||
|
||||
class Bridge(AbstractResource):
|
||||
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)
|
||||
nickname = 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)
|
||||
|
||||
conf = db.relationship("BridgeConf", back_populates="bridges")
|
||||
cloud_account = db.relationship("CloudAccount", back_populates="bridges")
|
||||
|
||||
@property
|
||||
def brn(self) -> BRN:
|
||||
return BRN(
|
||||
group_id=0,
|
||||
product="bridge",
|
||||
provider=self.provider,
|
||||
provider=self.cloud_account.provider.key,
|
||||
resource_type="bridge",
|
||||
resource_id=str(self.id)
|
||||
)
|
||||
|
|
|
@ -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")
|
|
@ -14,6 +14,7 @@ from app.models.bridges import Bridge
|
|||
from app.models.mirrors import Origin, Proxy
|
||||
from app.models.base import Group
|
||||
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.bridgeconf import bp as bridgeconf
|
||||
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(bridgeconf, url_prefix="/bridgeconf")
|
||||
portal.register_blueprint(bridge, url_prefix="/bridge")
|
||||
portal.register_blueprint(cloud, url_prefix="/cloud")
|
||||
portal.register_blueprint(eotk, url_prefix="/eotk")
|
||||
portal.register_blueprint(group, url_prefix="/group")
|
||||
portal.register_blueprint(list_, url_prefix="/list")
|
||||
|
|
|
@ -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)
|
|
@ -81,6 +81,12 @@
|
|||
<span>Configuration</span>
|
||||
</h6>
|
||||
<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">
|
||||
<a class="nav-link{% if section == "pool" %} active{% endif %}"
|
||||
href="{{ url_for("portal.pool.pool_list") }}">
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
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"/>
|
||||
</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" %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-collection"
|
||||
viewBox="0 0 16 16">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% from "tables.html.j2" import alarms_table, automations_table, bridgeconfs_table, bridges_table, eotk_table,
|
||||
groups_table, instances_table, mirrorlists_table, origins_table, origin_onion_table, onions_table, pools_table,
|
||||
proxies_table, webhook_table %}
|
||||
{% from "tables.html.j2" import alarms_table, automations_table, bridgeconfs_table, bridges_table, cloud_accounts_table,
|
||||
eotk_table, groups_table, instances_table, mirrorlists_table, origins_table, origin_onion_table, onions_table,
|
||||
pools_table, proxies_table, webhook_table %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="h2 mt-3">{{ title }}</h1>
|
||||
|
@ -19,6 +19,8 @@
|
|||
{{ bridgeconfs_table(items) }}
|
||||
{% elif item == "bridge" %}
|
||||
{{ bridges_table(items) }}
|
||||
{% elif item == "cloud account" %}
|
||||
{{ cloud_accounts_table(items) }}
|
||||
{% elif item == "eotk" %}
|
||||
{{ eotk_table(items) }}
|
||||
{% elif item == "group" %}
|
||||
|
|
|
@ -185,6 +185,34 @@
|
|||
</div>
|
||||
{% 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) %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
|
@ -491,10 +519,11 @@
|
|||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Pool</th>
|
||||
<th scope="col">Configuration</th>
|
||||
<th scope="col">Nickname</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">Actions</th>
|
||||
</tr>
|
||||
|
@ -503,10 +532,6 @@
|
|||
{% for bridge in bridges %}
|
||||
{% if not bridge.destroyed %}
|
||||
<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>
|
||||
<a href="https://metrics.torproject.org/rs.html#details/{{ bridge.hashed_fingerprint }}">
|
||||
{{ bridge.nickname }}
|
||||
|
@ -515,6 +540,15 @@
|
|||
<td>
|
||||
<code>{{ bridge.hashed_fingerprint }}</code>
|
||||
</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>
|
||||
{% for alarm in bridge.alarms %}
|
||||
<span title="{{ alarm.alarm_type }}">
|
||||
|
|
|
@ -1,14 +1,38 @@
|
|||
import datetime
|
||||
import os
|
||||
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.extensions import db
|
||||
from app.models.bridges import BridgeConf, Bridge
|
||||
from app.models.base import Group
|
||||
from app.models.bridges import Bridge, BridgeConf
|
||||
from app.models.cloud import CloudAccount, CloudProvider
|
||||
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):
|
||||
template: str
|
||||
|
@ -33,11 +57,8 @@ class BridgeAutomation(TerraformAutomation):
|
|||
def tf_generate(self) -> None:
|
||||
self.tf_write(
|
||||
self.template,
|
||||
groups=Group.query.all(),
|
||||
bridgeconfs=BridgeConf.query.filter(
|
||||
BridgeConf.destroyed.is_(None),
|
||||
BridgeConf.provider == self.provider
|
||||
).all(),
|
||||
active_resources=active_bridges_by_provider(self.provider),
|
||||
destroyed_resources=recently_destroyed_bridges_by_provider(self.provider),
|
||||
global_namespace=app.config['GLOBAL_NAMESPACE'],
|
||||
terraform_modules_path=os.path.join(*list(os.path.split(app.root_path))[:-1], 'terraform-modules'),
|
||||
backend_config=f"""backend "http" {{
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
from app.models.cloud import CloudProvider
|
||||
from app.terraform.bridge import BridgeAutomation
|
||||
|
||||
|
||||
class BridgeAWSAutomation(BridgeAutomation):
|
||||
short_name = "bridge_aws"
|
||||
description = "Deploy Tor bridges on AWS Lightsail"
|
||||
provider = "aws"
|
||||
description = "Deploy Tor bridges on AWS EC2"
|
||||
provider = CloudProvider.AWS
|
||||
|
||||
template_parameters = [
|
||||
"aws_access_key",
|
||||
"aws_secret_key",
|
||||
"ssh_public_key_path"
|
||||
"ssh_public_key_path",
|
||||
"ssh_private_key_path",
|
||||
]
|
||||
|
||||
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 {
|
||||
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 %}
|
||||
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"]
|
||||
{% for resource in destroyed_resources %}
|
||||
{% set bridge, bridgeconf, account = resource %}
|
||||
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 }}"
|
||||
}
|
||||
{% endfor %}
|
||||
|
||||
{% for bridgeconf in bridgeconfs %}
|
||||
{% for bridge in bridgeconf.bridges %}
|
||||
{% if not bridge.destroyed %}
|
||||
{% for resource in resources %}
|
||||
{% set bridge, bridgeconf, account = resource %}
|
||||
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 }}" {
|
||||
source = "{{ terraform_modules_path }}/terraform-aws-tor-bridge"
|
||||
ssh_key = local.ssh_key
|
||||
contact_info = "hi"
|
||||
context = module.label_{{ bridgeconf.group.id }}.context
|
||||
name = "br"
|
||||
attributes = ["{{ bridge.id }}"]
|
||||
distribution_method = "{{ bridge.conf.method }}"
|
||||
providers = {
|
||||
aws = aws.account_{{ bridge.id }}
|
||||
}
|
||||
source = "{{ terraform_modules_path }}/terraform-aws-tor-bridge"
|
||||
ssh_public_key = local.ssh_public_key
|
||||
ssh_private_key = local.ssh_private_key
|
||||
contact_info = "hi"
|
||||
namespace = "{{ global_namespace }}"
|
||||
name = "bridge"
|
||||
attributes = ["{{ bridge.id }}"]
|
||||
distribution_method = "{{ bridgeconf.method }}"
|
||||
}
|
||||
|
||||
output "bridge_hashed_fingerprint_{{ bridge.id }}" {
|
||||
|
@ -63,7 +68,5 @@ class BridgeAWSAutomation(BridgeAutomation):
|
|||
value = module.bridge_{{ bridge.id }}.bridgeline
|
||||
sensitive = true
|
||||
}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
"""
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
from app.models.cloud import CloudProvider
|
||||
from app.terraform.bridge import BridgeAutomation
|
||||
|
||||
|
||||
class BridgeGandiAutomation(BridgeAutomation):
|
||||
short_name = "bridge_gandi"
|
||||
description = "Deploy Tor bridges on GandiCloud VPS"
|
||||
provider = "gandi"
|
||||
provider = CloudProvider.GANDI
|
||||
|
||||
template_parameters = [
|
||||
"gandi_openstack_user",
|
||||
"gandi_openstack_password",
|
||||
"gandi_openstack_tenant_name",
|
||||
"ssh_public_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" {
|
||||
auth_url = "https://keystone.sd6.api.gandi.net:5000/v3"
|
||||
user_domain_name = "public"
|
||||
project_domain_name = "public"
|
||||
user_name = "{{ gandi_openstack_user }}"
|
||||
password = "{{ gandi_openstack_password }}"
|
||||
tenant_name = "{{ gandi_openstack_tenant_name }}"
|
||||
user_name = "{{ account.credentials["gandi_openstack_user"] }}"
|
||||
password = "{{ account.credentials["gandi_openstack_password"] }}"
|
||||
tenant_name = "{{ account.credentials["gandi_openstack_tenant_id"] }}"
|
||||
region = "FR-SD6"
|
||||
}
|
||||
|
||||
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"]
|
||||
alias = "account_{{ bridge.id }}"
|
||||
}
|
||||
{% endfor %}
|
||||
|
||||
{% for bridgeconf in bridgeconfs %}
|
||||
{% for bridge in bridgeconf.bridges %}
|
||||
{% if not bridge.destroyed %}
|
||||
{% for resource in active_resources %}
|
||||
{% set bridge, bridgeconf, account = resource %}
|
||||
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 }}" {
|
||||
providers = {
|
||||
openstack = openstack.account_{{ bridge.id }}
|
||||
}
|
||||
source = "{{ terraform_modules_path }}/terraform-openstack-tor-bridge"
|
||||
context = module.label_{{ bridgeconf.group.id }}.context
|
||||
name = "br"
|
||||
namespace = "{{ global_namespace }}"
|
||||
name = "bridge"
|
||||
attributes = ["{{ bridge.id }}"]
|
||||
ssh_key = local.public_ssh_key
|
||||
ssh_private_key = local.private_ssh_key
|
||||
contact_info = "hi"
|
||||
distribution_method = "{{ bridge.conf.method }}"
|
||||
contact_info = "did not write the code to populate yet"
|
||||
distribution_method = "{{ bridgeconf.method }}"
|
||||
|
||||
image_name = "Debian 11 Bullseye"
|
||||
flavor_name = "V-R1"
|
||||
|
@ -77,7 +82,5 @@ class BridgeGandiAutomation(BridgeAutomation):
|
|||
value = module.bridge_{{ bridge.id }}.bridgeline
|
||||
sensitive = true
|
||||
}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
"""
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
from app.models.cloud import CloudProvider
|
||||
from app.terraform.bridge import BridgeAutomation
|
||||
|
||||
|
||||
class BridgeHcloudAutomation(BridgeAutomation):
|
||||
short_name = "bridge_hcloud"
|
||||
description = "Deploy Tor bridges on Hetzner Cloud"
|
||||
provider = "hcloud"
|
||||
provider = CloudProvider.HCLOUD
|
||||
|
||||
template_parameters = [
|
||||
"hcloud_token",
|
||||
"ssh_private_key_path"
|
||||
"ssh_private_key_path",
|
||||
"ssh_public_key_path"
|
||||
]
|
||||
|
||||
template = """
|
||||
|
@ -26,36 +27,36 @@ class BridgeHcloudAutomation(BridgeAutomation):
|
|||
}
|
||||
}
|
||||
|
||||
provider "hcloud" {
|
||||
token = "{{ hcloud_token }}"
|
||||
}
|
||||
|
||||
locals {
|
||||
ssh_private_key = file("{{ ssh_private_key_path }}")
|
||||
ssh_private_key = "{{ ssh_private_key_path }}"
|
||||
}
|
||||
|
||||
data "hcloud_datacenters" "ds" {
|
||||
}
|
||||
|
||||
data "hcloud_server_type" "cx11" {
|
||||
name = "cx11"
|
||||
}
|
||||
|
||||
{% 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"]
|
||||
{% for resource in destroyed_resources %}
|
||||
{% set bridge, bridgeconf, account = resource %}
|
||||
provider "hcloud" {
|
||||
token = "{{ account.credentials["hcloud_token"] }}"
|
||||
alias = "account_{{ bridge.id }}"
|
||||
}
|
||||
{% endfor %}
|
||||
|
||||
{% for bridgeconf in bridgeconfs %}
|
||||
{% for bridge in bridgeconf.bridges %}
|
||||
{% if not bridge.destroyed %}
|
||||
{% for resource in active_resources %}
|
||||
{% set bridge, bridgeconf, account = resource %}
|
||||
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 }}" {
|
||||
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
|
||||
|
||||
lifecycle {
|
||||
|
@ -64,15 +65,17 @@ class BridgeHcloudAutomation(BridgeAutomation):
|
|||
}
|
||||
|
||||
module "bridge_{{ bridge.id }}" {
|
||||
providers = {
|
||||
hcloud = hcloud.account_{{ bridge.id }}
|
||||
}
|
||||
source = "{{ terraform_modules_path }}/terraform-hcloud-tor-bridge"
|
||||
datacenter = one(random_shuffle.datacenter_{{ bridge.id }}.result)
|
||||
context = module.label_{{ bridgeconf.group.id }}.context
|
||||
name = "br"
|
||||
namespace = "{{ global_namespace }}"
|
||||
name = "bridge"
|
||||
attributes = ["{{ bridge.id }}"]
|
||||
ssh_key_name = "bc"
|
||||
ssh_private_key = local.ssh_private_key
|
||||
contact_info = "hi"
|
||||
distribution_method = "{{ bridge.conf.method }}"
|
||||
contact_info = "this used to be sanitised and I did not write the code to populate it yet"
|
||||
distribution_method = "{{ bridgeconf.method }}"
|
||||
}
|
||||
|
||||
output "bridge_hashed_fingerprint_{{ bridge.id }}" {
|
||||
|
@ -83,7 +86,5 @@ class BridgeHcloudAutomation(BridgeAutomation):
|
|||
value = module.bridge_{{ bridge.id }}.bridgeline
|
||||
sensitive = true
|
||||
}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
"""
|
||||
|
|
|
@ -4,18 +4,40 @@ from typing import Tuple, List
|
|||
|
||||
from app import db
|
||||
from app.models.bridges import BridgeConf, Bridge
|
||||
from app.models.cloud import CloudProvider, CloudAccount
|
||||
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
|
||||
BridgeHcloudAutomation,
|
||||
BridgeGandiAutomation,
|
||||
BridgeOvhAutomation,
|
||||
# BridgeAWSAutomation, TODO: This module is broken right now
|
||||
] if p.enabled}
|
||||
CloudProvider.HCLOUD,
|
||||
CloudProvider.GANDI,
|
||||
CloudProvider.OVH,
|
||||
CloudProvider.AWS,
|
||||
]
|
||||
|
||||
|
||||
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:
|
||||
|
@ -24,24 +46,17 @@ def create_bridges(bridgeconf: BridgeConf, count: int) -> int:
|
|||
"""
|
||||
logging.debug("Creating %s bridges for configuration %s", count, bridgeconf.id)
|
||||
created = 0
|
||||
# TODO: deal with the fact that I created a dictionary and then forgot it wasn't ordered
|
||||
while created < count:
|
||||
for provider in BRIDGE_PROVIDERS:
|
||||
if BRIDGE_PROVIDERS[provider].max_bridges > BRIDGE_PROVIDERS[provider].active_bridges_count():
|
||||
logging.debug("Creating bridge for configuration %s with provider %s", bridgeconf.id, provider)
|
||||
bridge = Bridge()
|
||||
bridge.pool_id = bridgeconf.pool.id
|
||||
bridge.conf_id = bridgeconf.id
|
||||
bridge.provider = provider
|
||||
bridge.added = datetime.datetime.utcnow()
|
||||
bridge.updated = datetime.datetime.utcnow()
|
||||
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)
|
||||
for provider in BRIDGE_PROVIDERS:
|
||||
if created >= count:
|
||||
break
|
||||
logging.info("Creating bridges in %s accounts", provider.description)
|
||||
for account in CloudAccount.query.filter(
|
||||
CloudAccount.destroyed.is_(None),
|
||||
CloudAccount.enabled.is_(True),
|
||||
CloudAccount.provider == provider,
|
||||
):
|
||||
logging.info("Creating bridges in %s", account)
|
||||
created += create_bridges_in_account(bridgeconf, account, count - created)
|
||||
logging.debug("Created %s bridges", created)
|
||||
return created
|
||||
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
from app.models.cloud import CloudProvider
|
||||
from app.terraform.bridge import BridgeAutomation
|
||||
|
||||
|
||||
class BridgeOvhAutomation(BridgeAutomation):
|
||||
short_name = "bridge_ovh"
|
||||
description = "Deploy Tor bridges on OVH Public Cloud"
|
||||
provider = "ovh"
|
||||
provider = CloudProvider.OVH
|
||||
|
||||
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_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" {
|
||||
auth_url = "https://auth.cloud.ovh.net/v3/"
|
||||
domain_name = "Default" # Domain name - Always at 'default' for OVHcloud
|
||||
user_name = "{{ ovh_openstack_user }}"
|
||||
password = "{{ ovh_openstack_password }}"
|
||||
tenant_id = "{{ ovh_openstack_tenant_id }}"
|
||||
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 }}"
|
||||
}
|
||||
{% 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" {
|
||||
endpoint = "ovh-eu"
|
||||
application_key = "{{ ovh_cloud_application_key }}"
|
||||
application_secret = "{{ ovh_cloud_application_secret }}"
|
||||
consumer_key = "{{ ovh_cloud_consumer_key }}"
|
||||
application_key = "{{ account.credentials["ovh_cloud_application_key"] }}"
|
||||
application_secret = "{{ account.credentials["ovh_cloud_application_secret"] }}"
|
||||
consumer_key = "{{ account.credentials["ovh_cloud_consumer_key"] }}"
|
||||
alias = "account_{{ bridge.id }}"
|
||||
}
|
||||
|
||||
locals {
|
||||
public_ssh_key = file("{{ ssh_public_key_path }}")
|
||||
private_ssh_key = file("{{ ssh_private_key_path }}")
|
||||
}
|
||||
|
||||
data "ovh_cloud_project_regions" "regions" {
|
||||
service_name = "{{ ovh_openstack_tenant_id }}"
|
||||
data "ovh_cloud_project_regions" "regions_{{ bridge.id }}" {
|
||||
provider = ovh.account_{{ bridge.id }}
|
||||
service_name = "{{ account.credentials["ovh_openstack_tenant_id"] }}"
|
||||
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 }}" {
|
||||
input = data.ovh_cloud_project_regions.regions.names
|
||||
input = data.ovh_cloud_project_regions.regions_{{ bridge.id }}.names
|
||||
result_count = 1
|
||||
|
||||
lifecycle {
|
||||
|
@ -84,15 +83,18 @@ class BridgeOvhAutomation(BridgeAutomation):
|
|||
}
|
||||
|
||||
module "bridge_{{ bridge.id }}" {
|
||||
providers = {
|
||||
openstack = openstack.account_{{ bridge.id }}
|
||||
}
|
||||
source = "{{ terraform_modules_path }}/terraform-openstack-tor-bridge"
|
||||
region = one(random_shuffle.region_{{ bridge.id }}.result)
|
||||
context = module.label_{{ bridgeconf.group.id }}.context
|
||||
name = "br"
|
||||
namespace = "{{ global_namespace }}"
|
||||
name = "bridge"
|
||||
attributes = ["{{ bridge.id }}"]
|
||||
ssh_key = local.public_ssh_key
|
||||
ssh_private_key = local.private_ssh_key
|
||||
contact_info = "hi"
|
||||
distribution_method = "{{ bridge.conf.method }}"
|
||||
contact_info = "hello from onionland"
|
||||
distribution_method = "{{ bridgeconf.method }}"
|
||||
}
|
||||
|
||||
output "bridge_hashed_fingerprint_{{ bridge.id }}" {
|
||||
|
@ -103,7 +105,5 @@ class BridgeOvhAutomation(BridgeAutomation):
|
|||
value = module.bridge_{{ bridge.id }}.bridgeline
|
||||
sensitive = true
|
||||
}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
"""
|
||||
|
|
|
@ -43,6 +43,9 @@ class ListAutomation(TerraformAutomation):
|
|||
in the templating of the Terraform configuration.
|
||||
"""
|
||||
|
||||
provider: str # type: ignore[assignment]
|
||||
# TODO: remove temporary override
|
||||
|
||||
def tf_generate(self) -> None:
|
||||
if not self.working_dir:
|
||||
raise RuntimeError("No working directory specified.")
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
||||
provider: str # type: ignore[assignment]
|
||||
# TODO: Temporary override
|
||||
|
||||
@abstractmethod
|
||||
def import_state(self, state: Any) -> None:
|
||||
raise NotImplementedError()
|
||||
|
|
|
@ -3,6 +3,7 @@ import subprocess # nosec
|
|||
from abc import abstractmethod
|
||||
from typing import Any, Optional, Tuple
|
||||
|
||||
from app.models.cloud import CloudProvider
|
||||
from app.terraform import BaseAutomation
|
||||
|
||||
|
||||
|
@ -22,7 +23,7 @@ class TerraformAutomation(BaseAutomation):
|
|||
Default parallelism for remote API calls.
|
||||
"""
|
||||
|
||||
provider: str
|
||||
provider: CloudProvider
|
||||
"""
|
||||
Short name for the provider used by this module.
|
||||
"""
|
||||
|
|
|
@ -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')
|
Loading…
Add table
Add a link
Reference in a new issue