Skip to content
Closed
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 @@ -3,7 +3,7 @@
"Metadata": {
"RecommendedMinimumExecutionTime": "00:15:00",
"SupportedPlatforms": "linux-x64,linux-arm64",
"SupportedOperatingSystems": "Ubuntu",
"SupportedOperatingSystems": "AzureLinux,Ubuntu",
"Notes": "Database sizes vary based on RecordCount: Small (500000 records) ~8-10 GB, Medium (2500000 records) ~40-50 GB, Large (20000000 records) ~320-400 GB, XLarge (55000000 records) ~900 GB-1 TB. Default configuration uses Medium size (~40-50 GB) with 2500000 records."
},
"Parameters": {
Expand Down Expand Up @@ -107,7 +107,7 @@
"MetricScenario": "Read_Latest_95_Read_5_Insert_{RecordCount}_Records",
"Database": "$.Parameters.Database",
"Port": "$.Parameters.Port",
"RunCommand": "run {Database} -s -p maxexecutiontime={Duration.TotalSeconds} -p operationcount={OperationCount} -p recordcount={RecordCount} -threads {ThreadCount} -p fieldcount={FieldCount} -p fieldlength={FieldLength} -p mongodb.url=mongodb://{ServerIP}:{Port}/ycsb -P {PackagePath:ycsb}/ycsb-0.17.0/workloads/workloadd",
"RunCommand": "run {Database} -s -p maxexecutiontime={Duration.TotalSeconds} -p operationcount={OperationCount} -p recordcount={RecordCount} -threads {ThreadCount} -p fieldcount={FieldCount} -p fieldlength={FieldLength} -p mongodb.url=mongodb://{ServerIP}:{Port}/ycsb -p mongodb.upsert=true -P {PackagePath:ycsb}/ycsb-0.17.0/workloads/workloadd",
"Duration": "$.Parameters.Duration",
"ThreadCount": "$.Parameters.ThreadCount",
"OperationCount": "5000000",
Expand All @@ -126,7 +126,7 @@
"MetricScenario": "Short_Range_Scan_95_Scan_5_Insert_{RecordCount}_Records",
"Database": "$.Parameters.Database",
"Port": "$.Parameters.Port",
"RunCommand": "run {Database} -s -p maxexecutiontime={Duration.TotalSeconds} -p operationcount={OperationCount} -p recordcount={RecordCount} -threads {ThreadCount} -p fieldcount={FieldCount} -p fieldlength={FieldLength} -p mongodb.url=mongodb://{ServerIP}:{Port}/ycsb -P {PackagePath:ycsb}/ycsb-0.17.0/workloads/workloade",
"RunCommand": "run {Database} -s -p maxexecutiontime={Duration.TotalSeconds} -p operationcount={OperationCount} -p recordcount={RecordCount} -threads {ThreadCount} -p fieldcount={FieldCount} -p fieldlength={FieldLength} -p mongodb.url=mongodb://{ServerIP}:{Port}/ycsb -p mongodb.upsert=true -P {PackagePath:ycsb}/ycsb-0.17.0/workloads/workloade",
"Duration": "$.Parameters.Duration",
"ThreadCount": "$.Parameters.ThreadCount",
"OperationCount": "5000000",
Expand Down Expand Up @@ -173,50 +173,55 @@
"Type": "LinuxPackageInstallation",
"Parameters": {
"Scenario": "InstallMongoPrereqs",
"Packages-Apt": "gnupg,curl",
"Packages-Dnf": "gnupg,curl",
"Packages-Yum": "gnupg,curl",
"Packages-Zypper": "gpg2,curl"
"Packages-Apt": "gnupg,curl,lshw",
"Packages-Dnf": "gnupg2,curl,lshw",
"Packages-Yum": "gnupg2,curl,lshw",
"Packages-Zypper": "gpg2,curl,lshw"
}
},
{
"Type": "ExecuteCommand",
"Parameters": {
"Scenario": "MongoGPGKey",
"SupportedPlatforms": "linux-x64,linux-arm64",
"Command": "curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc -o server-8.0.asc"
"UseShell": true,
"Command": "set -e; if command -v apt-get >/dev/null 2>&1; then curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc -o /tmp/mongodb-server-8.0.asc; elif command -v dnf >/dev/null 2>&1 || command -v tdnf >/dev/null 2>&1; then sudo rpm --import https://pgp.mongodb.com/server-8.0.asc; else echo 'Unsupported package manager. MongoDB repository setup requires apt-get or dnf.' 1>&2; exit 1; fi"
}
},
{
"Type": "ExecuteCommand",
"Parameters": {
"Scenario": "MongoGPGDearmor",
"SupportedPlatforms": "linux-x64,linux-arm64",
"Command": "sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/mongodb-server-8.0.gpg server-8.0.asc"
"UseShell": true,
"Command": "set -e; if command -v apt-get >/dev/null 2>&1; then sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/mongodb-server-8.0.gpg /tmp/mongodb-server-8.0.asc; elif command -v dnf >/dev/null 2>&1 || command -v tdnf >/dev/null 2>&1; then echo 'Not applicable. RPM-based distributions import the key directly via rpm --import.'; else echo 'Unsupported package manager. MongoDB repository setup requires apt-get or dnf.' 1>&2; exit 1; fi"
}
},
{
"Type": "ExecuteCommand",
"Parameters": {
"Scenario": "MongoAddRepository",
"SupportedPlatforms": "linux-x64,linux-arm64",
"Command": "sudo sh -c \"echo 'deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/8.0 multiverse' > /etc/apt/sources.list.d/mongodb-org-8.0.list\""
"UseShell": true,
"Command": "set -e; if command -v apt-get >/dev/null 2>&1; then echo 'deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/8.0 multiverse' | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list; elif command -v dnf >/dev/null 2>&1 || command -v tdnf >/dev/null 2>&1; then { echo '[mongodb-org-8.0]'; echo 'name=MongoDB Repository'; echo 'baseurl=https://repo.mongodb.org/yum/redhat/9/mongodb-org/8.0/$basearch/'; echo 'gpgcheck=1'; echo 'enabled=1'; echo 'gpgkey=https://pgp.mongodb.com/server-8.0.asc'; } | sudo tee /etc/yum.repos.d/mongodb-org-8.0.repo; else echo 'Unsupported package manager. MongoDB repository setup requires apt-get or dnf.' 1>&2; exit 1; fi"
}
},
{
"Type": "ExecuteCommand",
"Parameters": {
"Scenario": "MongoUpdatePackageList",
"SupportedPlatforms": "linux-x64,linux-arm64",
"Command": "sudo apt-get update"
"UseShell": true,
"Command": "set -e; if command -v apt-get >/dev/null 2>&1; then sudo apt-get update; elif command -v dnf >/dev/null 2>&1 || command -v tdnf >/dev/null 2>&1; then sudo dnf makecache --refresh; else echo 'Unsupported package manager. MongoDB repository setup requires apt-get or dnf.' 1>&2; exit 1; fi"
}
},
{
"Type": "ExecuteCommand",
"Parameters": {
"Scenario": "MongoInstallServer",
"SupportedPlatforms": "linux-x64,linux-arm64",
"Command": "sudo apt-get install -y mongodb-org",
"UseShell": true,
"Command": "set -e; if command -v apt-get >/dev/null 2>&1; then sudo apt-get install -y mongodb-org; elif command -v dnf >/dev/null 2>&1 || command -v tdnf >/dev/null 2>&1; then sudo dnf install -y mongodb-org; else echo 'Unsupported package manager. MongoDB installation requires apt-get or dnf.' 1>&2; exit 1; fi",
"Role": "Server"
}
},
Expand All @@ -225,7 +230,8 @@
"Parameters": {
"Scenario": "MongoInstallClient",
"SupportedPlatforms": "linux-x64,linux-arm64",
"Command": "sudo apt-get install -y mongodb-mongosh",
"UseShell": true,
"Command": "set -e; if command -v apt-get >/dev/null 2>&1; then sudo apt-get install -y mongodb-mongosh; elif command -v dnf >/dev/null 2>&1 || command -v tdnf >/dev/null 2>&1; then sudo dnf install -y mongodb-mongosh; else echo 'Unsupported package manager. MongoDB installation requires apt-get or dnf.' 1>&2; exit 1; fi",
"Role": "Client"
}
},
Expand Down
1 change: 1 addition & 0 deletions website/docs/workloads/mongodb/mongodb-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ This profile loads a dataset into MongoDB and then runs various read, write, sca
* linux-arm64

* **Supported Operating Systems**
* AzureLinux
* Ubuntu

* **Supports Disconnected Scenarios**
Expand Down
Loading