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";
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";

View File

@ -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 = "<p>Please wait page is still loading</p>"
};
String source = @"<head>
<style>
tbody {
background-color: #e4f0f5;
}
table {
border-collapse: collapse;
width: 100%;
border: 2px solid rgb(200, 200, 200);
letter-spacing: 1px;
font-family: sans-serif;
font-size: 0.8rem;
position: absolute; top: 0; bottom: 0; left: 0; right: 0;
}
td,
th {
border: 1px solid rgb(190, 190, 190);
padding: 5px 10px;
}
td {
text-align: left;
}</style></head>
<table>
<tbody>
{{#inst}}
{{> installations}}
{{/inst}}
</tbody>
</table>";
<style>
tbody {
background-color: #e4f0f5;
}
table {
border-collapse: collapse;
width: 100%;
border: 2px solid rgb(200, 200, 200);
letter-spacing: 1px;
font-family: sans-serif;
font-size: 0.8rem;
position: absolute; top: 0; bottom: 0; left: 0; right: 0;
}
td,
th {
border: 1px solid rgb(190, 190, 190);
padding: 5px 10px;
}
td {
text-align: left;
}</style></head>
<table>
<tbody>
{{#inst}}
{{> installations}}
{{/inst}}
</tbody>
</table>";
String partialSource =
@"<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://salidomo.innovenergy.ch/d/ENkNRQXmk/installation?refresh=5s&orgId=1&var-Installation={{EscapedName}}&kiosk=tv'>Grafana</a></td>
<td>{{Identifier}}</td>
<td>{{LastSeen}}</td>
<td>{{Serial}}</td>
</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);
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 = "<Pending>")]
public static String? Serial(Installation installation)
{
return Db.InstallationsAndDetails[installation].Details.MachineSerial() ?? "Unknown";
return Deserialize<IEnumerable<Detail>>(installation.Details).MachineSerial();
}
// [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 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<Detail> 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
}

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.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<Detail> details)
public InstallationDetails(String? ip, IReadOnlyList<Detail> details)
{
Details = details;
Ip = ip;
}
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()
{
InstallationsAndDetails = new Dictionary<Lib.Victron.VictronVRM.Installation, InstallationDetails>();
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 = "<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 acc = Deserialize<AccToken>(fileContent);
var user = VrmAccount.Token(acc!.idUser, acc.token);
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);
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);
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<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")
.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<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 UInt64 idUser { 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 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 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
{

View File

@ -118,7 +118,7 @@ public static class StringUtils
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);
}