Skip to content

Commit 3e96703

Browse files
committed
Add feature to enable defining environment layout directly on the command line in addition to a file path.
1 parent 67d8655 commit 3e96703

9 files changed

Lines changed: 198 additions & 82 deletions

File tree

src/VirtualClient/VirtualClient.Contracts/ClientInstance.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,5 +177,14 @@ public override int GetHashCode()
177177

178178
return this.hashCode.Value;
179179
}
180+
181+
/// <summary>
182+
/// Returns a string representation of the client instance
183+
/// (e.g. client01,10.1.0.1,Client).
184+
/// </summary>
185+
public override string ToString()
186+
{
187+
return string.Join(",", this.Name, this.IPAddress, this.Role);
188+
}
180189
}
181190
}

src/VirtualClient/VirtualClient.Contracts/EnvironmentLayout.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
namespace VirtualClient.Contracts
55
{
66
using System.Collections.Generic;
7+
using System.Linq;
78
using Newtonsoft.Json;
89
using VirtualClient.Common.Extensions;
910

@@ -25,10 +26,18 @@ public EnvironmentLayout(IEnumerable<ClientInstance> clients)
2526
}
2627

2728
/// <summary>
28-
/// The type of IP address of the Virtual Client instance
29-
/// (e.g. Public vs. Private).
29+
/// The set of client instances that are part of the experiment.
3030
/// </summary>
3131
[JsonProperty(PropertyName = "clients", Required = Required.Always)]
3232
public IEnumerable<ClientInstance> Clients { get; }
33+
34+
/// <summary>
35+
/// Returns a string representation of the environment layout
36+
/// (e.g. client01,10.1.0.1,Client;client02,10.1.0.2,Server).
37+
/// </summary>
38+
public override string ToString()
39+
{
40+
return string.Join(";", this.Clients.Select(client => client.ToString()));
41+
}
3342
}
3443
}

src/VirtualClient/VirtualClient.Main/CommandLineParser.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public static CommandLineParser Create(IEnumerable<string> args, CancellationTok
105105
OptionFactory.CreateKeyVaultStoreOption(required: false),
106106

107107
// --layout-path
108-
OptionFactory.CreateLayoutPathOption(required: false),
108+
OptionFactory.CreateLayoutOption(required: false),
109109

110110
// --log-dir
111111
OptionFactory.CreateLogDirectoryOption(required: false),
@@ -344,7 +344,7 @@ private static Command CreateBootstrapSubcommand(string[] args, CancellationToke
344344
OptionFactory.CreateIterationsOption(required: false),
345345

346346
// --layout-path (for integration only. not used.)
347-
OptionFactory.CreateLayoutPathOption(required: false),
347+
OptionFactory.CreateLayoutOption(required: false),
348348

349349
// --log-dir
350350
OptionFactory.CreateLogDirectoryOption(required: false),

src/VirtualClient/VirtualClient.Main/ExecuteProfileCommand.cs

Lines changed: 11 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ protected bool IsPowerShell
8080
public bool InstallDependencies { get; set; }
8181

8282
/// <summary>
83-
/// The path to the environment layout .json file.
83+
/// The environment layout definition.
8484
/// </summary>
85-
public string LayoutPath { get; set; }
85+
public EnvironmentLayout Layout { get; set; }
8686

8787
/// <summary>
8888
/// A seed that can be used to guarantee identical randomization bases for workloads that
@@ -532,42 +532,6 @@ protected async Task<ExecutionProfile> InitializeProfilesAsync(IEnumerable<strin
532532
return profile;
533533
}
534534

535-
/// <summary>
536-
/// Loads/Reads the environment layout file provided to the Virtual Client on the command line.
537-
/// </summary>
538-
protected async Task<EnvironmentLayout> ReadEnvironmentLayoutAsync(IServiceCollection dependencies, CancellationToken cancellationToken)
539-
{
540-
EnvironmentLayout layout = null;
541-
542-
if (!cancellationToken.IsCancellationRequested)
543-
{
544-
if (!string.IsNullOrWhiteSpace(this.LayoutPath))
545-
{
546-
ISystemManagement systemManagement = dependencies.GetService<ISystemManagement>();
547-
ILogger logger = dependencies.GetService<ILogger>();
548-
549-
string layoutFullPath = systemManagement.PlatformSpecifics.StandardizePath(Path.GetFullPath(this.LayoutPath));
550-
551-
if (!systemManagement.FileSystem.File.Exists(layoutFullPath))
552-
{
553-
throw new StartupException(
554-
$"Invalid path specified. An environment layout file does not exist at path '{layoutFullPath}'.",
555-
ErrorReason.LayoutInvalid);
556-
}
557-
558-
string layoutContent = await RetryPolicies.FileOperations
559-
.ExecuteAsync(() =>
560-
{
561-
return systemManagement.FileSystem.File.ReadAllTextAsync(layoutFullPath);
562-
});
563-
564-
layout = layoutContent.FromJson<EnvironmentLayout>();
565-
}
566-
}
567-
568-
return layout;
569-
}
570-
571535
/// <summary>
572536
/// Loads/reads the execution profile file provided to the Virtual Client on the command line.
573537
/// </summary>
@@ -705,7 +669,7 @@ protected void SetHostMetadataTelemetryProperties(IEnumerable<string> profiles,
705669
new Dictionary<string, object>
706670
{
707671
{ "exitWait", this.ExitWait },
708-
{ "layout", this.LayoutPath },
672+
{ "layout", this.Layout.ToString() },
709673
{ "logToFile", this.LogToFile },
710674
{ "iterations", this.Iterations?.ProfileIterations },
711675
{ "profiles", string.Join(",", profiles.Select(p => Path.GetFileName(p))) },
@@ -755,16 +719,10 @@ private async Task ExecuteProfileDependenciesInstallationAsync(IEnumerable<strin
755719

756720
this.SetGlobalTelemetryProperties(profile);
757721

758-
// The environment layout provides information for other Virtual Client instances
759-
// that may be a part of the workload execution. This enables support for client/server
760-
// workload requirements.
761-
EnvironmentLayout environmentLayout = await this.ReadEnvironmentLayoutAsync(dependencies, cancellationToken)
762-
.ConfigureAwait(false);
763-
764-
if (environmentLayout != null)
722+
if (this.Layout != null)
765723
{
766-
dependencies.AddSingleton<EnvironmentLayout>(environmentLayout);
767-
telemetryContext.AddContext("layout", environmentLayout);
724+
dependencies.AddSingleton<EnvironmentLayout>(this.Layout);
725+
telemetryContext.AddContext("layout", this.Layout);
768726
}
769727

770728
logger.LogMessage($"ProfileExecution.Begin", telemetryContext);
@@ -846,16 +804,10 @@ private async Task ExecuteProfileAsync(IEnumerable<string> profiles, IServiceCol
846804

847805
this.SetGlobalTelemetryProperties(profile);
848806

849-
// The environment layout provides information for other Virtual Client instances
850-
// that may be a part of the workload execution. This enables support for client/server
851-
// workload requirements.
852-
EnvironmentLayout environmentLayout = await this.ReadEnvironmentLayoutAsync(dependencies, cancellationToken)
853-
.ConfigureAwait(false);
854-
855-
if (environmentLayout != null)
807+
if (this.Layout != null)
856808
{
857-
dependencies.AddSingleton<EnvironmentLayout>(environmentLayout);
858-
telemetryContext.AddContext("layout", environmentLayout);
809+
dependencies.AddSingleton<EnvironmentLayout>(this.Layout);
810+
telemetryContext.AddContext("layout", this.Layout);
859811
}
860812

861813
logger.LogMessage($"ProfileExecution.Begin", telemetryContext);
@@ -956,10 +908,9 @@ private void LogContextToConsole(IServiceCollection dependencies)
956908
ConsoleLogger.Default.LogMessage($"State Directory: {platformSpecifics.StateDirectory}", telemetryContext);
957909
ConsoleLogger.Default.LogMessage($"Temp Directory: {platformSpecifics.TempDirectory}", telemetryContext);
958910

959-
if (!string.IsNullOrWhiteSpace(this.LayoutPath))
911+
if (this.Layout != null)
960912
{
961-
string layoutFullPath = platformSpecifics.StandardizePath(Path.GetFullPath(this.LayoutPath));
962-
ConsoleLogger.Default.LogMessage($"Environment Layout: {layoutFullPath}", telemetryContext);
913+
ConsoleLogger.Default.LogMessage($"Environment Layout: {this.Layout}", telemetryContext);
963914
}
964915

965916
if (this.Timeout?.Duration != null)

src/VirtualClient/VirtualClient.Main/OptionFactory.cs

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ namespace VirtualClient
1212
using System.IO.Abstractions;
1313
using System.Linq;
1414
using System.Net;
15+
using System.Runtime.InteropServices;
1516
using System.Text.RegularExpressions;
1617
using Microsoft.CodeAnalysis;
1718
using Microsoft.Extensions.Logging;
19+
using VirtualClient.Common.Contracts;
1820
using VirtualClient.Common.Extensions;
1921
using VirtualClient.Contracts;
2022
using VirtualClient.Contracts.Extensibility;
@@ -29,6 +31,7 @@ public static class OptionFactory
2931
internal const string HtmlQuote = "&quot;";
3032
private static readonly ICertificateManager defaultCertificateManager = new CertificateManager();
3133
private static readonly IFileSystem defaultFileSystem = new FileSystem();
34+
private static readonly PlatformSpecifics defaultPlatformSpecifics = new PlatformSpecifics(Environment.OSVersion.Platform, RuntimeInformation.ProcessArchitecture);
3235
private static readonly char[] argumentTrimChars = new char[] { '\'', '"', ' ' };
3336

3437
/// <summary>
@@ -671,21 +674,28 @@ public static Option CreateKeyVaultStoreOption(bool required = false, object def
671674
}
672675

673676
/// <summary>
674-
/// Command line option defines the path to the environment layout file.
677+
/// Command line option defines the environment layout or a path to the layout file.
675678
/// </summary>
676679
/// <param name="required">Sets this option as required.</param>
677680
/// <param name="defaultValue">Sets the default value when none is provided.</param>
678-
public static Option CreateLayoutPathOption(bool required = true, object defaultValue = null)
681+
/// <param name="fileSystem">Optional parameter to use to validate file system paths.</param>
682+
/// <param name="platformSpecifics">Optional parameter defines the certificate manager to use for accessing certificates on the system.</param>
683+
public static Option CreateLayoutOption(bool required = true, object defaultValue = null, IFileSystem fileSystem = null, PlatformSpecifics platformSpecifics = null)
679684
{
680685
// Note:
681686
// Only the first 3 of these will display in help output (i.e. --help).
682-
Option<string> option = new Option<string>(new string[] { "--layout", "--layout-path", })
683-
{
684-
Name = "LayoutPath",
685-
Description = "The path to the environment layout .json file required for client/server operations. The contents of this " +
686-
"file are used by the self-hosted API service for example to enable individual instances of the application running on different " +
687-
"systems to synchronize with each other.",
688-
ArgumentHelpName = "path",
687+
Option<EnvironmentLayout> option = new Option<EnvironmentLayout>(
688+
new string[] { "--layout", "--layout-path", },
689+
parseArgument: result => OptionFactory.ParseEnvironmentLayout(
690+
result,
691+
fileSystem ?? OptionFactory.defaultFileSystem,
692+
platformSpecifics ?? OptionFactory.defaultPlatformSpecifics))
693+
{
694+
Name = "Layout",
695+
Description =
696+
"An environment layout definition or path to a *.json file defining the set of systems associated with client/server operations. This definition " +
697+
"enable individual instances of the application running on different systems to synchronize with each other.",
698+
ArgumentHelpName = "definition",
689699
AllowMultipleArgumentsPerToken = false
690700
};
691701

@@ -1682,6 +1692,65 @@ private static DependencyStore ParseBlobStore(ArgumentResult parsedResult, strin
16821692
return store;
16831693
}
16841694

1695+
private static EnvironmentLayout ParseEnvironmentLayout(ArgumentResult parsedResult, IFileSystem fileSystem, PlatformSpecifics platformSpecifics)
1696+
{
1697+
EnvironmentLayout layout = null;
1698+
1699+
// A layout can be a path to a file or an inline definition:
1700+
//
1701+
// e.g. inline
1702+
// --layout "client01,10.1.0.1,Client;client02,10.1.0.2,Server"
1703+
//
1704+
// e.g. file path
1705+
// --layout-path="C:\Users\Any\VirtualClient\layout.json"
1706+
//
1707+
string layoutValue = parsedResult.Tokens?.FirstOrDefault()?.Value;
1708+
if (!string.IsNullOrWhiteSpace(layoutValue) && Regex.IsMatch(layoutValue, "[,;]+"))
1709+
{
1710+
List<ClientInstance> clientsInstances = new List<ClientInstance>();
1711+
string[] clients = layoutValue.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
1712+
1713+
if (clients?.Any() == true)
1714+
{
1715+
foreach (string client in clients)
1716+
{
1717+
// e.g.
1718+
// client01,10.1.0.1,Client
1719+
// client02,10.1.0.2,Server
1720+
string[] clientParts = client.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
1721+
1722+
if (clientParts?.Length == 3)
1723+
{
1724+
clientsInstances.Add(new ClientInstance(clientParts[0], ipAddress: clientParts[1], role:clientParts[2]));
1725+
}
1726+
}
1727+
}
1728+
1729+
if (!clientsInstances.Any())
1730+
{
1731+
throw new ArgumentException(
1732+
"Invalid layout definition. The environment layout definition provided is not in a valid format: " +
1733+
"{client_name},{ip_address},{role};{client_name},{ip_address},{role} (e.g. client01,10.1.0.1,Client;client02,10.1.0.2,Server).");
1734+
}
1735+
1736+
layout = new EnvironmentLayout(clientsInstances);
1737+
}
1738+
else
1739+
{
1740+
string layoutFullPath = platformSpecifics.StandardizePath(Path.GetFullPath(layoutValue));
1741+
1742+
if (!fileSystem.File.Exists(layoutFullPath))
1743+
{
1744+
throw new ArgumentException($"Invalid path specified. An environment layout file does not exist at path '{layoutFullPath}'.");
1745+
}
1746+
1747+
string layoutContent = RetryPolicies.Synchronous.FileOperations.Execute(() => fileSystem.File.ReadAllText(layoutFullPath));
1748+
layout = layoutContent.FromJson<EnvironmentLayout>();
1749+
}
1750+
1751+
return layout;
1752+
}
1753+
16851754
private static DependencyStore ParseKeyVaultStore(ArgumentResult parsedResult, string storeName, ICertificateManager certificateManager, IFileSystem fileSystem)
16861755
{
16871756
string endpoint = OptionFactory.GetValue(parsedResult);

src/VirtualClient/VirtualClient.UnitTests/CommandLineOptionTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -449,8 +449,8 @@ public void VirtualClientThrowsWhenAnUnrecognizedOptionIsSuppliedOnTheCommandLin
449449
[TestCase("--exit-wait", "00:10:00")]
450450
[TestCase("--iterations", "3")]
451451
[TestCase("--key-vault", "https://anyvault.vault.windows.net")]
452-
[TestCase("--layout-path", "C:\\any\\path\\to\\layout.json")]
453-
[TestCase("--layout", "C:\\any\\path\\to\\layout.json")]
452+
[TestCase("--layout-path", "client01,10.1.0.1,Client;client02,10.1.0.2,Server")]
453+
[TestCase("--layout", "client01,10.1.0.1,Client;client02,10.1.0.2,Server")]
454454
[TestCase("--logger", "file")]
455455
[TestCase("--log-dir", "C:\\any\\path\\to\\logs")]
456456
[TestCase("--log-level", "2")]
@@ -703,7 +703,7 @@ public void VirtualClientBootstrapCommandHandlesNoOpArguments()
703703
"--package", "anypackage.1.0.0.zip",
704704
"--package-store", "https://anystorageaccount.blob.core.windows.net/?sv=2020-08-04&ss=b",
705705
"--iterations", "1",
706-
"--layout-path", "/home/user/any/layout.json"
706+
"--layout", "client01,10.1.2.3,Client;client02,10.1.2.4,Server"
707707
};
708708

709709
Assert.DoesNotThrow(() =>

0 commit comments

Comments
 (0)