New: tools/SiteConfigMerge (net48 console app) - dump: decode any .siteconfig (squads, pods, IPs, MACs, host types) - merge: combine <siteName>.siteconfig inputs into master.siteconfig, renaming squads "<siteName>-<original squad name>" - Pod records pass through byte-for-byte; only squad records (the rename) are re-serialized, under the TeslaConsole assembly identity captured from the input. Stand-in types + SerializationBinder, so no build dependency on TeslaSuite. - Warns on duplicate pod GUID/MAC and cross-site IP overlap. - Verified end-to-end: the real TeslaConsole.exe 4.11.4.1 loaded the merged master via its own Site.LoadFromFile (reflection harness). Doc updates from operator decisions: - Operating model settled: sites voluntarily hand console authority to the central console for the duration of a SiteLink event, by contributing their siteconfig. Federation ruled out at current scale. - Siteconfig "secrets" framing corrected: pod keys have no practical value outside the air-gapped bay; files are exchanged per event and never stored in this repo (tools only). - Fleet scale recorded: 6 active pod bays, <120 cockpits in existence; bay sizes range console+2 cockpits up to the full 20-node complement. Open question 9 answered. - Hub hosting direction: neutral Firestorm host at the WireGuard hub; the FS server usually IS the Live Cam, so stream its output to all sites and optionally to the public internet. Mission Review instance runs at the hub too - one authoritative debrief streamed everywhere. - Virtual PDF scoresheet printer at the hub: event debriefings print centrally, retrievable from any site on the link. - Voice (Mumble) backburnered - revisit only on event interest. - .gitignore: build outputs; siteconfig exclusion rationale reworded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
161 lines
6.6 KiB
C#
161 lines
6.6 KiB
C#
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 <file.siteconfig> [...]
|
|
Decode and print squads/pods of each file.
|
|
|
|
SiteConfigMerge merge -o <master.siteconfig> <siteName>.siteconfig [...]
|
|
Merge sites into one config. The site name is taken from each input
|
|
file name; every squad is renamed ""<siteName>-<original squad name>"".
|
|
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<string>();
|
|
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<SquadEntry>();
|
|
var squadNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
var podIds = new Dictionary<Guid, string>();
|
|
var podMacs = new Dictionary<string, string>();
|
|
var podIps = new Dictionary<string, string>();
|
|
|
|
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})");
|
|
return 0;
|
|
}
|
|
|
|
private static void Warn(string message) => Console.Error.WriteLine("WARNING: " + message);
|
|
|
|
private static string FormatMac(byte[] mac) =>
|
|
mac == null || mac.Length == 0 ? "none" : BitConverter.ToString(mac).Replace('-', ':');
|
|
}
|
|
}
|