From 76be3c169a125ac35d6cd0661e56ed9deade2f38 Mon Sep 17 00:00:00 2001 From: June Rhodes Date: Mon, 23 Dec 2024 16:12:25 +1100 Subject: [PATCH] Get remote ZFS server implemented (#104) --- .../Impl/TcpGrpcServerCall.cs | 6 + .../RemoteZfs/RemoteZfsProtocol.proto | 10 +- .../RemoteZfs/RemoteZfsServer.cs | 136 ------------ .../RemoteZfsSerializerContext.cs | 14 ++ .../RemoteZfsServer/RemoteZfsServerCommand.cs | 199 ++++++++++++------ .../RemoteZfsServer/RemoteZfsServerConfig.cs | 36 ++++ .../RemoteZfsServerConfigTemplate.cs | 31 +++ .../Internal/RemoteZfsServer/TrueNasQuery.cs | 13 ++ .../RemoteZfsServer/TrueNasQueryOptions.cs | 16 ++ .../RemoteZfsServer/TrueNasSnapshot.cs | 28 +++ .../RemoteZfsServer/TrueNasSnapshotClone.cs | 13 ++ .../RemoteZfsTest/RemoteZfsTestCommand.cs | 14 +- 12 files changed, 310 insertions(+), 206 deletions(-) delete mode 100644 UET/Redpoint.Uet.Workspace/RemoteZfs/RemoteZfsServer.cs create mode 100644 UET/uet/Commands/Internal/RemoteZfsServer/RemoteZfsSerializerContext.cs create mode 100644 UET/uet/Commands/Internal/RemoteZfsServer/RemoteZfsServerConfig.cs create mode 100644 UET/uet/Commands/Internal/RemoteZfsServer/RemoteZfsServerConfigTemplate.cs create mode 100644 UET/uet/Commands/Internal/RemoteZfsServer/TrueNasQuery.cs create mode 100644 UET/uet/Commands/Internal/RemoteZfsServer/TrueNasQueryOptions.cs create mode 100644 UET/uet/Commands/Internal/RemoteZfsServer/TrueNasSnapshot.cs create mode 100644 UET/uet/Commands/Internal/RemoteZfsServer/TrueNasSnapshotClone.cs diff --git a/UET/Redpoint.GrpcPipes.Transport.Tcp/Impl/TcpGrpcServerCall.cs b/UET/Redpoint.GrpcPipes.Transport.Tcp/Impl/TcpGrpcServerCall.cs index 4f0b2dd3..d7c7fffa 100644 --- a/UET/Redpoint.GrpcPipes.Transport.Tcp/Impl/TcpGrpcServerCall.cs +++ b/UET/Redpoint.GrpcPipes.Transport.Tcp/Impl/TcpGrpcServerCall.cs @@ -65,6 +65,12 @@ await _incoming.Connection.WriteAsync(new TcpGrpcResponseComplete _incoming.LogTrace($"Wrote status ({statusCode}, '{details}') to client."); } } + catch (RpcException ex) when (ex.StatusCode == StatusCode.Unavailable) + { + // We can't send any content to the client, because the client has already disconnected. + _incoming.LogTrace($"Unable to send ({statusCode}, '{details}') to client because the client has already disconnected."); + return; + } catch (OperationCanceledException) when (_serverCallContext.DeadlineCancellationToken.IsCancellationRequested) { // We can't send any content to the client, because we have exceeded our extended deadline cancellation. diff --git a/UET/Redpoint.Uet.Workspace/RemoteZfs/RemoteZfsProtocol.proto b/UET/Redpoint.Uet.Workspace/RemoteZfs/RemoteZfsProtocol.proto index e4657283..4425b51e 100644 --- a/UET/Redpoint.Uet.Workspace/RemoteZfs/RemoteZfsProtocol.proto +++ b/UET/Redpoint.Uet.Workspace/RemoteZfs/RemoteZfsProtocol.proto @@ -2,13 +2,15 @@ syntax = 'proto3'; package Redpoint.Uet.Workspace.RemoteZfs; -message EmptyRequest { +message AcquireWorkspaceRequest { + string templateId = 1; + repeated string disambiguators = 2; } -message AcquireResponse { - string windowsSharePath = 1; +message AcquireWorkspaceResponse { + string windowsShareRemotePath = 1; } service RemoteZfs { - rpc Acquire(stream EmptyRequest) returns (stream AcquireResponse) {} + rpc AcquireWorkspace(AcquireWorkspaceRequest) returns (stream AcquireWorkspaceResponse) {} } \ No newline at end of file diff --git a/UET/Redpoint.Uet.Workspace/RemoteZfs/RemoteZfsServer.cs b/UET/Redpoint.Uet.Workspace/RemoteZfs/RemoteZfsServer.cs deleted file mode 100644 index 3e1c78a3..00000000 --- a/UET/Redpoint.Uet.Workspace/RemoteZfs/RemoteZfsServer.cs +++ /dev/null @@ -1,136 +0,0 @@ -namespace Redpoint.Uet.Workspace.RemoteZfs -{ - using Grpc.Core; - using Microsoft.Extensions.Logging; - using Redpoint.ProcessExecution; - using System; - using System.Runtime.Versioning; - using System.Threading.Tasks; - - [SupportedOSPlatform("linux")] - public class RemoteZfsServer : RemoteZfs.RemoteZfsBase - { - private readonly IProcessExecutor _processExecutor; - private readonly ILogger _logger; - private readonly string _zvolRoot; - private readonly string _zvolSource; - private readonly string _windowsShareNetworkPrefix; - private long _requestId; - - public RemoteZfsServer( - IProcessExecutor processExecutor, - ILogger logger, - string zvolRoot, - string zvolSource, - string windowsShareNetworkPrefix) - { - _processExecutor = processExecutor; - _logger = logger; - _zvolRoot = zvolRoot; - _zvolSource = zvolSource; - _windowsShareNetworkPrefix = windowsShareNetworkPrefix; - } - - public override async Task Acquire( - IAsyncStreamReader requestStream, - IServerStreamWriter responseStream, - ServerCallContext context) - { - ArgumentNullException.ThrowIfNull(requestStream); - ArgumentNullException.ThrowIfNull(responseStream); - ArgumentNullException.ThrowIfNull(context); - - var id = $"rzfs-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}-${_requestId++}"; - var didCreateSnapshot = false; - var didCreateVolume = false; - - _logger.LogInformation($"Received acquire request, allocating with ID {id}..."); - - try - { - // Snapshot the source. - _logger.LogInformation($"Creating snapshot of source volume '{_zvolRoot}/{_zvolSource}@{id}'..."); - var exitCode = await _processExecutor.ExecuteAsync( - new ProcessSpecification - { - FilePath = "/sbin/zfs", - Arguments = ["snapshot", $"{_zvolRoot}/{_zvolSource}@{id}"] - }, - CaptureSpecification.Passthrough, - context.CancellationToken).ConfigureAwait(false); - if (exitCode != 0) - { - return; - } - didCreateSnapshot = true; - - // Create a new volume from the snapshot. - _logger.LogInformation($"Cloning source volume snapshot '{_zvolRoot}/{_zvolSource}@{id}' to '{_zvolRoot}/{id}'..."); - exitCode = await _processExecutor.ExecuteAsync( - new ProcessSpecification - { - FilePath = "/sbin/zfs", - Arguments = ["clone", $"{_zvolRoot}/{_zvolSource}@{id}", $"{_zvolRoot}/{id}"] - }, - CaptureSpecification.Passthrough, - context.CancellationToken).ConfigureAwait(false); - if (exitCode != 0) - { - return; - } - didCreateVolume = true; - - // Tell the client where it can find the new snapshot. - _logger.LogInformation($"Informing client it's snapshot is available at '{_windowsShareNetworkPrefix.TrimEnd('\\')}\\{id}'..."); - await responseStream.WriteAsync( - new AcquireResponse - { - WindowsSharePath = $"{_windowsShareNetworkPrefix.TrimEnd('\\')}\\{id}", - }, - context.CancellationToken).ConfigureAwait(false); - - // Now wait for our client to disconnect - that indicates it's done. - _logger.LogInformation($"Waiting for the client to disconnect..."); - while (!context.CancellationToken.IsCancellationRequested) - { - await Task.Delay(60000, context.CancellationToken).ConfigureAwait(false); - } - } - catch (OperationCanceledException) - { - // This is expected. - _logger.LogInformation($"Client closed request."); - } - finally - { - // If we created the volume, delete it. - if (didCreateVolume) - { - _logger.LogInformation($"Destroying volume '{_zvolRoot}/{id}'..."); - await _processExecutor.ExecuteAsync( - new ProcessSpecification - { - FilePath = "/sbin/zfs", - Arguments = ["destroy", "-f", $"{_zvolRoot}/{id}"] - }, - CaptureSpecification.Passthrough, - CancellationToken.None).ConfigureAwait(false); - } - - // If we created the snapshot, delete it. - if (didCreateSnapshot) - { - _logger.LogInformation($"Destroying snapshot '{_zvolRoot}/{_zvolSource}@{id}'..."); - await _processExecutor.ExecuteAsync( - new ProcessSpecification - { - FilePath = "/sbin/zfs", - Arguments = ["destroy", $"{_zvolRoot}/{_zvolSource}@{id}"] - }, - CaptureSpecification.Passthrough, - CancellationToken.None).ConfigureAwait(false); - } - } - } - } -} diff --git a/UET/uet/Commands/Internal/RemoteZfsServer/RemoteZfsSerializerContext.cs b/UET/uet/Commands/Internal/RemoteZfsServer/RemoteZfsSerializerContext.cs new file mode 100644 index 00000000..5c3894d5 --- /dev/null +++ b/UET/uet/Commands/Internal/RemoteZfsServer/RemoteZfsSerializerContext.cs @@ -0,0 +1,14 @@ +namespace UET.Commands.Internal.RemoteZfsServer +{ + using System.Text.Json.Serialization; + + [JsonSerializable(typeof(TrueNasQueryOptions))] + [JsonSerializable(typeof(TrueNasQuery))] + [JsonSerializable(typeof(TrueNasSnapshot[]))] + [JsonSerializable(typeof(TrueNasSnapshotClone))] + [JsonSerializable(typeof(RemoteZfsServerConfig))] + [JsonSerializable(typeof(RemoteZfsServerConfigTemplate))] + internal partial class RemoteZfsSerializerContext : JsonSerializerContext + { + } +} diff --git a/UET/uet/Commands/Internal/RemoteZfsServer/RemoteZfsServerCommand.cs b/UET/uet/Commands/Internal/RemoteZfsServer/RemoteZfsServerCommand.cs index 8a9f4c28..7aed9a5e 100644 --- a/UET/uet/Commands/Internal/RemoteZfsServer/RemoteZfsServerCommand.cs +++ b/UET/uet/Commands/Internal/RemoteZfsServer/RemoteZfsServerCommand.cs @@ -1,52 +1,25 @@ namespace UET.Commands.Internal.RemoteZfsServer { + using Grpc.Core; + using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Redpoint.Concurrency; using Redpoint.GrpcPipes; using Redpoint.ProcessExecution; + using Redpoint.Uet.Workspace.RemoteZfs; using System; using System.CommandLine; using System.CommandLine.Invocation; + using System.Net.Http.Json; + using System.Text.Json; using System.Threading.Tasks; - using RemoteZfsServer = Redpoint.Uet.Workspace.RemoteZfs.RemoteZfsServer; + using System.Web; internal sealed class RemoteZfsServerCommand { public sealed class Options { - public Option Port; - public Option ZvolRoot; - public Option ZvolSource; - public Option WindowsShareNetworkPrefix; - - public Options() - { - Port = new Option( - name: "--port", - description: "The port for the service to listen on.") - { - IsRequired = true, - }; - ZvolRoot = new Option( - name: "--zvol-root", - description: "The zvol which contains the source volume and which should contain our temporary created volumes.") - { - IsRequired = true, - }; - ZvolSource = new Option( - name: "--zvol-source", - description: "The name of the source volume to snapshot.") - { - IsRequired = true, - }; - WindowsShareNetworkPrefix = new Option( - name: "--windows-share-network-prefix", - description: "The Windows share prefix to prepend to the new volume name for clients to access the volume at.") - { - IsRequired = true, - }; - } } public static Command CreateRemoteZfsServerCommand() @@ -58,56 +31,47 @@ public static Command CreateRemoteZfsServerCommand() return command; } - private sealed class RemoteZfsServerCommandInstance : ICommandInstance + private sealed class RemoteZfsServerCommandInstance : RemoteZfs.RemoteZfsBase, ICommandInstance { - private readonly Options _options; private readonly ILogger _logger; private readonly IGrpcPipeFactory _grpcPipeFactory; - private readonly IServiceProvider _serviceProvider; + private RemoteZfsServerConfig? _config; public RemoteZfsServerCommandInstance( - Options options, ILogger logger, - IGrpcPipeFactory grpcPipeFactory, - IServiceProvider serviceProvider) + IGrpcPipeFactory grpcPipeFactory) { - _options = options; _logger = logger; _grpcPipeFactory = grpcPipeFactory; - _serviceProvider = serviceProvider; } public async Task ExecuteAsync(InvocationContext context) { - if (!OperatingSystem.IsLinux()) + var configJson = Environment.GetEnvironmentVariable("REMOTE_ZFS_SERVER_CONFIG"); + var configJsonPath = Environment.GetEnvironmentVariable("REMOTE_ZFS_SERVER_CONFIG_PATH"); + if (string.IsNullOrEmpty(configJson) && string.IsNullOrEmpty(configJsonPath)) { - _logger.LogError("This command can only be used on Linux (with ZFS)."); + _logger.LogError("Expected the REMOTE_ZFS_SERVER_CONFIG or REMOTE_ZFS_SERVER_CONFIG_PATH environment variable to be set."); return 1; } - var port = context.ParseResult.GetValueForOption(_options.Port); - var zvolRoot = context.ParseResult.GetValueForOption(_options.ZvolRoot); - var zvolSource = context.ParseResult.GetValueForOption(_options.ZvolSource); - var windowsShareNetworkPrefix = context.ParseResult.GetValueForOption(_options.WindowsShareNetworkPrefix); + if (!string.IsNullOrEmpty(configJsonPath)) + { + configJson = File.ReadAllText(configJsonPath); + } - if (string.IsNullOrWhiteSpace(zvolRoot) || - string.IsNullOrWhiteSpace(zvolSource) || - string.IsNullOrWhiteSpace(windowsShareNetworkPrefix)) + _config = JsonSerializer.Deserialize(configJson!, RemoteZfsSerializerContext.Default.RemoteZfsServerConfig); + if (_config == null) { - _logger.LogError("Invalid or missing arguments."); + _logger.LogError("Expected the remote ZFS server config to be valid."); return 1; } - var remoteZfsServer = new RemoteZfsServer( - _serviceProvider.GetRequiredService(), - _serviceProvider.GetRequiredService>(), - zvolRoot, - zvolSource, - windowsShareNetworkPrefix); - try { - await using (_grpcPipeFactory.CreateNetworkServer(remoteZfsServer, networkPort: port).AsAsyncDisposable(out var server).ConfigureAwait(false)) + await using (_grpcPipeFactory.CreateNetworkServer(this, networkPort: _config.ServerPort ?? 9000) + .AsAsyncDisposable(out var server) + .ConfigureAwait(false)) { await server.StartAsync().ConfigureAwait(false); @@ -126,6 +90,123 @@ public async Task ExecuteAsync(InvocationContext context) return 0; } + + public async override Task AcquireWorkspace( + AcquireWorkspaceRequest request, + IServerStreamWriter responseStream, + ServerCallContext context) + { + if (_config == null) + { + throw new InvalidOperationException(); + } + + _logger.LogInformation($"Obtained workspace request for template {request.TemplateId}."); + + var template = _config.Templates.FirstOrDefault(x => x.TemplateId == request.TemplateId); + if (template == null) + { + throw new RpcException(new Status(StatusCode.NotFound, "no such template exists.")); + } + + _logger.LogInformation($"Looking for latest ZFS snapshot for dataset '{template.ZfsSnapshotDataset}'."); + + using var httpClient = new HttpClient(new HttpClientHandler + { + // Ignore TLS errors because TrueNAS is usually running with a self-signed certificate on the local network. + ClientCertificateOptions = ClientCertificateOption.Manual, + ServerCertificateCustomValidationCallback = (_, _, _, _) => true, + }); + httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + _config.TrueNasApiKey); + + var latestSnapshotResponse = await httpClient.SendAsync( + new HttpRequestMessage + { + Method = HttpMethod.Get, + Content = new StringContent( + JsonSerializer.Serialize( + new TrueNasQuery + { + QueryFilters = new[] + { + new[] { "dataset", "=", template.ZfsSnapshotDataset }, + }, + QueryOptions = new TrueNasQueryOptions + { + Extra = new Dictionary { { "properties", "name, createtxg" } }, + OrderBy = new[] { "-createtxg" }, + Limit = 1, + } + }, + RemoteZfsSerializerContext.Default.TrueNasQuery), + new System.Net.Http.Headers.MediaTypeHeaderValue("application/json")), + RequestUri = new Uri($"{_config.TrueNasUrl}/zfs/snapshot"), + }, + cancellationToken: context.CancellationToken).ConfigureAwait(false); + latestSnapshotResponse.EnsureSuccessStatusCode(); + + var latestSnapshot = await latestSnapshotResponse.Content + .ReadFromJsonAsync(RemoteZfsSerializerContext.Default.TrueNasSnapshotArray) + .ConfigureAwait(false); + + if (latestSnapshot == null || latestSnapshot.Length != 1) + { + throw new RpcException(new Status(StatusCode.NotFound, "the target dataset has no latest snapshot.")); + } + + var rzfsTimestamp = $"RZFS_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"; + var rzfsId = $"{template.LinuxParentDataset}/{rzfsTimestamp}"; + + _logger.LogInformation($"Cloning ZFS snapshot to dataset '{rzfsId}'."); + + var cloneResponse = await httpClient.SendAsync( + new HttpRequestMessage + { + Method = HttpMethod.Post, + Content = new StringContent( + JsonSerializer.Serialize( + new TrueNasSnapshotClone + { + Snapshot = latestSnapshot[0].Id, + DatasetDest = rzfsId, + }, + RemoteZfsSerializerContext.Default.TrueNasSnapshotClone), + new System.Net.Http.Headers.MediaTypeHeaderValue("application/json")), + RequestUri = new Uri($"{_config.TrueNasUrl}/zfs/snapshot/clone"), + }, + cancellationToken: CancellationToken.None /* so this operation can't be interrupted after it succeeds */).ConfigureAwait(false); + cloneResponse.EnsureSuccessStatusCode(); + + try + { + await responseStream.WriteAsync(new AcquireWorkspaceResponse + { + WindowsShareRemotePath = $"{template.WindowsNetworkShareParentPath}\\{rzfsTimestamp}", + }, context.CancellationToken).ConfigureAwait(false); + + _logger.LogInformation($"Waiting for client to close connection..."); + while (!context.CancellationToken.IsCancellationRequested) + { + await Task.Delay(1000, context.CancellationToken); + } + } + catch (OperationCanceledException) + { + } + + _logger.LogInformation($"Deleting ZFS dataset '{rzfsId}'..."); + + var deleteResponse = await httpClient.SendAsync( + new HttpRequestMessage + { + Method = HttpMethod.Delete, + RequestUri = new Uri($"{_config.TrueNasUrl}/pool/dataset/id/{HttpUtility.UrlEncode(rzfsId)}"), + }, + cancellationToken: CancellationToken.None).ConfigureAwait(false); + deleteResponse.EnsureSuccessStatusCode(); + + _logger.LogInformation($"ZFS request complete."); + } } } } diff --git a/UET/uet/Commands/Internal/RemoteZfsServer/RemoteZfsServerConfig.cs b/UET/uet/Commands/Internal/RemoteZfsServer/RemoteZfsServerConfig.cs new file mode 100644 index 00000000..e5680433 --- /dev/null +++ b/UET/uet/Commands/Internal/RemoteZfsServer/RemoteZfsServerConfig.cs @@ -0,0 +1,36 @@ +namespace UET.Commands.Internal.RemoteZfsServer +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Text.Json.Serialization; + using System.Threading.Tasks; + + internal class RemoteZfsServerConfig + { + /// + /// The port that the remote ZFS server will listen on. If not set, defaults to 9000. + /// + [JsonPropertyName("ServerPort")] + public int? ServerPort { get; set; } + + /// + /// The TrueNAS REST API key to use. + /// + [JsonPropertyName("TrueNasApiKey")] + public required string TrueNasApiKey { get; set; } + + /// + /// The URL of the TrueNAS REST API. + /// + [JsonPropertyName("TrueNasUrl")] + public required string TrueNasUrl { get; set; } + + /// + /// The available templates. + /// + [JsonPropertyName("Templates")] + public required RemoteZfsServerConfigTemplate[] Templates { get; set; } + } +} diff --git a/UET/uet/Commands/Internal/RemoteZfsServer/RemoteZfsServerConfigTemplate.cs b/UET/uet/Commands/Internal/RemoteZfsServer/RemoteZfsServerConfigTemplate.cs new file mode 100644 index 00000000..a0f72733 --- /dev/null +++ b/UET/uet/Commands/Internal/RemoteZfsServer/RemoteZfsServerConfigTemplate.cs @@ -0,0 +1,31 @@ +namespace UET.Commands.Internal.RemoteZfsServer +{ + using System.Text.Json.Serialization; + + internal class RemoteZfsServerConfigTemplate + { + /// + /// The template ID that remote callers will use to instantiate a workspace. + /// + [JsonPropertyName("TemplateId")] + public required string TemplateId { get; set; } + + /// + /// The name of the ZFS dataset whose latest snapshot will be used to instantiate the workspace. + /// + [JsonPropertyName("ZfsSnapshotDataset")] + public required string ZfsSnapshotDataset { get; set; } + + /// + /// The path under which the ZFS snapshot should be instantiated. + /// + [JsonPropertyName("LinuxParentDataset")] + public required string LinuxParentDataset { get; set; } + + /// + /// The Windows network share directory that the ZFS snapshot will appear under once instantiated (i.e. this should be the Windows network share that exposes the directory referred to by LinuxParentDataset). + /// + [JsonPropertyName("WindowsNetworkShareParentPath")] + public required string WindowsNetworkShareParentPath { get; set; } + } +} diff --git a/UET/uet/Commands/Internal/RemoteZfsServer/TrueNasQuery.cs b/UET/uet/Commands/Internal/RemoteZfsServer/TrueNasQuery.cs new file mode 100644 index 00000000..fd0377a6 --- /dev/null +++ b/UET/uet/Commands/Internal/RemoteZfsServer/TrueNasQuery.cs @@ -0,0 +1,13 @@ +namespace UET.Commands.Internal.RemoteZfsServer +{ + using System.Text.Json.Serialization; + + internal class TrueNasQuery + { + [JsonPropertyName("query-filters"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string[][]? QueryFilters { get; set; } + + [JsonPropertyName("query-options"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public TrueNasQueryOptions? QueryOptions { get; set; } + } +} diff --git a/UET/uet/Commands/Internal/RemoteZfsServer/TrueNasQueryOptions.cs b/UET/uet/Commands/Internal/RemoteZfsServer/TrueNasQueryOptions.cs new file mode 100644 index 00000000..be67a52a --- /dev/null +++ b/UET/uet/Commands/Internal/RemoteZfsServer/TrueNasQueryOptions.cs @@ -0,0 +1,16 @@ +namespace UET.Commands.Internal.RemoteZfsServer +{ + using System.Text.Json.Serialization; + + internal class TrueNasQueryOptions + { + [JsonPropertyName("extra"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Dictionary? Extra { get; set; } + + [JsonPropertyName("order_by"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string[]? OrderBy { get; set; } + + [JsonPropertyName("limit"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? Limit { get; set; } + } +} diff --git a/UET/uet/Commands/Internal/RemoteZfsServer/TrueNasSnapshot.cs b/UET/uet/Commands/Internal/RemoteZfsServer/TrueNasSnapshot.cs new file mode 100644 index 00000000..c5ba2c91 --- /dev/null +++ b/UET/uet/Commands/Internal/RemoteZfsServer/TrueNasSnapshot.cs @@ -0,0 +1,28 @@ +namespace UET.Commands.Internal.RemoteZfsServer +{ + using System.Text.Json.Serialization; + + internal class TrueNasSnapshot + { + [JsonPropertyName("pool")] + public required string Pool { get; set; } + + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("type")] + public required string Type { get; set; } + + [JsonPropertyName("snapshot_name")] + public required string SnapshotName { get; set; } + + [JsonPropertyName("dataset")] + public required string Dataset { get; set; } + + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("createtxg")] + public required string CreateTxg { get; set; } + } +} diff --git a/UET/uet/Commands/Internal/RemoteZfsServer/TrueNasSnapshotClone.cs b/UET/uet/Commands/Internal/RemoteZfsServer/TrueNasSnapshotClone.cs new file mode 100644 index 00000000..f22e0fcc --- /dev/null +++ b/UET/uet/Commands/Internal/RemoteZfsServer/TrueNasSnapshotClone.cs @@ -0,0 +1,13 @@ +namespace UET.Commands.Internal.RemoteZfsServer +{ + using System.Text.Json.Serialization; + + internal class TrueNasSnapshotClone + { + [JsonPropertyName("snapshot"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Snapshot { get; set; } + + [JsonPropertyName("dataset_dst"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? DatasetDest { get; set; } + } +} diff --git a/UET/uet/Commands/Internal/RemoteZfsTest/RemoteZfsTestCommand.cs b/UET/uet/Commands/Internal/RemoteZfsTest/RemoteZfsTestCommand.cs index 5495e263..3798c78e 100644 --- a/UET/uet/Commands/Internal/RemoteZfsTest/RemoteZfsTestCommand.cs +++ b/UET/uet/Commands/Internal/RemoteZfsTest/RemoteZfsTestCommand.cs @@ -70,11 +70,14 @@ public async Task ExecuteAsync(InvocationContext context) _logger.LogInformation($"Connecting..."); var client = _grpcPipeFactory.CreateNetworkClient( - IPEndPoint.Parse(host), + new IPEndPoint(IPAddress.Parse(host), port), invoker => new RemoteZfs.RemoteZfsClient(invoker)); - _logger.LogInformation($"Requesting share..."); - var response = client.Acquire(cancellationToken: context.GetCancellationToken()); + _logger.LogInformation($"Opening stream..."); + using var response = client.AcquireWorkspace(new AcquireWorkspaceRequest + { + TemplateId = "unreal-engine", + }, cancellationToken: context.GetCancellationToken()); _logger.LogInformation($"Waiting for acquisition..."); var acquired = await response.ResponseStream.MoveNext(context.GetCancellationToken()).ConfigureAwait(false); @@ -84,14 +87,11 @@ public async Task ExecuteAsync(InvocationContext context) return 1; } - _logger.LogInformation($"Allocated share: {response.ResponseStream.Current.WindowsSharePath}"); + _logger.LogInformation($"Allocated share: {response.ResponseStream.Current.WindowsShareRemotePath}"); _logger.LogInformation($"Waiting 10 seconds..."); await Task.Delay(10000, context.GetCancellationToken()).ConfigureAwait(false); - _logger.LogInformation($"Closing request stream..."); - await response.RequestStream.CompleteAsync().ConfigureAwait(false); - return 0; } }