hyperion.ng/libsrc/db/DBMigrationManager.cpp
LordGrey ecceb4e7ae
Refactor Settings DB and Handling (#1786)
* Refactor config API

* Corrections

* Test Qt 6.8

* Revert "Test Qt 6.8"

This reverts commit eceebec49ecf1a3eda281a0630a9a7577b44ef0a.

* Corrections 2

* Update Changelog

* Add configFilter element for getconfig call

* Do not create errors for DB updates when in read-only mode

* Have configuration migration and validation before Hyperion starts

* Correct Tests

* Corrections

* Add migration items

* Correct windows build

* Ensure that first instance as default one exists

* Remove dependency between AuthManager and SSDPHandler

* Correct typos

* Address CodeQL findings

* Replace CamkeSettings by Presets and provide debug scenarios
2024-09-30 22:03:13 +02:00

768 lines
22 KiB
C++

#include <db/DBMigrationManager.h>
#include "db/SettingsTable.h"
#include <utils/Logger.h>
#include <utils/QStringUtils.h>
#include <HyperionConfig.h>
#include <QJsonObject>
DBMigrationManager::DBMigrationManager(QObject *parent)
: DBManager{parent}
{
}
bool DBMigrationManager::isMigrationRequired()
{
bool isNewRelease = false;
SettingsTable settingsTableGlobal;
if (settingsTableGlobal.resolveConfigVersion())
{
semver::version BUILD_VERSION(HYPERION_VERSION);
if (!BUILD_VERSION.isValid())
{
Error(_log, "Current Hyperion version [%s] is invalid. Exiting...", BUILD_VERSION.getVersion().c_str());
exit(1);
}
const semver::version& currentVersion = settingsTableGlobal.getConfigVersion();
if (currentVersion > BUILD_VERSION)
{
Error(_log, "Database version [%s] is greater than current Hyperion version [%s]. Exiting...", currentVersion.getVersion().c_str(), BUILD_VERSION.getVersion().c_str());
exit(1);
}
if (currentVersion < BUILD_VERSION)
{
isNewRelease = true;
}
}
return isNewRelease;
}
bool DBMigrationManager::migrateSettings(QJsonObject& config)
{
bool migrated = false;
semver::version BUILD_VERSION(HYPERION_VERSION);
SettingsTable settingsTableGlobal;
QJsonObject generalConfig = config.value("global").toObject().value("settings").toObject().value("general").toObject();
if (settingsTableGlobal.resolveConfigVersion(generalConfig))
{
semver::version currentVersion = settingsTableGlobal.getConfigVersion();
if (currentVersion < BUILD_VERSION)
{
Info(_log, "Migration from current version [%s] to new version [%s] started", currentVersion.getVersion().c_str(), BUILD_VERSION.getVersion().c_str());
// Extract, modify, and reinsert the global settings
QJsonObject globalSettings = config.value("global").toObject().value("settings").toObject();
upgradeGlobalSettings(currentVersion, globalSettings);
QJsonObject globalConfig = config.value("global").toObject();
globalConfig.insert("settings", globalSettings);
config.insert("global", globalConfig);
// Update each instance directly within the config
QJsonArray instancesConfig = config.value("instances").toArray();
for (int i = 0; i < instancesConfig.size(); ++i)
{
QJsonObject instanceConfig = instancesConfig[i].toObject();
QJsonObject instanceSettings = instanceConfig.value("settings").toObject();
upgradeInstanceSettings(currentVersion, static_cast<quint8>(i), instanceSettings);
// Reinsert the modified instance settings back into the instanceConfig
instanceConfig.insert("settings", instanceSettings);
instancesConfig.replace(i, instanceConfig);
}
config.insert("instances", instancesConfig);
Info(_log, "Migration from current version [%s] to new version [%s] finished", currentVersion.getVersion().c_str(), BUILD_VERSION.getVersion().c_str());
migrated = true;
}
}
return migrated;
}
bool DBMigrationManager::upgradeGlobalSettings(const semver::version& currentVersion, QJsonObject& config)
{
bool migrated = false;
semver::version migratedVersion = currentVersion;
//Migration step for versions < alpha 9
upgradeGlobalSettings_alpha_9(migratedVersion, config);
//Migration step for versions < 2.0.12
upgradeGlobalSettings_2_0_12(migratedVersion, config);
//Migration step for versions < 2.0.16
upgradeGlobalSettings_2_0_16(migratedVersion, config);
//Migration step for versions < 2.0.17
upgradeGlobalSettings_2_1_0(migratedVersion, config);
// Set the daqtabase version to the current build version
QJsonObject generalConfig = config["general"].toObject();
// Update the configVersion if necessary
if (generalConfig["configVersion"].toString() != HYPERION_VERSION) {
generalConfig["configVersion"] = HYPERION_VERSION;
migrated = true;
}
// Re-insert the modified "general" object back into the config
config["general"] = generalConfig;
return migrated;
}
bool DBMigrationManager::upgradeInstanceSettings(const semver::version& currentVersion, quint8 instance, QJsonObject& config)
{
bool migrated = false;
semver::version migratedVersion = currentVersion;
//Migration step for versions < alpha 9
upgradeInstanceSettings_alpha_9(migratedVersion, instance, config);
//Migration step for versions < 2.0.12
upgradeInstanceSettings_2_0_12(migratedVersion, instance, config);
//Migration step for versions < 2.0.13
upgradeInstanceSettings_2_0_13(migratedVersion, instance, config);
//Migration step for versions < 2.0.16
upgradeInstanceSettings_2_0_16(migratedVersion, instance, config);
return migrated;
}
bool DBMigrationManager::upgradeGlobalSettings_alpha_9(semver::version& currentVersion, QJsonObject& config)
{
bool migrated = false;
const semver::version targetVersion{ "2.0.0-alpha.9" };
if (currentVersion < targetVersion)
{
Info(_log, "Global settings: Migrate from version [%s] to version [%s] or later", currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str());
currentVersion = targetVersion;
if (config.contains("grabberV4L2"))
{
QJsonObject newGrabberV4L2Config = config["grabberV4L2"].toObject();
if (newGrabberV4L2Config.contains("encoding_format"))
{
newGrabberV4L2Config.remove("encoding_format");
newGrabberV4L2Config["grabberV4L2"] = newGrabberV4L2Config;
migrated = true;
}
//Add new element enable
if (!newGrabberV4L2Config.contains("enable"))
{
newGrabberV4L2Config["enable"] = false;
migrated = true;
}
config["grabberV4L2"] = newGrabberV4L2Config;
Debug(_log, "GrabberV4L2 records migrated");
}
if (config.contains("grabberAudio"))
{
QJsonObject newGrabberAudioConfig = config["grabberAudio"].toObject();
//Add new element enable
if (!newGrabberAudioConfig.contains("enable"))
{
newGrabberAudioConfig["enable"] = false;
migrated = true;
}
config["grabberAudio"] = newGrabberAudioConfig;
Debug(_log, "GrabberAudio records migrated");
}
if (config.contains("framegrabber"))
{
QJsonObject newFramegrabberConfig = config["framegrabber"].toObject();
//Align element namings with grabberV4L2
//Rename element type -> device
if (newFramegrabberConfig.contains("type"))
{
newFramegrabberConfig["device"] = newFramegrabberConfig["type"].toString();
newFramegrabberConfig.remove("type");
migrated = true;
}
//Rename element frequency_Hz -> fps
if (newFramegrabberConfig.contains("frequency_Hz"))
{
newFramegrabberConfig["fps"] = newFramegrabberConfig["frequency_Hz"].toInt(25);
newFramegrabberConfig.remove("frequency_Hz");
migrated = true;
}
//Rename element display -> input
if (newFramegrabberConfig.contains("display"))
{
newFramegrabberConfig["input"] = newFramegrabberConfig["display"];
newFramegrabberConfig.remove("display");
migrated = true;
}
//Add new element enable
if (!newFramegrabberConfig.contains("enable"))
{
newFramegrabberConfig["enable"] = false;
migrated = true;
}
config["framegrabber"] = newFramegrabberConfig;
Debug(_log, "Framegrabber records migrated");
}
}
return migrated;
}
bool DBMigrationManager::upgradeGlobalSettings_2_0_12(semver::version& currentVersion, QJsonObject& config)
{
bool migrated = false;
const semver::version targetVersion{ "2.0.12" };
if (currentVersion < targetVersion)
{
Info(_log, "Global settings: Migrate from version [%s] to version [%s] or later", currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str());
currentVersion = targetVersion;
// Have Hostname/IP-address separate from port for Forwarder
if (config.contains("forwarder"))
{
QJsonObject newForwarderConfig = config["forwarder"].toObject();
QJsonArray json;
if (newForwarderConfig.contains("json"))
{
const QJsonArray oldJson = newForwarderConfig["json"].toArray();
QJsonObject newJsonConfig;
for (const QJsonValue& value : oldJson)
{
if (value.isString())
{
QString oldHost = value.toString();
// Resolve hostname and port
QStringList addressparts = QStringUtils::split(oldHost, ":", QStringUtils::SplitBehavior::SkipEmptyParts);
QString host = addressparts[0];
if (host != "127.0.0.1")
{
newJsonConfig["host"] = host;
if (addressparts.size() > 1)
{
newJsonConfig["port"] = addressparts[1].toInt();
}
else
{
newJsonConfig["port"] = 19444;
}
newJsonConfig["name"] = host;
json.append(newJsonConfig);
migrated = true;
}
}
}
if (!json.isEmpty())
{
newForwarderConfig["jsonapi"] = json;
}
newForwarderConfig.remove("json");
migrated = true;
}
QJsonArray flatbuffer;
if (newForwarderConfig.contains("flat"))
{
const QJsonArray oldFlatbuffer = newForwarderConfig["flat"].toArray();
QJsonObject newFlattbufferConfig;
for (const QJsonValue& value : oldFlatbuffer)
{
if (value.isString())
{
QString oldHost = value.toString();
// Resolve hostname and port
QStringList addressparts = QStringUtils::split(oldHost, ":", QStringUtils::SplitBehavior::SkipEmptyParts);
QString host = addressparts[0];
if (host != "127.0.0.1")
{
newFlattbufferConfig["host"] = host;
if (addressparts.size() > 1)
{
newFlattbufferConfig["port"] = addressparts[1].toInt();
}
else
{
newFlattbufferConfig["port"] = 19400;
}
newFlattbufferConfig["name"] = host;
flatbuffer.append(newFlattbufferConfig);
}
}
if (!flatbuffer.isEmpty())
{
newForwarderConfig["flatbuffer"] = flatbuffer;
}
newForwarderConfig.remove("flat");
migrated = true;
}
}
if (json.isEmpty() && flatbuffer.isEmpty())
{
newForwarderConfig["enable"] = false;
}
if (migrated)
{
config["forwarder"] = newForwarderConfig;
Debug(_log, "Forwarder records migrated");
currentVersion = targetVersion;
}
}
}
return migrated;
}
bool DBMigrationManager::upgradeGlobalSettings_2_0_16(semver::version& currentVersion, QJsonObject& config)
{
bool migrated = false;
const semver::version targetVersion{ "2.0.16" };
if (currentVersion < targetVersion)
{
Info(_log, "Global settings: Migrate from version [%s] to version [%s] or later", currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str());
currentVersion = targetVersion;
if (config.contains("cecEvents"))
{
bool isCECEnabled {false};
if (config.contains("grabberV4L2"))
{
QJsonObject newGrabberV4L2Config = config["grabberV4L2"].toObject();
if (newGrabberV4L2Config.contains("cecDetection"))
{
isCECEnabled = newGrabberV4L2Config.value("cecDetection").toBool(false);
newGrabberV4L2Config.remove("cecDetection");
config["grabberV4L2"] = newGrabberV4L2Config;
QJsonObject newGCecEventsConfig = config["cecEvents"].toObject();
newGCecEventsConfig["enable"] = isCECEnabled;
if (!newGCecEventsConfig.contains("actions"))
{
QJsonObject action1
{
{"action", "Suspend"},
{"event", "standby"}
};
QJsonObject action2
{
{"action", "Resume"},
{"event", "set stream path"}
};
QJsonArray actions { action1, action2 };
newGCecEventsConfig.insert("actions",actions);
}
config["cecEvents"] = newGCecEventsConfig;
migrated = true;
Debug(_log, "CEC configuration records migrated");
}
}
}
}
return migrated;
}
bool DBMigrationManager::upgradeGlobalSettings_2_1_0(semver::version& currentVersion, QJsonObject& config)
{
bool migrated = false;
const semver::version targetVersion{ "2.0.17-beta.2" };
if (currentVersion < targetVersion)
{
Info(_log, "Global settings: Migrate from version [%s] to version [%s] or later", currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str());
currentVersion = targetVersion;
if (config.contains("general"))
{
QJsonObject newGeneralConfig = config["general"].toObject();
newGeneralConfig.remove("previousVersion");
config.insert("general", newGeneralConfig);
Debug(_log, "General settings migrated");
migrated = true;
}
if (config.contains("network"))
{
QJsonObject newNetworkConfig = config["network"].toObject();
newNetworkConfig.remove("apiAuth");
newNetworkConfig.remove("localAdminAuth");
config.insert("network", newNetworkConfig);
Debug(_log, "Network settings migrated");
migrated = true;
}
}
//Remove wrong instance 255 configuration records, created by the global instance #255
SettingsTable globalSettingsTable(255);
globalSettingsTable.deleteInstance();
migrated = true;
return migrated;
}
bool DBMigrationManager::upgradeInstanceSettings_alpha_9(semver::version& currentVersion, quint8 instance, QJsonObject& config)
{
bool migrated = false;
const semver::version targetVersion{ "2.0.0-alpha.9" };
if (currentVersion < targetVersion)
{
Info(_log, "Settings instance [%u]: Migrate from version [%s] to version [%s] or later", instance, currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str());
currentVersion = targetVersion;
// LED LAYOUT UPGRADE
// from { hscan: { minimum: 0.2, maximum: 0.3 }, vscan: { minimum: 0.2, maximum: 0.3 } }
// from { h: { min: 0.2, max: 0.3 }, v: { min: 0.2, max: 0.3 } }
// to { hmin: 0.2, hmax: 0.3, vmin: 0.2, vmax: 0.3}
if (config.contains("leds"))
{
const QJsonArray ledarr = config["leds"].toArray();
const QJsonObject firstLed = ledarr[0].toObject();
if (firstLed.contains("hscan") || firstLed.contains("h"))
{
const bool whscan = firstLed.contains("hscan");
QJsonArray newLedarr;
for (const auto& entry : ledarr)
{
const QJsonObject led = entry.toObject();
QJsonObject hscan;
QJsonObject vscan;
QJsonValue hmin;
QJsonValue hmax;
QJsonValue vmin;
QJsonValue vmax;
QJsonObject nL;
if (whscan)
{
hscan = led["hscan"].toObject();
vscan = led["vscan"].toObject();
hmin = hscan["minimum"];
hmax = hscan["maximum"];
vmin = vscan["minimum"];
vmax = vscan["maximum"];
}
else
{
hscan = led["h"].toObject();
vscan = led["v"].toObject();
hmin = hscan["min"];
hmax = hscan["max"];
vmin = vscan["min"];
vmax = vscan["max"];
}
// append to led object
nL["hmin"] = hmin;
nL["hmax"] = hmax;
nL["vmin"] = vmin;
nL["vmax"] = vmax;
newLedarr.append(nL);
}
// replace
config["leds"] = newLedarr;
migrated = true;
Info(_log, "Instance [%u]: LED Layout migrated", instance);
}
}
if (config.contains("ledConfig"))
{
QJsonObject oldLedConfig = config["ledConfig"].toObject();
if (!oldLedConfig.contains("classic"))
{
QJsonObject newLedConfig;
newLedConfig.insert("classic", oldLedConfig);
QJsonObject defaultMatrixConfig{ {"ledshoriz", 1}
,{"ledsvert", 1}
,{"cabling","snake"}
,{"start","top-left"}
};
newLedConfig.insert("matrix", defaultMatrixConfig);
config["ledConfig"] = newLedConfig;
migrated = true;
Info(_log, "Instance [%u]: LED-Config migrated", instance);
}
}
// LED Hardware count is leading for versions after alpha 9
// Setting Hardware LED count to number of LEDs configured via layout, if layout number is greater than number of hardware LEDs
if (config.contains("device"))
{
QJsonObject newDeviceConfig = config["device"].toObject();
if (newDeviceConfig.contains("hardwareLedCount"))
{
int hwLedcount = newDeviceConfig["hardwareLedCount"].toInt();
if (config.contains("leds"))
{
const QJsonArray ledarr = config["leds"].toArray();
int layoutLedCount = ledarr.size();
if (hwLedcount < layoutLedCount)
{
Warning(_log, "Instance [%u]: HwLedCount/Layout mismatch! Setting Hardware LED count to number of LEDs configured via layout", instance);
hwLedcount = layoutLedCount;
newDeviceConfig["hardwareLedCount"] = hwLedcount;
migrated = true;
}
}
}
if (newDeviceConfig.contains("type"))
{
QString type = newDeviceConfig["type"].toString();
if (type == "atmoorb" || type == "fadecandy" || type == "philipshue")
{
if (newDeviceConfig.contains("output"))
{
newDeviceConfig["host"] = newDeviceConfig["output"].toString();
newDeviceConfig.remove("output");
migrated = true;
}
}
}
if (migrated)
{
config["device"] = newDeviceConfig;
Debug(_log, "LED-Device records migrated");
}
}
}
return migrated;
}
bool DBMigrationManager::upgradeInstanceSettings_2_0_12(semver::version& currentVersion, quint8 instance, QJsonObject& config)
{
bool migrated = false;
const semver::version targetVersion{ "2.0.12" };
if (currentVersion < targetVersion)
{
Info(_log, "Settings instance [%u]: Migrate from version [%s] to version [%s] or later", instance, currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str());
currentVersion = targetVersion;
// Have Hostname/IP-address separate from port for LED-Devices
if (config.contains("device"))
{
QJsonObject newDeviceConfig = config["device"].toObject();
if (newDeviceConfig.contains("host"))
{
QString oldHost = newDeviceConfig["host"].toString();
// Resolve hostname and port
QStringList addressparts = QStringUtils::split(oldHost, ":", QStringUtils::SplitBehavior::SkipEmptyParts);
newDeviceConfig["host"] = addressparts[0];
if (addressparts.size() > 1)
{
if (!newDeviceConfig.contains("port"))
{
newDeviceConfig["port"] = addressparts[1].toInt();
}
migrated = true;
}
}
if (newDeviceConfig.contains("type"))
{
QString type = newDeviceConfig["type"].toString();
if (type == "apa102")
{
if (newDeviceConfig.contains("colorOrder"))
{
QString colorOrder = newDeviceConfig["colorOrder"].toString();
if (colorOrder == "bgr")
{
newDeviceConfig["colorOrder"] = "rgb";
migrated = true;
}
}
}
}
if (migrated)
{
config["device"] = newDeviceConfig;
Debug(_log, "LED-Device records migrated");
}
}
}
return migrated;
}
bool DBMigrationManager::upgradeInstanceSettings_2_0_13(semver::version& currentVersion, quint8 instance, QJsonObject& config)
{
bool migrated = false;
const semver::version targetVersion{ "2.0.13" };
if (currentVersion < targetVersion)
{
Info(_log, "Settings instance [%u]: Migrate from version [%s] to version [%s] or later", instance, currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str());
currentVersion = targetVersion;
// Have Hostname/IP-address separate from port for LED-Devices
if (config.contains("device"))
{
QJsonObject newDeviceConfig = config["device"].toObject();
if (newDeviceConfig.contains("type"))
{
QString type = newDeviceConfig["type"].toString();
const QStringList serialDevices{ "adalight", "dmx", "atmo", "sedu", "tpm2", "karate" };
if (serialDevices.contains(type))
{
if (!newDeviceConfig.contains("rateList"))
{
newDeviceConfig["rateList"] = "CUSTOM";
migrated = true;
}
}
if (type == "adalight")
{
if (newDeviceConfig.contains("lightberry_apa102_mode"))
{
bool lightberry_apa102_mode = newDeviceConfig["lightberry_apa102_mode"].toBool();
if (lightberry_apa102_mode)
{
newDeviceConfig["streamProtocol"] = "1";
}
else
{
newDeviceConfig["streamProtocol"] = "0";
}
newDeviceConfig.remove("lightberry_apa102_mode");
migrated = true;
}
}
}
if (migrated)
{
config["device"] = newDeviceConfig;
Debug(_log, "LED-Device records migrated");
}
}
}
return migrated;
}
bool DBMigrationManager::upgradeInstanceSettings_2_0_16(semver::version& currentVersion, quint8 instance, QJsonObject& config)
{
bool migrated = false;
const semver::version targetVersion{ "2.0.16" };
if (currentVersion >= targetVersion) return migrated;
Info(_log, "Settings instance [%u]: Migrate from version [%s] to version [%s] or later", instance, currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str());
currentVersion = targetVersion;
if (config.contains("device"))
{
QJsonObject newDeviceConfig = config["device"].toObject();
auto convertIntToString = [&](const QString& key) {
if (newDeviceConfig.contains(key) && newDeviceConfig[key].isDouble()) {
int value = newDeviceConfig[key].toInt();
newDeviceConfig[key] = QString::number(value);
migrated = true;
}
};
if (newDeviceConfig.contains("type"))
{
QString type = newDeviceConfig["type"].toString();
if (type == "philipshue")
{
convertIntToString("groupId");
if (newDeviceConfig.contains("lightIds"))
{
QJsonArray lightIds = newDeviceConfig["lightIds"].toArray();
for (int i = 0; i < lightIds.size(); ++i)
{
if (lightIds[i].isDouble())
{
lightIds[i] = QString::number(lightIds[i].toInt());
migrated = true;
}
}
newDeviceConfig["lightIds"] = lightIds;
}
}
else if (type == "nanoleaf")
{
const auto updatePanelOrder = [&](const QString& key, const QString& zeroStr, const QString& oneStr) {
if (newDeviceConfig.contains(key))
{
int order = newDeviceConfig[key].isDouble() ? newDeviceConfig[key].toInt() : newDeviceConfig[key].toString().toInt();
newDeviceConfig[key] = (order == 0) ? zeroStr : oneStr;
migrated = true;
}
};
newDeviceConfig.remove("panelStartPos");
migrated = true;
updatePanelOrder("panelOrderTopDown", "top2down", "bottom2up");
updatePanelOrder("panelOrderLeftRight", "left2right", "right2left");
}
}
if (migrated)
{
config["device"] = newDeviceConfig;
Debug(_log, "LED-Device records migrated");
}
}
return migrated;
}