mirror of
https://github.com/emqx/neuron.git
synced 2026-10-16 20:30:39 +08:00
467 lines
15 KiB
Python
Executable File
467 lines
15 KiB
Python
Executable File
#!/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<version>[^_]*)_?(?P<description>.*)\\.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)
|