558 lines
25 KiB
C#
558 lines
25 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Security.AccessControl;
|
|
using System.Security.Principal;
|
|
using System.Text;
|
|
using System.Xml;
|
|
|
|
namespace BizTalkCheckmkPulse.Setup
|
|
{
|
|
internal sealed class InstallerEngine
|
|
{
|
|
internal const string TaskName = "BizTalk Checkmk Pulse Provider";
|
|
private static readonly TimeSpan PostInstallValidationTimeout = TimeSpan.FromMinutes(4);
|
|
private readonly string packageDirectory;
|
|
private readonly string installDirectory;
|
|
private readonly string runtimeDirectory;
|
|
private readonly string checkmkLocalDirectory;
|
|
|
|
public InstallerEngine(string packageDirectory)
|
|
{
|
|
this.packageDirectory = Path.GetFullPath(packageDirectory);
|
|
installDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "BizTalkCheckmkPulse");
|
|
runtimeDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "BizTalkCheckmkPulse");
|
|
checkmkLocalDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "checkmk", "agent", "local");
|
|
}
|
|
|
|
public void Install(
|
|
string account,
|
|
string password,
|
|
bool isGmsa,
|
|
string environmentName,
|
|
bool allowServiceNameChange,
|
|
Action<string> report)
|
|
{
|
|
Validate(account, password, isGmsa, environmentName);
|
|
report = report ?? delegate { };
|
|
var accountSid = (SecurityIdentifier)new NTAccount(account).Translate(typeof(SecurityIdentifier));
|
|
report("Konto aufgeloest: " + account + " (" + accountSid.Value + ")");
|
|
|
|
var sourceApplication = Path.Combine(packageDirectory, "application");
|
|
var sourceExe = Path.Combine(sourceApplication, "BizTalkCheckmkPulse.exe");
|
|
var sourceConfig = sourceExe + ".config";
|
|
var sourceWrapper = Path.Combine(packageDirectory, "biztalk_checkmk_pulse.cmd");
|
|
RequireFile(sourceExe);
|
|
RequireFile(sourceConfig);
|
|
RequireFile(sourceWrapper);
|
|
var targetExe = Path.Combine(installDirectory, "BizTalkCheckmkPulse.exe");
|
|
var targetConfig = targetExe + ".config";
|
|
var stagingDirectory = installDirectory + ".staging." + Guid.NewGuid().ToString("N");
|
|
var backupDirectory = installDirectory + ".backup." + Guid.NewGuid().ToString("N");
|
|
var targetWrapper = Path.Combine(checkmkLocalDirectory, "biztalk_checkmk_pulse.cmd");
|
|
var previousWrapper = File.Exists(targetWrapper) ? File.ReadAllBytes(targetWrapper) : null;
|
|
var hadExistingInstallation = Directory.Exists(installDirectory);
|
|
var hadExistingExecutable = File.Exists(targetExe);
|
|
var mutationStarted = false;
|
|
var backupCreated = false;
|
|
var filesActivated = false;
|
|
var scheduler = new TaskSchedulerService();
|
|
|
|
try
|
|
{
|
|
RunSelfTest(sourceExe);
|
|
report("Paket-Vorpruefung erfolgreich: neun Checkmk-Services.");
|
|
if (!isGmsa)
|
|
{
|
|
ValidateBatchLogon(account, password);
|
|
report("Collector-Anmeldung und 'Log on as a batch job' vor dem Update bestaetigt.");
|
|
}
|
|
|
|
Directory.CreateDirectory(stagingDirectory);
|
|
var stagedExe = Path.Combine(stagingDirectory, "BizTalkCheckmkPulse.exe");
|
|
var stagedConfig = stagedExe + ".config";
|
|
File.Copy(sourceExe, stagedExe, false);
|
|
var effectiveEnvironment = PrepareConfig(
|
|
sourceConfig,
|
|
stagedConfig,
|
|
File.Exists(targetConfig) ? targetConfig : null,
|
|
environmentName);
|
|
var stagedServiceNames = RunSelfTest(stagedExe);
|
|
report("Update-Staging validiert. Umgebung=" + (effectiveEnvironment.Length == 0 ? "(keine)" : effectiveEnvironment) + ".");
|
|
|
|
if (File.Exists(targetExe))
|
|
{
|
|
var installedServiceNames = RunSelfTest(targetExe);
|
|
var serviceNameChange = EnsureServiceNameCompatibility(
|
|
installedServiceNames,
|
|
stagedServiceNames,
|
|
allowServiceNameChange);
|
|
report(serviceNameChange.Length == 0
|
|
? "Checkmk-Servicevertrag unveraendert: keine Service Discovery erforderlich."
|
|
: "Checkmk-Service-Rename ausdruecklich bestaetigt; Service Discovery erforderlich. " + serviceNameChange);
|
|
}
|
|
|
|
// Erst nach vollstaendiger Staging-Pruefung wird der laufende Provider angehalten.
|
|
mutationStarted = true;
|
|
scheduler.DeleteIfExists(TaskName);
|
|
report("Vorhandener Scheduled Task angehalten und fuer das Update entfernt.");
|
|
|
|
if (hadExistingInstallation)
|
|
{
|
|
Directory.Move(installDirectory, backupDirectory);
|
|
backupCreated = true;
|
|
}
|
|
|
|
Directory.Move(stagingDirectory, installDirectory);
|
|
filesActivated = true;
|
|
report(hadExistingInstallation
|
|
? "Programmdateien atomar auf die neue Version umgestellt."
|
|
: "Programmdateien installiert: " + installDirectory);
|
|
|
|
var dataDirectory = Path.Combine(runtimeDirectory, "data");
|
|
var logDirectory = Path.Combine(runtimeDirectory, "logs");
|
|
Directory.CreateDirectory(runtimeDirectory);
|
|
Directory.CreateDirectory(dataDirectory);
|
|
Directory.CreateDirectory(logDirectory);
|
|
|
|
ApplyDirectoryAcl(installDirectory, accountSid, FileSystemRights.ReadAndExecute, FileSystemRights.ReadAndExecute);
|
|
ApplyDirectoryAcl(runtimeDirectory, accountSid, FileSystemRights.ReadAndExecute, FileSystemRights.ReadAndExecute);
|
|
ApplyDirectoryAcl(dataDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.ReadAndExecute);
|
|
ApplyDirectoryAcl(logDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.Modify);
|
|
report("Least-Privilege-Verzeichnisrechte gesetzt; vorhandene Runtime-Daten bleiben erhalten.");
|
|
|
|
Directory.CreateDirectory(checkmkLocalDirectory);
|
|
File.Copy(sourceWrapper, targetWrapper, true);
|
|
report("Checkmk Local Check installiert: " + checkmkLocalDirectory);
|
|
|
|
RunSelfTest(targetExe);
|
|
report("Installierter Self-Test erfolgreich: neun Checkmk-Services.");
|
|
|
|
var runtimeValidationStartedUtc = DateTime.UtcNow;
|
|
scheduler.RegisterValidationAndStart(
|
|
TaskName,
|
|
targetExe,
|
|
installDirectory,
|
|
account,
|
|
isGmsa ? null : password,
|
|
isGmsa);
|
|
report("Einmaliger Provider-Abnahmelauf mit erzwungenem Endpoint-Katalogabgleich gestartet.");
|
|
|
|
var completedRun = scheduler.WaitForSuccessfulRun(
|
|
TaskName,
|
|
runtimeValidationStartedUtc,
|
|
PostInstallValidationTimeout,
|
|
report);
|
|
report("Provider-Abnahmelauf erfolgreich: LastTaskResult=" + completedRun.ExitCode + ".");
|
|
|
|
var runtimeValidation = RunRuntimeValidation(
|
|
targetExe,
|
|
runtimeValidationStartedUtc,
|
|
account);
|
|
report("Post-Install-Runtime-Abnahme erfolgreich: " + runtimeValidation);
|
|
|
|
scheduler.RegisterRecurring(
|
|
TaskName,
|
|
targetExe,
|
|
installDirectory,
|
|
account,
|
|
isGmsa ? null : password,
|
|
isGmsa);
|
|
report("Validierter minuetlicher Scheduled Task registriert: " + TaskName);
|
|
|
|
TryDeleteDirectory(backupDirectory, report);
|
|
}
|
|
catch (Exception installException)
|
|
{
|
|
var rollbackFailures = new List<string>();
|
|
if (mutationStarted)
|
|
{
|
|
try
|
|
{
|
|
scheduler.DeleteIfExists(TaskName);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
rollbackFailures.Add("Task stoppen: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
if (filesActivated || backupCreated)
|
|
{
|
|
try
|
|
{
|
|
if (Directory.Exists(installDirectory)) Directory.Delete(installDirectory, true);
|
|
if (backupCreated && Directory.Exists(backupDirectory)) Directory.Move(backupDirectory, installDirectory);
|
|
report("Vorherige Programmversion wiederhergestellt.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
rollbackFailures.Add("Programmdateien wiederherstellen: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
if (mutationStarted)
|
|
{
|
|
try
|
|
{
|
|
RestoreWrapper(targetWrapper, previousWrapper);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
rollbackFailures.Add("Checkmk-Wrapper wiederherstellen: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
if (mutationStarted && hadExistingExecutable && File.Exists(targetExe))
|
|
{
|
|
try
|
|
{
|
|
var rollbackRunStartedUtc = DateTime.UtcNow;
|
|
scheduler.RegisterRecurringAndStart(
|
|
TaskName,
|
|
targetExe,
|
|
installDirectory,
|
|
account,
|
|
isGmsa ? null : password,
|
|
isGmsa);
|
|
scheduler.WaitForSuccessfulRun(
|
|
TaskName,
|
|
rollbackRunStartedUtc,
|
|
PostInstallValidationTimeout,
|
|
report);
|
|
report("Scheduled Task und frischer Snapshot der vorherigen Version wiederhergestellt.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
rollbackFailures.Add("Scheduled Task wiederherstellen: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
var rollback = rollbackFailures.Count == 0
|
|
? "Rollback erfolgreich."
|
|
: "Rollback unvollstaendig: " + string.Join(" | ", rollbackFailures);
|
|
throw new InvalidOperationException(
|
|
"Installation/Update fehlgeschlagen. " + rollback + " Ursache: " + installException.Message,
|
|
installException);
|
|
}
|
|
finally
|
|
{
|
|
TryDeleteDirectory(stagingDirectory, null);
|
|
}
|
|
}
|
|
|
|
public void Uninstall(bool keepRuntimeData, Action<string> report)
|
|
{
|
|
new TaskSchedulerService().DeleteIfExists(TaskName);
|
|
report("Scheduled Task entfernt.");
|
|
|
|
var wrapper = Path.Combine(checkmkLocalDirectory, "biztalk_checkmk_pulse.cmd");
|
|
if (File.Exists(wrapper)) File.Delete(wrapper);
|
|
if (Directory.Exists(installDirectory)) Directory.Delete(installDirectory, true);
|
|
if (!keepRuntimeData && Directory.Exists(runtimeDirectory)) Directory.Delete(runtimeDirectory, true);
|
|
}
|
|
|
|
private static void Validate(string account, string password, bool isGmsa, string environmentName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(account)) throw new ArgumentException("Collector-Konto fehlt.");
|
|
if (account.IndexOf('\\') <= 0 || account.EndsWith("\\", StringComparison.Ordinal))
|
|
throw new ArgumentException(@"Collector-Konto im Format DOMAIN\Benutzer eingeben, z.B. BEW\t231bizmon.");
|
|
if (isGmsa && !account.EndsWith("$", StringComparison.Ordinal))
|
|
throw new ArgumentException(@"Ein gMSA-Konto muss mit '$' enden, z.B. BEW\svc_biztalk_cmk$.");
|
|
if (!isGmsa && string.IsNullOrEmpty(password)) throw new ArgumentException("Kennwort fehlt.");
|
|
if (!new[] { "", "ACC", "DEV", "TST", "PRD" }.Contains(environmentName))
|
|
throw new ArgumentException("Ungueltige Umgebung.");
|
|
}
|
|
|
|
private static void RequireFile(string path)
|
|
{
|
|
if (!File.Exists(path))
|
|
throw new FileNotFoundException("Installationspaket ist unvollstaendig. Datei fehlt: " + path, path);
|
|
}
|
|
|
|
internal string GetInstalledEnvironment()
|
|
{
|
|
var config = Path.Combine(installDirectory, "BizTalkCheckmkPulse.exe.config");
|
|
if (!File.Exists(config)) return string.Empty;
|
|
try
|
|
{
|
|
var document = LoadXml(config);
|
|
var setting = FindAppSetting(document, "EnvironmentName");
|
|
return setting == null ? string.Empty : setting.GetAttribute("value").Trim();
|
|
}
|
|
catch
|
|
{
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
internal bool IsInstalled
|
|
{
|
|
get { return File.Exists(Path.Combine(installDirectory, "BizTalkCheckmkPulse.exe")); }
|
|
}
|
|
|
|
internal static string PrepareConfig(
|
|
string sourceConfig,
|
|
string stagedConfig,
|
|
string existingConfig,
|
|
string requestedEnvironment)
|
|
{
|
|
var document = LoadXml(sourceConfig);
|
|
if (!string.IsNullOrWhiteSpace(existingConfig) && File.Exists(existingConfig))
|
|
{
|
|
var existing = LoadXml(existingConfig);
|
|
var existingSettings = existing.SelectNodes("/configuration/appSettings/add[@key]");
|
|
if (existingSettings != null)
|
|
{
|
|
foreach (XmlNode node in existingSettings)
|
|
{
|
|
var element = node as XmlElement;
|
|
if (element == null) continue;
|
|
var key = element.GetAttribute("key");
|
|
var value = element.GetAttribute("value");
|
|
if (IsSupersededDefault(key, value)) continue;
|
|
var target = FindAppSetting(document, key);
|
|
if (target != null) target.SetAttribute("value", value);
|
|
}
|
|
}
|
|
}
|
|
|
|
var environmentSetting = FindAppSetting(document, "EnvironmentName");
|
|
if (environmentSetting == null)
|
|
throw new InvalidDataException("EnvironmentName fehlt in " + sourceConfig + ".");
|
|
if (!string.IsNullOrWhiteSpace(requestedEnvironment))
|
|
environmentSetting.SetAttribute("value", requestedEnvironment.Trim());
|
|
|
|
document.Save(stagedConfig);
|
|
return environmentSetting.GetAttribute("value").Trim();
|
|
}
|
|
|
|
private static XmlDocument LoadXml(string path)
|
|
{
|
|
var document = new XmlDocument { PreserveWhitespace = true, XmlResolver = null };
|
|
using (var reader = XmlReader.Create(path, new XmlReaderSettings
|
|
{
|
|
DtdProcessing = DtdProcessing.Prohibit,
|
|
XmlResolver = null
|
|
}))
|
|
{
|
|
document.Load(reader);
|
|
}
|
|
return document;
|
|
}
|
|
|
|
private static XmlElement FindAppSetting(XmlDocument document, string key)
|
|
{
|
|
var nodes = document.SelectNodes("/configuration/appSettings/add[@key]");
|
|
if (nodes == null) return null;
|
|
foreach (XmlNode node in nodes)
|
|
{
|
|
var element = node as XmlElement;
|
|
if (element != null && string.Equals(element.GetAttribute("key"), key, StringComparison.Ordinal))
|
|
return element;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static bool IsSupersededDefault(string key, string value)
|
|
{
|
|
return string.Equals(key, "EndpointProbeMaxConcurrency", StringComparison.Ordinal)
|
|
&& string.Equals(value, "12", StringComparison.Ordinal)
|
|
|| string.Equals(key, "EndpointMaxCount", StringComparison.Ordinal)
|
|
&& string.Equals(value, "500", StringComparison.Ordinal);
|
|
}
|
|
|
|
private static void ValidateBatchLogon(string account, string password)
|
|
{
|
|
var separator = account.IndexOf('\\');
|
|
var domain = account.Substring(0, separator);
|
|
var user = account.Substring(separator + 1);
|
|
IntPtr token;
|
|
if (!LogonUser(user, domain, password, 4, 0, out token))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Collector-Anmeldung als Batch fehlgeschlagen: "
|
|
+ new Win32Exception(Marshal.GetLastWin32Error()).Message
|
|
+ ". Kennwort und lokales Recht 'Log on as a batch job' pruefen.");
|
|
}
|
|
|
|
CloseHandle(token);
|
|
}
|
|
|
|
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool LogonUser(
|
|
string userName,
|
|
string domain,
|
|
string password,
|
|
int logonType,
|
|
int logonProvider,
|
|
out IntPtr token);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool CloseHandle(IntPtr handle);
|
|
|
|
private static void RestoreWrapper(string path, byte[] previousContent)
|
|
{
|
|
if (previousContent == null)
|
|
{
|
|
if (File.Exists(path)) File.Delete(path);
|
|
return;
|
|
}
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
|
File.WriteAllBytes(path, previousContent);
|
|
}
|
|
|
|
private static void TryDeleteDirectory(string path, Action<string> report)
|
|
{
|
|
if (!Directory.Exists(path)) return;
|
|
try
|
|
{
|
|
Directory.Delete(path, true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (report != null) report("Hinweis: temporaeres Verzeichnis konnte nicht entfernt werden: " + path + " (" + ex.Message + ")");
|
|
}
|
|
}
|
|
|
|
private static void ApplyDirectoryAcl(
|
|
string path,
|
|
SecurityIdentifier collectorSid,
|
|
FileSystemRights collectorRights,
|
|
FileSystemRights systemRights)
|
|
{
|
|
var inheritance = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit;
|
|
var security = new DirectorySecurity();
|
|
security.SetAccessRuleProtection(true, false);
|
|
security.AddAccessRule(new FileSystemAccessRule(
|
|
new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null),
|
|
FileSystemRights.FullControl,
|
|
inheritance,
|
|
PropagationFlags.None,
|
|
AccessControlType.Allow));
|
|
security.AddAccessRule(new FileSystemAccessRule(
|
|
new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null),
|
|
systemRights,
|
|
inheritance,
|
|
PropagationFlags.None,
|
|
AccessControlType.Allow));
|
|
security.AddAccessRule(new FileSystemAccessRule(
|
|
collectorSid,
|
|
collectorRights,
|
|
inheritance,
|
|
PropagationFlags.None,
|
|
AccessControlType.Allow));
|
|
new DirectoryInfo(path).SetAccessControl(security);
|
|
}
|
|
|
|
private static IReadOnlyList<string> RunSelfTest(string executable)
|
|
{
|
|
var start = new ProcessStartInfo(executable, "--self-test")
|
|
{
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true
|
|
};
|
|
using (var process = Process.Start(start))
|
|
{
|
|
if (process == null) throw new InvalidOperationException("Self-Test konnte nicht gestartet werden.");
|
|
var output = process.StandardOutput.ReadToEnd();
|
|
var error = process.StandardError.ReadToEnd();
|
|
if (!process.WaitForExit(30000))
|
|
{
|
|
process.Kill();
|
|
throw new InvalidOperationException("Self-Test hat das Zeitlimit ueberschritten.");
|
|
}
|
|
var lines = output.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
|
|
if (process.ExitCode != 0 || lines.Length != 9 || lines.Any(x => !x.StartsWith("0 ", StringComparison.Ordinal)))
|
|
throw new InvalidOperationException("Self-Test fehlgeschlagen. Exitcode=" + process.ExitCode + ", Zeilen=" + lines.Length + ". " + error);
|
|
|
|
var serviceNames = lines.Select(ExtractServiceName).ToArray();
|
|
if (serviceNames.Distinct(StringComparer.Ordinal).Count() != serviceNames.Length)
|
|
throw new InvalidOperationException("Self-Test fehlgeschlagen: Checkmk-Servicenamen sind nicht eindeutig.");
|
|
return serviceNames;
|
|
}
|
|
}
|
|
|
|
private static string RunRuntimeValidation(
|
|
string executable,
|
|
DateTime notBeforeUtc,
|
|
string expectedIdentity)
|
|
{
|
|
var identityBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(expectedIdentity));
|
|
var arguments = "--validate-runtime --validation-not-before-utc "
|
|
+ notBeforeUtc.ToUniversalTime().ToString("o")
|
|
+ " --expected-identity-base64 " + identityBase64;
|
|
var start = new ProcessStartInfo(executable, arguments)
|
|
{
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true
|
|
};
|
|
using (var process = Process.Start(start))
|
|
{
|
|
if (process == null) throw new InvalidOperationException("Runtime-Abnahme konnte nicht gestartet werden.");
|
|
var output = process.StandardOutput.ReadToEnd().Trim();
|
|
var error = process.StandardError.ReadToEnd().Trim();
|
|
if (!process.WaitForExit(30000))
|
|
{
|
|
process.Kill();
|
|
throw new TimeoutException("Runtime-Abnahme hat das Zeitlimit ueberschritten.");
|
|
}
|
|
|
|
if (process.ExitCode != 0
|
|
|| !output.StartsWith("RUNTIME_VALIDATION_V1 ", StringComparison.Ordinal))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Runtime-Abnahme fehlgeschlagen. Exitcode=" + process.ExitCode
|
|
+ ". " + (error.Length == 0 ? output : error));
|
|
}
|
|
|
|
return output;
|
|
}
|
|
}
|
|
|
|
private static string ExtractServiceName(string line)
|
|
{
|
|
var firstQuote = line.IndexOf('"');
|
|
var secondQuote = firstQuote < 0 ? -1 : line.IndexOf('"', firstQuote + 1);
|
|
if (firstQuote < 0 || secondQuote <= firstQuote + 1)
|
|
throw new InvalidOperationException("Self-Test fehlgeschlagen: Checkmk-Servicename kann nicht gelesen werden: " + line);
|
|
return line.Substring(firstQuote + 1, secondQuote - firstQuote - 1);
|
|
}
|
|
|
|
internal static string EnsureServiceNameCompatibility(
|
|
IEnumerable<string> installedServiceNames,
|
|
IEnumerable<string> stagedServiceNames,
|
|
bool allowServiceNameChange)
|
|
{
|
|
var installed = new HashSet<string>(installedServiceNames ?? Enumerable.Empty<string>(), StringComparer.Ordinal);
|
|
var staged = new HashSet<string>(stagedServiceNames ?? Enumerable.Empty<string>(), StringComparer.Ordinal);
|
|
if (installed.SetEquals(staged)) return string.Empty;
|
|
|
|
var removed = installed.Except(staged, StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal).ToArray();
|
|
var added = staged.Except(installed, StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal).ToArray();
|
|
var description = "Entfernt=[" + string.Join(", ", removed) + "]; Neu=[" + string.Join(", ", added) + "].";
|
|
if (!allowServiceNameChange)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Sicherheitsstopp: Das Update wuerde Checkmk-Servicenamen aendern. "
|
|
+ description
|
|
+ " Ohne Service Discovery entstehen verwaiste bzw. fehlende Services. "
|
|
+ "Nur wenn die Aenderung beabsichtigt ist, im Setup 'Service Discovery ist eingeplant' bestaetigen und anschliessend die Discovery ausfuehren.");
|
|
}
|
|
|
|
return description;
|
|
}
|
|
}
|
|
}
|