Skip to content

Add cmdlet for Resolving Hyak and AutoRest errors #3984

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions src/Common/Commands.Common/Extensions/CmdletExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Text;

namespace Microsoft.WindowsAzure.Commands.Utilities.Common
{
Expand Down Expand Up @@ -64,6 +67,80 @@ public static string ResolvePath(this PSCmdlet psCmdlet, string path)
return fullPath;
}

/// <summary>
/// Execute the given cmdlet in powershell usign the given pipeline parameters.
/// </summary>
/// <typeparam name="T">The output type for the cmdlet</typeparam>
/// <param name="cmdlet">The cmdlet to execute</param>
/// <param name="name">The name of the cmdlet</param>
/// <param name="cmdletParameters">The parameters to pass to the cmdlet on the pipeline</param>
/// <returns>The output of executing the cmdlet</returns>
public static List<T> ExecuteCmdletInPipeline<T>(this PSCmdlet cmdlet, string name, params object[] cmdletParameters)
{
List<T> output = new List<T>();
using (System.Management.Automation.PowerShell powershell = System.Management.Automation.PowerShell.Create(RunspaceMode.NewRunspace))
{
var info = new CmdletInfo(name, cmdlet.GetType());
Collection<T> result = powershell.AddCommand(info).Invoke<T>(cmdletParameters);
if (powershell.Streams.Error != null && powershell.Streams.Error.Count > 0)
{
StringBuilder details = new StringBuilder();
powershell.Streams.Error.ForEach(e => details.AppendFormat("Error: {0}\n", e.ToString()));
throw new InvalidOperationException(string.Format("Errors while running cmdlet:\n {0}", details.ToString()));
}

if (result != null && result.Count > 0)
{
result.ForEach(output.Add);
}
}

return output;
}

/// <summary>
/// Execute the given cmdlet in powershell with the given parameters after injecting the given exception. It is expected that the cmdlet has a runtime that can be used for receiving output
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="cmdlet">The cmdlet to execute</param>
/// <param name="name">The name of the cmdlet</param>
/// <param name="exception">The exception to inject into the error stream</param>
/// <param name="cmdletParameters">The parameters to pass to the cmdlet on the pipeline</param>
public static void ExecuteCmdletWithExceptionInPipeline<T>(this PSCmdlet cmdlet, string name, Exception exception, params KeyValuePair<string, object>[] cmdletParameters)
{
List<T> output = new List<T>();
using (System.Management.Automation.PowerShell powershell = System.Management.Automation.PowerShell.Create(RunspaceMode.NewRunspace))
{
var info = new CmdletInfo(name, cmdlet.GetType());
powershell.AddCommand("Write-Error");
powershell.AddParameter("Exception", exception);
powershell.Invoke();
powershell.Commands.Clear();
powershell.AddCommand(info);
foreach (var pair in cmdletParameters)
{
if (pair.Value == null)
{
powershell.AddParameter(pair.Key);
}
else
{
powershell.AddParameter(pair.Key, pair.Value);
}
}
Collection<T> result = powershell.Invoke<T>();
powershell.Streams.Error.ForEach(cmdlet.WriteError);
powershell.Streams.Debug.ForEach(d => cmdlet.WriteDebug(d.Message));
powershell.Streams.Verbose.ForEach(v => cmdlet.WriteWarning(v.Message));
powershell.Streams.Warning.ForEach(w => cmdlet.WriteWarning(w.Message));

if (result != null && result.Count > 0)
{
result.ForEach(r => cmdlet.WriteObject(r));
}
}
}

public static List<T> ExecuteScript<T>(this PSCmdlet cmdlet, string contents)
{
List<T> output = new List<T>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using System;
using System.Management.Automation;
using System.Reflection;
using System.Threading;

namespace Microsoft.WindowsAzure.Commands.ScenarioTest
{
Expand Down Expand Up @@ -46,6 +47,17 @@ public static void ExecuteCmdlet(this PSCmdlet cmdlet)
throw e.InnerException;
}
}
public static void ExecuteCommand(this PSCmdlet cmdlet)
{
try
{
GetProtectedMethod("ProcessRecord").Invoke(cmdlet, new object[] { });
}
catch (TargetInvocationException e)
{
throw e.InnerException;
}
}

public static void SetCommandRuntimeMock(this PSCmdlet cmdlet, ICommandRuntime value)
{
Expand Down
4 changes: 2 additions & 2 deletions src/ResourceManager/Profile/AzureRM.Profile.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,13 @@ CmdletsToExport = 'Disable-AzureRmDataCollection', 'Enable-AzureRmDataCollection
'Set-AzureRmEnvironment', 'Add-AzureRmEnvironment',
'Get-AzureRmSubscription', 'Add-AzureRmAccount', 'Get-AzureRmContext',
'Set-AzureRmContext', 'Import-AzureRmContext', 'Save-AzureRmContext',
'Get-AzureRmTenant', 'Send-Feedback'
'Get-AzureRmTenant', 'Send-Feedback', 'Resolve-AzureRmError'

# Variables to export from this module
# VariablesToExport = @()

# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = 'Login-AzureRmAccount', 'Select-AzureRmSubscription'
AliasesToExport = 'Login-AzureRmAccount', 'Select-AzureRmSubscription', 'Resolve-Error'

# DSC resources to export from this module
# DscResourcesToExport = @()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@
<Compile Include="ClientFactoryTests.cs" />
<Compile Include="CommonDataCmdletTests.cs" />
<Compile Include="EnvironmentCmdletTests.cs" />
<Compile Include="ErrorResolutionTests.cs" />
<Compile Include="MockDataStore.cs" />
<Compile Include="MockSubscriptionClientFactory.cs" />
<Compile Include="NullClient.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// ----------------------------------------------------------------------------------
//
// Copyright Microsoft Corporation
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ----------------------------------------------------------------------------------

using Hyak.Common;
using Microsoft.Azure.Commands.Profile.Errors;
using Microsoft.Azure.Commands.ScenarioTest;
using Microsoft.WindowsAzure.Commands.Common.Test.Mocks;
using Microsoft.WindowsAzure.Commands.ScenarioTest;
using Microsoft.WindowsAzure.Commands.Utilities.Common;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using System.Net;
using System.Net.Http;
using Xunit;

namespace Microsoft.Azure.Commands.Profile.Test
{
public class ErrorResolutionTests
{
class TestHyakException : CloudException
{
public TestHyakException(string message, CloudHttpRequestErrorInfo request, CloudHttpResponseErrorInfo response) : base(message)
{
Request = request;
Response = response;
}
}

[Fact]
[Trait(Category.AcceptanceType, Category.CheckIn)]
public void DoesNotThrowWithNullError()
{
TestExecutionHelpers.SetUpSessionAndProfile();
var cmdlet = new ResolveError();
var output = cmdlet.ExecuteCmdletInPipeline<AzureErrorRecord>("Resolve-Error");
Assert.True(output == null || output.Count == 0);
output = cmdlet.ExecuteCmdletInPipeline<AzureErrorRecord>("Resolve-Error", new ErrorRecord[] { null, null });
Assert.True(output == null || output.Count == 0);
output = cmdlet.ExecuteCmdletInPipeline<AzureErrorRecord>("Resolve-Error", new ErrorRecord[] { null, new ErrorRecord(new Exception(null), null, ErrorCategory.AuthenticationError, null) });
Assert.NotNull(output);
Assert.Equal(1, output.Count);
var record = output[0] as AzureExceptionRecord;
Assert.NotNull(record);
Assert.Equal(ErrorCategory.AuthenticationError, record.ErrorCategory.Category);
}

[Fact]
[Trait(Category.AcceptanceType, Category.CheckIn)]
public void HandlesExceptionError()
{
var runtime = new MockCommandRuntime();
var request = new HttpRequestMessage(HttpMethod.Get, new Uri("https://www.contoso.com/resource?api-version-1.0"));
request.Headers.Add("x-ms-request-id", "HyakRequestId");
var response = new HttpResponseMessage(HttpStatusCode.BadRequest);
var hyakException = new TestHyakException("exception message", CloudHttpRequestErrorInfo.Create(request), CloudHttpResponseErrorInfo.Create(response))
{
Error = new Hyak.Common.CloudError { Code="HyakCode", Message="HyakError"}
};

var autorestException = new Microsoft.Rest.Azure.CloudException("exception message")
{
Body = new Microsoft.Rest.Azure.CloudError { Code = "AutorestCode", Message = "Autorest message" },
Request = new Rest.HttpRequestMessageWrapper(request, ""),
Response = new Rest.HttpResponseMessageWrapper(response, ""),
RequestId = "AutoRestRequestId"
};

var cmdlet = new ResolveError
{
Error = new []
{
new ErrorRecord(new Exception("exception message"), "errorCode", ErrorCategory.AuthenticationError, this),
new ErrorRecord(hyakException, "errorCode", ErrorCategory.ConnectionError, this),
new ErrorRecord(autorestException , "errorCode", ErrorCategory.InvalidOperation, this),
},
CommandRuntime = runtime
};

cmdlet.ExecuteCmdlet();
Assert.NotNull(runtime.OutputPipeline);
Assert.Equal(3, runtime.OutputPipeline.Count);
var errorResult = runtime.OutputPipeline[0] as AzureExceptionRecord;
Assert.NotNull(errorResult);
Assert.Equal(ErrorCategory.AuthenticationError, errorResult.ErrorCategory.Category);
Assert.NotNull(errorResult.Exception);
Assert.Equal(errorResult.Exception.GetType(), typeof(Exception));
Assert.Equal("exception message", errorResult.Exception.Message);
var hyakResult = runtime.OutputPipeline[1] as AzureRestExceptionRecord;
Assert.NotNull(hyakResult);
Assert.Equal(ErrorCategory.ConnectionError, hyakResult.ErrorCategory.Category);
Assert.NotNull(errorResult.Exception);
Assert.Equal(hyakResult.Exception.GetType(), typeof(TestHyakException));
Assert.Equal("exception message", hyakResult.Exception.Message);
Assert.NotNull(hyakResult.RequestMessage);
Assert.Equal(HttpMethod.Get.ToString(), hyakResult.RequestMessage.Verb);
Assert.Equal(new Uri("https://www.contoso.com/resource?api-version-1.0"), hyakResult.RequestMessage.Uri);
Assert.NotNull(hyakResult.ServerResponse);
Assert.Equal(HttpStatusCode.BadRequest.ToString(), hyakResult.ServerResponse.ResponseStatusCode);
var autorestResult = runtime.OutputPipeline[2] as AzureRestExceptionRecord;
Assert.NotNull(autorestResult);
Assert.Equal(ErrorCategory.InvalidOperation, autorestResult.ErrorCategory.Category);
Assert.NotNull(autorestResult.Exception);
Assert.Equal(autorestResult.Exception.GetType(), typeof(Microsoft.Rest.Azure.CloudException));
Assert.Equal("exception message", autorestResult.Exception.Message);
Assert.NotNull(autorestResult.RequestMessage);
Assert.Equal(HttpMethod.Get.ToString(), autorestResult.RequestMessage.Verb);
Assert.Equal(new Uri("https://www.contoso.com/resource?api-version-1.0"), autorestResult.RequestMessage.Uri);
Assert.NotNull(autorestResult.ServerResponse);
Assert.Equal(HttpStatusCode.BadRequest.ToString(), autorestResult.ServerResponse.ResponseStatusCode);
Assert.Equal("AutoRestRequestId", autorestResult.RequestId);
Assert.Contains("AutorestCode", autorestResult.ServerMessage);
Assert.Contains("Autorest message", autorestResult.ServerMessage);
}

[Fact]
[Trait(Category.AcceptanceType, Category.CheckIn)]
public void LastParameterFindsLastError()
{
TestExecutionHelpers.SetUpSessionAndProfile();
var mock = new MockCommandRuntime();
var cmdlet = new ResolveError { CommandRuntime = mock };
var message = "RuntimeErrorMessage";
var exception = new Exception(message);
cmdlet.ExecuteCmdletWithExceptionInPipeline<AzureErrorRecord>("Resolve-AzureRmError", exception, new KeyValuePair<string, object>("Last", null ) );
Assert.NotNull(mock.ErrorStream);
Assert.Equal(1, mock.ErrorStream.Count);
Assert.NotNull(mock.OutputPipeline);
Assert.Equal(1, mock.OutputPipeline.Count);
var record = mock.OutputPipeline[0] as AzureExceptionRecord;
Assert.NotNull(record);
Assert.NotNull(record.Exception);
Assert.Equal(typeof(Exception), record.Exception.GetType());
Assert.Equal(message, record.Message);


}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
"Select-AzureRmSubscription" = "Set-AzureRmContext";
"Save-AzureRmProfile" = "Save-AzureRmContext";
"Select-AzureRmProfile" = "Import-AzureRmContext";
"Resolve-Error" = "Resolve-AzureRmError";
}.GetEnumerator() | Select @{Name='Name'; Expression={$_.Key}}, @{Name='Value'; Expression={$_.Value}} | New-Alias -Description "AzureAlias"
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@
<Compile Include="Environment\GetAzureRMEnvironment.cs" />
<Compile Include="Environment\SetAzureRMEnvironment.cs" />
<Compile Include="Environment\AddAzureRMEnvironment.cs" />
<Compile Include="Errors\AzureRestExceptionRecord.cs" />
<Compile Include="Errors\AzureExceptionRecord.cs" />
<Compile Include="Errors\AzureErrorRecord.cs" />
<Compile Include="Errors\HttpRequestInfo.cs" />
<Compile Include="Errors\HttpResponseInfo.cs" />
<Compile Include="Errors\HttpMessageInfo.cs" />
<Compile Include="Errors\ResolveError.cs" />
<Compile Include="Feedback\SendFeedback.cs" />
<Compile Include="Models\AzureRmProfileExtensions.cs" />
<Compile Include="Models\ModelExtensions.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// ----------------------------------------------------------------------------------
//
// Copyright Microsoft Corporation
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ----------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Text;
using System.Threading.Tasks;

namespace Microsoft.Azure.Commands.Profile.Errors
{
public class AzureErrorRecord
{
public AzureErrorRecord(ErrorRecord record)
{
InvocationInfo = record.InvocationInfo;
ScriptStackTrace = record.ScriptStackTrace;
ErrorCategory = record.CategoryInfo;
ErrorDetails = record.ErrorDetails;
}

public ErrorDetails ErrorDetails { get; set; }

public ErrorCategoryInfo ErrorCategory { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@markcowl nit: can we put an empty line between ErrorCategory and InvocationInfo?


public InvocationInfo InvocationInfo { get; set; }

public string ScriptStackTrace { get; set; }

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// ----------------------------------------------------------------------------------
//
// Copyright Microsoft Corporation
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ----------------------------------------------------------------------------------

using System;
using System.Management.Automation;

namespace Microsoft.Azure.Commands.Profile.Errors
{
public class AzureExceptionRecord : AzureErrorRecord
{
public AzureExceptionRecord(Exception exception, ErrorRecord record, bool inner = false) : base(record)
{
Message = exception.Message;
HelpLink = exception.HelpLink;
StackTrace = exception.StackTrace;
Exception = exception;
}

public bool InnerException { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@markcowl nit: can we put an empty line between InnerException and Exception?


public Exception Exception { get; }

public string Message { get; set; }

public string StackTrace { get; set; }

public string HelpLink { get; set; }
}
}
Loading