2022-05-08 17:20:04 +01:00
|
|
|
import datetime
|
|
|
|
import logging
|
2022-05-12 09:57:42 +01:00
|
|
|
from traceback import TracebackException
|
2022-06-17 13:21:35 +01:00
|
|
|
from typing import Type
|
2022-05-08 17:20:04 +01:00
|
|
|
|
|
|
|
from app import app
|
2022-06-17 13:21:35 +01:00
|
|
|
from app.cli import _SubparserType, BaseCliHandler
|
2022-05-08 17:20:04 +01:00
|
|
|
from app.extensions import db
|
2022-05-14 10:18:00 +01:00
|
|
|
from app.models.activity import Activity
|
2022-05-08 17:20:04 +01:00
|
|
|
from app.models.automation import Automation, AutomationState, AutomationLogs
|
|
|
|
from app.terraform import BaseAutomation
|
2022-05-09 08:09:57 +01:00
|
|
|
from app.terraform.block_bridge_github import BlockBridgeGitHubAutomation
|
|
|
|
from app.terraform.block_external import BlockExternalAutomation
|
|
|
|
from app.terraform.block_ooni import BlockOONIAutomation
|
2022-05-09 14:11:05 +01:00
|
|
|
from app.terraform.block_roskomsvoboda import BlockRoskomsvobodaAutomation
|
2022-05-13 15:40:59 +01:00
|
|
|
from app.terraform.eotk.aws import EotkAWSAutomation
|
2022-05-18 15:49:36 +01:00
|
|
|
from app.terraform.alarms.eotk_aws import AlarmEotkAwsAutomation
|
2022-05-08 17:20:04 +01:00
|
|
|
from app.terraform.alarms.proxy_azure_cdn import AlarmProxyAzureCdnAutomation
|
|
|
|
from app.terraform.alarms.proxy_cloudfront import AlarmProxyCloudfrontAutomation
|
|
|
|
from app.terraform.alarms.proxy_http_status import AlarmProxyHTTPStatusAutomation
|
2022-05-27 10:38:40 +01:00
|
|
|
from app.terraform.alarms.smart_aws import AlarmSmartAwsAutomation
|
2022-05-08 17:20:04 +01:00
|
|
|
from app.terraform.bridge.aws import BridgeAWSAutomation
|
|
|
|
from app.terraform.bridge.gandi import BridgeGandiAutomation
|
|
|
|
from app.terraform.bridge.hcloud import BridgeHcloudAutomation
|
|
|
|
from app.terraform.bridge.ovh import BridgeOvhAutomation
|
|
|
|
from app.terraform.list.github import ListGithubAutomation
|
|
|
|
from app.terraform.list.gitlab import ListGitlabAutomation
|
|
|
|
from app.terraform.list.s3 import ListS3Automation
|
|
|
|
from app.terraform.proxy.azure_cdn import ProxyAzureCdnAutomation
|
|
|
|
from app.terraform.proxy.cloudfront import ProxyCloudfrontAutomation
|
2022-06-23 15:49:09 +01:00
|
|
|
from app.terraform.proxy.fastly import ProxyFastlyAutomation
|
2022-05-08 17:20:04 +01:00
|
|
|
|
|
|
|
jobs = {
|
|
|
|
x.short_name: x
|
|
|
|
for x in [
|
2022-05-18 15:49:36 +01:00
|
|
|
AlarmEotkAwsAutomation,
|
2022-05-08 17:20:04 +01:00
|
|
|
AlarmProxyAzureCdnAutomation,
|
|
|
|
AlarmProxyCloudfrontAutomation,
|
|
|
|
AlarmProxyHTTPStatusAutomation,
|
2022-05-27 10:38:40 +01:00
|
|
|
AlarmSmartAwsAutomation,
|
2022-05-09 08:09:57 +01:00
|
|
|
BlockBridgeGitHubAutomation,
|
|
|
|
BlockExternalAutomation,
|
|
|
|
BlockOONIAutomation,
|
2022-05-09 14:11:05 +01:00
|
|
|
BlockRoskomsvobodaAutomation,
|
2022-05-08 17:20:04 +01:00
|
|
|
BridgeAWSAutomation,
|
|
|
|
BridgeGandiAutomation,
|
|
|
|
BridgeHcloudAutomation,
|
|
|
|
BridgeOvhAutomation,
|
2022-05-13 15:40:59 +01:00
|
|
|
EotkAWSAutomation,
|
2022-05-08 17:20:04 +01:00
|
|
|
ListGithubAutomation,
|
|
|
|
ListGitlabAutomation,
|
|
|
|
ListS3Automation,
|
|
|
|
ProxyAzureCdnAutomation,
|
2022-06-23 15:49:09 +01:00
|
|
|
ProxyCloudfrontAutomation,
|
|
|
|
ProxyFastlyAutomation
|
2022-05-08 17:20:04 +01:00
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-05-16 09:24:37 +01:00
|
|
|
def run_all(**kwargs: bool) -> None:
|
2022-06-17 13:21:35 +01:00
|
|
|
"""
|
|
|
|
Run all automation tasks.
|
|
|
|
|
|
|
|
:param kwargs: this function takes the same arguments as :func:`run_job` and will pass the same arguments
|
|
|
|
to every task
|
|
|
|
:return: None
|
|
|
|
"""
|
2022-05-08 17:20:04 +01:00
|
|
|
for job in jobs.values():
|
2022-05-16 10:08:18 +01:00
|
|
|
run_job(job, **kwargs) # type: ignore
|
2022-05-08 17:20:04 +01:00
|
|
|
|
|
|
|
|
2022-05-16 09:24:37 +01:00
|
|
|
def run_job(job_cls: Type[BaseAutomation], *,
|
|
|
|
force: bool = False, ignore_schedule: bool = False) -> None:
|
|
|
|
automation = Automation.query.filter(Automation.short_name == job_cls.short_name).first()
|
2022-05-08 17:20:04 +01:00
|
|
|
if automation is None:
|
|
|
|
automation = Automation()
|
2022-05-16 09:24:37 +01:00
|
|
|
automation.short_name = job_cls.short_name
|
|
|
|
automation.description = job_cls.description
|
2022-05-14 10:18:47 +01:00
|
|
|
automation.enabled = False
|
2022-05-08 17:20:04 +01:00
|
|
|
automation.next_is_full = False
|
|
|
|
automation.added = datetime.datetime.utcnow()
|
|
|
|
automation.updated = automation.added
|
|
|
|
db.session.add(automation)
|
2022-05-14 12:05:06 +01:00
|
|
|
db.session.commit()
|
2022-05-08 17:20:04 +01:00
|
|
|
else:
|
|
|
|
if automation.state == AutomationState.RUNNING and not force:
|
|
|
|
logging.warning("Not running an already running automation")
|
|
|
|
return
|
|
|
|
if not ignore_schedule and not force:
|
|
|
|
if automation.next_run is not None and automation.next_run > datetime.datetime.utcnow():
|
|
|
|
logging.warning("Not time to run this job yet")
|
|
|
|
return
|
|
|
|
if not automation.enabled and not force:
|
2022-05-17 09:03:43 +01:00
|
|
|
logging.warning("job %s is disabled and --force not specified", job_cls.short_name)
|
2022-05-08 17:20:04 +01:00
|
|
|
return
|
|
|
|
automation.state = AutomationState.RUNNING
|
|
|
|
db.session.commit()
|
2022-05-16 09:24:37 +01:00
|
|
|
job: BaseAutomation = job_cls()
|
2022-05-08 17:20:04 +01:00
|
|
|
try:
|
|
|
|
success, logs = job.automate()
|
2022-05-17 09:03:43 +01:00
|
|
|
# We want to catch any and all exceptions that would cause problems here, because
|
|
|
|
# the error handling process isn't really handling the error, but rather causing it
|
|
|
|
# to be logged for investigation. Catching more specific exceptions would just mean that
|
|
|
|
# others go unrecorded and are difficult to debug.
|
2022-06-17 13:21:35 +01:00
|
|
|
except Exception as exc: # pylint: disable=broad-except
|
|
|
|
trace = TracebackException.from_exception(exc)
|
2022-05-08 17:20:04 +01:00
|
|
|
success = False
|
2022-06-17 13:21:35 +01:00
|
|
|
logs = "\n".join(trace.format())
|
2022-05-08 17:20:04 +01:00
|
|
|
if success:
|
|
|
|
automation.state = AutomationState.IDLE
|
2022-05-09 14:11:05 +01:00
|
|
|
automation.next_run = datetime.datetime.utcnow() + datetime.timedelta(
|
|
|
|
minutes=getattr(job, "frequency", 7))
|
2022-05-08 17:20:04 +01:00
|
|
|
else:
|
|
|
|
automation.state = AutomationState.ERROR
|
|
|
|
automation.enabled = False
|
|
|
|
automation.next_run = None
|
|
|
|
log = AutomationLogs()
|
|
|
|
log.automation_id = automation.id
|
|
|
|
log.added = datetime.datetime.utcnow()
|
|
|
|
log.updated = datetime.datetime.utcnow()
|
2022-06-18 14:17:46 +01:00
|
|
|
log.logs = str(logs)
|
2022-05-08 17:20:04 +01:00
|
|
|
db.session.add(log)
|
2022-05-14 10:18:00 +01:00
|
|
|
activity = Activity(
|
|
|
|
activity_type="automation",
|
2022-06-18 14:16:38 +01:00
|
|
|
text=(f"[{automation.short_name}] 🚨 Automation failure: It was not possible to handle this failure safely "
|
|
|
|
"and so the automation task has been automatically disabled. It may be possible to simply re-enable "
|
|
|
|
"the task, but repeated failures will usually require deeper investigation. See logs for full "
|
|
|
|
"details.")
|
2022-05-14 10:18:00 +01:00
|
|
|
)
|
2022-05-16 14:06:07 +01:00
|
|
|
db.session.add(activity)
|
2022-05-14 10:18:00 +01:00
|
|
|
activity.notify() # Notify before commit because the failure occurred even if we can't commit.
|
2022-05-08 17:20:04 +01:00
|
|
|
automation.last_run = datetime.datetime.utcnow()
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
|
2022-06-17 13:21:35 +01:00
|
|
|
class AutomateCliHandler(BaseCliHandler):
|
2022-05-08 17:20:04 +01:00
|
|
|
@classmethod
|
2022-05-16 09:24:37 +01:00
|
|
|
def add_subparser_to(cls, subparsers: _SubparserType) -> None:
|
2022-05-08 17:20:04 +01:00
|
|
|
parser = subparsers.add_parser("automate", help="automation operations")
|
|
|
|
parser.add_argument("-a", "--all", dest="all", help="run all automation jobs", action="store_true")
|
|
|
|
parser.add_argument("-j", "--job", dest="job", choices=sorted(jobs.keys()),
|
|
|
|
help="run a specific automation job")
|
|
|
|
parser.add_argument("--force", help="run job even if disabled and it's not time yet", action="store_true")
|
|
|
|
parser.add_argument("--ignore-schedule", help="run job even if it's not time yet", action="store_true")
|
|
|
|
parser.set_defaults(cls=cls)
|
|
|
|
|
2022-05-16 09:24:37 +01:00
|
|
|
def run(self) -> None:
|
2022-05-08 17:20:04 +01:00
|
|
|
with app.app_context():
|
|
|
|
if self.args.job:
|
2022-05-16 10:08:18 +01:00
|
|
|
run_job(jobs[self.args.job], # type: ignore
|
|
|
|
force=self.args.force,
|
|
|
|
ignore_schedule=self.args.ignore_schedule)
|
2022-05-08 17:20:04 +01:00
|
|
|
elif self.args.all:
|
|
|
|
run_all(force=self.args.force, ignore_schedule=self.args.ignore_schedule)
|
|
|
|
else:
|
|
|
|
logging.error("No action requested")
|