Expand valid scaleset names (#3045)

Scaleset names are now permitted to be any (valid) strings, instead of only GUIDs. When we generate a scaleset name it is now based upon the pool name; for example the pool `pool` might get a scaleset named `pool-3b24ba211cad4b078655914754485838`.

This should be backwards-compatible since GUIDs are [already serialized to table storage as strings](dddcfa4949/src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs (L190-L191)), so this simply loosens the restrictions placed upon them.

Scaleset IDs now have a strong type in the same way as other IDs; this helps to avoid mixing them up with other strings. Because of this I found one bug in the scaleset search query logic due to Pool ID/VMSS ID confusion. As part of fixing this I've changed the scaleset search query to only return nodes from the table rather than querying Azure to find a list; this seems to be sufficient for the CLI.
This commit is contained in:
George Pollard
2023-05-17 09:58:58 +12:00
committed by GitHub
parent d84b72b5fd
commit 2f478d6c0b
38 changed files with 380 additions and 291 deletions

View File

@ -1329,7 +1329,6 @@ If webhook is set to have Event Grid message format then the payload will look a
"type": "string"
},
"scaleset_id": {
"format": "uuid",
"title": "Scaleset Id",
"type": "string"
}
@ -1369,7 +1368,6 @@ If webhook is set to have Event Grid message format then the payload will look a
"type": "string"
},
"scaleset_id": {
"format": "uuid",
"title": "Scaleset Id",
"type": "string"
}
@ -1429,7 +1427,6 @@ If webhook is set to have Event Grid message format then the payload will look a
"type": "string"
},
"scaleset_id": {
"format": "uuid",
"title": "Scaleset Id",
"type": "string"
}
@ -1487,7 +1484,6 @@ If webhook is set to have Event Grid message format then the payload will look a
"type": "string"
},
"scaleset_id": {
"format": "uuid",
"title": "Scaleset Id",
"type": "string"
},
@ -2597,7 +2593,7 @@ If webhook is set to have Event Grid message format then the payload will look a
"image": "Canonical:0001-com-ubuntu-server-focal:20_04-lts:latest",
"pool_name": "example",
"region": "eastus",
"scaleset_id": "00000000-0000-0000-0000-000000000000",
"scaleset_id": "example-000",
"size": 10,
"vm_sku": "Standard_D2s_v3"
}
@ -2621,7 +2617,6 @@ If webhook is set to have Event Grid message format then the payload will look a
"type": "string"
},
"scaleset_id": {
"format": "uuid",
"title": "Scaleset Id",
"type": "string"
},
@ -2654,7 +2649,7 @@ If webhook is set to have Event Grid message format then the payload will look a
```json
{
"pool_name": "example",
"scaleset_id": "00000000-0000-0000-0000-000000000000"
"scaleset_id": "example-000"
}
```
@ -2668,7 +2663,6 @@ If webhook is set to have Event Grid message format then the payload will look a
"type": "string"
},
"scaleset_id": {
"format": "uuid",
"title": "Scaleset Id",
"type": "string"
}
@ -2695,7 +2689,7 @@ If webhook is set to have Event Grid message format then the payload will look a
]
},
"pool_name": "example",
"scaleset_id": "00000000-0000-0000-0000-000000000000"
"scaleset_id": "example-000"
}
```
@ -2764,7 +2758,6 @@ If webhook is set to have Event Grid message format then the payload will look a
"type": "string"
},
"scaleset_id": {
"format": "uuid",
"title": "Scaleset Id",
"type": "string"
}
@ -2786,7 +2779,7 @@ If webhook is set to have Event Grid message format then the payload will look a
```json
{
"pool_name": "example",
"scaleset_id": "00000000-0000-0000-0000-000000000000",
"scaleset_id": "example-000",
"size": 0
}
```
@ -2801,7 +2794,6 @@ If webhook is set to have Event Grid message format then the payload will look a
"type": "string"
},
"scaleset_id": {
"format": "uuid",
"title": "Scaleset Id",
"type": "string"
},
@ -2827,7 +2819,7 @@ If webhook is set to have Event Grid message format then the payload will look a
```json
{
"pool_name": "example",
"scaleset_id": "00000000-0000-0000-0000-000000000000",
"scaleset_id": "example-000",
"state": "init"
}
```
@ -2857,7 +2849,6 @@ If webhook is set to have Event Grid message format then the payload will look a
"type": "string"
},
"scaleset_id": {
"format": "uuid",
"title": "Scaleset Id",
"type": "string"
},
@ -5658,7 +5649,6 @@ If webhook is set to have Event Grid message format then the payload will look a
"type": "string"
},
"scaleset_id": {
"format": "uuid",
"title": "Scaleset Id",
"type": "string"
}
@ -5682,7 +5672,6 @@ If webhook is set to have Event Grid message format then the payload will look a
"type": "string"
},
"scaleset_id": {
"format": "uuid",
"title": "Scaleset Id",
"type": "string"
}
@ -5709,7 +5698,6 @@ If webhook is set to have Event Grid message format then the payload will look a
"type": "string"
},
"scaleset_id": {
"format": "uuid",
"title": "Scaleset Id",
"type": "string"
}
@ -5733,7 +5721,6 @@ If webhook is set to have Event Grid message format then the payload will look a
"type": "string"
},
"scaleset_id": {
"format": "uuid",
"title": "Scaleset Id",
"type": "string"
},
@ -5926,7 +5913,6 @@ If webhook is set to have Event Grid message format then the payload will look a
"type": "string"
},
"scaleset_id": {
"format": "uuid",
"title": "Scaleset Id",
"type": "string"
},
@ -5957,7 +5943,6 @@ If webhook is set to have Event Grid message format then the payload will look a
"type": "string"
},
"scaleset_id": {
"format": "uuid",
"title": "Scaleset Id",
"type": "string"
}
@ -5979,7 +5964,6 @@ If webhook is set to have Event Grid message format then the payload will look a
"type": "string"
},
"scaleset_id": {
"format": "uuid",
"title": "Scaleset Id",
"type": "string"
}
@ -5999,7 +5983,6 @@ If webhook is set to have Event Grid message format then the payload will look a
"type": "string"
},
"scaleset_id": {
"format": "uuid",
"title": "Scaleset Id",
"type": "string"
},
@ -6023,7 +6006,6 @@ If webhook is set to have Event Grid message format then the payload will look a
"type": "string"
},
"scaleset_id": {
"format": "uuid",
"title": "Scaleset Id",
"type": "string"
},

View File

@ -52,7 +52,7 @@ public class Proxy {
var proxyGet = request.OkV;
switch ((proxyGet.ScalesetId, proxyGet.MachineId, proxyGet.DstPort)) {
case (Guid scalesetId, Guid machineId, int dstPort):
case (ScalesetId scalesetId, Guid machineId, int dstPort):
var scaleset = await _context.ScalesetOperations.GetById(scalesetId);
if (!scaleset.IsOk) {
return await _context.RequestHandling.NotOk(req, scaleset.ErrorV, "ProxyGet");

View File

@ -118,7 +118,7 @@ public class Scaleset {
}
var scaleset = new Service.Scaleset(
ScalesetId: Guid.NewGuid(),
ScalesetId: Service.Scaleset.GenerateNewScalesetId(create.PoolName),
State: ScalesetState.Init,
NeedsConfigUpdate: false,
Auth: await Auth.BuildAuth(_log),
@ -206,7 +206,7 @@ public class Scaleset {
}
var search = request.OkV;
if (search.ScalesetId is Guid id) {
if (search.ScalesetId is ScalesetId id) {
var scalesetResult = await _context.ScalesetOperations.GetById(id);
if (!scalesetResult.IsOk) {
return await _context.RequestHandling.NotOk(req, scalesetResult.ErrorV, "ScalesetSearch");

View File

@ -185,7 +185,7 @@ public record EventPing(
[EventType(EventType.ScalesetCreated)]
public record EventScalesetCreated(
Guid ScalesetId,
ScalesetId ScalesetId,
PoolName PoolName,
string VmSku,
string Image,
@ -195,7 +195,7 @@ public record EventScalesetCreated(
[EventType(EventType.ScalesetFailed)]
public sealed record EventScalesetFailed(
Guid ScalesetId,
ScalesetId ScalesetId,
PoolName PoolName,
Error Error
) : BaseEvent();
@ -203,7 +203,7 @@ public sealed record EventScalesetFailed(
[EventType(EventType.ScalesetDeleted)]
public record EventScalesetDeleted(
Guid ScalesetId,
ScalesetId ScalesetId,
PoolName PoolName
) : BaseEvent();
@ -211,7 +211,7 @@ public record EventScalesetDeleted(
[EventType(EventType.ScalesetResizeScheduled)]
public record EventScalesetResizeScheduled(
Guid ScalesetId,
ScalesetId ScalesetId,
PoolName PoolName,
long size
) : BaseEvent();
@ -267,14 +267,14 @@ public record EventProxyStateUpdated(
[EventType(EventType.NodeCreated)]
public record EventNodeCreated(
Guid MachineId,
Guid? ScalesetId,
ScalesetId? ScalesetId,
PoolName PoolName
) : BaseEvent();
[EventType(EventType.NodeHeartbeat)]
public record EventNodeHeartbeat(
Guid MachineId,
Guid? ScalesetId,
ScalesetId? ScalesetId,
PoolName PoolName,
NodeState state
) : BaseEvent();
@ -283,7 +283,7 @@ public record EventNodeHeartbeat(
[EventType(EventType.NodeDeleted)]
public record EventNodeDeleted(
Guid MachineId,
Guid? ScalesetId,
ScalesetId? ScalesetId,
PoolName PoolName,
NodeState? MachineState
) : BaseEvent();
@ -291,7 +291,7 @@ public record EventNodeDeleted(
[EventType(EventType.ScalesetStateUpdated)]
public record EventScalesetStateUpdated(
Guid ScalesetId,
ScalesetId ScalesetId,
PoolName PoolName,
ScalesetState State
) : BaseEvent();
@ -299,7 +299,7 @@ public record EventScalesetStateUpdated(
[EventType(EventType.NodeStateUpdated)]
public record EventNodeStateUpdated(
Guid MachineId,
Guid? ScalesetId,
ScalesetId? ScalesetId,
PoolName PoolName,
NodeState state
) : BaseEvent();

View File

@ -1,6 +1,7 @@
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.OneFuzz.Service.OneFuzzLib.Orm;
using Endpoint = System.String;
@ -107,7 +108,7 @@ public record Node
// a string internally.
string? InstanceId = null,
Guid? ScalesetId = null,
ScalesetId? ScalesetId = null,
bool ReimageRequested = false,
bool DeleteRequested = false,
@ -132,7 +133,7 @@ public record ProxyForward
(
[PartitionKey] Region Region,
[RowKey] long Port,
Guid ScalesetId,
ScalesetId ScalesetId,
Guid MachineId,
Guid? ProxyId,
long DstPort,
@ -263,7 +264,7 @@ public record TaskEventSummary(
public record NodeAssignment(
Guid NodeId,
Guid? ScalesetId,
ScalesetId? ScalesetId,
NodeTaskState State
);
@ -392,7 +393,7 @@ public record InstanceConfig
}
public record AutoScale(
[PartitionKey, RowKey] Guid ScalesetId,
[PartitionKey, RowKey] ScalesetId ScalesetId,
long Min,
long Max,
long Default,
@ -402,15 +403,10 @@ public record AutoScale(
long ScaleInCooldown
) : EntityBase;
public record ScalesetNodeState(
Guid MachineId,
string InstanceId,
NodeState? State
);
public record Scaleset(
public partial record Scaleset(
[PartitionKey] PoolName PoolName,
[RowKey] Guid ScalesetId,
[RowKey] ScalesetId ScalesetId,
ScalesetState State,
string VmSku,
ImageReference Image,
@ -425,7 +421,31 @@ public record Scaleset(
Guid? ClientId = null,
Guid? ClientObjectId = null
// 'Nodes' removed when porting from Python: only used in search response
) : StatefulEntityBase<ScalesetState>(State);
) : StatefulEntityBase<ScalesetState>(State) {
[GeneratedRegex(@"[^a-zA-Z0-9\-]+")]
private static partial Regex InvalidCharacterRegex();
public static ScalesetId GenerateNewScalesetId(PoolName poolName)
=> GenerateNewScalesetIdUsingGuid(poolName, Guid.NewGuid());
public static ScalesetId GenerateNewScalesetIdUsingGuid(PoolName poolName, Guid guid) {
// poolnames permit underscores but not scaleset names; use hyphen instead:
var name = poolName.ToString().Replace("_", "-");
// since poolnames are not actually validated, take only the valid characters:
name = InvalidCharacterRegex().Replace(name, "");
// trim off any starting and ending dashes:
name = name.Trim('-');
// this should now be a valid name; generate a unique suffix:
// max length is 64; length of Guid in "N" format is 32, -1 for the hyphen
name = name[..Math.Min(64 - 32 - 1, name.Length)] + "-" + guid.ToString("N");
return ScalesetId.Parse(name);
}
}
public record Notification(
[PartitionKey] Guid NotificationId,
@ -733,7 +753,7 @@ public record WorkSetSummary(
);
public record ScalesetSummary(
Guid ScalesetId,
ScalesetId ScalesetId,
ScalesetState State
);

View File

@ -35,7 +35,7 @@ public record NodeUpdate(
public record NodeSearch(
Guid? MachineId = null,
List<NodeState>? State = null,
Guid? ScalesetId = null,
ScalesetId? ScalesetId = null,
PoolName? PoolName = null
) : BaseRequest;
@ -172,20 +172,20 @@ public record ReproCreate(
) : BaseRequest;
public record ProxyGet(
Guid? ScalesetId,
ScalesetId? ScalesetId,
Guid? MachineId,
int? DstPort
) : BaseRequest;
public record ProxyCreate(
[property: Required] Guid ScalesetId,
[property: Required] ScalesetId ScalesetId,
[property: Required] Guid MachineId,
[property: Required] int DstPort,
[property: Required] int Duration
) : BaseRequest;
public record ProxyDelete(
[property: Required] Guid ScalesetId,
[property: Required] ScalesetId ScalesetId,
[property: Required] Guid MachineId,
int? DstPort
) : BaseRequest;
@ -217,18 +217,18 @@ public record AutoScaleOptions(
);
public record ScalesetSearch(
Guid? ScalesetId = null,
ScalesetId? ScalesetId = null,
List<ScalesetState>? State = null,
bool IncludeAuth = false
) : BaseRequest;
public record ScalesetStop(
[property: Required] Guid ScalesetId,
[property: Required] ScalesetId ScalesetId,
[property: Required] bool Now
) : BaseRequest;
public record ScalesetUpdate(
[property: Required] Guid ScalesetId,
[property: Required] ScalesetId ScalesetId,
[property: Range(1, long.MaxValue)]
long? Size
) : BaseRequest;
@ -313,7 +313,7 @@ public record AgentRegistrationGet(
public record AgentRegistrationPost(
[property: Required] PoolName PoolName,
Guid? ScalesetId,
ScalesetId? ScalesetId,
[property: Required] Guid MachineId,
Os? Os,
string? MachineName,

View File

@ -30,7 +30,7 @@ public record NodeSearchResult(
DateTimeOffset? Heartbeat,
DateTimeOffset? InitializedAt,
NodeState State,
Guid? ScalesetId,
ScalesetId? ScalesetId,
bool ReimageRequested,
bool DeleteRequested,
bool DebugKeepNode
@ -123,7 +123,7 @@ public record PoolGetResult(
public record ScalesetResponse(
PoolName PoolName,
Guid ScalesetId,
ScalesetId ScalesetId,
ScalesetState State,
Authentication? Auth,
string VmSku,
@ -159,6 +159,12 @@ public record ScalesetResponse(
Nodes: null);
}
public record ScalesetNodeState(
Guid MachineId,
string? InstanceId,
NodeState? State
);
public record ConfigResponse(
string? Authority,
string? ClientId,

View File

@ -16,9 +16,15 @@ static partial class Check {
public static bool IsAlnumDash(string input) => IsAlnumDashRegex().IsMatch(input);
// Permits 1-64 characters: alphanumeric, underscore, period, or dash.
[GeneratedRegex("\\A[._a-zA-Z0-9\\-]{1,64}\\z")]
private static partial Regex IsNameLikeRegex();
public static bool IsNameLike(string input) => IsNameLikeRegex().IsMatch(input);
// Cannot start with underscore (or dash) or end with period or dash.
[GeneratedRegex(@"\A(?![_\-])[._a-zA-Z0-9\-]{1,64}(?<![.\-])\z")]
private static partial Regex ResourceNameRegex();
public static bool IsResourceName(string input) => ResourceNameRegex().IsMatch(input);
// The same as ResourceNameRegex but underscore and period are not permitted.
[GeneratedRegex(@"\A(?!-)[a-zA-Z0-9\-]{1,64}(?<!-)\z")]
private static partial Regex VmssNameRegex();
public static bool IsVmssName(string input) => VmssNameRegex().IsMatch(input);
// This regex is based upon DNS labels but more restricted.
// It is used for many different Storage resources.
@ -31,13 +37,16 @@ static partial class Check {
public static bool IsStorageDnsLabel(string input) => StorageDnsLabelRegex().IsMatch(input);
}
public interface IValidatedString<T> where T : IValidatedString<T> {
public static abstract T Parse(string input);
public interface IValidatedString {
public static abstract bool IsValid(string input);
public static abstract string Requirements { get; }
public string String { get; }
}
public interface IValidatedString<T> : IValidatedString where T : IValidatedString<T> {
public static abstract T Parse(string input);
}
public abstract record ValidatedStringBase<T> where T : IValidatedString<T> {
protected ValidatedStringBase(string value) {
if (!T.IsValid(value)) {
@ -109,3 +118,11 @@ public sealed record Container : ValidatedStringBase<Container>, IValidatedStrin
public static bool IsValid(string input) => Check.IsStorageDnsLabel(input);
public static string Requirements => "Container name must be 3-63 lowercase letters, numbers, or non-consecutive hyphens (see: https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules#microsoftstorage)";
}
[JsonConverter(typeof(ValidatedStringConverter<ScalesetId>))]
public sealed record ScalesetId : ValidatedStringBase<ScalesetId>, IValidatedString<ScalesetId> {
private ScalesetId(string value) : base(value) { }
public static ScalesetId Parse(string input) => new(input);
public static bool IsValid(string input) => Check.IsVmssName(input);
public static string Requirements => "Virtual machine scaleset names must be 1-64 numbers, letters, or dashes (not at start or end).";
}

View File

@ -159,7 +159,10 @@ namespace ApiService.TestHooks {
var query = UriExtension.GetQueryComponents(req.Url);
Guid? poolId = UriExtension.GetGuid("poolId", query);
Guid? scaleSetId = UriExtension.GetGuid("scaleSetId", query);
var scaleSetId = UriExtension.GetString("scaleSetId", query)
is string scalesetId
? ScalesetId.Parse(scalesetId)
: null;
List<NodeState>? states = default;
if (query.TryGetValue("states", out var value)) {
@ -196,7 +199,7 @@ namespace ApiService.TestHooks {
_log.Info($"reimage long lived nodes");
var query = UriExtension.GetQueryComponents(req.Url);
var r = _nodeOps.ReimageLongLivedNodes(Guid.Parse(query["scaleSetId"]));
var r = _nodeOps.ReimageLongLivedNodes(ScalesetId.Parse(query["scaleSetId"]));
var resp = req.CreateResponse(HttpStatusCode.OK);
await resp.WriteAsJsonAsync(r);
return resp;
@ -213,9 +216,9 @@ namespace ApiService.TestHooks {
var poolName = PoolName.Parse(query["poolName"]);
Guid machineId = Guid.Parse(query["machineId"]);
Guid? scaleSetId = default;
ScalesetId? scaleSetId = null;
if (query.TryGetValue("scaleSetId", out var value)) {
scaleSetId = Guid.Parse(value);
scaleSetId = ScalesetId.Parse(value);
}
string version = query["version"];
@ -236,10 +239,10 @@ namespace ApiService.TestHooks {
var query = UriExtension.GetQueryComponents(req.Url);
Guid scaleSetId = Guid.Parse(query["scaleSetId"]);
var scaleSetId = ScalesetId.Parse(query["scaleSetId"]);
TimeSpan timeSpan = TimeSpan.Parse(query["timeSpan"]);
var nodes = await (_nodeOps.GetDeadNodes(scaleSetId, timeSpan).ToListAsync());
var nodes = await _nodeOps.GetDeadNodes(scaleSetId, timeSpan).ToListAsync();
var json = JsonSerializer.Serialize(nodes, EntityConverter.GetJsonSerializerOptions());
var resp = req.CreateResponse(HttpStatusCode.OK);
await resp.WriteStringAsync(json);

View File

@ -27,7 +27,7 @@ namespace ApiService.TestHooks {
var query = UriExtension.GetQueryComponents(req.Url);
var poolRes = _proxyForward.SearchForward(
UriExtension.GetGuid("scaleSetId", query),
UriExtension.GetString("scaleSetId", query) is string scalesetId ? ScalesetId.Parse(scalesetId) : null,
UriExtension.GetString("region", query) is string region ? Region.Parse(region) : null,
UriExtension.GetGuid("machineId", query),
UriExtension.GetGuid("proxyId", query),

View File

@ -27,8 +27,8 @@ namespace ApiService.TestHooks {
public async Task<HttpResponseData> ListInstanceIds([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "testhooks/vmssOperations/listInstanceIds")] HttpRequestData req) {
_log.Info($"list instance ids");
var query = UriExtension.GetQueryComponents(req.Url);
var name = UriExtension.GetGuid("name", query) ?? throw new Exception("name must be set");
var ids = await _vmssOps.ListInstanceIds(name);
var name = UriExtension.GetString("name", query) ?? throw new Exception("name must be set");
var ids = await _vmssOps.ListInstanceIds(ScalesetId.Parse(name));
var json = JsonSerializer.Serialize(ids, EntityConverter.GetJsonSerializerOptions());
var resp = req.CreateResponse(HttpStatusCode.OK);
@ -40,9 +40,9 @@ namespace ApiService.TestHooks {
public async Task<HttpResponseData> GetInstanceId([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "testhooks/vmssOperations/getInstanceId")] HttpRequestData req) {
_log.Info($"list instance ids");
var query = UriExtension.GetQueryComponents(req.Url);
var name = UriExtension.GetGuid("name", query) ?? throw new Exception("name must be set");
var name = UriExtension.GetString("name", query) ?? throw new Exception("name must be set");
var vmId = UriExtension.GetGuid("vmId", query) ?? throw new Exception("vmId must be set");
var id = await _vmssOps.GetInstanceId(name, vmId);
var id = await _vmssOps.GetInstanceId(ScalesetId.Parse(name), vmId);
var json = JsonSerializer.Serialize(id, EntityConverter.GetJsonSerializerOptions());
var resp = req.CreateResponse(HttpStatusCode.OK);
@ -54,9 +54,9 @@ namespace ApiService.TestHooks {
public async Task<HttpResponseData> UpdateScaleInProtection([HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "testhooks/vmssOperations/updateScaleInProtection")] HttpRequestData req) {
_log.Info($"list instance ids");
var query = UriExtension.GetQueryComponents(req.Url);
var name = UriExtension.GetGuid("name", query) ?? throw new Exception("name must be set");
var name = UriExtension.GetString("name", query) ?? throw new Exception("name must be set");
var instanceId = UriExtension.GetString("instanceId", query) ?? throw new Exception("instanceId must be set");
var scalesetResult = await _scalesetOperations.GetById(name);
var scalesetResult = await _scalesetOperations.GetById(ScalesetId.Parse(name));
if (!scalesetResult.IsOk) {
throw new Exception("invalid scaleset name");
}

View File

@ -13,7 +13,7 @@ public interface IAutoScaleOperations {
public Async.Task<ResultVoid<(HttpStatusCode Status, string Reason)>> Insert(AutoScale autoScale);
public Async.Task<AutoScale?> GetSettingsForScaleset(Guid scalesetId);
public Async.Task<AutoScale?> GetSettingsForScaleset(ScalesetId scalesetId);
AutoscaleProfile CreateAutoScaleProfile(
string queueUri,
@ -26,16 +26,16 @@ public interface IAutoScaleOperations {
double scaleInCooldownMinutes);
AutoscaleProfile DefaultAutoScaleProfile(string queueUri, long scaleSetSize);
Async.Task<OneFuzzResultVoid> AddAutoScaleToVmss(Guid vmss, AutoscaleProfile autoScaleProfile);
Async.Task<OneFuzzResultVoid> AddAutoScaleToVmss(ScalesetId vmss, AutoscaleProfile autoScaleProfile);
OneFuzzResult<AutoscaleSettingResource?> GetAutoscaleSettings(Guid vmss);
OneFuzzResult<AutoscaleSettingResource?> GetAutoscaleSettings(ScalesetId vmss);
Async.Task<OneFuzzResultVoid> UpdateAutoscale(AutoscaleSettingData autoscale);
Async.Task<OneFuzzResult<AutoscaleProfile>> GetAutoScaleProfile(Guid scalesetId);
Async.Task<OneFuzzResult<AutoscaleProfile>> GetAutoScaleProfile(ScalesetId scalesetId);
Async.Task<AutoScale> Update(
Guid scalesetId,
ScalesetId scalesetId,
long minAmount,
long maxAmount,
long defaultAmount,
@ -54,7 +54,7 @@ public class AutoScaleOperations : Orm<AutoScale>, IAutoScaleOperations {
}
public async Async.Task<AutoScale> Create(
Guid scalesetId,
ScalesetId scalesetId,
long minAmount,
long maxAmount,
long defaultAmount,
@ -81,7 +81,7 @@ public class AutoScaleOperations : Orm<AutoScale>, IAutoScaleOperations {
return entry;
}
public async Async.Task<AutoScale?> GetSettingsForScaleset(Guid scalesetId) {
public async Async.Task<AutoScale?> GetSettingsForScaleset(ScalesetId scalesetId) {
try {
var autoscale = await GetEntityAsync(scalesetId.ToString(), scalesetId.ToString());
return autoscale;
@ -91,7 +91,7 @@ public class AutoScaleOperations : Orm<AutoScale>, IAutoScaleOperations {
}
}
public async Async.Task<OneFuzzResult<AutoscaleProfile>> GetAutoScaleProfile(Guid scalesetId) {
public async Async.Task<OneFuzzResult<AutoscaleProfile>> GetAutoScaleProfile(ScalesetId scalesetId) {
_logTracer.Info($"getting scaleset for existing auto-scale resources {scalesetId:Tag:ScalesetId}");
var settings = _context.Creds.GetResourceGroupResource().GetAutoscaleSettings();
if (settings is null) {
@ -114,7 +114,7 @@ public class AutoScaleOperations : Orm<AutoScale>, IAutoScaleOperations {
return OneFuzzResult<AutoscaleProfile>.Error(ErrorCode.INVALID_CONFIGURATION, $"could not find auto-scale settings for scaleset {scalesetId}");
}
public async Async.Task<OneFuzzResultVoid> AddAutoScaleToVmss(Guid vmss, AutoscaleProfile autoScaleProfile) {
public async Async.Task<OneFuzzResultVoid> AddAutoScaleToVmss(ScalesetId vmss, AutoscaleProfile autoScaleProfile) {
_logTracer.Info($"Checking scaleset {vmss:Tag:ScalesetId} for existing auto scale resource");
var existingAutoScaleResource = GetAutoscaleSettings(vmss);
@ -141,7 +141,7 @@ public class AutoScaleOperations : Orm<AutoScale>, IAutoScaleOperations {
return OneFuzzResultVoid.Ok;
}
private async Async.Task<OneFuzzResult<AutoscaleSettingResource>> CreateAutoScaleResourceFor(Guid resourceId, Region location, AutoscaleProfile profile) {
private async Async.Task<OneFuzzResult<AutoscaleSettingResource>> CreateAutoScaleResourceFor(ScalesetId resourceId, Region location, AutoscaleProfile profile) {
_logTracer.Info($"Creating auto-scale resource for: {resourceId:Tag:AutoscaleResourceId}");
var resourceGroup = _context.Creds.GetBaseResourceGroup();
@ -287,7 +287,7 @@ public class AutoScaleOperations : Orm<AutoScale>, IAutoScaleOperations {
}
}
public OneFuzzResult<AutoscaleSettingResource?> GetAutoscaleSettings(Guid vmss) {
public OneFuzzResult<AutoscaleSettingResource?> GetAutoscaleSettings(ScalesetId vmss) {
_logTracer.Info($"Checking scaleset {vmss:Tag:ScalesetId} for existing auto scale resource");
try {
var autoscale = _context.Creds.GetResourceGroupResource().GetAutoscaleSettings()
@ -354,7 +354,7 @@ public class AutoScaleOperations : Orm<AutoScale>, IAutoScaleOperations {
}
public async Async.Task<AutoScale> Update(
Guid scalesetId,
ScalesetId scalesetId,
long minAmount,
long maxAmount,
long defaultAmount,

View File

@ -24,16 +24,15 @@ public interface IIpOperations {
public Async.Task DeleteIp(string resourceGroup, string name);
public Async.Task<string?> GetScalesetInstanceIp(Guid scalesetId, Guid machineId);
public Async.Task<string?> GetScalesetInstanceIp(ScalesetId scalesetId, Guid machineId);
public Async.Task CreateIp(string resourceGroup, string name, Region region);
}
public class IpOperations : IIpOperations {
private ILogTracer _logTracer;
private IOnefuzzContext _context;
private readonly ILogTracer _logTracer;
private readonly IOnefuzzContext _context;
private readonly NetworkInterfaceQuery _networkInterfaceQuery;
public IpOperations(ILogTracer log, IOnefuzzContext context) {
@ -86,7 +85,7 @@ public class IpOperations : IIpOperations {
}
}
public async Task<string?> GetScalesetInstanceIp(Guid scalesetId, Guid machineId) {
public async Task<string?> GetScalesetInstanceIp(ScalesetId scalesetId, Guid machineId) {
var instance = await _context.VmssOperations.GetInstanceId(scalesetId, machineId);
if (!instance.IsOk) {
_logTracer.Verbose($"failed to get vmss {scalesetId:Tag:ScalesetId} for instance id {machineId:Tag:MachineId} due to {instance.ErrorV:Tag:Error}");
@ -253,7 +252,7 @@ public class IpOperations : IIpOperations {
}
public async Task<List<string>> ListInstancePrivateIps(Guid scalesetId, string instanceId) {
public async Task<List<string>> ListInstancePrivateIps(ScalesetId scalesetId, string instanceId) {
var token = _context.Creds.GetIdentity().GetToken(
new TokenRequestContext(
new[] { $"https://management.azure.com" }));

View File

@ -27,7 +27,7 @@ public interface INodeOperations : IStatefulOrm<Node, NodeState> {
Async.Task<Node> ToReimage(Node node, bool done = false);
Async.Task SendStopIfFree(Node node);
IAsyncEnumerable<Node> SearchStates(Guid? poolId = default,
Guid? scalesetId = default,
ScalesetId? scalesetId = default,
IEnumerable<NodeState>? states = default,
PoolName? poolName = default,
bool excludeUpdateScheduled = false,
@ -35,18 +35,18 @@ public interface INodeOperations : IStatefulOrm<Node, NodeState> {
Async.Task Delete(Node node, string reason);
Async.Task ReimageLongLivedNodes(Guid scaleSetId);
Async.Task ReimageLongLivedNodes(ScalesetId scaleSetId);
Async.Task<Node?> Create(
Guid poolId,
PoolName poolName,
Guid machineId,
string? instanceId,
Guid? scaleSetId,
ScalesetId? scaleSetId,
string version,
bool isNew = false);
IAsyncEnumerable<Node> GetDeadNodes(Guid scaleSetId, TimeSpan expirationPeriod);
IAsyncEnumerable<Node> GetDeadNodes(ScalesetId scaleSetId, TimeSpan expirationPeriod);
Async.Task MarkTasksStoppedEarly(Node node, Error? error);
static readonly TimeSpan NODE_EXPIRATION_TIME = TimeSpan.FromHours(1.0);
@ -88,7 +88,7 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
}
public async Task<OneFuzzResult<Node>> AcquireScaleInProtection(Node node) {
if (node.ScalesetId is Guid scalesetId &&
if (node.ScalesetId is ScalesetId scalesetId &&
await TryGetNodeInfo(node) is NodeInfo nodeInfo) {
_logTracer.Info($"Setting scale-in protection on node {node.MachineId:Tag:MachineId}");
@ -118,7 +118,7 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
public async Task<OneFuzzResultVoid> ReleaseScaleInProtection(Node node) {
if (!node.DebugKeepNode &&
node.ScalesetId is Guid scalesetId &&
node.ScalesetId is ScalesetId scalesetId &&
await TryGetNodeInfo(node) is NodeInfo nodeInfo) {
_logTracer.Info($"Removing scale-in protection on node {node.MachineId:Tag:MachineId}");
@ -150,7 +150,7 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
return null;
}
var scalesetResult = await _context.ScalesetOperations.GetById(scalesetId.Value);
var scalesetResult = await _context.ScalesetOperations.GetById(scalesetId);
if (!scalesetResult.IsOk || scalesetResult.OkV == null) {
return null;
}
@ -205,8 +205,8 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
return CanProcessNewWorkResponse.NotAllowed("node is scheduled to shrink");
}
if (node.ScalesetId != null) {
var scalesetResult = await _context.ScalesetOperations.GetById(node.ScalesetId.Value);
if (node.ScalesetId is not null) {
var scalesetResult = await _context.ScalesetOperations.GetById(node.ScalesetId);
if (!scalesetResult.IsOk) {
return CanProcessNewWorkResponse.NotAllowed("invalid scaleset");
}
@ -235,7 +235,7 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
/// This helps keep nodes on scalesets that use `latest` OS image SKUs
/// reasonably up-to-date with OS patches without disrupting running
/// fuzzing tasks with patch reboot cycles.
public async Async.Task ReimageLongLivedNodes(Guid scaleSetId) {
public async Async.Task ReimageLongLivedNodes(ScalesetId scaleSetId) {
var timeFilter = Query.OlderThan("initialized_at", DateTimeOffset.UtcNow - INodeOperations.NODE_REIMAGE_TIME);
await foreach (var node in QueryAsync(Query.And(Query.CreateQueryFilter($"scaleset_id eq {scaleSetId}"), timeFilter))) {
@ -270,7 +270,7 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
public static string SearchOutdatedQuery(
string oneFuzzVersion,
Guid? poolId = null,
Guid? scalesetId = null,
ScalesetId? scalesetId = null,
IEnumerable<NodeState>? states = null,
PoolName? poolName = null,
bool excludeUpdateScheduled = false,
@ -283,7 +283,7 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
}
if (poolName is not null) {
queryParts.Add(Query.CreateQueryFilter($"(pool_name eq {poolName.String})"));
queryParts.Add(Query.CreateQueryFilter($"(pool_name eq {poolName})"));
}
if (scalesetId is not null) {
@ -310,7 +310,7 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
IAsyncEnumerable<Node> SearchOutdated(
Guid? poolId = null,
Guid? scalesetId = null,
ScalesetId? scalesetId = null,
IEnumerable<NodeState>? states = null,
PoolName? poolName = null,
bool excludeUpdateScheduled = false,
@ -366,11 +366,11 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
return updatedNode;
}
public IAsyncEnumerable<Node> GetDeadNodes(Guid scaleSetId, TimeSpan expirationPeriod) {
public IAsyncEnumerable<Node> GetDeadNodes(ScalesetId scaleSetId, TimeSpan expirationPeriod) {
var minDate = DateTimeOffset.UtcNow - expirationPeriod;
var filter = $"heartbeat lt datetime'{minDate.ToString("o")}' or Timestamp lt datetime'{minDate.ToString("o")}'";
var query = Query.And(filter, $"scaleset_id eq '{scaleSetId}'");
var query = Query.And(filter, Query.CreateQueryFilter($"scaleset_id eq {scaleSetId}"));
return QueryAsync(query);
}
@ -380,7 +380,7 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
PoolName poolName,
Guid machineId,
string? instanceId,
Guid? scaleSetId,
ScalesetId? scaleSetId,
string version,
bool isNew = false) {
@ -495,7 +495,7 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
}
public async Task<bool> CouldShrinkScaleset(Node node) {
if (node.ScalesetId is Guid scalesetId) {
if (node.ScalesetId is ScalesetId scalesetId) {
var queue = new ShrinkQueue(scalesetId, _context.Queue, _logTracer);
if (await queue.ShouldShrink()) {
return true;
@ -536,7 +536,7 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
public static string SearchStatesQuery(
Guid? poolId = default,
Guid? scaleSetId = default,
ScalesetId? scaleSetId = default,
IEnumerable<NodeState>? states = default,
PoolName? poolName = default,
int? numResults = default) {
@ -544,15 +544,15 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
List<string> queryParts = new();
if (poolId is not null) {
queryParts.Add($"(pool_id eq '{poolId}')");
queryParts.Add(Query.CreateQueryFilter($"(pool_id eq {poolId})"));
}
if (poolName is not null) {
queryParts.Add($"(PartitionKey eq '{poolName.String}')");
queryParts.Add(Query.CreateQueryFilter($"(PartitionKey eq {poolName})"));
}
if (scaleSetId is not null) {
queryParts.Add($"(scaleset_id eq '{scaleSetId}')");
queryParts.Add(Query.CreateQueryFilter($"(scaleset_id eq {scaleSetId})"));
}
if (states is not null) {
@ -566,7 +566,7 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
public IAsyncEnumerable<Node> SearchStates(
Guid? poolId = default,
Guid? scalesetId = default,
ScalesetId? scalesetId = default,
IEnumerable<NodeState>? states = default,
PoolName? poolName = default,
bool excludeUpdateScheduled = false,

View File

@ -5,10 +5,10 @@ namespace Microsoft.OneFuzz.Service;
public interface IProxyForwardOperations : IOrm<ProxyForward> {
IAsyncEnumerable<ProxyForward> SearchForward(Guid? scalesetId = null, Region? region = null, Guid? machineId = null, Guid? proxyId = null, int? dstPort = null);
IAsyncEnumerable<ProxyForward> SearchForward(ScalesetId? scalesetId = null, Region? region = null, Guid? machineId = null, Guid? proxyId = null, int? dstPort = null);
Forward ToForward(ProxyForward proxyForward);
Task<OneFuzzResult<ProxyForward>> UpdateOrCreate(Region region, Guid scalesetId, Guid machineId, int dstPort, int duration);
Task<HashSet<Region>> RemoveForward(Guid scalesetId, Guid? machineId = null, int? dstPort = null, Guid? proxyId = null);
Task<OneFuzzResult<ProxyForward>> UpdateOrCreate(Region region, ScalesetId scalesetId, Guid machineId, int dstPort, int duration);
Task<HashSet<Region>> RemoveForward(ScalesetId scalesetId, Guid? machineId = null, int? dstPort = null, Guid? proxyId = null);
}
@ -20,12 +20,12 @@ public class ProxyForwardOperations : Orm<ProxyForward>, IProxyForwardOperations
}
public IAsyncEnumerable<ProxyForward> SearchForward(Guid? scalesetId = null, Region? region = null, Guid? machineId = null, Guid? proxyId = null, int? dstPort = null) {
public IAsyncEnumerable<ProxyForward> SearchForward(ScalesetId? scalesetId = null, Region? region = null, Guid? machineId = null, Guid? proxyId = null, int? dstPort = null) {
var conditions =
new[] {
scalesetId is not null ? Query.CreateQueryFilter($"scaleset_id eq {scalesetId}") : null,
region is not null ? Query.CreateQueryFilter($"PartitionKey eq {region.String}") : null ,
region is not null ? Query.CreateQueryFilter($"PartitionKey eq {region}") : null ,
machineId is not null ? Query.CreateQueryFilter($"machine_id eq {machineId}") : null ,
proxyId is not null ? Query.CreateQueryFilter($"proxy_id eq {proxyId}") : null ,
dstPort is not null ? Query.CreateQueryFilter($"dst_port eq {dstPort}") : null ,
@ -39,7 +39,7 @@ public class ProxyForwardOperations : Orm<ProxyForward>, IProxyForwardOperations
return new Forward(proxyForward.Port, proxyForward.DstPort, proxyForward.DstIp);
}
public async Task<OneFuzzResult<ProxyForward>> UpdateOrCreate(Region region, Guid scalesetId, Guid machineId, int dstPort, int duration) {
public async Task<OneFuzzResult<ProxyForward>> UpdateOrCreate(Region region, ScalesetId scalesetId, Guid machineId, int dstPort, int duration) {
var privateIp = await _context.IpOperations.GetScalesetInstanceIp(scalesetId, machineId);
if (privateIp == null) {
@ -87,7 +87,7 @@ public class ProxyForwardOperations : Orm<ProxyForward>, IProxyForwardOperations
}
public async Task<HashSet<Region>> RemoveForward(Guid scalesetId, Guid? machineId, int? dstPort, Guid? proxyId) {
public async Task<HashSet<Region>> RemoveForward(ScalesetId scalesetId, Guid? machineId, int? dstPort, Guid? proxyId) {
var entries = await SearchForward(scalesetId: scalesetId, machineId: machineId, proxyId: proxyId, dstPort: dstPort).ToListAsync();
var regions = new HashSet<Region>();

View File

@ -15,7 +15,7 @@ public interface IScalesetOperations : IStatefulOrm<Scaleset, ScalesetState> {
Async.Task<Scaleset> UpdateConfigs(Scaleset scaleSet);
Async.Task<OneFuzzResult<Scaleset>> GetById(Guid scalesetId);
Async.Task<OneFuzzResult<Scaleset>> GetById(ScalesetId scalesetId);
IAsyncEnumerable<Scaleset> GetByObjectId(Guid objectId);
Async.Task<(bool, Scaleset)> CleanupNodes(Scaleset scaleSet);
@ -584,7 +584,7 @@ public class ScalesetOperations : StatefulOrm<Scaleset, ScalesetState, ScalesetO
} else {
if (await new ShrinkQueue(scaleSet.ScalesetId, _context.Queue, _log).ShouldShrink()) {
toDelete[node.MachineId] = await _context.NodeOperations.SetHalt(node);
} else if (await new ShrinkQueue(pool.OkV!.PoolId, _context.Queue, _log).ShouldShrink()) {
} else if (await new ShrinkQueue(pool.OkV.PoolId, _context.Queue, _log).ShouldShrink()) {
toDelete[node.MachineId] = await _context.NodeOperations.SetHalt(node);
} else {
_logTracer.Info($"Node ready to reimage {node.MachineId:Tag:MachineId} {node.ScalesetId:Tag:ScalesetId} {node.State:Tag:State}");
@ -730,7 +730,7 @@ public class ScalesetOperations : StatefulOrm<Scaleset, ScalesetState, ScalesetO
return OneFuzzResultVoid.Ok;
}
public async Task<OneFuzzResult<Scaleset>> GetById(Guid scalesetId) {
public async Task<OneFuzzResult<Scaleset>> GetById(ScalesetId scalesetId) {
var data = QueryAsync(filter: Query.RowKey(scalesetId.ToString()));
var scaleSets = data is not null ? (await data.ToListAsync()) : null;
@ -800,17 +800,11 @@ public class ScalesetOperations : StatefulOrm<Scaleset, ScalesetState, ScalesetO
return new List<ScalesetNodeState>();
}
var (nodes, azureNodes) = await (
_context.NodeOperations.SearchStates(scaleset.ScalesetId).ToListAsync().AsTask(),
_context.VmssOperations.ListInstanceIds(scaleset.ScalesetId));
var nodes = _context.NodeOperations.SearchStates(scalesetId: scaleset.ScalesetId);
var result = new List<ScalesetNodeState>();
foreach (var (machineId, instanceId) in azureNodes) {
var node = nodes.FirstOrDefault(n => n.MachineId == machineId);
result.Add(new ScalesetNodeState(
MachineId: machineId,
InstanceId: instanceId,
node?.State));
await foreach (var node in nodes) {
result.Add(new ScalesetNodeState(node.MachineId, node.InstanceId, node.State));
}
return result;

View File

@ -2,25 +2,40 @@
public record ShrinkEntry(Guid ShrinkId);
public class ShrinkQueue {
readonly Guid _baseId;
public sealed class ShrinkQueue {
readonly IQueue _queueOps;
readonly ILogTracer _log;
public ShrinkQueue(Guid baseId, IQueue queueOps, ILogTracer log) {
_baseId = baseId;
public ShrinkQueue(ScalesetId baseId, IQueue queueOps, ILogTracer log)
// backwards compat
// scaleset ID used to be a GUID and then this class would format it with "N" format
// to retain the same behaviour remove any dashes in the name
: this(baseId.ToString().Replace("-", ""), queueOps, log) { }
public ShrinkQueue(Guid poolId, IQueue queueOps, ILogTracer log)
: this(poolId.ToString("N"), queueOps, log) { }
private ShrinkQueue(string baseId, IQueue queueOps, ILogTracer log) {
var name = ShrinkQueueNamePrefix + baseId.ToLowerInvariant();
// queue names can be no longer than 63 characters
// if we exceed that, trim off the end. we will still have
// sufficient random chracters to stop collisions from happening
if (name.Length > 63) {
name = name[..63];
}
QueueName = name;
_queueOps = queueOps;
_log = log;
}
public static string ShrinkQueueNamePrefix => "to-shrink-";
public override string ToString() {
return $"{ShrinkQueueNamePrefix}{_baseId:N}";
}
public override string ToString()
=> QueueName;
public string QueueName => ToString();
public string QueueName { get; }
public async Async.Task Clear() {
await _queueOps.ClearQueue(QueueName, StorageType.Config);

View File

@ -13,23 +13,23 @@ namespace Microsoft.OneFuzz.Service;
public interface IVmssOperations {
Async.Task<OneFuzzResultVoid> UpdateScaleInProtection(Scaleset scaleset, string instanceId, bool protectFromScaleIn);
Async.Task<OneFuzzResult<string>> GetInstanceId(Guid name, Guid vmId);
Async.Task<OneFuzzResultVoid> UpdateExtensions(Guid name, IList<VirtualMachineScaleSetExtensionData> extensions);
Async.Task<VirtualMachineScaleSetData?> GetVmss(Guid name);
Async.Task<OneFuzzResult<string>> GetInstanceId(ScalesetId name, Guid vmId);
Async.Task<OneFuzzResultVoid> UpdateExtensions(ScalesetId name, IList<VirtualMachineScaleSetExtensionData> extensions);
Async.Task<VirtualMachineScaleSetData?> GetVmss(ScalesetId name);
Async.Task<IReadOnlyList<string>> ListAvailableSkus(Region region);
Async.Task<bool> DeleteVmss(Guid name, bool? forceDeletion = null);
Async.Task<bool> DeleteVmss(ScalesetId name, bool? forceDeletion = null);
Async.Task<IDictionary<Guid, string>> ListInstanceIds(Guid name);
Async.Task<IDictionary<Guid, string>> ListInstanceIds(ScalesetId name);
Async.Task<long?> GetVmssSize(Guid name);
Async.Task<long?> GetVmssSize(ScalesetId name);
Async.Task<OneFuzzResultVoid> ResizeVmss(Guid name, long capacity);
Async.Task<OneFuzzResultVoid> ResizeVmss(ScalesetId name, long capacity);
Async.Task<OneFuzzResultVoid> CreateVmss(
Region location,
Guid name,
ScalesetId name,
string vmSku,
long vmCount,
ImageReference image,
@ -41,9 +41,9 @@ public interface IVmssOperations {
string sshPublicKey,
IDictionary<string, string> tags);
IAsyncEnumerable<VirtualMachineScaleSetVmResource> ListVmss(Guid name);
Async.Task<OneFuzzResultVoid> ReimageNodes(Guid scalesetId, IEnumerable<Node> nodes);
Async.Task<OneFuzzResultVoid> DeleteNodes(Guid scalesetId, IEnumerable<Node> nodes);
IAsyncEnumerable<VirtualMachineScaleSetVmResource> ListVmss(ScalesetId name);
Async.Task<OneFuzzResultVoid> ReimageNodes(ScalesetId scalesetId, IEnumerable<Node> nodes);
Async.Task<OneFuzzResultVoid> DeleteNodes(ScalesetId scalesetId, IEnumerable<Node> nodes);
}
public class VmssOperations : IVmssOperations {
@ -60,7 +60,7 @@ public class VmssOperations : IVmssOperations {
_cache = cache;
}
public async Async.Task<bool> DeleteVmss(Guid name, bool? forceDeletion = null) {
public async Async.Task<bool> DeleteVmss(ScalesetId name, bool? forceDeletion = null) {
var r = GetVmssResource(name);
var result = await r.DeleteAsync(WaitUntil.Started, forceDeletion: forceDeletion);
var raw = result.GetRawResponse();
@ -72,7 +72,7 @@ public class VmssOperations : IVmssOperations {
}
}
public async Async.Task<long?> GetVmssSize(Guid name) {
public async Async.Task<long?> GetVmssSize(ScalesetId name) {
var vmss = await GetVmss(name);
if (vmss == null) {
return null;
@ -80,7 +80,7 @@ public class VmssOperations : IVmssOperations {
return vmss.Sku.Capacity;
}
public async Async.Task<OneFuzzResultVoid> ResizeVmss(Guid name, long capacity) {
public async Async.Task<OneFuzzResultVoid> ResizeVmss(ScalesetId name, long capacity) {
var canUpdate = await CheckCanUpdate(name);
if (canUpdate.IsOk) {
var scalesetResource = GetVmssResource(name);
@ -100,7 +100,7 @@ public class VmssOperations : IVmssOperations {
}
private VirtualMachineScaleSetResource GetVmssResource(Guid name) {
private VirtualMachineScaleSetResource GetVmssResource(ScalesetId name) {
var id = VirtualMachineScaleSetResource.CreateResourceIdentifier(
_creds.GetSubscription(),
_creds.GetBaseResourceGroup(),
@ -108,7 +108,7 @@ public class VmssOperations : IVmssOperations {
return _creds.ArmClient.GetVirtualMachineScaleSetResource(id);
}
private VirtualMachineScaleSetVmResource GetVmssVmResource(Guid name, string instanceId) {
private VirtualMachineScaleSetVmResource GetVmssVmResource(ScalesetId name, string instanceId) {
var id = VirtualMachineScaleSetVmResource.CreateResourceIdentifier(
_creds.GetSubscription(),
_creds.GetBaseResourceGroup(),
@ -117,7 +117,7 @@ public class VmssOperations : IVmssOperations {
return _creds.ArmClient.GetVirtualMachineScaleSetVmResource(id);
}
public async Async.Task<VirtualMachineScaleSetData?> GetVmss(Guid name) {
public async Async.Task<VirtualMachineScaleSetData?> GetVmss(ScalesetId name) {
try {
var res = await GetVmssResource(name).GetAsync();
_log.Verbose($"getting vmss: {name:Tag:VmssName}");
@ -127,7 +127,7 @@ public class VmssOperations : IVmssOperations {
}
}
public async Async.Task<OneFuzzResult<VirtualMachineScaleSetData>> CheckCanUpdate(Guid name) {
public async Async.Task<OneFuzzResult<VirtualMachineScaleSetData>> CheckCanUpdate(ScalesetId name) {
var vmss = await GetVmss(name);
if (vmss is null) {
return OneFuzzResult<VirtualMachineScaleSetData>.Error(ErrorCode.UNABLE_TO_UPDATE, $"vmss not found: {name}");
@ -139,7 +139,7 @@ public class VmssOperations : IVmssOperations {
}
public async Async.Task<OneFuzzResultVoid> UpdateExtensions(Guid name, IList<VirtualMachineScaleSetExtensionData> extensions) {
public async Async.Task<OneFuzzResultVoid> UpdateExtensions(ScalesetId name, IList<VirtualMachineScaleSetExtensionData> extensions) {
var canUpdate = await CheckCanUpdate(name);
if (canUpdate.IsOk) {
var res = GetVmssResource(name);
@ -165,7 +165,7 @@ public class VmssOperations : IVmssOperations {
}
}
public async Async.Task<IDictionary<Guid, string>> ListInstanceIds(Guid name) {
public async Async.Task<IDictionary<Guid, string>> ListInstanceIds(ScalesetId name) {
_log.Verbose($"get instance IDs for scaleset {name:Tag:VmssName}");
try {
var results = new Dictionary<Guid, string>();
@ -185,8 +185,8 @@ public class VmssOperations : IVmssOperations {
}
}
private sealed record InstanceIdKey(Guid Scaleset, Guid VmId);
private Task<string> GetInstanceIdForVmId(Guid scaleset, Guid vmId)
private sealed record InstanceIdKey(ScalesetId Scaleset, Guid VmId);
private Task<string> GetInstanceIdForVmId(ScalesetId scaleset, Guid vmId)
=> _cache.GetOrCreateAsync(new InstanceIdKey(scaleset, vmId), async entry => {
var scalesetResource = GetVmssResource(scaleset);
var vmIdString = vmId.ToString();
@ -214,7 +214,7 @@ public class VmssOperations : IVmssOperations {
}
})!; // NULLABLE: only this method inserts InstanceIdKey so it cannot be null
public async Async.Task<OneFuzzResult<VirtualMachineScaleSetVmResource>> GetInstanceVm(Guid name, Guid vmId) {
public async Async.Task<OneFuzzResult<VirtualMachineScaleSetVmResource>> GetInstanceVm(ScalesetId name, Guid vmId) {
_log.Info($"get instance ID for scaleset node: {name:Tag:VmssName}:{vmId:Tag:VmId}");
var instanceId = await GetInstanceId(name, vmId);
if (!instanceId.IsOk) {
@ -231,7 +231,7 @@ public class VmssOperations : IVmssOperations {
}
}
public async Async.Task<OneFuzzResult<string>> GetInstanceId(Guid name, Guid vmId) {
public async Async.Task<OneFuzzResult<string>> GetInstanceId(ScalesetId name, Guid vmId) {
try {
return OneFuzzResult.Ok(await GetInstanceIdForVmId(name, vmId));
} catch {
@ -265,7 +265,7 @@ public class VmssOperations : IVmssOperations {
public async Async.Task<OneFuzzResultVoid> CreateVmss(
Region location,
Guid name,
ScalesetId name,
string vmSku,
long vmCount,
ImageReference image,
@ -397,7 +397,7 @@ public class VmssOperations : IVmssOperations {
}
}
public IAsyncEnumerable<VirtualMachineScaleSetVmResource> ListVmss(Guid name)
public IAsyncEnumerable<VirtualMachineScaleSetVmResource> ListVmss(ScalesetId name)
=> GetVmssResource(name)
.GetVirtualMachineScaleSetVms()
.SelectAwait(async vm => vm.HasData ? vm : await vm.GetAsync());
@ -431,7 +431,7 @@ public class VmssOperations : IVmssOperations {
return skuNames;
})!; // NULLABLE: only this method inserts AvailableSkusKey so it cannot be null
private async Async.Task<HashSet<string>> ResolveInstanceIds(Guid scalesetId, IEnumerable<Node> nodes) {
private async Async.Task<HashSet<string>> ResolveInstanceIds(ScalesetId scalesetId, IEnumerable<Node> nodes) {
// only initialize this if we find a missing InstanceId
var machineToInstanceLazy = new Lazy<Task<IDictionary<Guid, string>>>(async () => {
@ -461,7 +461,7 @@ public class VmssOperations : IVmssOperations {
return instanceIds;
}
public async Async.Task<OneFuzzResultVoid> ReimageNodes(Guid scalesetId, IEnumerable<Node> nodes) {
public async Async.Task<OneFuzzResultVoid> ReimageNodes(ScalesetId scalesetId, IEnumerable<Node> nodes) {
var result = await CheckCanUpdate(scalesetId);
if (!result.IsOk) {
return OneFuzzResultVoid.Error(result.ErrorV);
@ -514,7 +514,7 @@ public class VmssOperations : IVmssOperations {
return OneFuzzResultVoid.Ok;
}
public async Async.Task<OneFuzzResultVoid> DeleteNodes(Guid scalesetId, IEnumerable<Node> nodes) {
public async Async.Task<OneFuzzResultVoid> DeleteNodes(ScalesetId scalesetId, IEnumerable<Node> nodes) {
var result = await CheckCanUpdate(scalesetId);
if (!result.IsOk) {
_log.Warning($"cannot delete nodes from scaleset {scalesetId} : {result.ErrorV}");

View File

@ -1,6 +1,7 @@
using System.Runtime.CompilerServices;
using System.Text.Json;
using Azure.Data.Tables;
using Microsoft.OneFuzz.Service;
using Microsoft.OneFuzz.Service.OneFuzzLib.Orm;
namespace ApiService.OneFuzzLib.Orm {
@ -15,6 +16,8 @@ namespace ApiService.OneFuzzLib.Orm {
for (int i = 0; i < args.Length; i++) {
if (args[i] is Guid g) {
args[i] = g.ToString();
} else if (args[i] is IValidatedString s) {
args[i] = s.String;
}
}

View File

@ -24,7 +24,7 @@ public class Node : IFromJsonElement<Node> {
public string State => _e.GetStringProperty("state");
public Guid? ScalesetId => _e.GetNullableGuidProperty("scaleset_id");
public string? ScalesetId => _e.GetNullableStringProperty("scaleset_id");
public bool ReimageRequested => _e.GetBoolProperty("reimage_requested");
public bool DeleteRequested => _e.GetBoolProperty("delete_requested");
public bool DebugKeepNode => _e.GetBoolProperty("debug_keep_node");
@ -45,7 +45,7 @@ public class NodeApi : ApiBase {
.AddV("machine_id", machineId);
return Return<BooleanResult>(await Post(j));
}
public async Task<Result<IEnumerable<Node>, Error>> Get(Guid? machineId = null, IEnumerable<string>? state = null, Guid? scalesetId = null, string? poolName = null) {
public async Task<Result<IEnumerable<Node>, Error>> Get(Guid? machineId = null, IEnumerable<string>? state = null, string? scalesetId = null, string? poolName = null) {
var j = new JsonObject()
.AddIfNotNullV("machine_id", machineId)
.AddIfNotNullEnumerableV("state", state)

View File

@ -91,7 +91,7 @@ public class ProxyApi : ApiBase {
base(endpoint, "/api/proxy", request, output) {
}
public async Task<Result<IEnumerable<Proxy>, Error>> Get(Guid? scalesetId = null, Guid? machineId = null, int? dstPort = null) {
public async Task<Result<IEnumerable<Proxy>, Error>> Get(string? scalesetId = null, Guid? machineId = null, int? dstPort = null) {
var root = new JsonObject()
.AddIfNotNullV("scaleset_id", scalesetId)
.AddIfNotNullV("machine_id", machineId)
@ -101,7 +101,7 @@ public class ProxyApi : ApiBase {
return IEnumerableResult<Proxy>(r.GetProperty("proxies"));
}
public async Task<BooleanResult> Delete(Guid scalesetId, Guid machineId, int? dstPort = null) {
public async Task<BooleanResult> Delete(string scalesetId, Guid machineId, int? dstPort = null) {
var root = new JsonObject()
.AddV("scaleset_id", scalesetId)
.AddV("machine_id", machineId)
@ -115,10 +115,10 @@ public class ProxyApi : ApiBase {
return Return<BooleanResult>(r);
}
public async Task<Result<ProxyGetResult, Error>> Create(Guid scalesetId, Guid machineId, int dstPort, int duration) {
public async Task<Result<ProxyGetResult, Error>> Create(string scalesetId, Guid machineId, int dstPort, int duration) {
var root = new JsonObject()
.AddV("scaleset_id", scalesetId)
.AddV("machin_id", machineId)
.AddV("machine_id", machineId)
.AddV("dst_port", dstPort)
.AddV("duration", duration);

View File

@ -23,7 +23,7 @@ public class Scaleset : IFromJsonElement<Scaleset> {
public Scaleset(JsonElement e) => _e = e;
public static Scaleset Convert(JsonElement e) => new(e);
public Guid ScalesetId => _e.GetGuidProperty("scaleset_id");
public string ScalesetId => _e.GetStringProperty("scaleset_id");
public string PoolName => _e.GetStringProperty("pool_name");
public string State => _e.GetStringProperty("state");
@ -59,7 +59,7 @@ public class ScalesetApi : ApiBase {
base(endpoint, "/api/Scaleset", request, output) { }
public async Task<Result<IEnumerable<Scaleset>, Error>> Get(Guid? id = null, string? state = null, bool? includeAuth = false) {
public async Task<Result<IEnumerable<Scaleset>, Error>> Get(string? id = null, string? state = null, bool? includeAuth = false) {
var j = new JsonObject()
.AddIfNotNullV("scaleset_id", id)
.AddIfNotNullV("state", state)
@ -84,14 +84,14 @@ public class ScalesetApi : ApiBase {
return Result<Scaleset>(await Post(rootScalesetCreate));
}
public async Task<Result<Scaleset, Error>> Patch(Guid id, int size) {
public async Task<Result<Scaleset, Error>> Patch(string id, int size) {
var scalesetPatch = new JsonObject()
.AddV("scaleset_id", id)
.AddV("size", size);
return Result<Scaleset>(await Patch(scalesetPatch));
}
public async Task<BooleanResult> Delete(Guid id, bool now) {
public async Task<BooleanResult> Delete(string id, bool now) {
var scalesetDelete = new JsonObject()
.AddV("scaleset_id", id)
.AddV("now", now);
@ -99,7 +99,7 @@ public class ScalesetApi : ApiBase {
}
public async Task<Scaleset> WaitWhile(Guid id, Func<Scaleset, bool> wait) {
public async Task<Scaleset> WaitWhile(string id, Func<Scaleset, bool> wait) {
var currentState = "";
Scaleset newScaleset;
do {

View File

@ -30,7 +30,7 @@ public abstract class AgentRegistrationTestsBase : FunctionTestBase {
private readonly Guid _machineId = Guid.NewGuid();
private readonly Guid _poolId = Guid.NewGuid();
private readonly Guid _scalesetId = Guid.NewGuid();
private readonly ScalesetId _scalesetId = ScalesetId.Parse($"scaleset-{Guid.NewGuid()}");
private readonly PoolName _poolName = PoolName.Parse($"pool-{Guid.NewGuid()}");
[Fact]

View File

@ -58,6 +58,7 @@ public sealed class TestContext : IOnefuzzContext {
Pool p => PoolOperations.Insert(p),
Job j => JobOperations.Insert(j),
Repro r => ReproOperations.Insert(r),
Scaleset ss => ScalesetOperations.Insert(ss),
NodeTasks nt => NodeTasksOperations.Insert(nt),
InstanceConfig ic => ConfigOperations.Insert(ic),
Notification n => NotificationOperations.Insert(n),

View File

@ -20,40 +20,40 @@ sealed class TestVmssOperations : IVmssOperations {
/* below not implemented */
public Task<OneFuzzResultVoid> CreateVmss(Region location, Guid name, string vmSku, long vmCount, ImageReference image, string networkId, bool? spotInstance, bool ephemeralOsDisks, IList<VirtualMachineScaleSetExtensionData>? extensions, string password, string sshPublicKey, IDictionary<string, string> tags) {
public Task<OneFuzzResultVoid> CreateVmss(Region location, ScalesetId name, string vmSku, long vmCount, ImageReference image, string networkId, bool? spotInstance, bool ephemeralOsDisks, IList<VirtualMachineScaleSetExtensionData>? extensions, string password, string sshPublicKey, IDictionary<string, string> tags) {
throw new NotImplementedException();
}
public Task<bool> DeleteVmss(Guid name, bool? forceDeletion = null) {
public Task<bool> DeleteVmss(ScalesetId name, bool? forceDeletion = null) {
throw new NotImplementedException();
}
public Task<OneFuzzResult<string>> GetInstanceId(Guid name, Guid vmId) {
public Task<OneFuzzResult<string>> GetInstanceId(ScalesetId name, Guid vmId) {
throw new NotImplementedException();
}
public Task<VirtualMachineScaleSetData?> GetVmss(Guid name) {
public Task<VirtualMachineScaleSetData?> GetVmss(ScalesetId name) {
throw new NotImplementedException();
}
public Task<long?> GetVmssSize(Guid name) {
public Task<long?> GetVmssSize(ScalesetId name) {
throw new NotImplementedException();
}
public Task<IDictionary<Guid, string>> ListInstanceIds(Guid name) {
public Task<IDictionary<Guid, string>> ListInstanceIds(ScalesetId name) {
throw new NotImplementedException();
}
public IAsyncEnumerable<VirtualMachineScaleSetVmResource> ListVmss(Guid name) {
public IAsyncEnumerable<VirtualMachineScaleSetVmResource> ListVmss(ScalesetId name) {
throw new NotImplementedException();
}
public Task<OneFuzzResultVoid> ResizeVmss(Guid name, long capacity) {
public Task<OneFuzzResultVoid> ResizeVmss(ScalesetId name, long capacity) {
throw new NotImplementedException();
}
public Task<OneFuzzResultVoid> UpdateExtensions(Guid name, IList<VirtualMachineScaleSetExtensionData> extensions) {
public Task<OneFuzzResultVoid> UpdateExtensions(ScalesetId name, IList<VirtualMachineScaleSetExtensionData> extensions) {
throw new NotImplementedException();
}
@ -61,11 +61,11 @@ sealed class TestVmssOperations : IVmssOperations {
throw new NotImplementedException();
}
public Task<OneFuzzResultVoid> ReimageNodes(Guid scalesetId, IEnumerable<Node> nodes) {
public Task<OneFuzzResultVoid> ReimageNodes(ScalesetId scalesetId, IEnumerable<Node> nodes) {
throw new NotImplementedException();
}
public Async.Task<OneFuzzResultVoid> DeleteNodes(Guid scalesetId, IEnumerable<Node> nodes) {
public Async.Task<OneFuzzResultVoid> DeleteNodes(ScalesetId scalesetId, IEnumerable<Node> nodes) {
throw new NotImplementedException();
}
}

View File

@ -24,10 +24,12 @@ public class AzuriteNodeTest : NodeTestBase {
public abstract class NodeTestBase : FunctionTestBase {
public NodeTestBase(ITestOutputHelper output, IStorage storage)
: base(output, storage) { }
: base(output, storage) {
_scalesetId = Scaleset.GenerateNewScalesetId(_poolName);
}
private readonly Guid _machineId = Guid.NewGuid();
private readonly Guid _scalesetId = Guid.NewGuid();
private readonly ScalesetId _scalesetId;
private readonly PoolName _poolName = PoolName.Parse($"pool-{Guid.NewGuid()}");
private readonly string _version = Guid.NewGuid().ToString();

View File

@ -45,7 +45,7 @@ public abstract class ScalesetTestBase : FunctionTestBase {
public async Async.Task Search_SpecificScaleset_ReturnsErrorIfNoneFound() {
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);
var req = new ScalesetSearch(ScalesetId: Guid.NewGuid());
var req = new ScalesetSearch(ScalesetId: ScalesetId.Parse(Guid.NewGuid().ToString()));
var func = new ScalesetFunction(Logger, auth, Context);
var result = await func.Run(TestHttpRequestData.FromJson("GET", req));
@ -66,6 +66,33 @@ public abstract class ScalesetTestBase : FunctionTestBase {
Assert.Equal("[]", BodyAsString(result));
}
[Fact]
public async Async.Task Search_CanFindScaleset_AndReturnsNodes() {
var scalesetId = ScalesetId.Parse(Guid.NewGuid().ToString());
var poolName = PoolName.Parse($"pool-${Guid.NewGuid()}");
var poolId = Guid.NewGuid();
await Context.InsertAll(
new Pool(poolName, poolId, Os.Linux, Managed: true, Architecture.x86_64, PoolState.Running),
// scaleset to be found must exist
new Scaleset(poolName, scalesetId, ScalesetState.Running, "", ImageReference.MustParse("x:y:z:v"), Region.Parse("region"), 1, null, false, false, new Dictionary<string, string>()),
// some nodes
new Node(poolName, Guid.NewGuid(), poolId, "version", ScalesetId: scalesetId),
new Node(poolName, Guid.NewGuid(), poolId, "version", ScalesetId: scalesetId)
);
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);
var req = new ScalesetSearch(ScalesetId: scalesetId);
var func = new ScalesetFunction(Logger, auth, Context);
var result = await func.Run(TestHttpRequestData.FromJson("GET", req));
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
var resp = BodyAs<ScalesetResponse>(result);
Assert.Equal(scalesetId, resp.ScalesetId);
Assert.Equal(2, resp.Nodes?.Count);
}
[Fact]
public async Async.Task Create_Scaleset() {
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);

View File

@ -91,26 +91,32 @@ namespace Tests {
where PoolName.IsValid(name.Get)
select PoolName.Parse(name.Get);
public static Gen<ScalesetId> ScalesetIdGen { get; }
= from name in Arb.Generate<NonEmptyString>()
where ScalesetId.IsValid(name.Get)
select ScalesetId.Parse(name.Get);
public static Gen<Region> RegionGen { get; }
= from name in Arb.Generate<NonEmptyString>()
where Region.IsValid(name.Get)
select Region.Parse(name.Get);
public static Gen<Node> Node { get; }
= from arg in Arb.Generate<Tuple<Tuple<DateTimeOffset?, Guid?, Guid, NodeState>, Tuple<Guid?, DateTimeOffset, string, bool, bool, bool>>>()
= from arg in Arb.Generate<Tuple<Tuple<DateTimeOffset?, Guid?, Guid, NodeState>, Tuple<DateTimeOffset, string, bool, bool, bool>>>()
from poolName in PoolNameGen
from scalesetId in Arb.Generate<Guid>()
select new Node(
InitializedAt: arg.Item1.Item1,
PoolName: poolName,
PoolId: arg.Item1.Item3,
MachineId: arg.Item1.Item3,
State: arg.Item1.Item4,
ScalesetId: arg.Item2.Item1,
Heartbeat: arg.Item2.Item2,
Version: arg.Item2.Item3,
ReimageRequested: arg.Item2.Item4,
DeleteRequested: arg.Item2.Item5,
DebugKeepNode: arg.Item2.Item6);
ScalesetId: ScalesetId.Parse(scalesetId.ToString()),
Heartbeat: arg.Item2.Item1,
Version: arg.Item2.Item2,
ReimageRequested: arg.Item2.Item3,
DeleteRequested: arg.Item2.Item4,
DebugKeepNode: arg.Item2.Item5);
public static Gen<ProxyForward> ProxyForward { get; } =
from region in RegionGen
@ -124,7 +130,7 @@ namespace Tests {
select new ProxyForward(
Region: region,
Port: port,
ScalesetId: scalesetId,
ScalesetId: ScalesetId.Parse(scalesetId.ToString()),
MachineId: machineId,
ProxyId: proxyId,
DstPort: dstPort,
@ -241,18 +247,19 @@ namespace Tests {
public static Gen<Scaleset> Scaleset { get; }
= from arg in Arb.Generate<Tuple<
Tuple<Guid, ScalesetState, Authentication?, string>,
Tuple<ScalesetState, Authentication?, string>,
Tuple<int, bool, bool, bool, Error?, Guid?>,
Tuple<Guid?, Dictionary<string, string>>>>()
from scalesetId in Arb.Generate<Guid>()
from poolName in PoolNameGen
from region in RegionGen
from image in ImageReferenceGen
select new Scaleset(
PoolName: poolName,
ScalesetId: arg.Item1.Item1,
State: arg.Item1.Item2,
Auth: arg.Item1.Item3,
VmSku: arg.Item1.Item4,
ScalesetId: ScalesetId.Parse(scalesetId.ToString()),
State: arg.Item1.Item1,
Auth: arg.Item1.Item2,
VmSku: arg.Item1.Item3,
Image: image,
Region: region,
@ -511,6 +518,7 @@ namespace Tests {
public class OrmArb {
public static Arbitrary<PoolName> PoolName { get; } = OrmGenerators.PoolNameGen.ToArbitrary();
public static Arbitrary<ScalesetId> ScalesetId { get; } = OrmGenerators.ScalesetIdGen.ToArbitrary();
public static Arbitrary<IReadOnlyList<T>> ReadOnlyList<T>()
=> Arb.Default.List<T>().Convert(x => (IReadOnlyList<T>)x, x => (List<T>)x);

View File

@ -236,7 +236,9 @@ namespace Tests {
[Fact]
public void TestEventSerialization() {
var expectedEvent = new EventMessage(Guid.NewGuid(), EventType.NodeHeartbeat, new EventNodeHeartbeat(Guid.NewGuid(), Guid.NewGuid(), PoolName.Parse("test-Poool"), NodeState.Busy), Guid.NewGuid(), "test", DateTime.UtcNow);
var scalesetId = ScalesetId.Parse(Guid.NewGuid().ToString());
var hb = new EventNodeHeartbeat(Guid.NewGuid(), scalesetId, PoolName.Parse("test-Poool"), NodeState.Busy);
var expectedEvent = new EventMessage(Guid.NewGuid(), EventType.NodeHeartbeat, hb, Guid.NewGuid(), "test", DateTime.UtcNow);
var serialized = JsonSerializer.Serialize(expectedEvent, EntityConverter.GetJsonSerializerOptions());
var actualEvent = JsonSerializer.Deserialize<EventMessage>((string)serialized, EntityConverter.GetJsonSerializerOptions());
Assert.Equal(expectedEvent, actualEvent);

View File

@ -17,7 +17,7 @@ namespace Tests {
var query2 = NodeOperations.SearchStatesQuery(poolId: Guid.Parse("3b0426d3-9bde-4ae8-89ac-4edf0d3b3618"));
Assert.Equal("((pool_id eq '3b0426d3-9bde-4ae8-89ac-4edf0d3b3618'))", query2);
var query3 = NodeOperations.SearchStatesQuery(scaleSetId: Guid.Parse("4c96dd6b-9bdb-4758-9720-1010c244fa4b"));
var query3 = NodeOperations.SearchStatesQuery(scaleSetId: ScalesetId.Parse("4c96dd6b-9bdb-4758-9720-1010c244fa4b"));
Assert.Equal("((scaleset_id eq '4c96dd6b-9bdb-4758-9720-1010c244fa4b'))", query3);
var query4 = NodeOperations.SearchStatesQuery(states: new[] { NodeState.Free, NodeState.Done, NodeState.Ready });
@ -25,7 +25,7 @@ namespace Tests {
var query7 = NodeOperations.SearchStatesQuery(
poolId: Guid.Parse("3b0426d3-9bde-4ae8-89ac-4edf0d3b3618"),
scaleSetId: Guid.Parse("4c96dd6b-9bdb-4758-9720-1010c244fa4b"),
scaleSetId: ScalesetId.Parse("4c96dd6b-9bdb-4758-9720-1010c244fa4b"),
states: new[] { NodeState.Free, NodeState.Done, NodeState.Ready });
Assert.Equal("((pool_id eq '3b0426d3-9bde-4ae8-89ac-4edf0d3b3618')) and ((scaleset_id eq '4c96dd6b-9bdb-4758-9720-1010c244fa4b')) and (((state eq 'free') or (state eq 'done') or (state eq 'ready')))", query7);
}
@ -33,7 +33,7 @@ namespace Tests {
[Fact]
public void QueryFilterTest() {
var scalesetId = Guid.Parse("3b0426d3-9bde-4ae8-89ac-4edf0d3b3618");
var scalesetId = ScalesetId.Parse(Guid.Parse("3b0426d3-9bde-4ae8-89ac-4edf0d3b3618").ToString());
var proxyId = Guid.Parse("4c96dd6b-9bdb-4758-9720-1010c244fa4b");
var region = "westus2";
var outdated = false;

View File

@ -1,4 +1,5 @@
using System.Text.Json;
using System;
using System.Text.Json;
using Microsoft.OneFuzz.Service;
using Xunit;
@ -42,4 +43,40 @@ public class ValidatedStringTests {
public void PoolNames(string name, bool valid) {
Assert.Equal(valid, PoolName.IsValid(name));
}
[Theory]
[InlineData("", false)]
[InlineData("abc", true)]
[InlineData("a-bc", true)]
[InlineData("-abc", false)]
[InlineData("abc-", false)]
[InlineData("ef052a0d-f235-4115-bd47-b359bcc5078b", true)]
public void ScalesetIds(string name, bool valid) {
Assert.Equal(valid, ScalesetId.IsValid(name));
}
private static readonly Guid _fixedGuid = Guid.Parse("3b24ba21-1cad-4b07-8655-914754485838");
[Fact]
public void ScalesetId_FromBasicPool() {
var pool = PoolName.Parse("pool");
var id = Scaleset.GenerateNewScalesetIdUsingGuid(pool, _fixedGuid).ToString();
Assert.Equal("pool-3b24ba211cad4b078655914754485838", id);
}
[Fact]
public void ScalesetId_FromReallyLongPool() {
var pool = PoolName.Parse(new string('x', 100));
var id = Scaleset.GenerateNewScalesetIdUsingGuid(pool, _fixedGuid).ToString();
Assert.Equal(64, id.Length);
Assert.Equal($"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-3b24ba211cad4b078655914754485838", id);
}
[Fact]
public void ScalesetId_FromPoolWithBadCharacters() {
var pool = PoolName.Parse("_.-po-!?(*!&@#$)o_.l-._");
var id = Scaleset.GenerateNewScalesetIdUsingGuid(pool, _fixedGuid).ToString();
// hyphens preserved except at start and end, and underscores turned into hyphens
Assert.Equal($"po-o-l-3b24ba211cad4b078655914754485838", id);
}
}

View File

@ -1451,11 +1451,10 @@ class Node(Endpoint):
self,
*,
state: Optional[List[enums.NodeState]] = None,
scaleset_id: Optional[UUID_EXPANSION] = None,
scaleset_id: Optional[str] = None,
pool_name: Optional[primitives.PoolName] = None,
) -> List[models.Node]:
self.logger.debug("list nodes")
scaleset_id_expanded: Optional[UUID] = None
if pool_name is not None:
pool_name = primitives.PoolName(
@ -1467,18 +1466,11 @@ class Node(Endpoint):
)
)
if scaleset_id is not None:
scaleset_id_expanded = self._disambiguate_uuid(
"scaleset_id",
scaleset_id,
lambda: [str(x.scaleset_id) for x in self.onefuzz.scalesets.list()],
)
return self._req_model_list(
"GET",
models.Node,
data=requests.NodeSearch(
scaleset_id=scaleset_id_expanded, state=state, pool_name=pool_name
scaleset_id=scaleset_id, state=state, pool_name=pool_name
),
)
@ -1510,7 +1502,7 @@ class Scaleset(Endpoint):
def _expand_scaleset_machine(
self,
scaleset_id: UUID_EXPANSION,
scaleset_id: str,
machine_id: UUID_EXPANSION,
*,
include_auth: bool = False,
@ -1577,54 +1569,32 @@ class Scaleset(Endpoint):
),
)
def shutdown(
self, scaleset_id: UUID_EXPANSION, *, now: bool = False
) -> responses.BoolResult:
scaleset_id_expanded = self._disambiguate_uuid(
"scaleset_id",
scaleset_id,
lambda: [str(x.scaleset_id) for x in self.list()],
)
self.logger.debug("shutdown scaleset: %s (now: %s)", scaleset_id_expanded, now)
def shutdown(self, scaleset_id: str, *, now: bool = False) -> responses.BoolResult:
self.logger.debug("shutdown scaleset: %s (now: %s)", scaleset_id, now)
return self._req_model(
"DELETE",
responses.BoolResult,
data=requests.ScalesetStop(scaleset_id=scaleset_id_expanded, now=now),
data=requests.ScalesetStop(scaleset_id=scaleset_id, now=now),
)
def get(
self, scaleset_id: UUID_EXPANSION, *, include_auth: bool = False
) -> models.Scaleset:
def get(self, scaleset_id: str, *, include_auth: bool = False) -> models.Scaleset:
self.logger.debug("get scaleset: %s", scaleset_id)
scaleset_id_expanded = self._disambiguate_uuid(
"scaleset_id",
scaleset_id,
lambda: [str(x.scaleset_id) for x in self.list()],
)
return self._req_model(
"GET",
models.Scaleset,
data=requests.ScalesetSearch(
scaleset_id=scaleset_id_expanded, include_auth=include_auth
scaleset_id=scaleset_id, include_auth=include_auth
),
)
def update(
self, scaleset_id: UUID_EXPANSION, *, size: Optional[int] = None
self, scaleset_id: str, *, size: Optional[int] = None
) -> models.Scaleset:
self.logger.debug("update scaleset: %s", scaleset_id)
scaleset_id_expanded = self._disambiguate_uuid(
"scaleset_id",
scaleset_id,
lambda: [str(x.scaleset_id) for x in self.list()],
)
return self._req_model(
"PATCH",
models.Scaleset,
data=requests.ScalesetUpdate(scaleset_id=scaleset_id_expanded, size=size),
data=requests.ScalesetUpdate(scaleset_id=scaleset_id, size=size),
)
def list(
@ -1645,7 +1615,7 @@ class ScalesetProxy(Endpoint):
def delete(
self,
scaleset_id: UUID_EXPANSION,
scaleset_id: str,
machine_id: UUID_EXPANSION,
*,
dst_port: Optional[int] = None,
@ -1681,7 +1651,7 @@ class ScalesetProxy(Endpoint):
)
def get(
self, scaleset_id: UUID_EXPANSION, machine_id: UUID_EXPANSION, dst_port: int
self, scaleset_id: str, machine_id: UUID_EXPANSION, dst_port: int
) -> responses.ProxyGetResult:
"""Get information about a specific job"""
(
@ -1705,7 +1675,7 @@ class ScalesetProxy(Endpoint):
def create(
self,
scaleset_id: UUID_EXPANSION,
scaleset_id: str,
machine_id: UUID_EXPANSION,
dst_port: int,
*,

View File

@ -103,7 +103,7 @@ class DebugScaleset(Command):
"""Debug tasks"""
def _get_proxy_setup(
self, scaleset_id: UUID, machine_id: UUID, port: int, duration: Optional[int]
self, scaleset_id: str, machine_id: UUID, port: int, duration: Optional[int]
) -> Tuple[bool, str, Optional[Tuple[str, int]]]:
proxy = self.onefuzz.scaleset_proxy.create(
scaleset_id, machine_id, port, duration=duration
@ -115,7 +115,7 @@ class DebugScaleset(Command):
def rdp(
self,
scaleset_id: UUID_EXPANSION,
scaleset_id: str,
machine_id: UUID_EXPANSION,
duration: Optional[int] = 1,
) -> None:
@ -144,7 +144,7 @@ class DebugScaleset(Command):
def ssh(
self,
scaleset_id: UUID_EXPANSION,
scaleset_id: str,
machine_id: UUID_EXPANSION,
duration: Optional[int] = 1,
command: Optional[str] = None,
@ -185,7 +185,7 @@ class DebugTask(Command):
def _get_node(
self, task_id: UUID_EXPANSION, node_id: Optional[UUID]
) -> Tuple[UUID, UUID]:
) -> Tuple[str, UUID]:
nodes = self.list_nodes(task_id)
if not nodes:
raise Exception("task is not currently executing on nodes")

View File

@ -186,7 +186,7 @@ def main() -> None:
),
EventPoolDeleted(pool_name=PoolName("example")),
EventScalesetCreated(
scaleset_id=UUID(int=0),
scaleset_id="example-000",
pool_name=PoolName("example"),
vm_sku="Standard_D2s_v3",
image="Canonical:0001-com-ubuntu-server-focal:20_04-lts:latest",
@ -194,20 +194,20 @@ def main() -> None:
size=10,
),
EventScalesetFailed(
scaleset_id=UUID(int=0),
scaleset_id="example-000",
pool_name=PoolName("example"),
error=Error(
code=ErrorCode.UNABLE_TO_RESIZE, errors=["example error message"]
),
),
EventScalesetDeleted(scaleset_id=UUID(int=0), pool_name=PoolName("example")),
EventScalesetDeleted(scaleset_id="example-000", pool_name=PoolName("example")),
EventScalesetStateUpdated(
scaleset_id=UUID(int=0),
scaleset_id="example-000",
pool_name=PoolName("example"),
state=ScalesetState.init,
),
EventScalesetResizeScheduled(
scaleset_id=UUID(int=0), pool_name=PoolName("example"), size=0
scaleset_id="example-000", pool_name=PoolName("example"), size=0
),
EventJobCreated(
job_id=UUID(int=0),

View File

@ -98,7 +98,7 @@ class EventPing(BaseEvent, BaseResponse):
class EventScalesetCreated(BaseEvent):
scaleset_id: UUID
scaleset_id: str
pool_name: PoolName
vm_sku: str
image: str
@ -107,18 +107,18 @@ class EventScalesetCreated(BaseEvent):
class EventScalesetFailed(BaseEvent):
scaleset_id: UUID
scaleset_id: str
pool_name: PoolName
error: Error
class EventScalesetDeleted(BaseEvent):
scaleset_id: UUID
scaleset_id: str
pool_name: PoolName
class EventScalesetResizeScheduled(BaseEvent):
scaleset_id: UUID
scaleset_id: str
pool_name: PoolName
size: int
@ -159,32 +159,32 @@ class EventProxyStateUpdated(BaseEvent):
class EventNodeCreated(BaseEvent):
machine_id: UUID
scaleset_id: Optional[UUID]
scaleset_id: Optional[str]
pool_name: PoolName
class EventNodeHeartbeat(BaseEvent):
machine_id: UUID
scaleset_id: Optional[UUID]
scaleset_id: Optional[str]
pool_name: PoolName
machine_state: Optional[NodeState]
class EventNodeDeleted(BaseEvent):
machine_id: UUID
scaleset_id: Optional[UUID]
scaleset_id: Optional[str]
pool_name: PoolName
class EventScalesetStateUpdated(BaseEvent):
scaleset_id: UUID
scaleset_id: str
pool_name: PoolName
state: ScalesetState
class EventNodeStateUpdated(BaseEvent):
machine_id: UUID
scaleset_id: Optional[UUID]
scaleset_id: Optional[str]
pool_name: PoolName
state: NodeState

View File

@ -604,7 +604,7 @@ class Node(BaseModel):
pool_id: Optional[UUID]
machine_id: UUID
state: NodeState = Field(default=NodeState.init)
scaleset_id: Optional[UUID] = None
scaleset_id: Optional[str] = None
tasks: Optional[List[NodeTasks]] = None
messages: Optional[List[NodeCommand]] = None
heartbeat: Optional[datetime]
@ -615,7 +615,7 @@ class Node(BaseModel):
class ScalesetSummary(BaseModel):
scaleset_id: UUID
scaleset_id: str
state: ScalesetState
@ -668,7 +668,7 @@ class ScalesetNodeState(BaseModel):
class Scaleset(BaseModel):
timestamp: Optional[datetime] = Field(alias="Timestamp")
pool_name: PoolName
scaleset_id: UUID = Field(default_factory=uuid4)
scaleset_id: str
state: ScalesetState = Field(default=ScalesetState.init)
auth: Optional[Authentication]
vm_sku: str
@ -686,7 +686,7 @@ class Scaleset(BaseModel):
class AutoScale(BaseModel):
scaleset_id: UUID
scaleset_id: str
min: int = Field(ge=0)
max: int = Field(ge=1)
default: int = Field(ge=0)
@ -812,7 +812,7 @@ class TaskEventSummary(BaseModel):
class NodeAssignment(BaseModel):
node_id: UUID
scaleset_id: Optional[UUID]
scaleset_id: Optional[str]
state: NodeTaskState

View File

@ -91,7 +91,7 @@ class AgentRegistrationGet(BaseRequest):
class AgentRegistrationPost(BaseRequest):
pool_name: PoolName
scaleset_id: Optional[UUID]
scaleset_id: Optional[str]
machine_id: UUID
version: str = Field(default="1.0.0")
@ -122,7 +122,7 @@ class PoolStop(BaseRequest):
class ProxyGet(BaseRequest):
scaleset_id: Optional[UUID]
scaleset_id: Optional[str]
machine_id: Optional[UUID]
dst_port: Optional[int]
@ -139,14 +139,14 @@ class ProxyGet(BaseRequest):
class ProxyCreate(BaseRequest):
scaleset_id: UUID
scaleset_id: str
machine_id: UUID
dst_port: int
duration: int = Field(ge=ONE_HOUR, le=SEVEN_DAYS)
class ProxyDelete(BaseRequest):
scaleset_id: UUID
scaleset_id: str
machine_id: UUID
dst_port: Optional[int]
@ -154,7 +154,7 @@ class ProxyDelete(BaseRequest):
class NodeSearch(BaseRequest):
machine_id: Optional[UUID]
state: Optional[List[NodeState]]
scaleset_id: Optional[UUID]
scaleset_id: Optional[str]
pool_name: Optional[PoolName]
@ -168,18 +168,18 @@ class NodeUpdate(BaseRequest):
class ScalesetSearch(BaseRequest):
scaleset_id: Optional[UUID]
scaleset_id: Optional[str]
state: Optional[List[ScalesetState]]
include_auth: bool = Field(default=False)
class ScalesetStop(BaseRequest):
scaleset_id: UUID
scaleset_id: str
now: bool
class ScalesetUpdate(BaseRequest):
scaleset_id: UUID
scaleset_id: str
size: Optional[int] = Field(ge=1)

View File

@ -54,6 +54,7 @@ class TestScaleset(unittest.TestCase):
def test_scaleset_size(self) -> None:
with self.assertRaises(ValueError):
Scaleset(
scaleset_id="test-pool-000",
pool_name=PoolName("test-pool"),
vm_sku="Standard_D2ds_v4",
image="Canonical:0001-com-ubuntu-server-focal:20_04-lts:latest",
@ -63,6 +64,7 @@ class TestScaleset(unittest.TestCase):
)
scaleset = Scaleset(
scaleset_id="test-pool-000",
pool_name=PoolName("test-pool"),
vm_sku="Standard_D2ds_v4",
image="Canonical:0001-com-ubuntu-server-focal:20_04-lts:latest",
@ -73,6 +75,7 @@ class TestScaleset(unittest.TestCase):
self.assertEqual(scaleset.size, 0)
scaleset = Scaleset(
scaleset_id="test-pool-000",
pool_name=PoolName("test-pool"),
vm_sku="Standard_D2ds_v4",
image="Canonical:0001-com-ubuntu-server-focal:20_04-lts:latest",