feat: switch all timezone naive datetimes to timezone aware

This commit is contained in:
Iain Learmonth 2024-12-06 16:08:48 +00:00
parent 41fc0a73a5
commit e22abb383c
30 changed files with 322 additions and 226 deletions

View file

@ -1,5 +1,5 @@
import datetime from datetime import datetime, timezone
from typing import Optional, List from typing import List, Optional
from app.brm.brn import BRN from app.brm.brn import BRN
from app.extensions import db from app.extensions import db
@ -25,8 +25,8 @@ def _get_alarm(target: BRN,
alarm.aspect = aspect alarm.aspect = aspect
alarm.target = target_str alarm.target = target_str
alarm.text = "New alarm" alarm.text = "New alarm"
alarm.state_changed = datetime.datetime.utcnow() alarm.state_changed = datetime.now(tz=timezone.utc)
alarm.last_updated = datetime.datetime.utcnow() alarm.last_updated = datetime.now(tz=timezone.utc)
db.session.add(alarm) db.session.add(alarm)
return alarm return alarm

View file

@ -1,44 +1,48 @@
import datetime
import logging import logging
import os import os
import shutil import shutil
import tempfile import tempfile
from datetime import datetime, timedelta, timezone
from traceback import TracebackException from traceback import TracebackException
from typing import Type from typing import Type
from app import app from app import app
from app.cli import _SubparserType, BaseCliHandler from app.cli import BaseCliHandler, _SubparserType
from app.extensions import db from app.extensions import db
from app.models.activity import Activity from app.models.activity import Activity
from app.models.automation import Automation, AutomationState, AutomationLogs from app.models.automation import Automation, AutomationLogs, AutomationState
from app.terraform import BaseAutomation from app.terraform import BaseAutomation
from app.terraform.alarms.eotk_aws import AlarmEotkAwsAutomation
from app.terraform.alarms.proxy_azure_cdn import AlarmProxyAzureCdnAutomation
from app.terraform.alarms.proxy_cloudfront import \
AlarmProxyCloudfrontAutomation
from app.terraform.alarms.proxy_http_status import \
AlarmProxyHTTPStatusAutomation
from app.terraform.alarms.smart_aws import AlarmSmartAwsAutomation
from app.terraform.block.block_blocky import BlockBlockyAutomation from app.terraform.block.block_blocky import BlockBlockyAutomation
from app.terraform.block.block_scriptzteam import \
BlockBridgeScriptzteamAutomation
from app.terraform.block.bridge_github import BlockBridgeGitHubAutomation from app.terraform.block.bridge_github import BlockBridgeGitHubAutomation
from app.terraform.block.bridge_gitlab import BlockBridgeGitlabAutomation from app.terraform.block.bridge_gitlab import BlockBridgeGitlabAutomation
from app.terraform.block.bridge_roskomsvoboda import BlockBridgeRoskomsvobodaAutomation from app.terraform.block.bridge_roskomsvoboda import \
from app.terraform.block.block_scriptzteam import BlockBridgeScriptzteamAutomation BlockBridgeRoskomsvobodaAutomation
from app.terraform.block_external import BlockExternalAutomation from app.terraform.block_external import BlockExternalAutomation
from app.terraform.block_ooni import BlockOONIAutomation from app.terraform.block_ooni import BlockOONIAutomation
from app.terraform.block_roskomsvoboda import BlockRoskomsvobodaAutomation 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
from app.terraform.alarms.proxy_cloudfront import AlarmProxyCloudfrontAutomation
from app.terraform.alarms.proxy_http_status import AlarmProxyHTTPStatusAutomation
from app.terraform.alarms.smart_aws import AlarmSmartAwsAutomation
from app.terraform.bridge.aws import BridgeAWSAutomation from app.terraform.bridge.aws import BridgeAWSAutomation
from app.terraform.bridge.gandi import BridgeGandiAutomation from app.terraform.bridge.gandi import BridgeGandiAutomation
from app.terraform.bridge.hcloud import BridgeHcloudAutomation from app.terraform.bridge.hcloud import BridgeHcloudAutomation
from app.terraform.bridge.meta import BridgeMetaAutomation
from app.terraform.bridge.ovh import BridgeOvhAutomation from app.terraform.bridge.ovh import BridgeOvhAutomation
from app.terraform.eotk.aws import EotkAWSAutomation
from app.terraform.list.github import ListGithubAutomation from app.terraform.list.github import ListGithubAutomation
from app.terraform.list.gitlab import ListGitlabAutomation from app.terraform.list.gitlab import ListGitlabAutomation
from app.terraform.list.http_post import ListHttpPostAutomation from app.terraform.list.http_post import ListHttpPostAutomation
from app.terraform.list.s3 import ListS3Automation from app.terraform.list.s3 import ListS3Automation
from app.terraform.proxy.meta import ProxyMetaAutomation
from app.terraform.proxy.azure_cdn import ProxyAzureCdnAutomation from app.terraform.proxy.azure_cdn import ProxyAzureCdnAutomation
from app.terraform.proxy.cloudfront import ProxyCloudfrontAutomation from app.terraform.proxy.cloudfront import ProxyCloudfrontAutomation
from app.terraform.proxy.fastly import ProxyFastlyAutomation from app.terraform.proxy.fastly import ProxyFastlyAutomation
from app.terraform.proxy.meta import ProxyMetaAutomation
from app.terraform.static.aws import StaticAWSAutomation from app.terraform.static.aws import StaticAWSAutomation
from app.terraform.static.meta import StaticMetaAutomation from app.terraform.static.meta import StaticMetaAutomation
@ -108,7 +112,7 @@ def run_job(job_cls: Type[BaseAutomation], *,
automation.description = job_cls.description automation.description = job_cls.description
automation.enabled = False automation.enabled = False
automation.next_is_full = False automation.next_is_full = False
automation.added = datetime.datetime.utcnow() automation.added = datetime.now(tz=timezone.utc)
automation.updated = automation.added automation.updated = automation.added
db.session.add(automation) db.session.add(automation)
db.session.commit() db.session.commit()
@ -117,7 +121,7 @@ def run_job(job_cls: Type[BaseAutomation], *,
logging.warning("Not running an already running automation") logging.warning("Not running an already running automation")
return return
if not ignore_schedule and not force: if not ignore_schedule and not force:
if automation.next_run is not None and automation.next_run > datetime.datetime.utcnow(): if automation.next_run is not None and automation.next_run > datetime.now(tz=timezone.utc):
logging.warning("Not time to run this job yet") logging.warning("Not time to run this job yet")
return return
if not automation.enabled and not force: if not automation.enabled and not force:
@ -145,7 +149,7 @@ def run_job(job_cls: Type[BaseAutomation], *,
logs = "\n".join(trace.format()) logs = "\n".join(trace.format())
if job is not None and success: if job is not None and success:
automation.state = AutomationState.IDLE automation.state = AutomationState.IDLE
automation.next_run = datetime.datetime.utcnow() + datetime.timedelta( automation.next_run = datetime.now(tz=timezone.utc) + timedelta(
minutes=getattr(job, "frequency", 7)) minutes=getattr(job, "frequency", 7))
if 'TERRAFORM_DIRECTORY' not in app.config and working_dir is not None: if 'TERRAFORM_DIRECTORY' not in app.config and working_dir is not None:
# We used a temporary working directory # We used a temporary working directory
@ -168,8 +172,8 @@ def run_job(job_cls: Type[BaseAutomation], *,
automation.next_run = None automation.next_run = None
log = AutomationLogs() log = AutomationLogs()
log.automation_id = automation.id log.automation_id = automation.id
log.added = datetime.datetime.utcnow() log.added = datetime.now(tz=timezone.utc)
log.updated = datetime.datetime.utcnow() log.updated = datetime.now(tz=timezone.utc)
log.logs = str(logs) log.logs = str(logs)
db.session.add(log) db.session.add(log)
db.session.commit() db.session.commit()
@ -182,7 +186,7 @@ def run_job(job_cls: Type[BaseAutomation], *,
) )
db.session.add(activity) db.session.add(activity)
activity.notify() # Notify before commit because the failure occurred even if we can't commit. activity.notify() # Notify before commit because the failure occurred even if we can't commit.
automation.last_run = datetime.datetime.utcnow() automation.last_run = datetime.now(tz=timezone.utc)
db.session.commit() db.session.commit()

View file

@ -1,6 +1,7 @@
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from typing import Dict, List, Optional, TypedDict from typing import Dict, List, Optional, TypedDict
from flask import current_app from flask import current_app
from sqlalchemy import or_ from sqlalchemy import or_
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@ -8,7 +9,7 @@ from tldextract import extract
from app.extensions import db from app.extensions import db
from app.models.base import Group, Pool from app.models.base import Group, Pool
from app.models.mirrors import Proxy, Origin from app.models.mirrors import Origin, Proxy
class MirrorMappingMirror(TypedDict): class MirrorMappingMirror(TypedDict):
@ -29,7 +30,7 @@ class MirrorMapping(TypedDict):
def mirror_mapping(_: Optional[Pool]) -> MirrorMapping: def mirror_mapping(_: Optional[Pool]) -> MirrorMapping:
two_days_ago = datetime.utcnow() - timedelta(days=2) two_days_ago = datetime.now(tz=timezone.utc) - timedelta(days=2)
proxies = ( proxies = (
db.session.query(Proxy) db.session.query(Proxy)

View file

@ -1,7 +1,7 @@
import logging import logging
from abc import abstractmethod from abc import abstractmethod
from datetime import datetime from datetime import datetime, timezone
from typing import Union, List, Optional, Any, Dict from typing import Any, Dict, List, Optional, Union
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
@ -14,9 +14,9 @@ class AbstractConfiguration(db.Model): # type: ignore
id: Mapped[int] = mapped_column(db.Integer, primary_key=True) id: Mapped[int] = mapped_column(db.Integer, primary_key=True)
description: Mapped[str] description: Mapped[str]
added: Mapped[datetime] added: Mapped[datetime] = mapped_column(db.DateTime(timezone=True))
updated: Mapped[datetime] updated: Mapped[datetime] = mapped_column(db.DateTime(timezone=True))
destroyed: Mapped[Optional[datetime]] destroyed: Mapped[Optional[datetime]] = mapped_column(db.DateTime(timezone=True), nullable=True)
@property @property
@abstractmethod @abstractmethod
@ -24,8 +24,8 @@ class AbstractConfiguration(db.Model): # type: ignore
raise NotImplementedError() raise NotImplementedError()
def destroy(self) -> None: def destroy(self) -> None:
self.destroyed = datetime.utcnow() self.destroyed = datetime.now(tz=timezone.utc)
self.updated = datetime.utcnow() self.updated = datetime.now(tz=timezone.utc)
@classmethod @classmethod
def csv_header(cls) -> List[str]: def csv_header(cls) -> List[str]:
@ -41,11 +41,11 @@ class AbstractConfiguration(db.Model): # type: ignore
class Deprecation(db.Model): # type: ignore[name-defined,misc] class Deprecation(db.Model): # type: ignore[name-defined,misc]
id: Mapped[int] = mapped_column(db.Integer, primary_key=True) id: Mapped[int] = mapped_column(db.Integer, primary_key=True)
resource_type: Mapped[str] = mapped_column(db.String(50)) resource_type: Mapped[str]
resource_id: Mapped[int] = mapped_column(db.Integer) resource_id: Mapped[int]
deprecated_at: Mapped[datetime] = mapped_column(db.DateTime(), default=datetime.utcnow, nullable=False) deprecated_at: Mapped[datetime] = mapped_column(db.DateTime(timezone=True))
meta: Mapped[Optional[Dict[str, Any]]] = mapped_column(db.JSON()) meta: Mapped[Optional[Dict[str, Any]]] = mapped_column(db.JSON())
reason: Mapped[str] = mapped_column(db.String(), nullable=False) reason: Mapped[str]
@property @property
def resource(self) -> "AbstractResource": def resource(self) -> "AbstractResource":
@ -58,11 +58,11 @@ class AbstractResource(db.Model): # type: ignore
__abstract__ = True __abstract__ = True
id: Mapped[int] = mapped_column(db.Integer, primary_key=True) id: Mapped[int] = mapped_column(db.Integer, primary_key=True)
added: Mapped[datetime] = mapped_column(db.DateTime(), default=datetime.utcnow) added: Mapped[datetime] = mapped_column(db.DateTime(timezone=True))
updated: Mapped[datetime] = mapped_column(db.DateTime(), default=datetime.utcnow) updated: Mapped[datetime] = mapped_column(db.DateTime(timezone=True))
deprecated: Mapped[Optional[datetime]] deprecated: Mapped[Optional[datetime]] = mapped_column(db.DateTime(timezone=True), nullable=True)
deprecation_reason: Mapped[Optional[str]] deprecation_reason: Mapped[Optional[str]]
destroyed: Mapped[Optional[datetime]] destroyed: Mapped[Optional[datetime]] = mapped_column(db.DateTime(timezone=True), nullable=True)
def __init__(self, *, def __init__(self, *,
id: Optional[int] = None, id: Optional[int] = None,
@ -73,9 +73,9 @@ class AbstractResource(db.Model): # type: ignore
destroyed: Optional[datetime] = None, destroyed: Optional[datetime] = None,
**kwargs: Any) -> None: **kwargs: Any) -> None:
if added is None: if added is None:
added = datetime.utcnow() added = datetime.now(tz=timezone.utc)
if updated is None: if updated is None:
updated = datetime.utcnow() updated = datetime.now(tz=timezone.utc)
super().__init__(id=id, super().__init__(id=id,
added=added, added=added,
updated=updated, updated=updated,
@ -101,9 +101,9 @@ class AbstractResource(db.Model): # type: ignore
""" """
if self.deprecated is None: if self.deprecated is None:
logging.info("Deprecating %s (reason=%s)", self.brn, reason) logging.info("Deprecating %s (reason=%s)", self.brn, reason)
self.deprecated = datetime.utcnow() self.deprecated = datetime.now(tz=timezone.utc)
self.deprecation_reason = reason self.deprecation_reason = reason
self.updated = datetime.utcnow() self.updated = datetime.now(tz=timezone.utc)
if reason not in [d.reason for d in self.deprecations]: if reason not in [d.reason for d in self.deprecations]:
new_deprecation = Deprecation( new_deprecation = Deprecation(
resource_type=type(self).__name__, resource_type=type(self).__name__,
@ -132,8 +132,8 @@ class AbstractResource(db.Model): # type: ignore
""" """
if self.deprecated is None: if self.deprecated is None:
self.deprecate(reason="destroyed") self.deprecate(reason="destroyed")
self.destroyed = datetime.utcnow() self.destroyed = datetime.now(tz=timezone.utc)
self.updated = datetime.utcnow() self.updated = datetime.now(tz=timezone.utc)
@classmethod @classmethod
def csv_header(cls) -> List[str]: def csv_header(cls) -> List[str]:

View file

@ -1,39 +1,40 @@
import datetime from datetime import datetime, timezone
from typing import Any, Optional from typing import Any, Optional
import requests import requests
from sqlalchemy.orm import Mapped, mapped_column
from app.brm.brn import BRN from app.brm.brn import BRN
from app.models import AbstractConfiguration
from app.extensions import db from app.extensions import db
from app.models import AbstractConfiguration
class Activity(db.Model): # type: ignore class Activity(db.Model): # type: ignore
id = db.Column(db.Integer(), primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
group_id = db.Column(db.Integer(), nullable=True) group_id: Mapped[Optional[int]]
activity_type = db.Column(db.String(20), nullable=False) activity_type: Mapped[str]
text = db.Column(db.Text(), nullable=False) text: Mapped[str]
added = db.Column(db.DateTime(), nullable=False) added: Mapped[datetime] = mapped_column(db.DateTime(timezone=True))
def __init__(self, *, def __init__(self, *,
id: Optional[int] = None, id: Optional[int] = None,
group_id: Optional[int] = None, group_id: Optional[int] = None,
activity_type: str, activity_type: str,
text: str, text: str,
added: Optional[datetime.datetime] = None, added: Optional[datetime] = None,
**kwargs: Any) -> None: **kwargs: Any) -> None:
if not isinstance(activity_type, str) or len(activity_type) > 20 or activity_type == "": if not isinstance(activity_type, str) or len(activity_type) > 20 or activity_type == "":
raise TypeError("expected string for activity type between 1 and 20 characters") raise TypeError("expected string for activity type between 1 and 20 characters")
if not isinstance(text, str): if not isinstance(text, str):
raise TypeError("expected string for text") raise TypeError("expected string for text")
if added is None:
added = datetime.now(tz=timezone.utc)
super().__init__(id=id, super().__init__(id=id,
group_id=group_id, group_id=group_id,
activity_type=activity_type, activity_type=activity_type,
text=text, text=text,
added=added, added=added,
**kwargs) **kwargs)
if self.added is None:
self.added = datetime.datetime.utcnow()
def notify(self) -> int: def notify(self) -> int:
count = 0 count = 0
@ -47,8 +48,8 @@ class Activity(db.Model): # type: ignore
class Webhook(AbstractConfiguration): class Webhook(AbstractConfiguration):
format = db.Column(db.String(20)) format: Mapped[str]
url = db.Column(db.String(255)) url: Mapped[str]
@property @property
def brn(self) -> BRN: def brn(self) -> BRN:

View file

@ -1,6 +1,8 @@
import enum import enum
from datetime import datetime from datetime import datetime, timezone
from typing import List, Any from typing import Any, List
from sqlalchemy.orm import Mapped, mapped_column
from app.extensions import db from app.extensions import db
from app.models.activity import Activity from app.models.activity import Activity
@ -24,13 +26,13 @@ class AlarmState(enum.Enum):
class Alarm(db.Model): # type: ignore class Alarm(db.Model): # type: ignore
id = db.Column(db.Integer, primary_key=True) id: Mapped[int] = mapped_column(db.Integer, primary_key=True)
target = db.Column(db.String(255), nullable=False) target: Mapped[str]
aspect = db.Column(db.String(255), nullable=False) aspect: Mapped[str]
alarm_state = db.Column(db.Enum(AlarmState), default=AlarmState.UNKNOWN, nullable=False) alarm_state: Mapped[AlarmState] = mapped_column(default=AlarmState.UNKNOWN)
state_changed = db.Column(db.DateTime(), nullable=False) state_changed: Mapped[datetime] = mapped_column(db.DateTime(timezone=True))
last_updated = db.Column(db.DateTime(), nullable=False) last_updated: Mapped[datetime] = mapped_column(db.DateTime(timezone=True))
text = db.Column(db.String(255), nullable=False) text: Mapped[str]
@classmethod @classmethod
def csv_header(cls) -> List[str]: def csv_header(cls) -> List[str]:
@ -40,11 +42,8 @@ class Alarm(db.Model): # type: ignore
return [getattr(self, x) for x in self.csv_header()] return [getattr(self, x) for x in self.csv_header()]
def update_state(self, state: AlarmState, text: str) -> None: def update_state(self, state: AlarmState, text: str) -> None:
if self.alarm_state is None:
self.alarm_state = AlarmState.UNKNOWN
if self.alarm_state != state or self.state_changed is None: if self.alarm_state != state or self.state_changed is None:
self.state_changed = datetime.utcnow() self.state_changed = datetime.now(tz=timezone.utc)
activity = Activity(activity_type="alarm_state", activity = Activity(activity_type="alarm_state",
text=f"[{self.aspect}] {state.emoji} Alarm state changed from " text=f"[{self.aspect}] {state.emoji} Alarm state changed from "
f"{self.alarm_state.name} to {state.name} on {self.target}: {text}.") f"{self.alarm_state.name} to {state.name} on {self.target}: {text}.")
@ -56,4 +55,4 @@ class Alarm(db.Model): # type: ignore
db.session.add(activity) db.session.add(activity)
self.alarm_state = state self.alarm_state = state
self.text = text self.text = text
self.last_updated = datetime.utcnow() self.last_updated = datetime.now(tz=timezone.utc)

View file

@ -1,5 +1,8 @@
import datetime
import enum import enum
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy.orm import Mapped, mapped_column
from app.brm.brn import BRN from app.brm.brn import BRN
from app.extensions import db from app.extensions import db
@ -13,12 +16,12 @@ class AutomationState(enum.Enum):
class Automation(AbstractConfiguration): class Automation(AbstractConfiguration):
short_name = db.Column(db.String(25), nullable=False) short_name: Mapped[str]
state = db.Column(db.Enum(AutomationState), default=AutomationState.IDLE, nullable=False) state: Mapped[AutomationState] = mapped_column(default=AutomationState.IDLE)
enabled = db.Column(db.Boolean, nullable=False) enabled: Mapped[bool]
last_run = db.Column(db.DateTime(), nullable=True) last_run: Mapped[Optional[datetime]] = mapped_column(db.DateTime(timezone=True))
next_run = db.Column(db.DateTime(), nullable=True) next_run: Mapped[Optional[datetime]] = mapped_column(db.DateTime(timezone=True))
next_is_full = db.Column(db.Boolean(), nullable=False) next_is_full: Mapped[bool]
logs = db.relationship("AutomationLogs", back_populates="automation") logs = db.relationship("AutomationLogs", back_populates="automation")
@ -34,13 +37,13 @@ class Automation(AbstractConfiguration):
def kick(self) -> None: def kick(self) -> None:
self.enabled = True self.enabled = True
self.next_run = datetime.datetime.utcnow() self.next_run = datetime.now(tz=timezone.utc)
self.updated = datetime.datetime.utcnow() self.updated = datetime.now(tz=timezone.utc)
class AutomationLogs(AbstractResource): class AutomationLogs(AbstractResource):
automation_id = db.Column(db.Integer, db.ForeignKey(Automation.id), nullable=False) automation_id: Mapped[int] = mapped_column(db.ForeignKey("automation.id"))
logs = db.Column(db.Text) logs: Mapped[str]
automation = db.relationship("Automation", back_populates="logs") automation = db.relationship("Automation", back_populates="logs")

View file

@ -1,5 +1,5 @@
from datetime import datetime from datetime import datetime, timezone
from typing import List, TypedDict, Optional, TYPE_CHECKING from typing import TYPE_CHECKING, List, Optional, TypedDict
from sqlalchemy import and_ from sqlalchemy import and_
from sqlalchemy.orm import Mapped, aliased, mapped_column, relationship from sqlalchemy.orm import Mapped, aliased, mapped_column, relationship
@ -22,7 +22,7 @@ class GroupDict(TypedDict):
class Group(AbstractConfiguration): class Group(AbstractConfiguration):
group_name: Mapped[str] = db.Column(db.String(80), unique=True, nullable=False) group_name: Mapped[str] = mapped_column(unique=True)
eotk: Mapped[bool] eotk: Mapped[bool]
origins: Mapped[List["Origin"]] = relationship("Origin", back_populates="group") origins: Mapped[List["Origin"]] = relationship("Origin", back_populates="group")
@ -93,19 +93,19 @@ class Pool(AbstractConfiguration):
class PoolGroup(db.Model): # type: ignore[name-defined,misc] class PoolGroup(db.Model): # type: ignore[name-defined,misc]
pool_id = db.Column(db.Integer, db.ForeignKey("pool.id"), primary_key=True) pool_id: Mapped[int] = mapped_column(db.ForeignKey("pool.id"), primary_key=True)
group_id = db.Column(db.Integer, db.ForeignKey("group.id"), primary_key=True) group_id: Mapped[int] = mapped_column(db.ForeignKey("group.id"), primary_key=True)
class MirrorList(AbstractConfiguration): class MirrorList(AbstractConfiguration):
pool_id = db.Column(db.Integer, db.ForeignKey("pool.id")) pool_id: Mapped[int] = mapped_column(db.ForeignKey("pool.id"))
provider = db.Column(db.String(255), nullable=False) provider: Mapped[str]
format = db.Column(db.String(20), nullable=False) format: Mapped[str]
encoding = db.Column(db.String(20), nullable=False) encoding: Mapped[str]
container = db.Column(db.String(255), nullable=False) container: Mapped[str]
branch = db.Column(db.String(255), nullable=False) branch: Mapped[str]
role = db.Column(db.String(255), nullable=True) role: Mapped[Optional[str]]
filename = db.Column(db.String(255), nullable=False) filename: Mapped[str]
pool = db.relationship("Pool", back_populates="lists") pool = db.relationship("Pool", back_populates="lists")
@ -132,8 +132,8 @@ class MirrorList(AbstractConfiguration):
} }
def destroy(self) -> None: def destroy(self) -> None:
self.destroyed = datetime.utcnow() self.destroyed = datetime.now(tz=timezone.utc)
self.updated = datetime.utcnow() self.updated = datetime.now(tz=timezone.utc)
def url(self) -> str: def url(self) -> str:
if self.provider == "gitlab": if self.provider == "gitlab":

View file

@ -1,6 +1,6 @@
import enum import enum
from datetime import datetime from datetime import datetime, timezone
from typing import List from typing import List, Optional
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
@ -37,12 +37,12 @@ class BridgeConf(AbstractConfiguration):
) )
def destroy(self) -> None: def destroy(self) -> None:
self.destroyed = datetime.utcnow() self.destroyed = datetime.now(tz=timezone.utc)
self.updated = datetime.utcnow() self.updated = datetime.now(tz=timezone.utc)
for bridge in self.bridges: for bridge in self.bridges:
if bridge.destroyed is None: if bridge.destroyed is None:
bridge.destroyed = datetime.utcnow() bridge.destroyed = datetime.now(tz=timezone.utc)
bridge.updated = datetime.utcnow() bridge.updated = datetime.now(tz=timezone.utc)
@classmethod @classmethod
def csv_header(cls) -> List[str]: def csv_header(cls) -> List[str]:
@ -52,13 +52,13 @@ class BridgeConf(AbstractConfiguration):
class Bridge(AbstractResource): class Bridge(AbstractResource):
conf_id = db.Column(db.Integer, db.ForeignKey("bridge_conf.id"), nullable=False) conf_id: Mapped[int] = mapped_column(db.ForeignKey("bridge_conf.id"))
cloud_account_id = db.Column(db.Integer, db.ForeignKey("cloud_account.id")) cloud_account_id: Mapped[int] = mapped_column(db.ForeignKey("cloud_account.id"))
terraform_updated = db.Column(db.DateTime(), nullable=True) terraform_updated: Mapped[Optional[datetime]] = mapped_column(db.DateTime(timezone=True), nullable=True)
nickname = db.Column(db.String(255), nullable=True) nickname: Mapped[Optional[str]]
fingerprint = db.Column(db.String(255), nullable=True) fingerprint: Mapped[Optional[str]]
hashed_fingerprint = db.Column(db.String(255), nullable=True) hashed_fingerprint: Mapped[Optional[str]]
bridgeline = db.Column(db.String(255), nullable=True) bridgeline: Mapped[Optional[str]]
conf = db.relationship("BridgeConf", back_populates="bridges") conf = db.relationship("BridgeConf", back_populates="bridges")
cloud_account = db.relationship("CloudAccount", back_populates="bridges") cloud_account = db.relationship("CloudAccount", back_populates="bridges")

View file

@ -1,8 +1,8 @@
from __future__ import annotations from __future__ import annotations
import json import json
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from typing import Optional, List, Union, Any, Dict, TypedDict, Literal from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
import tldextract import tldextract
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
@ -10,10 +10,11 @@ from tldextract import extract
from werkzeug.datastructures import FileStorage from werkzeug.datastructures import FileStorage
from app.brm.brn import BRN from app.brm.brn import BRN
from app.brm.utils import thumbnail_uploaded_image, create_data_uri, normalize_color from app.brm.utils import (create_data_uri, normalize_color,
thumbnail_uploaded_image)
from app.extensions import db from app.extensions import db
from app.models import AbstractConfiguration, AbstractResource, Deprecation from app.models import AbstractConfiguration, AbstractResource, Deprecation
from app.models.base import Pool, Group from app.models.base import Group, Pool
from app.models.onions import Onion from app.models.onions import Onion
country_origin = db.Table( country_origin = db.Table(
@ -93,14 +94,14 @@ class Origin(AbstractConfiguration):
.filter( .filter(
Origin.id == self.id, Origin.id == self.id,
Deprecation.resource_type == 'Proxy', Deprecation.resource_type == 'Proxy',
Deprecation.deprecated_at >= datetime.utcnow() - timedelta(hours=168), Deprecation.deprecated_at >= datetime.now(tz=timezone.utc) - timedelta(hours=168),
Deprecation.reason != "destroyed" Deprecation.reason != "destroyed"
) )
.distinct(Proxy.id) .distinct(Proxy.id)
.all() .all()
) )
for deprecation in recent_deprecations: for deprecation in recent_deprecations:
recency_factor += 1 / max((datetime.utcnow() - deprecation.deprecated_at).total_seconds() // 3600, 1) recency_factor += 1 / max((datetime.now(tz=timezone.utc) - deprecation.deprecated_at).total_seconds() // 3600, 1)
frequency_factor += 1 frequency_factor += 1
risk_levels: Dict[str, int] = {} risk_levels: Dict[str, int] = {}
for country in self.countries: for country in self.countries:
@ -149,14 +150,14 @@ class Country(AbstractConfiguration):
.filter( .filter(
Country.id == self.id, Country.id == self.id,
Deprecation.resource_type == 'Proxy', Deprecation.resource_type == 'Proxy',
Deprecation.deprecated_at >= datetime.utcnow() - timedelta(hours=168), Deprecation.deprecated_at >= datetime.now(tz=timezone.utc) - timedelta(hours=168),
Deprecation.reason != "destroyed" Deprecation.reason != "destroyed"
) )
.distinct(Proxy.id) .distinct(Proxy.id)
.all() .all()
) )
for deprecation in recent_deprecations: for deprecation in recent_deprecations:
recency_factor += 1 / max((datetime.utcnow() - deprecation.deprecated_at).total_seconds() // 3600, 1) recency_factor += 1 / max((datetime.now(tz=timezone.utc) - deprecation.deprecated_at).total_seconds() // 3600, 1)
frequency_factor += 1 frequency_factor += 1
return int(max(1, min(10, frequency_factor * recency_factor))) return int(max(1, min(10, frequency_factor * recency_factor)))
@ -255,7 +256,7 @@ class StaticOrigin(AbstractConfiguration):
raise ValueError("clean_insights_backend must be a str, bool, or None") raise ValueError("clean_insights_backend must be a str, bool, or None")
if db_session_commit: if db_session_commit:
db.session.commit() db.session.commit()
self.updated = datetime.utcnow() self.updated = datetime.now(tz=timezone.utc)
ResourceStatus = Union[Literal["active"], Literal["pending"], Literal["expiring"], Literal["destroyed"]] ResourceStatus = Union[Literal["active"], Literal["pending"], Literal["expiring"], Literal["destroyed"]]
@ -274,7 +275,7 @@ class Proxy(AbstractResource):
provider: Mapped[str] = mapped_column(db.String(20), nullable=False) provider: Mapped[str] = mapped_column(db.String(20), nullable=False)
psg: Mapped[Optional[int]] = mapped_column(db.Integer, nullable=True) psg: Mapped[Optional[int]] = mapped_column(db.Integer, nullable=True)
slug: Mapped[Optional[str]] = mapped_column(db.String(20), nullable=True) slug: Mapped[Optional[str]] = mapped_column(db.String(20), nullable=True)
terraform_updated: Mapped[Optional[datetime]] = mapped_column(db.DateTime(), nullable=True) terraform_updated: Mapped[Optional[datetime]] = mapped_column(db.DateTime(timezone=True), nullable=True)
url: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True) url: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True)
origin: Mapped[Origin] = relationship("Origin", back_populates="proxies") origin: Mapped[Origin] = relationship("Origin", back_populates="proxies")

View file

@ -3,28 +3,28 @@ import logging
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional from typing import Optional
from flask import Blueprint, render_template, request, url_for, redirect from flask import Blueprint, redirect, render_template, request, url_for
from flask.typing import ResponseReturnValue from flask.typing import ResponseReturnValue
from markupsafe import Markup from markupsafe import Markup
from sqlalchemy import desc, or_, func from sqlalchemy import desc, func, or_
from app.alarms import alarms_for from app.alarms import alarms_for
from app.models.activity import Activity from app.models.activity import Activity
from app.models.alarms import Alarm, AlarmState from app.models.alarms import Alarm, AlarmState
from app.models.base import Group
from app.models.bridges import Bridge 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.onions import Eotk from app.models.onions import Eotk
from app.portal.country import bp as country
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.bridge import bp as bridge from app.portal.bridge import bp as bridge
from app.portal.bridgeconf import bp as bridgeconf
from app.portal.cloud import bp as cloud
from app.portal.country import bp as country
from app.portal.eotk import bp as eotk from app.portal.eotk import bp as eotk
from app.portal.group import bp as group from app.portal.group import bp as group
from app.portal.list import bp as list_ from app.portal.list import bp as list_
from app.portal.origin import bp as origin
from app.portal.onion import bp as onion from app.portal.onion import bp as onion
from app.portal.origin import bp as origin
from app.portal.pool import bp as pool from app.portal.pool import bp as pool
from app.portal.proxy import bp as proxy from app.portal.proxy import bp as proxy
from app.portal.smart_proxy import bp as smart_proxy from app.portal.smart_proxy import bp as smart_proxy
@ -57,7 +57,7 @@ def calculate_bridge_expiry(b: Bridge) -> str:
logging.warning("Bridge expiry requested by template for a bridge %s that was not expiring.", b.id) logging.warning("Bridge expiry requested by template for a bridge %s that was not expiring.", b.id)
return "Not expiring" return "Not expiring"
expiry = b.deprecated + timedelta(hours=b.conf.expiry_hours) expiry = b.deprecated + timedelta(hours=b.conf.expiry_hours)
countdown = expiry - datetime.utcnow() countdown = expiry - datetime.now(tz=timezone.utc)
if countdown.days == 0: if countdown.days == 0:
return f"{countdown.seconds // 3600} hours" return f"{countdown.seconds // 3600} hours"
return f"{countdown.days} days" return f"{countdown.days} days"
@ -66,7 +66,7 @@ def calculate_bridge_expiry(b: Bridge) -> str:
@portal.app_template_filter("mirror_expiry") @portal.app_template_filter("mirror_expiry")
def calculate_mirror_expiry(s: datetime) -> str: def calculate_mirror_expiry(s: datetime) -> str:
expiry = s + timedelta(days=3) expiry = s + timedelta(days=3)
countdown = expiry - datetime.utcnow() countdown = expiry - datetime.now(tz=timezone.utc)
if countdown.days == 0: if countdown.days == 0:
return f"{countdown.seconds // 3600} hours" return f"{countdown.seconds // 3600} hours"
return f"{countdown.days} days" return f"{countdown.days} days"

View file

@ -1,16 +1,16 @@
from datetime import datetime from datetime import datetime, timezone
from typing import Optional from typing import Optional
from flask import render_template, flash, Response, Blueprint, current_app from flask import Blueprint, Response, current_app, flash, render_template
from flask.typing import ResponseReturnValue from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from sqlalchemy import exc, desc from sqlalchemy import desc, exc
from wtforms import SubmitField, BooleanField from wtforms import BooleanField, SubmitField
from app.extensions import db from app.extensions import db
from app.models.automation import Automation, AutomationLogs from app.models.automation import Automation, AutomationLogs
from app.models.tfstate import TerraformState from app.models.tfstate import TerraformState
from app.portal.util import view_lifecycle, response_404 from app.portal.util import response_404, view_lifecycle
bp = Blueprint("automation", __name__) bp = Blueprint("automation", __name__)
@ -54,7 +54,7 @@ def automation_edit(automation_id: int) -> ResponseReturnValue:
form = EditAutomationForm(enabled=automation.enabled) form = EditAutomationForm(enabled=automation.enabled)
if form.validate_on_submit(): if form.validate_on_submit():
automation.enabled = form.enabled.data automation.enabled = form.enabled.data
automation.updated = datetime.utcnow() automation.updated = datetime.now(tz=timezone.utc)
try: try:
db.session.commit() db.session.commit()
flash("Saved changes to bridge configuration.", "success") flash("Saved changes to bridge configuration.", "success")

View file

@ -1,11 +1,12 @@
from datetime import datetime from datetime import datetime, timezone
from typing import Optional, List from typing import List, Optional
from flask import render_template, url_for, flash, redirect, Response, Blueprint from flask import (Blueprint, Response, flash, redirect, render_template,
url_for)
from flask.typing import ResponseReturnValue from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from sqlalchemy import exc from sqlalchemy import exc
from wtforms import SelectField, StringField, IntegerField, SubmitField from wtforms import IntegerField, SelectField, StringField, SubmitField
from wtforms.validators import DataRequired, NumberRange from wtforms.validators import DataRequired, NumberRange
from app.extensions import db from app.extensions import db
@ -99,8 +100,8 @@ def bridgeconf_new(group_id: Optional[int] = None) -> ResponseReturnValue:
bridgeconf.max_number = form.max_number.data bridgeconf.max_number = form.max_number.data
bridgeconf.expiry_hours = form.expiry_hours.data bridgeconf.expiry_hours = form.expiry_hours.data
bridgeconf.provider_allocation = ProviderAllocation[form.provider_allocation.data] bridgeconf.provider_allocation = ProviderAllocation[form.provider_allocation.data]
bridgeconf.created = datetime.utcnow() bridgeconf.added = datetime.now(tz=timezone.utc)
bridgeconf.updated = datetime.utcnow() bridgeconf.updated = datetime.now(tz=timezone.utc)
try: try:
db.session.add(bridgeconf) db.session.add(bridgeconf)
db.session.commit() db.session.commit()
@ -137,7 +138,7 @@ def bridgeconf_edit(bridgeconf_id: int) -> ResponseReturnValue:
bridgeconf.max_number = form.max_number.data bridgeconf.max_number = form.max_number.data
bridgeconf.expiry_hours = form.expiry_hours.data bridgeconf.expiry_hours = form.expiry_hours.data
bridgeconf.provider_allocation = ProviderAllocation[form.provider_allocation.data] bridgeconf.provider_allocation = ProviderAllocation[form.provider_allocation.data]
bridgeconf.updated = datetime.utcnow() bridgeconf.updated = datetime.now(tz=timezone.utc)
try: try:
db.session.commit() db.session.commit()
flash("Saved changes to bridge configuration.", "success") flash("Saved changes to bridge configuration.", "success")

View file

@ -1,10 +1,10 @@
from datetime import datetime from datetime import datetime, timezone
import sqlalchemy import sqlalchemy
from flask import Blueprint, render_template, Response, flash from flask import Blueprint, Response, flash, render_template
from flask.typing import ResponseReturnValue from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import IntegerField, BooleanField, SubmitField from wtforms import BooleanField, IntegerField, SubmitField
from app.extensions import db from app.extensions import db
from app.models.mirrors import Country from app.models.mirrors import Country
@ -63,7 +63,7 @@ def country_edit(country_id: int) -> ResponseReturnValue:
country.risk_level_override = form.risk_level_override_number.data country.risk_level_override = form.risk_level_override_number.data
else: else:
country.risk_level_override = None country.risk_level_override = None
country.updated = datetime.utcnow() country.updated = datetime.now(tz=timezone.utc)
try: try:
db.session.commit() db.session.commit()
flash("Saved changes to country.", "success") flash("Saved changes to country.", "success")

View file

@ -1,10 +1,11 @@
from datetime import datetime from datetime import datetime, timezone
from flask import render_template, url_for, flash, redirect, Response, Blueprint import sqlalchemy
from flask import (Blueprint, Response, flash, redirect, render_template,
url_for)
from flask.typing import ResponseReturnValue from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
import sqlalchemy from wtforms import BooleanField, StringField, SubmitField
from wtforms import StringField, BooleanField, SubmitField
from wtforms.validators import DataRequired from wtforms.validators import DataRequired
from app.extensions import db from app.extensions import db
@ -72,7 +73,7 @@ def group_edit(group_id: int) -> ResponseReturnValue:
if form.validate_on_submit(): if form.validate_on_submit():
group.description = form.description.data group.description = form.description.data
group.eotk = form.eotk.data group.eotk = form.eotk.data
group.updated = datetime.utcnow() group.updated = datetime.now(tz=timezone.utc)
try: try:
db.session.commit() db.session.commit()
flash("Saved changes to group.", "success") flash("Saved changes to group.", "success")

View file

@ -1,8 +1,9 @@
import json import json
from datetime import datetime from datetime import datetime, timezone
from typing import Optional, Any from typing import Any, Optional
from flask import render_template, url_for, flash, redirect, Blueprint, Response from flask import (Blueprint, Response, flash, redirect, render_template,
url_for)
from flask.typing import ResponseReturnValue from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from sqlalchemy import exc from sqlalchemy import exc
@ -103,8 +104,8 @@ def list_new(group_id: Optional[int] = None) -> ResponseReturnValue:
list_.branch = form.branch.data list_.branch = form.branch.data
list_.role = form.role.data list_.role = form.role.data
list_.filename = form.filename.data list_.filename = form.filename.data
list_.created = datetime.utcnow() list_.added = datetime.now(tz=timezone.utc)
list_.updated = datetime.utcnow() list_.updated = datetime.now(tz=timezone.utc)
try: try:
db.session.add(list_) db.session.add(list_)
db.session.commit() db.session.commit()
@ -176,7 +177,7 @@ def list_edit(list_id: int) -> ResponseReturnValue:
list_.branch = form.branch.data list_.branch = form.branch.data
list_.role = form.role.data list_.role = form.role.data
list_.filename = form.filename.data list_.filename = form.filename.data
list_.updated = datetime.utcnow() list_.updated = datetime.now(tz=timezone.utc)
try: try:
db.session.commit() db.session.commit()
flash("Saved changes to group.", "success") flash("Saved changes to group.", "success")

View file

@ -1,20 +1,22 @@
import urllib.parse import urllib.parse
from datetime import datetime from datetime import datetime, timezone
from typing import Optional, List from typing import List, Optional
import requests import requests
import sqlalchemy import sqlalchemy
from flask import flash, redirect, url_for, render_template, Response, Blueprint from flask import (Blueprint, Response, flash, redirect, render_template,
url_for)
from flask.typing import ResponseReturnValue from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from sqlalchemy import exc from sqlalchemy import exc
from wtforms import StringField, SelectField, SubmitField, BooleanField, IntegerField from wtforms import (BooleanField, IntegerField, SelectField, StringField,
SubmitField)
from wtforms.validators import DataRequired from wtforms.validators import DataRequired
from app.extensions import db from app.extensions import db
from app.models.base import Group from app.models.base import Group
from app.models.mirrors import Origin, Country from app.models.mirrors import Country, Origin
from app.portal.util import response_404, view_lifecycle, LifecycleForm from app.portal.util import LifecycleForm, response_404, view_lifecycle
bp = Blueprint("origin", __name__) bp = Blueprint("origin", __name__)
@ -64,8 +66,8 @@ def origin_new(group_id: Optional[int] = None) -> ResponseReturnValue:
origin.auto_rotation = form.auto_rotate.data origin.auto_rotation = form.auto_rotate.data
origin.smart = form.smart_proxy.data origin.smart = form.smart_proxy.data
origin.assets = form.asset_domain.data origin.assets = form.asset_domain.data
origin.created = datetime.utcnow() origin.added = datetime.now(tz=timezone.utc)
origin.updated = datetime.utcnow() origin.updated = datetime.now(tz=timezone.utc)
try: try:
db.session.add(origin) db.session.add(origin)
db.session.commit() db.session.commit()
@ -106,7 +108,7 @@ def origin_edit(origin_id: int) -> ResponseReturnValue:
origin.risk_level_override = form.risk_level_override_number.data origin.risk_level_override = form.risk_level_override_number.data
else: else:
origin.risk_level_override = None origin.risk_level_override = None
origin.updated = datetime.utcnow() origin.updated = datetime.now(tz=timezone.utc)
try: try:
db.session.commit() db.session.commit()
flash(f"Saved changes for origin {origin.domain_name}.", "success") flash(f"Saved changes for origin {origin.domain_name}.", "success")

View file

@ -1,22 +1,22 @@
# mypy: ignore-errors # mypy: ignore-errors
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from flask import Blueprint, render_template from flask import Blueprint, render_template
from flask.typing import ResponseReturnValue from flask.typing import ResponseReturnValue
from sqlalchemy import func, and_, desc from sqlalchemy import and_, desc, func
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
from app.extensions import db from app.extensions import db
from app.models import Deprecation from app.models import Deprecation
from app.models.mirrors import Proxy, Origin, Country from app.models.mirrors import Country, Origin, Proxy
report = Blueprint("report", __name__) report = Blueprint("report", __name__)
def generate_subqueries(): def generate_subqueries():
DeprecationAlias = aliased(Deprecation) DeprecationAlias = aliased(Deprecation)
now = datetime.utcnow() now = datetime.now(tz=timezone.utc)
deprecations_24hr_subquery = ( deprecations_24hr_subquery = (
db.session.query( db.session.query(
DeprecationAlias.resource_id, DeprecationAlias.resource_id,
@ -98,7 +98,7 @@ def report_blocks() -> ResponseReturnValue:
Proxy.deprecated, Proxy.deprecated,
Proxy.deprecation_reason Proxy.deprecation_reason
).join(Origin, Origin.id == Proxy.origin_id ).join(Origin, Origin.id == Proxy.origin_id
).filter(and_(Proxy.deprecated > datetime.utcnow() - timedelta(days=1), ).filter(and_(Proxy.deprecated > datetime.now(tz=timezone.utc) - timedelta(days=1),
Proxy.deprecation_reason.like('block_%'))).all() Proxy.deprecation_reason.like('block_%'))).all()
return render_template("report_blocks.html.j2", return render_template("report_blocks.html.j2",

View file

@ -1,16 +1,16 @@
from datetime import datetime from datetime import datetime, timezone
from typing import Optional from typing import Optional
from flask import render_template, flash, Response, Blueprint from flask import Blueprint, Response, flash, render_template
from flask.typing import ResponseReturnValue from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from sqlalchemy import exc from sqlalchemy import exc
from wtforms import SubmitField, BooleanField from wtforms import BooleanField, SubmitField
from app.extensions import db from app.extensions import db
from app.models.automation import Automation from app.models.automation import Automation
from app.models.tfstate import TerraformState from app.models.tfstate import TerraformState
from app.portal.util import view_lifecycle, response_404 from app.portal.util import response_404, view_lifecycle
bp = Blueprint("storage", __name__) bp = Blueprint("storage", __name__)
@ -39,7 +39,7 @@ def storage_edit(storage_key: str) -> ResponseReturnValue:
if form.validate_on_submit(): if form.validate_on_submit():
if form.force_unlock.data: if form.force_unlock.data:
storage.lock = None storage.lock = None
storage.updated = datetime.utcnow() storage.updated = datetime.now(tz=timezone.utc)
try: try:
db.session.commit() db.session.commit()
flash("Storage has been force unlocked.", "success") flash("Storage has been force unlocked.", "success")

View file

@ -1,11 +1,12 @@
from datetime import datetime from datetime import datetime, timezone
from typing import Optional from typing import Optional
from flask import Blueprint, flash, Response, render_template, redirect, url_for from flask import (Blueprint, Response, flash, redirect, render_template,
url_for)
from flask.typing import ResponseReturnValue from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from sqlalchemy import exc from sqlalchemy import exc
from wtforms import StringField, SelectField, SubmitField from wtforms import SelectField, StringField, SubmitField
from wtforms.validators import DataRequired from wtforms.validators import DataRequired
from app.extensions import db from app.extensions import db
@ -70,7 +71,7 @@ def webhook_edit(webhook_id: int) -> ResponseReturnValue:
webhook.description = form.description.data webhook.description = form.description.data
webhook.format = form.description.data webhook.format = form.description.data
webhook.url = form.description.data webhook.url = form.description.data
webhook.updated = datetime.utcnow() webhook.updated = datetime.now(tz=timezone.utc)
try: try:
db.session.commit() db.session.commit()
flash("Saved changes to webhook.", "success") flash("Saved changes to webhook.", "success")

View file

@ -1,7 +1,7 @@
from datetime import datetime, timedelta
import logging import logging
from abc import abstractmethod from abc import abstractmethod
from typing import Tuple, List, Callable, Optional, Any from datetime import datetime, timedelta, timezone
from typing import Any, Callable, List, Optional, Tuple
from app.extensions import db from app.extensions import db
from app.models.activity import Activity from app.models.activity import Activity
@ -25,14 +25,14 @@ class BlockBridgeAutomation(BaseAutomation):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def perform_deprecations(self, ids: List[str], bridge_select_func: Callable[[str], Optional[Bridge]] def perform_deprecations(self, ids: List[str], bridge_select_func: Callable[[str], Optional[Bridge]]
) -> List[Tuple[str, str, str]]: ) -> List[Tuple[Optional[str], Any, Any]]:
rotated = [] rotated = []
for id_ in ids: for id_ in ids:
bridge = bridge_select_func(id_) bridge = bridge_select_func(id_)
if bridge is None: if bridge is None:
continue continue
logging.debug("Found %s blocked", bridge.hashed_fingerprint) logging.debug("Found %s blocked", bridge.hashed_fingerprint)
if bridge.added > datetime.utcnow() - timedelta(hours=3): if bridge.added > datetime.now(tz=timezone.utc) - timedelta(hours=3):
logging.debug("Not rotating a bridge less than 3 hours old") logging.debug("Not rotating a bridge less than 3 hours old")
continue continue
if bridge.deprecate(reason=self.short_name): if bridge.deprecate(reason=self.short_name):

View file

@ -1,9 +1,9 @@
from collections import defaultdict import fnmatch
from datetime import datetime, timedelta
import logging import logging
from abc import abstractmethod from abc import abstractmethod
import fnmatch from collections import defaultdict
from typing import Tuple, List, Any, Optional, Dict from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple
from app.extensions import db from app.extensions import db
from app.models.activity import Activity from app.models.activity import Activity
@ -41,7 +41,7 @@ class BlockMirrorAutomation(BaseAutomation):
if not proxy.origin.auto_rotation: if not proxy.origin.auto_rotation:
logging.debug("Proxy auto-rotation forbidden for origin") logging.debug("Proxy auto-rotation forbidden for origin")
continue continue
if proxy.added > datetime.utcnow() - timedelta(hours=3): if proxy.added > datetime.now(tz=timezone.utc) - timedelta(hours=3):
logging.debug("Not rotating a proxy less than 3 hours old") logging.debug("Not rotating a proxy less than 3 hours old")
continue continue
if proxy.deprecate(reason=f"block_{source}"): if proxy.deprecate(reason=f"block_{source}"):

View file

@ -1,7 +1,6 @@
from collections import defaultdict from collections import defaultdict
from datetime import datetime from datetime import datetime, timedelta, timezone
from datetime import timedelta from typing import Any, Dict, Tuple
from typing import Dict, Tuple, Any
import requests import requests
@ -13,8 +12,8 @@ from app.terraform import BaseAutomation
def check_origin(domain_name: str) -> Dict[str, Any]: def check_origin(domain_name: str) -> Dict[str, Any]:
start_date = (datetime.utcnow() - timedelta(days=1)).strftime("%Y-%m-%dT%H%%3A%M") start_date = (datetime.now(tz=timezone.utc) - timedelta(days=1)).strftime("%Y-%m-%dT%H%%3A%M")
end_date = datetime.utcnow().strftime("%Y-%m-%dT%H%%3A%M") end_date = datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H%%3A%M")
api_url = f"https://api.ooni.io/api/v1/measurements?domain={domain_name}&since={start_date}&until={end_date}" api_url = f"https://api.ooni.io/api/v1/measurements?domain={domain_name}&since={start_date}&until={end_date}"
result: Dict[str, Dict[str, int]] = defaultdict(lambda: {"anomaly": 0, "confirmed": 0, "failure": 0, "ok": 0}) result: Dict[str, Dict[str, int]] = defaultdict(lambda: {"anomaly": 0, "confirmed": 0, "failure": 0, "ok": 0})
return _check_origin(api_url, result) return _check_origin(api_url, result)

View file

@ -1,9 +1,9 @@
import datetime
import os import os
import sys import sys
from typing import Optional, Any, List, Sequence, Tuple from datetime import datetime, timedelta, timezone
from typing import Any, List, Optional, Sequence, Tuple
from sqlalchemy import select, Row from sqlalchemy import Row, select
from app import app from app import app
from app.extensions import db from app.extensions import db
@ -25,7 +25,7 @@ def active_bridges_by_provider(provider: CloudProvider) -> Sequence[BridgeResour
def recently_destroyed_bridges_by_provider(provider: CloudProvider) -> Sequence[BridgeResourceRow]: def recently_destroyed_bridges_by_provider(provider: CloudProvider) -> Sequence[BridgeResourceRow]:
cutoff = datetime.datetime.utcnow() - datetime.timedelta(hours=72) cutoff = datetime.now(tz=timezone.utc) - timedelta(hours=72)
stmt = select(Bridge, BridgeConf, CloudAccount).join_from(Bridge, BridgeConf).join_from(Bridge, CloudAccount).where( stmt = select(Bridge, BridgeConf, CloudAccount).join_from(Bridge, BridgeConf).join_from(Bridge, CloudAccount).where(
CloudAccount.provider == provider, CloudAccount.provider == provider,
Bridge.destroyed.is_not(None), Bridge.destroyed.is_not(None),
@ -83,7 +83,7 @@ class BridgeAutomation(TerraformAutomation):
bridge = Bridge.query.filter(Bridge.id == output[len('bridge_hashed_fingerprint_'):]).first() bridge = Bridge.query.filter(Bridge.id == output[len('bridge_hashed_fingerprint_'):]).first()
bridge.nickname = parts[0] bridge.nickname = parts[0]
bridge.hashed_fingerprint = parts[1] bridge.hashed_fingerprint = parts[1]
bridge.terraform_updated = datetime.datetime.utcnow() bridge.terraform_updated = datetime.now(tz=timezone.utc)
if output.startswith('bridge_bridgeline_'): if output.startswith('bridge_bridgeline_'):
parts = outputs[output]['value'].split(" ") parts = outputs[output]['value'].split(" ")
if len(parts) < 4: if len(parts) < 4:
@ -91,7 +91,7 @@ class BridgeAutomation(TerraformAutomation):
bridge = Bridge.query.filter(Bridge.id == output[len('bridge_bridgeline_'):]).first() bridge = Bridge.query.filter(Bridge.id == output[len('bridge_bridgeline_'):]).first()
del parts[3] del parts[3]
bridge.bridgeline = " ".join(parts) bridge.bridgeline = " ".join(parts)
bridge.terraform_updated = datetime.datetime.utcnow() bridge.terraform_updated = datetime.now(tz=timezone.utc)
db.session.commit() db.session.commit()
@classmethod @classmethod

View file

@ -1,11 +1,11 @@
import datetime
import logging import logging
import random import random
from typing import Tuple, List from datetime import datetime, timedelta, timezone
from typing import List, Tuple
from app import db from app import db
from app.models.bridges import BridgeConf, Bridge, ProviderAllocation from app.models.bridges import Bridge, BridgeConf, ProviderAllocation
from app.models.cloud import CloudProvider, CloudAccount from app.models.cloud import CloudAccount, CloudProvider
from app.terraform import BaseAutomation from app.terraform import BaseAutomation
BRIDGE_PROVIDERS = [ BRIDGE_PROVIDERS = [
@ -33,8 +33,8 @@ def create_bridges_in_account(bridgeconf: BridgeConf, account: CloudAccount, cou
bridge.pool_id = bridgeconf.pool.id bridge.pool_id = bridgeconf.pool.id
bridge.conf_id = bridgeconf.id bridge.conf_id = bridgeconf.id
bridge.cloud_account = account bridge.cloud_account = account
bridge.added = datetime.datetime.utcnow() bridge.added = datetime.now(tz=timezone.utc)
bridge.updated = datetime.datetime.utcnow() bridge.updated = datetime.now(tz=timezone.utc)
logging.debug("Creating bridge %s", bridge) logging.debug("Creating bridge %s", bridge)
db.session.add(bridge) db.session.add(bridge)
created += 1 created += 1
@ -129,7 +129,7 @@ class BridgeMetaAutomation(BaseAutomation):
for bridge in deprecated_bridges: for bridge in deprecated_bridges:
if bridge.deprecated is None: if bridge.deprecated is None:
continue # Possible due to SQLAlchemy lazy loading continue # Possible due to SQLAlchemy lazy loading
cutoff = datetime.datetime.utcnow() - datetime.timedelta(hours=bridge.conf.expiry_hours) cutoff = datetime.now(tz=timezone.utc) - timedelta(hours=bridge.conf.expiry_hours)
if bridge.deprecated < cutoff: if bridge.deprecated < cutoff:
logging.debug("Destroying expired bridge") logging.debug("Destroying expired bridge")
bridge.destroy() bridge.destroy()

View file

@ -1,5 +1,5 @@
import datetime
import os import os
from datetime import datetime, timezone
from typing import Any from typing import Any
from app import app from app import app
@ -22,12 +22,12 @@ def update_eotk_instance(group_id: int,
).first() ).first()
if instance is None: if instance is None:
instance = Eotk() instance = Eotk()
instance.added = datetime.datetime.utcnow() instance.added = datetime.now(tz=timezone.utc)
instance.group_id = group_id instance.group_id = group_id
instance.provider = "aws" instance.provider = "aws"
instance.region = region instance.region = region
db.session.add(instance) db.session.add(instance)
instance.updated = datetime.datetime.utcnow() instance.updated = datetime.now(tz=timezone.utc)
instance.instance_id = instance_id instance.instance_id = instance_id

View file

@ -1,4 +1,4 @@
import datetime from datetime import datetime, timezone
from typing import Any from typing import Any
from app.extensions import db from app.extensions import db
@ -122,7 +122,7 @@ class ProxyCloudfrontAutomation(ProxyAutomation):
proxy = Proxy.query.filter(Proxy.id == mod['address'][len('module.cloudfront_'):]).first() proxy = Proxy.query.filter(Proxy.id == mod['address'][len('module.cloudfront_'):]).first()
proxy.url = "https://" + res['values']['domain_name'] proxy.url = "https://" + res['values']['domain_name']
proxy.slug = res['values']['id'] proxy.slug = res['values']['id']
proxy.terraform_updated = datetime.datetime.utcnow() proxy.terraform_updated = datetime.now(tz=timezone.utc)
break break
# EC2 instances (smart proxies) # EC2 instances (smart proxies)
for g in state["values"]["root_module"]["child_modules"]: for g in state["values"]["root_module"]["child_modules"]:

View file

@ -1,16 +1,17 @@
import datetime
import logging import logging
import random import random
import string import string
from collections import OrderedDict from collections import OrderedDict
from typing import Any, Dict, List, Optional, Tuple, Type from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional
from typing import OrderedDict as OrderedDictT from typing import OrderedDict as OrderedDictT
from typing import Tuple, Type
from tldextract import tldextract from tldextract import tldextract
from app import db from app import db
from app.models.base import Pool from app.models.base import Pool
from app.models.mirrors import Proxy, Origin from app.models.mirrors import Origin, Proxy
from app.terraform import BaseAutomation from app.terraform import BaseAutomation
from app.terraform.proxy import ProxyAutomation from app.terraform.proxy import ProxyAutomation
from app.terraform.proxy.azure_cdn import ProxyAzureCdnAutomation from app.terraform.proxy.azure_cdn import ProxyAzureCdnAutomation
@ -155,7 +156,7 @@ def auto_deprecate_proxies() -> None:
.all()) .all())
logging.debug("Max age: %s", max_age_proxies) logging.debug("Max age: %s", max_age_proxies)
for proxy in max_age_proxies: for proxy in max_age_proxies:
max_age_cutoff = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta( max_age_cutoff = datetime.now(timezone.utc) - timedelta(
days=1, seconds=86400 * random.random()) # nosec: B311 days=1, seconds=86400 * random.random()) # nosec: B311
if proxy.added < max_age_cutoff: if proxy.added < max_age_cutoff:
proxy.deprecate(reason="max_age_reached") proxy.deprecate(reason="max_age_reached")
@ -168,7 +169,7 @@ def destroy_expired_proxies() -> None:
This function finds all proxies that are not already destroyed and have been deprecated for more than 4 days. This function finds all proxies that are not already destroyed and have been deprecated for more than 4 days.
It then destroys these proxies. It then destroys these proxies.
""" """
expiry_cutoff = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=4) expiry_cutoff = datetime.now(timezone.utc) - timedelta(days=4)
proxies = Proxy.query.filter( proxies = Proxy.query.filter(
Proxy.destroyed.is_(None), Proxy.destroyed.is_(None),
Proxy.deprecated < expiry_cutoff Proxy.deprecated < expiry_cutoff
@ -200,7 +201,7 @@ def promote_hot_spare_proxy(pool_id: int, origin: Origin) -> bool:
if not proxy: if not proxy:
return False return False
proxy.pool_id = pool_id proxy.pool_id = pool_id
proxy.added = datetime.datetime.utcnow() proxy.added = datetime.now(tz=timezone.utc)
return True return True
@ -281,8 +282,8 @@ class ProxyMetaAutomation(BaseAutomation):
# The random usage below is good enough for its purpose: to create a slug that # The random usage below is good enough for its purpose: to create a slug that
# hasn't been used recently. # hasn't been used recently.
proxy.slug = random_slug(origin.domain_name) proxy.slug = random_slug(origin.domain_name)
proxy.added = datetime.datetime.utcnow() proxy.added = datetime.now(tz=timezone.utc)
proxy.updated = datetime.datetime.utcnow() proxy.updated = datetime.now(tz=timezone.utc)
logging.debug("Creating proxy %s", proxy) logging.debug("Creating proxy %s", proxy)
db.session.add(proxy) db.session.add(proxy)
return True return True

View file

@ -1,13 +1,13 @@
import logging import logging
import os import os
from datetime import datetime from datetime import datetime, timezone
from typing import List, Any from typing import Any, List
from flask import current_app from flask import current_app
from app.extensions import db from app.extensions import db
from app.models.base import Group from app.models.base import Group
from app.models.cloud import CloudProvider, CloudAccount from app.models.cloud import CloudAccount, CloudProvider
from app.models.mirrors import StaticOrigin from app.models.mirrors import StaticOrigin
from app.terraform.terraform import TerraformAutomation from app.terraform.terraform import TerraformAutomation
@ -30,7 +30,7 @@ def import_state(state: Any) -> None:
static.origin_domain_name = res['values']['domain_name'] static.origin_domain_name = res['values']['domain_name']
logging.debug("and found static origin: %s to update with domain name: %s", static.id, logging.debug("and found static origin: %s to update with domain name: %s", static.id,
static.origin_domain_name) static.origin_domain_name)
static.terraform_updated = datetime.utcnow() static.terraform_updated = datetime.now(tz=timezone.utc)
break break
db.session.commit() db.session.commit()

View file

@ -0,0 +1,81 @@
"""enforce more not null constraints
Revision ID: 54b31e87fe33
Revises: c14f25f364c5
Create Date: 2024-12-06 16:06:59.182836
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '54b31e87fe33'
down_revision = 'c14f25f364c5'
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table('activity', schema=None) as batch_op:
batch_op.alter_column('text',
existing_type=sa.TEXT(),
type_=sa.String(),
existing_nullable=False)
with op.batch_alter_table('automation_logs', schema=None) as batch_op:
batch_op.alter_column('logs',
existing_type=sa.TEXT(),
type_=sa.String(),
nullable=False)
with op.batch_alter_table('bridge', schema=None) as batch_op:
batch_op.alter_column('cloud_account_id',
existing_type=sa.INTEGER(),
nullable=False)
with op.batch_alter_table('mirror_list', schema=None) as batch_op:
batch_op.alter_column('pool_id',
existing_type=sa.INTEGER(),
nullable=False)
with op.batch_alter_table('webhook', schema=None) as batch_op:
batch_op.alter_column('format',
existing_type=sa.VARCHAR(length=20),
nullable=False)
batch_op.alter_column('url',
existing_type=sa.VARCHAR(length=255),
nullable=False)
def downgrade():
with op.batch_alter_table('webhook', schema=None) as batch_op:
batch_op.alter_column('url',
existing_type=sa.VARCHAR(length=255),
nullable=True)
batch_op.alter_column('format',
existing_type=sa.VARCHAR(length=20),
nullable=True)
with op.batch_alter_table('mirror_list', schema=None) as batch_op:
batch_op.alter_column('pool_id',
existing_type=sa.INTEGER(),
nullable=True)
with op.batch_alter_table('bridge', schema=None) as batch_op:
batch_op.alter_column('cloud_account_id',
existing_type=sa.INTEGER(),
nullable=True)
with op.batch_alter_table('automation_logs', schema=None) as batch_op:
batch_op.alter_column('logs',
existing_type=sa.String(),
type_=sa.TEXT(),
nullable=True)
with op.batch_alter_table('activity', schema=None) as batch_op:
batch_op.alter_column('text',
existing_type=sa.String(),
type_=sa.TEXT(),
existing_nullable=False)