diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/Agent.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/Agent.cs index 319c9acf3f6..142b8697ef8 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/Agent.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/Agent.cs @@ -30,7 +30,7 @@ public class Agent readonly AsyncLock reconcileLock = new AsyncLock(); readonly ISerde deploymentConfigInfoSerde; readonly IEncryptionProvider encryptionProvider; - readonly IAvailabilityMetric availabilityMetric; + readonly IDeploymentMetrics deploymentMetrics; IEnvironment environment; DeploymentConfigInfo currentConfig; DeploymentStatus status; @@ -46,7 +46,7 @@ public Agent( DeploymentConfigInfo initialDeployedConfigInfo, ISerde deploymentConfigInfoSerde, IEncryptionProvider encryptionProvider, - IAvailabilityMetric availabilityMetric) + IDeploymentMetrics deploymentMetrics) { this.configSource = Preconditions.CheckNotNull(configSource, nameof(configSource)); this.planner = Preconditions.CheckNotNull(planner, nameof(planner)); @@ -59,7 +59,7 @@ public Agent( this.deploymentConfigInfoSerde = Preconditions.CheckNotNull(deploymentConfigInfoSerde, nameof(deploymentConfigInfoSerde)); this.environment = this.environmentProvider.Create(this.currentConfig.DeploymentConfig); this.encryptionProvider = Preconditions.CheckNotNull(encryptionProvider, nameof(encryptionProvider)); - this.availabilityMetric = Preconditions.CheckNotNull(availabilityMetric, nameof(availabilityMetric)); + this.deploymentMetrics = Preconditions.CheckNotNull(deploymentMetrics, nameof(deploymentMetrics)); this.status = DeploymentStatus.Unknown; Events.AgentCreated(); } @@ -74,7 +74,7 @@ public static async Task Create( IEntityStore configStore, ISerde deploymentConfigInfoSerde, IEncryptionProvider encryptionProvider, - IAvailabilityMetric availabilityMetric) + IDeploymentMetrics deploymentMetrics) { Preconditions.CheckNotNull(deploymentConfigInfoSerde, nameof(deploymentConfigInfoSerde)); Preconditions.CheckNotNull(configStore, nameof(configStore)); @@ -106,7 +106,7 @@ await deploymentConfigInfoJson.ForEachAsync( deploymentConfigInfo.GetOrElse(DeploymentConfigInfo.Empty), deploymentConfigInfoSerde, encryptionProvider, - availabilityMetric); + deploymentMetrics); return agent; } @@ -133,7 +133,7 @@ public async Task ReconcileAsync(CancellationToken token) else { ModuleSet desiredModuleSet = deploymentConfig.GetModuleSet(); - _ = Task.Run(() => this.availabilityMetric.ComputeAvailability(desiredModuleSet, current)) + _ = Task.Run(() => this.deploymentMetrics.ComputeAvailability(desiredModuleSet, current)) .ContinueWith(t => Events.UnknownFailure(t.Exception), TaskContinuationOptions.OnlyOnFaulted) .ConfigureAwait(false); @@ -152,11 +152,14 @@ public async Task ReconcileAsync(CancellationToken token) { try { - bool result = await this.planRunner.ExecuteAsync(deploymentConfigInfo.Version, plan, token); - await this.UpdateCurrentConfig(deploymentConfigInfo); - if (result) + using (this.deploymentMetrics.ReportDeploymentTime()) { - this.status = DeploymentStatus.Success; + bool result = await this.planRunner.ExecuteAsync(deploymentConfigInfo.Version, plan, token); + await this.UpdateCurrentConfig(deploymentConfigInfo); + if (result) + { + this.status = DeploymentStatus.Success; + } } } catch (Exception ex) when (!ex.IsFatal()) diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/Constants.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/Constants.cs index 24038260f82..d0eb92a3e51 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/Constants.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/Constants.cs @@ -77,7 +77,7 @@ public static class Constants public const string NetworkIdKey = "NetworkId"; - public const string EdgeletClientApiVersion = "2019-11-05"; + public const string EdgeletClientApiVersion = "2020-07-07"; public const string EdgeletInitializationVectorFileName = "IOTEDGE_BACKUP_IV"; diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/SystemInfo.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/SystemInfo.cs index 2d5a73d86ce..768f3be043f 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/SystemInfo.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/SystemInfo.cs @@ -6,12 +6,20 @@ namespace Microsoft.Azure.Devices.Edge.Agent.Core public class SystemInfo { - [JsonConstructor] - public SystemInfo(string operatingSystemType, string architecture, string version) + public SystemInfo(string operatingSystemType, string architecture, string version, string serverVersion, string kernelVersion, string operatingSystem, int numCpus) { this.OperatingSystemType = operatingSystemType; this.Architecture = architecture; this.Version = version; + this.ServerVersion = serverVersion; + this.KernelVersion = kernelVersion; + this.OperatingSystem = operatingSystem; + this.NumCpus = numCpus; + } + + public SystemInfo(string operatingSystemType, string architecture, string version) + : this(operatingSystemType, architecture, version, string.Empty, string.Empty, string.Empty, 0) + { } public string OperatingSystemType { get; } @@ -20,6 +28,14 @@ public SystemInfo(string operatingSystemType, string architecture, string versio public string Version { get; } + public string ServerVersion { get; } + + public string KernelVersion { get; } + + public string OperatingSystem { get; } + + public int NumCpus { get; } + static SystemInfo Empty { get; } = new SystemInfo(string.Empty, string.Empty, string.Empty); } } diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/logs/ILogsUploader.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/logs/ILogsUploader.cs deleted file mode 100644 index c6f0bff472c..00000000000 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/logs/ILogsUploader.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -namespace Microsoft.Azure.Devices.Edge.Agent.Core.Logs -{ - using System; - using System.Threading.Tasks; - - public interface ILogsUploader - { - Task Upload(string uri, string module, byte[] payload, LogsContentEncoding logsContentEncoding, LogsContentType logsContentType); - - Task, Task>> GetUploaderCallback(string uri, string module, LogsContentEncoding logsContentEncoding, LogsContentType logsContentType); - } -} diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/logs/IRequestsUploader.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/logs/IRequestsUploader.cs new file mode 100644 index 00000000000..099f9307f1a --- /dev/null +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/logs/IRequestsUploader.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +namespace Microsoft.Azure.Devices.Edge.Agent.Core.Logs +{ + using System; + using System.IO; + using System.Threading.Tasks; + + public interface IRequestsUploader + { + Task UploadLogs(string uri, string module, byte[] payload, LogsContentEncoding logsContentEncoding, LogsContentType logsContentType); + + Task, Task>> GetLogsUploaderCallback(string uri, string module, LogsContentEncoding logsContentEncoding, LogsContentType logsContentType); + + Task UploadSupportBundle(string uri, Stream source); + } +} diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/metrics/AvaliabilityMetrics.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/metrics/DeploymentMetrics.cs similarity index 79% rename from edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/metrics/AvaliabilityMetrics.cs rename to edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/metrics/DeploymentMetrics.cs index 583263806ec..02d0f0a2db5 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/metrics/AvaliabilityMetrics.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/metrics/DeploymentMetrics.cs @@ -11,12 +11,16 @@ namespace Microsoft.Azure.Devices.Edge.Agent.Core.Metrics using Microsoft.Azure.Devices.Edge.Util.Metrics; using Microsoft.Extensions.Logging; - public class AvailabilityMetrics : IAvailabilityMetric, IDisposable + public class DeploymentMetrics : IDeploymentMetrics, IDisposable { readonly IMetricsGauge running; readonly IMetricsGauge expectedRunning; + readonly IMetricsCounter unsuccessfulSyncs; + readonly IMetricsCounter totalSyncs; + readonly IMetricsHistogram deploymentTime; + readonly ISystemTime systemTime; - readonly ILogger log = Logger.Factory.CreateLogger(); + readonly ILogger log = Logger.Factory.CreateLogger(); // This allows edgeAgent to track its own avaliability. If edgeAgent shutsdown unexpectedly, it can look at the last checkpoint time to determine its previous avaliability. readonly TimeSpan checkpointFrequency = TimeSpan.FromMinutes(5); @@ -26,7 +30,7 @@ public class AvailabilityMetrics : IAvailabilityMetric, IDisposable readonly List availabilities; readonly Lazy edgeAgent; - public AvailabilityMetrics(IMetricsProvider metricsProvider, string storageFolder, ISystemTime time = null) + public DeploymentMetrics(IMetricsProvider metricsProvider, string storageFolder, ISystemTime time = null) { this.systemTime = time ?? SystemTime.Instance; this.availabilities = new List(); @@ -43,6 +47,21 @@ public AvailabilityMetrics(IMetricsProvider metricsProvider, string storageFolde "The amount of time the module was specified in the deployment", new List { "module_name", MetricsConstants.MsTelemetry }); + this.unsuccessfulSyncs = metricsProvider.CreateCounter( + "total_unsuccessful_iothub_syncs", + "The amount of times edgeAgent failed to sync with iotHub", + new List { MetricsConstants.MsTelemetry }); + + this.totalSyncs = metricsProvider.CreateCounter( + "total_iothub_syncs", + "The amount of times edgeAgent attempted to sync with iotHub, both successful and unsuccessful", + new List { MetricsConstants.MsTelemetry }); + + this.deploymentTime = metricsProvider.CreateHistogram( + "deployment_time_seconds", + "The amount of time it took to complete a new deployment", + new List { MetricsConstants.MsTelemetry }); + string storageDirectory = Path.Combine(Preconditions.CheckNonWhiteSpace(storageFolder, nameof(storageFolder)), "availability"); try { @@ -132,6 +151,22 @@ public void IndicateCleanShutdown() } } + public IDisposable ReportDeploymentTime() + { + return DurationMeasurer.MeasureDuration(duration => this.deploymentTime.Update(duration.TotalSeconds, new string[] { true.ToString() })); + } + + public void ReportIotHubSync(bool successful) + { + string[] tags = { true.ToString() }; + this.totalSyncs.Increment(1, tags); + + if (!successful) + { + this.unsuccessfulSyncs.Increment(1, tags); + } + } + TimeSpan CalculateEdgeAgentDowntime() { try diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/metrics/IAvailabilityMetric.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/metrics/IDeploymentMetrics.cs similarity index 60% rename from edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/metrics/IAvailabilityMetric.cs rename to edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/metrics/IDeploymentMetrics.cs index 8e684834c0b..f92138d3cdd 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/metrics/IAvailabilityMetric.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/metrics/IDeploymentMetrics.cs @@ -2,9 +2,13 @@ namespace Microsoft.Azure.Devices.Edge.Agent.Core.Metrics { - public interface IAvailabilityMetric + using System; + + public interface IDeploymentMetrics { void ComputeAvailability(ModuleSet desired, ModuleSet current); void IndicateCleanShutdown(); + void ReportIotHubSync(bool successful); + IDisposable ReportDeploymentTime(); } } diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/metrics/MetadataMetrics.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/metrics/MetadataMetrics.cs new file mode 100644 index 00000000000..e9b549081c0 --- /dev/null +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/metrics/MetadataMetrics.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.Azure.Devices.Edge.Agent.Core.Metrics +{ + using System; + using System.Collections.Generic; + using System.Text; + using System.Threading.Tasks; + using Microsoft.Azure.Devices.Edge.Util; + using Microsoft.Azure.Devices.Edge.Util.Metrics; + using Microsoft.Extensions.Logging; + + public class MetadataMetrics + { + readonly IMetricsGauge metaData; + readonly Func> getSystemMetadata; + + public MetadataMetrics(IMetricsProvider metricsProvider, Func> getSystemMetadata) + { + this.getSystemMetadata = Preconditions.CheckNotNull(getSystemMetadata, nameof(getSystemMetadata)); + + Preconditions.CheckNotNull(metricsProvider, nameof(metricsProvider)); + this.metaData = metricsProvider.CreateGauge( + "metadata", + "General metadata about the device. The value is always 0, information is encoded in the tags.", + new List { "edge_agent_version", "experimental_features", "host_information", MetricsConstants.MsTelemetry }); + } + + public async Task Start(ILogger logger, string agentVersion, string experimentalFeatures) + { + logger.LogInformation("Collecting metadata metrics"); + string edgeletVersion = Newtonsoft.Json.JsonConvert.SerializeObject(await this.getSystemMetadata()); + + string[] values = { agentVersion, experimentalFeatures, edgeletVersion, true.ToString() }; + this.metaData.Set(0, values); + logger.LogInformation($"Set metadata metrics: {values.Join(", ")}"); + } + } +} diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/LogsRequest.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/ModuleLogsRequest.cs similarity index 94% rename from edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/LogsRequest.cs rename to edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/ModuleLogsRequest.cs index 4ab96a8c9d7..191b72419ce 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/LogsRequest.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/ModuleLogsRequest.cs @@ -8,9 +8,9 @@ namespace Microsoft.Azure.Devices.Edge.Agent.Core.Requests using Microsoft.Azure.Devices.Edge.Util.Json; using Newtonsoft.Json; - public class LogsRequest + public class ModuleLogsRequest { - public LogsRequest( + public ModuleLogsRequest( string schemaVersion, List items, LogsContentEncoding encoding, @@ -23,7 +23,7 @@ public LogsRequest( } [JsonConstructor] - LogsRequest( + ModuleLogsRequest( string schemaVersion, List items, LogsContentEncoding? encoding, diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/LogsRequestHandler.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/ModuleLogsRequestHandler.cs similarity index 84% rename from edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/LogsRequestHandler.cs rename to edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/ModuleLogsRequestHandler.cs index a27cd76ab5e..0f5751c0d8d 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/LogsRequestHandler.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/ModuleLogsRequestHandler.cs @@ -11,7 +11,7 @@ namespace Microsoft.Azure.Devices.Edge.Agent.Core.Requests using Microsoft.Azure.Devices.Edge.Util; using Microsoft.Extensions.Logging; - public class LogsRequestHandler : RequestHandlerBase> + public class ModuleLogsRequestHandler : RequestHandlerBase> { const int MaxTailValue = 500; @@ -20,7 +20,7 @@ public class LogsRequestHandler : RequestHandlerBase "GetLogs"; - protected override async Task>> HandleRequestInternal(Option payloadOption, CancellationToken cancellationToken) + protected override async Task>> HandleRequestInternal(Option payloadOption, CancellationToken cancellationToken) { - LogsRequest payload = payloadOption.Expect(() => new ArgumentException("Request payload not found")); + ModuleLogsRequest payload = payloadOption.Expect(() => new ArgumentException("Request payload not found")); if (ExpectedSchemaVersion.CompareMajorVersion(payload.SchemaVersion, "logs upload request schema") != 0) { Events.MismatchedMinorVersions(payload.SchemaVersion, ExpectedSchemaVersion); @@ -47,7 +47,7 @@ protected override async Task>> HandleRequestIn false); IList<(string id, ModuleLogOptions logOptions)> logOptionsList = await requestToOptionsMapper.MapToLogOptions(payload.Items, cancellationToken); - IEnumerable> uploadLogsTasks = logOptionsList.Select( + IEnumerable> uploadLogsTasks = logOptionsList.Select( async l => { Events.ReceivedLogOptions(l); @@ -65,17 +65,17 @@ protected override async Task>> HandleRequestIn Events.ReceivedModuleLogs(moduleLogs, l.id); return logOptions.ContentEncoding == LogsContentEncoding.Gzip - ? new LogsResponse(l.id, moduleLogs) - : new LogsResponse(l.id, moduleLogs.FromBytes()); + ? new ModuleLogsResponse(l.id, moduleLogs) + : new ModuleLogsResponse(l.id, moduleLogs.FromBytes()); }); - IEnumerable response = await Task.WhenAll(uploadLogsTasks); + IEnumerable response = await Task.WhenAll(uploadLogsTasks); return Option.Some(response); } static class Events { const int IdStart = AgentEventIds.LogsRequestHandler; - static readonly ILogger Log = Logger.Factory.CreateLogger(); + static readonly ILogger Log = Logger.Factory.CreateLogger(); enum EventIds { @@ -107,7 +107,7 @@ public static void ReceivedLogOptions((string id, ModuleLogOptions logOptions) r } } - public static void ProcessingRequest(LogsRequest payload) + public static void ProcessingRequest(ModuleLogsRequest payload) { Log.LogInformation((int)EventIds.ProcessingRequest, $"Processing request to get logs for {payload.ToJson()}"); } diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/LogsResponse.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/ModuleLogsResponse.cs similarity index 79% rename from edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/LogsResponse.cs rename to edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/ModuleLogsResponse.cs index bf26789e1f4..b7d93fbc8b1 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/LogsResponse.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/ModuleLogsResponse.cs @@ -5,20 +5,20 @@ namespace Microsoft.Azure.Devices.Edge.Agent.Core.Requests using Microsoft.Azure.Devices.Edge.Util.Json; using Newtonsoft.Json; - public class LogsResponse + public class ModuleLogsResponse { - public LogsResponse(string id, byte[] payloadBytes) + public ModuleLogsResponse(string id, byte[] payloadBytes) : this(id, null, payloadBytes) { } - public LogsResponse(string id, string payload) + public ModuleLogsResponse(string id, string payload) : this(id, payload, null) { } [JsonConstructor] - LogsResponse(string id, string payload, byte[] payloadBytes) + ModuleLogsResponse(string id, string payload, byte[] payloadBytes) { this.Id = Preconditions.CheckNonWhiteSpace(id, nameof(id)); this.PayloadBytes = Option.Maybe(payloadBytes); diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/LogsUploadRequest.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/ModuleLogsUploadRequest.cs similarity index 94% rename from edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/LogsUploadRequest.cs rename to edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/ModuleLogsUploadRequest.cs index 6da209e7fa9..038f0738c89 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/LogsUploadRequest.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/ModuleLogsUploadRequest.cs @@ -8,9 +8,9 @@ namespace Microsoft.Azure.Devices.Edge.Agent.Core.Requests using Microsoft.Azure.Devices.Edge.Util.Json; using Newtonsoft.Json; - public class LogsUploadRequest + public class ModuleLogsUploadRequest { - public LogsUploadRequest( + public ModuleLogsUploadRequest( string schemaVersion, List items, LogsContentEncoding encoding, @@ -25,7 +25,7 @@ public LogsUploadRequest( } [JsonConstructor] - LogsUploadRequest( + ModuleLogsUploadRequest( string schemaVersion, List items, LogsContentEncoding? encoding, diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/LogsUploadRequestHandler.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/ModuleLogsUploadRequestHandler.cs similarity index 79% rename from edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/LogsUploadRequestHandler.cs rename to edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/ModuleLogsUploadRequestHandler.cs index b32a6e0178a..a794be78ff0 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/LogsUploadRequestHandler.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/ModuleLogsUploadRequestHandler.cs @@ -11,26 +11,26 @@ namespace Microsoft.Azure.Devices.Edge.Agent.Core.Requests using Microsoft.Azure.Devices.Edge.Util; using Microsoft.Extensions.Logging; - public class LogsUploadRequestHandler : RequestHandlerBase + public class ModuleLogsUploadRequestHandler : RequestHandlerBase { static readonly Version ExpectedSchemaVersion = new Version("1.0"); - readonly ILogsUploader logsUploader; + readonly IRequestsUploader requestsUploader; readonly ILogsProvider logsProvider; readonly IRuntimeInfoProvider runtimeInfoProvider; - public LogsUploadRequestHandler(ILogsUploader logsUploader, ILogsProvider logsProvider, IRuntimeInfoProvider runtimeInfoProvider) + public ModuleLogsUploadRequestHandler(IRequestsUploader requestsUploader, ILogsProvider logsProvider, IRuntimeInfoProvider runtimeInfoProvider) { this.logsProvider = Preconditions.CheckNotNull(logsProvider, nameof(logsProvider)); - this.logsUploader = Preconditions.CheckNotNull(logsUploader, nameof(logsUploader)); + this.requestsUploader = Preconditions.CheckNotNull(requestsUploader, nameof(requestsUploader)); this.runtimeInfoProvider = Preconditions.CheckNotNull(runtimeInfoProvider, nameof(runtimeInfoProvider)); } - public override string RequestName => "UploadLogs"; + public override string RequestName => "UploadModuleLogs"; - protected override async Task> HandleRequestInternal(Option payloadOption, CancellationToken cancellationToken) + protected override async Task> HandleRequestInternal(Option payloadOption, CancellationToken cancellationToken) { - LogsUploadRequest payload = payloadOption.Expect(() => new ArgumentException("Request payload not found")); + ModuleLogsUploadRequest payload = payloadOption.Expect(() => new ArgumentException("Request payload not found")); if (ExpectedSchemaVersion.CompareMajorVersion(payload.SchemaVersion, "logs upload request schema") != 0) { Events.MismatchedMinorVersions(payload.SchemaVersion, ExpectedSchemaVersion); @@ -69,11 +69,11 @@ async Task UploadLogs(string sasUrl, string id, ModuleLogOptions moduleLogOption if (moduleLogOptions.ContentType == LogsContentType.Json) { byte[] logBytes = await this.logsProvider.GetLogs(id, moduleLogOptions, token); - await this.logsUploader.Upload(sasUrl, id, logBytes, moduleLogOptions.ContentEncoding, moduleLogOptions.ContentType); + await this.requestsUploader.UploadLogs(sasUrl, id, logBytes, moduleLogOptions.ContentEncoding, moduleLogOptions.ContentType); } else if (moduleLogOptions.ContentType == LogsContentType.Text) { - Func, Task> uploaderCallback = await this.logsUploader.GetUploaderCallback(sasUrl, id, moduleLogOptions.ContentEncoding, moduleLogOptions.ContentType); + Func, Task> uploaderCallback = await this.requestsUploader.GetLogsUploaderCallback(sasUrl, id, moduleLogOptions.ContentEncoding, moduleLogOptions.ContentType); await this.logsProvider.GetLogsStream(id, moduleLogOptions, uploaderCallback, token); } @@ -83,7 +83,7 @@ async Task UploadLogs(string sasUrl, string id, ModuleLogOptions moduleLogOption static class Events { const int IdStart = AgentEventIds.LogsUploadRequestHandler; - static readonly ILogger Log = Logger.Factory.CreateLogger(); + static readonly ILogger Log = Logger.Factory.CreateLogger(); enum EventIds { @@ -98,7 +98,7 @@ public static void MismatchedMinorVersions(string payloadSchemaVersion, Version Log.LogWarning((int)EventIds.MismatchedMinorVersions, $"Logs upload request schema version {payloadSchemaVersion} does not match expected schema version {expectedSchemaVersion}. Some settings may not be supported."); } - public static void ProcessingRequest(LogsUploadRequest payload) + public static void ProcessingRequest(ModuleLogsUploadRequest payload) { Log.LogInformation((int)EventIds.ProcessingRequest, $"Processing request to upload logs for {payload.ToJson()}"); } diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/RequestHandlerBase.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/RequestHandlerBase.cs index 7bc16deddb8..3cb40c0bcbd 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/RequestHandlerBase.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/RequestHandlerBase.cs @@ -6,6 +6,7 @@ namespace Microsoft.Azure.Devices.Edge.Agent.Core.Requests using System.Threading.Tasks; using Microsoft.Azure.Devices.Edge.Storage; using Microsoft.Azure.Devices.Edge.Util; + using Prometheus; public abstract class RequestHandlerBase : IRequestHandler where TU : class @@ -13,8 +14,17 @@ public abstract class RequestHandlerBase : IRequestHandler { public abstract string RequestName { get; } + static readonly Counter numCalls = Metrics.CreateCounter( + "edgeagent_direct_method_invocations_count", + "Number of times a direct method is called", + new CounterConfiguration + { + LabelNames = new[] { "method_name" } + }); + public async Task> HandleRequest(Option payloadJson, CancellationToken cancellationToken) { + numCalls.WithLabels(this.RequestName).Inc(); Option payload = this.ParsePayload(payloadJson); Option result = await this.HandleRequestInternal(payload, cancellationToken); Option responseJson = result.Map(r => r.ToJson()); diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/SupportBundleRequest.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/SupportBundleRequest.cs new file mode 100644 index 00000000000..093d9cf1ac3 --- /dev/null +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/SupportBundleRequest.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.Azure.Devices.Edge.Agent.Core.Requests +{ + using System; + using System.Collections.Generic; + using System.Text; + using Microsoft.Azure.Devices.Edge.Util; + using Newtonsoft.Json; + + public class SupportBundleRequest + { + public SupportBundleRequest(string schemaVersion, string sasUrl, string since, bool? edgeRuntimeOnly) + { + this.SchemaVersion = Preconditions.CheckNonWhiteSpace(schemaVersion, nameof(schemaVersion)); + this.SasUrl = Preconditions.CheckNotNull(sasUrl, nameof(sasUrl)); + this.Since = Option.Maybe(since); + this.EdgeRuntimeOnly = Option.Maybe(edgeRuntimeOnly); + } + + [JsonProperty("schemaVersion")] + public string SchemaVersion { get; } + + [JsonIgnore] + public string SasUrl { get; } + + [JsonProperty("since")] + public Option Since { get; } + + [JsonProperty("edgeRuntimeOnly")] + public Option EdgeRuntimeOnly { get; } + } +} diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/SupportBundleRequestHandler.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/SupportBundleRequestHandler.cs new file mode 100644 index 00000000000..a8aa21f1c15 --- /dev/null +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/requests/SupportBundleRequestHandler.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.Azure.Devices.Edge.Agent.Core.Requests +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Azure.Devices.Edge.Agent.Core.Logs; + using Microsoft.Azure.Devices.Edge.Util; + + public class SupportBundleRequestHandler : RequestHandlerBase + { + public delegate Task GetSupportBundle(Option since, Option iothubHostname, Option edgeRuntimeOnly, CancellationToken token); + + readonly GetSupportBundle getSupportBundle; + readonly IRequestsUploader requestsUploader; + readonly string iotHubHostName; + + public SupportBundleRequestHandler(GetSupportBundle getSupportBundle, IRequestsUploader requestsUploader, string iotHubHostName) + { + this.getSupportBundle = getSupportBundle; + this.requestsUploader = requestsUploader; + this.iotHubHostName = iotHubHostName; + } + + public override string RequestName => "UploadSupportBundle"; + + protected override Task> HandleRequestInternal(Option payloadOption, CancellationToken cancellationToken) + { + SupportBundleRequest payload = payloadOption.Expect(() => new ArgumentException("Request payload not found")); + + (string correlationId, BackgroundTaskStatus status) = BackgroundTask.Run( + async () => + { + Stream source = await this.getSupportBundle(payload.Since, Option.Maybe(this.iotHubHostName), payload.EdgeRuntimeOnly, cancellationToken); + await this.requestsUploader.UploadSupportBundle(payload.SasUrl, source); + }, + "upload support bundle", + cancellationToken); + + return Task.FromResult(Option.Some(TaskStatusResponse.Create(correlationId, status))); + } + } +} diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/IModuleManager.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/IModuleManager.cs index 793af66983a..6e96c64fae8 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/IModuleManager.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/IModuleManager.cs @@ -35,5 +35,7 @@ public interface IModuleManager Task PrepareUpdateAsync(ModuleSpec moduleSpec); Task GetModuleLogs(string name, bool follow, Option tail, Option since, CancellationToken cancellationToken); + + Task GetSupportBundle(Option since, Option iothubHostname, Option edgeRuntimeOnly, CancellationToken token); } } diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/ModuleManagementHttpClient.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/ModuleManagementHttpClient.cs index ca7de4b9ae2..4f6ca511a7c 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/ModuleManagementHttpClient.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/ModuleManagementHttpClient.cs @@ -60,9 +60,13 @@ public ModuleManagementHttpClient(Uri managementUri, string serverSupportedApiVe public Task GetModuleLogs(string name, bool follow, Option tail, Option since, CancellationToken cancellationToken) => this.inner.GetModuleLogs(name, follow, tail, since, cancellationToken); + public Task GetSupportBundle(Option since, Option iothubHostname, Option edgeRuntimeOnly, CancellationToken token) => + this.inner.GetSupportBundle(since, iothubHostname, edgeRuntimeOnly, token); + internal static ModuleManagementHttpClientVersioned GetVersionedModuleManagement(Uri managementUri, string serverSupportedApiVersion, string clientSupportedApiVersion) { ApiVersion supportedVersion = GetSupportedVersion(serverSupportedApiVersion, clientSupportedApiVersion); + if (supportedVersion == ApiVersion.Version20180628) { return new Version_2018_06_28.ModuleManagementHttpClient(managementUri); @@ -83,6 +87,11 @@ internal static ModuleManagementHttpClientVersioned GetVersionedModuleManagement return new Version_2019_11_05.ModuleManagementHttpClient(managementUri); } + if (supportedVersion == ApiVersion.Version20200707) + { + return new Version_2020_07_07.ModuleManagementHttpClient(managementUri); + } + return new Version_2018_06_28.ModuleManagementHttpClient(managementUri); } diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/RuntimeInfoProvider.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/RuntimeInfoProvider.cs index 1b91adda84c..752227fc5dd 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/RuntimeInfoProvider.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/RuntimeInfoProvider.cs @@ -24,5 +24,8 @@ public Task GetModuleLogs(string module, bool follow, Option tail, this.moduleManager.GetModuleLogs(module, follow, tail, since, cancellationToken); public Task GetSystemInfo(CancellationToken token) => this.moduleManager.GetSystemInfoAsync(token); + + public Task GetSupportBundle(Option since, Option iothubHostname, Option edgeRuntimeOnly, CancellationToken token) => + this.moduleManager.GetSupportBundle(since, iothubHostname, edgeRuntimeOnly, token); } } diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2018_06_28/ModuleManagementHttpClient.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2018_06_28/ModuleManagementHttpClient.cs index 53c2a54fe6a..ac226f4f549 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2018_06_28/ModuleManagementHttpClient.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2018_06_28/ModuleManagementHttpClient.cs @@ -193,6 +193,11 @@ public override Task GetSystemResourcesAsync() return Task.FromResult(null); } + public override Task GetSupportBundle(Option since, Option iothubHostname, Option edgeRuntimeOnly, CancellationToken token) + { + return Task.FromResult(System.IO.Stream.Null); + } + protected override void HandleException(Exception exception, string operation) { switch (exception) diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2019_01_30/ModuleManagementHttpClient.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2019_01_30/ModuleManagementHttpClient.cs index 2672d9ec439..248f85150bc 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2019_01_30/ModuleManagementHttpClient.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2019_01_30/ModuleManagementHttpClient.cs @@ -197,6 +197,11 @@ public override Task GetSystemResourcesAsync() return Task.FromResult(null); } + public override Task GetSupportBundle(Option since, Option iothubHostname, Option edgeRuntimeOnly, CancellationToken token) + { + return Task.FromResult(System.IO.Stream.Null); + } + protected override void HandleException(Exception exception, string operation) { switch (exception) diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2019_10_22/ModuleManagementHttpClient.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2019_10_22/ModuleManagementHttpClient.cs index 8a56ef26e89..0c6a960c101 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2019_10_22/ModuleManagementHttpClient.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2019_10_22/ModuleManagementHttpClient.cs @@ -201,6 +201,11 @@ public override Task GetSystemResourcesAsync() return Task.FromResult(null); } + public override Task GetSupportBundle(Option since, Option iothubHostname, Option edgeRuntimeOnly, CancellationToken token) + { + return Task.FromResult(System.IO.Stream.Null); + } + protected override void HandleException(Exception exception, string operation) { switch (exception) diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2019_11_05/ModuleManagementHttpClient.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2019_11_05/ModuleManagementHttpClient.cs index 2f96510c25a..144dc0e92e8 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2019_11_05/ModuleManagementHttpClient.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2019_11_05/ModuleManagementHttpClient.cs @@ -210,6 +210,11 @@ public override async Task ReprovisionDeviceAsync() } } + public override Task GetSupportBundle(Option since, Option iothubHostname, Option edgeRuntimeOnly, CancellationToken token) + { + return Task.FromResult(System.IO.Stream.Null); + } + protected override void HandleException(Exception exception, string operation) { switch (exception) diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2020_07_07/ModuleManagementHttpClient.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2020_07_07/ModuleManagementHttpClient.cs new file mode 100644 index 00000000000..9799cf3e535 --- /dev/null +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2020_07_07/ModuleManagementHttpClient.cs @@ -0,0 +1,333 @@ +// Copyright (c) Microsoft. All rights reserved. +namespace Microsoft.Azure.Devices.Edge.Agent.Edgelet.Version_2020_07_07 +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.IO; + using System.Linq; + using System.Net.Http; + using System.Runtime.ExceptionServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Azure.Devices.Edge.Agent.Core; + using Microsoft.Azure.Devices.Edge.Agent.Edgelet.Version_2020_07_07.GeneratedCode; + using Microsoft.Azure.Devices.Edge.Agent.Edgelet.Versioning; + using Microsoft.Azure.Devices.Edge.Util; + using Microsoft.Azure.Devices.Edge.Util.Edged; + using Microsoft.Azure.Devices.Edge.Util.TransientFaultHandling; + using Newtonsoft.Json.Linq; + using Disk = Microsoft.Azure.Devices.Edge.Agent.Edgelet.Models.Disk; + using Identity = Microsoft.Azure.Devices.Edge.Agent.Edgelet.Models.Identity; + using ModuleSpec = Microsoft.Azure.Devices.Edge.Agent.Edgelet.Models.ModuleSpec; + using SystemInfo = Microsoft.Azure.Devices.Edge.Agent.Core.SystemInfo; + using SystemResources = Microsoft.Azure.Devices.Edge.Agent.Edgelet.Models.SystemResources; + + class ModuleManagementHttpClient : ModuleManagementHttpClientVersioned + { + public ModuleManagementHttpClient(Uri managementUri) + : this(managementUri, Option.None()) + { + } + + internal ModuleManagementHttpClient(Uri managementUri, Option operationTimeout) + : base(managementUri, ApiVersion.Version20200707, new ErrorDetectionStrategy(), operationTimeout) + { + } + + public override async Task CreateIdentityAsync(string name, string managedBy) + { + using (HttpClient httpClient = HttpClientHelper.GetHttpClient(this.ManagementUri)) + { + var edgeletHttpClient = new EdgeletHttpClient(httpClient) { BaseUrl = HttpClientHelper.GetBaseUrl(this.ManagementUri) }; + GeneratedCode.Identity identity = await this.Execute( + () => edgeletHttpClient.CreateIdentityAsync( + this.Version.Name, + new IdentitySpec + { + ModuleId = name, + ManagedBy = managedBy + }), + $"Create identity for {name}"); + return this.MapFromIdentity(identity); + } + } + + public override async Task UpdateIdentityAsync(string name, string generationId, string managedBy) + { + using (HttpClient httpClient = HttpClientHelper.GetHttpClient(this.ManagementUri)) + { + var edgeletHttpClient = new EdgeletHttpClient(httpClient) { BaseUrl = HttpClientHelper.GetBaseUrl(this.ManagementUri) }; + GeneratedCode.Identity identity = await this.Execute( + () => edgeletHttpClient.UpdateIdentityAsync( + this.Version.Name, + name, + new UpdateIdentity + { + GenerationId = generationId, + ManagedBy = managedBy + }), + $"Update identity for {name} with generation ID {generationId}"); + return this.MapFromIdentity(identity); + } + } + + public override async Task DeleteIdentityAsync(string name) + { + using (HttpClient httpClient = HttpClientHelper.GetHttpClient(this.ManagementUri)) + { + var edgeletHttpClient = new EdgeletHttpClient(httpClient) { BaseUrl = HttpClientHelper.GetBaseUrl(this.ManagementUri) }; + await this.Execute(() => edgeletHttpClient.DeleteIdentityAsync(this.Version.Name, name), $"Delete identity for {name}"); + } + } + + public override async Task> GetIdentities() + { + using (HttpClient httpClient = HttpClientHelper.GetHttpClient(this.ManagementUri)) + { + var edgeletHttpClient = new EdgeletHttpClient(httpClient) { BaseUrl = HttpClientHelper.GetBaseUrl(this.ManagementUri) }; + IdentityList identityList = await this.Execute(() => edgeletHttpClient.ListIdentitiesAsync(this.Version.Name), $"List identities"); + return identityList.Identities.Select(i => this.MapFromIdentity(i)); + } + } + + public override async Task CreateModuleAsync(ModuleSpec moduleSpec) + { + using (HttpClient httpClient = HttpClientHelper.GetHttpClient(this.ManagementUri)) + { + var edgeletHttpClient = new EdgeletHttpClient(httpClient) { BaseUrl = HttpClientHelper.GetBaseUrl(this.ManagementUri) }; + await this.Execute(() => edgeletHttpClient.CreateModuleAsync(this.Version.Name, MapToModuleSpec(moduleSpec)), $"Create module {moduleSpec.Name}"); + } + } + + public override async Task DeleteModuleAsync(string name) + { + using (HttpClient httpClient = HttpClientHelper.GetHttpClient(this.ManagementUri)) + { + var edgeletHttpClient = new EdgeletHttpClient(httpClient) { BaseUrl = HttpClientHelper.GetBaseUrl(this.ManagementUri) }; + await this.Execute(() => edgeletHttpClient.DeleteModuleAsync(this.Version.Name, name), $"Delete module {name}"); + } + } + + public override async Task RestartModuleAsync(string name) + { + using (HttpClient httpClient = HttpClientHelper.GetHttpClient(this.ManagementUri)) + { + var edgeletHttpClient = new EdgeletHttpClient(httpClient) { BaseUrl = HttpClientHelper.GetBaseUrl(this.ManagementUri) }; + await this.Execute( + () => edgeletHttpClient.RestartModuleAsync(this.Version.Name, name), + $"Restart module {name}"); + } + } + + public override async Task GetSystemInfoAsync(CancellationToken cancellationToken) + { + using (HttpClient httpClient = HttpClientHelper.GetHttpClient(this.ManagementUri)) + { + var edgeletHttpClient = new EdgeletHttpClient(httpClient) { BaseUrl = HttpClientHelper.GetBaseUrl(this.ManagementUri) }; + GeneratedCode.SystemInfo systemInfo = await this.Execute( + () => edgeletHttpClient.GetSystemInfoAsync(this.Version.Name, cancellationToken), + "Getting System Info"); + return new SystemInfo(systemInfo.OsType, systemInfo.Architecture, systemInfo.Version, systemInfo.Server_version, systemInfo.Kernel_version, systemInfo.Operating_system, systemInfo.Cpus ?? 0); + } + } + + public override async Task GetSystemResourcesAsync() + { + using (HttpClient httpClient = HttpClientHelper.GetHttpClient(this.ManagementUri)) + { + var edgeletHttpClient = new EdgeletHttpClient(httpClient) { BaseUrl = HttpClientHelper.GetBaseUrl(this.ManagementUri) }; + GeneratedCode.SystemResources systemResources = await this.Execute( + () => edgeletHttpClient.GetSystemResourcesAsync(this.Version.Name), + "Getting System Resources"); + + return new SystemResources(systemResources.Host_uptime, systemResources.Process_uptime, systemResources.Used_cpu, systemResources.Used_ram, systemResources.Total_ram, systemResources.Disks.Select(d => new Disk(d.Name, d.Available_space, d.Total_space, d.File_system, d.File_type)).ToArray(), systemResources.Docker_stats); + } + } + + public override async Task GetSupportBundle(Option since, Option iothubHostname, Option edgeRuntimeOnly, CancellationToken token) + { + using (HttpClient httpClient = HttpClientHelper.GetHttpClient(this.ManagementUri)) + { + var edgeletHttpClient = new EdgeletHttpClient(httpClient) { BaseUrl = HttpClientHelper.GetBaseUrl(this.ManagementUri) }; + FileResponse response = await this.Execute(() => edgeletHttpClient.GetSupportBundleAsync(this.Version.Name, since.OrDefault(), null, iothubHostname.OrDefault(), edgeRuntimeOnly.Map(e => e).OrDefault()), "reprovision the device"); + + return response.Stream; + } + } + + public override async Task> GetModules(CancellationToken cancellationToken) + { + using (HttpClient httpClient = HttpClientHelper.GetHttpClient(this.ManagementUri)) + { + var edgeletHttpClient = new EdgeletHttpClient(httpClient) { BaseUrl = HttpClientHelper.GetBaseUrl(this.ManagementUri) }; + ModuleList moduleList = await this.Execute( + () => edgeletHttpClient.ListModulesAsync(this.Version.Name, cancellationToken), + $"List modules"); + return moduleList.Modules.Select(m => this.GetModuleRuntimeInfo(m)); + } + } + + public override async Task StartModuleAsync(string name) + { + using (HttpClient httpClient = HttpClientHelper.GetHttpClient(this.ManagementUri)) + { + var edgeletHttpClient = new EdgeletHttpClient(httpClient) { BaseUrl = HttpClientHelper.GetBaseUrl(this.ManagementUri) }; + await this.Execute(() => edgeletHttpClient.StartModuleAsync(this.Version.Name, name), $"start module {name}"); + } + } + + public override async Task StopModuleAsync(string name) + { + using (HttpClient httpClient = HttpClientHelper.GetHttpClient(this.ManagementUri)) + { + var edgeletHttpClient = new EdgeletHttpClient(httpClient) { BaseUrl = HttpClientHelper.GetBaseUrl(this.ManagementUri) }; + await this.Execute(() => edgeletHttpClient.StopModuleAsync(this.Version.Name, name), $"stop module {name}"); + } + } + + public override async Task UpdateModuleAsync(ModuleSpec moduleSpec) + { + using (HttpClient httpClient = HttpClientHelper.GetHttpClient(this.ManagementUri)) + { + var edgeletHttpClient = new EdgeletHttpClient(httpClient) { BaseUrl = HttpClientHelper.GetBaseUrl(this.ManagementUri) }; + await this.Execute(() => edgeletHttpClient.UpdateModuleAsync(this.Version.Name, moduleSpec.Name, null, MapToModuleSpec(moduleSpec)), $"update module {moduleSpec.Name}"); + } + } + + public override async Task UpdateAndStartModuleAsync(ModuleSpec moduleSpec) + { + using (HttpClient httpClient = HttpClientHelper.GetHttpClient(this.ManagementUri)) + { + var edgeletHttpClient = new EdgeletHttpClient(httpClient) { BaseUrl = HttpClientHelper.GetBaseUrl(this.ManagementUri) }; + await this.Execute(() => edgeletHttpClient.UpdateModuleAsync(this.Version.Name, moduleSpec.Name, true, MapToModuleSpec(moduleSpec)), $"update and start module {moduleSpec.Name}"); + } + } + + public override async Task PrepareUpdateAsync(ModuleSpec moduleSpec) + { + using (HttpClient httpClient = HttpClientHelper.GetHttpClient(this.ManagementUri)) + { + var edgeletHttpClient = new EdgeletHttpClient(httpClient) { BaseUrl = HttpClientHelper.GetBaseUrl(this.ManagementUri) }; + await this.Execute(() => edgeletHttpClient.PrepareUpdateModuleAsync(this.Version.Name, moduleSpec.Name, MapToModuleSpec(moduleSpec)), $"prepare update for module {moduleSpec.Name}"); + } + } + + public override async Task ReprovisionDeviceAsync() + { + using (HttpClient httpClient = HttpClientHelper.GetHttpClient(this.ManagementUri)) + { + var edgeletHttpClient = new EdgeletHttpClient(httpClient) { BaseUrl = HttpClientHelper.GetBaseUrl(this.ManagementUri) }; + await this.Execute(() => edgeletHttpClient.ReprovisionDeviceAsync(this.Version.Name), "reprovision the device"); + } + } + + protected override void HandleException(Exception exception, string operation) + { + switch (exception) + { + case SwaggerException errorResponseException: + throw new EdgeletCommunicationException($"Error calling {operation}: {errorResponseException.Result?.Message ?? string.Empty}", errorResponseException.StatusCode); + + case SwaggerException swaggerException: + if (swaggerException.StatusCode < 400) + { + return; + } + else + { + throw new EdgeletCommunicationException($"Error calling {operation}: {swaggerException.Response ?? string.Empty}", swaggerException.StatusCode); + } + + default: + ExceptionDispatchInfo.Capture(exception).Throw(); + break; + } + } + + static GeneratedCode.ModuleSpec MapToModuleSpec(ModuleSpec moduleSpec) + { + return new GeneratedCode.ModuleSpec() + { + Name = moduleSpec.Name, + Type = moduleSpec.Type, + ImagePullPolicy = ToGeneratedCodePullPolicy(moduleSpec.ImagePullPolicy), + Config = new Config() + { + Env = new ObservableCollection( + moduleSpec.EnvironmentVariables.Select( + e => new EnvVar() + { + Key = e.Key, + Value = e.Value + }).ToList()), + Settings = moduleSpec.Settings + } + }; + } + + internal static GeneratedCode.ModuleSpecImagePullPolicy ToGeneratedCodePullPolicy(Core.ImagePullPolicy imagePullPolicy) + { + GeneratedCode.ModuleSpecImagePullPolicy resultantPullPolicy; + switch (imagePullPolicy) + { + case Core.ImagePullPolicy.OnCreate: + resultantPullPolicy = GeneratedCode.ModuleSpecImagePullPolicy.OnCreate; + break; + case Core.ImagePullPolicy.Never: + resultantPullPolicy = GeneratedCode.ModuleSpecImagePullPolicy.Never; + break; + default: + throw new InvalidOperationException("Translation of this image pull policy type is not configured."); + } + + return resultantPullPolicy; + } + + Identity MapFromIdentity(GeneratedCode.Identity identity) + { + return new Identity(identity.ModuleId, identity.GenerationId, identity.ManagedBy); + } + + ModuleRuntimeInfo GetModuleRuntimeInfo(ModuleDetails moduleDetails) + { + ExitStatus exitStatus = moduleDetails.Status.ExitStatus; + if (exitStatus == null || !long.TryParse(exitStatus.StatusCode, out long exitCode)) + { + exitCode = 0; + } + + Option exitTime = exitStatus == null ? Option.None() : Option.Some(exitStatus.ExitTime.DateTime); + Option startTime = !moduleDetails.Status.StartTime.HasValue ? Option.None() : Option.Some(moduleDetails.Status.StartTime.Value.DateTime); + + if (!Enum.TryParse(moduleDetails.Status.RuntimeStatus.Status, true, out ModuleStatus status)) + { + status = ModuleStatus.Unknown; + } + + if (!(moduleDetails.Config.Settings is JObject jobject)) + { + throw new InvalidOperationException($"Module config is of type {moduleDetails.Config.Settings.GetType()}. Expected type JObject"); + } + + var config = jobject.ToObject(); + + var moduleRuntimeInfo = new ModuleRuntimeInfo( + moduleDetails.Name, + moduleDetails.Type, + status, + moduleDetails.Status.RuntimeStatus.Description, + exitCode, + startTime, + exitTime, + config); + return moduleRuntimeInfo; + } + + class ErrorDetectionStrategy : ITransientErrorDetectionStrategy + { + public bool IsTransient(Exception ex) => ex is SwaggerException se + && se.StatusCode >= 500; + } + } +} diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2020_07_07/generatedCode/EdgeletHttpClient.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2020_07_07/generatedCode/EdgeletHttpClient.cs new file mode 100644 index 00000000000..17be201014d --- /dev/null +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/version_2020_07_07/generatedCode/EdgeletHttpClient.cs @@ -0,0 +1,1991 @@ +//---------------------- +// +// Generated using the NSwag toolchain v13.6.2.0 (NJsonSchema v10.1.23.0 (Newtonsoft.Json v11.0.0.0)) (http://NSwag.org) +// +//---------------------- + +// Note: Code manually changed to replace System.Uri.EscapeDataString with System.Net.WebUtility.UrlEncode +// Note: GetSupportBundleAsync manualy modified to correctly return stream + +namespace Microsoft.Azure.Devices.Edge.Agent.Edgelet.Version_2020_07_07.GeneratedCode +{ + using System = global::System; +#pragma warning disable // Disable all warnings + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.6.2.0 (NJsonSchema v10.1.23.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class EdgeletHttpClient + { + private string _baseUrl = "http://"; + private System.Net.Http.HttpClient _httpClient; + private System.Lazy _settings; + + public EdgeletHttpClient(System.Net.Http.HttpClient httpClient) + { + _httpClient = httpClient; + _settings = new System.Lazy(CreateSerializerSettings); + } + + private Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() + { + var settings = new Newtonsoft.Json.JsonSerializerSettings(); + UpdateJsonSerializerSettings(settings); + return settings; + } + + public string BaseUrl + { + get { return _baseUrl; } + set { _baseUrl = value; } + } + + protected Newtonsoft.Json.JsonSerializerSettings JsonSerializerSettings { get { return _settings.Value; } } + + partial void UpdateJsonSerializerSettings(Newtonsoft.Json.JsonSerializerSettings settings); + partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url); + partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder); + partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response); + + /// List modules. + /// The version of the API. + /// Ok + /// A server side error occurred. + public System.Threading.Tasks.Task ListModulesAsync(string api_version) + { + return ListModulesAsync(api_version, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// List modules. + /// The version of the API. + /// Ok + /// A server side error occurred. + public async System.Threading.Tasks.Task ListModulesAsync(string api_version, System.Threading.CancellationToken cancellationToken) + { + if (api_version == null) + throw new System.ArgumentNullException("api_version"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/modules?"); + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("api-version") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(api_version, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + urlBuilder_.Length--; + + var client_ = _httpClient; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + PrepareRequest(client_, request_, urlBuilder_); + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = ((int)response_.StatusCode).ToString(); + if (status_ == "200") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + return objectResponse_.Object; + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (response_ != null) + response_.Dispose(); + } + } + } + finally + { + } + } + + /// Create module. + /// The version of the API. + /// Created + /// A server side error occurred. + public System.Threading.Tasks.Task CreateModuleAsync(string api_version, ModuleSpec module) + { + return CreateModuleAsync(api_version, module, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Create module. + /// The version of the API. + /// Created + /// A server side error occurred. + public async System.Threading.Tasks.Task CreateModuleAsync(string api_version, ModuleSpec module, System.Threading.CancellationToken cancellationToken) + { + if (api_version == null) + throw new System.ArgumentNullException("api_version"); + + if (module == null) + throw new System.ArgumentNullException("module"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/modules?"); + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("api-version") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(api_version, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + urlBuilder_.Length--; + + var client_ = _httpClient; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var content_ = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(module, _settings.Value)); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + PrepareRequest(client_, request_, urlBuilder_); + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = ((int)response_.StatusCode).ToString(); + if (status_ == "201") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + return objectResponse_.Object; + } + else + if (status_ == "409") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Conflict. Returned if module already exists.", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (response_ != null) + response_.Dispose(); + } + } + } + finally + { + } + } + + /// Get a module's status. + /// The version of the API. + /// The name of the module to get. (urlencoded) + /// Ok + /// A server side error occurred. + public System.Threading.Tasks.Task GetModuleAsync(string api_version, string name) + { + return GetModuleAsync(api_version, name, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Get a module's status. + /// The version of the API. + /// The name of the module to get. (urlencoded) + /// Ok + /// A server side error occurred. + public async System.Threading.Tasks.Task GetModuleAsync(string api_version, string name, System.Threading.CancellationToken cancellationToken) + { + if (name == null) + throw new System.ArgumentNullException("name"); + + if (api_version == null) + throw new System.ArgumentNullException("api_version"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/modules/{name}?"); + urlBuilder_.Replace("{name}", System.Net.WebUtility.UrlEncode(ConvertToString(name, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("api-version") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(api_version, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + urlBuilder_.Length--; + + var client_ = _httpClient; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + PrepareRequest(client_, request_, urlBuilder_); + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = ((int)response_.StatusCode).ToString(); + if (status_ == "200") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + return objectResponse_.Object; + } + else + if (status_ == "404") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Not Found", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (response_ != null) + response_.Dispose(); + } + } + } + finally + { + } + } + + /// Update a module. + /// The version of the API. + /// The name of the module to update. (urlencoded) + /// Flag indicating whether module should be started after updating. + /// Ok + /// A server side error occurred. + public System.Threading.Tasks.Task UpdateModuleAsync(string api_version, string name, bool? start, ModuleSpec module) + { + return UpdateModuleAsync(api_version, name, start, module, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Update a module. + /// The version of the API. + /// The name of the module to update. (urlencoded) + /// Flag indicating whether module should be started after updating. + /// Ok + /// A server side error occurred. + public async System.Threading.Tasks.Task UpdateModuleAsync(string api_version, string name, bool? start, ModuleSpec module, System.Threading.CancellationToken cancellationToken) + { + if (name == null) + throw new System.ArgumentNullException("name"); + + if (api_version == null) + throw new System.ArgumentNullException("api_version"); + + if (module == null) + throw new System.ArgumentNullException("module"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/modules/{name}?"); + urlBuilder_.Replace("{name}", System.Net.WebUtility.UrlEncode(ConvertToString(name, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("api-version") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(api_version, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + if (start != null) + { + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("start") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(start, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + } + urlBuilder_.Length--; + + var client_ = _httpClient; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var content_ = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(module, _settings.Value)); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("PUT"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + PrepareRequest(client_, request_, urlBuilder_); + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = ((int)response_.StatusCode).ToString(); + if (status_ == "200") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + return objectResponse_.Object; + } + else + if (status_ == "404") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Not Found", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (response_ != null) + response_.Dispose(); + } + } + } + finally + { + } + } + + /// Delete a module. + /// The version of the API. + /// The name of the module to delete. (urlencoded) + /// No Content + /// A server side error occurred. + public System.Threading.Tasks.Task DeleteModuleAsync(string api_version, string name) + { + return DeleteModuleAsync(api_version, name, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Delete a module. + /// The version of the API. + /// The name of the module to delete. (urlencoded) + /// No Content + /// A server side error occurred. + public async System.Threading.Tasks.Task DeleteModuleAsync(string api_version, string name, System.Threading.CancellationToken cancellationToken) + { + if (name == null) + throw new System.ArgumentNullException("name"); + + if (api_version == null) + throw new System.ArgumentNullException("api_version"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/modules/{name}?"); + urlBuilder_.Replace("{name}", System.Net.WebUtility.UrlEncode(ConvertToString(name, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("api-version") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(api_version, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + urlBuilder_.Length--; + + var client_ = _httpClient; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("DELETE"); + + PrepareRequest(client_, request_, urlBuilder_); + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = ((int)response_.StatusCode).ToString(); + if (status_ == "204") + { + return; + } + else + if (status_ == "404") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Not Found", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (response_ != null) + response_.Dispose(); + } + } + } + finally + { + } + } + + /// Prepare to update a module. + /// The version of the API. + /// The name of the module to update. (urlencoded) + /// No Content + /// A server side error occurred. + public System.Threading.Tasks.Task PrepareUpdateModuleAsync(string api_version, string name, ModuleSpec module) + { + return PrepareUpdateModuleAsync(api_version, name, module, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Prepare to update a module. + /// The version of the API. + /// The name of the module to update. (urlencoded) + /// No Content + /// A server side error occurred. + public async System.Threading.Tasks.Task PrepareUpdateModuleAsync(string api_version, string name, ModuleSpec module, System.Threading.CancellationToken cancellationToken) + { + if (name == null) + throw new System.ArgumentNullException("name"); + + if (api_version == null) + throw new System.ArgumentNullException("api_version"); + + if (module == null) + throw new System.ArgumentNullException("module"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/modules/{name}/prepareupdate?"); + urlBuilder_.Replace("{name}", System.Net.WebUtility.UrlEncode(ConvertToString(name, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("api-version") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(api_version, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + urlBuilder_.Length--; + + var client_ = _httpClient; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var content_ = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(module, _settings.Value)); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + + PrepareRequest(client_, request_, urlBuilder_); + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = ((int)response_.StatusCode).ToString(); + if (status_ == "204") + { + return; + } + else + if (status_ == "404") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Not Found", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (response_ != null) + response_.Dispose(); + } + } + } + finally + { + } + } + + /// Start a module. + /// The version of the API. + /// The name of the module to start. (urlencoded) + /// No Content + /// A server side error occurred. + public System.Threading.Tasks.Task StartModuleAsync(string api_version, string name) + { + return StartModuleAsync(api_version, name, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Start a module. + /// The version of the API. + /// The name of the module to start. (urlencoded) + /// No Content + /// A server side error occurred. + public async System.Threading.Tasks.Task StartModuleAsync(string api_version, string name, System.Threading.CancellationToken cancellationToken) + { + if (name == null) + throw new System.ArgumentNullException("name"); + + if (api_version == null) + throw new System.ArgumentNullException("api_version"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/modules/{name}/start?"); + urlBuilder_.Replace("{name}", System.Net.WebUtility.UrlEncode(ConvertToString(name, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("api-version") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(api_version, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + urlBuilder_.Length--; + + var client_ = _httpClient; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + + PrepareRequest(client_, request_, urlBuilder_); + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = ((int)response_.StatusCode).ToString(); + if (status_ == "204") + { + return; + } + else + if (status_ == "304") + { + string responseText_ = (response_.Content == null) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new SwaggerException("Not Modified", (int)response_.StatusCode, responseText_, headers_, null); + } + else + if (status_ == "404") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Not Found", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (response_ != null) + response_.Dispose(); + } + } + } + finally + { + } + } + + /// Stop a module. + /// The version of the API. + /// The name of the module to stop. (urlencoded) + /// No Content + /// A server side error occurred. + public System.Threading.Tasks.Task StopModuleAsync(string api_version, string name) + { + return StopModuleAsync(api_version, name, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Stop a module. + /// The version of the API. + /// The name of the module to stop. (urlencoded) + /// No Content + /// A server side error occurred. + public async System.Threading.Tasks.Task StopModuleAsync(string api_version, string name, System.Threading.CancellationToken cancellationToken) + { + if (name == null) + throw new System.ArgumentNullException("name"); + + if (api_version == null) + throw new System.ArgumentNullException("api_version"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/modules/{name}/stop?"); + urlBuilder_.Replace("{name}", System.Net.WebUtility.UrlEncode(ConvertToString(name, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("api-version") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(api_version, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + urlBuilder_.Length--; + + var client_ = _httpClient; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + + PrepareRequest(client_, request_, urlBuilder_); + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = ((int)response_.StatusCode).ToString(); + if (status_ == "204") + { + return; + } + else + if (status_ == "304") + { + string responseText_ = (response_.Content == null) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new SwaggerException("Not Modified", (int)response_.StatusCode, responseText_, headers_, null); + } + else + if (status_ == "404") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Not Found", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (response_ != null) + response_.Dispose(); + } + } + } + finally + { + } + } + + /// Restart a module. + /// The version of the API. + /// The name of the module to restart. (urlencoded) + /// No Content + /// A server side error occurred. + public System.Threading.Tasks.Task RestartModuleAsync(string api_version, string name) + { + return RestartModuleAsync(api_version, name, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Restart a module. + /// The version of the API. + /// The name of the module to restart. (urlencoded) + /// No Content + /// A server side error occurred. + public async System.Threading.Tasks.Task RestartModuleAsync(string api_version, string name, System.Threading.CancellationToken cancellationToken) + { + if (name == null) + throw new System.ArgumentNullException("name"); + + if (api_version == null) + throw new System.ArgumentNullException("api_version"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/modules/{name}/restart?"); + urlBuilder_.Replace("{name}", System.Net.WebUtility.UrlEncode(ConvertToString(name, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("api-version") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(api_version, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + urlBuilder_.Length--; + + var client_ = _httpClient; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + + PrepareRequest(client_, request_, urlBuilder_); + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = ((int)response_.StatusCode).ToString(); + if (status_ == "204") + { + return; + } + else + if (status_ == "304") + { + string responseText_ = (response_.Content == null) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new SwaggerException("Not Modified", (int)response_.StatusCode, responseText_, headers_, null); + } + else + if (status_ == "404") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Not Found", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (response_ != null) + response_.Dispose(); + } + } + } + finally + { + } + } + + /// Get module logs. + /// The version of the API. + /// The name of the module to obtain logs for. (urlencoded) + /// Return the logs as a stream. + /// Only return this number of lines from the end of the logs. + /// Only return logs since this time, as a duration (1 day, 1d, 90m, 2 days 3 hours 2 minutes), rfc3339 timestamp, or UNIX timestamp. + /// Logs returned as a string in response body + /// A server side error occurred. + public System.Threading.Tasks.Task ModuleLogsAsync(string api_version, string name, bool? follow, string tail, string since) + { + return ModuleLogsAsync(api_version, name, follow, tail, since, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Get module logs. + /// The version of the API. + /// The name of the module to obtain logs for. (urlencoded) + /// Return the logs as a stream. + /// Only return this number of lines from the end of the logs. + /// Only return logs since this time, as a duration (1 day, 1d, 90m, 2 days 3 hours 2 minutes), rfc3339 timestamp, or UNIX timestamp. + /// Logs returned as a string in response body + /// A server side error occurred. + public async System.Threading.Tasks.Task ModuleLogsAsync(string api_version, string name, bool? follow, string tail, string since, System.Threading.CancellationToken cancellationToken) + { + if (name == null) + throw new System.ArgumentNullException("name"); + + if (api_version == null) + throw new System.ArgumentNullException("api_version"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/modules/{name}/logs?"); + urlBuilder_.Replace("{name}", System.Net.WebUtility.UrlEncode(ConvertToString(name, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("api-version") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(api_version, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + if (follow != null) + { + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("follow") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(follow, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + } + if (tail != null) + { + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("tail") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(tail, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + } + if (since != null) + { + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("since") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(since, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + } + urlBuilder_.Length--; + + var client_ = _httpClient; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + + PrepareRequest(client_, request_, urlBuilder_); + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = ((int)response_.StatusCode).ToString(); + if (status_ == "101") + { + string responseText_ = (response_.Content == null) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new SwaggerException("Logs returned as a stream", (int)response_.StatusCode, responseText_, headers_, null); + } + else + if (status_ == "200") + { + return; + } + else + if (status_ == "404") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Not Found", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (response_ != null) + response_.Dispose(); + } + } + } + finally + { + } + } + + /// List identities. + /// The version of the API. + /// Ok + /// A server side error occurred. + public System.Threading.Tasks.Task ListIdentitiesAsync(string api_version) + { + return ListIdentitiesAsync(api_version, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// List identities. + /// The version of the API. + /// Ok + /// A server side error occurred. + public async System.Threading.Tasks.Task ListIdentitiesAsync(string api_version, System.Threading.CancellationToken cancellationToken) + { + if (api_version == null) + throw new System.ArgumentNullException("api_version"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/identities/?"); + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("api-version") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(api_version, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + urlBuilder_.Length--; + + var client_ = _httpClient; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + PrepareRequest(client_, request_, urlBuilder_); + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = ((int)response_.StatusCode).ToString(); + if (status_ == "200") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + return objectResponse_.Object; + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (response_ != null) + response_.Dispose(); + } + } + } + finally + { + } + } + + /// Create an identity. + /// The version of the API. + /// Created + /// A server side error occurred. + public System.Threading.Tasks.Task CreateIdentityAsync(string api_version, IdentitySpec identity) + { + return CreateIdentityAsync(api_version, identity, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Create an identity. + /// The version of the API. + /// Created + /// A server side error occurred. + public async System.Threading.Tasks.Task CreateIdentityAsync(string api_version, IdentitySpec identity, System.Threading.CancellationToken cancellationToken) + { + if (api_version == null) + throw new System.ArgumentNullException("api_version"); + + if (identity == null) + throw new System.ArgumentNullException("identity"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/identities/?"); + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("api-version") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(api_version, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + urlBuilder_.Length--; + + var client_ = _httpClient; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var content_ = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(identity, _settings.Value)); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + PrepareRequest(client_, request_, urlBuilder_); + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = ((int)response_.StatusCode).ToString(); + if (status_ == "200") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + return objectResponse_.Object; + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (response_ != null) + response_.Dispose(); + } + } + } + finally + { + } + } + + /// Update an identity. + /// The version of the API. + /// The name of the identity to update. (urlencoded) + /// Updated + /// A server side error occurred. + public System.Threading.Tasks.Task UpdateIdentityAsync(string api_version, string name, UpdateIdentity updateinfo) + { + return UpdateIdentityAsync(api_version, name, updateinfo, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Update an identity. + /// The version of the API. + /// The name of the identity to update. (urlencoded) + /// Updated + /// A server side error occurred. + public async System.Threading.Tasks.Task UpdateIdentityAsync(string api_version, string name, UpdateIdentity updateinfo, System.Threading.CancellationToken cancellationToken) + { + if (name == null) + throw new System.ArgumentNullException("name"); + + if (api_version == null) + throw new System.ArgumentNullException("api_version"); + + if (updateinfo == null) + throw new System.ArgumentNullException("updateinfo"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/identities/{name}?"); + urlBuilder_.Replace("{name}", System.Net.WebUtility.UrlEncode(ConvertToString(name, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("api-version") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(api_version, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + urlBuilder_.Length--; + + var client_ = _httpClient; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var content_ = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(updateinfo, _settings.Value)); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("PUT"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + PrepareRequest(client_, request_, urlBuilder_); + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = ((int)response_.StatusCode).ToString(); + if (status_ == "200") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + return objectResponse_.Object; + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (response_ != null) + response_.Dispose(); + } + } + } + finally + { + } + } + + /// Delete an identity. + /// The version of the API. + /// The name of the identity to delete. (urlencoded) + /// Ok + /// A server side error occurred. + public System.Threading.Tasks.Task DeleteIdentityAsync(string api_version, string name) + { + return DeleteIdentityAsync(api_version, name, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Delete an identity. + /// The version of the API. + /// The name of the identity to delete. (urlencoded) + /// Ok + /// A server side error occurred. + public async System.Threading.Tasks.Task DeleteIdentityAsync(string api_version, string name, System.Threading.CancellationToken cancellationToken) + { + if (name == null) + throw new System.ArgumentNullException("name"); + + if (api_version == null) + throw new System.ArgumentNullException("api_version"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/identities/{name}?"); + urlBuilder_.Replace("{name}", System.Net.WebUtility.UrlEncode(ConvertToString(name, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("api-version") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(api_version, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + urlBuilder_.Length--; + + var client_ = _httpClient; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("DELETE"); + + PrepareRequest(client_, request_, urlBuilder_); + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = ((int)response_.StatusCode).ToString(); + if (status_ == "204") + { + return; + } + else + if (status_ == "404") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Not Found", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (response_ != null) + response_.Dispose(); + } + } + } + finally + { + } + } + + /// Return host system information. + /// The version of the API. + /// Ok + /// A server side error occurred. + public System.Threading.Tasks.Task GetSystemInfoAsync(string api_version) + { + return GetSystemInfoAsync(api_version, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Return host system information. + /// The version of the API. + /// Ok + /// A server side error occurred. + public async System.Threading.Tasks.Task GetSystemInfoAsync(string api_version, System.Threading.CancellationToken cancellationToken) + { + if (api_version == null) + throw new System.ArgumentNullException("api_version"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/systeminfo?"); + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("api-version") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(api_version, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + urlBuilder_.Length--; + + var client_ = _httpClient; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + PrepareRequest(client_, request_, urlBuilder_); + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = ((int)response_.StatusCode).ToString(); + if (status_ == "200") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + return objectResponse_.Object; + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (response_ != null) + response_.Dispose(); + } + } + } + finally + { + } + } + + /// Return host resource usage (DISK, RAM, CPU). + /// The version of the API. + /// Ok + /// A server side error occurred. + public System.Threading.Tasks.Task GetSystemResourcesAsync(string api_version) + { + return GetSystemResourcesAsync(api_version, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Return host resource usage (DISK, RAM, CPU). + /// The version of the API. + /// Ok + /// A server side error occurred. + public async System.Threading.Tasks.Task GetSystemResourcesAsync(string api_version, System.Threading.CancellationToken cancellationToken) + { + if (api_version == null) + throw new System.ArgumentNullException("api_version"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/systeminfo/resources?"); + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("api-version") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(api_version, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + urlBuilder_.Length--; + + var client_ = _httpClient; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + PrepareRequest(client_, request_, urlBuilder_); + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = ((int)response_.StatusCode).ToString(); + if (status_ == "200") + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + return objectResponse_.Object; + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (response_ != null) + response_.Dispose(); + } + } + } + finally + { + } + } + + /// Return zip of support bundle. + /// The version of the API. + /// Duration to get logs from. Can be relative (1d, 10m, 1h30m etc.) or absolute (unix timestamp or rfc 3339) + /// Path to the management host + /// Hub to use when calling iotedge check + /// Exclude customer module logs + /// Ok + /// A server side error occurred. + public System.Threading.Tasks.Task GetSupportBundleAsync(string api_version, string since, string host, string iothub_hostname, bool? edge_runtime_only) + { + return GetSupportBundleAsync(api_version, since, host, iothub_hostname, edge_runtime_only, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Return zip of support bundle. + /// The version of the API. + /// Duration to get logs from. Can be relative (1d, 10m, 1h30m etc.) or absolute (unix timestamp or rfc 3339) + /// Path to the management host + /// Hub to use when calling iotedge check + /// Exclude customer module logs + /// Ok + /// A server side error occurred. + public async System.Threading.Tasks.Task GetSupportBundleAsync(string api_version, string since, string host, string iothub_hostname, bool? edge_runtime_only, System.Threading.CancellationToken cancellationToken) + { + if (api_version == null) + throw new System.ArgumentNullException("api_version"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/systeminfo/supportbundle?"); + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("api-version") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(api_version, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + if (since != null) + { + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("since") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(since, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + } + if (host != null) + { + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("host") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(host, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + } + if (iothub_hostname != null) + { + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("iothub_hostname") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(iothub_hostname, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + } + if (edge_runtime_only != null) + { + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("edge_runtime_only") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(edge_runtime_only, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + } + urlBuilder_.Length--; + + var client_ = _httpClient; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/zip")); + + PrepareRequest(client_, request_, urlBuilder_); + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + + return new FileResponse((int)response_.StatusCode, null, await response_.Content.ReadAsStreamAsync(), client_, response_); + } + } + finally + { + } + } + + /// Trigger a device reprovisioning flow. + /// The version of the API. + /// Ok + /// A server side error occurred. + public System.Threading.Tasks.Task ReprovisionDeviceAsync(string api_version) + { + return ReprovisionDeviceAsync(api_version, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Trigger a device reprovisioning flow. + /// The version of the API. + /// Ok + /// A server side error occurred. + public async System.Threading.Tasks.Task ReprovisionDeviceAsync(string api_version, System.Threading.CancellationToken cancellationToken) + { + if (api_version == null) + throw new System.ArgumentNullException("api_version"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/device/reprovision?"); + urlBuilder_.Append(System.Net.WebUtility.UrlEncode("api-version") + "=").Append(System.Net.WebUtility.UrlEncode(ConvertToString(api_version, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + urlBuilder_.Length--; + + var client_ = _httpClient; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + + PrepareRequest(client_, request_, urlBuilder_); + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = ((int)response_.StatusCode).ToString(); + if (status_ == "200") + { + return; + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); + throw new SwaggerException("Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (response_ != null) + response_.Dispose(); + } + } + } + finally + { + } + } + + protected struct ObjectResponseResult + { + public ObjectResponseResult(T responseObject, string responseText) + { + this.Object = responseObject; + this.Text = responseText; + } + + public T Object { get; } + + public string Text { get; } + } + + public bool ReadResponseAsString { get; set; } + + protected virtual async System.Threading.Tasks.Task> ReadObjectResponseAsync(System.Net.Http.HttpResponseMessage response, System.Collections.Generic.IReadOnlyDictionary> headers) + { + if (response == null || response.Content == null) + { + return new ObjectResponseResult(default(T), string.Empty); + } + + if (ReadResponseAsString) + { + var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + try + { + var typedBody = Newtonsoft.Json.JsonConvert.DeserializeObject(responseText, JsonSerializerSettings); + return new ObjectResponseResult(typedBody, responseText); + } + catch (Newtonsoft.Json.JsonException exception) + { + var message = "Could not deserialize the response body string as " + typeof(T).FullName + "."; + throw new SwaggerException(message, (int)response.StatusCode, responseText, headers, exception); + } + } + else + { + try + { + using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + using (var streamReader = new System.IO.StreamReader(responseStream)) + using (var jsonTextReader = new Newtonsoft.Json.JsonTextReader(streamReader)) + { + var serializer = Newtonsoft.Json.JsonSerializer.Create(JsonSerializerSettings); + var typedBody = serializer.Deserialize(jsonTextReader); + return new ObjectResponseResult(typedBody, string.Empty); + } + } + catch (Newtonsoft.Json.JsonException exception) + { + var message = "Could not deserialize the response body stream as " + typeof(T).FullName + "."; + throw new SwaggerException(message, (int)response.StatusCode, string.Empty, headers, exception); + } + } + } + + private string ConvertToString(object value, System.Globalization.CultureInfo cultureInfo) + { + if (value is System.Enum) + { + var name = System.Enum.GetName(value.GetType(), value); + if (name != null) + { + var field = System.Reflection.IntrospectionExtensions.GetTypeInfo(value.GetType()).GetDeclaredField(name); + if (field != null) + { + var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(field, typeof(System.Runtime.Serialization.EnumMemberAttribute)) + as System.Runtime.Serialization.EnumMemberAttribute; + if (attribute != null) + { + return attribute.Value != null ? attribute.Value : name; + } + } + + return System.Convert.ToString(System.Convert.ChangeType(value, System.Enum.GetUnderlyingType(value.GetType()), cultureInfo)); + } + } + else if (value is bool) + { + return System.Convert.ToString(value, cultureInfo)?.ToLowerInvariant(); + } + else if (value is byte[]) + { + return System.Convert.ToBase64String((byte[])value); + } + else if (value != null && value.GetType().IsArray) + { + var array = System.Linq.Enumerable.OfType((System.Array)value); + return string.Join(",", System.Linq.Enumerable.Select(array, o => ConvertToString(o, cultureInfo))); + } + + return System.Convert.ToString(value, cultureInfo); + } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.23.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class ModuleList + { + [Newtonsoft.Json.JsonProperty("modules", Required = Newtonsoft.Json.Required.Always)] + public System.Collections.Generic.ICollection Modules { get; set; } = new System.Collections.ObjectModel.Collection(); + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.23.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class ModuleDetails + { + /// System generated unique identitier. + [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.Always)] + public string Id { get; set; } + + /// The name of the module. + [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Always)] + public string Name { get; set; } + + /// The type of a module. + [Newtonsoft.Json.JsonProperty("type", Required = Newtonsoft.Json.Required.Always)] + public string Type { get; set; } + + [Newtonsoft.Json.JsonProperty("config", Required = Newtonsoft.Json.Required.Always)] + public Config Config { get; set; } = new Config(); + + [Newtonsoft.Json.JsonProperty("status", Required = Newtonsoft.Json.Required.Always)] + public Status Status { get; set; } = new Status(); + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.23.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class ModuleSpec + { + /// The name of a the module. + [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Always)] + public string Name { get; set; } + + [Newtonsoft.Json.JsonProperty("type", Required = Newtonsoft.Json.Required.Always)] + public string Type { get; set; } + + [Newtonsoft.Json.JsonProperty("imagePullPolicy", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public ModuleSpecImagePullPolicy? ImagePullPolicy { get; set; } + + [Newtonsoft.Json.JsonProperty("config", Required = Newtonsoft.Json.Required.Always)] + public Config Config { get; set; } = new Config(); + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.23.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class Config + { + [Newtonsoft.Json.JsonProperty("settings", Required = Newtonsoft.Json.Required.Always)] + public object Settings { get; set; } = new object(); + + [Newtonsoft.Json.JsonProperty("env", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public System.Collections.Generic.ICollection Env { get; set; } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.23.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class Status + { + [Newtonsoft.Json.JsonProperty("startTime", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public System.DateTimeOffset? StartTime { get; set; } + + [Newtonsoft.Json.JsonProperty("exitStatus", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public ExitStatus ExitStatus { get; set; } + + [Newtonsoft.Json.JsonProperty("runtimeStatus", Required = Newtonsoft.Json.Required.Always)] + public RuntimeStatus RuntimeStatus { get; set; } = new RuntimeStatus(); + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.23.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class EnvVar + { + [Newtonsoft.Json.JsonProperty("key", Required = Newtonsoft.Json.Required.Always)] + public string Key { get; set; } + + [Newtonsoft.Json.JsonProperty("value", Required = Newtonsoft.Json.Required.Always)] + public string Value { get; set; } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.23.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class ExitStatus + { + [Newtonsoft.Json.JsonProperty("exitTime", Required = Newtonsoft.Json.Required.Always)] + public System.DateTimeOffset ExitTime { get; set; } + + [Newtonsoft.Json.JsonProperty("statusCode", Required = Newtonsoft.Json.Required.Always)] + public string StatusCode { get; set; } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.23.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class RuntimeStatus + { + [Newtonsoft.Json.JsonProperty("status", Required = Newtonsoft.Json.Required.Always)] + public string Status { get; set; } + + [Newtonsoft.Json.JsonProperty("description", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Description { get; set; } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.23.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class SystemInfo + { + [Newtonsoft.Json.JsonProperty("osType", Required = Newtonsoft.Json.Required.Always)] + public string OsType { get; set; } + + [Newtonsoft.Json.JsonProperty("architecture", Required = Newtonsoft.Json.Required.Always)] + public string Architecture { get; set; } + + [Newtonsoft.Json.JsonProperty("version", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Version { get; set; } + + [Newtonsoft.Json.JsonProperty("server_version", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Server_version { get; set; } + + [Newtonsoft.Json.JsonProperty("kernel_version", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Kernel_version { get; set; } + + [Newtonsoft.Json.JsonProperty("operating_system", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Operating_system { get; set; } + + [Newtonsoft.Json.JsonProperty("cpus", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public int? Cpus { get; set; } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.23.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class SystemResources + { + [Newtonsoft.Json.JsonProperty("host_uptime", Required = Newtonsoft.Json.Required.Always)] + public long Host_uptime { get; set; } + + [Newtonsoft.Json.JsonProperty("process_uptime", Required = Newtonsoft.Json.Required.Always)] + public long Process_uptime { get; set; } + + [Newtonsoft.Json.JsonProperty("used_cpu", Required = Newtonsoft.Json.Required.Always)] + public double Used_cpu { get; set; } + + [Newtonsoft.Json.JsonProperty("used_ram", Required = Newtonsoft.Json.Required.Always)] + public long Used_ram { get; set; } + + [Newtonsoft.Json.JsonProperty("total_ram", Required = Newtonsoft.Json.Required.Always)] + public long Total_ram { get; set; } + + [Newtonsoft.Json.JsonProperty("disks", Required = Newtonsoft.Json.Required.Always)] + public System.Collections.Generic.ICollection Disks { get; set; } = new System.Collections.ObjectModel.Collection(); + + [Newtonsoft.Json.JsonProperty("docker_stats", Required = Newtonsoft.Json.Required.Always)] + public string Docker_stats { get; set; } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.23.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class Disk + { + [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Always)] + public string Name { get; set; } + + [Newtonsoft.Json.JsonProperty("available_space", Required = Newtonsoft.Json.Required.Always)] + public long Available_space { get; set; } + + [Newtonsoft.Json.JsonProperty("total_space", Required = Newtonsoft.Json.Required.Always)] + public long Total_space { get; set; } + + [Newtonsoft.Json.JsonProperty("file_system", Required = Newtonsoft.Json.Required.Always)] + public string File_system { get; set; } + + [Newtonsoft.Json.JsonProperty("file_type", Required = Newtonsoft.Json.Required.Always)] + public string File_type { get; set; } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.23.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class IdentityList + { + [Newtonsoft.Json.JsonProperty("identities", Required = Newtonsoft.Json.Required.Always)] + public System.Collections.Generic.ICollection Identities { get; set; } = new System.Collections.ObjectModel.Collection(); + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.23.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class IdentitySpec + { + [Newtonsoft.Json.JsonProperty("moduleId", Required = Newtonsoft.Json.Required.Always)] + public string ModuleId { get; set; } + + [Newtonsoft.Json.JsonProperty("managedBy", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string ManagedBy { get; set; } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.23.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class UpdateIdentity + { + [Newtonsoft.Json.JsonProperty("generationId", Required = Newtonsoft.Json.Required.Always)] + public string GenerationId { get; set; } + + [Newtonsoft.Json.JsonProperty("managedBy", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string ManagedBy { get; set; } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.23.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class Identity + { + [Newtonsoft.Json.JsonProperty("moduleId", Required = Newtonsoft.Json.Required.Always)] + public string ModuleId { get; set; } + + [Newtonsoft.Json.JsonProperty("managedBy", Required = Newtonsoft.Json.Required.Always)] + public string ManagedBy { get; set; } + + [Newtonsoft.Json.JsonProperty("generationId", Required = Newtonsoft.Json.Required.Always)] + public string GenerationId { get; set; } + + [Newtonsoft.Json.JsonProperty("authType", Required = Newtonsoft.Json.Required.Always)] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public IdentityAuthType AuthType { get; set; } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.23.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class ErrorResponse + { + [Newtonsoft.Json.JsonProperty("message", Required = Newtonsoft.Json.Required.Always)] + public string Message { get; set; } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.23.0 (Newtonsoft.Json v11.0.0.0)")] + public enum ModuleSpecImagePullPolicy + { + [System.Runtime.Serialization.EnumMember(Value = @"On-Create")] + OnCreate = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"Never")] + Never = 1, + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.23.0 (Newtonsoft.Json v11.0.0.0)")] + public enum IdentityAuthType + { + [System.Runtime.Serialization.EnumMember(Value = @"None")] + None = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"Sas")] + Sas = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"X509")] + X509 = 2, + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.6.2.0 (NJsonSchema v10.1.23.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class FileResponse : System.IDisposable + { + private System.IDisposable _client; + private System.IDisposable _response; + + public int StatusCode { get; private set; } + + public System.Collections.Generic.IReadOnlyDictionary> Headers { get; private set; } + + public System.IO.Stream Stream { get; private set; } + + public bool IsPartial + { + get { return StatusCode == 206; } + } + + public FileResponse(int statusCode, System.Collections.Generic.IReadOnlyDictionary> headers, System.IO.Stream stream, System.IDisposable client, System.IDisposable response) + { + StatusCode = statusCode; + Headers = headers; + Stream = stream; + _client = client; + _response = response; + } + + public void Dispose() + { + if (Stream != null) + Stream.Dispose(); + if (_response != null) + _response.Dispose(); + if (_client != null) + _client.Dispose(); + } + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.6.2.0 (NJsonSchema v10.1.23.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class SwaggerException : System.Exception + { + public int StatusCode { get; private set; } + + public string Response { get; private set; } + + public System.Collections.Generic.IReadOnlyDictionary> Headers { get; private set; } + + public SwaggerException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary> headers, System.Exception innerException) + : base(message + "\n\nStatus: " + statusCode + "\nResponse: \n" + ((response == null) ? "(null)" : response.Substring(0, response.Length >= 512 ? 512 : response.Length)), innerException) + { + StatusCode = statusCode; + Response = response; + Headers = headers; + } + + public override string ToString() + { + return string.Format("HTTP Response: \n\n{0}\n\n{1}", Response, base.ToString()); + } + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.6.2.0 (NJsonSchema v10.1.23.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class SwaggerException : SwaggerException + { + public TResult Result { get; private set; } + + public SwaggerException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary> headers, TResult result, System.Exception innerException) + : base(message, statusCode, response, headers, innerException) + { + Result = result; + } + } + +} diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/versioning/ModuleManagementHttpClientVersioned.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/versioning/ModuleManagementHttpClientVersioned.cs index c7df4615f07..a4e591711f2 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/versioning/ModuleManagementHttpClientVersioned.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet/versioning/ModuleManagementHttpClientVersioned.cs @@ -81,6 +81,8 @@ protected ModuleManagementHttpClientVersioned( public abstract Task ReprovisionDeviceAsync(); + public abstract Task GetSupportBundle(Option since, Option iothubHostname, Option edgeRuntimeOnly, CancellationToken token); + public virtual async Task GetModuleLogs(string module, bool follow, Option tail, Option since, CancellationToken cancellationToken) { using (HttpClient httpClient = HttpClientHelper.GetHttpClient(this.ManagementUri)) diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/EdgeAgentConnection.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/EdgeAgentConnection.cs index be3c4afbdc2..95f93c20720 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/EdgeAgentConnection.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/EdgeAgentConnection.cs @@ -7,6 +7,7 @@ namespace Microsoft.Azure.Devices.Edge.Agent.IoTHub using Microsoft.Azure.Devices.Edge.Agent.Core; using Microsoft.Azure.Devices.Edge.Agent.Core.ConfigSources; using Microsoft.Azure.Devices.Edge.Agent.Core.DeviceManager; + using Microsoft.Azure.Devices.Edge.Agent.Core.Metrics; using Microsoft.Azure.Devices.Edge.Agent.Core.Requests; using Microsoft.Azure.Devices.Edge.Agent.Core.Serde; using Microsoft.Azure.Devices.Edge.Util; @@ -35,6 +36,7 @@ public class EdgeAgentConnection : IEdgeAgentConnection readonly IModuleConnection moduleConnection; readonly bool pullOnReconnect; readonly IDeviceManager deviceManager; + readonly IDeploymentMetrics deploymentMetrics; Option desiredProperties; Option reportedProperties; @@ -44,8 +46,9 @@ public EdgeAgentConnection( IModuleClientProvider moduleClientProvider, ISerde desiredPropertiesSerDe, IRequestManager requestManager, - IDeviceManager deviceManager) - : this(moduleClientProvider, desiredPropertiesSerDe, requestManager, deviceManager, true, DefaultConfigRefreshFrequency, TransientRetryStrategy) + IDeviceManager deviceManager, + IDeploymentMetrics deploymentMetrics) + : this(moduleClientProvider, desiredPropertiesSerDe, requestManager, deviceManager, true, DefaultConfigRefreshFrequency, TransientRetryStrategy, deploymentMetrics) { } @@ -55,8 +58,9 @@ public EdgeAgentConnection( IRequestManager requestManager, IDeviceManager deviceManager, bool enableSubscriptions, - TimeSpan configRefreshFrequency) - : this(moduleClientProvider, desiredPropertiesSerDe, requestManager, deviceManager, enableSubscriptions, configRefreshFrequency, TransientRetryStrategy) + TimeSpan configRefreshFrequency, + IDeploymentMetrics deploymentMetrics) + : this(moduleClientProvider, desiredPropertiesSerDe, requestManager, deviceManager, enableSubscriptions, configRefreshFrequency, TransientRetryStrategy, deploymentMetrics) { } @@ -67,7 +71,8 @@ internal EdgeAgentConnection( IDeviceManager deviceManager, bool enableSubscriptions, TimeSpan refreshConfigFrequency, - RetryStrategy retryStrategy) + RetryStrategy retryStrategy, + IDeploymentMetrics deploymentMetrics) { this.desiredPropertiesSerDe = Preconditions.CheckNotNull(desiredPropertiesSerDe, nameof(desiredPropertiesSerDe)); this.deploymentConfigInfo = Option.None(); @@ -75,10 +80,11 @@ internal EdgeAgentConnection( this.moduleConnection = new ModuleConnection(moduleClientProvider, requestManager, this.OnConnectionStatusChanged, this.OnDesiredPropertiesUpdated, enableSubscriptions); this.retryStrategy = Preconditions.CheckNotNull(retryStrategy, nameof(retryStrategy)); this.refreshTwinTask = new PeriodicTask(this.ForceRefreshTwin, refreshConfigFrequency, refreshConfigFrequency, Events.Log, "refresh twin config"); - this.initTask = this.ForceRefreshTwin(); this.pullOnReconnect = enableSubscriptions; this.deviceManager = Preconditions.CheckNotNull(deviceManager, nameof(deviceManager)); Events.TwinRefreshInit(refreshConfigFrequency); + this.deploymentMetrics = Preconditions.CheckNotNull(deploymentMetrics, nameof(deploymentMetrics)); + this.initTask = this.ForceRefreshTwin(); } public Option ReportedProperties => this.reportedProperties; @@ -110,11 +116,13 @@ public async Task UpdateReportedPropertiesAsync(TwinCollection patch) } await moduleClient.ForEachAsync(d => d.UpdateReportedPropertiesAsync(patch)); + this.deploymentMetrics.ReportIotHubSync(true); Events.UpdatedReportedProperties(); } catch (Exception e) { Events.ErrorUpdatingReportedProperties(e); + this.deploymentMetrics.ReportIotHubSync(false); throw; } } @@ -258,11 +266,13 @@ async Task GetTwinFunc() retryPolicy.Retrying += (_, args) => Events.RetryingGetTwin(args); Twin twin = await retryPolicy.ExecuteAsync(GetTwinFunc); Events.GotTwin(twin); + this.deploymentMetrics.ReportIotHubSync(true); return Option.Some(twin); } catch (Exception e) { Events.ErrorGettingTwin(e); + this.deploymentMetrics.ReportIotHubSync(false); if (!retrying && moduleClient != null) { diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/blob/AzureBlob.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/blob/AzureBlob.cs index ca303479342..03cdc3ed45c 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/blob/AzureBlob.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/blob/AzureBlob.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. namespace Microsoft.Azure.Devices.Edge.Agent.IoTHub.Blob { + using System.IO; using System.Threading.Tasks; using Microsoft.Azure.Devices.Edge.Util; using Microsoft.WindowsAzure.Storage.Blob; @@ -17,5 +18,7 @@ public AzureBlob(CloudBlockBlob blockBlob) public string Name => this.blockBlob.Name; public Task UploadFromByteArrayAsync(byte[] bytes) => this.blockBlob.UploadFromByteArrayAsync(bytes, 0, bytes.Length); + + public Task UploadFromStreamAsync(Stream source) => this.blockBlob.UploadFromStreamAsync(source); } } diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/blob/AzureBlobLogsUploader.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/blob/AzureBlobRequestsUploader.cs similarity index 74% rename from edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/blob/AzureBlobLogsUploader.cs rename to edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/blob/AzureBlobRequestsUploader.cs index 35e0b7bcfe7..f57f218c92f 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/blob/AzureBlobLogsUploader.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/blob/AzureBlobRequestsUploader.cs @@ -3,6 +3,7 @@ namespace Microsoft.Azure.Devices.Edge.Agent.IoTHub.Blob { using System; using System.Globalization; + using System.IO; using System.Threading.Tasks; using Microsoft.Azure.Devices.Edge.Agent.Core; using Microsoft.Azure.Devices.Edge.Agent.Core.Logs; @@ -11,7 +12,7 @@ namespace Microsoft.Azure.Devices.Edge.Agent.IoTHub.Blob using Microsoft.Extensions.Logging; using Microsoft.WindowsAzure.Storage.Blob; - public class AzureBlobLogsUploader : ILogsUploader + public class AzureBlobRequestsUploader : IRequestsUploader { const int RetryCount = 2; @@ -24,19 +25,19 @@ public class AzureBlobLogsUploader : ILogsUploader readonly string deviceId; readonly IAzureBlobUploader azureBlobUploader; - public AzureBlobLogsUploader(string iotHubName, string deviceId) + public AzureBlobRequestsUploader(string iotHubName, string deviceId) : this(iotHubName, deviceId, new AzureBlobUploader()) { } - public AzureBlobLogsUploader(string iotHubName, string deviceId, IAzureBlobUploader azureBlobUploader) + public AzureBlobRequestsUploader(string iotHubName, string deviceId, IAzureBlobUploader azureBlobUploader) { this.iotHubName = Preconditions.CheckNonWhiteSpace(iotHubName, nameof(iotHubName)); this.deviceId = Preconditions.CheckNonWhiteSpace(deviceId, nameof(deviceId)); this.azureBlobUploader = Preconditions.CheckNotNull(azureBlobUploader, nameof(azureBlobUploader)); } - public async Task Upload(string uri, string id, byte[] payload, LogsContentEncoding logsContentEncoding, LogsContentType logsContentType) + public async Task UploadLogs(string uri, string id, byte[] payload, LogsContentEncoding logsContentEncoding, LogsContentType logsContentType) { Preconditions.CheckNonWhiteSpace(uri, nameof(uri)); Preconditions.CheckNonWhiteSpace(id, nameof(id)); @@ -45,7 +46,7 @@ public async Task Upload(string uri, string id, byte[] payload, LogsContentEncod try { var containerUri = new Uri(uri); - string blobName = this.GetBlobName(id, logsContentEncoding, logsContentType); + string blobName = this.GetBlobName(id, GetLogsExtension(logsContentEncoding, logsContentType)); var container = new CloudBlobContainer(containerUri); Events.Uploading(blobName, container.Name); await ExecuteWithRetry( @@ -65,20 +66,47 @@ await ExecuteWithRetry( } // This method returns a func instead of IAzureAppendBlob interface to keep the ILogsUploader interface non Azure specific. - public async Task, Task>> GetUploaderCallback(string uri, string id, LogsContentEncoding logsContentEncoding, LogsContentType logsContentType) + public async Task, Task>> GetLogsUploaderCallback(string uri, string id, LogsContentEncoding logsContentEncoding, LogsContentType logsContentType) { Preconditions.CheckNonWhiteSpace(uri, nameof(uri)); Preconditions.CheckNonWhiteSpace(id, nameof(id)); var containerUri = new Uri(uri); - string blobName = this.GetBlobName(id, logsContentEncoding, logsContentType); + string blobName = this.GetBlobName(id, GetLogsExtension(logsContentEncoding, logsContentType)); var container = new CloudBlobContainer(containerUri); Events.Uploading(blobName, container.Name); IAzureAppendBlob blob = await this.azureBlobUploader.GetAppendBlob(containerUri, blobName, GetContentType(logsContentType), GetContentEncoding(logsContentEncoding)); return blob.AppendByteArray; } - internal static string GetExtension(LogsContentEncoding logsContentEncoding, LogsContentType logsContentType) + public async Task UploadSupportBundle(string uri, Stream source) + { + Preconditions.CheckNonWhiteSpace(uri, nameof(uri)); + Preconditions.CheckNotNull(source, nameof(source)); + + try + { + var containerUri = new Uri(uri); + string blobName = this.GetBlobName("support-bundle", "zip"); + var container = new CloudBlobContainer(containerUri); + Events.Uploading(blobName, container.Name); + await ExecuteWithRetry( + () => + { + IAzureBlob blob = this.azureBlobUploader.GetBlob(containerUri, blobName, Option.Some("application/zip"), Option.Some("zip")); + return blob.UploadFromStreamAsync(source); + }, + r => Events.UploadErrorRetrying(blobName, container.Name, r)); + Events.UploadSuccess(blobName, container.Name); + } + catch (Exception e) + { + Events.UploadError(e, "support-bundle"); + throw; + } + } + + internal static string GetLogsExtension(LogsContentEncoding logsContentEncoding, LogsContentType logsContentType) { if (logsContentEncoding == LogsContentEncoding.Gzip) { @@ -90,9 +118,8 @@ internal static string GetExtension(LogsContentEncoding logsContentEncoding, Log } } - internal string GetBlobName(string id, LogsContentEncoding logsContentEncoding, LogsContentType logsContentType) + internal string GetBlobName(string id, string extension) { - string extension = GetExtension(logsContentEncoding, logsContentType); string blobName = $"{id}-{DateTime.UtcNow.ToString("yyyy-MM-dd--HH-mm-ss", CultureInfo.InvariantCulture)}"; return $"{this.iotHubName}/{this.deviceId}/{blobName}.{extension}"; } @@ -139,7 +166,7 @@ class ErrorDetectionStrategy : ITransientErrorDetectionStrategy static class Events { const int IdStart = AgentEventIds.AzureBlobLogsUploader; - static readonly ILogger Log = Logger.Factory.CreateLogger(); + static readonly ILogger Log = Logger.Factory.CreateLogger(); enum EventIds { diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/blob/IAzureBlob.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/blob/IAzureBlob.cs index 100e538f494..420bbf3efdd 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/blob/IAzureBlob.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/blob/IAzureBlob.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. namespace Microsoft.Azure.Devices.Edge.Agent.IoTHub.Blob { + using System.IO; using System.Threading.Tasks; public interface IAzureBlob @@ -8,5 +9,7 @@ public interface IAzureBlob string Name { get; } Task UploadFromByteArrayAsync(byte[] bytes); + + Task UploadFromStreamAsync(Stream source); } } diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/ExperimentalFeatures.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/ExperimentalFeatures.cs index 70fc959a6cd..572afc86e3c 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/ExperimentalFeatures.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/ExperimentalFeatures.cs @@ -7,12 +7,13 @@ namespace Microsoft.Azure.Devices.Edge.Agent.Service public class ExperimentalFeatures { - ExperimentalFeatures(bool enabled, bool disableCloudSubscriptions, bool enableUploadLogs, bool enableGetLogs) + ExperimentalFeatures(bool enabled, bool disableCloudSubscriptions, bool enableUploadLogs, bool enableGetLogs, bool enableUploadSupportBundle) { this.Enabled = enabled; this.DisableCloudSubscriptions = disableCloudSubscriptions; this.EnableUploadLogs = enableUploadLogs; this.EnableGetLogs = enableGetLogs; + this.EnableUploadSupportBundle = enableUploadSupportBundle; } public static ExperimentalFeatures Create(IConfiguration experimentalFeaturesConfig, ILogger logger) @@ -21,7 +22,8 @@ public static ExperimentalFeatures Create(IConfiguration experimentalFeaturesCon bool disableCloudSubscriptions = enabled && experimentalFeaturesConfig.GetValue("disableCloudSubscriptions", false); bool enableUploadLogs = enabled && experimentalFeaturesConfig.GetValue("enableUploadLogs", false); bool enableGetLogs = enabled && experimentalFeaturesConfig.GetValue("enableGetLogs", false); - var experimentalFeatures = new ExperimentalFeatures(enabled, disableCloudSubscriptions, enableUploadLogs, enableGetLogs); + bool enableUploadSupportBundle = enabled && experimentalFeaturesConfig.GetValue("enableUploadSupportBundle", false); + var experimentalFeatures = new ExperimentalFeatures(enabled, disableCloudSubscriptions, enableUploadLogs, enableGetLogs, enableUploadSupportBundle); logger.LogInformation($"Experimental features configuration: {experimentalFeatures.ToJson()}"); return experimentalFeatures; } @@ -33,5 +35,7 @@ public static ExperimentalFeatures Create(IConfiguration experimentalFeaturesCon public bool EnableUploadLogs { get; } public bool EnableGetLogs { get; } + + public bool EnableUploadSupportBundle { get; } } } diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/Program.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/Program.cs index 40c9d80e2ef..e0a22fa591a 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/Program.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/Program.cs @@ -309,6 +309,7 @@ public static async Task MainAsync(IConfiguration configuration) { container.Resolve().Start(logger); container.Resolve().Start(logger); + await container.Resolve().Start(logger, versionInfo.ToString(true), Newtonsoft.Json.JsonConvert.SerializeObject(experimentalFeatures)); } // Initialize metric uploading @@ -373,7 +374,7 @@ public static async Task MainAsync(IConfiguration configuration) if (metricsConfig.Enabled && returnCode == 0) { - container.Resolve().IndicateCleanShutdown(); + container.Resolve().IndicateCleanShutdown(); } completed.Set(); diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/modules/AgentModule.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/modules/AgentModule.cs index b158a7306e4..c6c9ef2b750 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/modules/AgentModule.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/modules/AgentModule.cs @@ -268,8 +268,8 @@ protected override void Load(ContainerBuilder builder) .SingleInstance(); // IAvailabilityMetric - builder.Register(c => new AvailabilityMetrics(c.Resolve(), this.storagePath)) - .As() + builder.Register(c => new DeploymentMetrics(c.Resolve(), this.storagePath)) + .As() .SingleInstance(); // Task @@ -285,7 +285,7 @@ protected override void Load(ContainerBuilder builder) var deploymentConfigInfoSerde = c.Resolve>(); var deploymentConfigInfoStore = await c.Resolve>>(); var encryptionProvider = c.Resolve>(); - var availabilityMetric = c.Resolve(); + var availabilityMetric = c.Resolve(); return await Agent.Create( await configSource, await planner, diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/modules/FileConfigSourceModule.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/modules/FileConfigSourceModule.cs index 4e61507e8ff..0279d4531eb 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/modules/FileConfigSourceModule.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/modules/FileConfigSourceModule.cs @@ -9,6 +9,7 @@ namespace Microsoft.Azure.Devices.Edge.Agent.Service.Modules using Microsoft.Azure.Devices.Edge.Agent.Core; using Microsoft.Azure.Devices.Edge.Agent.Core.ConfigSources; using Microsoft.Azure.Devices.Edge.Agent.Core.DeviceManager; + using Microsoft.Azure.Devices.Edge.Agent.Core.Metrics; using Microsoft.Azure.Devices.Edge.Agent.Core.Requests; using Microsoft.Azure.Devices.Edge.Agent.Core.Serde; using Microsoft.Azure.Devices.Edge.Agent.Docker; @@ -65,7 +66,8 @@ protected override void Load(ContainerBuilder builder) var deviceClientprovider = c.Resolve(); var requestManager = c.Resolve(); var deviceManager = c.Resolve(); - IEdgeAgentConnection edgeAgentConnection = new EdgeAgentConnection(deviceClientprovider, serde, requestManager, deviceManager); + var deploymentMetrics = c.Resolve(); + IEdgeAgentConnection edgeAgentConnection = new EdgeAgentConnection(deviceClientprovider, serde, requestManager, deviceManager, deploymentMetrics); return edgeAgentConnection; }) .As() diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/modules/MetricsModule.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/modules/MetricsModule.cs index 8a9f72ee3d9..ddfdb3f2a10 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/modules/MetricsModule.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/modules/MetricsModule.cs @@ -3,8 +3,11 @@ namespace Microsoft.Azure.Devices.Edge.Agent.Service { using System.IO; + using System.Threading; + using System.Threading.Tasks; using Autofac; using Microsoft.Azure.Devices.Edge.Agent.Core; + using Microsoft.Azure.Devices.Edge.Agent.Core.Metrics; using Microsoft.Azure.Devices.Edge.Util; using Microsoft.Azure.Devices.Edge.Util.Metrics; using Microsoft.Azure.Devices.Edge.Util.Metrics.NullMetrics; @@ -35,6 +38,13 @@ protected override void Load(ContainerBuilder builder) .As() .SingleInstance(); + builder.Register(c => + { + var moduleManager = c.Resolve(); + return new MetadataMetrics(c.Resolve(), () => moduleManager.GetSystemInfoAsync(CancellationToken.None)); + }) + .As() + .SingleInstance(); base.Load(builder); } } diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/modules/TwinConfigSourceModule.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/modules/TwinConfigSourceModule.cs index a9930bedf24..efc5f6843a4 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/modules/TwinConfigSourceModule.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Service/modules/TwinConfigSourceModule.cs @@ -10,6 +10,7 @@ namespace Microsoft.Azure.Devices.Edge.Agent.Service.Modules using Microsoft.Azure.Devices.Edge.Agent.Core.ConfigSources; using Microsoft.Azure.Devices.Edge.Agent.Core.DeviceManager; using Microsoft.Azure.Devices.Edge.Agent.Core.Logs; + using Microsoft.Azure.Devices.Edge.Agent.Core.Metrics; using Microsoft.Azure.Devices.Edge.Agent.Core.Requests; using Microsoft.Azure.Devices.Edge.Agent.Core.Serde; using Microsoft.Azure.Devices.Edge.Agent.Docker; @@ -58,8 +59,8 @@ public TwinConfigSourceModule( protected override void Load(ContainerBuilder builder) { // ILogsUploader - builder.Register(c => new AzureBlobLogsUploader(this.iotHubHostName, this.deviceId)) - .As() + builder.Register(c => new AzureBlobRequestsUploader(this.iotHubHostName, this.deviceId)) + .As() .SingleInstance(); // Task @@ -93,12 +94,12 @@ protected override void Load(ContainerBuilder builder) builder.Register( async c => { - var logsUploader = c.Resolve(); + var requestUploader = c.Resolve(); var runtimeInfoProviderTask = c.Resolve>(); var logsProviderTask = c.Resolve>(); IRuntimeInfoProvider runtimeInfoProvider = await runtimeInfoProviderTask; ILogsProvider logsProvider = await logsProviderTask; - return new LogsUploadRequestHandler(logsUploader, logsProvider, runtimeInfoProvider) as IRequestHandler; + return new ModuleLogsUploadRequestHandler(requestUploader, logsProvider, runtimeInfoProvider) as IRequestHandler; }) .As>() .SingleInstance(); @@ -114,7 +115,20 @@ protected override void Load(ContainerBuilder builder) var logsProviderTask = c.Resolve>(); IRuntimeInfoProvider runtimeInfoProvider = await runtimeInfoProviderTask; ILogsProvider logsProvider = await logsProviderTask; - return new LogsRequestHandler(logsProvider, runtimeInfoProvider) as IRequestHandler; + return new ModuleLogsRequestHandler(logsProvider, runtimeInfoProvider) as IRequestHandler; + }) + .As>() + .SingleInstance(); + } + + if (this.experimentalFeatures.EnableUploadSupportBundle) + { + // Task - SupportBundleRequestHandler + builder.Register( + async c => + { + await Task.Yield(); + return new SupportBundleRequestHandler(c.Resolve().GetSupportBundle, c.Resolve(), this.iotHubHostName) as IRequestHandler; }) .As>() .SingleInstance(); @@ -149,7 +163,8 @@ protected override void Load(ContainerBuilder builder) var requestManager = c.Resolve(); var deviceManager = c.Resolve(); bool enableSubscriptions = !this.experimentalFeatures.DisableCloudSubscriptions; - IEdgeAgentConnection edgeAgentConnection = new EdgeAgentConnection(deviceClientprovider, serde, requestManager, deviceManager, enableSubscriptions, this.configRefreshFrequency); + var deploymentMetrics = c.Resolve(); + IEdgeAgentConnection edgeAgentConnection = new EdgeAgentConnection(deviceClientprovider, serde, requestManager, deviceManager, enableSubscriptions, this.configRefreshFrequency, deploymentMetrics); return edgeAgentConnection; }) .As() diff --git a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Core.Test/AgentTests.cs b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Core.Test/AgentTests.cs index 07a32654d93..1240e79d754 100644 --- a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Core.Test/AgentTests.cs +++ b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Core.Test/AgentTests.cs @@ -51,7 +51,7 @@ public void AgentConstructorInvalidArgs() var configStore = Mock.Of>(); var serde = Mock.Of>(); var encryptionDecryptionProvider = Mock.Of(); - var availabilityMetric = Mock.Of(); + var availabilityMetric = Mock.Of(); Assert.Throws(() => new Agent(null, mockEnvironmentProvider.Object, mockPlanner.Object, mockPlanRunner.Object, mockReporter.Object, mockModuleLifecycleManager.Object, configStore, DeploymentConfigInfo.Empty, serde, encryptionDecryptionProvider, availabilityMetric)); Assert.Throws(() => new Agent(mockConfigSource.Object, null, mockPlanner.Object, mockPlanRunner.Object, mockReporter.Object, mockModuleLifecycleManager.Object, configStore, DeploymentConfigInfo.Empty, serde, encryptionDecryptionProvider, availabilityMetric)); @@ -78,7 +78,7 @@ public async void AgentCreateSuccessWhenDecryptFails() var configStore = new Mock>(); var serde = Mock.Of>(); var encryptionDecryptionProvider = new Mock(); - var availabilityMetric = Mock.Of(); + var availabilityMetric = Mock.Of(); configStore.Setup(cs => cs.Get(It.IsAny())) .ReturnsAsync(Option.Some("encrypted")); encryptionDecryptionProvider.Setup(ep => ep.DecryptAsync(It.IsAny())) @@ -105,7 +105,7 @@ public async void ReconcileAsyncOnEmptyPlan() var runtimeInfo = Mock.Of(); var configStore = Mock.Of>(); var encryptionDecryptionProvider = Mock.Of(); - var availabilityMetric = Mock.Of(); + var availabilityMetric = Mock.Of(); var deploymentConfig = new DeploymentConfig( "1.0", @@ -156,7 +156,7 @@ public async void ReconcileAsyncAbortsWhenConfigSourceThrows() var mockEnvironmentProvider = Mock.Of(m => m.Create(It.IsAny()) == mockEnvironment.Object); var serde = Mock.Of>(); var encryptionDecryptionProvider = Mock.Of(); - var availabilityMetric = Mock.Of(); + var availabilityMetric = Mock.Of(); mockConfigSource.Setup(cs => cs.GetDeploymentConfigInfoAsync()).Throws(); mockEnvironment.Setup(env => env.GetModulesAsync(token)) @@ -193,7 +193,7 @@ public async void ReconcileAsyncAbortsWhenConfigSourceReturnsKnownExceptions( var mockEnvironmentProvider = Mock.Of(m => m.Create(It.IsAny()) == mockEnvironment.Object); var serde = Mock.Of>(); var encryptionDecryptionProvider = Mock.Of(); - var availabilityMetric = Mock.Of(); + var availabilityMetric = Mock.Of(); var deploymentConfigInfo = new DeploymentConfigInfo(10, testException); mockConfigSource.Setup(cs => cs.GetDeploymentConfigInfoAsync()) @@ -228,7 +228,7 @@ public async void ReconcileAsyncAbortsWhenEnvironmentSourceThrows() var mockEnvironmentProvider = Mock.Of(m => m.Create(It.IsAny()) == mockEnvironment.Object); var serde = Mock.Of>(); var encryptionDecryptionProvider = Mock.Of(); - var availabilityMetric = Mock.Of(); + var availabilityMetric = Mock.Of(); var deploymentConfig = new DeploymentConfig("1.0", Mock.Of(), new SystemModules(null, null), new Dictionary()); var deploymentConfigInfo = new DeploymentConfigInfo(0, deploymentConfig); @@ -263,7 +263,7 @@ public async void ReconcileAsyncAbortsWhenModuleIdentityLifecycleManagerThrows() var mockEnvironmentProvider = Mock.Of(m => m.Create(It.IsAny()) == mockEnvironment.Object); var serde = Mock.Of>(); var encryptionDecryptionProvider = Mock.Of(); - var availabilityMetric = Mock.Of(); + var availabilityMetric = Mock.Of(); var deploymentConfig = new DeploymentConfig( "1.0", @@ -309,7 +309,7 @@ public async void ReconcileAsyncReportsFailedWhenEncryptProviderThrows() var runtimeInfo = Mock.Of(); var configStore = Mock.Of>(); var encryptionDecryptionProvider = new Mock(); - var availabilityMetric = Mock.Of(); + var availabilityMetric = Mock.Of(); var deploymentConfig = new DeploymentConfig( "1.0", runtimeInfo, @@ -388,7 +388,7 @@ public async void ReconcileAsyncOnSetPlan() var mockEnvironmentProvider = Mock.Of(m => m.Create(It.IsAny()) == mockEnvironment.Object); var serde = Mock.Of>(); var encryptionDecryptionProvider = Mock.Of(); - var availabilityMetric = Mock.Of(); + var availabilityMetric = Mock.Of(); mockConfigSource.Setup(cs => cs.GetDeploymentConfigInfoAsync()) .ReturnsAsync(deploymentConfigInfo); @@ -437,7 +437,7 @@ public async void ReconcileAsyncWithNoDeploymentChange() var mockEnvironmentProvider = Mock.Of(m => m.Create(It.IsAny()) == mockEnvironment.Object); var serde = Mock.Of>(); var encryptionDecryptionProvider = Mock.Of(); - var availabilityMetric = Mock.Of(); + var availabilityMetric = Mock.Of(); mockConfigSource.Setup(cs => cs.GetDeploymentConfigInfoAsync()) .ReturnsAsync(deploymentConfigInfo); @@ -475,7 +475,7 @@ public async Task DesiredIsNotNullBecauseCurrentThrew() var mockEnvironmentProvider = Mock.Of(m => m.Create(It.IsAny()) == mockEnvironment.Object); var serde = Mock.Of>(); var encryptionDecryptionProvider = Mock.Of(); - var availabilityMetric = Mock.Of(); + var availabilityMetric = Mock.Of(); var deploymentConfigInfo = new DeploymentConfigInfo(1, new DeploymentConfig("1.0", runtimeInfo.Object, new SystemModules(null, null), ImmutableDictionary.Empty)); mockConfigSource.Setup(cs => cs.GetDeploymentConfigInfoAsync()) @@ -507,7 +507,7 @@ public async Task CurrentIsNotNullBecauseDesiredThrew() var mockEnvironmentProvider = Mock.Of(m => m.Create(It.IsAny()) == mockEnvironment.Object); var serde = Mock.Of>(); var encryptionDecryptionProvider = Mock.Of(); - var availabilityMetric = Mock.Of(); + var availabilityMetric = Mock.Of(); mockConfigSource.Setup(cs => cs.GetDeploymentConfigInfoAsync()) .Throws(); @@ -551,7 +551,7 @@ public async void ReconcileAsyncExecuteAsyncIncompleteDefaulsUnknown() var mockEnvironmentProvider = Mock.Of(m => m.Create(It.IsAny()) == mockEnvironment.Object); var serde = Mock.Of>(); var encryptionDecryptionProvider = Mock.Of(); - var availabilityMetric = Mock.Of(); + var deploymentMetrics = Mock.Of(); mockConfigSource.Setup(cs => cs.GetDeploymentConfigInfoAsync()) .ReturnsAsync(deploymentConfigInfo); @@ -567,7 +567,7 @@ public async void ReconcileAsyncExecuteAsyncIncompleteDefaulsUnknown() .Returns(Task.CompletedTask); mockPlanRunner.SetupSequence(m => m.ExecuteAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(false); - var agent = new Agent(mockConfigSource.Object, mockEnvironmentProvider, mockPlanner.Object, mockPlanRunner.Object, mockReporter.Object, mockModuleIdentityLifecycleManager.Object, configStore, DeploymentConfigInfo.Empty, serde, encryptionDecryptionProvider, availabilityMetric); + var agent = new Agent(mockConfigSource.Object, mockEnvironmentProvider, mockPlanner.Object, mockPlanRunner.Object, mockReporter.Object, mockModuleIdentityLifecycleManager.Object, configStore, DeploymentConfigInfo.Empty, serde, encryptionDecryptionProvider, deploymentMetrics); await agent.ReconcileAsync(token); @@ -606,7 +606,7 @@ public async void ReconcileAsyncExecuteAsyncIncompleteReportsLastState() var mockEnvironmentProvider = Mock.Of(m => m.Create(It.IsAny()) == mockEnvironment.Object); var serde = Mock.Of>(); var encryptionDecryptionProvider = Mock.Of(); - var availabilityMetric = Mock.Of(); + var deploymentMetrics = Mock.Of(); mockConfigSource.Setup(cs => cs.GetDeploymentConfigInfoAsync()) .ReturnsAsync(deploymentConfigInfo); @@ -625,7 +625,7 @@ public async void ReconcileAsyncExecuteAsyncIncompleteReportsLastState() mockPlanRunner.SetupSequence(m => m.ExecuteAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new Exception("generic exception")) .ReturnsAsync(false); - var agent = new Agent(mockConfigSource.Object, mockEnvironmentProvider, mockPlanner.Object, mockPlanRunner.Object, mockReporter.Object, mockModuleIdentityLifecycleManager.Object, configStore, DeploymentConfigInfo.Empty, serde, encryptionDecryptionProvider, availabilityMetric); + var agent = new Agent(mockConfigSource.Object, mockEnvironmentProvider, mockPlanner.Object, mockPlanRunner.Object, mockReporter.Object, mockModuleIdentityLifecycleManager.Object, configStore, DeploymentConfigInfo.Empty, serde, encryptionDecryptionProvider, deploymentMetrics); await agent.ReconcileAsync(token); await agent.ReconcileAsync(token); @@ -652,7 +652,7 @@ public async Task ReportShutdownAsyncConfigTest() var mockEnvironmentProvider = Mock.Of(m => m.Create(It.IsAny()) == mockEnvironment.Object); var serde = Mock.Of>(); var encryptionDecryptionProvider = Mock.Of(); - var availabilityMetric = Mock.Of(); + var availabilityMetric = Mock.Of(); var deploymentConfig = new DeploymentConfig( "1.0", @@ -723,7 +723,7 @@ public async Task HandleShutdownTest() var mockEnvironmentProvider = Mock.Of(m => m.Create(It.IsAny()) == mockEnvironment.Object); var serde = Mock.Of>(); var encryptionDecryptionProvider = Mock.Of(); - var availabilityMetric = Mock.Of(); + var availabilityMetric = Mock.Of(); var deploymentConfig = new DeploymentConfig("1.0", Mock.Of(), new SystemModules(null, null), modules); var deploymentConfigInfo = new DeploymentConfigInfo(0, deploymentConfig); diff --git a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Core.Test/metrics/AvaliabilityMetricsTest.cs b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Core.Test/metrics/AvaliabilityMetricsTest.cs index 842c146e397..ff262881a79 100644 --- a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Core.Test/metrics/AvaliabilityMetricsTest.cs +++ b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Core.Test/metrics/AvaliabilityMetricsTest.cs @@ -60,7 +60,7 @@ Action OnSet(Dictionary result) DateTime fakeTime = DateTime.Now; systemTime.Setup(x => x.UtcNow).Returns(() => fakeTime); - AvailabilityMetrics availabilityMetrics = new AvailabilityMetrics(metricsProvider.Object, Path.GetTempPath(), systemTime.Object); + DeploymentMetrics availabilityMetrics = new DeploymentMetrics(metricsProvider.Object, Path.GetTempPath(), systemTime.Object); (TestRuntimeModule[] current, TestModule[] desired) = GetTestModules(3); ModuleSet currentModuleSet = ModuleSet.Create(current as IModule[]); diff --git a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Core.Test/requests/LogsRequestHandlerTests.cs b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Core.Test/requests/LogsRequestHandlerTests.cs index 6872ab06c25..e1d03a4d685 100644 --- a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Core.Test/requests/LogsRequestHandlerTests.cs +++ b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Core.Test/requests/LogsRequestHandlerTests.cs @@ -64,17 +64,17 @@ public async Task GetJsonLogsTest() .ReturnsAsync(mod1Logs.ToBytes()); // Act - var logsRequestHandler = new LogsRequestHandler(logsProvider.Object, runtimeInfoProvider.Object); + var logsRequestHandler = new ModuleLogsRequestHandler(logsProvider.Object, runtimeInfoProvider.Object); Option response = await logsRequestHandler.HandleRequest(Option.Maybe(payload), CancellationToken.None); // Assert Assert.True(response.HasValue); logsProvider.VerifyAll(); runtimeInfoProvider.VerifyAll(); - var logsResponseList = response.OrDefault().FromJson>(); + var logsResponseList = response.OrDefault().FromJson>(); Assert.NotNull(logsResponseList); Assert.Single(logsResponseList); - LogsResponse logsResponse = logsResponseList[0]; + ModuleLogsResponse logsResponse = logsResponseList[0]; Assert.Equal(mod1, logsResponse.Id); Assert.True(logsResponse.Payload.HasValue); Assert.False(logsResponse.PayloadBytes.HasValue); @@ -127,17 +127,17 @@ public async Task GetTextLogsTest() .ReturnsAsync(mod1Logs.ToBytes()); // Act - var logsRequestHandler = new LogsRequestHandler(logsProvider.Object, runtimeInfoProvider.Object); + var logsRequestHandler = new ModuleLogsRequestHandler(logsProvider.Object, runtimeInfoProvider.Object); Option response = await logsRequestHandler.HandleRequest(Option.Maybe(payload), CancellationToken.None); // Assert Assert.True(response.HasValue); logsProvider.VerifyAll(); runtimeInfoProvider.VerifyAll(); - var logsResponseList = response.OrDefault().FromJson>(); + var logsResponseList = response.OrDefault().FromJson>(); Assert.NotNull(logsResponseList); Assert.Single(logsResponseList); - LogsResponse logsResponse = logsResponseList[0]; + ModuleLogsResponse logsResponse = logsResponseList[0]; Assert.Equal(mod1, logsResponse.Id); Assert.True(logsResponse.Payload.HasValue); Assert.False(logsResponse.PayloadBytes.HasValue); @@ -192,17 +192,17 @@ public async Task GetJsonGzipLogsTest() .ReturnsAsync(mod1LogBytes); // Act - var logsRequestHandler = new LogsRequestHandler(logsProvider.Object, runtimeInfoProvider.Object); + var logsRequestHandler = new ModuleLogsRequestHandler(logsProvider.Object, runtimeInfoProvider.Object); Option response = await logsRequestHandler.HandleRequest(Option.Maybe(payload), CancellationToken.None); // Assert Assert.True(response.HasValue); logsProvider.VerifyAll(); runtimeInfoProvider.VerifyAll(); - var logsResponseList = response.OrDefault().FromJson>(); + var logsResponseList = response.OrDefault().FromJson>(); Assert.NotNull(logsResponseList); Assert.Single(logsResponseList); - LogsResponse logsResponse = logsResponseList[0]; + ModuleLogsResponse logsResponse = logsResponseList[0]; Assert.Equal(mod1, logsResponse.Id); Assert.False(logsResponse.Payload.HasValue); Assert.True(logsResponse.PayloadBytes.HasValue); @@ -255,17 +255,17 @@ public async Task GetTextGzipLogsTest() .ReturnsAsync(mod1LogBytes); // Act - var logsRequestHandler = new LogsRequestHandler(logsProvider.Object, runtimeInfoProvider.Object); + var logsRequestHandler = new ModuleLogsRequestHandler(logsProvider.Object, runtimeInfoProvider.Object); Option response = await logsRequestHandler.HandleRequest(Option.Maybe(payload), CancellationToken.None); // Assert Assert.True(response.HasValue); logsProvider.VerifyAll(); runtimeInfoProvider.VerifyAll(); - var logsResponseList = response.OrDefault().FromJson>(); + var logsResponseList = response.OrDefault().FromJson>(); Assert.NotNull(logsResponseList); Assert.Single(logsResponseList); - LogsResponse logsResponse = logsResponseList[0]; + ModuleLogsResponse logsResponse = logsResponseList[0]; Assert.Equal(mod1, logsResponse.Id); Assert.False(logsResponse.Payload.HasValue); Assert.True(logsResponse.PayloadBytes.HasValue); @@ -275,15 +275,15 @@ public async Task GetTextGzipLogsTest() [Fact] public void InvalidCtorTest() { - Assert.Throws(() => new LogsRequestHandler(null, Mock.Of())); + Assert.Throws(() => new ModuleLogsRequestHandler(null, Mock.Of())); - Assert.Throws(() => new LogsRequestHandler(Mock.Of(), null)); + Assert.Throws(() => new ModuleLogsRequestHandler(Mock.Of(), null)); } [Fact] public async Task InvalidInputsTest() { - var logsRequestHandler = new LogsRequestHandler(Mock.Of(), Mock.Of()); + var logsRequestHandler = new ModuleLogsRequestHandler(Mock.Of(), Mock.Of()); await Assert.ThrowsAsync(() => logsRequestHandler.HandleRequest(Option.None(), CancellationToken.None)); string payload = @"{ diff --git a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Core.Test/requests/LogsUploadRequestHandlerTest.cs b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Core.Test/requests/LogsUploadRequestHandlerTest.cs index 447e8705de8..755a61e348e 100644 --- a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Core.Test/requests/LogsUploadRequestHandlerTest.cs +++ b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Core.Test/requests/LogsUploadRequestHandlerTest.cs @@ -48,7 +48,7 @@ public static IEnumerable GetLogsUploadRequestHandlerData() public async Task TestLogsUploadRequest(string payload, string id, string sasUrl, LogsContentEncoding contentEncoding, LogsContentType contentType, ModuleLogFilter filter) { // Arrange - var logsUploader = new Mock(); + var logsUploader = new Mock(); var logsProvider = new Mock(); var uploadBytes = new byte[100]; var moduleLogOptions = new ModuleLogOptions(contentEncoding, contentType, filter, LogOutputFraming.None, Option.None(), false); @@ -56,7 +56,7 @@ public async Task TestLogsUploadRequest(string payload, string id, string sasUrl if (contentType == LogsContentType.Text) { Func, Task> getLogsCallback = bytes => Task.CompletedTask; - logsUploader.Setup(l => l.GetUploaderCallback(sasUrl, id, contentEncoding, contentType)) + logsUploader.Setup(l => l.GetLogsUploaderCallback(sasUrl, id, contentEncoding, contentType)) .ReturnsAsync(getLogsCallback); logsProvider.Setup(l => l.GetLogsStream(id, moduleLogOptions, getLogsCallback, It.IsAny())) .Returns(Task.CompletedTask); @@ -65,7 +65,7 @@ public async Task TestLogsUploadRequest(string payload, string id, string sasUrl { logsProvider.Setup(l => l.GetLogs(id, moduleLogOptions, It.IsAny())) .ReturnsAsync(uploadBytes); - logsUploader.Setup(l => l.Upload(sasUrl, id, uploadBytes, contentEncoding, contentType)) + logsUploader.Setup(l => l.UploadLogs(sasUrl, id, uploadBytes, contentEncoding, contentType)) .Returns(Task.CompletedTask); } @@ -76,7 +76,7 @@ public async Task TestLogsUploadRequest(string payload, string id, string sasUrl var runtimeInfoProvider = Mock.Of(r => r.GetModules(It.IsAny()) == Task.FromResult(moduleRuntimeInfoList)); // Act - var logsUploadRequestHandler = new LogsUploadRequestHandler(logsUploader.Object, logsProvider.Object, runtimeInfoProvider); + var logsUploadRequestHandler = new ModuleLogsUploadRequestHandler(logsUploader.Object, logsProvider.Object, runtimeInfoProvider); Option response = await logsUploadRequestHandler.HandleRequest(Option.Maybe(payload), CancellationToken.None); // Assert @@ -129,32 +129,32 @@ public async Task TestLogsUploadAllTaskRequest() runtimeInfoProvider.Setup(r => r.GetModules(It.IsAny())) .ReturnsAsync(moduleRuntimeInfoList); - var logsUploader = new Mock(); + var logsUploader = new Mock(); var logsProvider = new Mock(); var module1LogOptions = new ModuleLogOptions(contentEncoding, contentType, filter, LogOutputFraming.None, Option.None(), false); var mod1LogBytes = new byte[100]; logsProvider.Setup(l => l.GetLogs(mod1, module1LogOptions, It.IsAny())) .ReturnsAsync(mod1LogBytes); - logsUploader.Setup(l => l.Upload(sasUrl, mod1, mod1LogBytes, contentEncoding, contentType)) + logsUploader.Setup(l => l.UploadLogs(sasUrl, mod1, mod1LogBytes, contentEncoding, contentType)) .Returns(Task.CompletedTask); var module2LogOptions = new ModuleLogOptions(contentEncoding, contentType, filter, LogOutputFraming.None, Option.None(), false); var mod2LogBytes = new byte[80]; logsProvider.Setup(l => l.GetLogs(mod2, module2LogOptions, It.IsAny())) .ReturnsAsync(mod2LogBytes); - logsUploader.Setup(l => l.Upload(sasUrl, mod2, mod2LogBytes, contentEncoding, contentType)) + logsUploader.Setup(l => l.UploadLogs(sasUrl, mod2, mod2LogBytes, contentEncoding, contentType)) .Returns(Task.CompletedTask); var module3LogOptions = new ModuleLogOptions(contentEncoding, contentType, filter, LogOutputFraming.None, Option.None(), false); var mod3LogBytes = new byte[120]; logsProvider.Setup(l => l.GetLogs(mod3, module3LogOptions, It.IsAny())) .ReturnsAsync(mod3LogBytes); - logsUploader.Setup(l => l.Upload(sasUrl, mod3, mod3LogBytes, contentEncoding, contentType)) + logsUploader.Setup(l => l.UploadLogs(sasUrl, mod3, mod3LogBytes, contentEncoding, contentType)) .Returns(Task.CompletedTask); // Act - var logsUploadRequestHandler = new LogsUploadRequestHandler(logsUploader.Object, logsProvider.Object, runtimeInfoProvider.Object); + var logsUploadRequestHandler = new ModuleLogsUploadRequestHandler(logsUploader.Object, logsProvider.Object, runtimeInfoProvider.Object); Option response = await logsUploadRequestHandler.HandleRequest(Option.Maybe(payload), CancellationToken.None); // Assert @@ -197,13 +197,13 @@ static async Task WaitForBackgroundTaskCompletion(string correlationId) public async Task TestLogsUploadRequestWithInvalidSchemaVersion(string payload, Type exception) { // Arrange - var logsUploader = new Mock(); + var logsUploader = new Mock(); var logsProvider = new Mock(); var runtimeInfoProvider = Mock.Of(); // Act - var logsUploadRequestHandler = new LogsUploadRequestHandler(logsUploader.Object, logsProvider.Object, runtimeInfoProvider); + var logsUploadRequestHandler = new ModuleLogsUploadRequestHandler(logsUploader.Object, logsProvider.Object, runtimeInfoProvider); await Assert.ThrowsAsync(exception, () => logsUploadRequestHandler.HandleRequest(Option.Maybe(payload), CancellationToken.None)); } } diff --git a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Docker.E2E.Test/AgentTests.cs b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Docker.E2E.Test/AgentTests.cs index 34a13d30067..a01257df1fa 100644 --- a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Docker.E2E.Test/AgentTests.cs +++ b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Docker.E2E.Test/AgentTests.cs @@ -179,7 +179,7 @@ public async Task AgentStartsUpModules(TestConfig testConfig) }.ToImmutableDictionary(); var moduleIdentityLifecycleManager = new Mock(); moduleIdentityLifecycleManager.Setup(m => m.GetModuleIdentitiesAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(identities)); - var availabilityMetric = Mock.Of(); + var availabilityMetric = Mock.Of(); Agent agent = await Agent.Create( configSource.Object, diff --git a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Integration.Test/AgentTestsBase.cs b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Integration.Test/AgentTestsBase.cs index 62884621158..068b39217c9 100644 --- a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Integration.Test/AgentTestsBase.cs +++ b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Integration.Test/AgentTestsBase.cs @@ -120,7 +120,7 @@ protected async Task AgentExecutionTestAsync(TestConfig testConfig) IImmutableDictionary immutableIdentities = identities.ToImmutableDictionary(); var moduleIdentityLifecycleManager = new Mock(); moduleIdentityLifecycleManager.Setup(m => m.GetModuleIdentitiesAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(immutableIdentities)); - var availabilityMetric = Mock.Of(); + var availabilityMetric = Mock.Of(); var store = Mock.Of>(); HealthRestartPlanner restartPlanner = new HealthRestartPlanner(commandFactory, store, TimeSpan.FromSeconds(10), restartManager); diff --git a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.IoTHub.Test/EdgeAgentConnectionTest.cs b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.IoTHub.Test/EdgeAgentConnectionTest.cs index af4f1e5de0d..89d5e59f92d 100644 --- a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.IoTHub.Test/EdgeAgentConnectionTest.cs +++ b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.IoTHub.Test/EdgeAgentConnectionTest.cs @@ -11,6 +11,7 @@ namespace Microsoft.Azure.Devices.Edge.Agent.IoTHub.Test using Microsoft.Azure.Devices.Edge.Agent.Core; using Microsoft.Azure.Devices.Edge.Agent.Core.ConfigSources; using Microsoft.Azure.Devices.Edge.Agent.Core.DeviceManager; + using Microsoft.Azure.Devices.Edge.Agent.Core.Metrics; using Microsoft.Azure.Devices.Edge.Agent.Core.Requests; using Microsoft.Azure.Devices.Edge.Agent.Core.Serde; using Microsoft.Azure.Devices.Edge.Agent.Docker; @@ -245,7 +246,7 @@ static IEdgeAgentConnection CreateEdgeAgentConnection(IotHubConnectionStringBuil ISerde serde = new TypeSpecificSerDe(deserializerTypes); IEnumerable requestHandlers = new List { new PingRequestHandler() }; var deviceManager = new Mock(); - IEdgeAgentConnection edgeAgentConnection = new EdgeAgentConnection(moduleClientProvider, serde, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object); + IEdgeAgentConnection edgeAgentConnection = new EdgeAgentConnection(moduleClientProvider, serde, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, Mock.Of()); return edgeAgentConnection; } @@ -546,7 +547,7 @@ public async Task GetDeploymentConfigInfoAsyncReturnsConfigWhenThereAreNoErrors( var deviceManager = new Mock(); // Act - var connection = new EdgeAgentConnection(moduleClientProvider.Object, serde.Object, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object); + var connection = new EdgeAgentConnection(moduleClientProvider.Object, serde.Object, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, Mock.Of()); Option deploymentConfigInfo = await connection.GetDeploymentConfigInfoAsync(); // Assert @@ -596,7 +597,7 @@ public async Task GetDeploymentConfigInfoAsyncIncludesExceptionWhenDeserializeTh var deviceManager = new Mock(); // Act - var connection = new EdgeAgentConnection(moduleClientProvider.Object, serde.Object, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object); + var connection = new EdgeAgentConnection(moduleClientProvider.Object, serde.Object, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, Mock.Of()); // Assert // The connection hasn't been created yet. So wait for it. @@ -652,7 +653,7 @@ public async Task GetDeploymentConfigInfoAsyncIncludesExceptionWhenDeserializeTh var deviceManager = new Mock(); // Act - var connection = new EdgeAgentConnection(moduleClientProvider.Object, serde.Object, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object); + var connection = new EdgeAgentConnection(moduleClientProvider.Object, serde.Object, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, Mock.Of()); // Assert // The connection hasn't been created yet. So wait for it. @@ -724,7 +725,7 @@ internal async Task ConnectionStatusChangeReasonReprovisionsDevice( deviceManager.Setup(x => x.ReprovisionDeviceAsync()).Returns(Task.CompletedTask); // Act - var connection = new EdgeAgentConnection(moduleClientProvider.Object, serde.Object, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object); + var connection = new EdgeAgentConnection(moduleClientProvider.Object, serde.Object, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, Mock.Of()); // Assert // The connection hasn't been created yet. So wait for it. @@ -791,7 +792,7 @@ public async Task GetDeploymentConfigInfoIncludesExceptionWhenSchemaVersionDoesN var deviceManager = new Mock(); // Act - var connection = new EdgeAgentConnection(moduleClientProvider.Object, serde.Object, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object); + var connection = new EdgeAgentConnection(moduleClientProvider.Object, serde.Object, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, Mock.Of()); // Assert // The connection hasn't been created yet. So wait for it. @@ -857,7 +858,7 @@ public async Task GetDeploymentConfigInfoAsyncIDoesNotIncludeExceptionWhenGetTwi var deviceManager = new Mock(); // Act - IEdgeAgentConnection connection = new EdgeAgentConnection(moduleClientProvider.Object, serde.Object, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, true, TimeSpan.FromHours(1), retryStrategy.Object); + IEdgeAgentConnection connection = new EdgeAgentConnection(moduleClientProvider.Object, serde.Object, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, true, TimeSpan.FromHours(1), retryStrategy.Object, Mock.Of()); // Assert // The connection hasn't been created yet. So wait for it. @@ -940,7 +941,7 @@ public async Task GetDeploymentConfigInfoAsyncRetriesWhenGetTwinThrows() var deviceManager = new Mock(); // Act - IEdgeAgentConnection connection = new EdgeAgentConnection(moduleClientProvider.Object, serde.Object, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, true, TimeSpan.FromHours(1), retryStrategy.Object); + IEdgeAgentConnection connection = new EdgeAgentConnection(moduleClientProvider.Object, serde.Object, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, true, TimeSpan.FromHours(1), retryStrategy.Object, Mock.Of()); // Assert // The connection hasn't been created yet. So wait for it. @@ -1009,7 +1010,7 @@ public async Task GetDeploymentConfigInfoAsyncReturnsConfigWhenThereAreNoErrorsW IEnumerable requestHandlers = new List { new PingRequestHandler() }; var deviceManager = new Mock(); - var connection = new EdgeAgentConnection(moduleClientProvider.Object, serde.Object, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object); + var connection = new EdgeAgentConnection(moduleClientProvider.Object, serde.Object, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, Mock.Of()); // Assert // The connection hasn't been created yet. So wait for it. @@ -1112,7 +1113,7 @@ public async Task EdgeAgentConnectionStatusTest() Assert.NotNull(edgeAgentModule); Assert.True(edgeAgentModule.ConnectionState == DeviceConnectionState.Disconnected); - IEdgeAgentConnection edgeAgentConnection = new EdgeAgentConnection(moduleClientProvider, serde, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object); + IEdgeAgentConnection edgeAgentConnection = new EdgeAgentConnection(moduleClientProvider, serde, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, Mock.Of()); await Task.Delay(TimeSpan.FromSeconds(5)); edgeAgentModule = await registryManager.GetModuleAsync(edgeDeviceId, Constants.EdgeAgentModuleIdentityName); @@ -1210,7 +1211,7 @@ public async Task EdgeAgentPingMethodTest() // Assert await Assert.ThrowsAsync(() => serviceClient.InvokeDeviceMethodAsync(edgeDeviceId, Constants.EdgeAgentModuleIdentityName, new CloudToDeviceMethod("ping"))); - IEdgeAgentConnection edgeAgentConnection = new EdgeAgentConnection(moduleClientProvider, serde, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object); + IEdgeAgentConnection edgeAgentConnection = new EdgeAgentConnection(moduleClientProvider, serde, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, Mock.Of()); await Task.Delay(TimeSpan.FromSeconds(10)); CloudToDeviceMethodResult methodResult = await serviceClient.InvokeDeviceMethodAsync(edgeDeviceId, Constants.EdgeAgentModuleIdentityName, new CloudToDeviceMethod("ping")); @@ -1305,7 +1306,7 @@ public async Task EdgeAgentConnectionRefreshTest() var deviceManager = new Mock(); // Act - using (var edgeAgentConnection = new EdgeAgentConnection(moduleClientProvider.Object, serde, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, true, TimeSpan.FromSeconds(3))) + using (var edgeAgentConnection = new EdgeAgentConnection(moduleClientProvider.Object, serde, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, true, TimeSpan.FromSeconds(3), Mock.Of())) { await Task.Delay(TimeSpan.FromSeconds(8)); @@ -1382,7 +1383,7 @@ public async Task GetTwinFailureDoesNotUpdateState() var deviceManager = new Mock(); // Act - using (var edgeAgentConnection = new EdgeAgentConnection(moduleClientProvider.Object, serde, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, true, TimeSpan.FromSeconds(10), retryStrategy)) + using (var edgeAgentConnection = new EdgeAgentConnection(moduleClientProvider.Object, serde, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, true, TimeSpan.FromSeconds(10), retryStrategy, Mock.Of())) { await Task.Delay(TimeSpan.FromSeconds(3)); Option receivedDeploymentConfigInfo = await edgeAgentConnection.GetDeploymentConfigInfoAsync(); @@ -1493,7 +1494,7 @@ public async Task GetTwinRetryLogicGetsNewClient() var deviceManager = new Mock(); // Act - using (var edgeAgentConnection = new EdgeAgentConnection(moduleClientProvider.Object, serde, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, true, TimeSpan.FromSeconds(10), retryStrategy)) + using (var edgeAgentConnection = new EdgeAgentConnection(moduleClientProvider.Object, serde, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, true, TimeSpan.FromSeconds(10), retryStrategy, Mock.Of())) { await Task.Delay(TimeSpan.FromSeconds(3)); Option receivedDeploymentConfigInfo = await edgeAgentConnection.GetDeploymentConfigInfoAsync(); @@ -1596,7 +1597,7 @@ public async Task GetDeploymentConfigInfoAsync_CreateNewModuleClientWhenGetTwinT var deviceManager = new Mock(); // Act - IEdgeAgentConnection connection = new EdgeAgentConnection(moduleClientProvider.Object, serde.Object, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, true, TimeSpan.FromHours(1), retryStrategy.Object); + IEdgeAgentConnection connection = new EdgeAgentConnection(moduleClientProvider.Object, serde.Object, new RequestManager(requestHandlers, DefaultRequestTimeout), deviceManager.Object, true, TimeSpan.FromHours(1), retryStrategy.Object, Mock.Of()); // Assert // The connection hasn't been created yet. So wait for it. diff --git a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.IoTHub.Test/blob/AzureBlobLogsUploaderTest.cs b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.IoTHub.Test/blob/AzureBlobLogsUploaderTest.cs index 66b9a411b1e..2cec6146de0 100644 --- a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.IoTHub.Test/blob/AzureBlobLogsUploaderTest.cs +++ b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.IoTHub.Test/blob/AzureBlobLogsUploaderTest.cs @@ -28,7 +28,7 @@ public class AzureBlobLogsUploaderTest [InlineData(LogsContentEncoding.None, LogsContentType.Text, "log")] public void GetExtensionTest(LogsContentEncoding contentEncoding, LogsContentType contentType, string expectedExtension) { - Assert.Equal(expectedExtension, AzureBlobLogsUploader.GetExtension(contentEncoding, contentType)); + Assert.Equal(expectedExtension, AzureBlobRequestsUploader.GetLogsExtension(contentEncoding, contentType)); } [Fact] @@ -42,10 +42,10 @@ public void GetBlobNameTest() var regex = new Regex(BlobNameRegexPattern); - var azureBlobLogsUploader = new AzureBlobLogsUploader(iotHub, deviceId, Mock.Of()); + var azureBlobLogsUploader = new AzureBlobRequestsUploader(iotHub, deviceId, Mock.Of()); // Act - string blobName = azureBlobLogsUploader.GetBlobName(id, LogsContentEncoding.Gzip, LogsContentType.Json); + string blobName = azureBlobLogsUploader.GetBlobName(id, AzureBlobRequestsUploader.GetLogsExtension(LogsContentEncoding.Gzip, LogsContentType.Json)); // Assert Assert.NotNull(blobName); @@ -95,10 +95,10 @@ public async Task UploadTest() }) .Returns(azureBlob.Object); - var azureBlobLogsUploader = new AzureBlobLogsUploader(iotHub, deviceId, azureBlobUploader.Object); + var azureBlobLogsUploader = new AzureBlobRequestsUploader(iotHub, deviceId, azureBlobUploader.Object); // Act - await azureBlobLogsUploader.Upload(sasUri, id, payload, LogsContentEncoding.Gzip, LogsContentType.Json); + await azureBlobLogsUploader.UploadLogs(sasUri, id, payload, LogsContentEncoding.Gzip, LogsContentType.Json); // Assert Assert.NotNull(receivedBlobName); @@ -143,10 +143,10 @@ public async Task GetUploaderCallbackTest() }) .ReturnsAsync(azureAppendBlob.Object); - var azureBlobLogsUploader = new AzureBlobLogsUploader(iotHub, deviceId, azureBlobUploader.Object); + var azureBlobLogsUploader = new AzureBlobRequestsUploader(iotHub, deviceId, azureBlobUploader.Object); // Act - Func, Task> callback = await azureBlobLogsUploader.GetUploaderCallback(sasUri, id, LogsContentEncoding.Gzip, LogsContentType.Json); + Func, Task> callback = await azureBlobLogsUploader.GetLogsUploaderCallback(sasUri, id, LogsContentEncoding.Gzip, LogsContentType.Json); Assert.NotNull(callback); await callback.Invoke(new ArraySegment(payload1)); diff --git a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Kubernetes.IntegrationTest/DummyModuleManager.cs b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Kubernetes.IntegrationTest/DummyModuleManager.cs index 5b98ba2518a..944e3d5fafb 100644 --- a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Kubernetes.IntegrationTest/DummyModuleManager.cs +++ b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Kubernetes.IntegrationTest/DummyModuleManager.cs @@ -37,5 +37,7 @@ public class DummyModuleManager : IModuleManager public Task PrepareUpdateAsync(ModuleSpec moduleSpec) => throw new NotImplementedException(); public Task GetModuleLogs(string name, bool follow, Option tail, Option since, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task GetSupportBundle(Option since, Option iothubHostname, Option edgeRuntimeOnly, CancellationToken token) => throw new NotImplementedException(); } } diff --git a/edge-util/src/Microsoft.Azure.Devices.Edge.Util/edged/ApiVersion.cs b/edge-util/src/Microsoft.Azure.Devices.Edge.Util/edged/ApiVersion.cs index b9154525561..da88fe3daf1 100644 --- a/edge-util/src/Microsoft.Azure.Devices.Edge.Util/edged/ApiVersion.cs +++ b/edge-util/src/Microsoft.Azure.Devices.Edge.Util/edged/ApiVersion.cs @@ -9,6 +9,7 @@ public sealed class ApiVersion public static readonly ApiVersion Version20190130 = new ApiVersion(2, "2019-01-30"); public static readonly ApiVersion Version20191022 = new ApiVersion(3, "2019-10-22"); public static readonly ApiVersion Version20191105 = new ApiVersion(4, "2019-11-05"); + public static readonly ApiVersion Version20200707 = new ApiVersion(5, "2020-07-07"); public static readonly ApiVersion VersionUnknown = new ApiVersion(100, "Unknown"); static readonly Dictionary Instance = new Dictionary @@ -16,7 +17,8 @@ public sealed class ApiVersion { Version20180628.Name, Version20180628 }, { Version20190130.Name, Version20190130 }, { Version20191022.Name, Version20191022 }, - { Version20191105.Name, Version20191105 } + { Version20191105.Name, Version20191105 }, + { Version20200707.Name, Version20200707 } }; ApiVersion(int value, string name) diff --git a/edgelet/Cargo.lock b/edgelet/Cargo.lock index e988fe94c11..92eba3d1136 100755 --- a/edgelet/Cargo.lock +++ b/edgelet/Cargo.lock @@ -554,6 +554,7 @@ dependencies = [ "provisioning 0.1.0", "serde 1.0.92 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", + "support-bundle 0.1.0", "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1061,7 +1062,8 @@ dependencies = [ "serde 1.0.92 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.92 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", - "sysinfo 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)", + "support-bundle 0.1.0", + "sysinfo 0.14.15 (registry+https://github.com/rust-lang/crates.io-index)", "tabwriter 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "termcolor 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2063,6 +2065,21 @@ name = "strsim" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "support-bundle" +version = "0.1.0" +dependencies = [ + "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", + "edgelet-core 0.1.0", + "edgelet-test-utils 0.1.0", + "failure 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", + "zip 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "syn" version = "0.14.9" diff --git a/edgelet/Cargo.toml b/edgelet/Cargo.toml index e0cc9f0555a..09111a531c3 100644 --- a/edgelet/Cargo.toml +++ b/edgelet/Cargo.toml @@ -28,6 +28,7 @@ members = [ "mini-sntp", "provisioning", "signal-future", + "support-bundle", "systemd", "tokio-named-pipe", "win-logger", diff --git a/edgelet/api/managementVersion_2020_07_07.yaml b/edgelet/api/managementVersion_2020_07_07.yaml new file mode 100644 index 00000000000..47e0568432c --- /dev/null +++ b/edgelet/api/managementVersion_2020_07_07.yaml @@ -0,0 +1,787 @@ +swagger: '2.0' +schemes: + - http +info: + title: IoT Edge Management API + version: '2020-07-07' +tags: + - name: Module + x-displayName: Modules + description: | + Create and manage modules. + - name: Identity + x-displayName: Identities + description: | + Create and manage module identity. + - name: SystemInformation + x-displayName: SystemInformation + description: | + Get information about the runtime. +paths: + /modules: + get: + tags: + - Module + summary: List modules. + produces: + - application/json + description: | + This returns the list of currently running modules and their statuses. + operationId: ListModules + parameters: + - $ref: '#/parameters/api-version' + responses: + '200': + description: Ok + schema: + $ref: '#/definitions/ModuleList' + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + post: + tags: + - Module + summary: Create module. + operationId: CreateModule + consumes: + - application/json + produces: + - application/json + parameters: + - $ref: '#/parameters/api-version' + - in: body + name: module + required: true + schema: + $ref: '#/definitions/ModuleSpec' + responses: + '201': + description: Created + schema: + $ref: '#/definitions/ModuleDetails' + '409': + description: Conflict. Returned if module already exists. + schema: + $ref: '#/definitions/ErrorResponse' + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + '/modules/{name}': + get: + tags: + - Module + summary: Get a module's status. + operationId: GetModule + produces: + - application/json + parameters: + - $ref: '#/parameters/api-version' + - in: path + name: name + description: The name of the module to get. (urlencoded) + required: true + type: string + responses: + '200': + description: Ok + schema: + $ref: '#/definitions/ModuleDetails' + '404': + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + put: + tags: + - Module + summary: Update a module. + operationId: UpdateModule + consumes: + - application/json + produces: + - application/json + parameters: + - $ref: '#/parameters/api-version' + - in: path + name: name + description: The name of the module to update. (urlencoded) + required: true + type: string + - name: start + in: query + description: Flag indicating whether module should be started after updating. + required: false + type: boolean + default: false + allowEmptyValue: true + - in: body + name: module + required: true + schema: + $ref: '#/definitions/ModuleSpec' + responses: + '200': + description: Ok + schema: + $ref: '#/definitions/ModuleDetails' + '404': + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + delete: + tags: + - Module + summary: Delete a module. + operationId: DeleteModule + produces: + - application/json + parameters: + - $ref: '#/parameters/api-version' + - in: path + name: name + description: The name of the module to delete. (urlencoded) + required: true + type: string + responses: + '204': + description: No Content + '404': + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + '/modules/{name}/prepareupdate': + post: + tags: + - Module + summary: Prepare to update a module. + operationId: PrepareUpdateModule + consumes: + - application/json + produces: + - application/json + parameters: + - $ref: '#/parameters/api-version' + - in: path + name: name + description: The name of the module to update. (urlencoded) + required: true + type: string + - in: body + name: module + required: true + schema: + $ref: '#/definitions/ModuleSpec' + responses: + '204': + description: No Content + '404': + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + '/modules/{name}/start': + post: + tags: + - Module + summary: Start a module. + operationId: StartModule + parameters: + - $ref: '#/parameters/api-version' + - in: path + name: name + description: The name of the module to start. (urlencoded) + required: true + type: string + responses: + '204': + description: No Content + '304': + description: Not Modified + '404': + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + '/modules/{name}/stop': + post: + tags: + - Module + summary: Stop a module. + operationId: StopModule + parameters: + - $ref: '#/parameters/api-version' + - in: path + name: name + description: The name of the module to stop. (urlencoded) + required: true + type: string + responses: + '204': + description: No Content + '304': + description: Not Modified + '404': + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + '/modules/{name}/restart': + post: + tags: + - Module + summary: Restart a module. + operationId: RestartModule + parameters: + - $ref: '#/parameters/api-version' + - in: path + name: name + description: The name of the module to restart. (urlencoded) + required: true + type: string + responses: + '204': + description: No Content + '304': + description: Not Modified + '404': + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + '/modules/{name}/logs': + get: + tags: + - Module + summary: Get module logs. + operationId: ModuleLogs + parameters: + - $ref: '#/parameters/api-version' + - in: path + name: name + description: The name of the module to obtain logs for. (urlencoded) + required: true + type: string + - in: query + name: follow + description: Return the logs as a stream. + type: boolean + default: false + - in: query + name: tail + description: Only return this number of lines from the end of the logs. + type: string + default: "all" + - in: query + name: since + description: Only return logs since this time, as a duration (1 day, 1d, 90m, 2 days 3 hours 2 minutes), rfc3339 timestamp, or UNIX timestamp. + type: string + default: "0" + responses: + '101': + description: Logs returned as a stream + '200': + description: Logs returned as a string in response body + '404': + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + + '/identities/': + get: + tags: + - Identity + summary: List identities. + produces: + - application/json + description: | + This returns the list of current known idenities. + operationId: ListIdentities + parameters: + - $ref: '#/parameters/api-version' + responses: + '200': + description: Ok + schema: + $ref: '#/definitions/IdentityList' + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + post: + tags: + - Identity + summary: Create an identity. + operationId: CreateIdentity + consumes: + - application/json + produces: + - application/json + parameters: + - $ref: '#/parameters/api-version' + - in: body + name: identity + required: true + schema: + $ref: '#/definitions/IdentitySpec' + responses: + '200': + description: Created + schema: + $ref: '#/definitions/Identity' + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + '/identities/{name}': + put: + tags: + - Identity + summary: Update an identity. + operationId: UpdateIdentity + consumes: + - application/json + produces: + - application/json + parameters: + - $ref: '#/parameters/api-version' + - in: path + name: name + description: The name of the identity to update. (urlencoded) + required: true + type: string + - in: body + name: updateinfo + required: true + schema: + $ref: '#/definitions/UpdateIdentity' + responses: + '200': + description: Updated + schema: + $ref: '#/definitions/Identity' + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + delete: + tags: + - Identity + summary: Delete an identity. + operationId: DeleteIdentity + produces: + - application/json + parameters: + - $ref: '#/parameters/api-version' + - in: path + name: name + description: The name of the identity to delete. (urlencoded) + required: true + type: string + responses: + '204': + description: Ok + '404': + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + + /systeminfo: + get: + tags: + - SystemInformation + summary: Return host system information. + produces: + - application/json + operationId: GetSystemInfo + parameters: + - $ref: '#/parameters/api-version' + responses: + '200': + description: Ok + schema: + $ref: '#/definitions/SystemInfo' + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + + '/systeminfo/resources': + get: + tags: + - SystemInformation + summary: Return host resource usage (DISK, RAM, CPU). + produces: + - application/json + operationId: GetSystemResources + parameters: + - $ref: '#/parameters/api-version' + responses: + '200': + description: Ok + schema: + $ref: '#/definitions/SystemResources' + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + + '/systeminfo/supportbundle': + get: + tags: + - SystemInformation + summary: Return zip of support bundle. + produces: + - application/zip + operationId: GetSupportBundle + parameters: + - $ref: '#/parameters/api-version' + - in: query + name: since + description: Duration to get logs from. Can be relative (1d, 10m, 1h30m etc.) or absolute (unix timestamp or rfc 3339) + required: false + type: string + - in: query + name: host + description: Path to the management host + required: false + type: string + - in: query + name: iothub_hostname + description: Hub to use when calling iotedge check + required: false + type: string + - in: query + name: edge_runtime_only + description: Exclude customer module logs + required: false + type: boolean + default: false + responses: + '200': + description: Ok + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + + '/device/reprovision': + post: + tags: + - DeviceActions + summary: Trigger a device reprovisioning flow. + operationId: ReprovisionDevice + parameters: + - $ref: '#/parameters/api-version' + responses: + '200': + description: Ok + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + +definitions: + ModuleList: + type: object + properties: + modules: + type: array + items: + $ref: '#/definitions/ModuleDetails' + required: + - modules + ModuleDetails: + type: object + properties: + id: + type: string + description: System generated unique identitier. + example: happy_hawking + name: + type: string + description: The name of the module. + example: edgeHub + type: + type: string + description: The type of a module. + example: docker + config: + $ref: '#/definitions/Config' + status: + $ref: '#/definitions/Status' + required: + - id + - name + - type + - config + - status + ModuleSpec: + type: object + properties: + name: + type: string + description: The name of a the module. + example: edgeHub + type: + type: string + example: docker + imagePullPolicy: + type: string + enum: + - On-Create + - Never + example: "On-Create" + config: + $ref: '#/definitions/Config' + required: + - name + - type + - config + Config: + type: object + properties: + settings: + type: object + example: + image: "microsoft/azureiotedge-hub:1.0" + createOptions: + HostConfig: + PortBindings: + "22/tcp": + - HostPort: "11022" + env: + type: array + items: + $ref: '#/definitions/EnvVar' + required: + - settings + Status: + type: object + properties: + startTime: + type: string + format: date-time + exitStatus: + $ref: '#/definitions/ExitStatus' + runtimeStatus: + $ref: '#/definitions/RuntimeStatus' + required: + - runtimeStatus + EnvVar: + type: object + properties: + key: + type: string + example: the_key + value: + type: string + example: the_value + required: + - key + - value + ExitStatus: + type: object + properties: + exitTime: + type: string + format: date-time + statusCode: + type: string + required: + - exitTime + - statusCode + example: + exitTime: '2018-04-03T09:31:00.000Z' + statusCode: '101' + RuntimeStatus: + type: object + properties: + status: + type: string + description: + type: string + required: + - status + example: + status: the status + description: the description + SystemInfo: + type: object + properties: + osType: + type: string + architecture: + type: string + version: + type: string + server_version: + type: string + kernel_version: + type: string + operating_system: + type: string + cpus: + type: integer + required: + - osType + - architecture + example: + osType: "linux/windows" + architecture: "arm/amd64/x86" + SystemResources: + type: object + properties: + host_uptime: + type: integer + format: int64 + process_uptime: + type: integer + format: int64 + used_cpu: + type: number + used_ram: + type: integer + format: int64 + total_ram: + type: integer + format: int64 + disks: + type: array + items: + $ref: '#/definitions/Disk' + docker_stats: + type: string + required: + - host_uptime + - process_uptime + - used_cpu + - used_ram + - total_ram + - disks + - docker_stats + Disk: + type: object + properties: + name: + type: string + available_space: + type: integer + format: int64 + total_space: + type: integer + format: int64 + file_system: + type: string + file_type: + type: string + required: + - name + - available_space + - total_space + - file_system + - file_type + IdentityList: + type: object + properties: + identities: + type: array + items: + $ref: '#/definitions/Identity' + required: + - identities + IdentitySpec: + type: object + properties: + moduleId: + type: string + example: "edgeHub" + managedBy: + type: string + example: "IotEdge" + required: + - moduleId + UpdateIdentity: + type: object + properties: + generationId: + type: string + example: "636463636967581550" + managedBy: + type: string + example: "IotEdge" + required: + - generationId + Identity: + type: object + properties: + moduleId: + type: string + example: "edgeHub" + managedBy: + type: string + example: "iot-edge" + generationId: + type: string + example: "636463636967581550" + authType: + type: string + enum: + - None + - Sas + - X509 + example: "Sas" + required: + - moduleId + - managedBy + - generationId + - authType + + ErrorResponse: + type: object + properties: + message: + type: string + required: + - message + +parameters: + api-version: + name: api-version + in: query + description: The version of the API. + required: true + type: string + default: '2018-06-28' diff --git a/edgelet/edgelet-core/src/module.rs b/edgelet/edgelet-core/src/module.rs index 7f4726f17e3..4abf21227fc 100644 --- a/edgelet/edgelet-core/src/module.rs +++ b/edgelet/edgelet-core/src/module.rs @@ -11,6 +11,7 @@ use std::time::Duration; use chrono::prelude::*; use failure::{Fail, ResultExt}; use futures::{Future, Stream}; +use serde_derive::Serialize; use edgelet_utils::ensure_not_empty_with_context; @@ -345,36 +346,19 @@ pub trait ModuleRegistry { fn remove(&self, name: &str) -> Self::RemoveFuture; } -#[derive(Debug)] +#[derive(Debug, Default, Serialize)] pub struct SystemInfo { /// OS Type of the Host. Example of value expected: \"linux\" and \"windows\". - os_type: String, + #[serde(rename = "osType")] + pub os_type: String, /// Hardware architecture of the host. Example of value expected: arm32, x86, amd64 - architecture: String, + pub architecture: String, /// iotedge version string - version: &'static str, -} - -impl SystemInfo { - pub fn new(os_type: String, architecture: String) -> Self { - SystemInfo { - os_type, - architecture, - version: super::version_with_source_version(), - } - } - - pub fn os_type(&self) -> &str { - &self.os_type - } - - pub fn architecture(&self) -> &str { - &self.architecture - } - - pub fn version(&self) -> &str { - self.version - } + pub version: &'static str, + pub server_version: String, + pub kernel_version: String, + pub operating_system: String, + pub cpus: i32, } #[derive(Debug, serde_derive::Serialize)] @@ -559,6 +543,7 @@ pub enum RuntimeOperation { CreateModule(String), GetModule(String), GetModuleLogs(String), + GetSupportBundle, Init, ListModules, RemoveModule(String), @@ -578,6 +563,7 @@ impl fmt::Display for RuntimeOperation { RuntimeOperation::GetModuleLogs(name) => { write!(f, "Could not get logs for module {}", name) } + RuntimeOperation::GetSupportBundle => write!(f, "Could not get support bundle"), RuntimeOperation::Init => write!(f, "Could not initialize module runtime"), RuntimeOperation::ListModules => write!(f, "Could not list modules"), RuntimeOperation::RemoveModule(name) => write!(f, "Could not remove module {}", name), @@ -621,7 +607,7 @@ impl FromStr for ImagePullPolicy { #[cfg(test)] mod tests { - use super::{BTreeMap, Default, ImagePullPolicy, ModuleSpec, SystemInfo}; + use super::{BTreeMap, Default, ImagePullPolicy, ModuleSpec}; use std::str::FromStr; use std::string::ToString; @@ -737,26 +723,4 @@ mod tests { } } } - - #[test] - fn system_info_new_and_access_succeed() { - //arrange - let system_info = SystemInfo::new( - "testValueOsType".to_string(), - "testArchitectureType".to_string(), - ); - let expected_value_os_type = "testValueOsType"; - let expected_test_architecture_type = "testArchitectureType"; - - //act - let current_value_os_type = system_info.os_type(); - let current_value_architecture_type = system_info.architecture(); - - //assert - assert_eq!(expected_value_os_type, current_value_os_type); - assert_eq!( - expected_test_architecture_type, - current_value_architecture_type - ); - } } diff --git a/edgelet/edgelet-docker/src/runtime.rs b/edgelet/edgelet-docker/src/runtime.rs index da8adcdab20..e263403f136 100644 --- a/edgelet/edgelet-docker/src/runtime.rs +++ b/edgelet/edgelet-docker/src/runtime.rs @@ -579,16 +579,30 @@ impl ModuleRuntime for DockerModuleRuntime { .system_info() .then(|result| match result { Ok(system_info) => { - let system_info = CoreSystemInfo::new( - system_info + let system_info = CoreSystemInfo { + os_type: system_info .os_type() .unwrap_or(&String::from("Unknown")) .to_string(), - system_info + architecture: system_info .architecture() .unwrap_or(&String::from("Unknown")) .to_string(), - ); + version: edgelet_core::version_with_source_version(), + cpus: system_info.NCPU().unwrap_or_default(), + kernel_version: system_info + .kernel_version() + .map(std::string::ToString::to_string) + .unwrap_or_default(), + operating_system: system_info + .operating_system() + .map(std::string::ToString::to_string) + .unwrap_or_default(), + server_version: system_info + .server_version() + .map(std::string::ToString::to_string) + .unwrap_or_default(), + }; info!("Successfully queried system info"); Ok(system_info) } diff --git a/edgelet/edgelet-docker/tests/runtime.rs b/edgelet/edgelet-docker/tests/runtime.rs index ad2bbf8aa9c..e24833f2233 100644 --- a/edgelet/edgelet-docker/tests/runtime.rs +++ b/edgelet/edgelet-docker/tests/runtime.rs @@ -1871,8 +1871,8 @@ fn runtime_system_info_succeeds() { //assert assert_eq!(true, *system_info_got_called_lock_cloned.read().unwrap()); - assert_eq!("linux", system_info.os_type()); - assert_eq!("x86_64", system_info.architecture()); + assert_eq!("linux", system_info.os_type); + assert_eq!("x86_64", system_info.architecture); } #[test] @@ -1928,6 +1928,6 @@ fn runtime_system_info_none_returns_unkown() { //assert assert_eq!(true, *system_info_got_called_lock_cloned.read().unwrap()); - assert_eq!("Unknown", system_info.os_type()); - assert_eq!("Unknown", system_info.architecture()); + assert_eq!("Unknown", system_info.os_type); + assert_eq!("Unknown", system_info.architecture); } diff --git a/edgelet/edgelet-http-mgmt/Cargo.toml b/edgelet/edgelet-http-mgmt/Cargo.toml index 35d9d934b9e..da5b8fca1a3 100644 --- a/edgelet/edgelet-http-mgmt/Cargo.toml +++ b/edgelet/edgelet-http-mgmt/Cargo.toml @@ -21,6 +21,7 @@ edgelet-http = { path = "../edgelet-http" } edgelet-iothub = { path = "../edgelet-iothub" } management = { path = "../management" } provisioning = { path = "../provisioning" } +support-bundle = { path = "../support-bundle" } [dev-dependencies] chrono = { version = "0.4", features = ["serde"] } diff --git a/edgelet/edgelet-http-mgmt/src/error.rs b/edgelet/edgelet-http-mgmt/src/error.rs index 28a33f5c021..ddcadeb3a81 100644 --- a/edgelet/edgelet-http-mgmt/src/error.rs +++ b/edgelet/edgelet-http-mgmt/src/error.rs @@ -67,6 +67,9 @@ pub enum ErrorKind { #[fail(display = "Could not update module {:?}", _0)] UpdateModule(String), + + #[fail(display = "Could not collect support bundle")] + SupportBundle, } impl Fail for Error { diff --git a/edgelet/edgelet-http-mgmt/src/server/mod.rs b/edgelet/edgelet-http-mgmt/src/server/mod.rs index 56a3a08bf54..ff76adb97fa 100644 --- a/edgelet/edgelet-http-mgmt/src/server/mod.rs +++ b/edgelet/edgelet-http-mgmt/src/server/mod.rs @@ -27,7 +27,7 @@ mod system_info; use self::device_actions::ReprovisionDevice; use self::identity::{CreateIdentity, DeleteIdentity, ListIdentities, UpdateIdentity}; pub use self::module::*; -use self::system_info::{GetSystemInfo, GetSystemResources}; +use self::system_info::{GetSupportBundle, GetSystemInfo, GetSystemResources}; use crate::error::{Error, ErrorKind}; lazy_static! { @@ -73,6 +73,7 @@ impl ManagementService { get Version2018_06_28 runtime Policy::Anonymous => "/systeminfo" => GetSystemInfo::new(runtime.clone()), get Version2019_11_05 runtime Policy::Anonymous => "/systeminfo/resources" => GetSystemResources::new(runtime.clone()), + get Version2020_07_07 runtime Policy::Anonymous => "/systeminfo/supportbundle" => GetSupportBundle::new(runtime.clone()), post Version2019_10_22 runtime Policy::Module(&*AGENT_NAME) => "/device/reprovision" => ReprovisionDevice::new(initiate_shutdown_and_reprovision), ); diff --git a/edgelet/edgelet-http-mgmt/src/server/system_info/get.rs b/edgelet/edgelet-http-mgmt/src/server/system_info/get.rs index 3ef8543f6a1..be5c283c4f5 100644 --- a/edgelet/edgelet-http-mgmt/src/server/system_info/get.rs +++ b/edgelet/edgelet-http-mgmt/src/server/system_info/get.rs @@ -10,7 +10,6 @@ use serde::Serialize; use edgelet_core::{Module, ModuleRuntime, RuntimeOperation}; use edgelet_http::route::{Handler, Parameters}; use edgelet_http::Error as HttpError; -use management::models::SystemInfo; use crate::error::{Error, ErrorKind}; use crate::IntoResponse; @@ -44,20 +43,14 @@ where let system_info = system_info .context(ErrorKind::RuntimeOperation(RuntimeOperation::SystemInfo))?; - let body = SystemInfo::new( - system_info.os_type().to_string(), - system_info.architecture().to_string(), - system_info.version().to_string(), - ); - - let b = serde_json::to_string(&body) + let body = serde_json::to_string(&system_info) .context(ErrorKind::RuntimeOperation(RuntimeOperation::SystemInfo))?; let response = Response::builder() .status(StatusCode::OK) .header(CONTENT_TYPE, "application/json") - .header(CONTENT_LENGTH, b.len().to_string().as_str()) - .body(b.into()) + .header(CONTENT_LENGTH, body.len().to_string().as_str()) + .body(body.into()) .context(ErrorKind::RuntimeOperation(RuntimeOperation::SystemInfo))?; Ok(response) }) diff --git a/edgelet/edgelet-http-mgmt/src/server/system_info/mod.rs b/edgelet/edgelet-http-mgmt/src/server/system_info/mod.rs index a9960a0fd16..c98f6b55d3f 100644 --- a/edgelet/edgelet-http-mgmt/src/server/system_info/mod.rs +++ b/edgelet/edgelet-http-mgmt/src/server/system_info/mod.rs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. mod get; mod resources; +mod support_bundle; pub use self::get::GetSystemInfo; pub use self::resources::GetSystemResources; +pub use self::support_bundle::GetSupportBundle; diff --git a/edgelet/edgelet-http-mgmt/src/server/system_info/support_bundle.rs b/edgelet/edgelet-http-mgmt/src/server/system_info/support_bundle.rs new file mode 100644 index 00000000000..e9e8905a43e --- /dev/null +++ b/edgelet/edgelet-http-mgmt/src/server/system_info/support_bundle.rs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft. All rights reserved. +use std::io::Read; + +use failure::ResultExt; +use futures::{Async, Future, Poll, Stream}; +use hyper::header::{CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE}; +use hyper::{Body, Request, Response, StatusCode}; +use log::debug; +use serde::Serialize; +use url::form_urlencoded; + +use edgelet_core::{parse_since, LogOptions, Module, ModuleRuntime, RuntimeOperation}; +use edgelet_http::route::{Handler, Parameters}; +use edgelet_http::Error as HttpError; +use support_bundle::{make_bundle, OutputLocation}; + +use crate::error::{Error, ErrorKind}; +use crate::IntoResponse; + +pub struct GetSupportBundle { + runtime: M, +} + +impl GetSupportBundle { + pub fn new(runtime: M) -> Self { + Self { runtime } + } +} + +impl Handler for GetSupportBundle +where + M: 'static + ModuleRuntime + Send + Clone + Sync, + ::Config: Serialize, +{ + fn handle( + &self, + req: Request, + _params: Parameters, + ) -> Box, Error = HttpError> + Send> { + debug!("Get Support Bundle"); + + let query = req.uri().query().unwrap_or(""); + + let response = get_bundle(self.runtime.clone(), query) + .and_then(|(bundle, size)| -> Result<_, Error> { + let body = Body::wrap_stream(ReadStream(bundle)); + + let response = Response::builder() + .status(StatusCode::OK) + .header(CONTENT_TYPE, "application/zip") + .header(CONTENT_ENCODING, "zip") + .header(CONTENT_LENGTH, size.to_string().as_str()) + .body(body) + .context(ErrorKind::RuntimeOperation( + RuntimeOperation::GetSupportBundle, + ))?; + Ok(response) + }) + .or_else(|e| Ok(e.into_response())); + + Box::new(response) + } +} + +fn get_bundle( + runtime: M, + query: &str, +) -> Box, u64), Error = Error> + Send> +where + M: 'static + ModuleRuntime + Send + Clone + Sync, +{ + let parse: Vec<_> = form_urlencoded::parse(query.as_bytes()).collect(); + + let mut log_options = LogOptions::new(); + if let Some((_, since)) = parse.iter().find(|&(ref key, _)| key == "since") { + if let Ok(since) = parse_since(since) { + log_options = log_options.with_since(since); + } + } + + let iothub_hostname = parse.iter().find_map(|(ref key, iothub_hostname)| { + if key == "iothub_hostname" { + Some(iothub_hostname.to_string()) + } else { + None + } + }); + + let edge_runtime_only = parse + .iter() + .find_map(|(ref key, edge_runtime_only)| { + if key == "edge_runtime_only" { + Some(edge_runtime_only == "true" || edge_runtime_only == "True") + } else { + None + } + }) + .unwrap_or_default(); + + let result = make_bundle( + OutputLocation::Memory, + log_options, + edge_runtime_only, + false, + iothub_hostname, + runtime, + ) + .map_err(|_| Error::from(ErrorKind::SupportBundle)); + + Box::new(result) +} + +struct ReadStream(Box); + +impl Stream for ReadStream { + type Item = Vec; + type Error = Box; + + fn poll(&mut self) -> Poll, Self::Error> { + let mut part: Vec = vec![0; 1024]; + let size = self.0.read(&mut part)?; + if size > 0 { + part.resize(size, 0); + Ok(Async::Ready(Some(part))) + } else { + Ok(Async::Ready(None)) + } + } +} diff --git a/edgelet/edgelet-http/src/version.rs b/edgelet/edgelet-http/src/version.rs index 02695d865a2..b000f473d00 100644 --- a/edgelet/edgelet-http/src/version.rs +++ b/edgelet/edgelet-http/src/version.rs @@ -3,7 +3,7 @@ use std::fmt; use std::str::FromStr; -pub const API_VERSION: Version = Version::Version2019_11_05; +pub const API_VERSION: Version = Version::Version2020_07_07; #[derive(Clone, Copy, Debug, PartialOrd, PartialEq)] pub enum Version { @@ -11,6 +11,7 @@ pub enum Version { Version2019_01_30, Version2019_10_22, Version2019_11_05, + Version2020_07_07, } impl FromStr for Version { @@ -22,6 +23,7 @@ impl FromStr for Version { "2019-01-30" => Ok(Version::Version2019_01_30), "2019-10-22" => Ok(Version::Version2019_10_22), "2019-11-05" => Ok(Version::Version2019_11_05), + "2020-07-07" => Ok(Version::Version2020_07_07), _ => Err(()), } } @@ -34,6 +36,7 @@ impl fmt::Display for Version { Version::Version2019_01_30 => write!(f, "2019-01-30"), Version::Version2019_10_22 => write!(f, "2019-10-22"), Version::Version2019_11_05 => write!(f, "2019-11-05"), + Version::Version2020_07_07 => write!(f, "2020-07-07"), } } } diff --git a/edgelet/edgelet-kube/src/runtime.rs b/edgelet/edgelet-kube/src/runtime.rs index 6e8908d2f18..d7914478330 100644 --- a/edgelet/edgelet-kube/src/runtime.rs +++ b/edgelet/edgelet-kube/src/runtime.rs @@ -231,17 +231,29 @@ where }) .collect::>(); - SystemInfo::new( - "Kubernetes".to_string(), - serde_json::to_string(&architectures).unwrap(), - ) + let not_supported = "Not supported on k8s".to_string(); + SystemInfo { + os_type: "Kubernetes".to_string(), + architecture: serde_json::to_string(&architectures).unwrap(), + version: edgelet_core::version_with_source_version(), + cpus: 0, + kernel_version: not_supported.clone(), + operating_system: not_supported.clone(), + server_version: not_supported, + } }), ) } else { - future::Either::B(future::ok(SystemInfo::new( - "Kubernetes".to_string(), - "Kubernetes".to_string(), - ))) + let not_supported = "Not supported on k8s".to_string(); + future::Either::B(future::ok(SystemInfo { + os_type: "Kubernetes".to_string(), + architecture: "Kubernetes".to_string(), + version: edgelet_core::version_with_source_version(), + cpus: 0, + kernel_version: not_supported.clone(), + operating_system: not_supported.clone(), + server_version: not_supported, + })) }; Box::new(fut) } @@ -409,7 +421,7 @@ mod tests { let info = runtime.block_on(task).unwrap(); assert_eq!( - info.architecture(), + info.architecture, "[{\"name\":\"amd64\",\"nodes_count\":2}]" ); } @@ -432,7 +444,7 @@ mod tests { let mut runtime = Runtime::new().unwrap(); let info = runtime.block_on(task).unwrap(); - assert_eq!(info.architecture(), "Kubernetes"); + assert_eq!(info.architecture, "Kubernetes"); } #[test] @@ -454,7 +466,7 @@ mod tests { let info = runtime.block_on(task).unwrap(); assert_eq!( - info.architecture(), + info.architecture, "[{\"name\":\"amd64\",\"nodes_count\":2}]" ); } diff --git a/edgelet/edgelet-test-utils/src/module.rs b/edgelet/edgelet-test-utils/src/module.rs index 5b5fbecd269..4e6bd7adb43 100644 --- a/edgelet/edgelet-test-utils/src/module.rs +++ b/edgelet/edgelet-test-utils/src/module.rs @@ -1,5 +1,3 @@ -// Copyright (c) Microsoft. All rights reserved. - use std::marker::PhantomData; use std::path::Path; use std::time::Duration; @@ -389,10 +387,15 @@ where fn system_info(&self) -> Self::SystemInfoFuture { match self.module.as_ref().unwrap() { - Ok(_) => future::ok(SystemInfo::new( - "os_type_sample".to_string(), - "architecture_sample".to_string(), - )), + Ok(_) => future::ok(SystemInfo { + os_type: "os_type_sample".to_string(), + architecture: "architecture_sample".to_string(), + version: edgelet_core::version_with_source_version(), + cpus: 0, + kernel_version: "test".to_string(), + operating_system: "test".to_string(), + server_version: "test".to_string(), + }), Err(ref e) => future::err(e.clone()), } } diff --git a/edgelet/iotedge/Cargo.toml b/edgelet/iotedge/Cargo.toml index fad4dfa1930..91ce08c7417 100644 --- a/edgelet/iotedge/Cargo.toml +++ b/edgelet/iotedge/Cargo.toml @@ -6,7 +6,6 @@ description = """ The iotedge tool is used to manage the IoT Edge runtime. """ edition = "2018" - [dependencies] atty = "0.2" bytes = "0.4" @@ -36,6 +35,7 @@ edgelet-http = { path = "../edgelet-http" } edgelet-http-mgmt = { path = "../edgelet-http-mgmt" } management = { path = "../management" } mini-sntp = { path = "../mini-sntp" } +support-bundle = { path = "../support-bundle" } [target.'cfg(unix)'.dependencies] byte-unit = "3.0.3" @@ -48,3 +48,4 @@ winapi = { version = "0.3", features = ["ntdef", "ntstatus", "winnt", "winsock2" [dev-dependencies] edgelet-test-utils = { path = "../edgelet-test-utils" } tempfile = "3.1.0" + diff --git a/edgelet/iotedge/src/lib.rs b/edgelet/iotedge/src/lib.rs index bdad17484a5..021d0dd68bc 100644 --- a/edgelet/iotedge/src/lib.rs +++ b/edgelet/iotedge/src/lib.rs @@ -31,7 +31,7 @@ pub use crate::error::{Error, ErrorKind, FetchLatestVersionsReason}; pub use crate::list::List; pub use crate::logs::Logs; pub use crate::restart::Restart; -pub use crate::support_bundle::{OutputLocation, SupportBundle}; +pub use crate::support_bundle::SupportBundleCommand; pub use crate::unknown::Unknown; pub use crate::version::Version; diff --git a/edgelet/iotedge/src/logs.rs b/edgelet/iotedge/src/logs.rs index 29d49e07996..9139752ea9b 100644 --- a/edgelet/iotedge/src/logs.rs +++ b/edgelet/iotedge/src/logs.rs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. -use std::io::{self, Write}; +use std::io::stdout; -use failure::Fail; use futures::prelude::*; -use edgelet_core::{Chunked, LogChunk, LogDecode, LogOptions, ModuleRuntime}; +use edgelet_core::{LogOptions, ModuleRuntime}; +use support_bundle::pull_logs; use crate::error::{Error, ErrorKind}; use crate::Command; @@ -34,39 +34,9 @@ where fn execute(self) -> Self::Future { let id = self.id.clone(); - let result = pull_logs(&self.runtime, &id, &self.options, io::stdout()).map(drop); + let result = pull_logs(&self.runtime, &id, &self.options, stdout()) + .map_err(|_| Error::from(ErrorKind::ModuleRuntime)) + .map(drop); Box::new(result) } } - -pub fn pull_logs( - runtime: &M, - id: &str, - options: &LogOptions, - writer: W, -) -> impl Future + Send -where - M: 'static + ModuleRuntime, - W: Write + Send, -{ - runtime - .logs(id, options) - .map_err(|err| Error::from(err.context(ErrorKind::ModuleRuntime))) - .and_then(move |logs| { - let chunked = - Chunked::new(logs.map_err(|_| io::Error::new(io::ErrorKind::Other, "unknown"))); - LogDecode::new(chunked) - .map_err(|err| Error::from(err.context(ErrorKind::ModuleRuntime))) - .fold(writer, |mut w, chunk| -> Result { - match chunk { - LogChunk::Stdin(b) - | LogChunk::Stdout(b) - | LogChunk::Stderr(b) - | LogChunk::Unknown(b) => w - .write(&b) - .map_err(|err| Error::from(err.context(ErrorKind::WriteToStdout)))?, - }; - Ok(w) - }) - }) -} diff --git a/edgelet/iotedge/src/main.rs b/edgelet/iotedge/src/main.rs index 1e570daf5da..fd24a0ecb77 100644 --- a/edgelet/iotedge/src/main.rs +++ b/edgelet/iotedge/src/main.rs @@ -16,10 +16,11 @@ use url::Url; use edgelet_core::{parse_since, LogOptions, LogTail}; use edgelet_http_mgmt::ModuleClient; +use support_bundle::OutputLocation; use iotedge::{ - Check, Command, Error, ErrorKind, List, Logs, OutputFormat, OutputLocation, Restart, - SupportBundle, Unknown, Version, + Check, Command, Error, ErrorKind, List, Logs, OutputFormat, Restart, SupportBundleCommand, + Unknown, Version, }; fn main() { @@ -388,13 +389,13 @@ fn run() -> Result<(), Error> { let verbose = !args.is_present("quiet"); let iothub_hostname = args.value_of("iothub-hostname").map(ToOwned::to_owned); let output_location = if location == "-" { - OutputLocation::Console + OutputLocation::Memory } else { OutputLocation::File(location.to_owned()) }; tokio_runtime.block_on( - SupportBundle::new( + SupportBundleCommand::new( options, include_ms_only, verbose, diff --git a/edgelet/iotedge/src/support_bundle.rs b/edgelet/iotedge/src/support_bundle.rs index 1347572d6ed..a492646470a 100644 --- a/edgelet/iotedge/src/support_bundle.rs +++ b/edgelet/iotedge/src/support_bundle.rs @@ -1,25 +1,18 @@ // Copyright (c) Microsoft. All rights reserved. -use std::env; -use std::ffi::OsString; -use std::fs::File; -use std::io::{stdout, Cursor, Seek}; -use std::path::{Path, PathBuf}; -use std::process::Command as ShellCommand; +use std::io::{copy, stdout}; +use std::path::PathBuf; -use chrono::{DateTime, Local, NaiveDateTime, Utc}; use failure::Fail; -use futures::{Future, Stream}; -use tokio::prelude::*; -use zip::{write::FileOptions, CompressionMethod, ZipWriter}; +use futures::Future; -use edgelet_core::{LogOptions, LogTail, Module, ModuleRuntime}; +use edgelet_core::{LogOptions, ModuleRuntime}; +use support_bundle::{make_bundle, OutputLocation}; use crate::error::{Error, ErrorKind}; -use crate::logs::pull_logs; use crate::Command; -pub struct SupportBundle { +pub struct SupportBundleCommand { runtime: M, log_options: LogOptions, include_ms_only: bool, @@ -28,56 +21,7 @@ pub struct SupportBundle { output_location: OutputLocation, } -struct BundleState -where - W: Write + Seek + Send, -{ - runtime: M, - log_options: LogOptions, - include_ms_only: bool, - verbose: bool, - iothub_hostname: Option, - file_options: FileOptions, - zip_writer: ZipWriter, -} - -impl Command for SupportBundle -where - M: 'static + ModuleRuntime + Clone + Send + Sync, -{ - type Future = Box + Send>; - - fn execute(self) -> Self::Future { - println!("Making support bundle"); - - match self.output_location.clone() { - OutputLocation::File(location) => Box::new( - Self::bundle_all(future::result(self.make_file_state())).map(|_state| { - let path = PathBuf::from(location); - println!( - "Created support bundle at {}", - path.canonicalize().unwrap_or_else(|_| path).display() - ); - }), - ), - OutputLocation::Console => Box::new( - Self::bundle_all(future::result(self.make_vector_state())).and_then(|mut state| { - stdout() - .write_all( - state - .zip_writer - .finish() - .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))? - .get_ref(), - ) - .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle))) - }), - ), - } - } -} - -impl SupportBundle +impl SupportBundleCommand where M: 'static + ModuleRuntime + Clone + Send + Sync, { @@ -89,7 +33,7 @@ where output_location: OutputLocation, runtime: M, ) -> Self { - SupportBundle { + Self { runtime, log_options, include_ms_only, @@ -98,751 +42,49 @@ where output_location, } } - - fn make_file_state(self) -> Result, Error> { - let writer = File::create(Path::new(self.output_location.get_file_location())) - .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; - - self.make_state(writer) - } - - fn make_vector_state(self) -> Result>>, Error> { - self.make_state(Cursor::new(Vec::new())) - } - - fn make_state(self, writer: W) -> Result, Error> - where - W: Write + Seek + Send, - { - let file_options = FileOptions::default().compression_method(CompressionMethod::Deflated); - let zip_writer = ZipWriter::new(writer); - - Ok(BundleState { - runtime: self.runtime, - log_options: self.log_options, - include_ms_only: self.include_ms_only, - verbose: self.verbose, - iothub_hostname: self.iothub_hostname, - file_options, - zip_writer, - }) - } - - fn bundle_all(state: S) -> impl Future, Error = Error> - where - S: Future, Error = Error>, - W: Write + Seek + Send, - { - state - .and_then(Self::write_check) - .and_then(Self::write_module_logs) - .and_then(Self::write_edgelet_log) - .and_then(Self::write_docker_log) - .and_then(Self::write_all_inspects) - .and_then(Self::write_all_network_inspects) - } - - fn write_module_logs( - state: BundleState, - ) -> impl Future, Error = Error> - where - W: Write + Seek + Send, - { - /* Print status */ - if state.verbose { - let since_time: DateTime = DateTime::from_utc( - NaiveDateTime::from_timestamp(state.log_options.since().into(), 0), - Utc, - ); - let since_local: DateTime = DateTime::from(since_time); - let max_lines = if let LogTail::Num(tail) = state.log_options.tail() { - format!("(maximum {} lines) ", tail) - } else { - "".to_owned() - }; - println!( - "Writing all logs {}since {} (local time {})", - max_lines, since_time, since_local - ); - } - - SupportBundle::get_modules(state).and_then(|(names, s2)| { - stream::iter_ok(names).fold(s2, SupportBundle::write_log_to_file) - }) - } - - fn write_all_inspects( - s1: BundleState, - ) -> impl Future, Error = Error> - where - W: Write + Seek + Send, - { - SupportBundle::get_modules(s1).and_then(|(names, s2)| { - stream::iter_ok(names).fold(s2, |s3, name| { - SupportBundle::write_inspect_to_file(s3, &name) - }) - }) - } - - fn write_all_network_inspects(s1: BundleState) -> Result, Error> - where - W: Write + Seek + Send, - { - SupportBundle::get_docker_networks(s1).and_then(|(names, s2)| { - names.into_iter().fold(Ok(s2), |s3, name| { - if let Ok(s3) = s3 { - SupportBundle::write_docker_network_to_file(s3, &name) - } else { - s3 - } - }) - }) - } - - fn get_modules( - state: BundleState, - ) -> impl Future, BundleState), Error = Error> - where - W: Write + Seek + Send, - { - const MS_MODULES: &[&str] = &["edgeAgent", "edgeHub"]; - - let include_ms_only = state.include_ms_only; - - state - .runtime - .list_with_details() - .map_err(|err| Error::from(err.context(ErrorKind::ModuleRuntime))) - .map(|(module, _s)| module.name().to_owned()) - .filter(move |name| !include_ms_only || MS_MODULES.iter().any(|ms| ms == name)) - .collect() - .map(|names| (names, state)) - } - - fn write_log_to_file( - state: BundleState, - module_name: String, - ) -> impl Future, Error = Error> - where - W: Write + Seek + Send, - { - state.print_verbose(&format!("Writing {} logs to file", module_name)); - let BundleState { - runtime, - log_options, - include_ms_only, - verbose, - iothub_hostname, - file_options, - mut zip_writer, - } = state; - - let file_name = format!("{}_log.txt", module_name); - zip_writer - .start_file_from_path(&Path::new("logs").join(file_name), file_options) - .into_future() - .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle))) - .and_then(move |_| { - pull_logs(&runtime, &module_name, &log_options, zip_writer).map(move |zw| { - let state = BundleState { - runtime, - log_options, - include_ms_only, - verbose, - iothub_hostname, - file_options, - zip_writer: zw, - }; - state.print_verbose(&format!("Wrote {} logs to file", module_name)); - state - }) - }) - } - - fn write_edgelet_log(mut state: BundleState) -> Result, Error> - where - W: Write + Seek + Send, - { - state.print_verbose("Getting system logs for iotedged"); - let since_time: DateTime = DateTime::from_utc( - NaiveDateTime::from_timestamp(state.log_options.since().into(), 0), - Utc, - ); - - #[cfg(unix)] - let inspect = ShellCommand::new("journalctl") - .arg("-a") - .args(&["-u", "iotedge"]) - .args(&["-S", &since_time.format("%F %T").to_string()]) - .arg("--no-pager") - .output(); - - #[cfg(windows)] - let inspect = ShellCommand::new("powershell.exe") - .arg("-NoProfile") - .arg("-Command") - .arg(&format!(r"Get-WinEvent -ea SilentlyContinue -FilterHashtable @{{ProviderName='iotedged';LogName='application';StartTime='{}'}} | - Select TimeCreated, Message | - Sort-Object @{{Expression='TimeCreated';Descending=$false}} | - Format-List", since_time.to_rfc3339())) - .output(); - - let (file_name, output) = if let Ok(result) = inspect { - if result.status.success() { - ("iotedged.txt", result.stdout) - } else { - ("iotedged_err.txt", result.stderr) - } - } else { - let err_message = inspect.err().unwrap().to_string(); - println!("Could not find system logs for iotedge. Including error in bundle.\nError message: {}", err_message); - ("iotedged_err.txt", err_message.as_bytes().to_vec()) - }; - - state - .zip_writer - .start_file_from_path(&Path::new("logs").join(file_name), state.file_options) - .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; - - state - .zip_writer - .write_all(&output) - .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; - - state.print_verbose("Got logs for iotedged"); - Ok(state) - } - - fn write_docker_log(mut state: BundleState) -> Result, Error> - where - W: Write + Seek + Send, - { - state.print_verbose("Getting system logs for docker"); - let since_time: DateTime = DateTime::from_utc( - NaiveDateTime::from_timestamp(state.log_options.since().into(), 0), - Utc, - ); - - #[cfg(unix)] - let inspect = ShellCommand::new("journalctl") - .arg("-a") - .args(&["-u", "docker"]) - .args(&["-S", &since_time.format("%F %T").to_string()]) - .arg("--no-pager") - .output(); - - /* from https://docs.microsoft.com/en-us/virtualization/windowscontainers/troubleshooting#finding-logs */ - #[cfg(windows)] - let inspect = ShellCommand::new("powershell.exe") - .arg("-NoProfile") - .arg("-Command") - .arg(&format!( - r#"Get-EventLog -LogName Application -Source Docker -After "{}" | - Sort-Object Time | - Format-List"#, - since_time.to_rfc3339() - )) - .output(); - - let (file_name, output) = if let Ok(result) = inspect { - if result.status.success() { - ("docker.txt", result.stdout) - } else { - ("docker_err.txt", result.stderr) - } - } else { - let err_message = inspect.err().unwrap().to_string(); - println!("Could not find system logs for docker. Including error in bundle.\nError message: {}", err_message); - ("docker_err.txt", err_message.as_bytes().to_vec()) - }; - - state - .zip_writer - .start_file_from_path(&Path::new("logs").join(file_name), state.file_options) - .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; - - state - .zip_writer - .write_all(&output) - .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; - - state.print_verbose("Got logs for docker"); - Ok(state) - } - - fn write_check(mut state: BundleState) -> Result, Error> - where - W: Write + Seek + Send, - { - let iotedge = env::args().next().unwrap(); - state.print_verbose("Calling iotedge check"); - - let mut check = ShellCommand::new(iotedge); - check.arg("check").args(&["-o", "json"]); - - if let Some(host_name) = state.iothub_hostname.clone() { - check.args(&["--iothub-hostname", &host_name]); - } - let check = check - .output() - .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; - - state - .zip_writer - .start_file_from_path(&Path::new("check.json"), state.file_options) - .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; - - state - .zip_writer - .write_all(&check.stdout) - .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; - - state.print_verbose("Wrote check output to file"); - Ok(state) - } - - fn write_inspect_to_file( - mut state: BundleState, - module_name: &str, - ) -> Result, Error> - where - W: Write + Seek + Send, - { - state.print_verbose(&format!("Running docker inspect for {}", module_name)); - let mut inspect = ShellCommand::new("docker"); - - /*** - * Note: this assumes using windows containers on a windows machine. - * This is the expected production scenario. - * Since the bundle command does not read the config.yaml, it cannot use the `moby.runtime_uri` from there. - * This will not fail the bundle, only note the failure to the user and in the bundle. - */ - #[cfg(windows)] - inspect.args(&["-H", "npipe:////./pipe/iotedge_moby_engine"]); - - inspect.arg("inspect").arg(&module_name); - let inspect = inspect.output(); - - let (file_name, output) = if let Ok(result) = inspect { - if result.status.success() { - (format!("inspect/{}.json", module_name), result.stdout) - } else { - (format!("inspect/{}_err.json", module_name), result.stderr) - } - } else { - let err_message = inspect.err().unwrap().to_string(); - println!( - "Could not reach docker. Including error in bundle.\nError message: {}", - err_message - ); - ( - format!("inspect/{}_err_docker.txt", module_name), - err_message.as_bytes().to_vec(), - ) - }; - - state - .zip_writer - .start_file_from_path(&Path::new(&file_name), state.file_options) - .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; - - state - .zip_writer - .write_all(&output) - .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; - - state.print_verbose(&format!("Got docker inspect for {}", module_name)); - Ok(state) - } - - fn get_docker_networks( - state: BundleState, - ) -> Result<(Vec, BundleState), Error> - where - W: Write + Seek + Send, - { - let mut inspect = ShellCommand::new("docker"); - - /*** - * Note: just like inspect, this assumes using windows containers on a windows machine. - */ - #[cfg(windows)] - inspect.args(&["-H", "npipe:////./pipe/iotedge_moby_engine"]); - - inspect.args(&["network", "ls"]); - inspect.args(&["--format", "{{.Name}}"]); - let inspect = inspect.output(); - - let result = if let Ok(result) = inspect { - if result.status.success() { - String::from_utf8_lossy(&result.stdout).to_string() - } else { - println!( - "Could not find network names: {}", - String::from_utf8_lossy(&result.stderr) - ); - "azure-iot-edge".to_owned() - } - } else { - println!("Could not find network names: {}", inspect.err().unwrap()); - "azure-iot-edge".to_owned() - }; - - Ok((result.lines().map(String::from).collect(), state)) - } - - fn write_docker_network_to_file( - mut state: BundleState, - network_name: &str, - ) -> Result, Error> - where - W: Write + Seek + Send, - { - state.print_verbose(&format!( - "Running docker network inspect for {}", - network_name - )); - let mut inspect = ShellCommand::new("docker"); - - /*** - * Note: just like inspect, this assumes using windows containers on a windows machine. - */ - #[cfg(windows)] - inspect.args(&["-H", "npipe:////./pipe/iotedge_moby_engine"]); - - inspect.args(&["network", "inspect", &network_name, "-v"]); - let inspect = inspect.output(); - - let (file_name, output) = if let Ok(result) = inspect { - if result.status.success() { - (format!("network/{}.json", network_name), result.stdout) - } else { - (format!("network/{}_err.json", network_name), result.stderr) - } - } else { - let err_message = inspect.err().unwrap().to_string(); - println!( - "Could not reach docker. Including error in bundle.\nError message: {}", - err_message - ); - ( - format!("network/{}_err_docker.txt", network_name), - err_message.as_bytes().to_vec(), - ) - }; - - state - .zip_writer - .start_file_from_path(&Path::new(&file_name), state.file_options) - .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; - - state - .zip_writer - .write_all(&output) - .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; - - state.print_verbose(&format!("Got docker network inspect for {}", network_name)); - Ok(state) - } } -impl BundleState +impl Command for SupportBundleCommand where - W: Write + Seek + Send, + M: 'static + ModuleRuntime + Clone + Send + Sync, { - fn print_verbose(&self, message: &str) { - if self.verbose { - println!("{}", message); - } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum OutputLocation { - File(OsString), - Console, -} - -impl OutputLocation { - fn get_file_location(&self) -> &OsString { - if let Self::File(location) = self { - location - } else { - panic!("Cannot get file location for console mode"); - } - } -} - -#[cfg(test)] -mod tests { - use std::fs; - use std::io; - use std::path::PathBuf; - use std::str; - - use regex::Regex; - use tempfile::tempdir; - - use edgelet_core::{MakeModuleRuntime, ModuleRuntimeState}; - use edgelet_test_utils::crypto::TestHsm; - use edgelet_test_utils::module::{ - TestConfig, TestModule, TestProvisioningResult, TestRuntime, TestSettings, - }; - - use super::{ - pull_logs, Command, Fail, File, Future, LogOptions, LogTail, OsString, OutputLocation, - SupportBundle, - }; - - #[allow(dead_code)] - #[derive(Clone, Copy, Debug, Fail)] - pub enum Error { - #[fail(display = "General error")] - General, - } - - #[test] - fn folder_structure() { - let module_name = "test-module"; - let runtime = make_runtime(module_name); - let tmp_dir = tempdir().unwrap(); - let file_path = tmp_dir - .path() - .join("iotedge_bundle.zip") - .to_str() - .unwrap() - .to_owned(); - - let bundle = SupportBundle::new( - LogOptions::default(), - false, - false, - None, - OutputLocation::File(OsString::from(file_path.to_owned())), - runtime, - ); - - bundle.execute().wait().unwrap(); - - let extract_path = tmp_dir.path().join("bundle").to_str().unwrap().to_owned(); - - extract_zip(&file_path, &extract_path); - - // expect logs - let mod_log = fs::read_to_string( - PathBuf::from(&extract_path) - .join("logs") - .join(format!("{}_log.txt", module_name)), - ) - .unwrap(); - assert_eq!("Roses are redviolets are blue", mod_log); - - let iotedged_log = Regex::new(r"iotedged.*\.txt").unwrap(); - assert!(fs::read_dir(PathBuf::from(&extract_path).join("logs")) - .unwrap() - .map(|file| file - .unwrap() - .path() - .file_name() - .unwrap() - .to_str() - .unwrap() - .to_owned()) - .any(|f| iotedged_log.is_match(&f))); - - let docker_log = Regex::new(r"docker.*\.txt").unwrap(); - assert!(fs::read_dir(PathBuf::from(&extract_path).join("logs")) - .unwrap() - .map(|file| file - .unwrap() - .path() - .file_name() - .unwrap() - .to_str() - .unwrap() - .to_owned()) - .any(|f| docker_log.is_match(&f))); - - //expect inspect - let module_in_inspect = Regex::new(&format!(r"{}.*\.json", module_name)).unwrap(); - assert!(fs::read_dir(PathBuf::from(&extract_path).join("inspect")) - .unwrap() - .map(|file| file - .unwrap() - .path() - .file_name() - .unwrap() - .to_str() - .unwrap() - .to_owned()) - .any(|f| module_in_inspect.is_match(&f))); - - // expect check - File::open(PathBuf::from(&extract_path).join("check.json")).unwrap(); - - // expect network inspect - let network_in_inspect = Regex::new(r".*\.json").unwrap(); - assert!(fs::read_dir(PathBuf::from(&extract_path).join("network")) - .unwrap() - .map(|file| file - .unwrap() - .path() - .file_name() - .unwrap() - .to_str() - .unwrap() - .to_owned()) - .any(|f| network_in_inspect.is_match(&f))); - } - - #[test] - fn get_logs() { - let module_name = "test-module"; - let runtime = make_runtime(module_name); - - let options = LogOptions::new() - .with_follow(false) - .with_tail(LogTail::Num(0)) - .with_since(0); - - let result: Vec = pull_logs(&runtime, module_name, &options, Vec::new()) - .wait() - .unwrap(); - let result_str = str::from_utf8(&result).unwrap(); - assert_eq!("Roses are redviolets are blue", result_str); - } - - #[test] - fn get_modules() { - let runtime = make_runtime("test-module"); - let tmp_dir = tempdir().unwrap(); - let file_path = tmp_dir - .path() - .join("iotedge_bundle.zip") - .to_str() - .unwrap() - .to_owned(); - let bundle = SupportBundle::new( - LogOptions::default(), - false, - true, - None, - OutputLocation::File(OsString::from(file_path.to_owned())), - runtime, - ); - - let state = bundle.make_file_state().unwrap(); - - let (modules, mut state) = SupportBundle::get_modules(state).wait().unwrap(); - assert_eq!(modules.len(), 1); - - state.include_ms_only = true; - - let (modules, _state) = SupportBundle::get_modules(state).wait().unwrap(); - assert_eq!(modules.len(), 0); - - /* with edge agent */ - let runtime = make_runtime("edgeAgent"); - let bundle = SupportBundle::new( - LogOptions::default(), - false, - true, - None, - OutputLocation::File(OsString::from(file_path)), - runtime, - ); - - let state = bundle.make_file_state().unwrap(); - - let (modules, mut state) = SupportBundle::get_modules(state).wait().unwrap(); - assert_eq!(modules.len(), 1); - - state.include_ms_only = true; - - let (modules, _state) = SupportBundle::get_modules(state).wait().unwrap(); - assert_eq!(modules.len(), 1); - } + type Future = Box + Send>; - #[test] - fn write_logs_to_file() { - let runtime = make_runtime("test-module"); - let tmp_dir = tempdir().unwrap(); - let file_path = tmp_dir - .path() - .join("iotedge_bundle.zip") - .to_str() - .unwrap() - .to_owned(); + fn execute(self) -> Self::Future { + println!("Making support bundle"); - let bundle = SupportBundle::new( - LogOptions::default(), - false, - true, - None, - OutputLocation::File(OsString::from(file_path.to_owned())), - runtime, + let output_location = self.output_location.clone(); + let bundle = make_bundle( + self.output_location, + self.log_options, + self.include_ms_only, + self.verbose, + self.iothub_hostname, + self.runtime, ); - bundle.execute().wait().unwrap(); - - File::open(file_path).unwrap(); - } - - fn make_runtime(module_name: &str) -> TestRuntime { - let logs = vec![ - &[0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, b'R', b'o'][..], - &b"ses are"[..], - &[b' ', b'r', b'e', b'd', 0x02, 0x00][..], - &[0x00, 0x00, 0x00, 0x00, 0x00, 0x10][..], - &b"violets"[..], - &b" are blue"[..], - ]; - - let state: Result = Ok(ModuleRuntimeState::default()); - let config = TestConfig::new(format!("microsoft/{}", module_name)); - let module = TestModule::new_with_logs(module_name.to_owned(), config, state, logs); - - TestRuntime::make_runtime( - TestSettings::new(), - TestProvisioningResult::new(), - TestHsm::default(), - ) - .wait() - .unwrap() - .with_module(Ok(module)) - } - - // From https://github.com/mvdnes/zip-rs/blob/master/examples/extract.rs - fn extract_zip(source: &str, destination: &str) { - let fname = std::path::Path::new(source); - let file = File::open(&fname).unwrap(); - let mut archive = zip::ZipArchive::new(file).unwrap(); - - for i in 0..archive.len() { - let mut file = archive.by_index(i).unwrap(); - let outpath = PathBuf::from(destination).join(file.sanitized_name()); + let result = bundle + .map_err(|_| Error::from(ErrorKind::SupportBundle)) + .and_then(|(mut bundle, _size)| -> Result<(), Error> { + match output_location { + OutputLocation::File(location) => { + let path = PathBuf::from(location); + println!( + "Created support bundle at {}", + path.canonicalize().unwrap_or_else(|_| path).display() + ); + + Ok(()) + } + OutputLocation::Memory => { + copy(&mut bundle, &mut stdout()) + .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; - if (&*file.name()).ends_with('/') { - fs::create_dir_all(&outpath).unwrap(); - } else { - if let Some(p) = outpath.parent() { - if !p.exists() { - fs::create_dir_all(&p).unwrap(); + Ok(()) } } - let mut outfile = fs::File::create(&outpath).unwrap(); - io::copy(&mut file, &mut outfile).unwrap(); - } - - // Get and Set permissions - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; + }); - if let Some(mode) = file.unix_mode() { - fs::set_permissions(&outpath, fs::Permissions::from_mode(mode)).unwrap(); - } - } - } + Box::new(result) } } diff --git a/edgelet/support-bundle/Cargo.toml b/edgelet/support-bundle/Cargo.toml new file mode 100644 index 00000000000..f42f58068b1 --- /dev/null +++ b/edgelet/support-bundle/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "support-bundle" +version = "0.1.0" +authors = ["Lee Fitchett "] +edition = "2018" + +[dependencies] +chrono = "0.4.7" +failure = "0.1" +futures = "0.1" +tokio = "0.1" +zip = "0.5.3" + +edgelet-core = { path = "../edgelet-core" } + +[dev-dependencies] +edgelet-test-utils = { path = "../edgelet-test-utils" } +regex = "0.2" +tempfile = "3.1.0" diff --git a/edgelet/support-bundle/src/error.rs b/edgelet/support-bundle/src/error.rs new file mode 100644 index 00000000000..9e7e4f3f9fc --- /dev/null +++ b/edgelet/support-bundle/src/error.rs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +use std::fmt; +use std::fmt::Display; + +use failure::{Backtrace, Context, Fail}; + +#[derive(Debug)] +pub struct Error { + inner: Context, +} + +#[derive(Clone, Debug, Fail)] +pub enum ErrorKind { + #[fail(display = "A module runtime error occurred")] + ModuleRuntime, + + #[fail(display = "Could not generate support bundle")] + SupportBundle, + + #[fail(display = "Could not write")] + Write, +} + +impl Fail for Error { + fn cause(&self) -> Option<&dyn Fail> { + self.inner.cause() + } + + fn backtrace(&self) -> Option<&Backtrace> { + self.inner.backtrace() + } +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&self.inner, f) + } +} + +impl From for Error { + fn from(kind: ErrorKind) -> Self { + Error { + inner: Context::new(kind), + } + } +} + +impl From> for Error { + fn from(inner: Context) -> Self { + Error { inner } + } +} diff --git a/edgelet/support-bundle/src/lib.rs b/edgelet/support-bundle/src/lib.rs new file mode 100644 index 00000000000..040e8f96b33 --- /dev/null +++ b/edgelet/support-bundle/src/lib.rs @@ -0,0 +1,7 @@ +mod error; +mod logs; +mod support_bundle; + +pub use crate::error::{Error, ErrorKind}; +pub use crate::logs::pull_logs; +pub use crate::support_bundle::{make_bundle, OutputLocation}; diff --git a/edgelet/support-bundle/src/logs.rs b/edgelet/support-bundle/src/logs.rs new file mode 100644 index 00000000000..0f6b7178872 --- /dev/null +++ b/edgelet/support-bundle/src/logs.rs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. + +use std::io::{self, Write}; + +use failure::Fail; +use futures::prelude::*; + +use edgelet_core::{Chunked, LogChunk, LogDecode, LogOptions, ModuleRuntime}; + +use crate::error::{Error, ErrorKind}; + +pub fn pull_logs( + runtime: &M, + id: &str, + options: &LogOptions, + writer: W, +) -> impl Future + Send +where + M: 'static + ModuleRuntime, + W: Write + Send, +{ + runtime + .logs(id, options) + .map_err(|err| Error::from(err.context(ErrorKind::ModuleRuntime))) + .and_then(move |logs| { + let chunked = + Chunked::new(logs.map_err(|_| io::Error::new(io::ErrorKind::Other, "unknown"))); + LogDecode::new(chunked) + .map_err(|err| Error::from(err.context(ErrorKind::ModuleRuntime))) + .fold(writer, |mut w, chunk| -> Result { + match chunk { + LogChunk::Stdin(b) + | LogChunk::Stdout(b) + | LogChunk::Stderr(b) + | LogChunk::Unknown(b) => w + .write(&b) + .map_err(|err| Error::from(err.context(ErrorKind::Write)))?, + }; + Ok(w) + }) + }) +} diff --git a/edgelet/support-bundle/src/support_bundle.rs b/edgelet/support-bundle/src/support_bundle.rs new file mode 100644 index 00000000000..0772c91fd78 --- /dev/null +++ b/edgelet/support-bundle/src/support_bundle.rs @@ -0,0 +1,712 @@ +// Copyright (c) Microsoft. All rights reserved. + +use std::env; +use std::ffi::OsString; +use std::fs::File; +use std::io::{Cursor, Seek}; +use std::path::Path; +use std::process::Command as ShellCommand; + +use chrono::{DateTime, Local, NaiveDateTime, Utc}; +use failure::Fail; +use futures::{Future, Stream}; +use tokio::prelude::*; +use zip::{write::FileOptions, CompressionMethod, ZipWriter}; + +use edgelet_core::{LogOptions, LogTail, Module, ModuleRuntime}; + +use crate::error::{Error, ErrorKind}; +use crate::logs::pull_logs; + +pub fn make_bundle( + output_location: OutputLocation, + log_options: LogOptions, + include_ms_only: bool, + verbose: bool, + iothub_hostname: Option, + runtime: M, +) -> Box, u64), Error = Error> + Send> +where + M: 'static + ModuleRuntime + Clone + Send + Sync, +{ + match output_location { + OutputLocation::File(location) => { + let writer = future::result( + File::create(Path::new(&location)) + .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle))), + ); + + let state = writer.and_then(move |writer| { + make_state( + log_options, + include_ms_only, + verbose, + iothub_hostname, + runtime, + writer, + ) + }); + + let bundle = state.and_then(|state| state.bundle_all()); + + let read = + bundle.and_then(|mut bundle| -> Result<(Box, u64), Error> { + let result: Box = Box::new( + bundle + .zip_writer + .finish() + .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?, + ); + Ok((result, 0)) // TODO: Get Size + }); + + Box::new(read) + } + OutputLocation::Memory => { + let writer = future::ok(Cursor::new(Vec::new())); + + let state = writer.and_then(move |writer| { + make_state( + log_options, + include_ms_only, + verbose, + iothub_hostname, + runtime, + writer, + ) + }); + + let bundle = state.and_then(|state| state.bundle_all()); + + let read = + bundle.and_then(|mut bundle| -> Result<(Box, u64), Error> { + let mut cursor = bundle + .zip_writer + .finish() + .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; + let len = cursor.position(); + cursor.set_position(0); + let reader: Box = Box::new(cursor); + Ok((reader, len)) + }); + + Box::new(read) + } + } +} + +fn make_state( + log_options: LogOptions, + include_ms_only: bool, + verbose: bool, + iothub_hostname: Option, + runtime: M, + writer: W, +) -> Result, Error> +where + W: Write + Seek + Send, + M: 'static + ModuleRuntime + Clone + Send + Sync, +{ + let file_options = FileOptions::default().compression_method(CompressionMethod::Deflated); + let zip_writer = ZipWriter::new(writer); + + Ok(BundleState { + runtime, + log_options, + include_ms_only, + verbose, + iothub_hostname, + file_options, + zip_writer, + }) +} + +struct BundleState +where + W: Write + Seek + Send, +{ + runtime: M, + log_options: LogOptions, + include_ms_only: bool, + verbose: bool, + iothub_hostname: Option, + file_options: FileOptions, + zip_writer: ZipWriter, +} + +impl BundleState +where + M: 'static + ModuleRuntime + Clone + Send + Sync, + W: Write + Seek + Send, +{ + fn bundle_all(self) -> impl Future { + future::ok(self) + .and_then(Self::write_check) + .and_then(Self::write_module_logs) + .and_then(Self::write_edgelet_log) + .and_then(Self::write_docker_log) + .and_then(Self::write_all_inspects) + .and_then(Self::write_all_network_inspects) + } + + fn write_module_logs(self) -> impl Future { + /* Print status */ + if self.verbose { + let since_time: DateTime = DateTime::from_utc( + NaiveDateTime::from_timestamp(self.log_options.since().into(), 0), + Utc, + ); + let since_local: DateTime = DateTime::from(since_time); + let max_lines = if let LogTail::Num(tail) = self.log_options.tail() { + format!("(maximum {} lines) ", tail) + } else { + "".to_owned() + }; + println!( + "Writing all logs {}since {} (local time {})", + max_lines, since_time, since_local + ); + } + + self.get_modules() + .and_then(|(names, state)| stream::iter_ok(names).fold(state, Self::write_log_to_file)) + } + + fn write_all_inspects(self) -> impl Future { + self.get_modules().and_then(|(names, s2)| { + stream::iter_ok(names).fold(s2, |s3, name| s3.write_inspect_to_file(&name)) + }) + } + + fn write_all_network_inspects(self) -> Result { + self.get_docker_networks().and_then(|(names, s2)| { + names.into_iter().fold(Ok(s2), |s3, name| { + if let Ok(s3) = s3 { + s3.write_docker_network_to_file(&name) + } else { + s3 + } + }) + }) + } + + fn get_modules(self) -> impl Future, Self), Error = Error> { + const MS_MODULES: &[&str] = &["edgeAgent", "edgeHub"]; + + let include_ms_only = self.include_ms_only; + + self.runtime + .list_with_details() + .map_err(|err| Error::from(err.context(ErrorKind::ModuleRuntime))) + .map(|(module, _s)| module.name().to_owned()) + .filter(move |name| !include_ms_only || MS_MODULES.iter().any(|ms| ms == name)) + .collect() + .map(|names| (names, self)) + } + + fn write_log_to_file( + state: Self, + module_name: String, + ) -> impl Future { + state.print_verbose(&format!("Writing {} logs to file", module_name)); + let BundleState { + runtime, + log_options, + include_ms_only, + verbose, + iothub_hostname, + file_options, + mut zip_writer, + } = state; + + let file_name = format!("{}_log.txt", module_name); + zip_writer + .start_file_from_path(&Path::new("logs").join(file_name), file_options) + .into_future() + .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle))) + .and_then(move |_| { + pull_logs(&runtime, &module_name, &log_options, zip_writer).map(move |zw| { + let state = BundleState { + runtime, + log_options, + include_ms_only, + verbose, + iothub_hostname, + file_options, + zip_writer: zw, + }; + state.print_verbose(&format!("Wrote {} logs to file", module_name)); + state + }) + }) + } + + fn write_edgelet_log(mut self) -> Result { + self.print_verbose("Getting system logs for iotedged"); + let since_time: DateTime = DateTime::from_utc( + NaiveDateTime::from_timestamp(self.log_options.since().into(), 0), + Utc, + ); + + #[cfg(unix)] + let inspect = ShellCommand::new("journalctl") + .arg("-a") + .args(&["-u", "iotedge"]) + .args(&["-S", &since_time.format("%F %T").to_string()]) + .arg("--no-pager") + .output(); + + #[cfg(windows)] + let inspect = ShellCommand::new("powershell.exe") + .arg("-NoProfile") + .arg("-Command") + .arg(&format!(r"Get-WinEvent -ea SilentlyContinue -FilterHashtable @{{ProviderName='iotedged';LogName='application';StartTime='{}'}} | + Select TimeCreated, Message | + Sort-Object @{{Expression='TimeCreated';Descending=$false}} | + Format-List", since_time.to_rfc3339())) + .output(); + + let (file_name, output) = if let Ok(result) = inspect { + if result.status.success() { + ("iotedged.txt", result.stdout) + } else { + ("iotedged_err.txt", result.stderr) + } + } else { + let err_message = inspect.err().unwrap().to_string(); + println!( + "Could not find system logs for iotedge. Including error in bundle.\nError message: {}", + err_message + ); + ("iotedged_err.txt", err_message.as_bytes().to_vec()) + }; + + self.zip_writer + .start_file_from_path(&Path::new("logs").join(file_name), self.file_options) + .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; + + self.zip_writer + .write_all(&output) + .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; + + self.print_verbose("Got logs for iotedged"); + Ok(self) + } + + fn write_docker_log(mut self) -> Result { + self.print_verbose("Getting system logs for docker"); + let since_time: DateTime = DateTime::from_utc( + NaiveDateTime::from_timestamp(self.log_options.since().into(), 0), + Utc, + ); + + #[cfg(unix)] + let inspect = ShellCommand::new("journalctl") + .arg("-a") + .args(&["-u", "docker"]) + .args(&["-S", &since_time.format("%F %T").to_string()]) + .arg("--no-pager") + .output(); + + /* from https://docs.microsoft.com/en-us/virtualization/windowscontainers/troubleshooting#finding-logs */ + #[cfg(windows)] + let inspect = ShellCommand::new("powershell.exe") + .arg("-NoProfile") + .arg("-Command") + .arg(&format!( + r#"Get-EventLog -LogName Application -Source Docker -After "{}" | + Sort-Object Time | + Format-List"#, + since_time.to_rfc3339() + )) + .output(); + + let (file_name, output) = if let Ok(result) = inspect { + if result.status.success() { + ("docker.txt", result.stdout) + } else { + ("docker_err.txt", result.stderr) + } + } else { + let err_message = inspect.err().unwrap().to_string(); + println!( + "Could not find system logs for docker. Including error in bundle.\nError message: {}", + err_message + ); + ("docker_err.txt", err_message.as_bytes().to_vec()) + }; + + self.zip_writer + .start_file_from_path(&Path::new("logs").join(file_name), self.file_options) + .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; + + self.zip_writer + .write_all(&output) + .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; + + self.print_verbose("Got logs for docker"); + Ok(self) + } + + fn write_check(mut self) -> Result { + let iotedge = env::args().next().unwrap(); + self.print_verbose("Calling iotedge check"); + + let mut check = ShellCommand::new(iotedge); + check.arg("check").args(&["-o", "json"]); + + if let Some(host_name) = self.iothub_hostname.clone() { + check.args(&["--iothub-hostname", &host_name]); + } + let check = check + .output() + .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; + + self.zip_writer + .start_file_from_path(&Path::new("check.json"), self.file_options) + .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; + + self.zip_writer + .write_all(&check.stdout) + .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; + + self.print_verbose("Wrote check output to file"); + Ok(self) + } + + fn write_inspect_to_file(mut self, module_name: &str) -> Result { + self.print_verbose(&format!("Running docker inspect for {}", module_name)); + let mut inspect = ShellCommand::new("docker"); + + /*** + * Note: this assumes using windows containers on a windows machine. + * This is the expected production scenario. + * Since the bundle command does not read the config.yaml, it cannot use the `moby.runtime_uri` from there. + * This will not fail the bundle, only note the failure to the user and in the bundle. + */ + #[cfg(windows)] + inspect.args(&["-H", "npipe:////./pipe/iotedge_moby_engine"]); + + inspect.arg("inspect").arg(&module_name); + let inspect = inspect.output(); + + let (file_name, output) = if let Ok(result) = inspect { + if result.status.success() { + (format!("inspect/{}.json", module_name), result.stdout) + } else { + (format!("inspect/{}_err.json", module_name), result.stderr) + } + } else { + let err_message = inspect.err().unwrap().to_string(); + println!( + "Could not reach docker. Including error in bundle.\nError message: {}", + err_message + ); + ( + format!("inspect/{}_err_docker.txt", module_name), + err_message.as_bytes().to_vec(), + ) + }; + + self.zip_writer + .start_file_from_path(&Path::new(&file_name), self.file_options) + .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; + + self.zip_writer + .write_all(&output) + .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; + + self.print_verbose(&format!("Got docker inspect for {}", module_name)); + Ok(self) + } + + fn get_docker_networks(self) -> Result<(Vec, Self), Error> { + let mut inspect = ShellCommand::new("docker"); + + /*** + * Note: just like inspect, this assumes using windows containers on a windows machine. + */ + #[cfg(windows)] + inspect.args(&["-H", "npipe:////./pipe/iotedge_moby_engine"]); + + inspect.args(&["network", "ls"]); + inspect.args(&["--format", "{{.Name}}"]); + let inspect = inspect.output(); + + let result = if let Ok(result) = inspect { + if result.status.success() { + String::from_utf8_lossy(&result.stdout).to_string() + } else { + println!( + "Could not find network names: {}", + String::from_utf8_lossy(&result.stderr) + ); + "azure-iot-edge".to_owned() + } + } else { + println!("Could not find network names: {}", inspect.err().unwrap()); + "azure-iot-edge".to_owned() + }; + + Ok((result.lines().map(String::from).collect(), self)) + } + + fn write_docker_network_to_file(mut self, network_name: &str) -> Result { + self.print_verbose(&format!( + "Running docker network inspect for {}", + network_name + )); + let mut inspect = ShellCommand::new("docker"); + + /*** + * Note: just like inspect, this assumes using windows containers on a windows machine. + */ + #[cfg(windows)] + inspect.args(&["-H", "npipe:////./pipe/iotedge_moby_engine"]); + + inspect.args(&["network", "inspect", &network_name, "-v"]); + let inspect = inspect.output(); + + let (file_name, output) = if let Ok(result) = inspect { + if result.status.success() { + (format!("network/{}.json", network_name), result.stdout) + } else { + (format!("network/{}_err.json", network_name), result.stderr) + } + } else { + let err_message = inspect.err().unwrap().to_string(); + println!( + "Could not reach docker. Including error in bundle.\nError message: {}", + err_message + ); + ( + format!("network/{}_err_docker.txt", network_name), + err_message.as_bytes().to_vec(), + ) + }; + + self.zip_writer + .start_file_from_path(&Path::new(&file_name), self.file_options) + .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; + + self.zip_writer + .write_all(&output) + .map_err(|err| Error::from(err.context(ErrorKind::SupportBundle)))?; + + self.print_verbose(&format!("Got docker network inspect for {}", network_name)); + Ok(self) + } + + fn print_verbose(&self, message: &str) { + if self.verbose { + println!("{}", message); + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum OutputLocation { + File(OsString), + Memory, +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::io; + use std::path::PathBuf; + use std::str; + + use regex::Regex; + use tempfile::tempdir; + + use edgelet_core::{MakeModuleRuntime, ModuleRuntimeState}; + use edgelet_test_utils::crypto::TestHsm; + use edgelet_test_utils::module::{ + TestConfig, TestModule, TestProvisioningResult, TestRuntime, TestSettings, + }; + + use super::{ + make_bundle, make_state, pull_logs, Fail, File, Future, LogOptions, LogTail, OsString, + OutputLocation, + }; + + #[allow(dead_code)] + #[derive(Clone, Copy, Debug, Fail)] + pub enum Error { + #[fail(display = "General error")] + General, + } + + #[test] + fn folder_structure() { + let module_name = "test-module"; + let runtime = make_runtime(module_name); + let tmp_dir = tempdir().unwrap(); + let file_path = tmp_dir + .path() + .join("iotedge_bundle.zip") + .to_str() + .unwrap() + .to_owned(); + + let bundle = make_bundle( + OutputLocation::File(OsString::from(file_path.to_owned())), + LogOptions::default(), + false, + false, + None, + runtime, + ); + + tokio::runtime::current_thread::Runtime::new() + .unwrap() + .block_on(bundle) + .unwrap(); + + let extract_path = tmp_dir.path().join("bundle").to_str().unwrap().to_owned(); + + extract_zip(&file_path, &extract_path); + + // expect logs + let mod_log = fs::read_to_string( + PathBuf::from(&extract_path) + .join("logs") + .join(format!("{}_log.txt", module_name)), + ) + .unwrap(); + assert_eq!("Roses are redviolets are blue", mod_log); + + let iotedged_log = Regex::new(r"iotedged.*\.txt").unwrap(); + assert!(fs::read_dir(PathBuf::from(&extract_path).join("logs")) + .unwrap() + .map(|file| file + .unwrap() + .path() + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_owned()) + .any(|f| iotedged_log.is_match(&f))); + + let docker_log = Regex::new(r"docker.*\.txt").unwrap(); + assert!(fs::read_dir(PathBuf::from(&extract_path).join("logs")) + .unwrap() + .map(|file| file + .unwrap() + .path() + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_owned()) + .any(|f| docker_log.is_match(&f))); + + //expect inspect + let module_in_inspect = Regex::new(&format!(r"{}.*\.json", module_name)).unwrap(); + assert!(fs::read_dir(PathBuf::from(&extract_path).join("inspect")) + .unwrap() + .map(|file| file + .unwrap() + .path() + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_owned()) + .any(|f| module_in_inspect.is_match(&f))); + + // expect check + File::open(PathBuf::from(&extract_path).join("check.json")).unwrap(); + + // expect network inspect + let network_in_inspect = Regex::new(r".*\.json").unwrap(); + assert!(fs::read_dir(PathBuf::from(&extract_path).join("network")) + .unwrap() + .map(|file| file + .unwrap() + .path() + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_owned()) + .any(|f| network_in_inspect.is_match(&f))); + } + + #[test] + fn get_logs() { + let module_name = "test-module"; + let runtime = make_runtime(module_name); + + let options = LogOptions::new() + .with_follow(false) + .with_tail(LogTail::Num(0)) + .with_since(0); + + let result: Vec = pull_logs(&runtime, module_name, &options, Vec::new()) + .wait() + .unwrap(); + let result_str = str::from_utf8(&result).unwrap(); + assert_eq!("Roses are redviolets are blue", result_str); + } + + fn make_runtime(module_name: &str) -> TestRuntime { + let logs = vec![ + &[0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, b'R', b'o'][..], + &b"ses are"[..], + &[b' ', b'r', b'e', b'd', 0x02, 0x00][..], + &[0x00, 0x00, 0x00, 0x00, 0x00, 0x10][..], + &b"violets"[..], + &b" are blue"[..], + ]; + + let state: Result = Ok(ModuleRuntimeState::default()); + let config = TestConfig::new(format!("microsoft/{}", module_name)); + let module = TestModule::new_with_logs(module_name.to_owned(), config, state, logs); + + TestRuntime::make_runtime( + TestSettings::new(), + TestProvisioningResult::new(), + TestHsm::default(), + ) + .wait() + .unwrap() + .with_module(Ok(module)) + } + + // From https://github.com/mvdnes/zip-rs/blob/master/examples/extract.rs + fn extract_zip(source: &str, destination: &str) { + let fname = std::path::Path::new(source); + let file = File::open(&fname).unwrap(); + let mut archive = zip::ZipArchive::new(file).unwrap(); + + for i in 0..archive.len() { + let mut file = archive.by_index(i).unwrap(); + let outpath = PathBuf::from(destination).join(file.sanitized_name()); + + if (&*file.name()).ends_with('/') { + fs::create_dir_all(&outpath).unwrap(); + } else { + if let Some(p) = outpath.parent() { + if !p.exists() { + fs::create_dir_all(&p).unwrap(); + } + } + let mut outfile = fs::File::create(&outpath).unwrap(); + io::copy(&mut file, &mut outfile).unwrap(); + } + + // Get and Set permissions + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + if let Some(mode) = file.unix_mode() { + fs::set_permissions(&outpath, fs::Permissions::from_mode(mode)).unwrap(); + } + } + } + } +}