diff --git a/app/cli/automate.py b/app/cli/automate.py index c7b1ea7..b001e64 100644 --- a/app/cli/automate.py +++ b/app/cli/automate.py @@ -19,6 +19,7 @@ from app.terraform.block.bridge_roskomsvoboda import BlockBridgeRoskomsvobodaAut from app.terraform.block_external import BlockExternalAutomation from app.terraform.block_ooni import BlockOONIAutomation from app.terraform.block_roskomsvoboda import BlockRoskomsvobodaAutomation +from app.terraform.bridge.meta import BridgeMetaAutomation from app.terraform.eotk.aws import EotkAWSAutomation from app.terraform.alarms.eotk_aws import AlarmEotkAwsAutomation from app.terraform.alarms.proxy_azure_cdn import AlarmProxyAzureCdnAutomation @@ -56,6 +57,7 @@ jobs = { BridgeAWSAutomation, BridgeGandiAutomation, BridgeHcloudAutomation, + BridgeMetaAutomation, BridgeOvhAutomation, EotkAWSAutomation, ListGithubAutomation, diff --git a/app/models/base.py b/app/models/base.py index 9c94c95..e33b68f 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -11,7 +11,6 @@ class Group(AbstractConfiguration): eotk = db.Column(db.Boolean()) origins = db.relationship("Origin", back_populates="group") - bridgeconfs = db.relationship("BridgeConf", back_populates="group") eotks = db.relationship("Eotk", back_populates="group") onions = db.relationship("Onion", back_populates="group") smart_proxies = db.relationship("SmartProxy", back_populates="group") @@ -39,16 +38,17 @@ class Pool(AbstractConfiguration): api_key = db.Column(db.String(80), nullable=False) redirector_domain = db.Column(db.String(128), nullable=True) + bridgeconfs = db.relationship("BridgeConf", back_populates="pool") + proxies = db.relationship("Proxy", back_populates="pool") + lists = db.relationship("MirrorList", back_populates="pool") + groups = db.relationship("Group", secondary="pool_group", back_populates="pools") + @classmethod def csv_header(cls) -> List[str]: return super().csv_header() + [ "pool_name" ] - proxies = db.relationship("Proxy", back_populates="pool") - lists = db.relationship("MirrorList", back_populates="pool") - groups = db.relationship("Group", secondary="pool_group", back_populates="pools") - @property def brn(self) -> BRN: return BRN( diff --git a/app/models/bridges.py b/app/models/bridges.py index 99717d6..7f97de7 100644 --- a/app/models/bridges.py +++ b/app/models/bridges.py @@ -1,3 +1,4 @@ +import enum from datetime import datetime from typing import List @@ -6,13 +7,20 @@ from app.extensions import db from app.models import AbstractConfiguration, AbstractResource -class BridgeConf(AbstractConfiguration): - group_id = db.Column(db.Integer, db.ForeignKey("group.id"), nullable=False) - provider = db.Column(db.String(20), nullable=False) - method = db.Column(db.String(20), nullable=False) - number = db.Column(db.Integer()) +class ProviderAllocation(enum.Enum): + RANDOM = "random" + COST = "cost" - group = db.relationship("Group", back_populates="bridgeconfs") + +class BridgeConf(AbstractConfiguration): + pool_id = db.Column(db.Integer, db.ForeignKey("pool.id"), nullable=False) + method = db.Column(db.String(20), nullable=False) + target_number = db.Column(db.Integer()) + max_number = db.Column(db.Integer()) + expiry_hours = db.Column(db.Integer()) + provider_allocation = db.Column(db.Enum(ProviderAllocation)) + + pool = db.relationship("Pool", back_populates="bridgeconfs") bridges = db.relationship("Bridge", back_populates="conf") @property @@ -36,12 +44,13 @@ class BridgeConf(AbstractConfiguration): @classmethod def csv_header(cls) -> List[str]: return super().csv_header() + [ - "group_id", "provider", "method", "description", "number" + "pool_id", "provider", "method", "description", "target_number", "max_number", "expiry_hours" ] class Bridge(AbstractResource): conf_id = db.Column(db.Integer, db.ForeignKey("bridge_conf.id"), nullable=False) + provider = db.Column(db.String(), nullable=False) terraform_updated = db.Column(db.DateTime(), nullable=True) nickname = db.Column(db.String(255), nullable=True) fingerprint = db.Column(db.String(255), nullable=True) @@ -53,9 +62,9 @@ class Bridge(AbstractResource): @property def brn(self) -> BRN: return BRN( - group_id=self.conf.group_id, + group_id=0, product="bridge", - provider=self.conf.provider, + provider=self.provider, resource_type="bridge", resource_id=str(self.id) ) diff --git a/app/portal/__init__.py b/app/portal/__init__.py index cdfbe7d..82618fe 100644 --- a/app/portal/__init__.py +++ b/app/portal/__init__.py @@ -44,6 +44,15 @@ portal.register_blueprint(storage, url_prefix="/state") portal.register_blueprint(webhook, url_prefix="/webhook") +@portal.app_template_filter("bridge_expiry") +def calculate_bridge_expiry(b: Bridge) -> str: + expiry = b.deprecated + timedelta(hours=b.conf.expiry_hours) + countdown = expiry - datetime.utcnow() + if countdown.days == 0: + return f"{countdown.seconds // 3600} hours" + return f"{countdown.days} days" + + @portal.app_template_filter("mirror_expiry") def calculate_mirror_expiry(s: datetime) -> str: expiry = s + timedelta(days=3) diff --git a/app/portal/bridge.py b/app/portal/bridge.py index 0acdff8..d4fd278 100644 --- a/app/portal/bridge.py +++ b/app/portal/bridge.py @@ -9,7 +9,6 @@ from app.portal.util import LifecycleForm bp = Blueprint("bridge", __name__) - _SECTION_TEMPLATE_VARS = { "section": "bridge", "help_url": "https://bypass.censorship.guide/user/bridges.html" @@ -45,3 +44,24 @@ def bridge_blocked(bridge_id: int) -> ResponseReturnValue: message=bridge.hashed_fingerprint, form=form, **_SECTION_TEMPLATE_VARS) + + +@bp.route("/expire/", methods=['GET', 'POST']) +def bridge_expire(bridge_id: int) -> ResponseReturnValue: + bridge: Optional[Bridge] = Bridge.query.filter(Bridge.id == bridge_id, Bridge.destroyed.is_(None)).first() + if bridge is None: + return Response(render_template("error.html.j2", + header="404 Proxy Not Found", + message="The requested bridge could not be found.", + **_SECTION_TEMPLATE_VARS)) + form = LifecycleForm() + if form.validate_on_submit(): + bridge.destroy() + db.session.commit() + flash("Bridge will be shortly destroyed.", "success") + return redirect(url_for("portal.bridgeconf.bridgeconf_edit", bridgeconf_id=bridge.conf_id)) + return render_template("lifecycle.html.j2", + header=f"Destroy bridge {bridge.hashed_fingerprint}?", + message=bridge.hashed_fingerprint, + form=form, + **_SECTION_TEMPLATE_VARS) diff --git a/app/portal/bridgeconf.py b/app/portal/bridgeconf.py index 5cef81e..42348fc 100644 --- a/app/portal/bridgeconf.py +++ b/app/portal/bridgeconf.py @@ -9,7 +9,7 @@ from wtforms import SelectField, StringField, IntegerField, SubmitField from wtforms.validators import DataRequired, NumberRange from app.extensions import db -from app.models.base import Group +from app.models.base import Pool from app.models.bridges import BridgeConf from app.portal.util import response_404, view_lifecycle @@ -23,17 +23,34 @@ _SECTION_TEMPLATE_VARS = { class NewBridgeConfForm(FlaskForm): # type: ignore - provider = SelectField('Provider', validators=[DataRequired()]) method = SelectField('Distribution Method', validators=[DataRequired()]) description = StringField('Description') - group = SelectField('Group', validators=[DataRequired()]) - number = IntegerField('Number', validators=[NumberRange(1, message="One or more bridges must be created")]) + pool = SelectField('Pool', validators=[DataRequired()]) + target_number = IntegerField('Target Number', + description="The number of active bridges to deploy (excluding deprecated bridges).", + validators=[NumberRange(1, message="One or more bridges must be created.")]) + max_number = IntegerField('Maximum Number', + description="The maximum number of bridges to deploy (including deprecated bridges).", + validators=[ + NumberRange(1, message="Must be at least 1, ideally greater than target number.")]) + expiry_hours = IntegerField('Expiry Timer (hours)', + description=("The number of hours to wait after a bridge is deprecated before its " + "destruction.")) submit = SubmitField('Save Changes') class EditBridgeConfForm(FlaskForm): # type: ignore description = StringField('Description') - number = IntegerField('Number', validators=[NumberRange(1, message="One or more bridges must be created")]) + target_number = IntegerField('Target Number', + description="The number of active bridges to deploy (excluding deprecated bridges).", + validators=[NumberRange(1, message="One or more bridges must be created.")]) + max_number = IntegerField('Maximum Number', + description="The maximum number of bridges to deploy (including deprecated bridges).", + validators=[ + NumberRange(1, message="Must be at least 1, ideally greater than target number.")]) + expiry_hours = IntegerField('Expiry Timer (hours)', + description=("The number of hours to wait after a bridge is deprecated before its " + "destruction.")) submit = SubmitField('Save Changes') @@ -52,13 +69,7 @@ def bridgeconf_list() -> ResponseReturnValue: @bp.route("/new/", methods=['GET', 'POST']) def bridgeconf_new(group_id: Optional[int] = None) -> ResponseReturnValue: form = NewBridgeConfForm() - form.group.choices = [(x.id, x.group_name) for x in Group.query.all()] - form.provider.choices = [ - ("aws", "AWS Lightsail"), - ("hcloud", "Hetzner Cloud"), - ("ovh", "OVH Public Cloud"), - ("gandi", "GandiCloud VPS") - ] + form.pool.choices = [(x.id, x.pool_name) for x in Pool.query.filter(Pool.destroyed.is_(None)).all()] form.method.choices = [ ("any", "Any (BridgeDB)"), ("email", "E-Mail (BridgeDB)"), @@ -67,18 +78,19 @@ def bridgeconf_new(group_id: Optional[int] = None) -> ResponseReturnValue: ("none", "None (Private)") ] if form.validate_on_submit(): - bridge_conf = BridgeConf() - bridge_conf.group_id = form.group.data - bridge_conf.provider = form.provider.data - bridge_conf.method = form.method.data - bridge_conf.description = form.description.data - bridge_conf.number = form.number.data - bridge_conf.created = datetime.utcnow() - bridge_conf.updated = datetime.utcnow() + bridgeconf = BridgeConf() + bridgeconf.pool_id = form.pool.data + bridgeconf.method = form.method.data + bridgeconf.description = form.description.data + bridgeconf.target_number = form.target_number.data + bridgeconf.max_number = form.max_number.data + bridgeconf.expiry_hours = form.expiry_hours.data + bridgeconf.created = datetime.utcnow() + bridgeconf.updated = datetime.utcnow() try: - db.session.add(bridge_conf) + db.session.add(bridgeconf) db.session.commit() - flash(f"Created new bridge configuration {bridge_conf.id}.", "success") + flash(f"Created new bridge configuration {bridgeconf.id}.", "success") return redirect(url_for("portal.bridgeconf.bridgeconf_list")) except exc.SQLAlchemyError: flash("Failed to create new bridge configuration.", "danger") @@ -100,10 +112,15 @@ def bridgeconf_edit(bridgeconf_id: int) -> ResponseReturnValue: **_SECTION_TEMPLATE_VARS), status=404) form = EditBridgeConfForm(description=bridgeconf.description, - number=bridgeconf.number) + target_number=bridgeconf.target_number, + max_number=bridgeconf.max_number, + expiry_hours=bridgeconf.expiry_hours, + ) if form.validate_on_submit(): bridgeconf.description = form.description.data - bridgeconf.number = form.number.data + bridgeconf.target_number = form.target_number.data + bridgeconf.max_number = form.max_number.data + bridgeconf.expiry_hours = form.expiry_hours.data bridgeconf.updated = datetime.utcnow() try: db.session.commit() diff --git a/app/portal/templates/bridgeconf.html.j2 b/app/portal/templates/bridgeconf.html.j2 index 2de1e81..9b5f24a 100644 --- a/app/portal/templates/bridgeconf.html.j2 +++ b/app/portal/templates/bridgeconf.html.j2 @@ -4,7 +4,7 @@ {% block content %}

Tor Bridge Configuration

-

{{ bridgeconf.group.group_name }}: {{ bridgeconf.provider }}/{{ bridgeconf.method }}

+

{{ bridgeconf.pool.pool_name }}: {{ bridgeconf.method }}

{{ render_form(form) }} diff --git a/app/portal/templates/tables.html.j2 b/app/portal/templates/tables.html.j2 index 9d5860c..06c3d02 100644 --- a/app/portal/templates/tables.html.j2 +++ b/app/portal/templates/tables.html.j2 @@ -453,10 +453,11 @@ - - + - + + + @@ -465,11 +466,12 @@ {% if not bridgeconf.destroyed %} - - + + +
GroupProviderPool Distribution MethodNumberTarget NumberMax NumberExpiry Timer Actions
- {{ bridgeconf.group.group_name }} + {{ bridgeconf.pool.pool_name }} {{ bridgeconf.provider }} {{ bridgeconf.method }}{{ bridgeconf.number }}{{ bridgeconf.target_number }}{{ bridgeconf.max_number }}{{ bridgeconf.expiry_hours }} View/Edit @@ -489,7 +491,7 @@ - + @@ -502,9 +504,9 @@ {% if not bridge.destroyed %} - +
GroupPool Configuration Nickname Hashed Fingerprint
- {{ bridge.conf.group.group_name }} + {{ bridge.conf.pool.pool_name }} {{ bridge.conf.description }} ({{ bridge.conf.provider }}/{{ bridge.conf.method }}){{ bridge.conf.description }} ({{ bridge.provider }}/{{ bridge.conf.method }}) {{ bridge.nickname }} @@ -541,7 +543,9 @@ {% if bridge.deprecated %} Expiring - in {{ bridge.deprecated | mirror_expiry }} + in {{ bridge | bridge_expiry }} + Expire {% else %} Mark blocked diff --git a/app/terraform/bridge/__init__.py b/app/terraform/bridge/__init__.py index 2a78c6f..4c0db78 100644 --- a/app/terraform/bridge/__init__.py +++ b/app/terraform/bridge/__init__.py @@ -1,6 +1,7 @@ import datetime import os -from typing import Iterable, Optional, Any, List +import sys +from typing import Optional, Any, List from app import app from app.extensions import db @@ -21,45 +22,12 @@ class BridgeAutomation(TerraformAutomation): in the templating of the Terraform configuration. """ - def create_missing(self) -> None: - bridgeconfs: Iterable[BridgeConf] = BridgeConf.query.filter( - BridgeConf.provider == self.provider, - BridgeConf.destroyed.is_(None) - ).all() - for bridgeconf in bridgeconfs: - active_bridges = Bridge.query.filter( - Bridge.conf_id == bridgeconf.id, - Bridge.deprecated.is_(None) - ).all() - if len(active_bridges) < bridgeconf.number: - for _idx in range(bridgeconf.number - len(active_bridges)): - bridge = Bridge() - bridge.conf_id = bridgeconf.id - bridge.added = datetime.datetime.utcnow() - bridge.updated = datetime.datetime.utcnow() - db.session.add(bridge) - elif len(active_bridges) > bridgeconf.number: - active_bridge_count = len(active_bridges) - for bridge in active_bridges: - bridge.deprecate(reason="redundant") - active_bridge_count -= 1 - if active_bridge_count == bridgeconf.number: - break - db.session.commit() + max_bridges = sys.maxsize - def destroy_expired(self) -> None: - cutoff = datetime.datetime.utcnow() - datetime.timedelta(days=0) - bridges = [b for b in Bridge.query.filter( - Bridge.destroyed.is_(None), - Bridge.deprecated < cutoff - ).all() if b.conf.provider == self.provider] - for bridge in bridges: - bridge.destroy() - db.session.commit() + # TODO: Only enable providers that have details configured + enabled = True def tf_prehook(self) -> Optional[Any]: # pylint: disable=useless-return - self.create_missing() - self.destroy_expired() return None def tf_generate(self) -> None: @@ -103,3 +71,11 @@ class BridgeAutomation(TerraformAutomation): bridge.bridgeline = " ".join(parts) bridge.terraform_updated = datetime.datetime.utcnow() db.session.commit() + + @classmethod + def active_bridges_count(self) -> int: + active_bridges = Bridge.query.filter( + Bridge.provider == self.provider, + Bridge.destroyed.is_(None), + ).all() + return len(active_bridges) diff --git a/app/terraform/bridge/meta.py b/app/terraform/bridge/meta.py new file mode 100644 index 0000000..0f858e3 --- /dev/null +++ b/app/terraform/bridge/meta.py @@ -0,0 +1,122 @@ +import datetime +import logging +from typing import Tuple, List + +from app import db +from app.models.bridges import BridgeConf, Bridge +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 [ + # In order of cost + BridgeHcloudAutomation, + BridgeGandiAutomation, + BridgeOvhAutomation, + # BridgeAWSAutomation, TODO: This module is broken right now +] if p.enabled} + + +def create_bridges(bridgeconf: BridgeConf, count: int) -> int: + """ + Creates a bridge resource for the given bridge configuration. + """ + 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) + logging.debug("Created %s bridges", created) + return created + + +def deprecate_bridges(bridgeconf: BridgeConf, count: int, reason: str = "redundant") -> int: + logging.debug("Deprecating %s bridges (%s) for configuration %s", count, reason, bridgeconf.id) + deprecated = 0 + active_conf_bridges = iter(Bridge.query.filter( + Bridge.conf_id == bridgeconf.id, + Bridge.deprecated.is_(None), + Bridge.destroyed.is_(None), + ).all()) + while deprecated < count: + logging.debug("Deprecating bridge %s for configuration %s", deprecated + 1, bridgeconf.id) + bridge = next(active_conf_bridges) + logging.debug("Bridge %r", bridge) + bridge.deprecate(reason=reason) + deprecated += 1 + return deprecated + + +class BridgeMetaAutomation(BaseAutomation): + short_name = "bridge_meta" + description = "Housekeeping for bridges" + frequency = 1 + + def automate(self, full: bool = False) -> Tuple[bool, str]: + # Destroy expired bridges + deprecated_bridges: List[Bridge] = Bridge.query.filter( + Bridge.destroyed.is_(None), + Bridge.deprecated.is_not(None), + ).all() + logging.debug("Found %s deprecated bridges", len(deprecated_bridges)) + for bridge in deprecated_bridges: + cutoff = datetime.datetime.utcnow() - datetime.timedelta(hours=bridge.conf.expiry_hours) + if bridge.deprecated < cutoff: + logging.debug("Destroying expired bridge") + bridge.destroy() + # Deprecate orphaned bridges + active_bridges = Bridge.query.filter( + Bridge.deprecated.is_(None), + Bridge.destroyed.is_(None), + ).all() + logging.debug("Found %s active bridges", len(active_bridges)) + for bridge in active_bridges: + if bridge.conf.destroyed is not None: + bridge.deprecate(reason="conf_destroyed") + # Create new bridges + activate_bridgeconfs = BridgeConf.query.filter( + BridgeConf.destroyed.is_(None), + ).all() + logging.debug("Found %s active bridge configurations", len(activate_bridgeconfs)) + for bridgeconf in activate_bridgeconfs: + active_conf_bridges = Bridge.query.filter( + Bridge.conf_id == bridgeconf.id, + Bridge.deprecated.is_(None), + Bridge.destroyed.is_(None), + ).all() + total_conf_bridges = Bridge.query.filter( + Bridge.conf_id == bridgeconf.id, + Bridge.destroyed.is_(None), + ).all() + logging.debug("Generating new bridges for %s (active: %s, total: %s, target: %s, max: %s)", + bridgeconf.id, + len(active_conf_bridges), + len(total_conf_bridges), + bridgeconf.target_number, + bridgeconf.max_number + ) + missing = min( + bridgeconf.target_number - len(active_conf_bridges), + bridgeconf.max_number - len(total_conf_bridges)) + if missing > 0: + create_bridges(bridgeconf, missing) + elif missing < 0: + deprecate_bridges(bridgeconf, 0 - missing) + db.session.commit() + return True, "" diff --git a/app/terraform/proxy/meta.py b/app/terraform/proxy/meta.py index 00da652..6afdcdf 100644 --- a/app/terraform/proxy/meta.py +++ b/app/terraform/proxy/meta.py @@ -23,6 +23,19 @@ PROXY_PROVIDERS = {p.provider: p for p in [ # type: ignore[attr-defined] def create_proxy(pool: Pool, origin: Origin) -> bool: + """ + Creates a web proxy resource for the given origin and pool combination. + + Initially it will attempt to create smart proxies on providers that support smart proxies, + and "simple" proxies on other providers. If other providers have exhausted their quota + already then a "simple" proxy may be created on a platform that supports smart proxies. + + A boolean is returned to indicate whether a proxy resource was created. + + :param pool: pool to create the resource for + :param origin: origin to create the resource for + :return: whether a proxy resource was created + """ for desperate in [False, True]: for provider in PROXY_PROVIDERS.values(): if origin.smart and not provider.smart_proxies: # type: ignore[attr-defined] @@ -31,12 +44,12 @@ def create_proxy(pool: Pool, origin: Origin) -> bool: continue next_subgroup = provider.next_subgroup(origin.group_id) # type: ignore[attr-defined] if next_subgroup is None: - continue + continue # Exceeded maximum number of subgroups and last subgroup is full proxy = Proxy() proxy.pool_id = pool.id proxy.origin_id = origin.id proxy.provider = provider.provider # type: ignore[attr-defined] - proxy.psg = provider.next_subgroup(origin.group_id) # type: ignore[attr-defined] + proxy.psg = next_subgroup # The random usage below is good enough for its purpose: to create a slug that # hasn't been used recently. proxy.slug = tldextract.extract(origin.domain_name).domain[:5] + ''.join( diff --git a/migrations/versions/89a74e347d85_next_generation_bridge_management.py b/migrations/versions/89a74e347d85_next_generation_bridge_management.py new file mode 100644 index 0000000..1bfa0d8 --- /dev/null +++ b/migrations/versions/89a74e347d85_next_generation_bridge_management.py @@ -0,0 +1,68 @@ +"""next generation bridge management + +Revision ID: 89a74e347d85 +Revises: 09b5f4bd75b8 +Create Date: 2023-01-25 12:51:28.620426 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '89a74e347d85' +down_revision = '09b5f4bd75b8' +branch_labels = None +depends_on = None + + +def upgrade(): + op.drop_constraint('fk_bridge_conf_id_bridge_conf', 'bridge') + op.drop_constraint('fk_bridge_conf_group_id_group', 'bridge_conf') + op.drop_constraint('pk_bridge', 'bridge') + op.drop_constraint('pk_bridge_conf', 'bridge_conf') + op.rename_table('bridge', 'original_bridge') + op.rename_table('bridge_conf', 'original_bridge_conf') + op.create_table('bridge_conf', + 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('pool_id', sa.Integer(), nullable=False), + sa.Column('method', sa.String(length=20), nullable=False), + sa.Column('target_number', sa.Integer(), nullable=True), + sa.Column('max_number', sa.Integer(), nullable=True), + sa.Column('expiry_hours', sa.Integer(), nullable=True), + sa.Column('provider_allocation', sa.Enum('RANDOM', 'COST', name='providerallocation'), nullable=True), + sa.ForeignKeyConstraint(['pool_id'], ['pool.id'], name=op.f('fk_bridge_conf_pool_id_pool')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_bridge_conf')) + ) + 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')) + ) + + +def downgrade(): + op.drop_table('bridge') + op.drop_table('bridge_conf') + op.rename_table('original_bridge_conf', 'bridge_conf') + op.create_primary_key('pk_bridge_conf', 'bridge_conf', ['id']) + op.create_foreign_key('fk_bridge_conf_group_id_group', 'bridge_conf', 'group', ['group_id'], ['id']) + op.rename_table('original_bridge', 'bridge') + op.create_primary_key('pk_bridge', 'bridge', ['id']) + op.create_foreign_key('fk_bridge_conf_id_bridge_conf', 'bridge', 'bridge_conf', ['conf_id'], ['id'])