Changed VrmGrabber to run with an SQLite DB

This commit is contained in:
Kim 2023-05-18 10:45:44 +02:00
parent 3453b05a94
commit 24684ccb09
11 changed files with 258 additions and 114 deletions

View File

@ -7,7 +7,7 @@ public static class Ssh
{ {
const String ConfigFile = "/data/innovenergy/openvpn/installation-name"; const String ConfigFile = "/data/innovenergy/openvpn/installation-name";
public static Task<Int32> Interactive(String host, String installationName, String user, String port) public static Task<Int32> Interactive(String host, String? installationName, String user, String port)
{ {
var fixInstallationName = $"echo '{installationName}' > {ConfigFile}; exec bash -l"; var fixInstallationName = $"echo '{installationName}' > {ConfigFile}; exec bash -l";

View File

@ -1,23 +1,19 @@
using System.Data.Common;
using System.IdentityModel.Tokens.Jwt;
using System.Web; using System.Web;
using HandlebarsDotNet; using HandlebarsDotNet;
using InnovEnergy.App.RemoteSupportConsole; using InnovEnergy.App.RemoteSupportConsole;
using static System.Text.Json.JsonSerializer;
using InnovEnergy.App.VrmGrabber.Database; using InnovEnergy.App.VrmGrabber.Database;
using InnovEnergy.Lib.Victron.VictronVRM; using InnovEnergy.Lib.Victron.VictronVRM;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using FILE=System.IO.File; 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; namespace InnovEnergy.App.VrmGrabber;
public record Install(
String Name,
String Ip,
UInt64 Vrm,
String Identifier,
String Serial,
String EscapedName,
String Online
);
[Controller] [Controller]
public class Controller : ControllerBase public class Controller : ControllerBase
{ {
@ -26,46 +22,39 @@ public class Controller : ControllerBase
[Produces("text/html")] [Produces("text/html")]
public ActionResult Index() public ActionResult Index()
{ {
var instList = Db.InstallationsAndDetails.Keys.ToList();
if (instList.Count == 0) return new ContentResult
{
ContentType = "text/html",
Content = "<p>Please wait page is still loading</p>"
};
String source = @"<head> String source = @"<head>
<style> <style>
tbody { tbody {
background-color: #e4f0f5; background-color: #e4f0f5;
} }
table { table {
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
border: 2px solid rgb(200, 200, 200); border: 2px solid rgb(200, 200, 200);
letter-spacing: 1px; letter-spacing: 1px;
font-family: sans-serif; font-family: sans-serif;
font-size: 0.8rem; font-size: 0.8rem;
position: absolute; top: 0; bottom: 0; left: 0; right: 0; position: absolute; top: 0; bottom: 0; left: 0; right: 0;
} }
td, td,
th { th {
border: 1px solid rgb(190, 190, 190); border: 1px solid rgb(190, 190, 190);
padding: 5px 10px; padding: 5px 10px;
} }
td { td {
text-align: left; text-align: left;
}</style></head> }</style></head>
<table> <table>
<tbody> <tbody>
{{#inst}} {{#inst}}
{{> installations}} {{> installations}}
{{/inst}} {{/inst}}
</tbody> </tbody>
</table>"; </table>";
String partialSource = String partialSource =
@"<tr><td>{{Name}}</td> @"<tr><td>{{Name}}</td>
@ -73,21 +62,39 @@ td {
<td><a target='_blank' href=https://vrm.victronenergy.com/installation/{{Vrm}}/dashboard>VRM</a></td> <td><a target='_blank' href=https://vrm.victronenergy.com/installation/{{Vrm}}/dashboard>VRM</a></td>
<td><a target='_blank' href='https://salidomo.innovenergy.ch/d/ENkNRQXmk/installation?refresh=5s&orgId=1&var-Installation={{EscapedName}}&kiosk=tv'>Grafana</a></td> <td><a target='_blank' href='https://salidomo.innovenergy.ch/d/ENkNRQXmk/installation?refresh=5s&orgId=1&var-Installation={{EscapedName}}&kiosk=tv'>Grafana</a></td>
<td>{{Identifier}}</td> <td>{{Identifier}}</td>
<td>{{LastSeen}}</td>
<td>{{Serial}}</td> <td>{{Serial}}</td>
</tr>"; </tr>";
var instList = Db.Installations.ToList();
if (instList.Count == 0) return new ContentResult
{
ContentType = "text/html",
Content = "<p>Please wait page is still loading</p>"
};
Handlebars.RegisterTemplate("installations", partialSource); Handlebars.RegisterTemplate("installations", partialSource);
var template = Handlebars.Compile(source); var template = Handlebars.Compile(source);
var insts = instList.Select(i => // var insts = instList.Select(i =>
{ // {
var ip = Ip(i); // var ip = Ip(i);
return new Install(i.Name, ip[0], i.IdSite, i.Identifier, Serial(i), HttpUtility.UrlEncode(i.Name), ip[1]); // return new Install(
}); // i.Name,
// ip[0],
// i.Vrm,
// i.Identifier,
// i.Serial,
// i.EscapedName,
// ip[1],
// LastSeen(i));
// });
var data = new var data = new
{ {
inst = insts inst = instList
}; };
var result = template(data); var result = template(data);
@ -99,27 +106,15 @@ td {
}; };
} }
private String[] Ip(Installation installation) private String? LastSeen(Installation installation)
{ {
var online = "❌"; return Db.GetInstallationByIdentifier(installation.Identifier)?.Details.ToString();
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};
} }
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 = "<Pending>")]
public static String? Serial(Installation installation)
{ {
return Db.InstallationsAndDetails[installation].Details.MachineSerial() ?? "Unknown"; return Deserialize<IEnumerable<Detail>>(installation.Details).MachineSerial();
} }
// [HttpGet(nameof(GetInstallation))] // [HttpGet(nameof(GetInstallation))]
@ -139,9 +134,3 @@ td {
// } // }
} }
// installation Name, ip (link uf gui), idSite (vrm link), identifier , machineserial (HQ...)

View File

@ -1,17 +1,36 @@
using InnovEnergy.Lib.Victron.VictronVRM; using InnovEnergy.Lib.Victron.VictronVRM;
using SQLite;
namespace InnovEnergy.App.VrmGrabber.DataTypes; 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 Installation()
{ {
} }
public String Ip { get; set; }
public String Name { get; set; } public String? Name { get; set;}
// Settings public String? Ip { get; set;}
public IReadOnlyList<Detail> Notes { 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
} }

View File

@ -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;
}
}

View File

@ -1,7 +1,5 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Reactive.Concurrency; using System.Web;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using CliWrap; using CliWrap;
using CliWrap.Buffered; using CliWrap.Buffered;
using InnovEnergy.App.RemoteSupportConsole; using InnovEnergy.App.RemoteSupportConsole;
@ -10,29 +8,51 @@ using InnovEnergy.Lib.Victron.VictronVRM;
using SQLite; using SQLite;
using static System.Text.Json.JsonSerializer; using static System.Text.Json.JsonSerializer;
using Installation = InnovEnergy.App.VrmGrabber.DataTypes.Installation; using Installation = InnovEnergy.App.VrmGrabber.DataTypes.Installation;
using VrmInstallation = InnovEnergy.Lib.Victron.VictronVRM.Installation;
using FILE=System.IO.File;
namespace InnovEnergy.App.VrmGrabber.Database; namespace InnovEnergy.App.VrmGrabber.Database;
public class InstallationDetails public class InstallationDetails
{ {
public InstallationDetails(String ip, IReadOnlyList<Detail> details) public InstallationDetails(String? ip, IReadOnlyList<Detail> details)
{ {
Details = details; Details = details;
Ip = ip; Ip = ip;
} }
public IReadOnlyList<Detail>? Details { get; set; } public IReadOnlyList<Detail>? 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<Lib.Victron.VictronVRM.Installation, InstallationDetails> InstallationsAndDetails; public static Dictionary<Installation, InstallationDetails> InstallationsAndDetails;
internal const String DbPath = "./db.sqlite";
private static SQLiteConnection Connection { get; } = new SQLiteConnection(DbPath);
public static TableQuery<Installation> Installations => Connection.Table<Installation>();
public static void Init()
{
// used to force static constructor
}
static Db()
{
// on startup create/migrate tables
Connection.RunInTransaction(() => { Connection.CreateTable<Installation>(); });
}
public static async Task UpdateDetailsAndInstallations() public static async Task UpdateDetailsAndInstallations()
{ {
InstallationsAndDetails = new Dictionary<Lib.Victron.VictronVRM.Installation, InstallationDetails>();
while (true) while (true)
{ {
await UpdateInstallationsAndDetailsFromVrm(0); 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 = "<Pending>")] [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
private static async Task<Dictionary<Lib.Victron.VictronVRM.Installation, InstallationDetails>> UpdateInstallationsAndDetailsFromVrm(Int32 _) private static async Task UpdateInstallationsAndDetailsFromVrm(Int32 _)
{ {
var fileContent = await File.ReadAllTextAsync("./token.json"); var fileContent = await File.ReadAllTextAsync("./token.json");
var acc = Deserialize<AccToken>(fileContent); var acc = Deserialize<AccToken>(fileContent);
var user = VrmAccount.Token(acc!.idUser, acc.token); var user = VrmAccount.Token(acc!.idUser, acc.token);
var installations = await user.GetInstallations(); var installations = await user.GetInstallations();
var returnDictionary = new Dictionary<Lib.Victron.VictronVRM.Installation, InstallationDetails>();
foreach (var installation in installations) // var returnDictionary = new Dictionary<VrmInstallation, InstallationDetails>();
foreach (var installation in installations.Take(20)) //TODO REMOVE TAKE
{ {
Console.WriteLine(installation.Name); Console.WriteLine(installation.Name);
var details = await GetInstallationDetails(installation); 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<InstallationDetails> 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<InstallationDetails> GetInstallationDetails(VrmInstallation i)
{ {
await Task.Delay(1000); await Task.Delay(1000);
try try
{ {
var details = await i.GetDetails(); var details = await i.GetDetails();
var ip = await VpnInfo.LookUpIp(i.Identifier, details.MachineSerial()) ?? "Unknown"; var ip = await VpnInfo.LookUpIp(i.Identifier, details.MachineSerial()) ?? "Unknown";
if(ip != "Unknown") if (ip != "Unknown")
await UpdateInstallationName(i, ip); await UpdateInstallationName(i, ip);
return new InstallationDetails(ip,details); return new InstallationDetails(ip, details);
} }
catch (Exception e) catch (Exception e)
{ {
@ -80,7 +136,7 @@ public static partial class Db
return new InstallationDetails("Unknown", Array.Empty<Detail>()); return new InstallationDetails("Unknown", Array.Empty<Detail>());
} }
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") var oldNameInFileRequest = await Cli.Wrap("ssh")
.WithArguments($@"root@{ip}") .WithArguments($@"root@{ip}")
@ -89,16 +145,16 @@ public static partial class Db
.WithValidation(CommandResultValidation.None).ExecuteBufferedAsync(); .WithValidation(CommandResultValidation.None).ExecuteBufferedAsync();
var oldNameInFileWithoutNewLine = oldNameInFileRequest.StandardOutput.TrimEnd(); var oldNameInFileWithoutNewLine = oldNameInFileRequest.StandardOutput.TrimEnd();
if (oldNameInFileRequest.ExitCode == 0 && oldNameInFileWithoutNewLine != installation.Name) if (oldNameInFileRequest.ExitCode == 0 && oldNameInFileWithoutNewLine != installation.Name)
{ {
var overwriteNameCommand = Cli.Wrap("ssh") var overwriteNameCommand = Cli.Wrap("ssh")
.WithArguments($@"root@{ip}") .WithArguments($@"root@{ip}")
.AppendArgument($"echo '{installation.Name}' > /data/innovenergy/openvpn/installation-name"); .AppendArgument($"echo '{installation.Name}' > /data/innovenergy/openvpn/installation-name");
var overwriteNameResponse = await overwriteNameCommand var overwriteNameResponse = await overwriteNameCommand
.WithValidation(CommandResultValidation.None).ExecuteBufferedAsync(); .WithValidation(CommandResultValidation.None).ExecuteBufferedAsync();
if (overwriteNameResponse.ExitCode != 0) if (overwriteNameResponse.ExitCode != 0)
{ {
Console.WriteLine(overwriteNameResponse.StandardError); Console.WriteLine(overwriteNameResponse.StandardError);
@ -123,10 +179,31 @@ public static partial class Db
} }
} }
} }
private static Boolean RunTransaction(Func<Boolean> 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 class AccToken
{ {
public UInt64 idUser { get; init;} public UInt64 idUser { get; init;}
public String token { get; init;} public String token { get; init;}
} }

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

Binary file not shown.

View File

@ -11,7 +11,7 @@ public static class JsonNodeAccessors
public static T Get<T>(this JsonNode n, String propName) => n[propName]!.GetValue<T>(); public static T Get<T>(this JsonNode n, String propName) => n[propName]!.GetValue<T>();
public static String GetString(this JsonNode n, String propName) => n.Get<String>(propName); public static String? GetString(this JsonNode n, String propName) => n.TryGetString(propName);
public static Boolean GetBoolean(this JsonNode n, String propName) => n.Get<Boolean>(propName); public static Boolean GetBoolean(this JsonNode n, String propName) => n.Get<Boolean>(propName);
public static UInt32 GetUInt32(this JsonNode n, String propName) => n.Get<UInt32>(propName); public static UInt32 GetUInt32(this JsonNode n, String propName) => n.Get<UInt32>(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 try
{ {

View File

@ -118,7 +118,7 @@ public static class StringUtils
return String.Join("", strings); return String.Join("", strings);
} }
public static String JoinWith(this IEnumerable<String> strings, String separator) public static String JoinWith(this IEnumerable<String?> strings, String separator)
{ {
return String.Join(separator, strings); return String.Join(separator, strings);
} }