From c4b54357f7c9485eefca015cf423169900ac4050 Mon Sep 17 00:00:00 2001 From: Tobias Date: Fri, 22 Nov 2019 14:26:16 +0100 Subject: [PATCH] automatically adding recorders, models and rooms and association of those --- backend/__init__.py | 9 ++- backend/__main__.py | 15 ++-- backend/api/models.py | 10 ++- backend/config.py | Bin 4352 -> 4409 bytes backend/models/initial_recorders.json | Bin 9768 -> 9760 bytes backend/models/recorder_model.py | 18 +++-- backend/models/room_model.py | 11 ++- backend/tools/helpers.py | 1 - backend/tools/model_updater.py | 96 ++++++++++++++++++-------- 9 files changed, 114 insertions(+), 46 deletions(-) diff --git a/backend/__init__.py b/backend/__init__.py index 7e83474..055fdcb 100644 --- a/backend/__init__.py +++ b/backend/__init__.py @@ -15,6 +15,7 @@ from flask import Flask, jsonify from flask_httpauth import HTTPTokenAuth, HTTPBasicAuth, MultiAuth from flask_jwt_extended import JWTManager, decode_token from flask_login import LoginManager +from flask_restplus import abort from flask_sqlalchemy import SQLAlchemy from flask_cors import CORS from backend.config import Config @@ -128,10 +129,16 @@ login_manager.init_app(app) # flask_jwt_extended: to be used usually by API jwt_extended = JWTManager(app) -# +# this is another library.... this is (probaby ->verify) used to check JWTs provided by external sources (KIT, etc.) jwt_auth = HTTPTokenAuth('Bearer') +@jwt_extended.invalid_token_loader +def unauthorized_jwt(token): + main_logger.info("Unauthorized access; invalid token provided: {}".format(token)) + abort(401) + + @jwt_auth.verify_token def verify_token(token): """This function (and HTTPTokenAuth('Bearer')) has been defined to be used together with MultiAuth. For API calls diff --git a/backend/__main__.py b/backend/__main__.py index 413d465..95e5a1e 100644 --- a/backend/__main__.py +++ b/backend/__main__.py @@ -8,16 +8,19 @@ import ssl from jinja2.exceptions import TemplateNotFound from backend import app, db -from backend.models import room_model, recorder_model +from backend.models import room_model, recorder_model, RecorderCommand from backend.recorder_adapters import get_defined_recorder_adapters -from backend.tools.model_updater import update_recorder_models_database +from backend.tools.model_updater import update_recorder_models_database, create_default_recorders def main(): logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') - db.drop_all() - db.create_all() + #db.drop_all() + #db.create_all() + room_model.pre_fill_table() + update_recorder_models_database(drop=False) + create_default_recorders() print(app.config.get("SERVER_NAME", None)) server_name = app.config.get("SERVER_NAME", None) @@ -34,10 +37,6 @@ def main(): except Exception as e: logging.critical(e) - - room_model.pre_fill_table() - update_recorder_models_database() - app.run(debug=True, host="0.0.0.0") diff --git a/backend/api/models.py b/backend/api/models.py index e774d09..2e89c12 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -29,12 +29,21 @@ recorder_model = api_recorder.model('Recorder', { 'created_at': fields.DateTime(required=False, description='Creation date of the recorder'), 'last_time_modified': fields.DateTime(required=False, description='Creation date of the recorder'), 'name': fields.String(min_length=3, required=True, description='The recorder\'s name'), + 'model_name': fields.String(min_length=3, required=True, + description='The recorder\'s model name (might slightly ' + 'differ from actual name of the model)'), + 'serial_number': fields.String(required=False, description='The recorder\'s serial number'), + 'firmware_version': fields.String(required=False, description='The recorder\'s firmware'), + 'description': fields.String(required=False, description='The recorder\'s description'), 'locked': fields.Boolean(required=False, description='Indicates whether the recorder settings can be altered'), 'lock_message': fields.String(required=False, description='Optional: message explaining lock state'), 'offline': fields.Boolean(required=False, description='Should be set when recorder is disconnected for maintenance, etc.'), + 'additional_camera_connected': fields.Boolean(required=False, + description='Indicates whether an additional camera is connected'), 'ip': fields.String(required=False, description='The recorder\'s IP address'), + 'mac': fields.String(required=False, description='The recorder\'s IP address'), 'network_name': fields.String(required=False, description='The recorder\'s network name'), 'ssh_port': fields.Integer(required=True, default=22, description='The recorder\'s SSH port number'), 'telnet_port': fields.Integer(required=True, default=23, description='The recorder\'s telnet port number'), @@ -91,4 +100,3 @@ recorder_model_model = api_recorder.model('Recorder Model', { description='Model of the recorder.'), 'commands': fields.List(fields.Nested(recorder_command_model), attribute="recorder_commands") }) - diff --git a/backend/config.py b/backend/config.py index 6d3ed2b5e33b5d54b18a671ae990f61a5d9c265e..1286c24d55454b297be7ae7efa497eab43005177 100644 GIT binary patch literal 4409 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 literal 4352 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 C{{Kn< diff --git a/backend/models/recorder_model.py b/backend/models/recorder_model.py index 7c8cfdd..0a9b4cd 100644 --- a/backend/models/recorder_model.py +++ b/backend/models/recorder_model.py @@ -60,11 +60,11 @@ class RecorderModel(db.Model): return RecorderModel.query.filter(RecorderModel.checksum == md5_sum).first() @hybrid_property - def requires_user(self): + def requires_username(self): return self._requires_user > 0 - @requires_user.setter - def requires_user(self, val: bool): + @requires_username.setter + def requires_username(self, val: bool): self._requires_user = 1 if val else 0 @hybrid_property @@ -87,10 +87,9 @@ class Recorder(db.Model): name = db.Column(db.Unicode(63), unique=True, nullable=False) model_name = db.Column(db.Unicode(63), unique=False, nullable=False) serial_number = db.Column(db.Unicode(63), unique=True, nullable=True) - locked = db.Column(db.Boolean, default=False) + locked = db.Column(db.Boolean, default=False) # no modifications allowed lock_message = db.Column(db.String, nullable=True, default=None) - in_maintenance = db.Column(db.Boolean, default=False) - offline = db.Column(db.Boolean, default=False) + offline = db.Column(db.Boolean, default=False) # maintenance, etc. description = db.Column(db.Unicode(255), unique=False, nullable=True, default="") _mac = db.Column(db.String(17), unique=True, nullable=True) _ip = db.Column(db.String(15), unique=True, nullable=True, default=None) @@ -122,6 +121,13 @@ class Recorder(db.Model): def get_by_identifier(identifier): return Recorder.query.filter(Recorder.id == identifier).first() + @staticmethod + def get_by_mac(mac: str): + if mac is None or mac == '': + return None + mac = mac.replace('-', ':').lower() + return Recorder.query.filter(Recorder._mac == mac).first() + @staticmethod def get_all(): return Recorder.query.all() diff --git a/backend/models/room_model.py b/backend/models/room_model.py index 5d5864f..e074e0b 100644 --- a/backend/models/room_model.py +++ b/backend/models/room_model.py @@ -3,6 +3,7 @@ Models for lecture recorder """ import json +import logging from sqlalchemy import MetaData, CheckConstraint from sqlalchemy.exc import IntegrityError @@ -12,6 +13,8 @@ from datetime import datetime, timedelta from backend import db, app, login_manager from backend.tools.scrape_rooms import scrape_rooms +logger = logging.getLogger("lrc."+__name__) + metadata = MetaData() @@ -74,12 +77,16 @@ class Room(db.Model): def pre_fill_table(): rooms = scrape_rooms() - i_tunes_u_mappings = [Room(name=room['name'], number=room['room_number'], + logger.debug("tada") + logger.debug("got {} rooms".format(len(rooms))) + db_rooms = [Room(name=room['name'], number=room['room_number'], building_name=room['building_name'], building_number=room['building_number']) for room in rooms] try: - db.session.bulk_save_objects(i_tunes_u_mappings) + db.session.bulk_save_objects(db_rooms) db.session.commit() + logger.debug("rooms commited to DB!") except IntegrityError as e: + logger.error("Could not add rooms! ({})".format(e)) db.session.rollback() diff --git a/backend/tools/helpers.py b/backend/tools/helpers.py index e27da4f..0361d21 100644 --- a/backend/tools/helpers.py +++ b/backend/tools/helpers.py @@ -1,7 +1,6 @@ import hashlib - def calculate_md5_checksum(string_to_md5_sum: str): return hashlib.md5(string_to_md5_sum.encode('utf-8')).hexdigest() diff --git a/backend/tools/model_updater.py b/backend/tools/model_updater.py index e4d5b60..92c480f 100644 --- a/backend/tools/model_updater.py +++ b/backend/tools/model_updater.py @@ -21,21 +21,27 @@ from backend.models.recorder_model import RecorderModel, RecorderCommand, Record from backend.recorder_adapters import get_defined_recorder_adapters from backend.tools.helpers import calculate_md5_checksum +logger = logging.getLogger("lrc." + __name__) + KNOWN_RECORDERS = {re.compile(r'(?PSMP)[\s]*(?P[\d]+)[\s]*.?[\s]*(?P[\S]*)'): 'SMP', re.compile( r'(?PLectureRecorder X2|LectureRecorder|VGADVI Recorder|DVI Broadcaster DL|DVIRecorderDL)'): 'Epiphan'} def create_recorder_commands_for_recorder_adapter(command_definitions: dict, recorder_model: RecorderModel): + logger.debug("Received {} command definitions to create " + "for recoder model {}".format(len(command_definitions), + recorder_model.record_adapter_id)) existing_recorder_commands = RecorderCommand.query.filter( - and_(RecorderCommand.name.in_(command_definitions.keys())), + and_( + RecorderCommand.name.in_([recorder_model.record_adapter_id + ":" + k for k in command_definitions.keys()])), RecorderCommand.recorder_model == recorder_model) existing_commands = set() for existing_command in existing_recorder_commands: existing_commands.add(existing_command.name) args = command_definitions.get(existing_command.name) if dumps(existing_command.parameters) != dumps(args): - logging.warning( + logger.warning( "The function definition {} collides with an existing definition of the same name " "but different parameters!".format( existing_command.name)) @@ -44,15 +50,21 @@ def create_recorder_commands_for_recorder_adapter(command_definitions: dict, rec db.session.commit() for c_d in set(command_definitions.keys()) - existing_commands: - # print(c_d) args = command_definitions.get(c_d) + # print(args) # create new recorder command(s) - r_c = RecorderCommand(name=c_d, parameters=args, recorder_model=recorder_model) + r_c = RecorderCommand(name=recorder_model.record_adapter_id + ":" + c_d, parameters=args, + recorder_model=recorder_model) db.session.add(r_c) + db.session.flush() db.session.commit() -def update_recorder_models_database(): +def update_recorder_models_database(drop: bool = False): + if drop: + for r in RecorderModel.get_all(): + db.session.delete(r) + db.session.commit() r_as = get_defined_recorder_adapters() for r_a in r_as: try: @@ -64,35 +76,40 @@ def update_recorder_models_database(): db.session.add(r_m) db.session.flush() db.session.refresh(r_m) + logger.debug("Creating command definitions for rec mod adapter {}".format(r_m.record_adapter_id)) + create_recorder_commands_for_recorder_adapter(r_a["commands"], r_m) else: - if not r_m.model_name == r_a["name"]: + if r_m.model_name != r_a["name"]: r_m.model_name = r_a["name"] r_m.last_time_modified = datetime.utcnow() - if not model_checksum == r_m.checksum: + if model_checksum != r_m.checksum: r_m.last_time_modified = datetime.utcnow() r_m.checksum = model_checksum r_m.last_checksum_change = datetime.utcnow() + logger.debug("Updating command definitions for rec mod adapter {}".format(r_m.record_adapter_id)) create_recorder_commands_for_recorder_adapter(r_a["commands"], r_m) except IntegrityError as e: + logger.error(e) db.session.rollback() db.session.commit() def get_recorder_room(rec: dict) -> Room(): rooms = Room.get_by_building_number(rec.get('building', None)) - if rooms.count() <= 1: + if rooms.count() <= 0: + logger.warning("Building {} unknown! Can not find room for recorder {}.".format(rec['building'], rec['name'])) + return None + if rooms.count() == 1: return rooms.first() room_name = rec.get('room') + room_name = room_name.replace('–', ' ') for room in rooms: if all([r_n in room.name for r_n in room_name.split()]): return room + logger.warning("No room found for recorder {}".format(rec['name'])) def create_default_recorders(): - models = RecorderModel.get_all() - for m in models: - print(m) - f = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir, 'models', 'initial_recorders.json')) with open(f, 'r') as json_file: recorders = json.load(json_file)['recorders'] @@ -112,39 +129,64 @@ def create_default_recorders(): model_number = match.groupdict().get('number', None) options = match.groupdict().get('options', None) + if options is not None and options != "": + options = options.split(',') if model_number is not None: - model_name = name + model_number[:-1] # just get prefix (remove last digit) + model_name = name + " " + model_number + model_search_name = name + model_number[:-1] # just get prefix (remove last digit); SMP 35x -> 35 else: - model_name = KNOWN_RECORDERS[k_r] - rec_model = RecorderModel.get_where_adapter_id_contains(model_name) - - rec = Recorder(name=room_rec_name + " Recorder", model_name=model_name, recorder_model=rec_model, - username=username, password=password, firmware_version=firmware_version) - rec.mac = mac + model_name = name + model_search_name = KNOWN_RECORDERS[k_r] + rec_model = RecorderModel.get_where_adapter_id_contains(model_search_name) + rec = Recorder.get_by_mac(mac) + if rec is None: + rec = Recorder.get_by_name(room_rec_name + " Recorder") + if rec is None: + rec = Recorder() + rec.mac = mac + db.session.add(rec) + rec.name = room_rec_name + " Recorder" + rec.model_name = model_name + rec.recorder_model = rec_model + rec.username = username + rec.password = password + rec.firmware_version = firmware_version rec.ip = ip4 rec.additional_camera_connected = additional_camera rec.additional_note = description + rec.configured_options = options rec.room = get_recorder_room(r) - print(rec) - - db.session.add(rec) db.session.flush() db.session.refresh(rec) - print(recorders) + db.session.commit() if __name__ == '__main__': + # for r in Room.get_all(): + # print(r) + # exit() + + commands = RecorderCommand.get_all() + for c in commands: + print(c) + print(c.recorder_model) + + for r_m in RecorderModel.get_all(): + print(r_m.recorder_commands) + + exit() recorders = Recorder.get_all() for r in recorders: - print("{}: {}".format(r, r.room)) + if r.room is None: + print("{}: {}".format(r, r.room)) # db.drop_all() # db.create_all() - # update_recorder_models_database() - # create_default_recorders() - # db.session.commit() + update_recorder_models_database() + create_default_recorders() + db.session.commit() # print(get_recorder_room({"room": "Grosser Hörsaal Bauingenieure", "building": "10.50"})) # print(get_recorder_room({"room": "Grosser Hörsaal Bauingenieure", "building": "30.95"}))