From 0c349091e7423cae8e069c0b94be3457f63def1f Mon Sep 17 00:00:00 2001 From: Iain Learmonth Date: Fri, 22 Apr 2022 12:52:41 +0100 Subject: [PATCH] cli: initial import for import/export --- app/cli/__main__.py | 29 ++++++++++++++++++ app/cli/db.py | 74 +++++++++++++++++++++++++++++++++++++++++++++ app/models.py | 53 ++++++++++++++++++++++++-------- 3 files changed, 143 insertions(+), 13 deletions(-) create mode 100644 app/cli/__main__.py create mode 100644 app/cli/db.py diff --git a/app/cli/__main__.py b/app/cli/__main__.py new file mode 100644 index 0000000..08cbedf --- /dev/null +++ b/app/cli/__main__.py @@ -0,0 +1,29 @@ +import argparse +import logging +import sys +from os.path import basename + +from app.cli.db import DbCliHandler + + +def parse_args(argv): + if basename(argv[0]) == "__main__.py": + argv[0] = "bypass" + parser = argparse.ArgumentParser() + parser.add_argument("-v", "--verbose", help="increase logging verbosity", action="store_true") + subparsers = parser.add_subparsers(title="command", help="command to run") + DbCliHandler.add_subparser_to(subparsers) + args = parser.parse_args(argv[1:]) + if "cls" in args: + command = args.cls(args) + command.run() + else: + parser.print_help() + + +if __name__ == "__main__": + verbose = "-v" in sys.argv or "--verbose" in sys.argv + logging.basicConfig( + level=logging.DEBUG if verbose else logging.INFO) + logging.debug("Arguments: %s", sys.argv) + parse_args(sys.argv) diff --git a/app/cli/db.py b/app/cli/db.py new file mode 100644 index 0000000..9e3a94b --- /dev/null +++ b/app/cli/db.py @@ -0,0 +1,74 @@ +import argparse +import csv +import datetime +import logging +import sys + +from app import app +from app.extensions import db +from app.models import Group, Origin, Proxy, BridgeConf, Alarm + +models = { + "group": Group, + "origin": Origin, + "proxy": Proxy, + "bridge": BridgeConf, + "alarm": Alarm +} + + +def export(model: db.Model): + out = csv.writer(sys.stdout) + out.writerow(model.csv_header()) + for r in model.query.all(): + out.writerow(r.csv_row()) + + +def impot(model: db.Model): + first = True + header = model.csv_header() + try: + for line in csv.reader(sys.stdin): + if first: + if line != header: + logging.error("CSV header mismatch") + sys.exit(1) + first = False + continue + x = model() + for i in range(len(header)): + if header[i] in ["added", "updated", "destroyed", "deprecated", "last_updated", "terraform_updated"]: + if line[i] == "": + line[i] = None + else: + line[i] = datetime.datetime.strptime(line[i], "%Y-%m-%d %H:%M:%S.%f") + setattr(x, header[i], line[i]) + db.session.add(x) + db.session.commit() + logging.info("Import completed successfully") + except Exception as e: + logging.exception(e) + db.session.rollback() + + +class DbCliHandler: + @classmethod + def add_subparser_to(cls, subparsers: argparse._SubParsersAction) -> None: + parser = subparsers.add_parser("db", help="database operations") + parser.add_argument("--export", choices=["group", "origin", "proxy", "bridge"], + help="export data to CSV format") + parser.add_argument("--import", choices=["group", "origin", "proxy", "bridge"], + help="import data from CSV format", dest="impot") + parser.set_defaults(cls=cls) + + def __init__(self, args): + self.args = args + + def run(self): + with app.app_context(): + if self.args.export: + export(models[self.args.export]) + elif self.args.impot: + impot(models[self.args.impot]) + else: + logging.error("No action requested") diff --git a/app/models.py b/app/models.py index ce94029..3f3f618 100644 --- a/app/models.py +++ b/app/models.py @@ -18,6 +18,17 @@ class AbstractConfiguration(db.Model): self.updated = datetime.utcnow() db.session.commit() + @classmethod + def csv_header(self): + return [ + "id", "description", "added", "updated", "destroyed" + ] + + def csv_row(self): + return [ + getattr(self, x) for x in self.csv_header() + ] + class AbstractResource(db.Model): __abstract__ = True @@ -40,30 +51,28 @@ class AbstractResource(db.Model): self.updated = datetime.utcnow() db.session.commit() + def csv_row(self): + return [ + self[x] for x in self.csv_header() + ] + def __repr__(self): return f"<{self.__class__.__name__} #{self.id}>" -class Group(db.Model): - id = db.Column(db.Integer, primary_key=True) +class Group(AbstractConfiguration): group_name = db.Column(db.String(80), unique=True, nullable=False) - description = db.Column(db.String(255), nullable=False) eotk = db.Column(db.Boolean()) - added = db.Column(db.DateTime(), default=datetime.utcnow, nullable=False) - updated = db.Column(db.DateTime(), default=datetime.utcnow, nullable=False) origins = db.relationship("Origin", back_populates="group") bridgeconfs = db.relationship("BridgeConf", back_populates="group") alarms = db.relationship("Alarm", back_populates="group") - def as_dict(self): - return { - "id": self.id, - "name": self.group_name, - "description": self.description, - "added": self.added, - "updated": self.updated - } + @classmethod + def csv_header(self): + return super().csv_header() + [ + "group_name", "eotk" + ] def __repr__(self): return '' % self.group_name @@ -78,6 +87,12 @@ class Origin(AbstractConfiguration): proxies = db.relationship("Proxy", back_populates="origin") alarms = db.relationship("Alarm", back_populates="origin") + @classmethod + def csv_header(cls): + return [ + "id", "description", "added", "updated", "destroyed", "group_id", "domain_name" + ] + def as_dict(self): return { "id": self.id, @@ -171,6 +186,18 @@ class Alarm(db.Model): proxy = db.relationship("Proxy", back_populates="alarms") bridge = db.relationship("Bridge", back_populates="alarms") + @classmethod + def csv_header(cls): + return [ + "id", "target", "group_id", "origin_id", "proxy_id", "bridge_id", "alarm_type", + "alarm_state", "state_changed", "last_updated", "text" + ] + + def csv_row(self): + return [ + self[x] for x in self.csv_header() + ] + def update_state(self, state: AlarmState, text: str): if self.alarm_state != state or self.state_changed is None: self.state_changed = datetime.utcnow()