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

View File

@ -52,7 +52,7 @@ public class Proxy {
var proxyGet = request.OkV; var proxyGet = request.OkV;
switch ((proxyGet.ScalesetId, proxyGet.MachineId, proxyGet.DstPort)) { 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); var scaleset = await _context.ScalesetOperations.GetById(scalesetId);
if (!scaleset.IsOk) { if (!scaleset.IsOk) {
return await _context.RequestHandling.NotOk(req, scaleset.ErrorV, "ProxyGet"); return await _context.RequestHandling.NotOk(req, scaleset.ErrorV, "ProxyGet");

View File

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

View File

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

View File

@ -1,6 +1,7 @@
using System.Reflection; using System.Reflection;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.OneFuzz.Service.OneFuzzLib.Orm; using Microsoft.OneFuzz.Service.OneFuzzLib.Orm;
using Endpoint = System.String; using Endpoint = System.String;
@ -107,7 +108,7 @@ public record Node
// a string internally. // a string internally.
string? InstanceId = null, string? InstanceId = null,
Guid? ScalesetId = null, ScalesetId? ScalesetId = null,
bool ReimageRequested = false, bool ReimageRequested = false,
bool DeleteRequested = false, bool DeleteRequested = false,
@ -132,7 +133,7 @@ public record ProxyForward
( (
[PartitionKey] Region Region, [PartitionKey] Region Region,
[RowKey] long Port, [RowKey] long Port,
Guid ScalesetId, ScalesetId ScalesetId,
Guid MachineId, Guid MachineId,
Guid? ProxyId, Guid? ProxyId,
long DstPort, long DstPort,
@ -263,7 +264,7 @@ public record TaskEventSummary(
public record NodeAssignment( public record NodeAssignment(
Guid NodeId, Guid NodeId,
Guid? ScalesetId, ScalesetId? ScalesetId,
NodeTaskState State NodeTaskState State
); );
@ -392,7 +393,7 @@ public record InstanceConfig
} }
public record AutoScale( public record AutoScale(
[PartitionKey, RowKey] Guid ScalesetId, [PartitionKey, RowKey] ScalesetId ScalesetId,
long Min, long Min,
long Max, long Max,
long Default, long Default,
@ -402,15 +403,10 @@ public record AutoScale(
long ScaleInCooldown long ScaleInCooldown
) : EntityBase; ) : EntityBase;
public record ScalesetNodeState(
Guid MachineId,
string InstanceId,
NodeState? State
);
public record Scaleset( public partial record Scaleset(
[PartitionKey] PoolName PoolName, [PartitionKey] PoolName PoolName,
[RowKey] Guid ScalesetId, [RowKey] ScalesetId ScalesetId,
ScalesetState State, ScalesetState State,
string VmSku, string VmSku,
ImageReference Image, ImageReference Image,
@ -425,7 +421,31 @@ public record Scaleset(
Guid? ClientId = null, Guid? ClientId = null,
Guid? ClientObjectId = null Guid? ClientObjectId = null
// 'Nodes' removed when porting from Python: only used in search response // '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( public record Notification(
[PartitionKey] Guid NotificationId, [PartitionKey] Guid NotificationId,
@ -733,7 +753,7 @@ public record WorkSetSummary(
); );
public record ScalesetSummary( public record ScalesetSummary(
Guid ScalesetId, ScalesetId ScalesetId,
ScalesetState State ScalesetState State
); );

View File

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

View File

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

View File

@ -16,9 +16,15 @@ static partial class Check {
public static bool IsAlnumDash(string input) => IsAlnumDashRegex().IsMatch(input); public static bool IsAlnumDash(string input) => IsAlnumDashRegex().IsMatch(input);
// Permits 1-64 characters: alphanumeric, underscore, period, or dash. // Permits 1-64 characters: alphanumeric, underscore, period, or dash.
[GeneratedRegex("\\A[._a-zA-Z0-9\\-]{1,64}\\z")] // Cannot start with underscore (or dash) or end with period or dash.
private static partial Regex IsNameLikeRegex(); [GeneratedRegex(@"\A(?![_\-])[._a-zA-Z0-9\-]{1,64}(?<![.\-])\z")]
public static bool IsNameLike(string input) => IsNameLikeRegex().IsMatch(input); 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. // This regex is based upon DNS labels but more restricted.
// It is used for many different Storage resources. // 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 static bool IsStorageDnsLabel(string input) => StorageDnsLabelRegex().IsMatch(input);
} }
public interface IValidatedString<T> where T : IValidatedString<T> { public interface IValidatedString {
public static abstract T Parse(string input);
public static abstract bool IsValid(string input); public static abstract bool IsValid(string input);
public static abstract string Requirements { get; } public static abstract string Requirements { get; }
public string String { 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> { public abstract record ValidatedStringBase<T> where T : IValidatedString<T> {
protected ValidatedStringBase(string value) { protected ValidatedStringBase(string value) {
if (!T.IsValid(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 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)"; 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); var query = UriExtension.GetQueryComponents(req.Url);
Guid? poolId = UriExtension.GetGuid("poolId", query); 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; List<NodeState>? states = default;
if (query.TryGetValue("states", out var value)) { if (query.TryGetValue("states", out var value)) {
@ -196,7 +199,7 @@ namespace ApiService.TestHooks {
_log.Info($"reimage long lived nodes"); _log.Info($"reimage long lived nodes");
var query = UriExtension.GetQueryComponents(req.Url); 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); var resp = req.CreateResponse(HttpStatusCode.OK);
await resp.WriteAsJsonAsync(r); await resp.WriteAsJsonAsync(r);
return resp; return resp;
@ -213,9 +216,9 @@ namespace ApiService.TestHooks {
var poolName = PoolName.Parse(query["poolName"]); var poolName = PoolName.Parse(query["poolName"]);
Guid machineId = Guid.Parse(query["machineId"]); Guid machineId = Guid.Parse(query["machineId"]);
Guid? scaleSetId = default; ScalesetId? scaleSetId = null;
if (query.TryGetValue("scaleSetId", out var value)) { if (query.TryGetValue("scaleSetId", out var value)) {
scaleSetId = Guid.Parse(value); scaleSetId = ScalesetId.Parse(value);
} }
string version = query["version"]; string version = query["version"];
@ -236,10 +239,10 @@ namespace ApiService.TestHooks {
var query = UriExtension.GetQueryComponents(req.Url); var query = UriExtension.GetQueryComponents(req.Url);
Guid scaleSetId = Guid.Parse(query["scaleSetId"]); var scaleSetId = ScalesetId.Parse(query["scaleSetId"]);
TimeSpan timeSpan = TimeSpan.Parse(query["timeSpan"]); 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 json = JsonSerializer.Serialize(nodes, EntityConverter.GetJsonSerializerOptions());
var resp = req.CreateResponse(HttpStatusCode.OK); var resp = req.CreateResponse(HttpStatusCode.OK);
await resp.WriteStringAsync(json); await resp.WriteStringAsync(json);

View File

@ -27,7 +27,7 @@ namespace ApiService.TestHooks {
var query = UriExtension.GetQueryComponents(req.Url); var query = UriExtension.GetQueryComponents(req.Url);
var poolRes = _proxyForward.SearchForward( 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.GetString("region", query) is string region ? Region.Parse(region) : null,
UriExtension.GetGuid("machineId", query), UriExtension.GetGuid("machineId", query),
UriExtension.GetGuid("proxyId", 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) { public async Task<HttpResponseData> ListInstanceIds([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "testhooks/vmssOperations/listInstanceIds")] HttpRequestData req) {
_log.Info($"list instance ids"); _log.Info($"list instance ids");
var query = UriExtension.GetQueryComponents(req.Url); 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 ids = await _vmssOps.ListInstanceIds(name); var ids = await _vmssOps.ListInstanceIds(ScalesetId.Parse(name));
var json = JsonSerializer.Serialize(ids, EntityConverter.GetJsonSerializerOptions()); var json = JsonSerializer.Serialize(ids, EntityConverter.GetJsonSerializerOptions());
var resp = req.CreateResponse(HttpStatusCode.OK); 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) { public async Task<HttpResponseData> GetInstanceId([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "testhooks/vmssOperations/getInstanceId")] HttpRequestData req) {
_log.Info($"list instance ids"); _log.Info($"list instance ids");
var query = UriExtension.GetQueryComponents(req.Url); 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 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 json = JsonSerializer.Serialize(id, EntityConverter.GetJsonSerializerOptions());
var resp = req.CreateResponse(HttpStatusCode.OK); 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) { public async Task<HttpResponseData> UpdateScaleInProtection([HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "testhooks/vmssOperations/updateScaleInProtection")] HttpRequestData req) {
_log.Info($"list instance ids"); _log.Info($"list instance ids");
var query = UriExtension.GetQueryComponents(req.Url); 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 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) { if (!scalesetResult.IsOk) {
throw new Exception("invalid scaleset name"); 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<ResultVoid<(HttpStatusCode Status, string Reason)>> Insert(AutoScale autoScale);
public Async.Task<AutoScale?> GetSettingsForScaleset(Guid scalesetId); public Async.Task<AutoScale?> GetSettingsForScaleset(ScalesetId scalesetId);
AutoscaleProfile CreateAutoScaleProfile( AutoscaleProfile CreateAutoScaleProfile(
string queueUri, string queueUri,
@ -26,16 +26,16 @@ public interface IAutoScaleOperations {
double scaleInCooldownMinutes); double scaleInCooldownMinutes);
AutoscaleProfile DefaultAutoScaleProfile(string queueUri, long scaleSetSize); 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<OneFuzzResultVoid> UpdateAutoscale(AutoscaleSettingData autoscale);
Async.Task<OneFuzzResult<AutoscaleProfile>> GetAutoScaleProfile(Guid scalesetId); Async.Task<OneFuzzResult<AutoscaleProfile>> GetAutoScaleProfile(ScalesetId scalesetId);
Async.Task<AutoScale> Update( Async.Task<AutoScale> Update(
Guid scalesetId, ScalesetId scalesetId,
long minAmount, long minAmount,
long maxAmount, long maxAmount,
long defaultAmount, long defaultAmount,
@ -54,7 +54,7 @@ public class AutoScaleOperations : Orm<AutoScale>, IAutoScaleOperations {
} }
public async Async.Task<AutoScale> Create( public async Async.Task<AutoScale> Create(
Guid scalesetId, ScalesetId scalesetId,
long minAmount, long minAmount,
long maxAmount, long maxAmount,
long defaultAmount, long defaultAmount,
@ -81,7 +81,7 @@ public class AutoScaleOperations : Orm<AutoScale>, IAutoScaleOperations {
return entry; return entry;
} }
public async Async.Task<AutoScale?> GetSettingsForScaleset(Guid scalesetId) { public async Async.Task<AutoScale?> GetSettingsForScaleset(ScalesetId scalesetId) {
try { try {
var autoscale = await GetEntityAsync(scalesetId.ToString(), scalesetId.ToString()); var autoscale = await GetEntityAsync(scalesetId.ToString(), scalesetId.ToString());
return autoscale; 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}"); _logTracer.Info($"getting scaleset for existing auto-scale resources {scalesetId:Tag:ScalesetId}");
var settings = _context.Creds.GetResourceGroupResource().GetAutoscaleSettings(); var settings = _context.Creds.GetResourceGroupResource().GetAutoscaleSettings();
if (settings is null) { 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}"); 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"); _logTracer.Info($"Checking scaleset {vmss:Tag:ScalesetId} for existing auto scale resource");
var existingAutoScaleResource = GetAutoscaleSettings(vmss); var existingAutoScaleResource = GetAutoscaleSettings(vmss);
@ -141,7 +141,7 @@ public class AutoScaleOperations : Orm<AutoScale>, IAutoScaleOperations {
return OneFuzzResultVoid.Ok; 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}"); _logTracer.Info($"Creating auto-scale resource for: {resourceId:Tag:AutoscaleResourceId}");
var resourceGroup = _context.Creds.GetBaseResourceGroup(); 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"); _logTracer.Info($"Checking scaleset {vmss:Tag:ScalesetId} for existing auto scale resource");
try { try {
var autoscale = _context.Creds.GetResourceGroupResource().GetAutoscaleSettings() var autoscale = _context.Creds.GetResourceGroupResource().GetAutoscaleSettings()
@ -354,7 +354,7 @@ public class AutoScaleOperations : Orm<AutoScale>, IAutoScaleOperations {
} }
public async Async.Task<AutoScale> Update( public async Async.Task<AutoScale> Update(
Guid scalesetId, ScalesetId scalesetId,
long minAmount, long minAmount,
long maxAmount, long maxAmount,
long defaultAmount, long defaultAmount,

View File

@ -24,16 +24,15 @@ public interface IIpOperations {
public Async.Task DeleteIp(string resourceGroup, string name); 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 Async.Task CreateIp(string resourceGroup, string name, Region region);
} }
public class IpOperations : IIpOperations { public class IpOperations : IIpOperations {
private ILogTracer _logTracer; private readonly ILogTracer _logTracer;
private readonly IOnefuzzContext _context;
private IOnefuzzContext _context;
private readonly NetworkInterfaceQuery _networkInterfaceQuery; private readonly NetworkInterfaceQuery _networkInterfaceQuery;
public IpOperations(ILogTracer log, IOnefuzzContext context) { 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); var instance = await _context.VmssOperations.GetInstanceId(scalesetId, machineId);
if (!instance.IsOk) { if (!instance.IsOk) {
_logTracer.Verbose($"failed to get vmss {scalesetId:Tag:ScalesetId} for instance id {machineId:Tag:MachineId} due to {instance.ErrorV:Tag:Error}"); _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( var token = _context.Creds.GetIdentity().GetToken(
new TokenRequestContext( new TokenRequestContext(
new[] { $"https://management.azure.com" })); 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<Node> ToReimage(Node node, bool done = false);
Async.Task SendStopIfFree(Node node); Async.Task SendStopIfFree(Node node);
IAsyncEnumerable<Node> SearchStates(Guid? poolId = default, IAsyncEnumerable<Node> SearchStates(Guid? poolId = default,
Guid? scalesetId = default, ScalesetId? scalesetId = default,
IEnumerable<NodeState>? states = default, IEnumerable<NodeState>? states = default,
PoolName? poolName = default, PoolName? poolName = default,
bool excludeUpdateScheduled = false, bool excludeUpdateScheduled = false,
@ -35,18 +35,18 @@ public interface INodeOperations : IStatefulOrm<Node, NodeState> {
Async.Task Delete(Node node, string reason); Async.Task Delete(Node node, string reason);
Async.Task ReimageLongLivedNodes(Guid scaleSetId); Async.Task ReimageLongLivedNodes(ScalesetId scaleSetId);
Async.Task<Node?> Create( Async.Task<Node?> Create(
Guid poolId, Guid poolId,
PoolName poolName, PoolName poolName,
Guid machineId, Guid machineId,
string? instanceId, string? instanceId,
Guid? scaleSetId, ScalesetId? scaleSetId,
string version, string version,
bool isNew = false); bool isNew = false);
IAsyncEnumerable<Node> GetDeadNodes(Guid scaleSetId, TimeSpan expirationPeriod); IAsyncEnumerable<Node> GetDeadNodes(ScalesetId scaleSetId, TimeSpan expirationPeriod);
Async.Task MarkTasksStoppedEarly(Node node, Error? error); Async.Task MarkTasksStoppedEarly(Node node, Error? error);
static readonly TimeSpan NODE_EXPIRATION_TIME = TimeSpan.FromHours(1.0); 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) { 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) { await TryGetNodeInfo(node) is NodeInfo nodeInfo) {
_logTracer.Info($"Setting scale-in protection on node {node.MachineId:Tag:MachineId}"); _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) { public async Task<OneFuzzResultVoid> ReleaseScaleInProtection(Node node) {
if (!node.DebugKeepNode && if (!node.DebugKeepNode &&
node.ScalesetId is Guid scalesetId && node.ScalesetId is ScalesetId scalesetId &&
await TryGetNodeInfo(node) is NodeInfo nodeInfo) { await TryGetNodeInfo(node) is NodeInfo nodeInfo) {
_logTracer.Info($"Removing scale-in protection on node {node.MachineId:Tag:MachineId}"); _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; return null;
} }
var scalesetResult = await _context.ScalesetOperations.GetById(scalesetId.Value); var scalesetResult = await _context.ScalesetOperations.GetById(scalesetId);
if (!scalesetResult.IsOk || scalesetResult.OkV == null) { if (!scalesetResult.IsOk || scalesetResult.OkV == null) {
return null; return null;
} }
@ -205,8 +205,8 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
return CanProcessNewWorkResponse.NotAllowed("node is scheduled to shrink"); return CanProcessNewWorkResponse.NotAllowed("node is scheduled to shrink");
} }
if (node.ScalesetId != null) { if (node.ScalesetId is not null) {
var scalesetResult = await _context.ScalesetOperations.GetById(node.ScalesetId.Value); var scalesetResult = await _context.ScalesetOperations.GetById(node.ScalesetId);
if (!scalesetResult.IsOk) { if (!scalesetResult.IsOk) {
return CanProcessNewWorkResponse.NotAllowed("invalid scaleset"); 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 /// This helps keep nodes on scalesets that use `latest` OS image SKUs
/// reasonably up-to-date with OS patches without disrupting running /// reasonably up-to-date with OS patches without disrupting running
/// fuzzing tasks with patch reboot cycles. /// 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); 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))) { 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( public static string SearchOutdatedQuery(
string oneFuzzVersion, string oneFuzzVersion,
Guid? poolId = null, Guid? poolId = null,
Guid? scalesetId = null, ScalesetId? scalesetId = null,
IEnumerable<NodeState>? states = null, IEnumerable<NodeState>? states = null,
PoolName? poolName = null, PoolName? poolName = null,
bool excludeUpdateScheduled = false, bool excludeUpdateScheduled = false,
@ -283,7 +283,7 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
} }
if (poolName is not null) { 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) { if (scalesetId is not null) {
@ -310,7 +310,7 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
IAsyncEnumerable<Node> SearchOutdated( IAsyncEnumerable<Node> SearchOutdated(
Guid? poolId = null, Guid? poolId = null,
Guid? scalesetId = null, ScalesetId? scalesetId = null,
IEnumerable<NodeState>? states = null, IEnumerable<NodeState>? states = null,
PoolName? poolName = null, PoolName? poolName = null,
bool excludeUpdateScheduled = false, bool excludeUpdateScheduled = false,
@ -366,11 +366,11 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
return updatedNode; return updatedNode;
} }
public IAsyncEnumerable<Node> GetDeadNodes(Guid scaleSetId, TimeSpan expirationPeriod) { public IAsyncEnumerable<Node> GetDeadNodes(ScalesetId scaleSetId, TimeSpan expirationPeriod) {
var minDate = DateTimeOffset.UtcNow - expirationPeriod; var minDate = DateTimeOffset.UtcNow - expirationPeriod;
var filter = $"heartbeat lt datetime'{minDate.ToString("o")}' or Timestamp lt datetime'{minDate.ToString("o")}'"; 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); return QueryAsync(query);
} }
@ -380,7 +380,7 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
PoolName poolName, PoolName poolName,
Guid machineId, Guid machineId,
string? instanceId, string? instanceId,
Guid? scaleSetId, ScalesetId? scaleSetId,
string version, string version,
bool isNew = false) { bool isNew = false) {
@ -495,7 +495,7 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
} }
public async Task<bool> CouldShrinkScaleset(Node node) { 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); var queue = new ShrinkQueue(scalesetId, _context.Queue, _logTracer);
if (await queue.ShouldShrink()) { if (await queue.ShouldShrink()) {
return true; return true;
@ -536,7 +536,7 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
public static string SearchStatesQuery( public static string SearchStatesQuery(
Guid? poolId = default, Guid? poolId = default,
Guid? scaleSetId = default, ScalesetId? scaleSetId = default,
IEnumerable<NodeState>? states = default, IEnumerable<NodeState>? states = default,
PoolName? poolName = default, PoolName? poolName = default,
int? numResults = default) { int? numResults = default) {
@ -544,15 +544,15 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
List<string> queryParts = new(); List<string> queryParts = new();
if (poolId is not null) { if (poolId is not null) {
queryParts.Add($"(pool_id eq '{poolId}')"); queryParts.Add(Query.CreateQueryFilter($"(pool_id eq {poolId})"));
} }
if (poolName is not null) { if (poolName is not null) {
queryParts.Add($"(PartitionKey eq '{poolName.String}')"); queryParts.Add(Query.CreateQueryFilter($"(PartitionKey eq {poolName})"));
} }
if (scaleSetId is not null) { if (scaleSetId is not null) {
queryParts.Add($"(scaleset_id eq '{scaleSetId}')"); queryParts.Add(Query.CreateQueryFilter($"(scaleset_id eq {scaleSetId})"));
} }
if (states is not null) { if (states is not null) {
@ -566,7 +566,7 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
public IAsyncEnumerable<Node> SearchStates( public IAsyncEnumerable<Node> SearchStates(
Guid? poolId = default, Guid? poolId = default,
Guid? scalesetId = default, ScalesetId? scalesetId = default,
IEnumerable<NodeState>? states = default, IEnumerable<NodeState>? states = default,
PoolName? poolName = default, PoolName? poolName = default,
bool excludeUpdateScheduled = false, bool excludeUpdateScheduled = false,

View File

@ -5,10 +5,10 @@ namespace Microsoft.OneFuzz.Service;
public interface IProxyForwardOperations : IOrm<ProxyForward> { 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); Forward ToForward(ProxyForward proxyForward);
Task<OneFuzzResult<ProxyForward>> UpdateOrCreate(Region region, Guid scalesetId, Guid machineId, int dstPort, int duration); Task<OneFuzzResult<ProxyForward>> UpdateOrCreate(Region region, ScalesetId scalesetId, Guid machineId, int dstPort, int duration);
Task<HashSet<Region>> RemoveForward(Guid scalesetId, Guid? machineId = null, int? dstPort = null, Guid? proxyId = null); 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 = var conditions =
new[] { new[] {
scalesetId is not null ? Query.CreateQueryFilter($"scaleset_id eq {scalesetId}") : null, 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 , machineId is not null ? Query.CreateQueryFilter($"machine_id eq {machineId}") : null ,
proxyId is not null ? Query.CreateQueryFilter($"proxy_id eq {proxyId}") : null , proxyId is not null ? Query.CreateQueryFilter($"proxy_id eq {proxyId}") : null ,
dstPort is not null ? Query.CreateQueryFilter($"dst_port eq {dstPort}") : 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); 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); var privateIp = await _context.IpOperations.GetScalesetInstanceIp(scalesetId, machineId);
if (privateIp == null) { 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 entries = await SearchForward(scalesetId: scalesetId, machineId: machineId, proxyId: proxyId, dstPort: dstPort).ToListAsync();
var regions = new HashSet<Region>(); 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<Scaleset> UpdateConfigs(Scaleset scaleSet);
Async.Task<OneFuzzResult<Scaleset>> GetById(Guid scalesetId); Async.Task<OneFuzzResult<Scaleset>> GetById(ScalesetId scalesetId);
IAsyncEnumerable<Scaleset> GetByObjectId(Guid objectId); IAsyncEnumerable<Scaleset> GetByObjectId(Guid objectId);
Async.Task<(bool, Scaleset)> CleanupNodes(Scaleset scaleSet); Async.Task<(bool, Scaleset)> CleanupNodes(Scaleset scaleSet);
@ -584,7 +584,7 @@ public class ScalesetOperations : StatefulOrm<Scaleset, ScalesetState, ScalesetO
} else { } else {
if (await new ShrinkQueue(scaleSet.ScalesetId, _context.Queue, _log).ShouldShrink()) { if (await new ShrinkQueue(scaleSet.ScalesetId, _context.Queue, _log).ShouldShrink()) {
toDelete[node.MachineId] = await _context.NodeOperations.SetHalt(node); 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); toDelete[node.MachineId] = await _context.NodeOperations.SetHalt(node);
} else { } else {
_logTracer.Info($"Node ready to reimage {node.MachineId:Tag:MachineId} {node.ScalesetId:Tag:ScalesetId} {node.State:Tag:State}"); _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; 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 data = QueryAsync(filter: Query.RowKey(scalesetId.ToString()));
var scaleSets = data is not null ? (await data.ToListAsync()) : null; 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>(); return new List<ScalesetNodeState>();
} }
var (nodes, azureNodes) = await ( var nodes = _context.NodeOperations.SearchStates(scalesetId: scaleset.ScalesetId);
_context.NodeOperations.SearchStates(scaleset.ScalesetId).ToListAsync().AsTask(),
_context.VmssOperations.ListInstanceIds(scaleset.ScalesetId));
var result = new List<ScalesetNodeState>(); var result = new List<ScalesetNodeState>();
foreach (var (machineId, instanceId) in azureNodes) { await foreach (var node in nodes) {
var node = nodes.FirstOrDefault(n => n.MachineId == machineId); result.Add(new ScalesetNodeState(node.MachineId, node.InstanceId, node.State));
result.Add(new ScalesetNodeState(
MachineId: machineId,
InstanceId: instanceId,
node?.State));
} }
return result; return result;

View File

@ -2,25 +2,40 @@
public record ShrinkEntry(Guid ShrinkId); public record ShrinkEntry(Guid ShrinkId);
public sealed class ShrinkQueue {
public class ShrinkQueue {
readonly Guid _baseId;
readonly IQueue _queueOps; readonly IQueue _queueOps;
readonly ILogTracer _log; readonly ILogTracer _log;
public ShrinkQueue(Guid baseId, IQueue queueOps, ILogTracer log) { public ShrinkQueue(ScalesetId baseId, IQueue queueOps, ILogTracer log)
_baseId = baseId; // 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; _queueOps = queueOps;
_log = log; _log = log;
} }
public static string ShrinkQueueNamePrefix => "to-shrink-"; public static string ShrinkQueueNamePrefix => "to-shrink-";
public override string ToString() { public override string ToString()
return $"{ShrinkQueueNamePrefix}{_baseId:N}"; => QueueName;
}
public string QueueName => ToString(); public string QueueName { get; }
public async Async.Task Clear() { public async Async.Task Clear() {
await _queueOps.ClearQueue(QueueName, StorageType.Config); await _queueOps.ClearQueue(QueueName, StorageType.Config);

View File

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

View File

@ -1,6 +1,7 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json; using System.Text.Json;
using Azure.Data.Tables; using Azure.Data.Tables;
using Microsoft.OneFuzz.Service;
using Microsoft.OneFuzz.Service.OneFuzzLib.Orm; using Microsoft.OneFuzz.Service.OneFuzzLib.Orm;
namespace ApiService.OneFuzzLib.Orm { namespace ApiService.OneFuzzLib.Orm {
@ -15,6 +16,8 @@ namespace ApiService.OneFuzzLib.Orm {
for (int i = 0; i < args.Length; i++) { for (int i = 0; i < args.Length; i++) {
if (args[i] is Guid g) { if (args[i] is Guid g) {
args[i] = g.ToString(); 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 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 ReimageRequested => _e.GetBoolProperty("reimage_requested");
public bool DeleteRequested => _e.GetBoolProperty("delete_requested"); public bool DeleteRequested => _e.GetBoolProperty("delete_requested");
public bool DebugKeepNode => _e.GetBoolProperty("debug_keep_node"); public bool DebugKeepNode => _e.GetBoolProperty("debug_keep_node");
@ -45,7 +45,7 @@ public class NodeApi : ApiBase {
.AddV("machine_id", machineId); .AddV("machine_id", machineId);
return Return<BooleanResult>(await Post(j)); 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() var j = new JsonObject()
.AddIfNotNullV("machine_id", machineId) .AddIfNotNullV("machine_id", machineId)
.AddIfNotNullEnumerableV("state", state) .AddIfNotNullEnumerableV("state", state)

View File

@ -91,7 +91,7 @@ public class ProxyApi : ApiBase {
base(endpoint, "/api/proxy", request, output) { 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() var root = new JsonObject()
.AddIfNotNullV("scaleset_id", scalesetId) .AddIfNotNullV("scaleset_id", scalesetId)
.AddIfNotNullV("machine_id", machineId) .AddIfNotNullV("machine_id", machineId)
@ -101,7 +101,7 @@ public class ProxyApi : ApiBase {
return IEnumerableResult<Proxy>(r.GetProperty("proxies")); 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() var root = new JsonObject()
.AddV("scaleset_id", scalesetId) .AddV("scaleset_id", scalesetId)
.AddV("machine_id", machineId) .AddV("machine_id", machineId)
@ -115,10 +115,10 @@ public class ProxyApi : ApiBase {
return Return<BooleanResult>(r); 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() var root = new JsonObject()
.AddV("scaleset_id", scalesetId) .AddV("scaleset_id", scalesetId)
.AddV("machin_id", machineId) .AddV("machine_id", machineId)
.AddV("dst_port", dstPort) .AddV("dst_port", dstPort)
.AddV("duration", duration); .AddV("duration", duration);

View File

@ -23,7 +23,7 @@ public class Scaleset : IFromJsonElement<Scaleset> {
public Scaleset(JsonElement e) => _e = e; public Scaleset(JsonElement e) => _e = e;
public static Scaleset Convert(JsonElement e) => new(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 PoolName => _e.GetStringProperty("pool_name");
public string State => _e.GetStringProperty("state"); public string State => _e.GetStringProperty("state");
@ -59,7 +59,7 @@ public class ScalesetApi : ApiBase {
base(endpoint, "/api/Scaleset", request, output) { } 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() var j = new JsonObject()
.AddIfNotNullV("scaleset_id", id) .AddIfNotNullV("scaleset_id", id)
.AddIfNotNullV("state", state) .AddIfNotNullV("state", state)
@ -84,14 +84,14 @@ public class ScalesetApi : ApiBase {
return Result<Scaleset>(await Post(rootScalesetCreate)); 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() var scalesetPatch = new JsonObject()
.AddV("scaleset_id", id) .AddV("scaleset_id", id)
.AddV("size", size); .AddV("size", size);
return Result<Scaleset>(await Patch(scalesetPatch)); 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() var scalesetDelete = new JsonObject()
.AddV("scaleset_id", id) .AddV("scaleset_id", id)
.AddV("now", now); .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 = ""; var currentState = "";
Scaleset newScaleset; Scaleset newScaleset;
do { do {

View File

@ -30,7 +30,7 @@ public abstract class AgentRegistrationTestsBase : FunctionTestBase {
private readonly Guid _machineId = Guid.NewGuid(); private readonly Guid _machineId = Guid.NewGuid();
private readonly Guid _poolId = 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()}"); private readonly PoolName _poolName = PoolName.Parse($"pool-{Guid.NewGuid()}");
[Fact] [Fact]

View File

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

View File

@ -20,40 +20,40 @@ sealed class TestVmssOperations : IVmssOperations {
/* below not implemented */ /* 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(); throw new NotImplementedException();
} }
public Task<bool> DeleteVmss(Guid name, bool? forceDeletion = null) { public Task<bool> DeleteVmss(ScalesetId name, bool? forceDeletion = null) {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task<OneFuzzResult<string>> GetInstanceId(Guid name, Guid vmId) { public Task<OneFuzzResult<string>> GetInstanceId(ScalesetId name, Guid vmId) {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task<VirtualMachineScaleSetData?> GetVmss(Guid name) { public Task<VirtualMachineScaleSetData?> GetVmss(ScalesetId name) {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task<long?> GetVmssSize(Guid name) { public Task<long?> GetVmssSize(ScalesetId name) {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task<IDictionary<Guid, string>> ListInstanceIds(Guid name) { public Task<IDictionary<Guid, string>> ListInstanceIds(ScalesetId name) {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public IAsyncEnumerable<VirtualMachineScaleSetVmResource> ListVmss(Guid name) { public IAsyncEnumerable<VirtualMachineScaleSetVmResource> ListVmss(ScalesetId name) {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task<OneFuzzResultVoid> ResizeVmss(Guid name, long capacity) { public Task<OneFuzzResultVoid> ResizeVmss(ScalesetId name, long capacity) {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task<OneFuzzResultVoid> UpdateExtensions(Guid name, IList<VirtualMachineScaleSetExtensionData> extensions) { public Task<OneFuzzResultVoid> UpdateExtensions(ScalesetId name, IList<VirtualMachineScaleSetExtensionData> extensions) {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -61,11 +61,11 @@ sealed class TestVmssOperations : IVmssOperations {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task<OneFuzzResultVoid> ReimageNodes(Guid scalesetId, IEnumerable<Node> nodes) { public Task<OneFuzzResultVoid> ReimageNodes(ScalesetId scalesetId, IEnumerable<Node> nodes) {
throw new NotImplementedException(); 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(); throw new NotImplementedException();
} }
} }

View File

@ -24,10 +24,12 @@ public class AzuriteNodeTest : NodeTestBase {
public abstract class NodeTestBase : FunctionTestBase { public abstract class NodeTestBase : FunctionTestBase {
public NodeTestBase(ITestOutputHelper output, IStorage storage) 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 _machineId = Guid.NewGuid();
private readonly Guid _scalesetId = Guid.NewGuid(); private readonly ScalesetId _scalesetId;
private readonly PoolName _poolName = PoolName.Parse($"pool-{Guid.NewGuid()}"); private readonly PoolName _poolName = PoolName.Parse($"pool-{Guid.NewGuid()}");
private readonly string _version = Guid.NewGuid().ToString(); 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() { public async Async.Task Search_SpecificScaleset_ReturnsErrorIfNoneFound() {
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context); 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 func = new ScalesetFunction(Logger, auth, Context);
var result = await func.Run(TestHttpRequestData.FromJson("GET", req)); var result = await func.Run(TestHttpRequestData.FromJson("GET", req));
@ -66,6 +66,33 @@ public abstract class ScalesetTestBase : FunctionTestBase {
Assert.Equal("[]", BodyAsString(result)); 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] [Fact]
public async Async.Task Create_Scaleset() { public async Async.Task Create_Scaleset() {
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context); var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);

View File

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

View File

@ -236,7 +236,9 @@ namespace Tests {
[Fact] [Fact]
public void TestEventSerialization() { 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 serialized = JsonSerializer.Serialize(expectedEvent, EntityConverter.GetJsonSerializerOptions());
var actualEvent = JsonSerializer.Deserialize<EventMessage>((string)serialized, EntityConverter.GetJsonSerializerOptions()); var actualEvent = JsonSerializer.Deserialize<EventMessage>((string)serialized, EntityConverter.GetJsonSerializerOptions());
Assert.Equal(expectedEvent, actualEvent); Assert.Equal(expectedEvent, actualEvent);

View File

@ -17,7 +17,7 @@ namespace Tests {
var query2 = NodeOperations.SearchStatesQuery(poolId: Guid.Parse("3b0426d3-9bde-4ae8-89ac-4edf0d3b3618")); var query2 = NodeOperations.SearchStatesQuery(poolId: Guid.Parse("3b0426d3-9bde-4ae8-89ac-4edf0d3b3618"));
Assert.Equal("((pool_id eq '3b0426d3-9bde-4ae8-89ac-4edf0d3b3618'))", query2); 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); Assert.Equal("((scaleset_id eq '4c96dd6b-9bdb-4758-9720-1010c244fa4b'))", query3);
var query4 = NodeOperations.SearchStatesQuery(states: new[] { NodeState.Free, NodeState.Done, NodeState.Ready }); var query4 = NodeOperations.SearchStatesQuery(states: new[] { NodeState.Free, NodeState.Done, NodeState.Ready });
@ -25,7 +25,7 @@ namespace Tests {
var query7 = NodeOperations.SearchStatesQuery( var query7 = NodeOperations.SearchStatesQuery(
poolId: Guid.Parse("3b0426d3-9bde-4ae8-89ac-4edf0d3b3618"), 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 }); 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); 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] [Fact]
public void QueryFilterTest() { 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 proxyId = Guid.Parse("4c96dd6b-9bdb-4758-9720-1010c244fa4b");
var region = "westus2"; var region = "westus2";
var outdated = false; var outdated = false;

View File

@ -1,4 +1,5 @@
using System.Text.Json; using System;
using System.Text.Json;
using Microsoft.OneFuzz.Service; using Microsoft.OneFuzz.Service;
using Xunit; using Xunit;
@ -42,4 +43,40 @@ public class ValidatedStringTests {
public void PoolNames(string name, bool valid) { public void PoolNames(string name, bool valid) {
Assert.Equal(valid, PoolName.IsValid(name)); 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, self,
*, *,
state: Optional[List[enums.NodeState]] = None, state: Optional[List[enums.NodeState]] = None,
scaleset_id: Optional[UUID_EXPANSION] = None, scaleset_id: Optional[str] = None,
pool_name: Optional[primitives.PoolName] = None, pool_name: Optional[primitives.PoolName] = None,
) -> List[models.Node]: ) -> List[models.Node]:
self.logger.debug("list nodes") self.logger.debug("list nodes")
scaleset_id_expanded: Optional[UUID] = None
if pool_name is not None: if pool_name is not None:
pool_name = primitives.PoolName( 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( return self._req_model_list(
"GET", "GET",
models.Node, models.Node,
data=requests.NodeSearch( 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( def _expand_scaleset_machine(
self, self,
scaleset_id: UUID_EXPANSION, scaleset_id: str,
machine_id: UUID_EXPANSION, machine_id: UUID_EXPANSION,
*, *,
include_auth: bool = False, include_auth: bool = False,
@ -1577,54 +1569,32 @@ class Scaleset(Endpoint):
), ),
) )
def shutdown( def shutdown(self, scaleset_id: str, *, now: bool = False) -> responses.BoolResult:
self, scaleset_id: UUID_EXPANSION, *, now: bool = False self.logger.debug("shutdown scaleset: %s (now: %s)", scaleset_id, now)
) -> 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)
return self._req_model( return self._req_model(
"DELETE", "DELETE",
responses.BoolResult, responses.BoolResult,
data=requests.ScalesetStop(scaleset_id=scaleset_id_expanded, now=now), data=requests.ScalesetStop(scaleset_id=scaleset_id, now=now),
) )
def get( def get(self, scaleset_id: str, *, include_auth: bool = False) -> models.Scaleset:
self, scaleset_id: UUID_EXPANSION, *, include_auth: bool = False
) -> models.Scaleset:
self.logger.debug("get scaleset: %s", scaleset_id) 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( return self._req_model(
"GET", "GET",
models.Scaleset, models.Scaleset,
data=requests.ScalesetSearch( data=requests.ScalesetSearch(
scaleset_id=scaleset_id_expanded, include_auth=include_auth scaleset_id=scaleset_id, include_auth=include_auth
), ),
) )
def update( def update(
self, scaleset_id: UUID_EXPANSION, *, size: Optional[int] = None self, scaleset_id: str, *, size: Optional[int] = None
) -> models.Scaleset: ) -> models.Scaleset:
self.logger.debug("update scaleset: %s", scaleset_id) 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( return self._req_model(
"PATCH", "PATCH",
models.Scaleset, models.Scaleset,
data=requests.ScalesetUpdate(scaleset_id=scaleset_id_expanded, size=size), data=requests.ScalesetUpdate(scaleset_id=scaleset_id, size=size),
) )
def list( def list(
@ -1645,7 +1615,7 @@ class ScalesetProxy(Endpoint):
def delete( def delete(
self, self,
scaleset_id: UUID_EXPANSION, scaleset_id: str,
machine_id: UUID_EXPANSION, machine_id: UUID_EXPANSION,
*, *,
dst_port: Optional[int] = None, dst_port: Optional[int] = None,
@ -1681,7 +1651,7 @@ class ScalesetProxy(Endpoint):
) )
def get( 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: ) -> responses.ProxyGetResult:
"""Get information about a specific job""" """Get information about a specific job"""
( (
@ -1705,7 +1675,7 @@ class ScalesetProxy(Endpoint):
def create( def create(
self, self,
scaleset_id: UUID_EXPANSION, scaleset_id: str,
machine_id: UUID_EXPANSION, machine_id: UUID_EXPANSION,
dst_port: int, dst_port: int,
*, *,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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