Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,77 @@ public async Task MongoDBServerExecutor_InitializeAsync_WithDiskFilter_CallsInit
"ServerApiClient should be initialized");
}

[Test]
public async Task MongoDBServerExecutor_ConfigureDisk_ResolvesTheMongoDBServiceUserForThePlatform()
{
// SETUP: A DiskFilter triggers the disk configuration workflow.
this.mockFixture.Parameters["DiskFilter"] = "BiggestSize";
this.mockFixture.Parameters["DiskDevicePath"] = "/dev/nvme0n1";

var volumes = new List<DiskVolume>();
var disk = new Disk(index: 0, devicePath: "/dev/nvme0n1", volumes: volumes, properties: null);
this.mockFixture.DiskManager.Setup(dm => dm.GetDisksAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Disk> { disk });

List<string> commandsExecuted = new List<string>();
this.mockFixture.ProcessManager.OnCreateProcess = (exe, args, workingDir) =>
{
commandsExecuted.Add($"{exe} {args}");
this.mockFixture.Process.StandardOutput.Clear();
this.mockFixture.Process.StandardOutput.Append("{ \"ok\" : 1 }");
this.mockFixture.Process.ExitCode = 0;
return this.mockFixture.Process;
};

var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters);

// ACT
await executor.InitializeAsync(EventContext.Persisted(), CancellationToken.None);

// ASSERT: The MongoDB service account is named 'mongodb' by Debian/Ubuntu packages but
// 'mongod' by RPM packages (Azure Linux, RHEL, Fedora). Hardcoding either one leaves the
// data directory owned by root on the other, and mongod then fails to create its journal.
string chownCommand = commandsExecuted.FirstOrDefault(cmd => cmd.Contains("chown", StringComparison.OrdinalIgnoreCase));

Assert.IsNotNull(chownCommand, "The data directory ownership command should have been executed.");

Assert.IsFalse(
chownCommand.Contains("chown -R mongodb:mongodb", StringComparison.OrdinalIgnoreCase),
"The ownership command must not hardcode the Debian-only 'mongodb' account.");

Assert.IsTrue(
chownCommand.Contains("id -u mongod", StringComparison.OrdinalIgnoreCase),
"The ownership command should probe for the RPM 'mongod' account.");

Assert.IsTrue(
chownCommand.Contains("echo mongodb", StringComparison.OrdinalIgnoreCase),
"The ownership command should fall back to the Debian 'mongodb' account.");
}

[Test]
public async Task MongoDBServerExecutor_InitializeAsync_OpensTheMongoDBPortOnTheLocalFirewall()
{
// SETUP: Capture the firewall entries the executor asks to be opened.
List<FirewallEntry> firewallEntries = new List<FirewallEntry>();
this.mockFixture.FirewallManager
.Setup(mgr => mgr.EnableInboundConnectionsAsync(It.IsAny<IEnumerable<FirewallEntry>>(), It.IsAny<CancellationToken>()))
.Callback<IEnumerable<FirewallEntry>, CancellationToken>((entries, token) => firewallEntries.AddRange(entries))
.Returns(Task.CompletedTask);

this.mockFixture.Parameters["Port"] = 27017;

var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters);

// ACT
await executor.InitializeAsync(EventContext.Persisted(), CancellationToken.None);

// ASSERT: Distros such as Azure Linux apply a default-deny inbound policy. Without opening
// the port, the YCSB client times out connecting to the server and loads zero records.
Assert.AreEqual(1, firewallEntries.Count, "The MongoDB port should have been opened on the local firewall.");
Assert.AreEqual("tcp", firewallEntries[0].Protocol);
CollectionAssert.AreEqual(new List<int> { 27017 }, firewallEntries[0].Ports.ToList());
}

[Test]
public async Task MongoDBServerExecutor_InitializeAsync_CallsConfigureBindAddressAndStartServer()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ namespace VirtualClient.Actions
[SupportedPlatforms("linux-arm64,linux-x64")]
public class MongoDBServerExecutor : MongoDBExecutor
{
/// <summary>
/// The MongoDB service account created by RPM-based packages (Azure Linux, RHEL, Fedora).
/// </summary>
private const string RpmServiceUser = "mongod";

/// <summary>
/// The MongoDB service account created by Debian-based packages (Ubuntu, Debian).
/// </summary>
private const string DebianServiceUser = "mongodb";

private IFileSystem fileSystem;
private ISystemManagement systemManagement;
private bool disposed;
Expand Down Expand Up @@ -68,6 +78,9 @@ protected override async Task InitializeAsync(EventContext telemetryContext, Can

this.InitializeApiClients();

await MongoDBServerExecutor.OpenFirewallPortsAsync(this.Port, this.systemManagement.FirewallManager, cancellationToken)
.ConfigureAwait(false);

// Initialize disk if DiskFilter is specified
if (!string.IsNullOrWhiteSpace(this.DiskFilter))
{
Expand Down Expand Up @@ -122,6 +135,25 @@ protected override void Dispose(bool disposing)
}
}

/// <summary>
/// Opens the MongoDB port on the local firewall so that the client instance is able to
/// connect to the MongoDB server. Distros such as Azure Linux apply a default-deny policy
/// to inbound traffic, so the port must be opened explicitly.
/// </summary>
private static Task OpenFirewallPortsAsync(int port, IFirewallManager firewallManager, CancellationToken cancellationToken)
{
return firewallManager.EnableInboundConnectionsAsync(
new List<FirewallEntry>
{
new FirewallEntry(
"MongoDB: Allow Multiple Machines communications",
"Allows individual machine instances to communicate with other machine in client-server scenario",
"tcp",
new List<int> { port })
},
cancellationToken);
}

/// <summary>
/// Configures MongoDB to bind to all network interfaces.
/// </summary>
Expand Down Expand Up @@ -266,10 +298,16 @@ await this.ExecuteMongoDBCommandAsync(
telemetryContext,
cancellationToken).ConfigureAwait(false);

// Set permissions
// Set permissions. The MongoDB service account name differs by package format:
// Debian/Ubuntu packages create 'mongodb' whereas RPM-based distributions
// (Azure Linux, RHEL, Fedora) create 'mongod'. Resolve it at runtime so the
// data directory is owned by the account mongod actually runs as.
string resolveServiceUser = $"MONGO_USER=$(id -u {MongoDBServerExecutor.RpmServiceUser} >/dev/null 2>&1 && echo {MongoDBServerExecutor.RpmServiceUser} || echo {MongoDBServerExecutor.DebianServiceUser}); " +
$"sudo chown -R $MONGO_USER:$MONGO_USER {mongoDataPath}";

await this.ExecuteMongoDBCommandAsync(
"bash",
$"-c \"sudo chown -R mongodb:mongodb {mongoDataPath}\"",
$"-c \"{resolveServiceUser}\"",
"SetPermissions",
telemetryContext,
cancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,121 @@ public void VirtualClientComponentIsSupportedRespectsSupportedPlatformAttribute(
Assert.IsFalse(VirtualClientComponent.IsSupported(component));
}

[Test]
public void VirtualClientComponentSupportedLinuxDistributionsIsEmptyWhenTheParameterIsNotDefined()
{
TestVirtualClientComponent component = new TestVirtualClientComponent(this.mockFixture.Dependencies, this.mockFixture.Parameters);

Assert.IsNotNull(component.SupportedLinuxDistributions);
Assert.IsEmpty(component.SupportedLinuxDistributions);
}

[Test]
public void VirtualClientComponentSupportedLinuxDistributionsParsesDelimitedValues()
{
this.mockFixture.Parameters[nameof(VirtualClientComponent.SupportedLinuxDistributions)] = "Debian,Ubuntu";
TestVirtualClientComponent component = new TestVirtualClientComponent(this.mockFixture.Dependencies, this.mockFixture.Parameters);

CollectionAssert.AreEqual(new List<string> { "Debian", "Ubuntu" }, component.SupportedLinuxDistributions);
}

[Test]
public void VirtualClientComponentIsSupportedWhenTheSupportedLinuxDistributionsParameterIsNotDefined()
{
// A component that does not define the parameter is not filtered on the Linux distribution
// at all and thus executes on any distribution.
this.mockFixture.Setup(PlatformID.Unix);
this.SetupLinuxDistribution(LinuxDistribution.AzureLinux, LinuxUpstreamDistribution.Fedora);

TestVirtualClientComponent component = new TestVirtualClientComponent(this.mockFixture.Dependencies, this.mockFixture.Parameters);

Assert.IsTrue(component.IsSupported());
}

[Test]
[TestCase("AzureLinux")]
[TestCase("Debian,Ubuntu,AzureLinux")]
[TestCase("azurelinux")]
[TestCase("AZURELINUX")]
[TestCase(" AzureLinux , Ubuntu ")]
public void VirtualClientComponentIsSupportedWhenTheLinuxDistributionMatchesTheSupportedLinuxDistributions(string supportedDistributions)
{
this.mockFixture.Setup(PlatformID.Unix);
this.SetupLinuxDistribution(LinuxDistribution.AzureLinux, LinuxUpstreamDistribution.Fedora);
this.mockFixture.Parameters[nameof(VirtualClientComponent.SupportedLinuxDistributions)] = supportedDistributions;

TestVirtualClientComponent component = new TestVirtualClientComponent(this.mockFixture.Dependencies, this.mockFixture.Parameters);

Assert.IsTrue(component.IsSupported());
}

[Test]
[TestCase("Debian,Ubuntu")]
[TestCase("Ubuntu")]
[TestCase("Fedora")]
public void VirtualClientComponentIsNotSupportedWhenTheLinuxDistributionDoesNotMatchTheSupportedLinuxDistributions(string supportedDistributions)
{
this.mockFixture.Setup(PlatformID.Unix);
this.SetupLinuxDistribution(LinuxDistribution.AzureLinux, LinuxUpstreamDistribution.Fedora);
this.mockFixture.Parameters[nameof(VirtualClientComponent.SupportedLinuxDistributions)] = supportedDistributions;

TestVirtualClientComponent component = new TestVirtualClientComponent(this.mockFixture.Dependencies, this.mockFixture.Parameters);

Assert.IsFalse(component.IsSupported());
}

[Test]
public void VirtualClientComponentIsSupportedWhenTheLinuxDistributionMatchesADownstreamDistribution()
{
// Ubuntu is a downstream distribution of Debian. The match is made on the distribution
// itself and not on the upstream distribution.
this.mockFixture.Setup(PlatformID.Unix);
this.SetupLinuxDistribution(LinuxDistribution.Ubuntu, LinuxUpstreamDistribution.Debian);
this.mockFixture.Parameters[nameof(VirtualClientComponent.SupportedLinuxDistributions)] = "Debian,Ubuntu";

TestVirtualClientComponent component = new TestVirtualClientComponent(this.mockFixture.Dependencies, this.mockFixture.Parameters);

Assert.IsTrue(component.IsSupported());
}

[Test]
public void VirtualClientComponentIsNotSupportedOnNonLinuxSystemsWhenTheSupportedLinuxDistributionsParameterIsDefined()
{
// A component defining the parameter is describing Linux-specific behavior and thus
// is never supported on non-Linux systems.
this.mockFixture.Setup(PlatformID.Win32NT);
this.mockFixture.Parameters[nameof(VirtualClientComponent.SupportedLinuxDistributions)] = "Ubuntu";

TestVirtualClientComponent component = new TestVirtualClientComponent(this.mockFixture.Dependencies, this.mockFixture.Parameters);

Assert.IsFalse(component.IsSupported());
}

[Test]
public void VirtualClientComponentIsNotSupportedWhenTheSupportedPlatformsDoNotMatchEvenIfTheLinuxDistributionMatches()
{
this.mockFixture.Setup(PlatformID.Unix, System.Runtime.InteropServices.Architecture.X64);
this.SetupLinuxDistribution(LinuxDistribution.AzureLinux, LinuxUpstreamDistribution.Fedora);
this.mockFixture.Parameters[nameof(VirtualClientComponent.SupportedPlatforms)] = "linux-arm64";
this.mockFixture.Parameters[nameof(VirtualClientComponent.SupportedLinuxDistributions)] = "AzureLinux";

TestVirtualClientComponent component = new TestVirtualClientComponent(this.mockFixture.Dependencies, this.mockFixture.Parameters);

Assert.IsFalse(component.IsSupported());
}

private void SetupLinuxDistribution(LinuxDistribution distribution, LinuxUpstreamDistribution upstreamDistribution)
{
this.mockFixture.SystemManagement
.Setup(sm => sm.GetLinuxDistributionAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new LinuxDistributionInfo
{
Name = distribution.ToString(),
Distribution = distribution,
UpstreamDistribution = upstreamDistribution
});
}

private class TestVirtualClientComponent : VirtualClientComponent
{
public TestVirtualClientComponent(VirtualClientComponent component)
Expand All @@ -743,6 +858,11 @@ public TestVirtualClientComponent(IServiceCollection dependencies, IDictionary<s
return base.IsInRole(role);
}

public new bool IsSupported()
{
return base.IsSupported();
}

protected override Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken)
{
this.OnExecute?.Invoke(telemetryContext, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,19 @@ protected set
/// </summary>
public DateTime StartTime { get; private set; }

/// <summary>
/// Parameter describes the Linux distributions (e.g. AzureLinux, Ubuntu) for which the component
/// is supported. A component defining this parameter is never executed on non-Linux systems.
/// </summary>
public IEnumerable<string> SupportedLinuxDistributions
{
get
{
this.Parameters.TryGetCollection<string>(nameof(this.SupportedLinuxDistributions), out IEnumerable<string> distributions);
return distributions ?? Array.Empty<string>();
}
}

/// <summary>
/// Parameter describes the platform/architectures for which the component is supported.
/// </summary>
Expand Down Expand Up @@ -943,6 +956,10 @@ protected virtual bool IsSupported()
{
isSupported = false;
}
else if (this.SupportedLinuxDistributions?.Any() == true && !this.IsSupportedLinuxDistribution())
{
isSupported = false;
}
else if (this.Layout?.Clients?.Count() >= 2 && this.Roles?.Any() == true)
{
// Execution Criteria
Expand Down Expand Up @@ -1018,5 +1035,21 @@ private bool IsMe(ClientInstance clientInstance)

return isMatch;
}

private bool IsSupportedLinuxDistribution()
{
bool isSupported = false;
if (this.Platform == PlatformID.Unix)
{
LinuxDistributionInfo distribution = this.systemInfo.GetLinuxDistributionAsync(CancellationToken.None)
.GetAwaiter().GetResult();

isSupported = this.SupportedLinuxDistributions.Contains(
distribution.Distribution.ToString(),
StringComparer.OrdinalIgnoreCase);
}

return isSupported;
}
}
}
Loading
Loading