diff --git a/src/ApiService/ApiService/OneFuzzTypes/Enums.cs b/src/ApiService/ApiService/OneFuzzTypes/Enums.cs index c5c378790..e18a98243 100644 --- a/src/ApiService/ApiService/OneFuzzTypes/Enums.cs +++ b/src/ApiService/ApiService/OneFuzzTypes/Enums.cs @@ -26,6 +26,16 @@ INVALID_CONFIGURATION = 473, } +public enum VmState +{ + Init, + ExtensionsLaunched, + ExtensionsFailed, + VmAllocationFailed, + Running, + Stopping, + Stopped +} public enum WebhookMessageState { diff --git a/src/ApiService/ApiService/OneFuzzTypes/Model.cs b/src/ApiService/ApiService/OneFuzzTypes/Model.cs index f4bd028ed..43489de8e 100644 --- a/src/ApiService/ApiService/OneFuzzTypes/Model.cs +++ b/src/ApiService/ApiService/OneFuzzTypes/Model.cs @@ -1,5 +1,6 @@ using Microsoft.OneFuzz.Service.OneFuzzLib.Orm; using System; +using System.Collections.Generic; using PoolName = System.String; namespace Microsoft.OneFuzz.Service; @@ -14,6 +15,12 @@ namespace Microsoft.OneFuzz.Service; /// the "partion key" and "row key" are identified by the [PartitionKey] and [RowKey] attributes /// Guids are mapped to string in the db +public record Authentication +( + string Password, + string PublicKey, + string PrivateKey +); [SkipRename] public enum HeartbeatType @@ -70,6 +77,13 @@ public enum NodeState Halt, } +public record ProxyHeartbeat +( + string Region, + Guid ProxyId, + List Forwards, + DateTimeOffset TimeStamp +); public partial record Node ( @@ -87,6 +101,39 @@ public partial record Node ) : EntityBase(); +public partial record ProxyForward +( + [PartitionKey] string Region, + [RowKey] int DstPort, + int SrcPort, + string DstIp +) : EntityBase(); + +public partial record ProxyConfig +( + Uri Url, + string Notification, + string Region, + Guid? ProxyId, + List Forwards, + string InstanceTelemetryKey, + string MicrosoftTelemetryKey + +); + +public partial record Proxy +( + [PartitionKey] string Region, + [RowKey] Guid ProxyId, + DateTimeOffset? CreatedTimestamp, + VmState State, + Authentication Auth, + string? Ip, + Error? Error, + string Version, + ProxyHeartbeat? heartbeat +) : EntityBase(); + public record Error(ErrorCode Code, string[]? Errors = null); public record UserInfo(Guid? ApplicationId, Guid? ObjectId, String? Upn); diff --git a/src/ApiService/ApiService/Program.cs b/src/ApiService/ApiService/Program.cs index 7c663de6d..fa06b0b8f 100644 --- a/src/ApiService/ApiService/Program.cs +++ b/src/ApiService/ApiService/Program.cs @@ -42,6 +42,7 @@ public class Program .AddSingleton() .AddSingleton(_ => new Creds()) .AddSingleton() + .AddSingleton() ) .Build(); diff --git a/src/ApiService/ApiService/QueueProxyHeartbeat.cs b/src/ApiService/ApiService/QueueProxyHeartbeat.cs new file mode 100644 index 000000000..dddfff032 --- /dev/null +++ b/src/ApiService/ApiService/QueueProxyHeartbeat.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.OneFuzz.Service.OneFuzzLib.Orm; + +namespace Microsoft.OneFuzz.Service; + +public class QueueProxyHearbeat +{ + private readonly ILogger _logger; + + private readonly IProxyOperations _proxy; + + public QueueProxyHearbeat(ILoggerFactory loggerFactory, IProxyOperations proxy) + { + _logger = loggerFactory.CreateLogger(); + _proxy = proxy; + } + + [Function("QueueProxyHearbeat")] + public async Task Run([QueueTrigger("myqueue-items", Connection = "AzureWebJobsStorage")] string msg) + { + _logger.LogInformation($"heartbeat: {msg}"); + + var hb = JsonSerializer.Deserialize(msg, EntityConverter.GetJsonSerializerOptions()).EnsureNotNull($"wrong data {msg}"); ; + var newHb = hb with { TimeStamp = DateTimeOffset.UtcNow }; + + var proxy = await _proxy.GetByProxyId(newHb.ProxyId); + + if (proxy == null) + { + _logger.LogWarning($"invalid proxy id: {newHb.ProxyId}"); + return; + } + var newProxy = proxy with { heartbeat = newHb }; + + await _proxy.Replace(newProxy); + + } +} diff --git a/src/ApiService/ApiService/onefuzzlib/ProxyOperations.cs b/src/ApiService/ApiService/onefuzzlib/ProxyOperations.cs new file mode 100644 index 000000000..bd05801d3 --- /dev/null +++ b/src/ApiService/ApiService/onefuzzlib/ProxyOperations.cs @@ -0,0 +1,30 @@ +using ApiService.OneFuzzLib.Orm; +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.OneFuzz.Service; + +public interface IProxyOperations : IOrm +{ + Task GetByProxyId(Guid proxyId); +} +public class ProxyOperations : Orm, IProxyOperations +{ + private readonly ILogger _logger; + + public ProxyOperations(ILoggerFactory loggerFactory, IStorage storage) + : base(storage) + { + _logger = loggerFactory.CreateLogger(); + } + + public async Task GetByProxyId(Guid proxyId) + { + + var data = QueryAsync(filter: $"RowKey eq '{proxyId}'"); + + return await data.FirstOrDefaultAsync(); + } +} diff --git a/src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs b/src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs index 50b6168d5..bef3afa80 100644 --- a/src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs +++ b/src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs @@ -245,6 +245,10 @@ public class EntityConverter else { var value = entity.GetString(fieldName); + if (value == null) + { + return null; + } return JsonSerializer.Deserialize(value, ef.type, options: _options); ; } } diff --git a/src/ApiService/Tests/OrmTest.cs b/src/ApiService/Tests/OrmTest.cs index 5e1e285e7..4f50dfbf6 100644 --- a/src/ApiService/Tests/OrmTest.cs +++ b/src/ApiService/Tests/OrmTest.cs @@ -39,7 +39,8 @@ namespace Tests TestEnum TheEnum, TestFlagEnum TheFlag, [property: JsonPropertyName("a__special__name")] string Renamed, - TestObject TheObject + TestObject TheObject, + TestObject? TestNull ) : EntityBase(); @@ -60,7 +61,8 @@ namespace Tests TheName = "testobject", TheEnum = TestEnum.TheTwo, TheFlag = TestFlagEnum.FlagOne | TestFlagEnum.FlagTwo - }); + }, + null); var tableEntity = converter.ToTableEntity(entity1); Assert.NotNull(tableEntity); @@ -97,6 +99,7 @@ namespace Tests { "the_flag", "flag_one,flag_two"}, { "a__special__name", "renamed"}, { "the_object", "{\"the_name\": \"testName\", \"the_enum\": \"the_one\", \"the_flag\": \"flag_one,flag_two\"}"}, + { "test_null", null}, }; var entity1 = converter.ToRecord(tableEntity); @@ -109,6 +112,8 @@ namespace Tests Assert.Equal(tableEntity.GetDouble("the_float"), entity1.TheFloat); Assert.Equal(TestEnum.TheTwo, entity1.TheEnum); Assert.Equal(tableEntity.GetString("a__special__name"), entity1.Renamed); + Assert.Null(tableEntity.GetString("test_null")); + Assert.Null(entity1.TestNull); Assert.Equal("testName", entity1.TheObject.TheName); Assert.Equal(TestEnum.TheOne, entity1.TheObject.TheEnum);