feat: use custom type to handle existing naive datetimes

This commit is contained in:
Iain Learmonth 2024-12-06 18:02:59 +00:00
parent e22abb383c
commit 39bdac1ecf
45 changed files with 210 additions and 84 deletions

View file

@ -3,24 +3,23 @@ import sys
from typing import Iterator
import yaml
from flask import Flask, redirect, url_for, send_from_directory
from flask import Flask, redirect, send_from_directory, url_for
from flask.typing import ResponseReturnValue
from prometheus_client import make_wsgi_app, Metric, CollectorRegistry
from prometheus_client.metrics_core import GaugeMetricFamily, CounterMetricFamily
from prometheus_client.registry import Collector, REGISTRY
from prometheus_client import CollectorRegistry, Metric, make_wsgi_app
from prometheus_client.metrics_core import (CounterMetricFamily,
GaugeMetricFamily)
from prometheus_client.registry import REGISTRY, Collector
from prometheus_flask_exporter import PrometheusMetrics
from sqlalchemy import text
from sqlalchemy.exc import SQLAlchemyError
from werkzeug.middleware.dispatcher import DispatcherMiddleware
from app.api import api
from app.extensions import bootstrap
from app.extensions import db
from app.extensions import migrate
from app.extensions import bootstrap, db, migrate
from app.models.automation import Automation, AutomationState
from app.portal import portal
from app.portal.report import report
from app.tfstate import tfstate
from prometheus_flask_exporter import PrometheusMetrics
app = Flask(__name__)
app.config.from_file("../config.yaml", load=yaml.safe_load)

View file

@ -1,18 +1,20 @@
import sys
from datetime import datetime, timezone
from typing import List, TypedDict, NotRequired, Optional
from typing import List, NotRequired, Optional, TypedDict
from cryptography import x509
from flask import request, abort, jsonify, Blueprint
from flask import Blueprint, abort, jsonify, request
from flask.typing import ResponseReturnValue
from sqlalchemy import exc
from app.api.util import (DOMAIN_NAME_REGEX, MAX_ALLOWED_ITEMS,
MAX_DOMAIN_NAME_LENGTH, ListFilter,
get_single_resource, list_resources,
validate_description)
from app.extensions import db
from app.api.util import ListFilter, MAX_DOMAIN_NAME_LENGTH, DOMAIN_NAME_REGEX, list_resources, MAX_ALLOWED_ITEMS, \
validate_description, get_single_resource
from app.models.base import Group
from app.models.onions import Onion
from app.util.onion import onion_hostname, decode_onion_keys
from app.util.onion import decode_onion_keys, onion_hostname
from app.util.x509 import validate_tls_keys
api_onion = Blueprint('api_onion', __name__)

View file

@ -2,9 +2,9 @@ import base64
import binascii
import logging
import re
from typing import Union, Any, Literal, Type, Callable, Dict, List, Optional
from typing import Any, Callable, Dict, List, Literal, Optional, Type, Union
from flask import abort, request, jsonify
from flask import abort, jsonify, request
from flask.typing import ResponseReturnValue
from sqlalchemy import BinaryExpression, ColumnElement, select

View file

@ -1,10 +1,11 @@
from datetime import datetime, timedelta, timezone
from typing import List
from flask import Blueprint, request, abort
from flask import Blueprint, abort, request
from flask.typing import ResponseReturnValue
from app.api.util import ListFilter, MAX_DOMAIN_NAME_LENGTH, DOMAIN_NAME_REGEX, list_resources, MAX_ALLOWED_ITEMS
from app.api.util import (DOMAIN_NAME_REGEX, MAX_ALLOWED_ITEMS,
MAX_DOMAIN_NAME_LENGTH, ListFilter, list_resources)
from app.models.base import Group
from app.models.mirrors import Origin, Proxy

View file

@ -3,6 +3,7 @@ Bypass Censorship Resource Names.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any

View file

@ -9,15 +9,15 @@ from typing import Any, Callable, Dict, List, Type
from sqlalchemy import inspect
from app import app
from app.cli import _SubparserType, BaseCliHandler
from app.cli import BaseCliHandler, _SubparserType
from app.extensions import db
from app.models.activity import Webhook, Activity
from app.models.automation import AutomationLogs, Automation, AutomationState
from app.models.base import Group, MirrorList, PoolGroup, Pool
from app.models.activity import Activity, Webhook
from app.models.alarms import Alarm, AlarmState
from app.models.automation import Automation, AutomationLogs, AutomationState
from app.models.base import Group, MirrorList, Pool, PoolGroup
from app.models.bridges import Bridge, BridgeConf
from app.models.mirrors import Origin, Proxy, SmartProxy
from app.models.alarms import Alarm, AlarmState
from app.models.onions import Onion, Eotk
from app.models.onions import Eotk, Onion
from app.models.tfstate import TerraformState
Model = Type[db.Model] # type: ignore[name-defined]

View file

@ -1,10 +1,10 @@
import json
import logging
import sys
from typing import Callable, Any
from typing import Any, Callable
from app import app
from app.cli import _SubparserType, BaseCliHandler
from app.cli import BaseCliHandler, _SubparserType
from app.lists import lists
from app.models.base import Pool

View file

@ -1,6 +1,6 @@
from flask_bootstrap import Bootstrap5
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from flask_bootstrap import Bootstrap5
from sqlalchemy import MetaData
convention = {

View file

@ -1,4 +1,4 @@
from typing import Dict, Callable, Any
from typing import Any, Callable, Dict
from app.lists.bc2 import mirror_sites
from app.lists.bridgelines import bridgelines

View file

@ -1,4 +1,5 @@
from typing import List, Optional, TypedDict
from sqlalchemy.orm import selectinload
from app.models.base import Pool

View file

@ -1,4 +1,5 @@
from typing import List, Dict, Optional, TypedDict
from typing import Dict, List, Optional, TypedDict
from sqlalchemy.orm import selectinload
from app.models.base import Pool

View file

@ -7,6 +7,7 @@ from sqlalchemy.orm import Mapped, mapped_column
from app.brm.brn import BRN
from app.extensions import db
from app.models.types import AwareDateTime
class AbstractConfiguration(db.Model): # type: ignore
@ -14,9 +15,9 @@ class AbstractConfiguration(db.Model): # type: ignore
id: Mapped[int] = mapped_column(db.Integer, primary_key=True)
description: Mapped[str]
added: Mapped[datetime] = mapped_column(db.DateTime(timezone=True))
updated: Mapped[datetime] = mapped_column(db.DateTime(timezone=True))
destroyed: Mapped[Optional[datetime]] = mapped_column(db.DateTime(timezone=True), nullable=True)
added: Mapped[datetime] = mapped_column(AwareDateTime())
updated: Mapped[datetime] = mapped_column(AwareDateTime())
destroyed: Mapped[Optional[datetime]] = mapped_column(AwareDateTime(), nullable=True)
@property
@abstractmethod
@ -43,7 +44,7 @@ class Deprecation(db.Model): # type: ignore[name-defined,misc]
id: Mapped[int] = mapped_column(db.Integer, primary_key=True)
resource_type: Mapped[str]
resource_id: Mapped[int]
deprecated_at: Mapped[datetime] = mapped_column(db.DateTime(timezone=True))
deprecated_at: Mapped[datetime] = mapped_column(AwareDateTime())
meta: Mapped[Optional[Dict[str, Any]]] = mapped_column(db.JSON())
reason: Mapped[str]
@ -58,11 +59,11 @@ class AbstractResource(db.Model): # type: ignore
__abstract__ = True
id: Mapped[int] = mapped_column(db.Integer, primary_key=True)
added: Mapped[datetime] = mapped_column(db.DateTime(timezone=True))
updated: Mapped[datetime] = mapped_column(db.DateTime(timezone=True))
deprecated: Mapped[Optional[datetime]] = mapped_column(db.DateTime(timezone=True), nullable=True)
added: Mapped[datetime] = mapped_column(AwareDateTime())
updated: Mapped[datetime] = mapped_column(AwareDateTime())
deprecated: Mapped[Optional[datetime]] = mapped_column(AwareDateTime(), nullable=True)
deprecation_reason: Mapped[Optional[str]]
destroyed: Mapped[Optional[datetime]] = mapped_column(db.DateTime(timezone=True), nullable=True)
destroyed: Mapped[Optional[datetime]] = mapped_column(AwareDateTime(), nullable=True)
def __init__(self, *,
id: Optional[int] = None,

View file

@ -7,6 +7,7 @@ from sqlalchemy.orm import Mapped, mapped_column
from app.brm.brn import BRN
from app.extensions import db
from app.models import AbstractConfiguration
from app.models.types import AwareDateTime
class Activity(db.Model): # type: ignore
@ -14,7 +15,7 @@ class Activity(db.Model): # type: ignore
group_id: Mapped[Optional[int]]
activity_type: Mapped[str]
text: Mapped[str]
added: Mapped[datetime] = mapped_column(db.DateTime(timezone=True))
added: Mapped[datetime] = mapped_column(AwareDateTime())
def __init__(self, *,
id: Optional[int] = None,

View file

@ -6,6 +6,7 @@ from sqlalchemy.orm import Mapped, mapped_column
from app.extensions import db
from app.models.activity import Activity
from app.models.types import AwareDateTime
class AlarmState(enum.Enum):
@ -30,8 +31,8 @@ class Alarm(db.Model): # type: ignore
target: Mapped[str]
aspect: Mapped[str]
alarm_state: Mapped[AlarmState] = mapped_column(default=AlarmState.UNKNOWN)
state_changed: Mapped[datetime] = mapped_column(db.DateTime(timezone=True))
last_updated: Mapped[datetime] = mapped_column(db.DateTime(timezone=True))
state_changed: Mapped[datetime] = mapped_column(AwareDateTime())
last_updated: Mapped[datetime] = mapped_column(AwareDateTime())
text: Mapped[str]
@classmethod

View file

@ -7,6 +7,7 @@ from sqlalchemy.orm import Mapped, mapped_column
from app.brm.brn import BRN
from app.extensions import db
from app.models import AbstractConfiguration, AbstractResource
from app.models.types import AwareDateTime
class AutomationState(enum.Enum):
@ -19,8 +20,8 @@ class Automation(AbstractConfiguration):
short_name: Mapped[str]
state: Mapped[AutomationState] = mapped_column(default=AutomationState.IDLE)
enabled: Mapped[bool]
last_run: Mapped[Optional[datetime]] = mapped_column(db.DateTime(timezone=True))
next_run: Mapped[Optional[datetime]] = mapped_column(db.DateTime(timezone=True))
last_run: Mapped[Optional[datetime]] = mapped_column(AwareDateTime())
next_run: Mapped[Optional[datetime]] = mapped_column(AwareDateTime())
next_is_full: Mapped[bool]
logs = db.relationship("AutomationLogs", back_populates="automation")

View file

@ -8,6 +8,7 @@ from app.brm.brn import BRN
from app.extensions import db
from app.models import AbstractConfiguration, AbstractResource
from app.models.base import Pool
from app.models.types import AwareDateTime
class ProviderAllocation(enum.Enum):
@ -54,7 +55,7 @@ class BridgeConf(AbstractConfiguration):
class Bridge(AbstractResource):
conf_id: Mapped[int] = mapped_column(db.ForeignKey("bridge_conf.id"))
cloud_account_id: Mapped[int] = mapped_column(db.ForeignKey("cloud_account.id"))
terraform_updated: Mapped[Optional[datetime]] = mapped_column(db.DateTime(timezone=True), nullable=True)
terraform_updated: Mapped[Optional[datetime]] = mapped_column(AwareDateTime(), nullable=True)
nickname: Mapped[Optional[str]]
fingerprint: Mapped[Optional[str]]
hashed_fingerprint: Mapped[Optional[str]]

View file

@ -1,5 +1,5 @@
import enum
from typing import Any, Dict, List, TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Dict, List
from sqlalchemy.orm import Mapped, mapped_column, relationship
@ -8,7 +8,6 @@ from app.extensions import db
from app.models import AbstractConfiguration
from app.models.mirrors import StaticOrigin
if TYPE_CHECKING:
from app.models.bridges import Bridge

View file

@ -16,6 +16,7 @@ from app.extensions import db
from app.models import AbstractConfiguration, AbstractResource, Deprecation
from app.models.base import Group, Pool
from app.models.onions import Onion
from app.models.types import AwareDateTime
country_origin = db.Table(
'country_origin',
@ -275,7 +276,7 @@ class Proxy(AbstractResource):
provider: Mapped[str] = mapped_column(db.String(20), nullable=False)
psg: Mapped[Optional[int]] = mapped_column(db.Integer, nullable=True)
slug: Mapped[Optional[str]] = mapped_column(db.String(20), nullable=True)
terraform_updated: Mapped[Optional[datetime]] = mapped_column(db.DateTime(timezone=True), nullable=True)
terraform_updated: Mapped[Optional[datetime]] = mapped_column(AwareDateTime(), nullable=True)
url: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True)
origin: Mapped[Origin] = relationship("Origin", back_populates="proxies")

View file

@ -7,6 +7,7 @@ from app.brm.brn import BRN
from app.extensions import db
from app.models import AbstractConfiguration, AbstractResource
from app.models.base import Group
from app.models.types import AwareDateTime
from app.util.onion import onion_hostname
@ -36,7 +37,7 @@ class Onion(AbstractConfiguration):
group_id: Mapped[int] = mapped_column(db.ForeignKey("group.id"))
domain_name: Mapped[str]
cert_expiry: Mapped[datetime] = mapped_column(db.DateTime(timezone=True))
cert_expiry: Mapped[datetime] = mapped_column(AwareDateTime())
cert_sans: Mapped[str]
onion_public_key: Mapped[bytes]
onion_private_key: Mapped[bytes]

21
app/models/types.py Normal file
View file

@ -0,0 +1,21 @@
from datetime import timezone
from sqlalchemy import DateTime, TypeDecorator
class AwareDateTime(TypeDecorator):
impl = DateTime(timezone=True)
cache_ok = True
def process_bind_param(self, value, dialect):
# Ensure the value is aware. If it's naive, assume UTC.
if value is not None and value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc)
return value
def process_result_value(self, value, dialect):
# Ensure the value is aware. If it's naive, assume UTC.
if value is not None and value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc)
return value

View file

@ -1,6 +1,7 @@
from typing import Optional
from flask import render_template, Response, flash, redirect, url_for, Blueprint
from flask import (Blueprint, Response, flash, redirect, render_template,
url_for)
from flask.typing import ResponseReturnValue
from app.extensions import db

View file

@ -1,9 +1,10 @@
from typing import List, Union, Optional, Dict, Type
from typing import Dict, List, Optional, Type, Union
from flask import render_template, url_for, redirect, Blueprint
from flask import Blueprint, redirect, render_template, url_for
from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm
from wtforms import SelectField, StringField, SubmitField, IntegerField, BooleanField, Form, FormField
from wtforms import (BooleanField, Form, FormField, IntegerField, SelectField,
StringField, SubmitField)
from wtforms.validators import InputRequired
from app.extensions import db

View file

@ -1,4 +1,4 @@
from flask import current_app, render_template, Blueprint, Response
from flask import Blueprint, Response, current_app, render_template
from flask.typing import ResponseReturnValue
from sqlalchemy import desc

View file

@ -1,5 +1,5 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, SelectField
from wtforms import SelectField, StringField, SubmitField
class EditMirrorForm(FlaskForm): # type: ignore

View file

@ -1,6 +1,6 @@
from typing import Optional
from flask import redirect, Blueprint
from flask import Blueprint, redirect
from flask.typing import ResponseReturnValue
from app.models.onions import Onion

View file

@ -2,15 +2,16 @@ import logging
import secrets
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_wtf import FlaskForm
import sqlalchemy
from wtforms import StringField, SubmitField, SelectField
from wtforms import SelectField, StringField, SubmitField
from wtforms.validators import DataRequired
from app.extensions import db
from app.models.base import Pool, Group
from app.models.base import Group, Pool
from app.portal.util import LifecycleForm
bp = Blueprint("pool", __name__)

View file

@ -1,4 +1,5 @@
from flask import render_template, Response, flash, redirect, url_for, Blueprint
from flask import (Blueprint, Response, flash, redirect, render_template,
url_for)
from flask.typing import ResponseReturnValue
from sqlalchemy import desc

View file

@ -1,4 +1,4 @@
from flask import render_template, Blueprint
from flask import Blueprint, render_template
from flask.typing import ResponseReturnValue
from sqlalchemy import desc

View file

@ -1,18 +1,20 @@
import logging
from typing import Optional, List, Any
from typing import Any, List, Optional
import sqlalchemy.exc
from flask import flash, redirect, url_for, render_template, Response, Blueprint, current_app
from flask import (Blueprint, Response, current_app, flash, redirect,
render_template, url_for)
from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm
from sqlalchemy import exc
from wtforms import StringField, SelectField, SubmitField, BooleanField, FileField
from wtforms import (BooleanField, FileField, SelectField, StringField,
SubmitField)
from wtforms.validators import DataRequired
from app.brm.static import create_static_origin
from app.models.base import Group
from app.models.cloud import CloudAccount, CloudProvider
from app.models.mirrors import StaticOrigin, Origin
from app.models.mirrors import Origin, StaticOrigin
from app.portal.util import response_404, view_lifecycle
bp = Blueprint("static", __name__)

View file

@ -1,4 +1,4 @@
from flask import Response, render_template, flash, redirect, url_for
from flask import Response, flash, redirect, render_template, url_for
from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm
from wtforms import SubmitField

View file

@ -1,7 +1,7 @@
import os
import stat
from typing import Tuple, Any, Optional
from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED
from typing import Any, Optional, Tuple
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
import jinja2

View file

@ -1,4 +1,4 @@
from typing import Tuple, Optional
from typing import Optional, Tuple
import boto3
from sqlalchemy import func
@ -6,8 +6,8 @@ from sqlalchemy import func
from app import app
from app.alarms import get_or_create_alarm
from app.extensions import db
from app.models.base import Group
from app.models.alarms import AlarmState
from app.models.base import Group
from app.models.onions import Eotk
from app.terraform import BaseAutomation

View file

@ -7,8 +7,8 @@ from app import app
from app.alarms import get_or_create_alarm
from app.brm.brn import BRN
from app.extensions import db
from app.models.mirrors import Proxy
from app.models.alarms import AlarmState
from app.models.mirrors import Proxy
from app.terraform import BaseAutomation

View file

@ -1,4 +1,4 @@
from typing import Tuple, Optional
from typing import Optional, Tuple
import boto3
from sqlalchemy import func
@ -6,8 +6,8 @@ from sqlalchemy import func
from app import app
from app.alarms import get_or_create_alarm
from app.extensions import db
from app.models.base import Group
from app.models.alarms import AlarmState
from app.models.base import Group
from app.models.mirrors import SmartProxy
from app.terraform import BaseAutomation

View file

@ -1,7 +1,8 @@
from flask import current_app
from github import Github
from app.terraform.block.bridge_reachability import BlockBridgeReachabilityAutomation
from app.terraform.block.bridge_reachability import \
BlockBridgeReachabilityAutomation
class BlockBridgeGitHubAutomation(BlockBridgeReachabilityAutomation):

View file

@ -1,7 +1,8 @@
from flask import current_app
from gitlab import Gitlab
from app.terraform.block.bridge_reachability import BlockBridgeReachabilityAutomation
from app.terraform.block.bridge_reachability import \
BlockBridgeReachabilityAutomation
class BlockBridgeGitlabAutomation(BlockBridgeReachabilityAutomation):

View file

@ -2,7 +2,7 @@ import json
import logging
from io import BytesIO
from typing import Any, Optional
from zipfile import ZipFile, BadZipFile
from zipfile import BadZipFile, ZipFile
import lxml # nosec: B410
import requests

View file

@ -1,7 +1,7 @@
from collections.abc import Mapping, Sequence
import json
import os
from typing import List, Any
from collections.abc import Mapping, Sequence
from typing import Any, List
from app import app
from app.lists import lists

View file

@ -1,9 +1,9 @@
import datetime
import os.path
import sys
from abc import abstractmethod
import datetime
from collections import defaultdict
from typing import Optional, Any, List, Dict
from typing import Any, Dict, List, Optional
from flask import current_app
from sqlalchemy import text
@ -11,7 +11,7 @@ from sqlalchemy import text
from app import app
from app.extensions import db
from app.models.base import Group
from app.models.mirrors import Proxy, Origin, SmartProxy
from app.models.mirrors import Origin, Proxy, SmartProxy
from app.terraform.terraform import TerraformAutomation

View file

@ -1,4 +1,4 @@
from typing import Optional, Any
from typing import Any, Optional
from app.extensions import db
from app.models.mirrors import Proxy

View file

@ -235,6 +235,7 @@ class ProxyMetaAutomation(BaseAutomation):
"""
pools = Pool.query.all()
for pool in pools:
logging.debug(pool.added < datetime.now(tz=timezone.utc))
if pool.id == -1:
continue # Skip hotspare pool
logging.debug("Missing proxy check for %s", pool.pool_name)

View file

@ -1,6 +1,6 @@
import json
from flask import Blueprint, request, Response
from flask import Blueprint, Response, request
from flask.typing import ResponseReturnValue
from app.extensions import db

View file

@ -1,6 +1,6 @@
import base64
import hashlib
from typing import Tuple, Optional, List, Dict
from typing import Dict, List, Optional, Tuple
def onion_hostname(onion_public_key: bytes) -> str:

View file

@ -1,12 +1,12 @@
import ssl
from datetime import datetime, timezone
from typing import Optional, Tuple, List, Dict, TYPE_CHECKING
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
from cryptography import x509
from cryptography.hazmat._oid import ExtensionOID
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey

View file

@ -0,0 +1,85 @@
"""switch to timezone aware
Revision ID: cb3d6f0cdb86
Revises: 54b31e87fe33
Create Date: 2024-12-06 17:34:51.630311
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cb3d6f0cdb86'
down_revision = '54b31e87fe33'
branch_labels = None
depends_on = None
def upgrade():
def alter_column_to_timezone_aware(table_name, column_name):
with op.batch_alter_table(table_name, schema=None) as batch_op:
batch_op.alter_column(
column_name,
type_=sa.DateTime(timezone=True)
)
# AbstractConfiguration derived tables
configuration_tables = [
"automation",
"cloud_account",
"bridge_conf",
"country",
"group",
"mirror_list",
"onion",
"origin",
"pool",
"static_origin",
"webhook"
]
for t in configuration_tables:
alter_column_to_timezone_aware(t, 'added')
alter_column_to_timezone_aware(t, 'updated')
alter_column_to_timezone_aware(t, 'destroyed')
# AbstractResource derived tables
resource_tables = [
"bridge",
"proxy",
"smart_proxy",
"automation_logs",
"eotk"
]
for t in resource_tables:
alter_column_to_timezone_aware(t, 'added')
alter_column_to_timezone_aware(t, 'updated')
alter_column_to_timezone_aware(t, 'deprecated')
alter_column_to_timezone_aware(t, 'destroyed')
# Deprecation
alter_column_to_timezone_aware("deprecation", "deprecated_at")
# Activity
alter_column_to_timezone_aware("activity", "added")
# Alarm
alter_column_to_timezone_aware("alarm", "state_changed")
alter_column_to_timezone_aware("alarm", "last_updated")
# Bridge terraform_updated
alter_column_to_timezone_aware("bridge", "terraform_updated")
# Proxy terraform_updated
alter_column_to_timezone_aware("proxy", "terraform_updated")
# Automation last_run, next_run
alter_column_to_timezone_aware("automation", "last_run")
alter_column_to_timezone_aware("automation", "next_run")
# Onion cert_expiry
alter_column_to_timezone_aware("onion", "cert_expiry")
def downgrade():
pass