#!/bin/sh # # NEURON IIoT System for Industry 4.0 # Copyright (C) 2020-2022 EMQ Technologies Co., Ltd All rights reserved. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # """": if type python3 > /dev/null 2>&1 then exec python3 "$0" "$@" elif type python > /dev/null 2>&1 then exec python "$0" "$@" else echo >&2 "Python not installed" fi exit 1 """ # """ from __future__ import print_function import argparse import os import re import sqlite3 import json VERBOSE = False def log(message): if VERBOSE: print(message) def parse_args(): """Parse command line arguments.""" parser = argparse.ArgumentParser() parser.add_argument("-v", "--verbose", action="count", help="verbosity") parser.add_argument( "-s", "--schema_dir", type=str, default="config/", help="schema directory", ) parser.add_argument( "-d", "--data_dir", type=str, default="persistence/", help="data directory", ) subparsers = parser.add_subparsers( dest="subcommand", help="supported subcommands" ) import_parser = subparsers.add_parser( "import", help="import persistence json data" ) show_parser = subparsers.add_parser( "show", help="show database schema version" ) up_parser = subparsers.add_parser( "up", help="bring database schema to latest version" ) up_parser.add_argument( "--import_once", action="store_true", help="import persistence json data if database is empty", ) return parser.parse_args() class Migration: def __init__(self, schema_dir="config", database_dir="persistence", database_name="sqlite.db"): try: database_path = os.path.join(database_dir, database_name) conn = sqlite3.connect(database_path) except Exception as e: print("Connecting database `{}` fail, {}".format(database_path, e)) raise e self.conn_ = conn self.cursor_ = self.conn_.cursor() self.schema_dir = schema_dir self.database_dir_ = database_dir self.database_name_ = database_name log("Creating migrations table if not exists") self.ensure_migrations_table_exists() def ensure_migrations_table_exists(self): """Create migration table if not exists.""" self.cursor_.execute( """ CREATE TABLE IF NOT EXISTS migrations ( migration_id INTEGER PRIMARY KEY, version TEXT NOT NULL UNIQUE, description TEXT NOT NULL, dirty INTEGER NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP) """ ) def head(self): """Read the current version from the database""" log("Reading migration head version") self.cursor_.execute( "SELECT version, description, dirty, created_at FROM migrations" " ORDER BY migration_id DESC LIMIT 1" ) row = self.cursor_.fetchone() if not row: return { "version": None, "description": None, "dirty": None, "created_at": None, } return { "version": row[0], "description": row[1], "dirty": row[2], "created_at": row[3], } def print_head(self): head = self.head() if not head["version"]: print("No version.") return print( "Version: {}, ({} {})".format( head["version"], head["created_at"], head["description"] ) ) if head["dirty"]: print("Warning: the migration database is dirty") def get_migration_info(self, file_name): """Extract the migration info from migration file name.""" match = re.search( "(?P[^_]*)_?(?P.*)\\.sql", file_name, re.IGNORECASE, ) if not match: return {"version": None, "description": None} return { "version": match.group("version"), "description": match.group("description"), } def should_apply(self, migration_filename, target_version=None): """Check whether to apply the migration file.""" head = self.head() current_version = head["version"] or "" if target_version and target_version == current_version: # already on the target version, don't apply the migration return False # extract the proposed version from the migration filename proposed_version = self.get_migration_info(migration_filename)[ "version" ] if not proposed_version: # does not have a valid version in the name return False if proposed_version <= current_version: return False if target_version and proposed_version > target_version: return False return True def apply(self, import_once): """Migrate to the latest state.""" head = self.head() if head["dirty"]: print("Latest version is dirty, please tune manually.") return migration_filenames = list( filter(self.should_apply, os.listdir(self.schema_dir)) ) # Sort the migrations based on the version encoded in the filename migration_filenames.sort() for migration_filename in migration_filenames: print("Applying migration " + migration_filename) info = self.get_migration_info(migration_filename) self.cursor_.execute( """INSERT INTO migrations (version, description, dirty) VALUES (?, ?, 1)""", ( info["version"], info["description"], ), ) self.conn_.commit() with open( os.path.join(self.schema_dir, migration_filename), "r" ) as f: sql = f.read() self.cursor_.executescript(sql) self.cursor_.execute( "UPDATE migrations SET dirty = 0 WHERE migration_id = ?", (self.cursor_.lastrowid,), ) self.conn_.commit() if import_once and self.head()["version"] and self.check_no_data(): self.import_json() def check_no_data(self): """Check the database contains no data.""" self.cursor_.execute("SELECT COUNT(*) FROM nodes") node_count = self.cursor_.fetchone()[0] return node_count == 0 def import_json(self): node_group_names = self.find_json_importable_node_and_group_names() node_names = self.find_json_importable_node_names() # 1. # self.import_plugins_json() # 2. self.import_nodes_json(node_names) # 3. self.import_settings_json(node_names) # 4. self.import_groups_json(node_group_names) # 5. self.import_tags_json(node_group_names) # 6. self.import_subscritions_json(node_names) def import_plugins_json(self): """Import data from persistence JSON files.""" plugin_file = os.path.join(self.database_dir_, "plugins.json") if not os.path.exists(plugin_file): return data = json.load(open(plugin_file, "r")) for library in data["plugins"]: log("Import library {}".format(library)) self.cursor_.execute( "INSERT INTO plugins (library) VALUES (?)", (library,) ) self.conn_.commit() def import_nodes_json(self, node_names): """Import nodes data from persistence JSON files.""" adapter_file = os.path.join(self.database_dir_, "adapters.json") if os.path.exists(adapter_file): data = json.load(open(adapter_file, "r")) for node in data["nodes"]: log("Import node {}".format(node)) self.cursor_.execute( """INSERT INTO nodes (name, type, state, plugin_name) VALUES (?, ?, ?, ?)""", ( node["name"], 1 if node["type"] == 1 else 2, node["state"], node["plugin_name"], ), ) self.conn_.commit() for node_name in node_names: adapter_file = os.path.join( self.database_dir_, "adapters", node_name, "adapter.json" ) if not os.path.exists(adapter_file): continue data = json.load(open(adapter_file, "r")) log("Import node {}".format(data)) self.cursor_.execute( """INSERT INTO nodes (name, type, state, plugin_name) VALUES (?, ?, ?, ?)""", ( data["name"], data["type"], data["state"], data["plugin_name"], ), ) self.conn_.commit() def import_settings_json(self, node_names): """Import node settings from persistence JSON files.""" for node_name in node_names: settings_file = os.path.join( self.database_dir_, "adapters", node_name, "settings.json" ) if not os.path.exists(settings_file): continue data = open(settings_file, "r").read() log("Import setting {}".format(data)) self.cursor_.execute( "INSERT INTO settings (node_name, setting) VALUES(?, ?)", (node_name, data), ) self.conn_.commit() def import_groups_json(self, node_group_names): """Import node groups from persistence JSON files.""" for node_name, group_name in node_group_names: group_file = os.path.join( self.database_dir_, "adapters", node_name, "groups", group_name, "config.json", ) if not os.path.exists(group_file): continue data = json.load(open(group_file, "r")) log("Import group {}".format(data)) self.cursor_.execute( """INSERT INTO groups (driver_name, name, interval) VALUES (?, ?, ?)""", (node_name, group_name, data["read_interval"]), ) self.conn_.commit() def import_tags_json(self, node_group_names): """Import node tags from persistence JSON files.""" for node_name, group_name in node_group_names: tags_file = os.path.join( self.database_dir_, "adapters", node_name, "groups", group_name, "datatags.json", ) if not os.path.exists(tags_file): continue data = json.load(open(tags_file, "r")) for tag in data["tags"]: log("Import tag {}".format(tag)) self.cursor_.execute( """INSERT INTO tags (driver_name, group_name, name, address, attribute, precision, decimal, type, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( node_name, group_name, tag["name"], tag["address"], tag["attribute"], 0, 0, tag["type"], tag.get("description", ""), ), ) self.conn_.commit() def import_subscritions_json(self, node_names): """Import subscriptions from persistence JSON files.""" for node_name in node_names: subscription_file = os.path.join( self.database_dir_, "adapters", node_name, "subscriptions.json" ) if not os.path.exists(subscription_file): continue data = json.load(open(subscription_file, "r")) for subscription in data["subscriptions"]: log("Import subscription {}".format(subscription)) self.cursor_.execute( """INSERT INTO subscriptions ( app_name, driver_name, group_name ) VALUES(?, ?, ?)""", ( subscription["sub_adapter_name"], subscription["src_adapter_name"], subscription["group_config_name"], ), ) self.conn_.commit() def find_json_importable_node_names(self): """Find node names from persistence directory.""" adapters_dir = os.path.join(self.database_dir_, "adapters") return ( list(os.listdir(adapters_dir)) if os.path.exists(adapters_dir) else [] ) def find_json_importable_node_and_group_names(self): """Find node names and group names from persistence directory.""" res = [] adapters_dir = os.path.join(self.database_dir_, "adapters") if not os.path.exists(adapters_dir): return res for adapter_name in os.listdir(adapters_dir): groups_dir = os.path.join(adapters_dir, adapter_name, "groups") if not os.path.exists(groups_dir): continue for group_name in os.listdir(groups_dir): res.append((adapter_name, group_name)) return res if __name__ == "__main__": args = parse_args() VERBOSE = args.verbose migrations = Migration(schema_dir=args.schema_dir, database_dir=args.data_dir) if args.subcommand == "import": migrations.import_json() elif args.subcommand == "show": migrations.print_head() elif args.subcommand == "up": migrations.apply(args.import_once)