feat(bridges): next generation bridge management
This commit is contained in:
parent
20fad30a06
commit
05285a4ae6
12 changed files with 329 additions and 89 deletions
|
@ -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)
|
||||
|
|
122
app/terraform/bridge/meta.py
Normal file
122
app/terraform/bridge/meta.py
Normal 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, ""
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue