diff --git a/csharp/App/RemoteSupportConsole/Ssh.cs b/csharp/App/RemoteSupportConsole/Ssh.cs index d9acc9662..dd4fe7a4e 100644 --- a/csharp/App/RemoteSupportConsole/Ssh.cs +++ b/csharp/App/RemoteSupportConsole/Ssh.cs @@ -7,7 +7,7 @@ public static class Ssh { const String ConfigFile = "/data/innovenergy/openvpn/installation-name"; - public static Task Interactive(String host, String installationName, String user, String port) + public static Task Interactive(String host, String? installationName, String user, String port) { var fixInstallationName = $"echo '{installationName}' > {ConfigFile}; exec bash -l"; diff --git a/csharp/App/VrmGrabber/Controller.cs b/csharp/App/VrmGrabber/Controller.cs index 08ee31f46..a21523d6c 100644 --- a/csharp/App/VrmGrabber/Controller.cs +++ b/csharp/App/VrmGrabber/Controller.cs @@ -1,23 +1,19 @@ +using System.Data.Common; +using System.IdentityModel.Tokens.Jwt; using System.Web; using HandlebarsDotNet; using InnovEnergy.App.RemoteSupportConsole; +using static System.Text.Json.JsonSerializer; using InnovEnergy.App.VrmGrabber.Database; using InnovEnergy.Lib.Victron.VictronVRM; using Microsoft.AspNetCore.Mvc; using FILE=System.IO.File; +using VrmInstallation = InnovEnergy.Lib.Victron.VictronVRM.Installation; +using Installation = InnovEnergy.App.VrmGrabber.DataTypes.Installation; +using System.Diagnostics.CodeAnalysis; namespace InnovEnergy.App.VrmGrabber; -public record Install( - String Name, - String Ip, - UInt64 Vrm, - String Identifier, - String Serial, - String EscapedName, - String Online -); - [Controller] public class Controller : ControllerBase { @@ -26,46 +22,39 @@ public class Controller : ControllerBase [Produces("text/html")] public ActionResult Index() { - var instList = Db.InstallationsAndDetails.Keys.ToList(); - if (instList.Count == 0) return new ContentResult - { - ContentType = "text/html", - Content = "

Please wait page is still loading

" - }; - String source = @" - - - - - {{#inst}} - {{> installations}} - {{/inst}} - -
"; + + + + + {{#inst}} + {{> installations}} + {{/inst}} + +
"; String partialSource = @"{{Name}} @@ -73,21 +62,39 @@ td { VRM Grafana {{Identifier}} + {{LastSeen}} {{Serial}} "; + + var instList = Db.Installations.ToList(); + if (instList.Count == 0) return new ContentResult + { + ContentType = "text/html", + Content = "

Please wait page is still loading

" + }; + + Handlebars.RegisterTemplate("installations", partialSource); var template = Handlebars.Compile(source); - var insts = instList.Select(i => - { - var ip = Ip(i); - return new Install(i.Name, ip[0], i.IdSite, i.Identifier, Serial(i), HttpUtility.UrlEncode(i.Name), ip[1]); - }); + // var insts = instList.Select(i => + // { + // var ip = Ip(i); + // return new Install( + // i.Name, + // ip[0], + // i.Vrm, + // i.Identifier, + // i.Serial, + // i.EscapedName, + // ip[1], + // LastSeen(i)); + // }); var data = new { - inst = insts + inst = instList }; var result = template(data); @@ -99,27 +106,15 @@ td { }; } - private String[] Ip(Installation installation) + private String? LastSeen(Installation installation) { - var online = "❌"; - var lookup = Db.InstallationsAndDetails[installation].Ip; - if (lookup == "Unknown") - { - var serial = Serial(installation); - if (serial != "Unknown" && FILE.Exists($@"/etc/openvpn/server/Salino/ccd/{serial}")) - lookup = FILE.ReadAllText($@"/etc/openvpn/server/Salino/ccd/{serial}").Split(' ')[1]; - } - else - { - online = "✅"; - } - - return new [] {lookup,online}; + return Db.GetInstallationByIdentifier(installation.Identifier)?.Details.ToString(); } - public static String Serial(Installation installation) + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + public static String? Serial(Installation installation) { - return Db.InstallationsAndDetails[installation].Details.MachineSerial() ?? "Unknown"; + return Deserialize>(installation.Details).MachineSerial(); } // [HttpGet(nameof(GetInstallation))] @@ -139,9 +134,3 @@ td { // } } - - - -// installation Name, ip (link uf gui), idSite (vrm link), identifier , machineserial (HQ...) - - diff --git a/csharp/App/VrmGrabber/DataTypes/Installation.cs b/csharp/App/VrmGrabber/DataTypes/Installation.cs index 6199bbb08..8bcd30db2 100644 --- a/csharp/App/VrmGrabber/DataTypes/Installation.cs +++ b/csharp/App/VrmGrabber/DataTypes/Installation.cs @@ -1,17 +1,36 @@ using InnovEnergy.Lib.Victron.VictronVRM; +using SQLite; namespace InnovEnergy.App.VrmGrabber.DataTypes; -public class Installation : TreeNode +public class Installation { - + public Installation(String? argName, String? argIp, Int64 argVrm, String? argIdentifier, String? serial, String? urlEncode, String? online, String? lastSeen, String details) + { + Name = argName; + Ip = argIp; + Vrm = argVrm; + Identifier = argIdentifier; + Serial = serial; + EscapedName = urlEncode; + Online = online; + LastSeen = lastSeen; + Details = details; + } public Installation() { } - public String Ip { get; set; } - public String Name { get; set; } - // Settings - public IReadOnlyList Notes { get; set; } + + public String? Name { get; set;} + public String? Ip { get; set;} + public Int64 Vrm { get; set;} + public String? Identifier { get; set;} + public String? Serial { get; set;} + public String? EscapedName { get; set;} + public String? Online { get; set;} + public String? LastSeen { get; set;} + + public String? Details { get; set; } //JSON } \ No newline at end of file diff --git a/csharp/App/VrmGrabber/Database/Create.cs b/csharp/App/VrmGrabber/Database/Create.cs new file mode 100644 index 000000000..9283b6a70 --- /dev/null +++ b/csharp/App/VrmGrabber/Database/Create.cs @@ -0,0 +1,14 @@ +using InnovEnergy.App.VrmGrabber.DataTypes; + + +namespace InnovEnergy.App.VrmGrabber.Database; + + +public static partial class Db +{ + public static Boolean Create(Installation installation) + { + // SQLite wrapper is smart and *modifies* t's Id to the one generated (autoincrement) by the insertion + return Connection.Insert(installation) > 0; + } +} \ No newline at end of file diff --git a/csharp/App/VrmGrabber/Database/Db.cs b/csharp/App/VrmGrabber/Database/Db.cs index cc8269ed2..dcc0f10dc 100644 --- a/csharp/App/VrmGrabber/Database/Db.cs +++ b/csharp/App/VrmGrabber/Database/Db.cs @@ -1,7 +1,5 @@ using System.Diagnostics.CodeAnalysis; -using System.Reactive.Concurrency; -using System.Reactive.Linq; -using System.Reactive.Threading.Tasks; +using System.Web; using CliWrap; using CliWrap.Buffered; using InnovEnergy.App.RemoteSupportConsole; @@ -10,29 +8,51 @@ using InnovEnergy.Lib.Victron.VictronVRM; using SQLite; using static System.Text.Json.JsonSerializer; using Installation = InnovEnergy.App.VrmGrabber.DataTypes.Installation; +using VrmInstallation = InnovEnergy.Lib.Victron.VictronVRM.Installation; +using FILE=System.IO.File; + namespace InnovEnergy.App.VrmGrabber.Database; public class InstallationDetails { - public InstallationDetails(String ip, IReadOnlyList details) + public InstallationDetails(String? ip, IReadOnlyList details) { Details = details; Ip = ip; } public IReadOnlyList? Details { get; set; } - public String Ip { get; set; } + public String? Ip { get; set; } } -public static partial class Db +public static partial class Db { - public static Dictionary InstallationsAndDetails; + public static Dictionary InstallationsAndDetails; + + + internal const String DbPath = "./db.sqlite"; + + private static SQLiteConnection Connection { get; } = new SQLiteConnection(DbPath); + + public static TableQuery Installations => Connection.Table(); + + public static void Init() + { + // used to force static constructor + } + + + static Db() + { + // on startup create/migrate tables + + Connection.RunInTransaction(() => { Connection.CreateTable(); }); + } public static async Task UpdateDetailsAndInstallations() { - InstallationsAndDetails = new Dictionary(); while (true) { await UpdateInstallationsAndDetailsFromVrm(0); @@ -40,37 +60,73 @@ public static partial class Db } [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] - private static async Task> UpdateInstallationsAndDetailsFromVrm(Int32 _) + private static async Task UpdateInstallationsAndDetailsFromVrm(Int32 _) { var fileContent = await File.ReadAllTextAsync("./token.json"); - + var acc = Deserialize(fileContent); var user = VrmAccount.Token(acc!.idUser, acc.token); var installations = await user.GetInstallations(); - var returnDictionary = new Dictionary(); - foreach (var installation in installations) + + // var returnDictionary = new Dictionary(); + foreach (var installation in installations.Take(20)) //TODO REMOVE TAKE { Console.WriteLine(installation.Name); var details = await GetInstallationDetails(installation); - returnDictionary.Add(installation, details); + var ip = Ip(details); + var updatedInstallation = new Installation( + installation.Name, + ip[0], + (Int64)installation.IdSite, + installation.Identifier, + details.Details.MachineSerial() ?? "Unknown", + HttpUtility.UrlEncode(installation.Name), + ip[1], + details.Details.Last().Json["timestamp"].ToString(), + Serialize(details.Details)); + + if (GetInstallationByIdentifier(installation.Identifier) == null) + { + Create(updatedInstallation); + } + else + { + Update(updatedInstallation); + } } - - return returnDictionary; } - private static async Task GetInstallationDetails(Lib.Victron.VictronVRM.Installation i) + private static String?[] Ip(InstallationDetails details) + { + var online = "❌"; + var lookup = details.Ip; + if (lookup == "Unknown") + { + var serial = details.Details.MachineSerial() ?? "Unknown"; + if (serial != "Unknown" && FILE.Exists($@"/etc/openvpn/server/Salino/ccd/{serial}")) + lookup = FILE.ReadAllText($@"/etc/openvpn/server/Salino/ccd/{serial}").Split(' ')[1]; + } + else + { + online = "✅"; + } + + return new[] { lookup, online }; + } + + private static async Task GetInstallationDetails(VrmInstallation i) { await Task.Delay(1000); try { var details = await i.GetDetails(); - + var ip = await VpnInfo.LookUpIp(i.Identifier, details.MachineSerial()) ?? "Unknown"; - - if(ip != "Unknown") + + if (ip != "Unknown") await UpdateInstallationName(i, ip); - - return new InstallationDetails(ip,details); + + return new InstallationDetails(ip, details); } catch (Exception e) { @@ -80,7 +136,7 @@ public static partial class Db return new InstallationDetails("Unknown", Array.Empty()); } - private static async Task UpdateInstallationName(Lib.Victron.VictronVRM.Installation installation, String ip) + private static async Task UpdateInstallationName(VrmInstallation installation, String? ip) { var oldNameInFileRequest = await Cli.Wrap("ssh") .WithArguments($@"root@{ip}") @@ -89,16 +145,16 @@ public static partial class Db .WithValidation(CommandResultValidation.None).ExecuteBufferedAsync(); var oldNameInFileWithoutNewLine = oldNameInFileRequest.StandardOutput.TrimEnd(); - + if (oldNameInFileRequest.ExitCode == 0 && oldNameInFileWithoutNewLine != installation.Name) { var overwriteNameCommand = Cli.Wrap("ssh") .WithArguments($@"root@{ip}") .AppendArgument($"echo '{installation.Name}' > /data/innovenergy/openvpn/installation-name"); - + var overwriteNameResponse = await overwriteNameCommand .WithValidation(CommandResultValidation.None).ExecuteBufferedAsync(); - + if (overwriteNameResponse.ExitCode != 0) { Console.WriteLine(overwriteNameResponse.StandardError); @@ -123,10 +179,31 @@ public static partial class Db } } } + + + private static Boolean RunTransaction(Func func) + { + var savepoint = Connection.SaveTransactionPoint(); + var success = false; + + try + { + success = func(); + } + finally + { + if (success) + Connection.Release(savepoint); + else + Connection.RollbackTo(savepoint); + } + + return success; + } } public class AccToken { public UInt64 idUser { get; init;} public String token { get; init;} -} \ No newline at end of file +} diff --git a/csharp/App/VrmGrabber/Database/Delete.cs b/csharp/App/VrmGrabber/Database/Delete.cs new file mode 100644 index 000000000..3dd008e4e --- /dev/null +++ b/csharp/App/VrmGrabber/Database/Delete.cs @@ -0,0 +1,18 @@ +using InnovEnergy.App.VrmGrabber.DataTypes; + + +namespace InnovEnergy.App.VrmGrabber.Database; + + +public static partial class Db +{ + public static Boolean Delete(Installation installation) + { + return RunTransaction(DeleteInstallationAndItsDependencies); + + Boolean DeleteInstallationAndItsDependencies() + { + return Installations.Delete(i => i.Identifier == installation.Identifier) > 0; + } + } +} \ No newline at end of file diff --git a/csharp/App/VrmGrabber/Database/Read.cs b/csharp/App/VrmGrabber/Database/Read.cs new file mode 100644 index 000000000..9f7643862 --- /dev/null +++ b/csharp/App/VrmGrabber/Database/Read.cs @@ -0,0 +1,14 @@ +using InnovEnergy.App.VrmGrabber.DataTypes; + +namespace InnovEnergy.App.VrmGrabber.Database; + + +public static partial class Db +{ + + public static Installation? GetInstallationByIdentifier(String? identifier) + { + return Installations + .FirstOrDefault(i => i.Identifier == identifier); + } +} \ No newline at end of file diff --git a/csharp/App/VrmGrabber/Database/Update.cs b/csharp/App/VrmGrabber/Database/Update.cs new file mode 100644 index 000000000..723378685 --- /dev/null +++ b/csharp/App/VrmGrabber/Database/Update.cs @@ -0,0 +1,13 @@ +using InnovEnergy.App.VrmGrabber.DataTypes; + +namespace InnovEnergy.App.VrmGrabber.Database; + + +public static partial class Db +{ + + public static Boolean Update(Installation installation) + { + return Connection.Update(installation) > 0; + } +} \ No newline at end of file diff --git a/csharp/App/VrmGrabber/db.sqlite b/csharp/App/VrmGrabber/db.sqlite index 8294c39d2..eb43946b3 100644 Binary files a/csharp/App/VrmGrabber/db.sqlite and b/csharp/App/VrmGrabber/db.sqlite differ diff --git a/csharp/Lib/Utils/JsonNodeAccessors.cs b/csharp/Lib/Utils/JsonNodeAccessors.cs index 182e30877..31cbb17a9 100644 --- a/csharp/Lib/Utils/JsonNodeAccessors.cs +++ b/csharp/Lib/Utils/JsonNodeAccessors.cs @@ -11,7 +11,7 @@ public static class JsonNodeAccessors public static T Get(this JsonNode n, String propName) => n[propName]!.GetValue(); - public static String GetString(this JsonNode n, String propName) => n.Get(propName); + public static String? GetString(this JsonNode n, String propName) => n.TryGetString(propName); public static Boolean GetBoolean(this JsonNode n, String propName) => n.Get(propName); public static UInt32 GetUInt32(this JsonNode n, String propName) => n.Get(propName); @@ -39,7 +39,7 @@ public static class JsonNodeAccessors } } - public static String TryGetString(this JsonNode n, String propName) + public static String? TryGetString(this JsonNode n, String propName) { try { diff --git a/csharp/Lib/Utils/StringUtils.cs b/csharp/Lib/Utils/StringUtils.cs index 46372e381..b98fbf53c 100644 --- a/csharp/Lib/Utils/StringUtils.cs +++ b/csharp/Lib/Utils/StringUtils.cs @@ -118,7 +118,7 @@ public static class StringUtils return String.Join("", strings); } - public static String JoinWith(this IEnumerable strings, String separator) + public static String JoinWith(this IEnumerable strings, String separator) { return String.Join(separator, strings); }