lots of typing fixes
This commit is contained in:
parent
51f580a304
commit
3665c34961
43 changed files with 260 additions and 178 deletions
|
@ -1,5 +1,5 @@
|
||||||
import boto3 as boto3
|
from flask import Flask, redirect, url_for
|
||||||
from flask import Flask, jsonify, Response, redirect, url_for
|
from flask.typing import ResponseReturnValue
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
@ -20,37 +20,9 @@ app.register_blueprint(portal, url_prefix="/portal")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index() -> ResponseReturnValue:
|
||||||
return redirect(url_for("portal.portal_home"))
|
return redirect(url_for("portal.portal_home"))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/import/cloudfront')
|
|
||||||
def import_cloudfront():
|
|
||||||
a = ""
|
|
||||||
not_found = []
|
|
||||||
cloudfront = boto3.client('cloudfront',
|
|
||||||
aws_access_key_id=app.config['AWS_ACCESS_KEY'],
|
|
||||||
aws_secret_access_key=app.config['AWS_SECRET_KEY'])
|
|
||||||
dist_paginator = cloudfront.get_paginator('list_distributions')
|
|
||||||
page_iterator = dist_paginator.paginate()
|
|
||||||
for page in page_iterator:
|
|
||||||
for dist in page['DistributionList']['Items']:
|
|
||||||
res = Proxy.query.all()
|
|
||||||
matches = [r for r in res if r.origin.domain_name == dist['Comment'][8:]]
|
|
||||||
if not matches:
|
|
||||||
not_found.append(dist['Comment'][8:])
|
|
||||||
continue
|
|
||||||
a += f"# {dist['Comment'][8:]}\n"
|
|
||||||
a += f"terraform import module.cloudfront_{matches[0].id}.aws_cloudfront_distribution.this {dist['Id']}\n"
|
|
||||||
for n in not_found:
|
|
||||||
a += f"# Not found: {n}\n"
|
|
||||||
return Response(a, content_type="text/plain")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mirrorSites.json')
|
|
||||||
def json_mirror_sites():
|
|
||||||
return jsonify(mirror_sites)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run()
|
app.run()
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.alarms import Alarm
|
from app.models.alarms import Alarm
|
||||||
|
@ -7,9 +8,10 @@ from app.models.alarms import Alarm
|
||||||
def _get_alarm(target: str,
|
def _get_alarm(target: str,
|
||||||
alarm_type: str,
|
alarm_type: str,
|
||||||
*,
|
*,
|
||||||
proxy_id=None,
|
proxy_id: Optional[int] = None,
|
||||||
origin_id=None,
|
origin_id: Optional[int] = None,
|
||||||
create_if_missing=True):
|
create_if_missing: bool = True) -> Optional[Alarm]:
|
||||||
|
alarm: Optional[Alarm]
|
||||||
if target == "proxy":
|
if target == "proxy":
|
||||||
alarm = Alarm.query.filter(
|
alarm = Alarm.query.filter(
|
||||||
Alarm.target == "proxy",
|
Alarm.target == "proxy",
|
||||||
|
@ -38,5 +40,7 @@ def _get_alarm(target: str,
|
||||||
return alarm
|
return alarm
|
||||||
|
|
||||||
|
|
||||||
def get_proxy_alarm(proxy_id: int, alarm_type: str):
|
def get_proxy_alarm(proxy_id: int, alarm_type: str) -> Alarm:
|
||||||
return _get_alarm("proxy", alarm_type, proxy_id=proxy_id)
|
alarm = _get_alarm("proxy", alarm_type, proxy_id=proxy_id)
|
||||||
|
assert(alarm is not None)
|
||||||
|
return alarm
|
||||||
|
|
|
@ -2,13 +2,14 @@ import argparse
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from os.path import basename
|
from os.path import basename
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from app.cli.automate import AutomateCliHandler
|
from app.cli.automate import AutomateCliHandler
|
||||||
from app.cli.db import DbCliHandler
|
from app.cli.db import DbCliHandler
|
||||||
from app.cli.list import ListCliHandler
|
from app.cli.list import ListCliHandler
|
||||||
|
|
||||||
|
|
||||||
def parse_args(argv):
|
def parse_args(argv: List[str]) -> None:
|
||||||
if basename(argv[0]) == "__main__.py":
|
if basename(argv[0]) == "__main__.py":
|
||||||
argv[0] = "bypass"
|
argv[0] = "bypass"
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
|
|
|
@ -3,6 +3,7 @@ import csv
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
@ -11,6 +12,11 @@ from app.models.bridges import Bridge, BridgeConf
|
||||||
from app.models.mirrors import Mirror, Origin, Proxy
|
from app.models.mirrors import Mirror, Origin, Proxy
|
||||||
from app.models.alarms import Alarm, AlarmState
|
from app.models.alarms import Alarm, AlarmState
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
_SubparserType = argparse._SubParsersAction[argparse.ArgumentParser]
|
||||||
|
else:
|
||||||
|
_SubparserType = Any
|
||||||
|
|
||||||
models = {
|
models = {
|
||||||
"bridge": Bridge,
|
"bridge": Bridge,
|
||||||
"bridgeconf": BridgeConf,
|
"bridgeconf": BridgeConf,
|
||||||
|
@ -23,14 +29,14 @@ models = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def export(model: db.Model):
|
def export(model: db.Model) -> None:
|
||||||
out = csv.writer(sys.stdout)
|
out = csv.writer(sys.stdout)
|
||||||
out.writerow(model.csv_header())
|
out.writerow(model.csv_header())
|
||||||
for r in model.query.all():
|
for r in model.query.all():
|
||||||
out.writerow(r.csv_row())
|
out.writerow(r.csv_row())
|
||||||
|
|
||||||
|
|
||||||
def impot(model: db.Model):
|
def impot(model: db.Model) -> None:
|
||||||
first = True
|
first = True
|
||||||
header = model.csv_header()
|
header = model.csv_header()
|
||||||
try:
|
try:
|
||||||
|
@ -46,15 +52,15 @@ def impot(model: db.Model):
|
||||||
if header[i] in ["added", "updated", "destroyed", "deprecated", "last_updated", "terraform_updated"]:
|
if header[i] in ["added", "updated", "destroyed", "deprecated", "last_updated", "terraform_updated"]:
|
||||||
# datetime fields
|
# datetime fields
|
||||||
if line[i] == "":
|
if line[i] == "":
|
||||||
line[i] = None
|
line[i] = None # type: ignore
|
||||||
else:
|
else:
|
||||||
line[i] = datetime.datetime.strptime(line[i], "%Y-%m-%d %H:%M:%S.%f")
|
line[i] = datetime.datetime.strptime(line[i], "%Y-%m-%d %H:%M:%S.%f") # type: ignore
|
||||||
elif header[i] in ["eotk"]:
|
elif header[i] in ["eotk"]:
|
||||||
# boolean fields
|
# boolean fields
|
||||||
line[i] = line[i] == "True"
|
line[i] = line[i] == "True" # type: ignore
|
||||||
elif header[i].endswith("_id") and line[i] == "":
|
elif header[i].endswith("_id") and line[i] == "":
|
||||||
# integer foreign keys
|
# integer foreign keys
|
||||||
line[i] = None
|
line[i] = None # type: ignore
|
||||||
elif header[i] in ["alarm_state"]:
|
elif header[i] in ["alarm_state"]:
|
||||||
# alarm states
|
# alarm states
|
||||||
line[i] = getattr(AlarmState, line[i][len("AlarmState."):])
|
line[i] = getattr(AlarmState, line[i][len("AlarmState."):])
|
||||||
|
@ -69,7 +75,7 @@ def impot(model: db.Model):
|
||||||
|
|
||||||
class DbCliHandler:
|
class DbCliHandler:
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_subparser_to(cls, subparsers: argparse._SubParsersAction) -> None:
|
def add_subparser_to(cls, subparsers: _SubparserType) -> None:
|
||||||
parser = subparsers.add_parser("db", help="database operations")
|
parser = subparsers.add_parser("db", help="database operations")
|
||||||
parser.add_argument("--export", choices=sorted(models.keys()),
|
parser.add_argument("--export", choices=sorted(models.keys()),
|
||||||
help="export data to CSV format")
|
help="export data to CSV format")
|
||||||
|
@ -77,10 +83,10 @@ class DbCliHandler:
|
||||||
help="import data from CSV format", dest="impot")
|
help="import data from CSV format", dest="impot")
|
||||||
parser.set_defaults(cls=cls)
|
parser.set_defaults(cls=cls)
|
||||||
|
|
||||||
def __init__(self, args):
|
def __init__(self, args: argparse.Namespace) -> None:
|
||||||
self.args = args
|
self.args = args
|
||||||
|
|
||||||
def run(self):
|
def run(self) -> None:
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
if self.args.export:
|
if self.args.export:
|
||||||
export(models[self.args.export])
|
export(models[self.args.export])
|
||||||
|
|
|
@ -2,35 +2,40 @@ import argparse
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from typing import Callable
|
from typing import Callable, TYPE_CHECKING, Any, Dict
|
||||||
|
|
||||||
from app import app, mirror_sites
|
from app import app, mirror_sites
|
||||||
from app.lists.bridgelines import bridgelines
|
from app.lists.bridgelines import bridgelines
|
||||||
from app.lists.mirror_mapping import mirror_mapping
|
from app.lists.mirror_mapping import mirror_mapping
|
||||||
|
|
||||||
lists = {
|
if TYPE_CHECKING:
|
||||||
|
_SubparserType = argparse._SubParsersAction[argparse.ArgumentParser]
|
||||||
|
else:
|
||||||
|
_SubparserType = Any
|
||||||
|
|
||||||
|
lists: Dict[str, Callable[[], Any]] = {
|
||||||
"mirror_mapping": mirror_mapping,
|
"mirror_mapping": mirror_mapping,
|
||||||
"bc2": mirror_sites,
|
"bc2": mirror_sites,
|
||||||
"bridgelines": bridgelines,
|
"bridgelines": bridgelines,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def dump(list_f: Callable):
|
def dump(list_f: Callable[[], Any]) -> None:
|
||||||
json.dump(list_f(), sys.stdout, indent=2)
|
json.dump(list_f(), sys.stdout, indent=2)
|
||||||
|
|
||||||
|
|
||||||
class ListCliHandler:
|
class ListCliHandler:
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_subparser_to(cls, subparsers: argparse._SubParsersAction) -> None:
|
def add_subparser_to(cls, subparsers: _SubparserType) -> None:
|
||||||
parser = subparsers.add_parser("list", help="list operations")
|
parser = subparsers.add_parser("list", help="list operations")
|
||||||
parser.add_argument("--dump", choices=sorted(lists.keys()),
|
parser.add_argument("--dump", choices=sorted(lists.keys()),
|
||||||
help="dump a list in JSON format")
|
help="dump a list in JSON format")
|
||||||
parser.set_defaults(cls=cls)
|
parser.set_defaults(cls=cls)
|
||||||
|
|
||||||
def __init__(self, args):
|
def __init__(self, args: argparse.Namespace) -> None:
|
||||||
self.args = args
|
self.args = args
|
||||||
|
|
||||||
def run(self):
|
def run(self) -> None:
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
if self.args.dump:
|
if self.args.dump:
|
||||||
dump(lists[self.args.dump])
|
dump(lists[self.args.dump])
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import builtins
|
import builtins
|
||||||
from typing import List, Iterable
|
from typing import List, Iterable, Dict, Any, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ class Bridgelines(BaseModel):
|
||||||
title = "Bridgelines Version 1"
|
title = "Bridgelines Version 1"
|
||||||
|
|
||||||
|
|
||||||
def bridgelines(*, distribution_method: str = None):
|
def bridgelines(*, distribution_method: Optional[str] = None) -> Dict[str, Any]:
|
||||||
bridges: Iterable[Bridge] = Bridge.query.filter(
|
bridges: Iterable[Bridge] = Bridge.query.filter(
|
||||||
Bridge.destroyed == None,
|
Bridge.destroyed == None,
|
||||||
Bridge.deprecated == None,
|
Bridge.deprecated == None,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import builtins
|
import builtins
|
||||||
from typing import Dict, List
|
from typing import Dict, List, Union
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from tldextract import extract
|
from tldextract import extract
|
||||||
|
@ -29,7 +29,7 @@ class MirrorMapping(BaseModel):
|
||||||
title = "Mirror Mapping Version 1.1"
|
title = "Mirror Mapping Version 1.1"
|
||||||
|
|
||||||
|
|
||||||
def mirror_mapping():
|
def mirror_mapping() -> Dict[str, Union[str, Dict[str, str]]]:
|
||||||
from app import app
|
from app import app
|
||||||
return MirrorMapping(
|
return MirrorMapping(
|
||||||
version="1.1",
|
version="1.1",
|
||||||
|
|
|
@ -4,7 +4,7 @@ from typing import Union, List, Optional, Any
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
|
||||||
|
|
||||||
class AbstractConfiguration(db.Model):
|
class AbstractConfiguration(db.Model): # type: ignore
|
||||||
__abstract__ = True
|
__abstract__ = True
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
@ -13,23 +13,23 @@ class AbstractConfiguration(db.Model):
|
||||||
updated = db.Column(db.DateTime(), default=datetime.utcnow, nullable=False)
|
updated = db.Column(db.DateTime(), default=datetime.utcnow, nullable=False)
|
||||||
destroyed = db.Column(db.DateTime(), nullable=True)
|
destroyed = db.Column(db.DateTime(), nullable=True)
|
||||||
|
|
||||||
def destroy(self):
|
def destroy(self) -> None:
|
||||||
self.destroyed = datetime.utcnow()
|
self.destroyed = datetime.utcnow()
|
||||||
self.updated = datetime.utcnow()
|
self.updated = datetime.utcnow()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def csv_header(cls):
|
def csv_header(cls) -> List[str]:
|
||||||
return [
|
return [
|
||||||
"id", "description", "added", "updated", "destroyed"
|
"id", "description", "added", "updated", "destroyed"
|
||||||
]
|
]
|
||||||
|
|
||||||
def csv_row(self):
|
def csv_row(self) -> List[Any]:
|
||||||
return [
|
return [
|
||||||
getattr(self, x) for x in self.csv_header()
|
getattr(self, x) for x in self.csv_header()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class AbstractResource(db.Model):
|
class AbstractResource(db.Model): # type: ignore
|
||||||
__abstract__ = True
|
__abstract__ = True
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
|
@ -7,7 +7,7 @@ from app.models import AbstractConfiguration
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
|
||||||
|
|
||||||
class Activity(db.Model):
|
class Activity(db.Model): # type: ignore
|
||||||
id = db.Column(db.Integer(), primary_key=True)
|
id = db.Column(db.Integer(), primary_key=True)
|
||||||
group_id = db.Column(db.Integer(), nullable=True)
|
group_id = db.Column(db.Integer(), nullable=True)
|
||||||
activity_type = db.Column(db.String(20), nullable=False)
|
activity_type = db.Column(db.String(20), nullable=False)
|
||||||
|
@ -49,7 +49,7 @@ class Webhook(AbstractConfiguration):
|
||||||
format = db.Column(db.String(20))
|
format = db.Column(db.String(20))
|
||||||
url = db.Column(db.String(255))
|
url = db.Column(db.String(255))
|
||||||
|
|
||||||
def send(self, text: str):
|
def send(self, text: str) -> None:
|
||||||
if self.format == "telegram":
|
if self.format == "telegram":
|
||||||
data = {"text": text}
|
data = {"text": text}
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import enum
|
import enum
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import List, Any
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
|
||||||
|
@ -11,7 +12,7 @@ class AlarmState(enum.Enum):
|
||||||
CRITICAL = 3
|
CRITICAL = 3
|
||||||
|
|
||||||
|
|
||||||
class Alarm(db.Model):
|
class Alarm(db.Model): # type: ignore
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
target = db.Column(db.String(60), nullable=False)
|
target = db.Column(db.String(60), nullable=False)
|
||||||
group_id = db.Column(db.Integer, db.ForeignKey("group.id"))
|
group_id = db.Column(db.Integer, db.ForeignKey("group.id"))
|
||||||
|
@ -30,18 +31,18 @@ class Alarm(db.Model):
|
||||||
bridge = db.relationship("Bridge", back_populates="alarms")
|
bridge = db.relationship("Bridge", back_populates="alarms")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def csv_header(cls):
|
def csv_header(cls) -> List[str]:
|
||||||
return [
|
return [
|
||||||
"id", "target", "group_id", "origin_id", "proxy_id", "bridge_id", "alarm_type",
|
"id", "target", "group_id", "origin_id", "proxy_id", "bridge_id", "alarm_type",
|
||||||
"alarm_state", "state_changed", "last_updated", "text"
|
"alarm_state", "state_changed", "last_updated", "text"
|
||||||
]
|
]
|
||||||
|
|
||||||
def csv_row(self):
|
def csv_row(self) -> List[Any]:
|
||||||
return [
|
return [
|
||||||
getattr(self, x) for x in self.csv_header()
|
getattr(self, x) for x in self.csv_header()
|
||||||
]
|
]
|
||||||
|
|
||||||
def update_state(self, state: AlarmState, text: str):
|
def update_state(self, state: AlarmState, text: str) -> None:
|
||||||
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.utcnow()
|
||||||
self.alarm_state = state
|
self.alarm_state = state
|
||||||
|
|
|
@ -21,7 +21,7 @@ class Automation(AbstractConfiguration):
|
||||||
|
|
||||||
logs = db.relationship("AutomationLogs", back_populates="automation")
|
logs = db.relationship("AutomationLogs", back_populates="automation")
|
||||||
|
|
||||||
def kick(self):
|
def kick(self) -> None:
|
||||||
self.enabled = True
|
self.enabled = True
|
||||||
self.next_run = datetime.datetime.utcnow()
|
self.next_run = datetime.datetime.utcnow()
|
||||||
self.updated = datetime.datetime.utcnow()
|
self.updated = datetime.datetime.utcnow()
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import AbstractConfiguration
|
from app.models import AbstractConfiguration
|
||||||
|
@ -15,7 +16,7 @@ class Group(AbstractConfiguration):
|
||||||
alarms = db.relationship("Alarm", back_populates="group")
|
alarms = db.relationship("Alarm", back_populates="group")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def csv_header(cls):
|
def csv_header(cls) -> List[str]:
|
||||||
return super().csv_header() + [
|
return super().csv_header() + [
|
||||||
"group_name", "eotk"
|
"group_name", "eotk"
|
||||||
]
|
]
|
||||||
|
@ -43,21 +44,21 @@ class MirrorList(AbstractConfiguration):
|
||||||
"bridgelines": "Tor Bridge Lines"
|
"bridgelines": "Tor Bridge Lines"
|
||||||
}
|
}
|
||||||
|
|
||||||
def destroy(self):
|
def destroy(self) -> None:
|
||||||
self.destroyed = datetime.utcnow()
|
self.destroyed = datetime.utcnow()
|
||||||
self.updated = datetime.utcnow()
|
self.updated = datetime.utcnow()
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
def url(self):
|
def url(self) -> str:
|
||||||
if self.provider == "gitlab":
|
if self.provider == "gitlab":
|
||||||
return f"https://gitlab.com/{self.container}/-/raw/{self.branch}/{self.filename}"
|
return f"https://gitlab.com/{self.container}/-/raw/{self.branch}/{self.filename}"
|
||||||
if self.provider == "github":
|
if self.provider == "github":
|
||||||
return f"https://raw.githubusercontent.com/{self.container}/{self.branch}/{self.filename}"
|
return f"https://raw.githubusercontent.com/{self.container}/{self.branch}/{self.filename}"
|
||||||
if self.provider == "s3":
|
if self.provider == "s3":
|
||||||
return f"s3://{self.container}/{self.filename}"
|
return f"s3://{self.container}/{self.filename}"
|
||||||
|
return "Unknown provider"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def csv_header(cls):
|
def csv_header(cls) -> List[str]:
|
||||||
return super().csv_header() + [
|
return super().csv_header() + [
|
||||||
"provider", "format", "container", "branch", "filename"
|
"provider", "format", "container", "branch", "filename"
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import AbstractConfiguration, AbstractResource
|
from app.models import AbstractConfiguration, AbstractResource
|
||||||
|
@ -14,7 +15,7 @@ class BridgeConf(AbstractConfiguration):
|
||||||
group = db.relationship("Group", back_populates="bridgeconfs")
|
group = db.relationship("Group", back_populates="bridgeconfs")
|
||||||
bridges = db.relationship("Bridge", back_populates="conf")
|
bridges = db.relationship("Bridge", back_populates="conf")
|
||||||
|
|
||||||
def destroy(self):
|
def destroy(self) -> None:
|
||||||
self.destroyed = datetime.utcnow()
|
self.destroyed = datetime.utcnow()
|
||||||
self.updated = datetime.utcnow()
|
self.updated = datetime.utcnow()
|
||||||
for bridge in self.bridges:
|
for bridge in self.bridges:
|
||||||
|
@ -23,7 +24,7 @@ class BridgeConf(AbstractConfiguration):
|
||||||
bridge.updated = datetime.utcnow()
|
bridge.updated = datetime.utcnow()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def csv_header(cls):
|
def csv_header(cls) -> List[str]:
|
||||||
return super().csv_header() + [
|
return super().csv_header() + [
|
||||||
"group_id", "provider", "method", "description", "number"
|
"group_id", "provider", "method", "description", "number"
|
||||||
]
|
]
|
||||||
|
@ -41,7 +42,7 @@ class Bridge(AbstractResource):
|
||||||
alarms = db.relationship("Alarm", back_populates="bridge")
|
alarms = db.relationship("Alarm", back_populates="bridge")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def csv_header(cls):
|
def csv_header(cls) -> List[str]:
|
||||||
return super().csv_header() + [
|
return super().csv_header() + [
|
||||||
"conf_id", "terraform_updated", "nickname", "fingerprint", "hashed_fingerprint", "bridgeline"
|
"conf_id", "terraform_updated", "nickname", "fingerprint", "hashed_fingerprint", "bridgeline"
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
|
|
||||||
from tldextract import extract
|
from tldextract import extract
|
||||||
|
|
||||||
|
@ -18,12 +18,12 @@ class Origin(AbstractConfiguration):
|
||||||
alarms = db.relationship("Alarm", back_populates="origin")
|
alarms = db.relationship("Alarm", back_populates="origin")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def csv_header(cls):
|
def csv_header(cls) -> List[str]:
|
||||||
return super().csv_header() + [
|
return super().csv_header() + [
|
||||||
"group_id", "domain_name"
|
"group_id", "domain_name"
|
||||||
]
|
]
|
||||||
|
|
||||||
def destroy(self):
|
def destroy(self) -> None:
|
||||||
super().destroy()
|
super().destroy()
|
||||||
for proxy in self.proxies:
|
for proxy in self.proxies:
|
||||||
proxy.destroy()
|
proxy.destroy()
|
||||||
|
@ -33,7 +33,8 @@ class Origin(AbstractConfiguration):
|
||||||
onion = Onion.query.filter(Onion.domain_name == tld).first()
|
onion = Onion.query.filter(Onion.domain_name == tld).first()
|
||||||
if not onion:
|
if not onion:
|
||||||
return None
|
return None
|
||||||
return self.domain_name.replace(tld, f"{onion.onion_name}")
|
domain_name: str = self.domain_name
|
||||||
|
return domain_name.replace(tld, f"{onion.onion_name}")
|
||||||
|
|
||||||
|
|
||||||
class Proxy(AbstractResource):
|
class Proxy(AbstractResource):
|
||||||
|
@ -48,7 +49,7 @@ class Proxy(AbstractResource):
|
||||||
alarms = db.relationship("Alarm", back_populates="proxy")
|
alarms = db.relationship("Alarm", back_populates="proxy")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def csv_header(cls):
|
def csv_header(cls) -> List[str]:
|
||||||
return super().csv_header() + [
|
return super().csv_header() + [
|
||||||
"origin_id", "provider", "psg", "slug", "terraform_updated", "url"
|
"origin_id", "provider", "psg", "slug", "terraform_updated", "url"
|
||||||
]
|
]
|
||||||
|
@ -61,7 +62,7 @@ class Mirror(AbstractResource):
|
||||||
origin = db.relationship("Origin", back_populates="mirrors")
|
origin = db.relationship("Origin", back_populates="mirrors")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def csv_header(cls):
|
def csv_header(cls) -> List[str]:
|
||||||
return super().csv_header() + [
|
return super().csv_header() + [
|
||||||
"origin_id", "url"
|
"origin_id", "url"
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from flask import Blueprint, render_template, request
|
from flask import Blueprint, render_template, request
|
||||||
|
from flask.typing import ResponseReturnValue
|
||||||
from sqlalchemy import desc, or_
|
from sqlalchemy import desc, or_
|
||||||
|
|
||||||
from app.models.activity import Activity
|
from app.models.activity import Activity
|
||||||
|
@ -43,13 +45,13 @@ def calculate_mirror_expiry(s: datetime) -> str:
|
||||||
|
|
||||||
|
|
||||||
@portal.app_template_filter("format_datetime")
|
@portal.app_template_filter("format_datetime")
|
||||||
def format_datetime(s: datetime) -> str:
|
def format_datetime(s: Optional[datetime]) -> str:
|
||||||
if s is None:
|
if s is None:
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
return s.strftime("%a, %d %b %Y %H:%M:%S")
|
return s.strftime("%a, %d %b %Y %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
def total_origins_blocked():
|
def total_origins_blocked() -> int:
|
||||||
count = 0
|
count = 0
|
||||||
for o in Origin.query.filter(Origin.destroyed == None).all():
|
for o in Origin.query.filter(Origin.destroyed == None).all():
|
||||||
for a in o.alarms:
|
for a in o.alarms:
|
||||||
|
@ -59,8 +61,9 @@ def total_origins_blocked():
|
||||||
break
|
break
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
@portal.route("/")
|
@portal.route("/")
|
||||||
def portal_home():
|
def portal_home() -> ResponseReturnValue:
|
||||||
groups = Group.query.order_by(Group.group_name).all()
|
groups = Group.query.order_by(Group.group_name).all()
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
proxies = Proxy.query.filter(Proxy.destroyed == None).all()
|
proxies = Proxy.query.filter(Proxy.destroyed == None).all()
|
||||||
|
@ -86,7 +89,7 @@ def portal_home():
|
||||||
|
|
||||||
|
|
||||||
@portal.route("/search")
|
@portal.route("/search")
|
||||||
def search():
|
def search() -> ResponseReturnValue:
|
||||||
query = request.args.get("query")
|
query = request.args.get("query")
|
||||||
proxies = Proxy.query.filter(or_(Proxy.url.contains(query)), Proxy.destroyed == None).all()
|
proxies = Proxy.query.filter(or_(Proxy.url.contains(query)), Proxy.destroyed == None).all()
|
||||||
origins = Origin.query.filter(or_(Origin.description.contains(query), Origin.domain_name.contains(query))).all()
|
origins = Origin.query.filter(or_(Origin.description.contains(query), Origin.domain_name.contains(query))).all()
|
||||||
|
@ -94,7 +97,7 @@ def search():
|
||||||
|
|
||||||
|
|
||||||
@portal.route('/alarms')
|
@portal.route('/alarms')
|
||||||
def view_alarms():
|
def view_alarms() -> ResponseReturnValue:
|
||||||
one_day_ago = datetime.now(timezone.utc) - timedelta(days=1)
|
one_day_ago = datetime.now(timezone.utc) - timedelta(days=1)
|
||||||
alarms = Alarm.query.filter(Alarm.last_updated >= one_day_ago).order_by(
|
alarms = Alarm.query.filter(Alarm.last_updated >= one_day_ago).order_by(
|
||||||
desc(Alarm.alarm_state), desc(Alarm.state_changed)).all()
|
desc(Alarm.alarm_state), desc(Alarm.state_changed)).all()
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from flask import render_template, flash, Response, Blueprint
|
from flask import render_template, flash, Response, Blueprint
|
||||||
|
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 SubmitField, BooleanField
|
||||||
|
@ -12,13 +14,13 @@ from app.portal.util import view_lifecycle, response_404
|
||||||
bp = Blueprint("automation", __name__)
|
bp = Blueprint("automation", __name__)
|
||||||
|
|
||||||
|
|
||||||
class EditAutomationForm(FlaskForm):
|
class EditAutomationForm(FlaskForm): # type: ignore
|
||||||
enabled = BooleanField('Enabled')
|
enabled = BooleanField('Enabled')
|
||||||
submit = SubmitField('Save Changes')
|
submit = SubmitField('Save Changes')
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/list")
|
@bp.route("/list")
|
||||||
def automation_list():
|
def automation_list() -> ResponseReturnValue:
|
||||||
automations = Automation.query.filter(
|
automations = Automation.query.filter(
|
||||||
Automation.destroyed == None).order_by(Automation.description).all()
|
Automation.destroyed == None).order_by(Automation.description).all()
|
||||||
return render_template("list.html.j2",
|
return render_template("list.html.j2",
|
||||||
|
@ -29,8 +31,8 @@ def automation_list():
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/edit/<automation_id>', methods=['GET', 'POST'])
|
@bp.route('/edit/<automation_id>', methods=['GET', 'POST'])
|
||||||
def automation_edit(automation_id):
|
def automation_edit(automation_id: int) -> ResponseReturnValue:
|
||||||
automation = Automation.query.filter(Automation.id == automation_id).first()
|
automation: Optional[Automation] = Automation.query.filter(Automation.id == automation_id).first()
|
||||||
if automation is None:
|
if automation is None:
|
||||||
return Response(render_template("error.html.j2",
|
return Response(render_template("error.html.j2",
|
||||||
section="automation",
|
section="automation",
|
||||||
|
@ -52,7 +54,7 @@ def automation_edit(automation_id):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/kick/<automation_id>", methods=['GET', 'POST'])
|
@bp.route("/kick/<automation_id>", methods=['GET', 'POST'])
|
||||||
def automation_kick(automation_id: int):
|
def automation_kick(automation_id: int) -> ResponseReturnValue:
|
||||||
automation = Automation.query.filter(
|
automation = Automation.query.filter(
|
||||||
Automation.id == automation_id,
|
Automation.id == automation_id,
|
||||||
Automation.destroyed == None).first()
|
Automation.destroyed == None).first()
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from flask import render_template, Response, flash, redirect, url_for, Blueprint
|
from flask import render_template, Response, flash, redirect, url_for, Blueprint
|
||||||
|
from flask.typing import ResponseReturnValue
|
||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.bridges import Bridge
|
from app.models.bridges import Bridge
|
||||||
|
@ -8,7 +11,7 @@ bp = Blueprint("bridge", __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/list")
|
@bp.route("/list")
|
||||||
def bridge_list():
|
def bridge_list() -> ResponseReturnValue:
|
||||||
bridges = Bridge.query.filter(Bridge.destroyed == None).all()
|
bridges = Bridge.query.filter(Bridge.destroyed == None).all()
|
||||||
return render_template("list.html.j2",
|
return render_template("list.html.j2",
|
||||||
section="bridge",
|
section="bridge",
|
||||||
|
@ -18,8 +21,8 @@ def bridge_list():
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/block/<bridge_id>", methods=['GET', 'POST'])
|
@bp.route("/block/<bridge_id>", methods=['GET', 'POST'])
|
||||||
def bridge_blocked(bridge_id):
|
def bridge_blocked(bridge_id: int) -> ResponseReturnValue:
|
||||||
bridge: Bridge = Bridge.query.filter(Bridge.id == bridge_id, Bridge.destroyed == None).first()
|
bridge: Optional[Bridge] = Bridge.query.filter(Bridge.id == bridge_id, Bridge.destroyed == None).first()
|
||||||
if bridge is None:
|
if bridge is None:
|
||||||
return Response(render_template("error.html.j2",
|
return Response(render_template("error.html.j2",
|
||||||
header="404 Proxy Not Found",
|
header="404 Proxy Not Found",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
from flask import render_template, url_for, flash, redirect, Response, Blueprint
|
from flask import render_template, url_for, flash, redirect, Response, Blueprint
|
||||||
|
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 SelectField, StringField, IntegerField, SubmitField
|
||||||
|
@ -14,7 +16,7 @@ from app.portal.util import response_404, view_lifecycle
|
||||||
bp = Blueprint("bridgeconf", __name__)
|
bp = Blueprint("bridgeconf", __name__)
|
||||||
|
|
||||||
|
|
||||||
class NewBridgeConfForm(FlaskForm):
|
class NewBridgeConfForm(FlaskForm): # type: ignore
|
||||||
provider = SelectField('Provider', validators=[DataRequired()])
|
provider = SelectField('Provider', validators=[DataRequired()])
|
||||||
method = SelectField('Distribution Method', validators=[DataRequired()])
|
method = SelectField('Distribution Method', validators=[DataRequired()])
|
||||||
description = StringField('Description')
|
description = StringField('Description')
|
||||||
|
@ -23,15 +25,15 @@ class NewBridgeConfForm(FlaskForm):
|
||||||
submit = SubmitField('Save Changes')
|
submit = SubmitField('Save Changes')
|
||||||
|
|
||||||
|
|
||||||
class EditBridgeConfForm(FlaskForm):
|
class EditBridgeConfForm(FlaskForm): # type: ignore
|
||||||
description = StringField('Description')
|
description = StringField('Description')
|
||||||
number = IntegerField('Number', validators=[NumberRange(1, message="One or more bridges must be created")])
|
number = IntegerField('Number', validators=[NumberRange(1, message="One or more bridges must be created")])
|
||||||
submit = SubmitField('Save Changes')
|
submit = SubmitField('Save Changes')
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/list")
|
@bp.route("/list")
|
||||||
def bridgeconf_list():
|
def bridgeconf_list() -> ResponseReturnValue:
|
||||||
bridgeconfs = BridgeConf.query.filter(BridgeConf.destroyed == None).all()
|
bridgeconfs: List[BridgeConf] = BridgeConf.query.filter(BridgeConf.destroyed == None).all()
|
||||||
return render_template("list.html.j2",
|
return render_template("list.html.j2",
|
||||||
section="bridgeconf",
|
section="bridgeconf",
|
||||||
title="Tor Bridge Configurations",
|
title="Tor Bridge Configurations",
|
||||||
|
@ -42,7 +44,7 @@ def bridgeconf_list():
|
||||||
|
|
||||||
@bp.route("/new", methods=['GET', 'POST'])
|
@bp.route("/new", methods=['GET', 'POST'])
|
||||||
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
|
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
|
||||||
def bridgeconf_new(group_id=None):
|
def bridgeconf_new(group_id: Optional[int] = None) -> ResponseReturnValue:
|
||||||
form = NewBridgeConfForm()
|
form = NewBridgeConfForm()
|
||||||
form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
|
form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
|
||||||
form.provider.choices = [
|
form.provider.choices = [
|
||||||
|
@ -82,7 +84,7 @@ def bridgeconf_new(group_id=None):
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/edit/<bridgeconf_id>', methods=['GET', 'POST'])
|
@bp.route('/edit/<bridgeconf_id>', methods=['GET', 'POST'])
|
||||||
def bridgeconf_edit(bridgeconf_id):
|
def bridgeconf_edit(bridgeconf_id: int) -> ResponseReturnValue:
|
||||||
bridgeconf = BridgeConf.query.filter(BridgeConf.id == bridgeconf_id).first()
|
bridgeconf = BridgeConf.query.filter(BridgeConf.id == bridgeconf_id).first()
|
||||||
if bridgeconf is None:
|
if bridgeconf is None:
|
||||||
return Response(render_template("error.html.j2",
|
return Response(render_template("error.html.j2",
|
||||||
|
@ -107,7 +109,7 @@ def bridgeconf_edit(bridgeconf_id):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/destroy/<bridgeconf_id>", methods=['GET', 'POST'])
|
@bp.route("/destroy/<bridgeconf_id>", methods=['GET', 'POST'])
|
||||||
def bridgeconf_destroy(bridgeconf_id: int):
|
def bridgeconf_destroy(bridgeconf_id: int) -> ResponseReturnValue:
|
||||||
bridgeconf = BridgeConf.query.filter(BridgeConf.id == bridgeconf_id, BridgeConf.destroyed == None).first()
|
bridgeconf = BridgeConf.query.filter(BridgeConf.id == bridgeconf_id, BridgeConf.destroyed == None).first()
|
||||||
if bridgeconf is None:
|
if bridgeconf is None:
|
||||||
return response_404("The requested bridge configuration could not be found.")
|
return response_404("The requested bridge configuration could not be found.")
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from flask import render_template, Blueprint, Response
|
from flask import render_template, Blueprint, Response
|
||||||
|
from flask.typing import ResponseReturnValue
|
||||||
from sqlalchemy import desc
|
from sqlalchemy import desc
|
||||||
|
|
||||||
from app.models.base import Group
|
from app.models.base import Group
|
||||||
|
@ -8,7 +9,7 @@ bp = Blueprint("eotk", __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/list")
|
@bp.route("/list")
|
||||||
def eotk_list():
|
def eotk_list() -> ResponseReturnValue:
|
||||||
instances = Eotk.query.filter(Eotk.destroyed == None).order_by(desc(Eotk.added)).all()
|
instances = Eotk.query.filter(Eotk.destroyed == None).order_by(desc(Eotk.added)).all()
|
||||||
return render_template("list.html.j2",
|
return render_template("list.html.j2",
|
||||||
section="eotk",
|
section="eotk",
|
||||||
|
@ -18,7 +19,7 @@ def eotk_list():
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/conf/<group_id>")
|
@bp.route("/conf/<group_id>")
|
||||||
def eotk_conf(group_id: int):
|
def eotk_conf(group_id: int) -> ResponseReturnValue:
|
||||||
from app import app
|
from app import app
|
||||||
group = Group.query.filter(Group.id == group_id).first()
|
group = Group.query.filter(Group.id == group_id).first()
|
||||||
return Response(render_template("sites.conf.j2",
|
return Response(render_template("sites.conf.j2",
|
||||||
|
|
|
@ -2,12 +2,12 @@ from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, SubmitField, SelectField
|
from wtforms import StringField, SubmitField, SelectField
|
||||||
|
|
||||||
|
|
||||||
class EditMirrorForm(FlaskForm):
|
class EditMirrorForm(FlaskForm): # type: ignore
|
||||||
origin = SelectField('Origin')
|
origin = SelectField('Origin')
|
||||||
url = StringField('URL')
|
url = StringField('URL')
|
||||||
submit = SubmitField('Save Changes')
|
submit = SubmitField('Save Changes')
|
||||||
|
|
||||||
|
|
||||||
class EditProxyForm(FlaskForm):
|
class EditProxyForm(FlaskForm): # type: ignore
|
||||||
origin = SelectField('Origin')
|
origin = SelectField('Origin')
|
||||||
submit = SubmitField('Save Changes')
|
submit = SubmitField('Save Changes')
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from flask import render_template, url_for, flash, redirect, Response, Blueprint
|
from flask import render_template, url_for, flash, redirect, Response, Blueprint
|
||||||
|
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, BooleanField, SubmitField
|
from wtforms import StringField, BooleanField, SubmitField
|
||||||
|
@ -12,21 +13,21 @@ from app.models.base import Group
|
||||||
bp = Blueprint("group", __name__)
|
bp = Blueprint("group", __name__)
|
||||||
|
|
||||||
|
|
||||||
class NewGroupForm(FlaskForm):
|
class NewGroupForm(FlaskForm): # type: ignore
|
||||||
group_name = StringField("Short Name", validators=[DataRequired()])
|
group_name = StringField("Short Name", validators=[DataRequired()])
|
||||||
description = StringField("Description", validators=[DataRequired()])
|
description = StringField("Description", validators=[DataRequired()])
|
||||||
eotk = BooleanField("Deploy EOTK instances?")
|
eotk = BooleanField("Deploy EOTK instances?")
|
||||||
submit = SubmitField('Save Changes', render_kw={"class": "btn btn-success"})
|
submit = SubmitField('Save Changes', render_kw={"class": "btn btn-success"})
|
||||||
|
|
||||||
|
|
||||||
class EditGroupForm(FlaskForm):
|
class EditGroupForm(FlaskForm): # type: ignore
|
||||||
description = StringField('Description', validators=[DataRequired()])
|
description = StringField('Description', validators=[DataRequired()])
|
||||||
eotk = BooleanField("Deploy EOTK instances?")
|
eotk = BooleanField("Deploy EOTK instances?")
|
||||||
submit = SubmitField('Save Changes', render_kw={"class": "btn btn-success"})
|
submit = SubmitField('Save Changes', render_kw={"class": "btn btn-success"})
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/list")
|
@bp.route("/list")
|
||||||
def group_list():
|
def group_list() -> ResponseReturnValue:
|
||||||
groups = Group.query.order_by(Group.group_name).all()
|
groups = Group.query.order_by(Group.group_name).all()
|
||||||
return render_template("list.html.j2",
|
return render_template("list.html.j2",
|
||||||
section="group",
|
section="group",
|
||||||
|
@ -37,7 +38,7 @@ def group_list():
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/new", methods=['GET', 'POST'])
|
@bp.route("/new", methods=['GET', 'POST'])
|
||||||
def group_new():
|
def group_new() -> ResponseReturnValue:
|
||||||
form = NewGroupForm()
|
form = NewGroupForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
group = Group()
|
group = Group()
|
||||||
|
@ -59,7 +60,7 @@ def group_new():
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/edit/<group_id>', methods=['GET', 'POST'])
|
@bp.route('/edit/<group_id>', methods=['GET', 'POST'])
|
||||||
def group_edit(group_id):
|
def group_edit(group_id: int) -> ResponseReturnValue:
|
||||||
group = Group.query.filter(Group.id == group_id).first()
|
group = Group.query.filter(Group.id == group_id).first()
|
||||||
if group is None:
|
if group is None:
|
||||||
return Response(render_template("error.html.j2",
|
return Response(render_template("error.html.j2",
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from flask import render_template, url_for, flash, redirect, Blueprint, Response
|
from flask import render_template, url_for, flash, redirect, Blueprint, Response
|
||||||
|
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, SubmitField
|
from wtforms import SelectField, StringField, SubmitField
|
||||||
|
@ -28,7 +30,7 @@ def list_format_name(s: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/list')
|
@bp.route('/list')
|
||||||
def list_list():
|
def list_list() -> ResponseReturnValue:
|
||||||
lists = MirrorList.query.filter(MirrorList.destroyed == None).all()
|
lists = MirrorList.query.filter(MirrorList.destroyed == None).all()
|
||||||
return render_template("list.html.j2",
|
return render_template("list.html.j2",
|
||||||
section="list",
|
section="list",
|
||||||
|
@ -48,7 +50,7 @@ def list_list():
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/preview/<format_>')
|
@bp.route('/preview/<format_>')
|
||||||
def list_preview(format_: str):
|
def list_preview(format_: str) -> ResponseReturnValue:
|
||||||
if format_ == "bca":
|
if format_ == "bca":
|
||||||
return Response(json.dumps(mirror_mapping()), content_type="application/json")
|
return Response(json.dumps(mirror_mapping()), content_type="application/json")
|
||||||
if format_ == "bc2":
|
if format_ == "bc2":
|
||||||
|
@ -59,7 +61,7 @@ def list_preview(format_: str):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/destroy/<list_id>", methods=['GET', 'POST'])
|
@bp.route("/destroy/<list_id>", methods=['GET', 'POST'])
|
||||||
def list_destroy(list_id: int):
|
def list_destroy(list_id: int) -> ResponseReturnValue:
|
||||||
list_ = MirrorList.query.filter(MirrorList.id == list_id, MirrorList.destroyed == None).first()
|
list_ = MirrorList.query.filter(MirrorList.id == list_id, MirrorList.destroyed == None).first()
|
||||||
if list_ is None:
|
if list_ is None:
|
||||||
return response_404("The requested bridge configuration could not be found.")
|
return response_404("The requested bridge configuration could not be found.")
|
||||||
|
@ -76,10 +78,10 @@ def list_destroy(list_id: int):
|
||||||
|
|
||||||
@bp.route("/new", methods=['GET', 'POST'])
|
@bp.route("/new", methods=['GET', 'POST'])
|
||||||
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
|
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
|
||||||
def list_new(group_id=None):
|
def list_new(group_id: Optional[int] = None) -> ResponseReturnValue:
|
||||||
form = NewMirrorListForm()
|
form = NewMirrorListForm()
|
||||||
form.provider.choices = [(k, v) for k, v in MirrorList.providers_supported]
|
form.provider.choices = [(k, v) for k, v in MirrorList.providers_supported] # type: ignore
|
||||||
form.format.choices = [(k, v) for k, v in MirrorList.formats_supported]
|
form.format.choices = [(k, v) for k, v in MirrorList.formats_supported] # type: ignore
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
list_ = MirrorList()
|
list_ = MirrorList()
|
||||||
list_.provider = form.provider.data
|
list_.provider = form.provider.data
|
||||||
|
@ -105,7 +107,7 @@ def list_new(group_id=None):
|
||||||
return render_template("new.html.j2", section="list", form=form)
|
return render_template("new.html.j2", section="list", form=form)
|
||||||
|
|
||||||
|
|
||||||
class NewMirrorListForm(FlaskForm):
|
class NewMirrorListForm(FlaskForm): # type: ignore
|
||||||
provider = SelectField('Provider', validators=[DataRequired()])
|
provider = SelectField('Provider', validators=[DataRequired()])
|
||||||
format = SelectField('Distribution Method', validators=[DataRequired()])
|
format = SelectField('Distribution Method', validators=[DataRequired()])
|
||||||
description = StringField('Description', validators=[DataRequired()])
|
description = StringField('Description', validators=[DataRequired()])
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from flask import flash, redirect, url_for, render_template, Response, Blueprint
|
from flask import flash, redirect, url_for, render_template, Response, Blueprint
|
||||||
|
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 StringField, SelectField, SubmitField
|
||||||
|
@ -14,7 +16,7 @@ from app.portal.util import response_404, view_lifecycle
|
||||||
bp = Blueprint("onion", __name__)
|
bp = Blueprint("onion", __name__)
|
||||||
|
|
||||||
|
|
||||||
class NewOnionForm(FlaskForm):
|
class NewOnionForm(FlaskForm): # type: ignore
|
||||||
domain_name = StringField('Domain Name', validators=[DataRequired()])
|
domain_name = StringField('Domain Name', validators=[DataRequired()])
|
||||||
onion_name = StringField('Onion Name', validators=[DataRequired(), Length(min=56, max=56)],
|
onion_name = StringField('Onion Name', validators=[DataRequired(), Length(min=56, max=56)],
|
||||||
description="Onion service hostname, excluding the .onion suffix")
|
description="Onion service hostname, excluding the .onion suffix")
|
||||||
|
@ -23,7 +25,7 @@ class NewOnionForm(FlaskForm):
|
||||||
submit = SubmitField('Save Changes')
|
submit = SubmitField('Save Changes')
|
||||||
|
|
||||||
|
|
||||||
class EditOnionForm(FlaskForm):
|
class EditOnionForm(FlaskForm): # type: ignore
|
||||||
description = StringField('Description', validators=[DataRequired()])
|
description = StringField('Description', validators=[DataRequired()])
|
||||||
group = SelectField('Group', validators=[DataRequired()])
|
group = SelectField('Group', validators=[DataRequired()])
|
||||||
submit = SubmitField('Save Changes')
|
submit = SubmitField('Save Changes')
|
||||||
|
@ -31,7 +33,7 @@ class EditOnionForm(FlaskForm):
|
||||||
|
|
||||||
@bp.route("/new", methods=['GET', 'POST'])
|
@bp.route("/new", methods=['GET', 'POST'])
|
||||||
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
|
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
|
||||||
def onion_new(group_id=None):
|
def onion_new(group_id: Optional[int] = None) -> ResponseReturnValue:
|
||||||
form = NewOnionForm()
|
form = NewOnionForm()
|
||||||
form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
|
form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
|
@ -57,8 +59,8 @@ def onion_new(group_id=None):
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/edit/<onion_id>', methods=['GET', 'POST'])
|
@bp.route('/edit/<onion_id>', methods=['GET', 'POST'])
|
||||||
def onion_edit(onion_id):
|
def onion_edit(onion_id: int) -> ResponseReturnValue:
|
||||||
onion = Onion.query.filter(Onion.id == onion_id).first()
|
onion: Optional[Onion] = Onion.query.filter(Onion.id == onion_id).first()
|
||||||
if onion is None:
|
if onion is None:
|
||||||
return Response(render_template("error.html.j2",
|
return Response(render_template("error.html.j2",
|
||||||
section="onion",
|
section="onion",
|
||||||
|
@ -83,7 +85,7 @@ def onion_edit(onion_id):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/list")
|
@bp.route("/list")
|
||||||
def onion_list():
|
def onion_list() -> ResponseReturnValue:
|
||||||
onions = Onion.query.order_by(Onion.domain_name).all()
|
onions = Onion.query.order_by(Onion.domain_name).all()
|
||||||
return render_template("list.html.j2",
|
return render_template("list.html.j2",
|
||||||
section="onion",
|
section="onion",
|
||||||
|
@ -94,7 +96,7 @@ def onion_list():
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/destroy/<onion_id>", methods=['GET', 'POST'])
|
@bp.route("/destroy/<onion_id>", methods=['GET', 'POST'])
|
||||||
def onion_destroy(onion_id: int):
|
def onion_destroy(onion_id: int) -> ResponseReturnValue:
|
||||||
onion = Onion.query.filter(Onion.id == onion_id, Onion.destroyed == None).first()
|
onion = Onion.query.filter(Onion.id == onion_id, Onion.destroyed == None).first()
|
||||||
if onion is None:
|
if onion is None:
|
||||||
return response_404("The requested onion service could not be found.")
|
return response_404("The requested onion service could not be found.")
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
from flask import flash, redirect, url_for, render_template, Response, Blueprint
|
from flask import flash, redirect, url_for, render_template, Response, Blueprint
|
||||||
|
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
|
from wtforms import StringField, SelectField, SubmitField, BooleanField
|
||||||
|
@ -14,7 +16,7 @@ from app.portal.util import response_404, view_lifecycle
|
||||||
bp = Blueprint("origin", __name__)
|
bp = Blueprint("origin", __name__)
|
||||||
|
|
||||||
|
|
||||||
class NewOriginForm(FlaskForm):
|
class NewOriginForm(FlaskForm): # type: ignore
|
||||||
domain_name = StringField('Domain Name', validators=[DataRequired()])
|
domain_name = StringField('Domain Name', validators=[DataRequired()])
|
||||||
description = StringField('Description', validators=[DataRequired()])
|
description = StringField('Description', validators=[DataRequired()])
|
||||||
group = SelectField('Group', validators=[DataRequired()])
|
group = SelectField('Group', validators=[DataRequired()])
|
||||||
|
@ -22,7 +24,7 @@ class NewOriginForm(FlaskForm):
|
||||||
submit = SubmitField('Save Changes')
|
submit = SubmitField('Save Changes')
|
||||||
|
|
||||||
|
|
||||||
class EditOriginForm(FlaskForm):
|
class EditOriginForm(FlaskForm): # type: ignore
|
||||||
description = StringField('Description', validators=[DataRequired()])
|
description = StringField('Description', validators=[DataRequired()])
|
||||||
group = SelectField('Group', validators=[DataRequired()])
|
group = SelectField('Group', validators=[DataRequired()])
|
||||||
auto_rotate = BooleanField("Enable auto-rotation?")
|
auto_rotate = BooleanField("Enable auto-rotation?")
|
||||||
|
@ -31,7 +33,7 @@ class EditOriginForm(FlaskForm):
|
||||||
|
|
||||||
@bp.route("/new", methods=['GET', 'POST'])
|
@bp.route("/new", methods=['GET', 'POST'])
|
||||||
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
|
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
|
||||||
def origin_new(group_id=None):
|
def origin_new(group_id: Optional[int] = None) -> ResponseReturnValue:
|
||||||
form = NewOriginForm()
|
form = NewOriginForm()
|
||||||
form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
|
form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
|
@ -57,8 +59,8 @@ def origin_new(group_id=None):
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/edit/<origin_id>', methods=['GET', 'POST'])
|
@bp.route('/edit/<origin_id>', methods=['GET', 'POST'])
|
||||||
def origin_edit(origin_id):
|
def origin_edit(origin_id: int) -> ResponseReturnValue:
|
||||||
origin = Origin.query.filter(Origin.id == origin_id).first()
|
origin: Optional[Origin] = Origin.query.filter(Origin.id == origin_id).first()
|
||||||
if origin is None:
|
if origin is None:
|
||||||
return Response(render_template("error.html.j2",
|
return Response(render_template("error.html.j2",
|
||||||
section="origin",
|
section="origin",
|
||||||
|
@ -85,8 +87,8 @@ def origin_edit(origin_id):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/list")
|
@bp.route("/list")
|
||||||
def origin_list():
|
def origin_list() -> ResponseReturnValue:
|
||||||
origins = Origin.query.order_by(Origin.domain_name).all()
|
origins: List[Origin] = Origin.query.order_by(Origin.domain_name).all()
|
||||||
return render_template("list.html.j2",
|
return render_template("list.html.j2",
|
||||||
section="origin",
|
section="origin",
|
||||||
title="Web Origins",
|
title="Web Origins",
|
||||||
|
@ -101,7 +103,7 @@ def origin_list():
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/onion")
|
@bp.route("/onion")
|
||||||
def origin_onion():
|
def origin_onion() -> ResponseReturnValue:
|
||||||
origins = Origin.query.order_by(Origin.domain_name).all()
|
origins = Origin.query.order_by(Origin.domain_name).all()
|
||||||
return render_template("list.html.j2",
|
return render_template("list.html.j2",
|
||||||
section="origin",
|
section="origin",
|
||||||
|
@ -112,7 +114,7 @@ def origin_onion():
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/destroy/<origin_id>", methods=['GET', 'POST'])
|
@bp.route("/destroy/<origin_id>", methods=['GET', 'POST'])
|
||||||
def origin_destroy(origin_id: int):
|
def origin_destroy(origin_id: int) -> ResponseReturnValue:
|
||||||
origin = Origin.query.filter(Origin.id == origin_id, Origin.destroyed == None).first()
|
origin = Origin.query.filter(Origin.id == origin_id, Origin.destroyed == None).first()
|
||||||
if origin is None:
|
if origin is None:
|
||||||
return response_404("The requested origin could not be found.")
|
return response_404("The requested origin could not be found.")
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from flask import render_template, Response, flash, redirect, url_for, Blueprint
|
from flask import render_template, Response, flash, redirect, url_for, Blueprint
|
||||||
|
from flask.typing import ResponseReturnValue
|
||||||
from sqlalchemy import desc
|
from sqlalchemy import desc
|
||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
@ -9,7 +10,7 @@ bp = Blueprint("proxy", __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/list")
|
@bp.route("/list")
|
||||||
def proxy_list():
|
def proxy_list() -> ResponseReturnValue:
|
||||||
proxies = Proxy.query.filter(Proxy.destroyed == None).order_by(desc(Proxy.added)).all()
|
proxies = Proxy.query.filter(Proxy.destroyed == None).order_by(desc(Proxy.added)).all()
|
||||||
return render_template("list.html.j2",
|
return render_template("list.html.j2",
|
||||||
section="proxy",
|
section="proxy",
|
||||||
|
@ -19,7 +20,7 @@ def proxy_list():
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/block/<proxy_id>", methods=['GET', 'POST'])
|
@bp.route("/block/<proxy_id>", methods=['GET', 'POST'])
|
||||||
def proxy_block(proxy_id):
|
def proxy_block(proxy_id: int) -> ResponseReturnValue:
|
||||||
proxy = Proxy.query.filter(Proxy.id == proxy_id, Proxy.destroyed == None).first()
|
proxy = Proxy.query.filter(Proxy.id == proxy_id, Proxy.destroyed == None).first()
|
||||||
if proxy is None:
|
if proxy is None:
|
||||||
return Response(render_template("error.html.j2",
|
return Response(render_template("error.html.j2",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from flask import Response, render_template, flash, redirect, url_for
|
from flask import Response, render_template, flash, redirect, url_for
|
||||||
|
from flask.typing import ResponseReturnValue
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import SubmitField
|
from wtforms import SubmitField
|
||||||
|
|
||||||
|
@ -7,7 +8,7 @@ from app.models import AbstractResource
|
||||||
from app.models.activity import Activity
|
from app.models.activity import Activity
|
||||||
|
|
||||||
|
|
||||||
def response_404(message: str):
|
def response_404(message: str) -> ResponseReturnValue:
|
||||||
return Response(render_template("error.html.j2",
|
return Response(render_template("error.html.j2",
|
||||||
header="404 Not Found",
|
header="404 Not Found",
|
||||||
message=message))
|
message=message))
|
||||||
|
@ -20,7 +21,7 @@ def view_lifecycle(*,
|
||||||
success_view: str,
|
success_view: str,
|
||||||
section: str,
|
section: str,
|
||||||
resource: AbstractResource,
|
resource: AbstractResource,
|
||||||
action: str):
|
action: str) -> ResponseReturnValue:
|
||||||
form = LifecycleForm()
|
form = LifecycleForm()
|
||||||
if action == "destroy":
|
if action == "destroy":
|
||||||
form.submit.render_kw = {"class": "btn btn-danger"}
|
form.submit.render_kw = {"class": "btn btn-danger"}
|
||||||
|
@ -54,5 +55,5 @@ def view_lifecycle(*,
|
||||||
form=form)
|
form=form)
|
||||||
|
|
||||||
|
|
||||||
class LifecycleForm(FlaskForm):
|
class LifecycleForm(FlaskForm): # type: ignore
|
||||||
submit = SubmitField('Confirm')
|
submit = SubmitField('Confirm')
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from flask import Blueprint, flash, Response, render_template, redirect, url_for
|
from flask import Blueprint, flash, Response, render_template, redirect, url_for
|
||||||
|
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 StringField, SelectField, SubmitField
|
||||||
|
@ -22,7 +24,7 @@ def webhook_format_name(s: str) -> str:
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
|
|
||||||
|
|
||||||
class NewWebhookForm(FlaskForm):
|
class NewWebhookForm(FlaskForm): # type: ignore
|
||||||
description = StringField('Description', validators=[DataRequired()])
|
description = StringField('Description', validators=[DataRequired()])
|
||||||
format = SelectField('Format', choices=[
|
format = SelectField('Format', choices=[
|
||||||
("telegram", "Telegram"),
|
("telegram", "Telegram"),
|
||||||
|
@ -33,7 +35,7 @@ class NewWebhookForm(FlaskForm):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/new", methods=['GET', 'POST'])
|
@bp.route("/new", methods=['GET', 'POST'])
|
||||||
def webhook_new():
|
def webhook_new() -> ResponseReturnValue:
|
||||||
form = NewWebhookForm()
|
form = NewWebhookForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
webhook = Webhook(
|
webhook = Webhook(
|
||||||
|
@ -53,7 +55,7 @@ def webhook_new():
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/edit/<webhook_id>', methods=['GET', 'POST'])
|
@bp.route('/edit/<webhook_id>', methods=['GET', 'POST'])
|
||||||
def webhook_edit(webhook_id):
|
def webhook_edit(webhook_id: int) -> ResponseReturnValue:
|
||||||
webhook = Webhook.query.filter(Webhook.id == webhook_id).first()
|
webhook = Webhook.query.filter(Webhook.id == webhook_id).first()
|
||||||
if webhook is None:
|
if webhook is None:
|
||||||
return Response(render_template("error.html.j2",
|
return Response(render_template("error.html.j2",
|
||||||
|
@ -81,7 +83,7 @@ def webhook_edit(webhook_id):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/list")
|
@bp.route("/list")
|
||||||
def webhook_list():
|
def webhook_list() -> ResponseReturnValue:
|
||||||
webhooks = Webhook.query.all()
|
webhooks = Webhook.query.all()
|
||||||
return render_template("list.html.j2",
|
return render_template("list.html.j2",
|
||||||
section="webhook",
|
section="webhook",
|
||||||
|
@ -92,8 +94,8 @@ def webhook_list():
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/destroy/<webhook_id>", methods=['GET', 'POST'])
|
@bp.route("/destroy/<webhook_id>", methods=['GET', 'POST'])
|
||||||
def webhook_destroy(webhook_id: int):
|
def webhook_destroy(webhook_id: int) -> ResponseReturnValue:
|
||||||
webhook = Webhook.query.filter(Webhook.id == webhook_id, Webhook.destroyed == None).first()
|
webhook: Optional[Webhook] = Webhook.query.filter(Webhook.id == webhook_id, Webhook.destroyed == None).first()
|
||||||
if webhook is None:
|
if webhook is None:
|
||||||
return response_404("The requested webhook could not be found.")
|
return response_404("The requested webhook could not be found.")
|
||||||
return view_lifecycle(
|
return view_lifecycle(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
import os
|
import os
|
||||||
from typing import Tuple
|
from typing import Tuple, Optional
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ class BaseAutomation(metaclass=ABCMeta):
|
||||||
def automate(self, full: bool = False) -> Tuple[bool, str]:
|
def automate(self, full: bool = False) -> Tuple[bool, str]:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def working_directory(self, filename=None) -> str:
|
def working_directory(self, filename: Optional[str] = None) -> str:
|
||||||
"""
|
"""
|
||||||
Provides a filesystem path that can be used during the automation run.
|
Provides a filesystem path that can be used during the automation run.
|
||||||
This is currently a persistent path, but this should not be relied upon
|
This is currently a persistent path, but this should not be relied upon
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
from azure.identity import ClientSecretCredential
|
from azure.identity import ClientSecretCredential
|
||||||
from azure.mgmt.alertsmanagement import AlertsManagementClient
|
from azure.mgmt.alertsmanagement import AlertsManagementClient
|
||||||
|
|
||||||
|
@ -12,7 +14,7 @@ class AlarmProxyAzureCdnAutomation(BaseAutomation):
|
||||||
short_name = "monitor_proxy_azure_cdn"
|
short_name = "monitor_proxy_azure_cdn"
|
||||||
description = "Import alarms for Azure CDN proxies"
|
description = "Import alarms for Azure CDN proxies"
|
||||||
|
|
||||||
def automate(self):
|
def automate(self, full: bool = False) -> Tuple[bool, str]:
|
||||||
credential = ClientSecretCredential(
|
credential = ClientSecretCredential(
|
||||||
tenant_id=app.config['AZURE_TENANT_ID'],
|
tenant_id=app.config['AZURE_TENANT_ID'],
|
||||||
client_id=app.config['AZURE_CLIENT_ID'],
|
client_id=app.config['AZURE_CLIENT_ID'],
|
||||||
|
@ -33,4 +35,4 @@ class AlarmProxyAzureCdnAutomation(BaseAutomation):
|
||||||
alarm.update_state(AlarmState.OK, "Azure monitor alert not firing")
|
alarm.update_state(AlarmState.OK, "Azure monitor alert not firing")
|
||||||
else:
|
else:
|
||||||
alarm.update_state(AlarmState.CRITICAL, "Azure monitor alert firing")
|
alarm.update_state(AlarmState.CRITICAL, "Azure monitor alert firing")
|
||||||
return True, []
|
return True, ""
|
|
@ -1,4 +1,5 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
|
|
||||||
|
@ -14,7 +15,7 @@ class AlarmProxyCloudfrontAutomation(BaseAutomation):
|
||||||
short_name = "monitor_proxy_cloudfront"
|
short_name = "monitor_proxy_cloudfront"
|
||||||
description = "Import alarms for AWS CloudFront proxies"
|
description = "Import alarms for AWS CloudFront proxies"
|
||||||
|
|
||||||
def automate(self):
|
def automate(self, full: bool = False) -> Tuple[bool, str]:
|
||||||
cloudwatch = boto3.client('cloudwatch',
|
cloudwatch = boto3.client('cloudwatch',
|
||||||
aws_access_key_id=app.config['AWS_ACCESS_KEY'],
|
aws_access_key_id=app.config['AWS_ACCESS_KEY'],
|
||||||
aws_secret_access_key=app.config['AWS_SECRET_KEY'],
|
aws_secret_access_key=app.config['AWS_SECRET_KEY'],
|
||||||
|
@ -39,7 +40,7 @@ class AlarmProxyCloudfrontAutomation(BaseAutomation):
|
||||||
Alarm.alarm_type == "cloudfront-quota"
|
Alarm.alarm_type == "cloudfront-quota"
|
||||||
).first()
|
).first()
|
||||||
if alarm is None:
|
if alarm is None:
|
||||||
alarm = Alarm()
|
alarm = Alarm() # type: ignore
|
||||||
alarm.target = "service/cloudfront"
|
alarm.target = "service/cloudfront"
|
||||||
alarm.alarm_type = "cloudfront-quota"
|
alarm.alarm_type = "cloudfront-quota"
|
||||||
alarm.state_changed = datetime.datetime.utcnow()
|
alarm.state_changed = datetime.datetime.utcnow()
|
||||||
|
@ -57,4 +58,4 @@ class AlarmProxyCloudfrontAutomation(BaseAutomation):
|
||||||
if alarm.alarm_state != old_state:
|
if alarm.alarm_state != old_state:
|
||||||
alarm.state_changed = datetime.datetime.utcnow()
|
alarm.state_changed = datetime.datetime.utcnow()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return True, []
|
return True, ""
|
||||||
|
|
|
@ -8,7 +8,7 @@ from app.models.mirrors import Proxy
|
||||||
from app.terraform import BaseAutomation
|
from app.terraform import BaseAutomation
|
||||||
|
|
||||||
|
|
||||||
def set_http_alarm(proxy_id: int, state: AlarmState, text: str):
|
def set_http_alarm(proxy_id: int, state: AlarmState, text: str) -> None:
|
||||||
alarm = Alarm.query.filter(
|
alarm = Alarm.query.filter(
|
||||||
Alarm.proxy_id == proxy_id,
|
Alarm.proxy_id == proxy_id,
|
||||||
Alarm.alarm_type == "http-status"
|
Alarm.alarm_type == "http-status"
|
||||||
|
|
|
@ -19,7 +19,10 @@ class BlockBridgeGitHubAutomation(BaseAutomation):
|
||||||
g = Github(app.config['GITHUB_API_KEY'])
|
g = Github(app.config['GITHUB_API_KEY'])
|
||||||
repo = g.get_repo(app.config['GITHUB_BRIDGE_REPO'])
|
repo = g.get_repo(app.config['GITHUB_BRIDGE_REPO'])
|
||||||
for vp in app.config['GITHUB_BRIDGE_VANTAGE_POINTS']:
|
for vp in app.config['GITHUB_BRIDGE_VANTAGE_POINTS']:
|
||||||
results = repo.get_contents(f"recentResult_{vp}").decoded_content.decode('utf-8').splitlines()
|
contents = repo.get_contents(f"recentResult_{vp}")
|
||||||
|
if isinstance(contents, list):
|
||||||
|
return False, f"Expected a file at recentResult_{vp} but got a directory."
|
||||||
|
results = contents.decoded_content.decode('utf-8').splitlines()
|
||||||
for result in results:
|
for result in results:
|
||||||
parts = result.split("\t")
|
parts = result.split("\t")
|
||||||
if isoparse(parts[2]) < (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=3)):
|
if isoparse(parts[2]) < (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=3)):
|
||||||
|
|
|
@ -58,7 +58,7 @@ class BlockExternalAutomation(BaseAutomation):
|
||||||
continue
|
continue
|
||||||
activities.append(Activity(
|
activities.append(Activity(
|
||||||
activity_type="block",
|
activity_type="block",
|
||||||
text=(f"Proxy {p.url} for {p.origin.domain_name} detected blocked according to external source. "
|
text=(f"Proxy {proxy.url} for {proxy.origin.domain_name} detected blocked according to external source. "
|
||||||
"Rotation scheduled.")
|
"Rotation scheduled.")
|
||||||
))
|
))
|
||||||
proxy.deprecate(reason="external")
|
proxy.deprecate(reason="external")
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Dict, Tuple
|
from typing import Dict, Tuple, Union, Any
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ from app.models.mirrors import Origin
|
||||||
from app.terraform import BaseAutomation
|
from app.terraform import BaseAutomation
|
||||||
|
|
||||||
|
|
||||||
def check_origin(domain_name: str):
|
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.utcnow() - timedelta(days=1)).strftime("%Y-%m-%dT%H%%3A%M")
|
||||||
end_date = datetime.utcnow().strftime("%Y-%m-%dT%H%%3A%M")
|
end_date = datetime.utcnow().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}"
|
||||||
|
@ -19,7 +19,7 @@ def check_origin(domain_name: str):
|
||||||
return _check_origin(api_url, result)
|
return _check_origin(api_url, result)
|
||||||
|
|
||||||
|
|
||||||
def _check_origin(api_url: str, result: Dict):
|
def _check_origin(api_url: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
print(f"Processing {api_url}")
|
print(f"Processing {api_url}")
|
||||||
req = requests.get(api_url).json()
|
req = requests.get(api_url).json()
|
||||||
if 'results' not in req or not req['results']:
|
if 'results' not in req or not req['results']:
|
||||||
|
@ -38,7 +38,7 @@ def _check_origin(api_url: str, result: Dict):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def threshold_origin(domain_name):
|
def threshold_origin(domain_name: str) -> Dict[str, Any]:
|
||||||
ooni = check_origin(domain_name)
|
ooni = check_origin(domain_name)
|
||||||
for country in ooni:
|
for country in ooni:
|
||||||
total = sum([
|
total = sum([
|
||||||
|
@ -58,7 +58,7 @@ def threshold_origin(domain_name):
|
||||||
return ooni
|
return ooni
|
||||||
|
|
||||||
|
|
||||||
def set_ooni_alarm(origin_id: int, country: str, state: AlarmState, text: str):
|
def set_ooni_alarm(origin_id: int, country: str, state: AlarmState, text: str) -> None:
|
||||||
alarm = Alarm.query.filter(
|
alarm = Alarm.query.filter(
|
||||||
Alarm.origin_id == origin_id,
|
Alarm.origin_id == origin_id,
|
||||||
Alarm.alarm_type == f"origin-block-ooni-{country}"
|
Alarm.alarm_type == f"origin-block-ooni-{country}"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import datetime
|
import datetime
|
||||||
from typing import Iterable, Optional, Any
|
from typing import Iterable, Optional, Any, List
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
@ -9,7 +9,18 @@ from app.terraform.terraform import TerraformAutomation
|
||||||
|
|
||||||
|
|
||||||
class BridgeAutomation(TerraformAutomation):
|
class BridgeAutomation(TerraformAutomation):
|
||||||
def create_missing(self):
|
template: str
|
||||||
|
"""
|
||||||
|
Terraform configuration template using Jinja 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_parameters: List[str]
|
||||||
|
"""
|
||||||
|
List of parameters to be read from the application configuration for use
|
||||||
|
in the templating of the Terraform configuration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def create_missing(self) -> None:
|
||||||
bridgeconfs: Iterable[BridgeConf] = BridgeConf.query.filter(
|
bridgeconfs: Iterable[BridgeConf] = BridgeConf.query.filter(
|
||||||
BridgeConf.provider == self.provider,
|
BridgeConf.provider == self.provider,
|
||||||
BridgeConf.destroyed == None
|
BridgeConf.destroyed == None
|
||||||
|
@ -35,7 +46,7 @@ class BridgeAutomation(TerraformAutomation):
|
||||||
break
|
break
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
def destroy_expired(self):
|
def destroy_expired(self) -> None:
|
||||||
cutoff = datetime.datetime.utcnow() - datetime.timedelta(days=0)
|
cutoff = datetime.datetime.utcnow() - datetime.timedelta(days=0)
|
||||||
bridges = [b for b in Bridge.query.filter(
|
bridges = [b for b in Bridge.query.filter(
|
||||||
Bridge.destroyed == None,
|
Bridge.destroyed == None,
|
||||||
|
@ -48,8 +59,9 @@ class BridgeAutomation(TerraformAutomation):
|
||||||
def tf_prehook(self) -> Optional[Any]:
|
def tf_prehook(self) -> Optional[Any]:
|
||||||
self.create_missing()
|
self.create_missing()
|
||||||
self.destroy_expired()
|
self.destroy_expired()
|
||||||
|
return None
|
||||||
|
|
||||||
def tf_generate(self):
|
def tf_generate(self) -> None:
|
||||||
self.tf_write(
|
self.tf_write(
|
||||||
self.template,
|
self.template,
|
||||||
groups=Group.query.all(),
|
groups=Group.query.all(),
|
||||||
|
|
|
@ -8,7 +8,9 @@ from app.models.onions import Eotk
|
||||||
from app.terraform.terraform import TerraformAutomation
|
from app.terraform.terraform import TerraformAutomation
|
||||||
|
|
||||||
|
|
||||||
def update_eotk_instance(group_id: int, region: str, instance_id: str):
|
def update_eotk_instance(group_id: int,
|
||||||
|
region: str,
|
||||||
|
instance_id: str) -> None:
|
||||||
instance = Eotk.query.filter(
|
instance = Eotk.query.filter(
|
||||||
Eotk.group_id == group_id,
|
Eotk.group_id == group_id,
|
||||||
Eotk.region == region,
|
Eotk.region == region,
|
||||||
|
@ -74,7 +76,7 @@ class EotkAWSAutomation(TerraformAutomation):
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def tf_generate(self):
|
def tf_generate(self) -> None:
|
||||||
self.tf_write(
|
self.tf_write(
|
||||||
self.template,
|
self.template,
|
||||||
groups=Group.query.filter(
|
groups=Group.query.filter(
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import json
|
import json
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
from app.lists.mirror_mapping import mirror_mapping
|
from app.lists.mirror_mapping import mirror_mapping
|
||||||
|
@ -9,7 +10,18 @@ from app.terraform.terraform import TerraformAutomation
|
||||||
|
|
||||||
|
|
||||||
class ListAutomation(TerraformAutomation):
|
class ListAutomation(TerraformAutomation):
|
||||||
def tf_generate(self):
|
template: str
|
||||||
|
"""
|
||||||
|
Terraform configuration template using Jinja 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_parameters: List[str]
|
||||||
|
"""
|
||||||
|
List of parameters to be read from the application configuration for use
|
||||||
|
in the templating of the Terraform configuration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def tf_generate(self) -> None:
|
||||||
self.tf_write(
|
self.tf_write(
|
||||||
self.template,
|
self.template,
|
||||||
lists=MirrorList.query.filter(
|
lists=MirrorList.query.filter(
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
from abc import abstractmethod
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import datetime
|
import datetime
|
||||||
import math
|
import math
|
||||||
import string
|
import string
|
||||||
import random
|
import random
|
||||||
from typing import Dict
|
from typing import Dict, Optional, Any, List
|
||||||
|
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from tldextract import tldextract
|
from tldextract import tldextract
|
||||||
|
@ -17,6 +18,22 @@ from app.terraform.terraform import TerraformAutomation
|
||||||
|
|
||||||
class ProxyAutomation(TerraformAutomation):
|
class ProxyAutomation(TerraformAutomation):
|
||||||
subgroup_max = math.inf
|
subgroup_max = math.inf
|
||||||
|
"""
|
||||||
|
Maximum number of proxies to deploy per sub-group. This is required for some providers
|
||||||
|
where the number origins per group may exceed the number of proxies that can be configured
|
||||||
|
in a single "configuration block", e.g. Azure CDN's profiles.
|
||||||
|
"""
|
||||||
|
|
||||||
|
template: str
|
||||||
|
"""
|
||||||
|
Terraform configuration template using Jinja 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_parameters: List[str]
|
||||||
|
"""
|
||||||
|
List of parameters to be read from the application configuration for use
|
||||||
|
in the templating of the Terraform configuration.
|
||||||
|
"""
|
||||||
|
|
||||||
def get_subgroups(self) -> Dict[int, Dict[int, int]]:
|
def get_subgroups(self) -> Dict[int, Dict[int, int]]:
|
||||||
conn = db.engine.connect()
|
conn = db.engine.connect()
|
||||||
|
@ -27,12 +44,12 @@ class ProxyAutomation(TerraformAutomation):
|
||||||
AND proxy.provider = :provider
|
AND proxy.provider = :provider
|
||||||
GROUP BY origin.group_id, proxy.psg;
|
GROUP BY origin.group_id, proxy.psg;
|
||||||
"""), provider=self.provider)
|
"""), provider=self.provider)
|
||||||
subgroups = defaultdict(lambda: defaultdict(lambda: 0))
|
subgroups: Dict[int, Dict[int, int]] = defaultdict(lambda: defaultdict(lambda: 0))
|
||||||
for row in result:
|
for row in result:
|
||||||
subgroups[row[0]][row[1]] = row[2]
|
subgroups[row[0]][row[1]] = row[2]
|
||||||
return subgroups
|
return subgroups
|
||||||
|
|
||||||
def create_missing_proxies(self):
|
def create_missing_proxies(self) -> None:
|
||||||
groups = Group.query.all()
|
groups = Group.query.all()
|
||||||
subgroups = self.get_subgroups()
|
subgroups = self.get_subgroups()
|
||||||
for group in groups:
|
for group in groups:
|
||||||
|
@ -62,7 +79,7 @@ class ProxyAutomation(TerraformAutomation):
|
||||||
db.session.add(proxy)
|
db.session.add(proxy)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
def deprecate_orphaned_proxies(self):
|
def deprecate_orphaned_proxies(self) -> None:
|
||||||
proxies = Proxy.query.filter(
|
proxies = Proxy.query.filter(
|
||||||
Proxy.deprecated == None,
|
Proxy.deprecated == None,
|
||||||
Proxy.destroyed == None,
|
Proxy.destroyed == None,
|
||||||
|
@ -73,7 +90,7 @@ class ProxyAutomation(TerraformAutomation):
|
||||||
proxy.deprecate(reason="origin_destroyed")
|
proxy.deprecate(reason="origin_destroyed")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
def destroy_expired_proxies(self):
|
def destroy_expired_proxies(self) -> None:
|
||||||
cutoff = datetime.datetime.utcnow() - datetime.timedelta(days=3)
|
cutoff = datetime.datetime.utcnow() - datetime.timedelta(days=3)
|
||||||
proxies = Proxy.query.filter(
|
proxies = Proxy.query.filter(
|
||||||
Proxy.destroyed == None,
|
Proxy.destroyed == None,
|
||||||
|
@ -85,15 +102,20 @@ class ProxyAutomation(TerraformAutomation):
|
||||||
proxy.updated = datetime.datetime.utcnow()
|
proxy.updated = datetime.datetime.utcnow()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
def tf_prehook(self):
|
@abstractmethod
|
||||||
|
def import_state(self, state: Any) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def tf_prehook(self) -> Optional[Any]:
|
||||||
self.create_missing_proxies()
|
self.create_missing_proxies()
|
||||||
self.deprecate_orphaned_proxies()
|
self.deprecate_orphaned_proxies()
|
||||||
self.destroy_expired_proxies()
|
self.destroy_expired_proxies()
|
||||||
|
return None
|
||||||
|
|
||||||
def tf_posthook(self, *, prehook_result):
|
def tf_posthook(self, *, prehook_result: Any = None) -> None:
|
||||||
self.import_state(self.tf_show())
|
self.import_state(self.tf_show())
|
||||||
|
|
||||||
def tf_generate(self):
|
def tf_generate(self) -> None:
|
||||||
self.tf_write(
|
self.tf_write(
|
||||||
self.template,
|
self.template,
|
||||||
groups=Group.query.all(),
|
groups=Group.query.all(),
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from typing import Optional, Any
|
||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.mirrors import Proxy
|
from app.models.mirrors import Proxy
|
||||||
from app.terraform.proxy import ProxyAutomation
|
from app.terraform.proxy import ProxyAutomation
|
||||||
|
@ -157,7 +159,7 @@ class ProxyAzureCdnAutomation(ProxyAutomation):
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def import_state(self, state):
|
def import_state(self, state: Optional[Any]) -> None:
|
||||||
proxies = Proxy.query.filter(
|
proxies = Proxy.query.filter(
|
||||||
Proxy.provider == self.provider,
|
Proxy.provider == self.provider,
|
||||||
Proxy.destroyed == None
|
Proxy.destroyed == None
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.mirrors import Proxy
|
from app.models.mirrors import Proxy
|
||||||
|
@ -72,7 +73,11 @@ class ProxyCloudfrontAutomation(ProxyAutomation):
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def import_state(self, state):
|
def import_state(self, state: Any) -> None:
|
||||||
|
assert(isinstance(state, dict))
|
||||||
|
if "child_modules" not in state['values']['root_module']:
|
||||||
|
# There are no CloudFront proxies deployed to import state for
|
||||||
|
return
|
||||||
for mod in state['values']['root_module']['child_modules']:
|
for mod in state['values']['root_module']['child_modules']:
|
||||||
if mod['address'].startswith('module.cloudfront_'):
|
if mod['address'].startswith('module.cloudfront_'):
|
||||||
for res in mod['resources']:
|
for res in mod['resources']:
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
# type: ignore
|
||||||
|
# TODO: This module doesn't work at all
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import string
|
import string
|
||||||
|
|
|
@ -19,6 +19,11 @@ class TerraformAutomation(BaseAutomation):
|
||||||
Default parallelism for remote API calls.
|
Default parallelism for remote API calls.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
provider: str
|
||||||
|
"""
|
||||||
|
Short name for the provider used by this module.
|
||||||
|
"""
|
||||||
|
|
||||||
def automate(self, full: bool = False) -> Tuple[bool, str]:
|
def automate(self, full: bool = False) -> Tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
Runs the Terraform automation module. The run will follow these steps:
|
Runs the Terraform automation module. The run will follow these steps:
|
||||||
|
|
1
mypy.ini
1
mypy.ini
|
@ -24,4 +24,3 @@ no_implicit_optional = True
|
||||||
; Some libraries don't have hints yet
|
; Some libraries don't have hints yet
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
follow_imports = silent
|
follow_imports = silent
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue