diff --git a/src/ResourceManager/Automation/Commands.Automation.Test/Commands.Automation.Test.csproj b/src/ResourceManager/Automation/Commands.Automation.Test/Commands.Automation.Test.csproj index 05770ce7b707..10ed338d094d 100644 --- a/src/ResourceManager/Automation/Commands.Automation.Test/Commands.Automation.Test.csproj +++ b/src/ResourceManager/Automation/Commands.Automation.Test/Commands.Automation.Test.csproj @@ -63,13 +63,20 @@ ..\..\..\packages\Microsoft.Azure.Common.2.1.0\lib\net45\Microsoft.Azure.Common.NetFramework.dll + + ..\..\..\packages\Microsoft.Azure.Management.Automation.2.0.0\lib\net40\Microsoft.Azure.Management.Automation.dll + False ..\..\..\packages\Microsoft.Azure.Management.Resources.2.18.11-preview\lib\net40\Microsoft.Azure.ResourceManager.dll + + False + ..\..\..\packages\Microsoft.Azure.Test.Framework.1.0.5799.28345-prerelease\lib\net45\Microsoft.Azure.Test.Framework.dll + + False ..\..\..\packages\Microsoft.Azure.Test.HttpRecorder.1.0.5799.28345-prerelease\lib\net45\Microsoft.Azure.Test.HttpRecorder.dll - True False @@ -112,7 +119,9 @@ ..\..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll - + + False + @@ -135,6 +144,8 @@ + + @@ -195,6 +206,28 @@ + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + \ No newline at end of file diff --git a/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/AutomationScenarioTestsBase.cs b/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/AutomationScenarioTestsBase.cs new file mode 100644 index 000000000000..dff81da7fca9 --- /dev/null +++ b/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/AutomationScenarioTestsBase.cs @@ -0,0 +1,65 @@ +// ---------------------------------------------------------------------------------- +// +// 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 Microsoft.Azure.Common.Authentication; +using Microsoft.Azure.Management.Resources; +using Microsoft.Azure.Subscriptions; +using Microsoft.WindowsAzure.Commands.ScenarioTest; +using Microsoft.Azure.Test; +using Microsoft.Azure.Management.Automation; +using Microsoft.WindowsAzure.Commands.Test.Utilities.Common; + +namespace Microsoft.Azure.Commands.Automation.Test +{ + public abstract class AutomationScenarioTestsBase : RMTestBase + { + private EnvironmentSetupHelper helper; + + protected AutomationScenarioTestsBase() + { + helper = new EnvironmentSetupHelper(); + } + + protected void SetupManagementClients() + { + var automationManagementClient = GetAutomationManagementClient(); + + helper.SetupManagementClients(automationManagementClient); + } + + protected void RunPowerShellTest(params string[] scripts) + { + using (UndoContext context = UndoContext.Current) + { + context.Start(TestUtilities.GetCallingClass(2), TestUtilities.GetCurrentMethodName(2)); + + SetupManagementClients(); + + helper.SetupEnvironment(AzureModule.AzureResourceManager); + + helper.SetupModules(AzureModule.AzureResourceManager, + "ScenarioTests\\" + this.GetType().Name + ".ps1", + helper.RMProfileModule, + helper.GetRMModulePath(@"AzureRM.Automation.psd1")); + + helper.RunPowerShellTest(scripts); + } + } + + protected AutomationManagementClient GetAutomationManagementClient() + { + return TestBase.GetServiceClient(new CSMTestEnvironmentFactory()); + } + } +} diff --git a/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/AutomationTests.cs b/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/AutomationTests.cs new file mode 100644 index 000000000000..4b9fe27b4b7c --- /dev/null +++ b/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/AutomationTests.cs @@ -0,0 +1,78 @@ +// ---------------------------------------------------------------------------------- +// +// 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 Microsoft.WindowsAzure.Commands.ScenarioTest; +using Xunit; + +namespace Microsoft.Azure.Commands.Automation.Test +{ + public class AutomationTests : AutomationScenarioTestsBase + { + [Fact(Skip = "Need x64 test framework.")] + [Trait(Category.AcceptanceType, Category.CheckIn)] + [Trait(Category.Service, Category.Automation)] + public void TestAutomationStartAndStopRunbook() + { + RunPowerShellTest("Test-AutomationStartAndStopRunbook -runbookPath ScenarioTests\\Resources\\Test-Workflow.ps1"); + } + + [Fact(Skip = "Need x64 test framework.")] + [Trait(Category.AcceptanceType, Category.CheckIn)] + [Trait(Category.Service, Category.Automation)] + public void TestAutomationPublishAndEditRunbook() + { + RunPowerShellTest("Test-AutomationPublishAndEditRunbook -runbookPath ScenarioTests\\Resources\\Test-Workflow.ps1 -editRunbookPath Resources\\Automation\\Test-WorkflowV2.ps1"); + } + + [Fact(Skip = "Need x64 test framework.")] + [Trait(Category.AcceptanceType, Category.CheckIn)] + [Trait(Category.Service, Category.Automation)] + public void TestAutomationConfigureRunbook() + { + RunPowerShellTest("Test-AutomationConfigureRunbook -runbookPath ScenarioTests\\Resources\\Write-DebugAndVerboseOutput.ps1"); + } + + [Fact] + [Trait(Category.Service, Category.Automation)] + [Trait(Category.RunType, Category.LiveOnly)] + public void TestAutomationSuspendAndResumeJob() + { + RunPowerShellTest("Test-AutomationSuspendAndResumeJob -runbookPath ScenarioTests\\Resources\\Use-WorkflowCheckpointSample.ps1"); + } + + [Fact] + [Trait(Category.Service, Category.Automation)] + [Trait(Category.RunType, Category.LiveOnly)] + public void TestAutomationStartRunbookOnASchedule() + { + RunPowerShellTest("Test-AutomationStartRunbookOnASchedule -runbookPath ScenarioTests\\Resources\\Test-Workflow.ps1"); + } + + [Fact(Skip = "Need x64 test framework.")] + [Trait(Category.AcceptanceType, Category.CheckIn)] + [Trait(Category.Service, Category.Automation)] + public void TestAutomationStartUnpublishedRunbook() + { + RunPowerShellTest("Test-AutomationStartUnpublishedRunbook -runbookPath ScenarioTests\\Resources\\Test-WorkFlowWithVariousParameters.ps1"); + } + + [Fact(Skip = "Need x64 test framework.")] + [Trait(Category.AcceptanceType, Category.CheckIn)] + [Trait(Category.Service, Category.Automation)] + public void TestAutomationRunbookWithParameter() + { + RunPowerShellTest("Test-RunbookWithParameter -runbookPath ScenarioTests\\Resources\\fastJob.ps1 @{'nums'='[1,2,3,4,5,6,7]'} 28"); + } + } +} diff --git a/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/AutomationTests.ps1 b/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/AutomationTests.ps1 new file mode 100644 index 000000000000..0cc8902689bd --- /dev/null +++ b/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/AutomationTests.ps1 @@ -0,0 +1,363 @@ +# ---------------------------------------------------------------------------------- +# +# 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. +# ---------------------------------------------------------------------------------- + +<# +.SYNOPSIS +Checks whether the first string contains the second one +#> + +$accountName='account' +$location = "East US" + +function AssertContains +{ + param([string] $str, [string] $substr, [string] $message) + + if (!$message) + { + $message = "Assertion failed because '$str' does not contain '$substr'" + } + + if (!$str.Contains($substr)) + { + throw $message + } + + return $true +} + +<# +.SYNOPSIS +Checks whether the runbook exists and if it exists, removes it and then imports a new runbook +#> +function CreateRunbook +{ + param([string] $runbookPath, [boolean] $byName=$false, [string[]] $tag, [string] $description) + + $runbookName = gci $runbookPath | %{$_.BaseName} + $runbook = Get-AzureRmAutomationRunbook $accountName | where {$_.Name -eq $runbookName} + if ($runbook.Count -eq 1) + { + Remove-AzureRmAutomationRunbook $accountName -Name $runbookName -Force + } + + if(!$byName) + { + return New-AzureRmAutomationRunbook $accountName -Path $runbookPath -Tag $tag -Description $description + } + else + { + return New-AzureRmAutomationRunbook $accountName -Name $runbookName -Tag $tag -Description $description + } +} + +########################################################################### Automation Scenario Tests ########################################################################### + +######### $accountName and $subscriptionName should be provided in variables.yml in order to run the following Test Cases ####################### + +<# +.SYNOPSIS +Waits for Job to be a specific status for approximately $numOfSeconds +#> +function WaitForJobStatus +{ + param([Guid] $Id, [Int] $numOfSeconds = 150, [String] $Status) + + $timeElapse = 0 + $interval = 3 + $endStatus = @('completed','failed') + while($timeElapse -lt $numOfSeconds) + { + Wait-Seconds $interval + $timeElapse = $timeElapse + $interval + $job = Get-AzureRmAutomationJob -AutomationAccount $accountName -Id $Id + if($job.Status -eq $Status) + { + break + } + elseif($endStatus -contains $job.Status.ToLower()) + { + Write-Output ("The Job with ID $($job.Id) reached $($job.Status) Status already.") + return + } + } + Assert-AreEqual $Status $job.Status "Job did not reach $Status status within $numOfSeconds seconds."; +} + +<# +.SYNOPSIS +Tests Runbook with Parameters +#> +function Test-RunbookWithParameter +{ + param([string] $runbookPath, [HashTable] $parameters, [int]$expectedResult) + + #Setup + $automationAccount = Get-AzureRmAutomationAccount -Name $accountName + Assert-NotNull $automationAccount "Automation account $accountName does not exist." + + $runbook = CreateRunbook $runbookPath + Assert-NotNull $runbook "runBook $runbookPath does not import successfully." + $automationAccount | Publish-AzureRmAutomationRunbook -Name $runbook.Name + + #Test + $job = $automationAccount | Start-AzureRmAutomationRunbook -Name $runbook.Name -Parameters $parameters + WaitForJobStatus -Id $job.Id -Status "Completed" + $jobOutput = $automationAccount | Get-AzureRmAutomationJobOutput -Id $job.Id -Stream Output + $automationAccount | Remove-AzureRmAutomationRunbook -Name $runbook.Name -Force + Assert-Throws { $automationAccount | Get-AzureRmAutomationRunbook -Name $runbook.Name} +} + +<# +.SYNOPSIS +Tests of Start and Stop RunBook +#> +function Test-AutomationStartAndStopRunbook +{ + param([string] $runbookPath) + + $automationAccount = Get-AzureRmAutomationAccount -Name $accountName + Assert-NotNull $automationAccount "Automation account $accountName does not exist." + + $runbook = CreateRunbook $runbookPath + Assert-NotNull $runbook "runBook $runbookPath does not import successfully." + $automationAccount | Publish-AzureRmAutomationRunbook -Name $runbook.Name + + #Test + $job = Start-AzureRmAutomationRunbook -Name $runbook.Name -AutomationAccountName $accountName + WaitForJobStatus -Id $job.Id -Status "Running" + $automationAccount | Stop-AzureRmAutomationJob -Id $job.Id + WaitForJobStatus -Id $job.Id -Status "Stopped" + $automationAccount | Remove-AzureRmAutomationRunbook -Name $runbook.Name -Force + Assert-Throws { $automationAccount | Get-AzureRmAutomationRunbook -Name $runbook.Name} +} + +<# +.SYNOPSIS +Tests publishing runbook and editing runbook with and without Overwrite parameter +#> +function Test-AutomationPublishAndEditRunbook +{ + param([string] $runbookPath, [string] $editRunbookPath) + + $runbook = CreateRunbook $runbookPath $true + + #Publish Runbook + Publish-AzureRmAutomationRunbook $accountName -Name $runbook.Name + $publishedRunbook = Get-AzureRmAutomationRunbook $accountName -Name $runbook.Name + $runbookState = "Published" + Assert-AreEqual $publishedRunbook.State $runbookState "Runbook should be in $runbookState state" + $publishedRunbookDefn = Get-AzureRmAutomationRunbookDefinition $accountName -Name $runbook.Name + + #Edit Runbook + Set-AzureRmAutomationRunbookDefinition $accountName -Name $runbook.Name -Path $runbookPath -Overwrite + $runbook = Get-AzureRmAutomationRunbook $accountName -Name $runbook.Name + $runbookState = "Edit" + Assert-AreEqual $runbook.State $runbookState "Runbook should be in $runbookState state" + $editedRunbookDefn = Get-AzureRmAutomationRunbookDefinition $accountName -Name $runbook.Name -Slot "Draft" + Assert-AreNotEqual $editedRunbookDefn.Content $publishedRunbookDefn.Content "Old content and edited content of the runbook shouldn't be equal" + + Assert-Throws {Set-AzureRmAutomationRunbookDefinition $accountName -Name $runbook.Name -Path $editRunbookPath -PassThru -ErrorAction Stop} + Set-AzureRmAutomationRunbookDefinition $accountName -Name $runbook.Name -Path $editRunbookPath -Overwrite + $editedRunbookDefn2 = Get-AzureRmAutomationRunbookDefinition $accountName -Name $runbook.Name -Slot "Draft" + Assert-AreNotEqual $editedRunbookDefn2.Content $editedRunbookDefn.Content "Old content and edited content of the runbook shouldn't be equal" + + Remove-AzureRmAutomationRunbook $accountName -Name $runbook.Name -Force + Assert-Throws {Get-AzureRmAutomationRunbook $accountName -Name $runbook.Name} + +} + +<# +.SYNOPSIS +Tests setting runbook configuration +#> +function Test-AutomationConfigureRunbook +{ + param([string] $runbookPath) + + #Setup + $automationAccount = Get-AzureRmAutomationAccount -Name $accountName + Assert-NotNull $automationAccount "Automation account $accountName does not exist." + $runbook = CreateRunbook $runbookPath + Assert-NotNull $runbook "runbook ($runbookPath) isn't imported successfully." + Publish-AzureRmAutomationRunbook -Name $runbook.Name -AutomationAccountName $accountName + + #Test + + #Change the runbook configuration + $automationAccount | Set-AzureRmAutomationRunbook -Name $runbook.Name -LogVerbose $true -LogProgress $false + $runbook = $automationAccount | Get-AzureRmAutomationRunbook -Name $runbook.Name + Assert-NotNull $runbook "Runbook shouldn't be Null" + Assert-AreEqual $true $runbook.LogVerbose "Log Verbose mode should be true." + Assert-AreEqual $false $runbook.LogProgress "Log Progress mode should be false." + + #Start runbook and wait for job complete + $job = $automationAccount | Start-AzureRmAutomationRunbook -Name $runbook.Name + WaitForJobStatus -Id $job.Id -Status "Completed" + + #Check job output streams + $jobOutputs = $automationAccount | Get-AzureRmAutomationJobOutput -Id $job.Id -Stream "Output" + Assert-AreEqual 1 $jobOutputs.Count + AssertContains $jobOutputs[0].Text "output message" "The output stream is wrong." + #Verify that verbose streams are logged + $jobVerboseOutputs = Get-AzureRmAutomationJobOutput $accountName -Id $job.Id -Stream "Verbose" + Assert-AreEqual 1 $jobVerboseOutputs.Count + AssertContains $jobVerboseOutputs[0].Text "verbose message" "The verbose stream is wrong." + #Verify that progress stream isn't logged + $jobProgressOutputs = Get-AzureRmAutomationJobOutput -AutomationAccountName $accountName -Id $job.Id -Stream "Progress" + Assert-AreEqual 0 $jobProgressOutputs.Count + + #Change the runbook configuration again and start the runbook + Set-AzureRmAutomationRunbook $accountName -Name $runbook.Name -LogVerbose $false -LogProgress $true + $job = Start-AzureRmAutomationRunbook $accountName -Name $runbook.Name + WaitForJobStatus -Id $job.Id -Status "Completed" + #Verify that progress stream is logged + $jobProgressOutputs = Get-AzureRmAutomationJobOutput $accountName -Id $job.Id -Stream "Progress" + Assert-AreNotEqual 0 $jobProgressOutputs.Count + Assert-AreEqual $jobProgressOutputs[0].Type "Progress" + #Verify that verbose streams aren't logged + $jobVerboseOutputs = Get-AzureRmAutomationJobOutput $accountName -Id $job.Id -Stream "Verbose" + Assert-AreEqual 0 $jobVerboseOutputs.Count + + #Check whether the total number of jobs for the runbook is correct + $jobs = Get-AzureRmAutomationJob $accountName -RunbookName $runbook.Name + Assert-AreEqual 2 $jobs.Count "There should be 2 jobs in total for this runbook." + + #Remove runbook + $automationAccount | Remove-AzureRmAutomationRunbook -Name $runbook.Name -Force + Assert-Throws {$automationAccount | Get-AzureRmAutomationRunbook -Name $runbook.Name} +} + +<# +.SYNOPSIS +Tests suspending a started job and resuming a suspended job +#> +function Test-AutomationSuspendAndResumeJob +{ + param([string] $runbookPath) + + #Setup + $automationAccount = Get-AzureRmAutomationAccount $accountName + Assert-NotNull $automationAccount "Automation account $accountName does not exist." + $runbook = CreateRunbook $runbookPath + + #Test + + $automationAccount | Publish-AzureRmAutomationRunbook -Name $runbook.Name + #Start, suspend, and then resume job + $job = Start-AzureRmAutomationRunbook $accountName -Name $runbook.Name + WaitForJobStatus -Id $job.Id -Status "Running" + Suspend-AzureRmAutomationJob $accountName -Id $job.Id + WaitForJobStatus -Id $job.Id -Status "Suspended" + $automationAccount | Resume-AzureRmAutomationJob -Id $job.Id + WaitForJobStatus -Id $job.Id -Status "Completed" + + #Remove runbook + Remove-AzureRmAutomationRunbook -AutomationAccountName $accountName -Name $runbook.Name -Force + Assert-Throws {Get-AzureRmAutomationRunbook $accountName -Name $runbook.Name} +} + +<# +.SYNOPSIS +Tests starting a runbook on a schedule +#> +function Test-AutomationStartRunbookOnASchedule +{ + param([string] $runbookPath) + + #Setup + $automationAccount = Get-AzureRmAutomationAccount -Name $accountName + $runbook = CreateRunbook $runbookPath + Publish-AzureRmAutomationRunbook $accountName -Name $runbook.Name + + #Test + + #Create one time schedule + $oneTimeScheName = "oneTimeSchedule" + $schedule = Get-AzureRmAutomationSchedule $accountName | where {$_.Name -eq $oneTimeScheName} + if ($schedule.Count -eq 1) + { + Remove-AzureRmAutomationSchedule $accountName -Name $oneTimeScheName -Force + } + $startTime = (Get-Date).AddMinutes(7) + New-AzureRmAutomationSchedule $accountName -Name $oneTimeScheName -OneTime -StartTime $startTime + $oneTimeSchedule = Get-AzureRmAutomationSchedule $accountName -Name $oneTimeScheName + Assert-NotNull $oneTimeSchedule "$oneTimeScheName doesn't exist!" + + #Create daily schedule + $dailyScheName = "dailySchedule" + $schedule = Get-AzureRmAutomationSchedule $accountName | where {$_.Name -eq $dailyScheName} + if ($schedule.Count -eq 1) + { + Remove-AzureRmAutomationSchedule $accountName -Name $dailyScheName -Force + } + $startTime = (Get-Date).AddDays(1) + $expiryTime = (Get-Date).AddDays(3) + New-AzureRmAutomationSchedule $accountName -Name $DailyScheName -StartTime $startTime -ExpiryTime $expiryTime -DayInterval 1 + $dailySchedule = Get-AzureRmAutomationSchedule $accountName -Name $dailyScheName + Assert-NotNull $dailySchedule "$dailyScheName doesn't exist!" + + $runbook = Register-AzureRmAutomationScheduledRunbook $accountName -Name $runbook.Name -ScheduleName $oneTimeScheName + Assert-AreEqual $oneTimeScheName $runbook.ScheduleNames "The runbook should be associated with $oneTimeScheName" + $runbook = Register-AzureRmAutomationScheduledRunbook $accountName -Name $runbook.Name -ScheduleName $dailyScheName + Assert-True { $runbook.ScheduleNames -Contains $dailyScheName} "The runbook should be associated with $dailyScheName" + + #waiting for seven minutes + Wait-Seconds 420 + $job = Get-AzureRmAutomationJob $accountName -Name $runbook.Name | where {$_.ScheduleName -eq $oneTimeScheName} + $jobSchedule = Get-AzureRmAutomationScheduledRunbook $accountName -RunbookName $runbook.Name -ScheduleName $oneTimeScheName + Assert-AreEqual 1 $jobSchedule.Count + Assert-AreEqual 1 $job.Count + WaitForJobStatus -Id $job.Id -Status "Completed" + + #Edit schedule + $description = "Daily Schedule Description" + Set-AzureRmAutomationSchedule $accountName -Name $dailyScheName -Description $description + $dailySchedule = Get-AzureRmAutomationSchedule $accountName -Name $dailyScheName + Assert-AreEqual $description $dailySchedule.Description + + Unregister-AzureRmAutomationScheduledRunbook $accountName -Name $runbook.Name -ScheduleName $dailyScheName + $jobSchedule = Get-AzureRmAutomationScheduledRunbook $accountName -RunbookName $runbook.Name -ScheduleName $dailyScheName + Assert-Null $jobSchedule "The runbook shouldn't have an association with $dailyScheName" + + #Remove runbook and schedule + Remove-AzureRmAutomationSchedule $accountName -Name $oneTimeScheName -Force + Assert-Throws {$automationAccount | Get-AzureRmAutomationSchedule -Name $oneTimeScheName} + $automationAccount | Remove-AzureRmAutomationSchedule -Name $dailyScheName -Force + Assert-Throws {$automationAccount | Get-AzureRmAutomationSchedule -Name $dailyScheName} + Remove-AzureRmAutomationRunbook $accountName -Name $runbook.Name -Force + Assert-Throws {Get-AzureRmAutomationRunbook $accountName -Name $runbook.Name} +} + +<# +.SYNOPSIS +Tests starting an unpublished runbook +#> +function Test-AutomationStartUnpublishedRunbook +{ + param([string] $runbookPath) + + $tags = @("tag1","tag2") + $description = "Runbook Description" + $c = Get-Date + $runbookParameters = @{"a" = "stringParameter"; "b" = 123; "c" = $c} + $runbook = CreateRunbook $runbookPath $false $tags $description + Assert-NotNull $runbook "runBook $runbookPath does not import successfully." + Assert-NotNull $runbook.Tags "Tags of the runbook shouldn't be Null." + Assert-NotNull $runbook.Description "Description of the runbook shouldn't be Null." + Assert-Throws {Start-AzureRmAutomationRunbook $accountName -Name $runbook.Name -Parameters $runbookParameters -PassThru -ErrorAction Stop} + + Remove-AzureRmAutomationRunbook $accountName -Name $runbook.Name -Force + Assert-Throws {Get-AzureRmAutomationRunbook $accountName -Name $runbook.Name -Parameters $runbookParameters -PassThru -ErrorAction Stop} +} diff --git a/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/Resources/Test-WorkFlowWithVariousParameters.ps1 b/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/Resources/Test-WorkFlowWithVariousParameters.ps1 new file mode 100644 index 000000000000..62671711bd0c --- /dev/null +++ b/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/Resources/Test-WorkFlowWithVariousParameters.ps1 @@ -0,0 +1,7 @@ +workflow Test-WorkFlowWithVariousParameters +{ + param([string] $a, [int] $b, [DateTime] $c) + "a is " + $a + "b is " + $b + "c is " + $c +} \ No newline at end of file diff --git a/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/Resources/Test-Workflow.ps1 b/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/Resources/Test-Workflow.ps1 new file mode 100644 index 000000000000..3f2a2cde1916 --- /dev/null +++ b/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/Resources/Test-Workflow.ps1 @@ -0,0 +1,5 @@ +Workflow Test-Workflow { + get-date + start-sleep -s 40 + get-date +} \ No newline at end of file diff --git a/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/Resources/Test-WorkflowV2.ps1 b/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/Resources/Test-WorkflowV2.ps1 new file mode 100644 index 000000000000..7f2511572f23 --- /dev/null +++ b/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/Resources/Test-WorkflowV2.ps1 @@ -0,0 +1,3 @@ +Workflow Test-Workflow { + get-date +} \ No newline at end of file diff --git a/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/Resources/Use-WorkflowCheckpointSample.ps1 b/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/Resources/Use-WorkflowCheckpointSample.ps1 new file mode 100644 index 000000000000..2a3417513c8e --- /dev/null +++ b/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/Resources/Use-WorkflowCheckpointSample.ps1 @@ -0,0 +1,11 @@ +workflow Use-WorkflowCheckpointSample +{ + Write-Output "Before Checkpoint." + start-sleep -s 20 + + # A checkpoint is created. + Checkpoint-Workflow + + # This line occurs after the checkpoint. The runbook will start here on resume. + Write-Output "After Checkpoint." +} \ No newline at end of file diff --git a/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/Resources/Write-DebugAndVerboseOutput.ps1 b/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/Resources/Write-DebugAndVerboseOutput.ps1 new file mode 100644 index 000000000000..ff1eb3a97923 --- /dev/null +++ b/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/Resources/Write-DebugAndVerboseOutput.ps1 @@ -0,0 +1,6 @@ +workflow Write-DebugAndVerboseOutput +{ + "output message" + write-debug -message "debug message" + write-verbose -message "verbose message" +} \ No newline at end of file diff --git a/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/Resources/fastjob.ps1 b/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/Resources/fastjob.ps1 new file mode 100644 index 000000000000..0a9c430faa6a --- /dev/null +++ b/src/ResourceManager/Automation/Commands.Automation.Test/ScenarioTests/Resources/fastjob.ps1 @@ -0,0 +1,6 @@ +workflow fastJob +{ + param([int[]] $nums) + $sum=($nums | Measure-Object -Sum).Sum + echo $sum +} \ No newline at end of file diff --git a/src/ResourceManager/Automation/Commands.Automation.Test/packages.config b/src/ResourceManager/Automation/Commands.Automation.Test/packages.config index ea87477df30a..cb7e9dee7ed3 100644 --- a/src/ResourceManager/Automation/Commands.Automation.Test/packages.config +++ b/src/ResourceManager/Automation/Commands.Automation.Test/packages.config @@ -6,6 +6,7 @@ + diff --git a/src/ResourceManager/Automation/Commands.Automation/Cmdlet/NewAzureAutomationWebhook.cs b/src/ResourceManager/Automation/Commands.Automation/Cmdlet/NewAzureAutomationWebhook.cs index 9a38d81b3484..211adfdb3317 100644 --- a/src/ResourceManager/Automation/Commands.Automation/Cmdlet/NewAzureAutomationWebhook.cs +++ b/src/ResourceManager/Automation/Commands.Automation/Cmdlet/NewAzureAutomationWebhook.cs @@ -64,7 +64,7 @@ public class NewAzureAutomationWebhook : AzureAutomationBaseCmdlet /// /// Gets or sets the Runbook parameters /// - [Parameter(Mandatory = false, ValueFromPipelineByPropertyName = true, + [Parameter(Mandatory = false, ValueFromPipelineByPropertyName = false, HelpMessage = "The Runbook parameters name/value.")] public IDictionary Parameters { get; set; } diff --git a/src/ResourceManager/Automation/Commands.Automation/Cmdlet/RegisterAzureAutomationScheduledRunbook.cs b/src/ResourceManager/Automation/Commands.Automation/Cmdlet/RegisterAzureAutomationScheduledRunbook.cs index 3cc9ce57f463..a76a5e401cae 100644 --- a/src/ResourceManager/Automation/Commands.Automation/Cmdlet/RegisterAzureAutomationScheduledRunbook.cs +++ b/src/ResourceManager/Automation/Commands.Automation/Cmdlet/RegisterAzureAutomationScheduledRunbook.cs @@ -47,7 +47,7 @@ public class RegisterAzureAutomationScheduledRunbook : AzureAutomationBaseCmdlet /// /// Gets or sets the runbook parameters. /// - [Parameter(ParameterSetName = AutomationCmdletParameterSets.ByRunbookNameAndScheduleName, Mandatory = false, ValueFromPipelineByPropertyName = true, + [Parameter(ParameterSetName = AutomationCmdletParameterSets.ByRunbookNameAndScheduleName, Mandatory = false, ValueFromPipelineByPropertyName = false, HelpMessage = "The runbook parameters.")] public IDictionary Parameters { get; set; } diff --git a/src/ResourceManager/Automation/Commands.Automation/Cmdlet/RemoveAzureAutomationDscConfiguration.cs b/src/ResourceManager/Automation/Commands.Automation/Cmdlet/RemoveAzureAutomationDscConfiguration.cs new file mode 100644 index 000000000000..e008d76d0f3a --- /dev/null +++ b/src/ResourceManager/Automation/Commands.Automation/Cmdlet/RemoveAzureAutomationDscConfiguration.cs @@ -0,0 +1,56 @@ +// ---------------------------------------------------------------------------------- +// +// 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 Microsoft.Azure.Commands.Automation.Properties; +using System.Management.Automation; +using System.Security.Permissions; + +namespace Microsoft.Azure.Commands.Automation.Cmdlet +{ + /// + /// Remove a DSC configuration + /// + [Cmdlet(VerbsCommon.Remove, "AzureRmAutomationDscConfiguration")] + public class RemoveAzureAutomationDscConfiguration : AzureAutomationBaseCmdlet + { + /// + /// Gets or sets the Configuration name. + /// + [Parameter(Position = 2, Mandatory = true, ValueFromPipelineByPropertyName = true, + HelpMessage = "The configuration name.")] + [Alias("ConfigurationName")] + [ValidateNotNullOrEmpty] + public string Name { get; set; } + + [Parameter(Position = 3, HelpMessage = "Force confirmation of the removal of the configuration")] + public SwitchParameter Force { get; set; } + + /// + /// Execute this cmdlet. + /// + [PermissionSet(SecurityAction.Demand, Name = "FullTrust")] + protected override void AutomationProcessRecord() + { + ConfirmAction( + Force.IsPresent, + string.Format(Resources.RemovingAzureAutomationDscConfigurationWarning, "DSC Configuration"), + string.Format(Resources.RemoveAzureAutomationResourceDescription, "DSC Configuration"), + Name, + () => this.AutomationClient.DeleteConfiguration( + this.ResourceGroupName, + this.AutomationAccountName, + this.Name)); + } + } +} diff --git a/src/ResourceManager/Automation/Commands.Automation/Cmdlet/RemoveAzureAutomationDscNodeConfiguration.cs b/src/ResourceManager/Automation/Commands.Automation/Cmdlet/RemoveAzureAutomationDscNodeConfiguration.cs new file mode 100644 index 000000000000..2a43a5a37ea2 --- /dev/null +++ b/src/ResourceManager/Automation/Commands.Automation/Cmdlet/RemoveAzureAutomationDscNodeConfiguration.cs @@ -0,0 +1,60 @@ +// ---------------------------------------------------------------------------------- +// +// 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 Microsoft.Azure.Commands.Automation.Properties; +using System.Management.Automation; +using System.Security.Permissions; + +namespace Microsoft.Azure.Commands.Automation.Cmdlet +{ + /// + /// Remove a DSC configuration + /// + [Cmdlet(VerbsCommon.Remove, "AzureRmAutomationDscNodeConfiguration")] + public class RemoveAzureAutomationDscNodeConfiguration : AzureAutomationBaseCmdlet + { + /// + /// Gets or sets the Configuration name. + /// + [Parameter(Position = 2, Mandatory = true, ValueFromPipelineByPropertyName = true, + HelpMessage = "The node configuration name.")] + [Alias("NodeConfigurationName")] + [ValidateNotNullOrEmpty] + public string Name { get; set; } + + [Parameter(Position = 3, HelpMessage = "Force confirmation of the removal of the node configuration")] + public SwitchParameter Force { get; set; } + + [Parameter(Position = 4, HelpMessage = "Remove the node configuration even if the node configuration is mapped to one or more nodes")] + public SwitchParameter IgnoreNodeMappings { get; set; } + + /// + /// Execute this cmdlet. + /// + [PermissionSet(SecurityAction.Demand, Name = "FullTrust")] + protected override void AutomationProcessRecord() + { + ConfirmAction( + Force.IsPresent, + string.Format(Resources.RemovingAzureAutomationResourceWarning, "DSC node configuration"), + string.Format(Resources.RemoveAzureAutomationResourceDescription, "DSC node configuration"), + Name, + () => this.AutomationClient.DeleteNodeConfiguration( + this.ResourceGroupName, + this.AutomationAccountName, + this.Name, + IgnoreNodeMappings.IsPresent)); + } + } +} diff --git a/src/ResourceManager/Automation/Commands.Automation/Cmdlet/StartAzureAutomationDscCompilationJob.cs b/src/ResourceManager/Automation/Commands.Automation/Cmdlet/StartAzureAutomationDscCompilationJob.cs index 53a8afe1cc11..884e3b705e42 100644 --- a/src/ResourceManager/Automation/Commands.Automation/Cmdlet/StartAzureAutomationDscCompilationJob.cs +++ b/src/ResourceManager/Automation/Commands.Automation/Cmdlet/StartAzureAutomationDscCompilationJob.cs @@ -42,7 +42,7 @@ public class StartAzureAutomationDscCompilationJob : AzureAutomationBaseCmdlet /// /// Gets or sets the configuration parameters. /// - [Parameter(Mandatory = false, HelpMessage = "The compilation job parameters.")] + [Parameter(Mandatory = false, ValueFromPipelineByPropertyName = false, HelpMessage = "The compilation job parameters.")] public IDictionary Parameters { get; set; } /// diff --git a/src/ResourceManager/Automation/Commands.Automation/Cmdlet/StartAzureAutomationRunbook.cs b/src/ResourceManager/Automation/Commands.Automation/Cmdlet/StartAzureAutomationRunbook.cs index 04c562e53044..75c485f25181 100644 --- a/src/ResourceManager/Automation/Commands.Automation/Cmdlet/StartAzureAutomationRunbook.cs +++ b/src/ResourceManager/Automation/Commands.Automation/Cmdlet/StartAzureAutomationRunbook.cs @@ -12,12 +12,21 @@ // limitations under the License. // ---------------------------------------------------------------------------------- +using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; using System.Management.Automation; +using System.Management.Automation.Runspaces; using System.Security.Permissions; +using System.Threading; using Microsoft.Azure.Commands.Automation.Common; +using Microsoft.Azure.Commands.Automation.Model; using Job = Microsoft.Azure.Commands.Automation.Model.Job; +using JobStream = Microsoft.Azure.Commands.Automation.Model.JobStream; +using Microsoft.Azure.Commands.Automation.Properties; namespace Microsoft.Azure.Commands.Automation.Cmdlet @@ -25,14 +34,31 @@ namespace Microsoft.Azure.Commands.Automation.Cmdlet /// /// Starts an Azure automation runbook. /// - [Cmdlet(VerbsLifecycle.Start, "AzureRmAutomationRunbook", DefaultParameterSetName = AutomationCmdletParameterSets.ByRunbookName)] - [OutputType(typeof(Job))] + [Cmdlet(VerbsLifecycle.Start, "AzureRmAutomationRunbook", DefaultParameterSetName = AutomationCmdletParameterSets.ByAsynchronousReturnJob)] + [OutputType(typeof(Job), ParameterSetName = new []{ AutomationCmdletParameterSets.ByAsynchronousReturnJob })] + [OutputType(typeof(PSObject), ParameterSetName = new []{ AutomationCmdletParameterSets.BySynchronousReturnJobOutput })] public class StartAzureAutomationRunbook : AzureAutomationBaseCmdlet { + /// + /// True to wait for job output + /// + private bool wait; + + /// + /// Timeout value + /// + private int timeout = Constants.MaxWaitSeconds; + + /// + /// Polling interval + /// + private const int pollingIntervalInSeconds = 5; + /// /// Gets or sets the runbook name /// - [Parameter(ParameterSetName = AutomationCmdletParameterSets.ByRunbookName, Position = 2, Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The runbook name.")] + [Parameter(ParameterSetName = AutomationCmdletParameterSets.ByAsynchronousReturnJob, Position = 2, Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The runbook name.")] + [Parameter(ParameterSetName = AutomationCmdletParameterSets.BySynchronousReturnJobOutput, Position = 2, Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The runbook name.")] [ValidateNotNullOrEmpty] [Alias("RunbookName")] public string Name { get; set; } @@ -40,27 +66,102 @@ public class StartAzureAutomationRunbook : AzureAutomationBaseCmdlet /// /// Gets or sets the runbook parameters. /// - [Parameter(Mandatory = false, ValueFromPipelineByPropertyName = true, HelpMessage = "The runbook parameters.")] + [Parameter(ParameterSetName = AutomationCmdletParameterSets.ByAsynchronousReturnJob, Mandatory = false, HelpMessage = "The runbook parameters.")] + [Parameter(ParameterSetName = AutomationCmdletParameterSets.BySynchronousReturnJobOutput, Mandatory = false, HelpMessage = "The runbook parameters.")] public IDictionary Parameters { get; set; } /// /// Gets or sets the optional hybrid agent friendly name upon which the runbook should be executed. /// - [Parameter(Mandatory = false, ValueFromPipelineByPropertyName = true, HelpMessage = "Optional name of the hybrid agent which should execute the runbook")] + [Parameter(ParameterSetName = AutomationCmdletParameterSets.ByAsynchronousReturnJob, Mandatory = false, ValueFromPipelineByPropertyName = true, HelpMessage = "Optional name of the hybrid agent which should execute the runbook")] + [Parameter(ParameterSetName = AutomationCmdletParameterSets.BySynchronousReturnJobOutput, Mandatory = false, ValueFromPipelineByPropertyName = true, HelpMessage = "Optional name of the hybrid agent which should execute the runbook")] [Alias("HybridWorker")] public string RunOn { get; set; } + + /// + /// Gets or sets the switch parameter to wait for job output + /// + [Parameter(Mandatory = false, ParameterSetName = AutomationCmdletParameterSets.BySynchronousReturnJobOutput, HelpMessage = "Wait for job to complete, suspend, or fail.")] + public SwitchParameter Wait + { + get { return this.wait; } + set { this.wait = value; } + } + + /// + /// Gets or sets the switch parameter to wait for job output + /// + [Parameter(Mandatory = false, ParameterSetName = AutomationCmdletParameterSets.BySynchronousReturnJobOutput, HelpMessage = "Maximum time in seconds to wait for job completion. Default max wait time is 10800 seconds.")] + public int MaxWaitSeconds + { + get { return this.timeout; } + set { this.timeout = value; } + } + /// /// Execute this cmdlet. /// [PermissionSet(SecurityAction.Demand, Name = "FullTrust")] protected override void AutomationProcessRecord() { - Job job = null; + var job = this.AutomationClient.StartRunbook(this.ResourceGroupName, this.AutomationAccountName, this.Name, this.Parameters, this.RunOn); + + // if no wait, return job object + if (!this.Wait.IsPresent) + { + this.WriteObject(job); + return; + } + + // wait for job completion + if (!PollJobCompletion(job.JobId)) + { + throw new ResourceCommonException(typeof(Job), string.Format(CultureInfo.CurrentCulture, Resources.JobCompletionMaxWaitReached)); + } + + // retrieve job streams + var nextLink = string.Empty; + do + { + var jobOutputList = this.AutomationClient.GetJobStream(this.ResourceGroupName, this.AutomationAccountName, job.JobId, null, StreamType.Output.ToString(), ref nextLink); + foreach (var jobOutputRecord in jobOutputList) + { + var response = this.AutomationClient.GetJobStreamRecordAsPsObject(this.ResourceGroupName, this.AutomationAccountName, job.JobId, jobOutputRecord.StreamRecordId); + this.GenerateCmdletOutput(response); + } + } while (!string.IsNullOrEmpty(nextLink)); + } + + private bool PollJobCompletion(Guid jobId) + { + var timeoutIncrement = 0; + do + { + Thread.Sleep(TimeSpan.FromSeconds(pollingIntervalInSeconds)); + timeoutIncrement += pollingIntervalInSeconds; + + var job = this.AutomationClient.GetJob(this.ResourceGroupName, this.AutomationAccountName, jobId); + if (!IsJobTerminalState(job)) + { + if (IsVerbose()) WriteVerbose(string.Format(Resources.JobProgressState, job.JobId, job.Status, DateTime.Now)); + } + else + { + if (IsVerbose()) WriteVerbose(string.Format(Resources.JobTerminalState, job.JobId, job.Status, DateTime.Now)); + return true; + } + } while (this.MaxWaitSeconds < 0 || timeoutIncrement <= this.MaxWaitSeconds); - job = this.AutomationClient.StartRunbook(this.ResourceGroupName, this.AutomationAccountName, this.Name, this.Parameters, this.RunOn); + return false; + } - this.WriteObject(job); + private static bool IsJobTerminalState(Job job) + { + return job.Status.Equals(JobState.Completed.ToString(), StringComparison.OrdinalIgnoreCase) || + job.Status.Equals(JobState.Failed.ToString(), StringComparison.OrdinalIgnoreCase) || + job.Status.Equals(JobState.Stopped.ToString(), StringComparison.OrdinalIgnoreCase) || + job.Status.Equals(JobState.Suspended.ToString(), StringComparison.OrdinalIgnoreCase); } } } diff --git a/src/ResourceManager/Automation/Commands.Automation/Commands.Automation.csproj b/src/ResourceManager/Automation/Commands.Automation/Commands.Automation.csproj index eb5ac70d3dce..c7b3e78a4ea8 100644 --- a/src/ResourceManager/Automation/Commands.Automation/Commands.Automation.csproj +++ b/src/ResourceManager/Automation/Commands.Automation/Commands.Automation.csproj @@ -63,9 +63,9 @@ ..\..\..\packages\Microsoft.Azure.Common.Authentication.1.5.2-preview\lib\net45\Microsoft.Azure.Common.Authentication.dll True - + False - ..\..\..\packages\Microsoft.Azure.Management.Automation.0.50.2-prerelease\lib\portable-net45+wp8+wpa81+win\Microsoft.Azure.Management.Automation.dll + ..\..\..\packages\Microsoft.Azure.Management.Automation.2.0.0\lib\portable-net45+wp8+wpa81+win\Microsoft.Azure.Management.Automation.dll False @@ -164,10 +164,12 @@ + + diff --git a/src/ResourceManager/Automation/Commands.Automation/Common/AutomationClient.cs b/src/ResourceManager/Automation/Commands.Automation/Common/AutomationClient.cs index 775f13b74d73..22194c7c10a4 100644 --- a/src/ResourceManager/Automation/Commands.Automation/Common/AutomationClient.cs +++ b/src/ResourceManager/Automation/Commands.Automation/Common/AutomationClient.cs @@ -18,17 +18,20 @@ using System.Globalization; using System.Linq; using System.IO; +using System.Management.Automation; using System.Net; using System.Security; using System.Security.Cryptography.X509Certificates; -using Microsoft.Azure.Commands.Automation.Cmdlet; using Microsoft.Azure.Commands.Automation.Model; using Microsoft.Azure.Commands.Automation.Properties; using Microsoft.Azure.Management.Automation; using Microsoft.Azure.Management.Automation.Models; using Microsoft.Azure.Common.Authentication.Models; using Newtonsoft.Json; +using Microsoft.Azure.Common.Authentication; +using Hyak.Common; +using AutomationManagement = Microsoft.Azure.Management.Automation; using AutomationAccount = Microsoft.Azure.Commands.Automation.Model.AutomationAccount; using Module = Microsoft.Azure.Commands.Automation.Model.Module; using Runbook = Microsoft.Azure.Commands.Automation.Model.Runbook; @@ -43,11 +46,6 @@ namespace Microsoft.Azure.Commands.Automation.Common { - using AutomationManagement = Azure.Management.Automation; - using Microsoft.Azure.Common.Authentication; - using Hyak.Common; - - public partial class AutomationClient : IAutomationClient { private readonly AutomationManagement.IAutomationManagementClient automationManagementClient; @@ -996,6 +994,47 @@ public JobStreamRecord GetJobStreamRecord(string resourceGroupName, string autom return new JobStreamRecord(response.JobStream, resourceGroupName, automationAccountName, jobId); } + public object GetJobStreamRecordAsPsObject(string resourceGroupName, string automationAccountName, Guid jobId, string jobStreamId) + { + var response = this.automationManagementClient.JobStreams.Get(resourceGroupName, automationAccountName, jobId, jobStreamId); + + if (response.JobStream.Properties == null || response.JobStream.Properties.Value == null) return null; + + // PowerShell Workflow runbook jobs would have the below additional properties, remove them from job output + // we do not know the runbook type, remove will only remove if exists + response.JobStream.Properties.Value.Remove("PSComputerName"); + response.JobStream.Properties.Value.Remove("PSShowComputerName"); + response.JobStream.Properties.Value.Remove("PSSourceJobInstanceId"); + + var paramTable = new Hashtable(); + + foreach (var kvp in response.JobStream.Properties.Value) + { + object paramValue; + try + { + paramValue = ((object)PowerShellJsonConverter.Deserialize(kvp.Value.ToString())); + } + catch (CmdletInvocationException exception) + { + if (!exception.Message.Contains("Invalid JSON primitive")) + throw; + + paramValue = kvp.Value; + } + + // for primitive outputs, the record will be in form "value" : "primitive type value". Return the key and return the primitive type value + if (response.JobStream.Properties.Value.Count == 1 && response.JobStream.Properties.Value.ContainsKey("value")) + { + return paramValue; + } + + paramTable.Add(kvp.Key, paramValue); + } + + return paramTable; + } + public Job GetJob(string resourceGroupName, string automationAccountName, Guid Id) { var job = this.automationManagementClient.Jobs.Get(resourceGroupName, automationAccountName, Id).Job; @@ -1637,15 +1676,6 @@ private IDictionary ProcessRunbookParameters(string resourceGrou string.Format(CultureInfo.CurrentCulture, Resources.InvalidRunbookParameters)); } - var hasJobStartedBy = - filteredParameters.Any(filteredParameter => filteredParameter.Key == Constants.JobStartedByParameterName); - - if (!hasJobStartedBy) - { - filteredParameters.Add(Constants.JobStartedByParameterName, - PowerShellJsonConverter.Serialize(Constants.ClientIdentity)); - } - return filteredParameters; } diff --git a/src/ResourceManager/Automation/Commands.Automation/Common/AutomationClientDSC.cs b/src/ResourceManager/Automation/Commands.Automation/Common/AutomationClientDSC.cs index af9233e1857d..2923d3555e2d 100644 --- a/src/ResourceManager/Automation/Commands.Automation/Common/AutomationClientDSC.cs +++ b/src/ResourceManager/Automation/Commands.Automation/Common/AutomationClientDSC.cs @@ -300,6 +300,29 @@ public Model.DscConfiguration CreateConfiguration( } } + public void DeleteConfiguration(string resourceGroupName, string automationAccountName, string name) + { + Requires.Argument("ResourceGroupName", resourceGroupName).NotNull(); + Requires.Argument("AutomationAccountName", automationAccountName).NotNull(); + using (var request = new RequestSettings(this.automationManagementClient)) + { + try + { + this.automationManagementClient.Configurations.Delete(resourceGroupName, automationAccountName, name); + } + catch (CloudException cloudException) + { + if (cloudException.Response.StatusCode == HttpStatusCode.NoContent) + { + throw new ResourceNotFoundException( + typeof(Model.DscConfiguration), + string.Format(CultureInfo.CurrentCulture, Resources.ConfigurationNotFound, name)); + } + throw; + } + } + } + #endregion #region DscMetaConfig Operations @@ -568,7 +591,7 @@ public Model.DscNode GetDscNodeById( IEnumerable dscNodes; - if (!String.IsNullOrEmpty(status)) + if (!string.IsNullOrEmpty(status)) { dscNodes = AutomationManagementClient.ContinuationTokenHandler( skipToken => @@ -727,7 +750,7 @@ string nodeConfigurationName automationAccountName, new DscNodePatchParameters { - Id = nodeId, + NodeId = nodeId, NodeConfiguration = nodeConfiguration }).Node; @@ -1262,6 +1285,48 @@ public Model.NodeConfiguration CreateNodeConfiguration( } } + public void DeleteNodeConfiguration(string resourceGroupName, string automationAccountName, string name, bool ignoreNodeMappings) + { + Requires.Argument("ResourceGroupName", resourceGroupName).NotNull(); + Requires.Argument("AutomationAccountName", automationAccountName).NotNull(); + Requires.Argument("NodeConfigurationName", name).NotNull(); + + using (var request = new RequestSettings(this.automationManagementClient)) + { + try + { + if (ignoreNodeMappings) + { + this.automationManagementClient.NodeConfigurations.Delete(resourceGroupName, automationAccountName, name); + } + else + { + var nodeList = this.ListDscNodesByNodeConfiguration(resourceGroupName, automationAccountName, name, null); + if (nodeList.Any()) + { + throw new ResourceCommonException( + typeof (Model.NodeConfiguration), + string.Format(CultureInfo.CurrentCulture, Resources.CannotDeleteNodeConfiguration, name)); + } + else + { + this.automationManagementClient.NodeConfigurations.Delete(resourceGroupName, automationAccountName, name); + } + } + } + catch (CloudException cloudException) + { + if (cloudException.Response.StatusCode == HttpStatusCode.NotFound) + { + throw new ResourceNotFoundException( + typeof(Model.NodeConfiguration), + string.Format(CultureInfo.CurrentCulture, Resources.NodeConfigurationNotFound, name)); + } + throw; + } + } + } + #endregion #region dsc reports diff --git a/src/ResourceManager/Automation/Commands.Automation/Common/AutomationCmdletParameterSet.cs b/src/ResourceManager/Automation/Commands.Automation/Common/AutomationCmdletParameterSet.cs index 2996d41e1ae9..071510a61831 100644 --- a/src/ResourceManager/Automation/Commands.Automation/Common/AutomationCmdletParameterSet.cs +++ b/src/ResourceManager/Automation/Commands.Automation/Common/AutomationCmdletParameterSet.cs @@ -79,6 +79,16 @@ internal static class AutomationCmdletParameterSets /// internal const string ByRunbookName = "ByRunbookName"; + /// + /// The ByAsynchronousReturnJob. + /// + internal const string ByAsynchronousReturnJob = "ByAsynchronousReturnJob"; + + /// + /// The BySynchronousReturnJob. + /// + internal const string BySynchronousReturnJobOutput = "BySynchronousReturnJobOutput"; + /// /// The Configuration name parameter set. /// diff --git a/src/ResourceManager/Automation/Commands.Automation/Common/Constants.cs b/src/ResourceManager/Automation/Commands.Automation/Common/Constants.cs index fd95c4bcd3ea..b35962ebf345 100644 --- a/src/ResourceManager/Automation/Commands.Automation/Common/Constants.cs +++ b/src/ResourceManager/Automation/Commands.Automation/Common/Constants.cs @@ -63,6 +63,8 @@ public class AutomationAccountState public const int JobSummaryLength = 80; + public const int MaxWaitSeconds = 10800; + // The template file is a json public const string TemplateFile = @"https://eus2oaasibizamarketprod1.blob.core.windows.net/automationdscpreview/azuredeployV2.json"; diff --git a/src/ResourceManager/Automation/Commands.Automation/Common/IAutomationClient.cs b/src/ResourceManager/Automation/Commands.Automation/Common/IAutomationClient.cs index 7795b99e17ef..704bccf7a9d1 100644 --- a/src/ResourceManager/Automation/Commands.Automation/Common/IAutomationClient.cs +++ b/src/ResourceManager/Automation/Commands.Automation/Common/IAutomationClient.cs @@ -62,6 +62,8 @@ public interface IAutomationClient IEnumerable ListNodeConfigurations(string resourceGroupName, string automationAccountName, string rollupStatus); NodeConfiguration CreateNodeConfiguration(string resourceGroupName, string automationAccountName, string sourcePath, string nodeConfiguraionName, bool overWrite); + + void DeleteNodeConfiguration(string resourceGroupName, string automationAccountName, string name, bool ignoreNodeMappings); #endregion #region Configurations @@ -74,6 +76,8 @@ public interface IAutomationClient DirectoryInfo GetConfigurationContent(string resourceGroupName, string automationAccountName, string configurationName, bool? isDraft, string outputFolder, bool overwriteExistingFile); + void DeleteConfiguration(string resourceGroupName, string automationAccountName, string name); + #endregion #region AgentRegistrationInforamtion @@ -237,6 +241,8 @@ IEnumerable GetJobStream(string resourceGroupName, string automationA JobStreamRecord GetJobStreamRecord(string resourceGroupName, string automationAccountName, Guid jobId, string jobStreamId); + object GetJobStreamRecordAsPsObject(string resourceGroupName, string automationAccountName, Guid jobId, string jobStreamId); + #endregion #region Certificates diff --git a/src/ResourceManager/Automation/Commands.Automation/Model/AutomationAccount.cs b/src/ResourceManager/Automation/Commands.Automation/Model/AutomationAccount.cs index 016d4b3ca972..e0b0e52c5f25 100644 --- a/src/ResourceManager/Automation/Commands.Automation/Model/AutomationAccount.cs +++ b/src/ResourceManager/Automation/Commands.Automation/Model/AutomationAccount.cs @@ -48,6 +48,8 @@ public AutomationAccount(string resourceGroupName, AutomationManagement.Models.A this.ResourceGroupName = automationAccount.Id.Substring(1).Split(Convert.ToChar("/"))[3]; } + this.SubscriptionId = automationAccount.Id.Substring(1).Split(Convert.ToChar("/"))[1]; + this.AutomationAccountName = automationAccount.Name; this.Location = automationAccount.Location; @@ -73,6 +75,11 @@ public AutomationAccount() { } + /// + /// Gets or sets the Subscription ID + /// + public string SubscriptionId { get; set; } + /// /// Gets or sets the resource group name. /// diff --git a/src/ResourceManager/Automation/Commands.Automation/Model/DscNode.cs b/src/ResourceManager/Automation/Commands.Automation/Model/DscNode.cs index fdae4b164f0e..326b655623c3 100644 --- a/src/ResourceManager/Automation/Commands.Automation/Model/DscNode.cs +++ b/src/ResourceManager/Automation/Commands.Automation/Model/DscNode.cs @@ -39,7 +39,7 @@ public DscNode(string resourceGroupName, string automationAccountName, Automatio this.ResourceGroupName = resourceGroupName; this.AutomationAccountName = automationAccountName; this.Name = node.Name; - this.Id = node.Id; + this.Id = node.NodeId.ToString("D"); this.IpAddress = node.Ip; this.LastSeen = node.LastSeen.ToLocalTime(); this.RegistrationTime = node.RegistrationTime.ToLocalTime(); diff --git a/src/ResourceManager/Automation/Commands.Automation/Model/DscNodeReport.cs b/src/ResourceManager/Automation/Commands.Automation/Model/DscNodeReport.cs index a0c675392531..cf24712505de 100644 --- a/src/ResourceManager/Automation/Commands.Automation/Model/DscNodeReport.cs +++ b/src/ResourceManager/Automation/Commands.Automation/Model/DscNodeReport.cs @@ -36,7 +36,7 @@ public DscNodeReport(string resourceGroupName, string automationAccountName, str Requires.Argument("ResourceGroupName", resourceGroupName).NotNull(); Requires.Argument("AutomationAccountName", automationAccountName).NotNull(); Requires.Argument("dscNodeReport", dscNodeReport).NotNull(); - Requires.Argument("dscNodeReport", dscNodeReport.Id).NotNull(); + Requires.Argument("dscNodeReport", dscNodeReport.ReportId).NotNull(); this.ResourceGroupName = resourceGroupName; this.AutomationAccountName = automationAccountName; @@ -44,7 +44,7 @@ public DscNodeReport(string resourceGroupName, string automationAccountName, str this.EndTime = dscNodeReport.EndTime; this.LastModifiedTime = dscNodeReport.LastModifiedTime; this.ReportType = dscNodeReport.Type; - this.Id = dscNodeReport.Id.ToString("D"); + this.Id = dscNodeReport.ReportId.ToString("D"); this.NodeId = nodeId; this.Status = dscNodeReport.Status; this.RefreshMode = dscNodeReport.RefreshMode; diff --git a/src/ResourceManager/Automation/Commands.Automation/Model/Job.cs b/src/ResourceManager/Automation/Commands.Automation/Model/Job.cs index 43f7fd95d66b..2fbf68cd60d9 100644 --- a/src/ResourceManager/Automation/Commands.Automation/Model/Job.cs +++ b/src/ResourceManager/Automation/Commands.Automation/Model/Job.cs @@ -61,6 +61,7 @@ public Job(string resourceGroupName, string accountName, Azure.Management.Automa this.EndTime = job.Properties.EndTime.HasValue ? job.Properties.EndTime.Value.ToLocalTime() : (DateTimeOffset?) null; this.LastStatusModifiedTime = job.Properties.LastStatusModifiedTime; this.HybridWorker = job.Properties.RunOn; + this.StartedBy = job.Properties.StartedBy; this.JobParameters = new Hashtable(StringComparer.InvariantCultureIgnoreCase); foreach (var kvp in job.Properties.Parameters) { @@ -161,5 +162,10 @@ public Job() /// Gets or sets the HybridWorker. /// public string HybridWorker { get; set; } + + /// + /// Gets or sets the StartedBy property. + /// + public string StartedBy { get; set; } } } diff --git a/src/ResourceManager/Automation/Commands.Automation/Properties/Resources.Designer.cs b/src/ResourceManager/Automation/Commands.Automation/Properties/Resources.Designer.cs index db513eb8c67d..68a054224f0c 100644 --- a/src/ResourceManager/Automation/Commands.Automation/Properties/Resources.Designer.cs +++ b/src/ResourceManager/Automation/Commands.Automation/Properties/Resources.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.34014 +// Runtime Version:4.0.30319.18449 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -96,6 +96,15 @@ internal static string AutomationOperationFailed { } } + /// + /// Looks up a localized string similar to The node configuration '{0}' is currently assigned to one or more nodes. Either specify the IgnoreNodeMappings parameter, or reassign these nodes to a different node configuration, to delete this node configuration.. + /// + internal static string CannotDeleteNodeConfiguration { + get { + return ResourceManager.GetString("CannotDeleteNodeConfiguration", resourceCulture); + } + } + /// /// Looks up a localized string similar to The certificate already exists. Certificate name: {0}.. /// @@ -348,6 +357,15 @@ internal static string InvalidRunbookTypeForExtension { } } + /// + /// Looks up a localized string similar to Job completion maximum wait time reached.. + /// + internal static string JobCompletionMaxWaitReached { + get { + return ResourceManager.GetString("JobCompletionMaxWaitReached", resourceCulture); + } + } + /// /// Looks up a localized string similar to The Job having Id: {0} was not found.. /// @@ -357,6 +375,15 @@ internal static string JobNotFound { } } + /// + /// Looks up a localized string similar to "Job progress state : Id {0}, state {1}, time {2}". + /// + internal static string JobProgressState { + get { + return ResourceManager.GetString("JobProgressState", resourceCulture); + } + } + /// /// Looks up a localized string similar to The job schedule was not found. Runbook name {0}. Schedule name {1}.. /// @@ -375,6 +402,15 @@ internal static string JobScheduleWithIdNotFound { } } + /// + /// Looks up a localized string similar to "Job terminal state : Id {0}, state {1}, time {2}". + /// + internal static string JobTerminalState { + get { + return ResourceManager.GetString("JobTerminalState", resourceCulture); + } + } + /// /// Looks up a localized string similar to Metaconfig already exists. Specify the parameter to force an overwrite. File: {0}. /// @@ -555,6 +591,15 @@ internal static string RemoveDscNodeWarning { } } + /// + /// Looks up a localized string similar to Are you sure you want to remove the Azure Automation {0}? Note: Any DSC node configurations under this DSC configuration will not be removed.. + /// + internal static string RemovingAzureAutomationDscConfigurationWarning { + get { + return ResourceManager.GetString("RemovingAzureAutomationDscConfigurationWarning", resourceCulture); + } + } + /// /// Looks up a localized string similar to Are you sure you want to remove the Azure Automation {0} ?. /// diff --git a/src/ResourceManager/Automation/Commands.Automation/Properties/Resources.resx b/src/ResourceManager/Automation/Commands.Automation/Properties/Resources.resx index 999997efde7f..176debb5b9aa 100644 --- a/src/ResourceManager/Automation/Commands.Automation/Properties/Resources.resx +++ b/src/ResourceManager/Automation/Commands.Automation/Properties/Resources.resx @@ -173,6 +173,10 @@ Are you sure you want to remove the Azure Automation {0} ? Automation + + Are you sure you want to remove the Azure Automation {0}? Note: Any DSC node configurations under this DSC configuration will not be removed. + Automation + Resource exist. Automation @@ -384,6 +388,10 @@ The Dsc Configuration was not found. Configuration name {0}. Automation + + The node configuration '{0}' is currently assigned to one or more nodes. Either specify the IgnoreNodeMappings parameter, or reassign these nodes to a different node configuration, to delete this node configuration. + Automation + The Webhook with Name: {0} was not found. Automation @@ -421,4 +429,14 @@ ConfigurationData cannot be part of the job parameters. You can specify the ConfigurationData using the {0} switch + + Job completion maximum wait time reached. + Automation + + + "Job progress state : Id {0}, state {1}, time {2}" + + + "Job terminal state : Id {0}, state {1}, time {2}" + \ No newline at end of file diff --git a/src/ResourceManager/Automation/Commands.Automation/packages.config b/src/ResourceManager/Automation/Commands.Automation/packages.config index f30921fc01ab..4e8ace659683 100644 --- a/src/ResourceManager/Automation/Commands.Automation/packages.config +++ b/src/ResourceManager/Automation/Commands.Automation/packages.config @@ -4,7 +4,7 @@ - +