From acc2c8ab83a0e18b1d4a7e23e47a9fe4d4cc34f4 Mon Sep 17 00:00:00 2001 From: MaorNetApp Date: Tue, 23 Dec 2025 13:10:28 +0200 Subject: [PATCH 1/2] Add Netapp Ontap utils and custom tasks for horde --- .../AutomationUtils/OntapUtils.cs | 475 ++++++++++++++++++ .../Examples/CloneVolumeExample.xml | 31 ++ .../Examples/DeleteVolumeExample.xml | 39 ++ .../Examples/SyncAndSnapshotExample.xml | 27 + .../unreal/horde/OntapUnrealEngine/README.md | 43 ++ .../Tasks/CloneVolumeTask.cs | 236 +++++++++ .../Tasks/DeleteSnapshotTask.cs | 153 ++++++ .../Tasks/DeleteVolumeTask.cs | 153 ++++++ .../Tasks/SyncAndSnapshotTask.cs | 271 ++++++++++ 9 files changed, 1428 insertions(+) create mode 100644 modules/unreal/horde/OntapUnrealEngine/AutomationUtils/OntapUtils.cs create mode 100644 modules/unreal/horde/OntapUnrealEngine/Examples/CloneVolumeExample.xml create mode 100644 modules/unreal/horde/OntapUnrealEngine/Examples/DeleteVolumeExample.xml create mode 100644 modules/unreal/horde/OntapUnrealEngine/Examples/SyncAndSnapshotExample.xml create mode 100644 modules/unreal/horde/OntapUnrealEngine/README.md create mode 100644 modules/unreal/horde/OntapUnrealEngine/Tasks/CloneVolumeTask.cs create mode 100644 modules/unreal/horde/OntapUnrealEngine/Tasks/DeleteSnapshotTask.cs create mode 100644 modules/unreal/horde/OntapUnrealEngine/Tasks/DeleteVolumeTask.cs create mode 100644 modules/unreal/horde/OntapUnrealEngine/Tasks/SyncAndSnapshotTask.cs diff --git a/modules/unreal/horde/OntapUnrealEngine/AutomationUtils/OntapUtils.cs b/modules/unreal/horde/OntapUnrealEngine/AutomationUtils/OntapUtils.cs new file mode 100644 index 00000000..5fae56c9 --- /dev/null +++ b/modules/unreal/horde/OntapUnrealEngine/AutomationUtils/OntapUtils.cs @@ -0,0 +1,475 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using AutomationUtils; +using EpicGames.Core; +using Microsoft.Extensions.Logging; + +namespace AutomationTool +{ + /// + /// Helper class for NetApp ONTAP operations + /// + public class OntapUtils + { + private readonly string _fsxAdminIp; + private readonly string _ontapUser; + private readonly string _awsSecretName; + private readonly string _awsRegion; + private readonly ILogger _logger; + private string _password; + + /// + /// Constructor + /// + /// FSx ONTAP management IP address + /// ONTAP username + /// AWS Secrets Manager secret name containing the FSx password + /// AWS region where the secret is stored + /// Logger for output + public OntapUtils(string fsxAdminIp, string ontapUser, string awsSecretName, string awsRegion, ILogger logger) + { + _fsxAdminIp = fsxAdminIp; + _ontapUser = ontapUser; + _awsSecretName = awsSecretName; + _awsRegion = awsRegion; + _logger = logger; + } + + /// + /// Ensures the password is retrieved from AWS Secrets Manager (lazy loading) + /// + private async Task EnsurePasswordAsync(CancellationToken cancellationToken = default) + { + if (_password == null) + { + _password = await GetAwsSecretAsync(_awsSecretName, _awsRegion, _logger, cancellationToken); + } + } + + /// + /// Gets a secret value from AWS Secrets Manager using AWS CLI + /// + private static async Task GetAwsSecretAsync(string secretName, string region, ILogger logger, CancellationToken cancellationToken = default) + { + return await Task.Run(() => + { + try + { + // Use AWS CLI to get the secret + string arguments = $"secretsmanager get-secret-value --secret-id \"{secretName}\" --region {region} --query SecretString --output text"; + + IProcessResult result = CommandUtils.Run("aws", arguments, Options: CommandUtils.ERunOptions.Default); + + string secretValue = result.Output.Trim(); + + if (string.IsNullOrEmpty(secretValue)) + { + throw new AutomationException($"AWS secret '{secretName}' is empty"); + } + + logger.LogInformation("Successfully retrieved secret '{SecretName}'", secretName); + return secretValue; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get AWS secret '{SecretName}'", secretName); + throw new AutomationException(ex, $"Failed to get AWS secret '{secretName}'"); + } + }, cancellationToken); + } + + /// + /// Gets the UUID of an ONTAP volume + /// + private async Task GetVolumeUuidAsync(string volumeName, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Getting UUID for volume '{VolumeName}'...", volumeName); + + await EnsurePasswordAsync(cancellationToken); + using HttpClient client = CreateOntapHttpClient(_ontapUser, _password); + + string url = $"https://{_fsxAdminIp}/api/storage/volumes?name={volumeName}&fields=uuid"; + + try + { + HttpResponseMessage response = await client.GetAsync(url, cancellationToken); + response.EnsureSuccessStatusCode(); + + string jsonResponse = await response.Content.ReadAsStringAsync(cancellationToken); + using JsonDocument doc = JsonDocument.Parse(jsonResponse); + + JsonElement root = doc.RootElement; + if (root.TryGetProperty("records", out JsonElement records) && records.GetArrayLength() > 0) + { + JsonElement firstRecord = records[0]; + if (firstRecord.TryGetProperty("uuid", out JsonElement uuidElement)) + { + string uuid = uuidElement.GetString(); + _logger.LogInformation("Volume UUID for '{VolumeName}': {Uuid}", volumeName, uuid); + return uuid; + } + } + + throw new AutomationException($"Could not find UUID for volume '{volumeName}'"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get UUID for volume '{VolumeName}'", volumeName); + throw new AutomationException(ex, $"Failed to get UUID for volume '{volumeName}'"); + } + } + + /// + /// Creates a snapshot of an ONTAP volume + /// + /// Name of the volume to snapshot + /// Name for the new snapshot + /// Cancellation token + /// The snapshot name if successful + public async Task CreateOntapSnapshotAsync(string volumeName, string snapshotName, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Creating NetApp snapshot '{SnapshotName}' for volume '{VolumeName}'...", snapshotName, volumeName); + + await EnsurePasswordAsync(cancellationToken); + + // Get volume UUID + string volumeUuid = await GetVolumeUuidAsync(volumeName, cancellationToken); + + using HttpClient client = CreateOntapHttpClient(_ontapUser, _password); + + // Check if snapshot already exists + _logger.LogInformation("Checking if snapshot '{SnapshotName}' already exists...", snapshotName); + string checkUrl = $"https://{_fsxAdminIp}/api/storage/volumes/{volumeUuid}/snapshots?name={snapshotName}"; + + try + { + HttpResponseMessage checkResponse = await client.GetAsync(checkUrl, cancellationToken); + checkResponse.EnsureSuccessStatusCode(); + + string checkJson = await checkResponse.Content.ReadAsStringAsync(cancellationToken); + using JsonDocument checkDoc = JsonDocument.Parse(checkJson); + + if (checkDoc.RootElement.TryGetProperty("records", out JsonElement records) && records.GetArrayLength() > 0) + { + _logger.LogInformation("Snapshot '{SnapshotName}' already exists on volume '{VolumeName}'", snapshotName, volumeName); + return snapshotName; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to check if snapshot exists, will attempt to create it"); + } + + // Create snapshot + _logger.LogInformation("Creating snapshot '{SnapshotName}'...", snapshotName); + string createUrl = $"https://{_fsxAdminIp}/api/storage/volumes/{volumeUuid}/snapshots"; + + var snapshotData = new + { + name = snapshotName + }; + + string jsonContent = JsonSerializer.Serialize(snapshotData); + using StringContent content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + + try + { + HttpResponseMessage createResponse = await client.PostAsync(createUrl, content, cancellationToken); + + string responseBody = await createResponse.Content.ReadAsStringAsync(cancellationToken); + + if (!createResponse.IsSuccessStatusCode) + { + _logger.LogError("Failed to create snapshot. Status: {StatusCode}, Response: {Response}", createResponse.StatusCode, responseBody); + throw new AutomationException($"Failed to create snapshot '{snapshotName}'. Status: {createResponse.StatusCode}"); + } + + _logger.LogInformation("Snapshot '{SnapshotName}' created successfully on volume '{VolumeName}'", snapshotName, volumeName); + + // Wait for snapshot to be ready + await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); + + // Verify snapshot was created + _logger.LogInformation("Verifying snapshot '{SnapshotName}'...", snapshotName); + HttpResponseMessage verifyResponse = await client.GetAsync(checkUrl, cancellationToken); + verifyResponse.EnsureSuccessStatusCode(); + + string verifyJson = await verifyResponse.Content.ReadAsStringAsync(cancellationToken); + using JsonDocument verifyDoc = JsonDocument.Parse(verifyJson); + + if (verifyDoc.RootElement.TryGetProperty("records", out JsonElement verifyRecords) && verifyRecords.GetArrayLength() > 0) + { + _logger.LogInformation("Snapshot '{SnapshotName}' verified and ready", snapshotName); + return snapshotName; + } + + throw new AutomationException($"Snapshot '{snapshotName}' verification failed"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create snapshot '{SnapshotName}'", snapshotName); + throw new AutomationException(ex, $"Failed to create snapshot '{snapshotName}'"); + } + } + + /// + /// Checks if a volume exists in ONTAP + /// + /// Name of the volume to check + /// Storage Virtual Machine name (optional) + /// Cancellation token + /// True if volume exists, false otherwise + public async Task VolumeExistsAsync(string volumeName, string svmName, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Checking if volume '{VolumeName}' exists...", volumeName); + + await EnsurePasswordAsync(cancellationToken); + using HttpClient client = CreateOntapHttpClient(_ontapUser, _password); + + string url = $"https://{_fsxAdminIp}/api/storage/volumes?name={volumeName}"; + if (!String.IsNullOrEmpty(svmName)) + { + url += $"&svm.name={svmName}"; + } + + try + { + HttpResponseMessage response = await client.GetAsync(url, cancellationToken); + response.EnsureSuccessStatusCode(); + + string jsonResponse = await response.Content.ReadAsStringAsync(cancellationToken); + using JsonDocument doc = JsonDocument.Parse(jsonResponse); + + JsonElement root = doc.RootElement; + if (root.TryGetProperty("records", out JsonElement records) && records.GetArrayLength() > 0) + { + _logger.LogInformation("Volume '{VolumeName}' exists", volumeName); + return true; + } + + _logger.LogInformation("Volume '{VolumeName}' does not exist", volumeName); + return false; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to check if volume '{VolumeName}' exists", volumeName); + throw new AutomationException(ex, $"Failed to check if volume '{volumeName}' exists"); + } + } + + /// + /// Deletes a volume from ONTAP + /// + /// Name of the volume to delete + /// Cancellation token + public async Task DeleteVolumeAsync(string volumeName, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Deleting volume '{VolumeName}'...", volumeName); + + await EnsurePasswordAsync(cancellationToken); + + // First get the volume UUID + string volumeUuid = await GetVolumeUuidAsync(volumeName, cancellationToken); + + using HttpClient client = CreateOntapHttpClient(_ontapUser, _password); + + string url = $"https://{_fsxAdminIp}/api/storage/volumes/{volumeUuid}"; + + try + { + HttpResponseMessage response = await client.DeleteAsync(url, cancellationToken); + + string responseBody = await response.Content.ReadAsStringAsync(cancellationToken); + + if (!response.IsSuccessStatusCode) + { + _logger.LogError("Failed to delete volume. Status: {StatusCode}, Response: {Response}", response.StatusCode, responseBody); + throw new AutomationException($"Failed to delete volume '{volumeName}'. Status: {response.StatusCode}"); + } + + _logger.LogInformation("Volume '{VolumeName}' deleted successfully", volumeName); + + // Wait for deletion to complete + await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to delete volume '{VolumeName}'", volumeName); + throw new AutomationException(ex, $"Failed to delete volume '{volumeName}'"); + } + } + + /// + /// Creates a FlexClone volume from an existing snapshot + /// + /// Name of the source volume + /// Name of the snapshot to clone from + /// Name for the new FlexClone volume + /// Storage Virtual Machine name + /// Cancellation token + /// The clone volume name if successful + public async Task CreateFlexCloneVolumeAsync(string sourceVolumeName, string snapshotName, string cloneVolumeName, string svmName, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Creating FlexClone volume '{CloneVolumeName}' from snapshot '{SnapshotName}' on source volume '{SourceVolumeName}'...", + cloneVolumeName, snapshotName, sourceVolumeName); + + await EnsurePasswordAsync(cancellationToken); + using HttpClient client = CreateOntapHttpClient(_ontapUser, _password); + + // Check if clone volume already exists + if (await VolumeExistsAsync(cloneVolumeName, svmName, cancellationToken)) + { + throw new AutomationException($"Clone volume '{cloneVolumeName}' already exists. Please delete it first or use a different name."); + } + + // Create FlexClone volume + _logger.LogInformation("Creating FlexClone volume '{CloneVolumeName}'...", cloneVolumeName); + string createUrl = $"https://{_fsxAdminIp}/api/storage/volumes"; + + var cloneData = new + { + name = cloneVolumeName, + svm = new + { + name = svmName + }, + clone = new + { + is_flexclone = true, + parent_snapshot = new + { + name = snapshotName + }, + parent_volume = new + { + name = sourceVolumeName + } + }, + comment = $"FlexClone from snapshot {snapshotName}" + }; + + string jsonContent = JsonSerializer.Serialize(cloneData); + using StringContent content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + + try + { + HttpResponseMessage createResponse = await client.PostAsync(createUrl, content, cancellationToken); + + string responseBody = await createResponse.Content.ReadAsStringAsync(cancellationToken); + + if (!createResponse.IsSuccessStatusCode) + { + _logger.LogError("Failed to create FlexClone volume. Status: {StatusCode}, Response: {Response}", createResponse.StatusCode, responseBody); + throw new AutomationException($"Failed to create FlexClone volume '{cloneVolumeName}'. Status: {createResponse.StatusCode}"); + } + + _logger.LogInformation("FlexClone volume '{CloneVolumeName}' created successfully", cloneVolumeName); + + // Wait for volume to be ready + await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); + + // Verify volume was created + _logger.LogInformation("Verifying FlexClone volume '{CloneVolumeName}'...", cloneVolumeName); + if (await VolumeExistsAsync(cloneVolumeName, svmName, cancellationToken)) + { + _logger.LogInformation("FlexClone volume '{CloneVolumeName}' verified and ready", cloneVolumeName); + return cloneVolumeName; + } + + throw new AutomationException($"FlexClone volume '{cloneVolumeName}' verification failed"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create FlexClone volume '{CloneVolumeName}'", cloneVolumeName); + throw new AutomationException(ex, $"Failed to create FlexClone volume '{cloneVolumeName}'"); + } + } + + /// + /// Deletes an ONTAP snapshot + /// + /// Name of the volume containing the snapshot + /// Name of the snapshot to delete + /// Cancellation token + public async Task DeleteSnapshotAsync(string volumeName, string snapshotName, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Deleting snapshot '{SnapshotName}' from volume '{VolumeName}'...", snapshotName, volumeName); + + await EnsurePasswordAsync(cancellationToken); + + // Get volume UUID + string volumeUuid = await GetVolumeUuidAsync(volumeName, cancellationToken); + + using HttpClient client = CreateOntapHttpClient(_ontapUser, _password); + + // Get snapshot UUID + string getUrl = $"https://{_fsxAdminIp}/api/storage/volumes/{volumeUuid}/snapshots?name={snapshotName}"; + + try + { + HttpResponseMessage getResponse = await client.GetAsync(getUrl, cancellationToken); + getResponse.EnsureSuccessStatusCode(); + + string jsonResponse = await getResponse.Content.ReadAsStringAsync(cancellationToken); + using JsonDocument doc = JsonDocument.Parse(jsonResponse); + + JsonElement root = doc.RootElement; + if (root.TryGetProperty("records", out JsonElement records) && records.GetArrayLength() > 0) + { + JsonElement firstRecord = records[0]; + if (firstRecord.TryGetProperty("uuid", out JsonElement uuidElement)) + { + string snapshotUuid = uuidElement.GetString(); + _logger.LogInformation("Found snapshot '{SnapshotName}' with UUID: {Uuid}", snapshotName, snapshotUuid); + + // Delete the snapshot + string deleteUrl = $"https://{_fsxAdminIp}/api/storage/volumes/{volumeUuid}/snapshots/{snapshotUuid}"; + HttpResponseMessage deleteResponse = await client.DeleteAsync(deleteUrl, cancellationToken); + deleteResponse.EnsureSuccessStatusCode(); + + _logger.LogInformation("Snapshot '{SnapshotName}' deleted successfully", snapshotName); + return; + } + } + + throw new AutomationException($"Snapshot '{snapshotName}' not found on volume '{volumeName}'"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to delete snapshot '{SnapshotName}' from volume '{VolumeName}'", snapshotName, volumeName); + throw new AutomationException(ex, $"Failed to delete snapshot '{snapshotName}' from volume '{volumeName}'"); + } + } + + /// + /// Creates an HttpClient configured for ONTAP API calls + /// + private static HttpClient CreateOntapHttpClient(string username, string password) + { + // Create handler that bypasses SSL certificate validation + HttpClientHandler handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true + }; + + HttpClient client = new HttpClient(handler); + + // Set basic authentication + string credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}")); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials); + + // Set timeout + client.Timeout = TimeSpan.FromSeconds(30); + + return client; + } + } +} diff --git a/modules/unreal/horde/OntapUnrealEngine/Examples/CloneVolumeExample.xml b/modules/unreal/horde/OntapUnrealEngine/Examples/CloneVolumeExample.xml new file mode 100644 index 00000000..328a010c --- /dev/null +++ b/modules/unreal/horde/OntapUnrealEngine/Examples/CloneVolumeExample.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/modules/unreal/horde/OntapUnrealEngine/Examples/DeleteVolumeExample.xml b/modules/unreal/horde/OntapUnrealEngine/Examples/DeleteVolumeExample.xml new file mode 100644 index 00000000..dad559a5 --- /dev/null +++ b/modules/unreal/horde/OntapUnrealEngine/Examples/DeleteVolumeExample.xml @@ -0,0 +1,39 @@ + + + + + + + + diff --git a/modules/unreal/horde/OntapUnrealEngine/Examples/SyncAndSnapshotExample.xml b/modules/unreal/horde/OntapUnrealEngine/Examples/SyncAndSnapshotExample.xml new file mode 100644 index 00000000..29b4120e --- /dev/null +++ b/modules/unreal/horde/OntapUnrealEngine/Examples/SyncAndSnapshotExample.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/modules/unreal/horde/OntapUnrealEngine/README.md b/modules/unreal/horde/OntapUnrealEngine/README.md new file mode 100644 index 00000000..aa2cc991 --- /dev/null +++ b/modules/unreal/horde/OntapUnrealEngine/README.md @@ -0,0 +1,43 @@ +# Unreal Engine BuildGraph ONTAP Integration + +Custom BuildGraph tasks for integrating Perforce syncing with NetApp ONTAP FlexClone operations. + +## Files + +**Tasks/** - C# BuildGraph task implementations +- `SyncAndSnapshotTask.cs` - Sync from Perforce and create ONTAP snapshot +- `CloneVolumeTask.cs` - Create FlexClone volume from snapshot +- `DeleteVolumeTask.cs` - Delete ONTAP volume +- `DeleteSnapshotTask.cs` - Delete ONTAP snapshot + +**AutomationUtils/** - Helper utilities +- `OntapUtils.cs` - ONTAP REST API operations and AWS Secrets Manager integration + +**Examples/** - BuildGraph XML workflows +- `SyncAndSnapshotExample.xml` - Sync from Perforce and snapshot +- `CloneVolumeExample.xml` - Create FlexClone from snapshot +- `DeleteVolumeExample.xml` - Delete clone and snapshot + +## Prerequisites + +- AWS CLI configured with credentials +- Perforce client (p4) +- AWS Secrets Manager secret containing FSx ONTAP password +- FSx ONTAP file system with SVM configured + +## Common Parameters + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `FsxAdminIp` | FSx ONTAP management IP | - | +| `OntapUser` | ONTAP username | fsxadmin | +| `AwsSecretName` | AWS secret name for ONTAP password | - | +| `AwsRegion` | AWS region | us-east-1 | +| `SvmName` | Storage Virtual Machine name | fsx | + +## Notes + +- Snapshot deletion may fail immediately after clone deletion due to ONTAP lock timing +- FlexClone volumes are full read/write volumes with instant creation +- AWS Secrets Manager password retrieval is handled automatically by OntapUtils class +- All ONTAP operations use REST API over HTTPS (self-signed certificates accepted) diff --git a/modules/unreal/horde/OntapUnrealEngine/Tasks/CloneVolumeTask.cs b/modules/unreal/horde/OntapUnrealEngine/Tasks/CloneVolumeTask.cs new file mode 100644 index 00000000..ca444732 --- /dev/null +++ b/modules/unreal/horde/OntapUnrealEngine/Tasks/CloneVolumeTask.cs @@ -0,0 +1,236 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Xml; +using EpicGames.Core; +using Microsoft.Extensions.Logging; +using UnrealBuildBase; + +namespace AutomationTool.Tasks +{ + /// + /// Parameters for the CloneVolume task + /// + public class CloneVolumeTaskParameters + { + /// + /// Name of the source volume to clone from. + /// + [TaskParameter] + public string SourceVolume { get; set; } + + /// + /// Name of the snapshot to use for cloning. + /// + [TaskParameter] + public string SnapshotName { get; set; } + + /// + /// Name for the new FlexClone volume. + /// + [TaskParameter] + public string CloneVolumeName { get; set; } + + /// + /// Storage Virtual Machine (SVM) name in ONTAP. + /// + [TaskParameter] + public string SvmName { get; set; } + + /// + /// FSx ONTAP management IP address. + /// + [TaskParameter] + public string FsxAdminIp { get; set; } + + /// + /// ONTAP username (e.g., vsadmin). + /// + [TaskParameter] + public string OntapUser { get; set; } + + /// + /// AWS Secrets Manager secret name containing the FSx password. + /// + [TaskParameter] + public string AwsSecretName { get; set; } + + /// + /// AWS region where the secret is stored. + /// + [TaskParameter] + public string AwsRegion { get; set; } + } + + /// + /// Creates a FlexClone volume from an existing snapshot. + /// + [TaskElement("CloneVolume", typeof(CloneVolumeTaskParameters))] + public class CloneVolumeTask : CustomTask + { + /// + /// Parameters for the task + /// + private readonly CloneVolumeTaskParameters _parameters; + + /// + /// Constructor + /// + /// Parameters for this task + public CloneVolumeTask(CloneVolumeTaskParameters parameters) + { + _parameters = parameters; + } + + /// + /// Execute the task. + /// + /// Information about the current job + /// Set of build products produced by this node. + /// Mapping from tag names to the set of files they include + public override void Execute(JobContext job, HashSet buildProducts, Dictionary> tagNameToFileSet) + { + // Validate all required parameters + ValidateParameters(); + + Logger.LogInformation("Starting FlexClone volume creation"); + Logger.LogInformation("Source Volume: {SourceVolume}", _parameters.SourceVolume); + Logger.LogInformation("Snapshot: {SnapshotName}", _parameters.SnapshotName); + Logger.LogInformation("Clone Volume: {CloneVolumeName}", _parameters.CloneVolumeName); + Logger.LogInformation("SVM: {SvmName}", _parameters.SvmName); + + try + { + CreateFlexCloneVolumeAsync().Wait(); + Logger.LogInformation("FlexClone volume creation completed successfully"); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to create FlexClone volume '{CloneVolumeName}'", _parameters.CloneVolumeName); + throw; + } + } + + /// + /// Validates that all required parameters are provided + /// + private void ValidateParameters() + { + if (String.IsNullOrEmpty(_parameters.SourceVolume)) + { + throw new AutomationException("SourceVolume parameter is required"); + } + if (String.IsNullOrEmpty(_parameters.SnapshotName)) + { + throw new AutomationException("SnapshotName parameter is required"); + } + if (String.IsNullOrEmpty(_parameters.CloneVolumeName)) + { + throw new AutomationException("CloneVolumeName parameter is required"); + } + if (String.IsNullOrEmpty(_parameters.SvmName)) + { + throw new AutomationException("SvmName parameter is required"); + } + if (String.IsNullOrEmpty(_parameters.FsxAdminIp)) + { + throw new AutomationException("FsxAdminIp parameter is required"); + } + if (String.IsNullOrEmpty(_parameters.OntapUser)) + { + throw new AutomationException("OntapUser parameter is required"); + } + if (String.IsNullOrEmpty(_parameters.AwsSecretName)) + { + throw new AutomationException("AwsSecretName parameter is required"); + } + if (String.IsNullOrEmpty(_parameters.AwsRegion)) + { + throw new AutomationException("AwsRegion parameter is required"); + } + } + + /// + /// Creates a FlexClone volume from an existing snapshot + /// + private async Task CreateFlexCloneVolumeAsync() + { + try + { + // Create OntapUtils instance + OntapUtils ontapUtils = new OntapUtils( + _parameters.FsxAdminIp, + _parameters.OntapUser, + _parameters.AwsSecretName, + _parameters.AwsRegion, + Logger); + + // Verify source volume exists + Logger.LogInformation("Verifying source volume '{SourceVolume}' exists...", _parameters.SourceVolume); + bool sourceExists = await ontapUtils.VolumeExistsAsync(_parameters.SourceVolume, _parameters.SvmName); + + if (!sourceExists) + { + throw new AutomationException($"Source volume '{_parameters.SourceVolume}' not found in SVM '{_parameters.SvmName}'"); + } + + Logger.LogInformation("Source volume '{SourceVolume}' verified", _parameters.SourceVolume); + + // Check if clone volume already exists + Logger.LogInformation("Checking if clone volume '{CloneVolumeName}' already exists...", _parameters.CloneVolumeName); + bool cloneExists = await ontapUtils.VolumeExistsAsync(_parameters.CloneVolumeName, _parameters.SvmName); + + if (cloneExists) + { + throw new AutomationException($"Clone volume '{_parameters.CloneVolumeName}' already exists. Please delete it first or use a different name."); + } + + // Create the FlexClone volume + string cloneVolumeName = await ontapUtils.CreateFlexCloneVolumeAsync( + _parameters.SourceVolume, + _parameters.SnapshotName, + _parameters.CloneVolumeName, + _parameters.SvmName); + + Logger.LogInformation("FlexClone volume '{CloneVolumeName}' created successfully", cloneVolumeName); + Logger.LogInformation("Junction Path: /{CloneVolumeName}", cloneVolumeName); + Logger.LogInformation("✅ Full read/write regular NetApp volume"); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to create FlexClone volume"); + throw new AutomationException(ex, "Failed to create FlexClone volume"); + } + } + + /// + /// Output this task out to an XML writer. + /// + public override void Write(XmlWriter writer) + { + Write(writer, _parameters); + } + + /// + /// Find all the tags which are used as inputs to this task + /// + /// The tag names which are read by this task + public override IEnumerable FindConsumedTagNames() + { + return Enumerable.Empty(); + } + + /// + /// Find all the tags which are modified by this task + /// + /// The tag names which are modified by this task + public override IEnumerable FindProducedTagNames() + { + return Enumerable.Empty(); + } + } +} diff --git a/modules/unreal/horde/OntapUnrealEngine/Tasks/DeleteSnapshotTask.cs b/modules/unreal/horde/OntapUnrealEngine/Tasks/DeleteSnapshotTask.cs new file mode 100644 index 00000000..3c6d6a72 --- /dev/null +++ b/modules/unreal/horde/OntapUnrealEngine/Tasks/DeleteSnapshotTask.cs @@ -0,0 +1,153 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Xml; +using EpicGames.Core; +using Microsoft.Extensions.Logging; +using UnrealBuildBase; + +namespace AutomationTool.Tasks +{ + /// + /// Parameters for the DeleteSnapshot task + /// + public class DeleteSnapshotTaskParameters + { + /// + /// Name of the volume containing the snapshot. + /// + [TaskParameter] + public string VolumeName { get; set; } + + /// + /// Name of the snapshot to delete. + /// + [TaskParameter] + public string SnapshotName { get; set; } + + /// + /// FSx ONTAP management IP address. + /// + [TaskParameter] + public string FsxAdminIp { get; set; } + + /// + /// ONTAP username (e.g., vsadmin). + /// + [TaskParameter] + public string OntapUser { get; set; } + + /// + /// AWS Secrets Manager secret name containing the FSx password. + /// + [TaskParameter] + public string AwsSecretName { get; set; } + + /// + /// AWS region where the secret is stored. + /// + [TaskParameter] + public string AwsRegion { get; set; } + } + + /// + /// Deletes an ONTAP snapshot. + /// + [TaskElement("DeleteSnapshot", typeof(DeleteSnapshotTaskParameters))] + public class DeleteSnapshotTask : CustomTask + { + /// + /// Parameters for the task + /// + private readonly DeleteSnapshotTaskParameters _parameters; + + /// + /// Constructor + /// + /// Parameters for this task + public DeleteSnapshotTask(DeleteSnapshotTaskParameters parameters) + { + _parameters = parameters; + } + + /// + /// Execute the task. + /// + /// Information about the current job + /// Set of build products produced by this node. + /// Mapping from tag names to the set of files they include + public override void Execute(JobContext job, HashSet buildProducts, Dictionary> tagNameToFileSet) + { + Logger.LogInformation("Starting ONTAP snapshot deletion"); + Logger.LogInformation("Volume: {VolumeName}", _parameters.VolumeName); + Logger.LogInformation("Snapshot: {SnapshotName}", _parameters.SnapshotName); + + try + { + DeleteSnapshotAsync().Wait(); + Logger.LogInformation("Snapshot deletion completed successfully"); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to delete snapshot '{SnapshotName}' from volume '{VolumeName}'", _parameters.SnapshotName, _parameters.VolumeName); + throw; + } + } + + /// + /// Deletes an ONTAP snapshot + /// + private async Task DeleteSnapshotAsync() + { + try + { + // Create OntapUtils instance + OntapUtils ontapUtils = new OntapUtils( + _parameters.FsxAdminIp, + _parameters.OntapUser, + _parameters.AwsSecretName, + _parameters.AwsRegion, + Logger); + + // Delete the snapshot + await ontapUtils.DeleteSnapshotAsync(_parameters.VolumeName, _parameters.SnapshotName); + + Logger.LogInformation("Snapshot '{SnapshotName}' deleted successfully from volume '{VolumeName}'", _parameters.SnapshotName, _parameters.VolumeName); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to delete ONTAP snapshot"); + throw new AutomationException(ex, "Failed to delete ONTAP snapshot"); + } + } + + /// + /// Output this task out to an XML writer. + /// + public override void Write(XmlWriter writer) + { + Write(writer, _parameters); + } + + /// + /// Find all the tags which are used as inputs to this task + /// + /// The tag names which are read by this task + public override IEnumerable FindConsumedTagNames() + { + return Enumerable.Empty(); + } + + /// + /// Find all the tags which are modified by this task + /// + /// The tag names which are modified by this task + public override IEnumerable FindProducedTagNames() + { + return Enumerable.Empty(); + } + } +} diff --git a/modules/unreal/horde/OntapUnrealEngine/Tasks/DeleteVolumeTask.cs b/modules/unreal/horde/OntapUnrealEngine/Tasks/DeleteVolumeTask.cs new file mode 100644 index 00000000..0f42242f --- /dev/null +++ b/modules/unreal/horde/OntapUnrealEngine/Tasks/DeleteVolumeTask.cs @@ -0,0 +1,153 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Xml; +using EpicGames.Core; +using Microsoft.Extensions.Logging; +using UnrealBuildBase; + +namespace AutomationTool.Tasks +{ + /// + /// Parameters for the DeleteVolume task + /// + public class DeleteVolumeTaskParameters + { + /// + /// Name of the volume to delete. + /// + [TaskParameter] + public string VolumeName { get; set; } + + /// + /// Storage Virtual Machine (SVM) name in ONTAP. + /// + [TaskParameter] + public string SvmName { get; set; } + + /// + /// FSx ONTAP management IP address. + /// + [TaskParameter] + public string FsxAdminIp { get; set; } + + /// + /// ONTAP username (e.g., vsadmin). + /// + [TaskParameter] + public string OntapUser { get; set; } + + /// + /// AWS Secrets Manager secret name containing the FSx password. + /// + [TaskParameter] + public string AwsSecretName { get; set; } + + /// + /// AWS region where the secret is stored. + /// + [TaskParameter] + public string AwsRegion { get; set; } + } + + /// + /// Deletes an ONTAP volume. + /// + [TaskElement("DeleteVolume", typeof(DeleteVolumeTaskParameters))] + public class DeleteVolumeTask : CustomTask + { + /// + /// Parameters for the task + /// + private readonly DeleteVolumeTaskParameters _parameters; + + /// + /// Constructor + /// + /// Parameters for this task + public DeleteVolumeTask(DeleteVolumeTaskParameters parameters) + { + _parameters = parameters; + } + + /// + /// Execute the task. + /// + /// Information about the current job + /// Set of build products produced by this node. + /// Mapping from tag names to the set of files they include + public override void Execute(JobContext job, HashSet buildProducts, Dictionary> tagNameToFileSet) + { + Logger.LogInformation("Starting ONTAP volume deletion"); + Logger.LogInformation("Volume: {VolumeName}", _parameters.VolumeName); + Logger.LogInformation("SVM: {SvmName}", _parameters.SvmName); + + try + { + DeleteVolumeAsync().Wait(); + Logger.LogInformation("Volume deletion completed successfully"); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to delete volume '{VolumeName}'", _parameters.VolumeName); + throw; + } + } + + /// + /// Deletes an ONTAP volume + /// + private async Task DeleteVolumeAsync() + { + try + { + // Create OntapUtils instance + OntapUtils ontapUtils = new OntapUtils( + _parameters.FsxAdminIp, + _parameters.OntapUser, + _parameters.AwsSecretName, + _parameters.AwsRegion, + Logger); + + // Delete the volume + await ontapUtils.DeleteVolumeAsync(_parameters.VolumeName); + + Logger.LogInformation("Volume '{VolumeName}' deleted successfully", _parameters.VolumeName); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to delete ONTAP volume"); + throw new AutomationException(ex, "Failed to delete ONTAP volume"); + } + } + + /// + /// Output this task out to an XML writer. + /// + public override void Write(XmlWriter writer) + { + Write(writer, _parameters); + } + + /// + /// Find all the tags which are used as inputs to this task + /// + /// The tag names which are read by this task + public override IEnumerable FindConsumedTagNames() + { + return Enumerable.Empty(); + } + + /// + /// Find all the tags which are modified by this task + /// + /// The tag names which are modified by this task + public override IEnumerable FindProducedTagNames() + { + return Enumerable.Empty(); + } + } +} diff --git a/modules/unreal/horde/OntapUnrealEngine/Tasks/SyncAndSnapshotTask.cs b/modules/unreal/horde/OntapUnrealEngine/Tasks/SyncAndSnapshotTask.cs new file mode 100644 index 00000000..94c48cc1 --- /dev/null +++ b/modules/unreal/horde/OntapUnrealEngine/Tasks/SyncAndSnapshotTask.cs @@ -0,0 +1,271 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Xml; +using EpicGames.Core; +using Microsoft.Extensions.Logging; +using UnrealBuildBase; + +namespace AutomationTool.Tasks +{ + /// + /// Parameters for the SyncAndSnapshot task + /// + public class SyncAndSnapshotTaskParameters + { + /// + /// The Perforce stream to sync from (e.g., //UE5/Release-5.3). + /// + [TaskParameter] + public string Stream { get; set; } + + /// + /// Perforce server and port (e.g., perforce:1666). + /// + [TaskParameter(Optional = true)] + public string P4Port { get; set; } + + /// + /// Perforce user name. + /// + [TaskParameter(Optional = true)] + public string P4User { get; set; } + + /// + /// Directory where files should be synced to. If not specified, uses current directory. + /// + [TaskParameter(Optional = true)] + public string SyncDir { get; set; } + + /// + /// Perforce workspace (client) name. If not specified, uses default workspace name. + /// + [TaskParameter(Optional = true)] + public string WorkspaceName { get; set; } + + /// + /// Name for the ONTAP snapshot to create. If not specified, no snapshot is created. + /// + [TaskParameter(Optional = true)] + public string SnapshotName { get; set; } /// + /// FSx ONTAP management IP address. + /// + [TaskParameter(Optional = true)] + public string FsxAdminIp { get; set; } + + /// + /// ONTAP username (e.g., fsxadmin). + /// + [TaskParameter(Optional = true)] + public string OntapUser { get; set; } + + /// + /// AWS Secrets Manager secret name containing the FSx password. + /// + [TaskParameter(Optional = true)] + public string AwsSecretName { get; set; } + + /// + /// AWS region where the secret is stored. + /// + [TaskParameter(Optional = true)] + public string AwsRegion { get; set; } + + /// + /// ONTAP volume name where the snapshot should be created. + /// + [TaskParameter(Optional = true)] + public string VolumeName { get; set; } + } + + /// + /// Syncs files from a Perforce stream and creates a snapshot. + /// + [TaskElement("SyncAndSnapshot", typeof(SyncAndSnapshotTaskParameters))] + public class SyncAndSnapshotTask : CustomTask + { + /// + /// Parameters for the task + /// + readonly SyncAndSnapshotTaskParameters _parameters; + + /// + /// Constructor. + /// + /// Parameters for the task + public SyncAndSnapshotTask(SyncAndSnapshotTaskParameters parameters) + { + _parameters = parameters; + } + + /// + /// Execute the task. + /// + /// Information about the current job + /// Set of build products produced by this node. + /// Mapping from tag names to the set of files they include + public override void Execute(JobContext job, HashSet buildProducts, Dictionary> tagNameToFileSet) + { + // Build the sync path - always sync everything from the stream + string syncPath = _parameters.Stream + "/..."; + + Logger.LogInformation("Syncing from Perforce: {SyncPath}", syncPath); // Save original directory + string originalDir = Environment.CurrentDirectory; + + try + { + // Determine sync directory + string syncDir = !String.IsNullOrEmpty(_parameters.SyncDir) ? _parameters.SyncDir : Environment.CurrentDirectory; + + if (!Directory.Exists(syncDir)) + { + Logger.LogInformation("Creating sync directory: {SyncDir}", syncDir); + Directory.CreateDirectory(syncDir); + } + + // Create P4Connection with specified server/user or use defaults from P4Environment + string p4User = _parameters.P4User ?? CommandUtils.P4Env.User; + string p4Port = _parameters.P4Port ?? CommandUtils.P4Env.ServerAndPort; + + Logger.LogInformation("P4 User: {P4User}, P4 Port: {P4Port}", p4User, p4Port); + + P4Connection submitP4; + if (_parameters.WorkspaceName != null) + { + Logger.LogInformation("Creating/updating workspace '{WorkspaceName}'", _parameters.WorkspaceName); + + // Create a brand new workspace + P4ClientInfo client = new P4ClientInfo(); + client.Owner = p4User; + client.Host = Unreal.MachineName; + client.RootPath = syncDir; + client.Name = _parameters.WorkspaceName; + client.Options = P4ClientOption.NoAllWrite | P4ClientOption.NoClobber | P4ClientOption.NoCompress | P4ClientOption.Unlocked | P4ClientOption.NoModTime | P4ClientOption.NoRmDir; + client.SubmitOptions = P4SubmitOption.SubmitUnchanged; + client.LineEnd = P4LineEnd.Local; + client.Stream = _parameters.Stream; + + // Create the workspace using a temporary connection + P4Connection tempP4 = new P4Connection(p4User, null, p4Port); + tempP4.CreateClient(client, AllowSpew: true); + Logger.LogInformation("Successfully created/updated workspace '{Workspace}'", _parameters.WorkspaceName); + + // Create a new connection for it + submitP4 = new P4Connection(client.Owner, client.Name, p4Port); + Logger.LogInformation("Created P4Connection with user '{User}', client '{Client}', port '{Port}'", client.Owner, client.Name, p4Port); + } + else + { + // Use default connection or create one + submitP4 = new P4Connection(p4User, null, p4Port); + Logger.LogInformation("Using default P4Connection with user '{User}', port '{Port}'", p4User, p4Port); + } + + // Change to sync directory + Environment.CurrentDirectory = syncDir ?? Environment.CurrentDirectory; + Logger.LogInformation("Changed to sync directory: {SyncDir}", syncDir); + + // Perform the sync + Logger.LogInformation("Syncing: {SyncPath}", syncPath); + submitP4.Sync(syncPath, AllowSpew: true); + + Logger.LogInformation("Successfully synced from {Stream}", _parameters.Stream); + + // Create ONTAP snapshot if SnapshotName is provided + if (!String.IsNullOrEmpty(_parameters.SnapshotName)) + { + // Validate all required snapshot parameters are provided + if (String.IsNullOrEmpty(_parameters.FsxAdminIp)) + { + throw new AutomationException("SnapshotName is specified but FsxAdminIp is missing. All snapshot parameters are required when creating a snapshot."); + } + if (String.IsNullOrEmpty(_parameters.OntapUser)) + { + throw new AutomationException("SnapshotName is specified but OntapUser is missing. All snapshot parameters are required when creating a snapshot."); + } + if (String.IsNullOrEmpty(_parameters.AwsSecretName)) + { + throw new AutomationException("SnapshotName is specified but AwsSecretName is missing. All snapshot parameters are required when creating a snapshot."); + } + if (String.IsNullOrEmpty(_parameters.AwsRegion)) + { + throw new AutomationException("SnapshotName is specified but AwsRegion is missing. All snapshot parameters are required when creating a snapshot."); + } + if (String.IsNullOrEmpty(_parameters.VolumeName)) + { + throw new AutomationException("SnapshotName is specified but VolumeName is missing. All snapshot parameters are required when creating a snapshot."); + } + + CreateOntapSnapshotAsync().Wait(); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to sync from Perforce stream {Stream}", _parameters.Stream); + throw; + } + finally + { + // Restore original directory + Environment.CurrentDirectory = originalDir; + } + } + + /// + /// Creates an ONTAP snapshot + /// + private async Task CreateOntapSnapshotAsync() + { + try + { + // Create OntapUtils instance + OntapUtils ontapUtils = new OntapUtils( + _parameters.FsxAdminIp, + _parameters.OntapUser, + _parameters.AwsSecretName, + _parameters.AwsRegion, + Logger); + + // Create the snapshot + string snapshotName = await ontapUtils.CreateOntapSnapshotAsync(_parameters.VolumeName, _parameters.SnapshotName); + + Logger.LogInformation("ONTAP snapshot '{SnapshotName}' created successfully", snapshotName); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to create ONTAP snapshot"); + throw; + } + } + + /// + /// Output this task out to an XML writer. + /// + public override void Write(XmlWriter writer) + { + Write(writer, _parameters); + } + + /// + /// Find all the tags which are used as inputs to this task + /// + /// The tag names which are read by this task + public override IEnumerable FindConsumedTagNames() + { + return Enumerable.Empty(); + } + + /// + /// Find all the tags which are modified by this task + /// + /// The tag names which are modified by this task + public override IEnumerable FindProducedTagNames() + { + return Enumerable.Empty(); + } + } +} From fea05fb75895b7a3f83d954522408780f86d9a7e Mon Sep 17 00:00:00 2001 From: MaorNetApp Date: Tue, 23 Dec 2025 19:58:20 +0200 Subject: [PATCH 2/2] Move files to assets/buildgraph --- .../buildgraph}/README.md | 2 +- .../buildgraph/examples}/CloneVolumeExample.xml | 6 +++--- .../buildgraph/examples}/DeleteVolumeExample.xml | 8 ++++---- .../buildgraph/examples}/SyncAndSnapshotExample.xml | 6 +++--- .../buildgraph/tasks}/CloneVolumeTask.cs | 10 ++++------ .../buildgraph/tasks}/DeleteSnapshotTask.cs | 6 ++---- .../buildgraph/tasks}/DeleteVolumeTask.cs | 6 ++---- .../buildgraph/tasks}/SyncAndSnapshotTask.cs | 10 ++++------ .../buildgraph/utils}/OntapUtils.cs | 12 +++++------- 9 files changed, 28 insertions(+), 38 deletions(-) rename {modules/unreal/horde/OntapUnrealEngine => assets/buildgraph}/README.md (95%) rename {modules/unreal/horde/OntapUnrealEngine/Examples => assets/buildgraph/examples}/CloneVolumeExample.xml (86%) rename {modules/unreal/horde/OntapUnrealEngine/Examples => assets/buildgraph/examples}/DeleteVolumeExample.xml (85%) rename {modules/unreal/horde/OntapUnrealEngine/Examples => assets/buildgraph/examples}/SyncAndSnapshotExample.xml (85%) rename {modules/unreal/horde/OntapUnrealEngine/Tasks => assets/buildgraph/tasks}/CloneVolumeTask.cs (96%) rename {modules/unreal/horde/OntapUnrealEngine/Tasks => assets/buildgraph/tasks}/DeleteSnapshotTask.cs (96%) rename {modules/unreal/horde/OntapUnrealEngine/Tasks => assets/buildgraph/tasks}/DeleteVolumeTask.cs (96%) rename {modules/unreal/horde/OntapUnrealEngine/Tasks => assets/buildgraph/tasks}/SyncAndSnapshotTask.cs (96%) rename {modules/unreal/horde/OntapUnrealEngine/AutomationUtils => assets/buildgraph/utils}/OntapUtils.cs (97%) diff --git a/modules/unreal/horde/OntapUnrealEngine/README.md b/assets/buildgraph/README.md similarity index 95% rename from modules/unreal/horde/OntapUnrealEngine/README.md rename to assets/buildgraph/README.md index aa2cc991..89aa5d08 100644 --- a/modules/unreal/horde/OntapUnrealEngine/README.md +++ b/assets/buildgraph/README.md @@ -31,7 +31,7 @@ Custom BuildGraph tasks for integrating Perforce syncing with NetApp ONTAP FlexC |-----------|-------------|---------| | `FsxAdminIp` | FSx ONTAP management IP | - | | `OntapUser` | ONTAP username | fsxadmin | -| `AwsSecretName` | AWS secret name for ONTAP password | - | +| `OntapPasswordSecretName` | AWS secret name for ONTAP password | - | | `AwsRegion` | AWS region | us-east-1 | | `SvmName` | Storage Virtual Machine name | fsx | diff --git a/modules/unreal/horde/OntapUnrealEngine/Examples/CloneVolumeExample.xml b/assets/buildgraph/examples/CloneVolumeExample.xml similarity index 86% rename from modules/unreal/horde/OntapUnrealEngine/Examples/CloneVolumeExample.xml rename to assets/buildgraph/examples/CloneVolumeExample.xml index 328a010c..d518fad2 100644 --- a/modules/unreal/horde/OntapUnrealEngine/Examples/CloneVolumeExample.xml +++ b/assets/buildgraph/examples/CloneVolumeExample.xml @@ -11,10 +11,10 @@