using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace SiteConfigMerge { internal static class Program { private static int Main(string[] args) { try { if (args.Length == 0) return Usage(); switch (args[0].ToLowerInvariant()) { case "dump": return Dump(args.Skip(1).ToArray()); case "merge": return Merge(args.Skip(1).ToArray()); default: return Usage(); } } catch (Exception ex) { Console.Error.WriteLine("ERROR: " + ex.Message); return 2; } } private static int Usage() { Console.WriteLine( @"SiteConfigMerge — decode and merge TeslaConsole .siteconfig files (SiteLink) Usage: SiteConfigMerge dump [...] Decode and print squads/pods of each file. SiteConfigMerge merge -o .siteconfig [...] Merge sites into one config. The site name is taken from each input file name; every squad is renamed ""-"". Pod records are copied byte-for-byte from the inputs."); return 1; } private static int Dump(string[] files) { if (files.Length == 0) return Usage(); foreach (var file in files) { var binder = new TeslaBinder(); var squads = SiteConfigFile.Read(file, binder); Console.WriteLine($"=== {file}"); Console.WriteLine($" serialized by: {binder.CapturedAssembly ?? "(no TeslaConsole records)"}"); Console.WriteLine($" squads: {squads.Count}"); foreach (var entry in squads) { Console.WriteLine($" Squad \"{entry.Squad.Name}\" guid={entry.Squad.Guid} online={entry.Squad.Online} pods={entry.Pods.Count}"); foreach (var p in entry.Pods.Select(e => e.Pod)) { Console.WriteLine($" Pod \"{p.Name}\" host={p.HostName} type={p.HostType}"); Console.WriteLine($" ip={p.IPAddress} subnet={p.Subnet} gw={p.Gateway} dns={p.Dns}"); Console.WriteLine($" mac={FormatMac(p.MacAddress)} key={(p.Key == null || p.Key.Length == 0 ? "none" : p.Key.Length + " bytes")} " + $"art={p.PodArtPath ?? "(null)"} id={p.Id}"); } } } return 0; } private static int Merge(string[] args) { string output = null; var inputs = new List(); for (int i = 0; i < args.Length; i++) { if (args[i] == "-o" || args[i] == "--output") { if (i + 1 >= args.Length) throw new ArgumentException("-o requires a file name"); output = args[++i]; } else { inputs.Add(args[i]); } } if (output == null || inputs.Count == 0) return Usage(); var binder = new TeslaBinder(); // shared: output uses the first input's assembly identity var merged = new List(); var squadNames = new HashSet(StringComparer.OrdinalIgnoreCase); var podIds = new Dictionary(); var podMacs = new Dictionary(); var podIps = new Dictionary(); foreach (var file in inputs) { string site = Path.GetFileNameWithoutExtension(file); var squads = SiteConfigFile.Read(file, binder); Console.WriteLine($"{file}: site \"{site}\", {squads.Count} squad(s), {squads.Sum(s => s.Pods.Count)} pod(s)"); int unnamed = 0; foreach (var entry in squads) { string original = entry.Squad.Name; entry.Squad.Name = string.IsNullOrEmpty(original) ? $"{site}-squad{++unnamed}" : $"{site}-{original}"; if (!squadNames.Add(entry.Squad.Name)) throw new InvalidOperationException( $"duplicate squad name \"{entry.Squad.Name}\" — same site file given twice?"); foreach (var p in entry.Pods.Select(e => e.Pod)) { string where = $"{entry.Squad.Name}/\"{p.Name}\""; if (podIds.TryGetValue(p.Id, out var prev)) Warn($"pod GUID {p.Id} appears in both {prev} and {where} — same pod imported twice?"); else podIds[p.Id] = where; string mac = FormatMac(p.MacAddress); if (mac != "none") { if (podMacs.TryGetValue(mac, out prev)) Warn($"MAC {mac} appears in both {prev} and {where}"); else podMacs[mac] = where; } string ip = p.IPAddress?.ToString(); if (!string.IsNullOrEmpty(ip) && ip != "0.0.0.0") { if (podIps.TryGetValue(ip, out prev)) Warn($"IP {ip} used by both {prev} and {where} — sites need renumbering before linking"); else podIps[ip] = where; } } merged.Add(entry); } } SiteConfigFile.Write(output, merged, binder); Console.WriteLine($"wrote {output}: {merged.Count} squad(s), {merged.Sum(s => s.Pods.Count)} pod(s) " + $"(identity: {binder.CapturedAssembly ?? TeslaBinder.DefaultAssembly})"); if (mWarnings > 0) { // Under the SiteLink IP plan none of the consistency checks should ever // fire; a warning means plan drift (un-renumbered site) or stale // inventory (same pod in two site files). Exit 3 so event scripts can // gate on a clean merge. Console.Error.WriteLine($"{mWarnings} warning(s) — master written, but resolve these before the event."); return 3; } return 0; } private static int mWarnings; private static void Warn(string message) { mWarnings++; Console.Error.WriteLine("WARNING: " + message); } private static string FormatMac(byte[] mac) => mac == null || mac.Length == 0 ? "none" : BitConverter.ToString(mac).Replace('-', ':'); } }