feat(bridges): next generation bridge management

This commit is contained in:
Iain Learmonth 2023-01-26 15:42:25 +00:00
parent 20fad30a06
commit 05285a4ae6
12 changed files with 329 additions and 89 deletions

View file

@ -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)

View file

@ -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, ""

View file

@ -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(