Social

torsdag den 14. juni 2018

Set Expiry Date on DevTest Lab VMs

A colleague of mine recently handed me the script from https://github.com/Azure-Samples/virtual-machines-powershell-auto-expired/ and asked for my help with what he thought was a permission issue.
The script is 2 years old, so things have changed quite a bit since, but it is just not very clever as it fetches all resources in an entire subscription (something this colleague did not have permission to do), which by all means is a bad idea.

I made some improvements and wanted to share. Below is simply run and you are prompted to select a lab, then one or more VMs in that lab, and finally in how many days the VMs should expire.

Note that I will not be updating below with fixes so grab it from Technet here.



# you can remove the TenantId if you have just a single tenant
$TenantId = ''
Select-AzureRmSubscription -TenantId $TenantId -SubscriptionId '' | Out-Null
Function Set-AzureVirtualMachineExpiredDate 
{ 
    [CmdletBinding()] 
    Param 
    ( 
        [Parameter(Mandatory=$true, ValueFromPipeline, Position=1)][ValidateNotNull()][String]$VMName, 
        [Parameter(Mandatory=$true)][ValidateNotNull()][String]$LabName, 
        [Parameter(Mandatory=$true)][ValidateNotNull()][String]$LabResourceGroupName,
        [Parameter(Mandatory=$true)][ValidateNotNull()][DateTime]$ExpiredUTCDate 
    ) 
 
    Begin{
        $Jobs = @()
    }

    Process
    {
        try {
            # get vm info 
            $targetVMInfo = Get-AzureRmResource -ResourceName "$LabName/$VMName" -ResourceGroupName $LabResourceGroupName `
                                                -ResourceType 'Microsoft.DevTestLab/labs/virtualMachines' -ExpandProperties
        }
        catch {
            Throw "$VMName not found in $LabName, error was:`n$_" 
        }
     
        # get vm properties 
        $vmProperties = $targetVMInfo.Properties 
     
        # set expired date
        $vmProperties | Add-Member -MemberType NoteProperty -Name expirationDate -Value $ExpiredUTCDate -Force 
        
        Write-Host "Setting expiry date to $ExpiredUTCDate on $LabName/$VMName..."
        $Jobs += Set-AzureRmResource -ResourceId $targetVMInfo.ResourceId -Properties $vmProperties -Force `
                    -ErrorAction Stop -AsJob
    } # end of process

    End
    {
        Write-Host "Waiting for jobs to complete..."
        $Jobs | Wait-Job | Receive-Job | ForEach-Object {
            Write-Host "Expiry date on $($_.Name) set to $($_.Properties.expirationDate)"
        }
    }
} 

$Lab = Get-AzureRmResource -ResourceType 'Microsoft.DevTestLab/labs' | Out-GridView -Title "Select DevTest Lab" -PassThru
$LabName = $Lab | Select-Object -ExpandProperty Name

$VM = Get-AzureRmResource -ResourceName "$LabName/*" -ResourceType 'Microsoft.DevTestLab/labs/virtualMachines' | `
        Out-GridView -Title "Select VM" -PassThru

$AddDays = 1..14 | Out-GridView -Title "Expire in days..." -PassThru

$VM | ForEach-Object {("$($_.Name)".Split('/') | Select-Object -Last 1)} | Set-AzureVirtualMachineExpiredDate `
                                    -LabName $LabName `
                                    -LabResourceGroupName $Lab.ResourceGroupName `
                                    -ExpiredUTCDate (Get-Date).AddDays($AddDays)

Out-GridView with Selected Properties

A script is worth a thousand words, right?



<#
 I use Out-GridView (alias: ogv) alot for interactively selecting objects. Also sometimes I need some extra information not 
 described in the object itself.
 Let's say we need to enumerate files in c:\temp for a list of computers. After collecting all the files we wish to use ogv 
 for displaying some of the properties like the name of the file, the size in kb and the computer on which the file is found. 
 We will pretend (because this is an example anyone can run) that the file object is missing the last part, hence we add it 
 to the object using Add-Member

 another usecase is simply that ogv will not show the properties we wish to see. Using Select-Object will create a new object
 and if we use the -Passthru parameter to ogv it will not be the original object we get. In below example we convert the size
 of each file to kb, which also uses Select-Object and a calculated property to do the conversion
#>

$ComputerNames = @($env:COMPUTERNAME)
$FilesInTemp = @()

foreach($ComputerName in $ComputerNames)
{
    $FilesInTemp += Invoke-Command -ScriptBlock {
        Get-ChildItem -Path c:\temp -File
    } -ComputerName $ComputerName | `
        Add-Member -Name MachineName -Value $ComputerName -MemberType NoteProperty -PassThru
}
# Now we have a list of files that we can select from, but below fails
$FilesInTemp |  Select-Object -Property Name, @{ Name = 'SizeInKb'; Expression = {  $_.Length/1KB }}, MachineName | `
                Out-GridView -Title "Select files to delete (example 1)" -PassThru | `
                Remove-Item -WhatIf
<#
 the problem is that Select-Object creates a new object with just the selected properties. Why Select-Object? Try running 
 the line below
#>
Get-ChildItem -Path c:\temp -File | Out-GridView
<# 
 we did get some decent properties, but it is showing the same we would get from a Format-Table, ie. the default properties
 if we want something different we use Select-Object, but as mentioned we get an entirely new object (with just the properties
 selected) , which is why Remove-Item fails
 A solution which can be applied in probably every case is found below
 The only difference is that we add the object to itself as a member and then later "extract" it before the pipe to Remove-Item
#>
$FilesInTemp = @()
foreach($ComputerName in $ComputerNames)
{
    $FilesInTemp += Invoke-Command -ScriptBlock {
        Get-ChildItem -Path c:\temp -File
    } -ComputerName $ComputerName | `
        Add-Member -Name MachineName -Value $ComputerName -MemberType NoteProperty -PassThru | `
        ForEach-Object {$_ | Add-Member -Name _self -Value $_ -MemberType NoteProperty -PassThru}
}

$FilesInTemp | Select-Object -Property Name, @{ Name = 'SizeInKb'; Expression = {  $_.Length/1KB }}, MachineName, _self | `
    Out-GridView -Title "Select files to delete (example 2)" -PassThru | `
    Select-Object -ExpandProperty _self | `
    Remove-Item -WhatIf

<#
 We can make this even easier with some helper functions. Below I have used _self as the property name. Some may recognize
 the name as used in Python, and equivalent of "this" in C#
#>

Function Add-Self
{
    process
    {
        ForEach-Object {$_ | Add-Member -Name '_self' -Value $_ -MemberType NoteProperty -PassThru}
    }
}

Function Get-Self
{
    process
    {
        $_ | Select-Object -ExpandProperty '_self'
    }
}
$FilesInTemp = @()
foreach($ComputerName in $ComputerNames)
{
    $FilesInTemp += Invoke-Command -ScriptBlock {
        Get-ChildItem -Path c:\temp -File
    } -ComputerName $ComputerName | `
        Add-Member -Name MachineName -Value $ComputerName -MemberType NoteProperty -PassThru | `
        Add-Self
}

$FilesInTemp | Select-Object -Property Name, @{ Name = 'SizeInKb'; Expression = {  $_.Length/1KB }}, MachineName, _self | `
    Out-GridView -Title "Select files to delete (example 3)" -PassThru | `
    Get-Self | `
    Remove-Item -WhatIf

torsdag den 13. juli 2017

[PowerShell] List of Azure public IPs

I needed a list of public IPs in Azure that was attached to a virtual network interface. PowerShell to the rescue.


(Get-AzureRmPublicIpAddress | Where-Object {$_.PublicIpAllocationMethod -eq 'Static' -and $_.IpC
onfiguration.Id -like '*Microsoft.Network/networkinterfaces*'} | Select-Object -ExpandProperty IpAddress | ForEach-Objec
t {"$_,"}) -join '' | clip

onsdag den 19. oktober 2016

CustomScriptExtension in ARM Templates and Shared Access Signature (SAS) Tokens

I had some trouble with a custom script extension where the script required a SAS token to download some software. The token was simply truncated after the first '&'.

After some digging I thought I had to put the SAS token into quotes, and when looking into C:\Packages\Plugins\Microsoft.Compute.CustomScriptExtension\1.8\RuntimeSettings\0.settings I found that it was a sensible solution. I could also copy the "commandToExecute" and run it and get the expected result. In the variables section I added a:


  "variables": {
    "singlequote": "'",

And then put single quotes around the parameters('SASToken'). But no dice. The token was still getting truncated, this time with a 'in front...

So I decided to get rid of the '&', at least temporarily. base64 encoding to the rescue. And Luckily there is an ARM template function for just that. In the script I then added:

$SASToken = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($SASToken))

Problem solved!

Seems to me that there is something odd in how the custom script extension calls PowerShell in this particular instance.

onsdag den 5. oktober 2016

Begin..Process..End and Error Handling

I had to wrap my mind around error handling and the begin..process..end function in PowerShell. It becomes really fun when I start throwing different ErrorActions after it!

This will be mostly some PowerShell snippets and their result. So without further ado, lets dive into some code!

This is a really simple function:

function myfunc
{
    [cmdletbinding()]
    param()

    begin
    {
        # some init code that throws an error
        try
        {
            throw 'some error'
            # code never reaches here
            Write-Output 'begin block'
        }
        catch [System.Exception]
        {
            Write-Error 'begin block'
        }
    }
    process
    {
        Write-Output 'process block'
    }
    end
    {
        Write-Output 'end block'
    }
}
Clear-Host
$VerbosePreference = "Continue"

Write-Host "-ErrorAction SilentlyContinue: the Write-Error in the begin block is suppressed" `
    -ForegroundColor Cyan
myfunc -ErrorAction SilentlyContinue
Write-Host "-ErrorAction Continue: displays the Write-Error in the begin block,
but the process and end block is executed" `
    -ForegroundColor Cyan
myfunc -ErrorAction Continue
Write-Host "-ErrorAction Stop: displays the Write-Error in the begin block. 
The Write-Error in the begin block becomes a terminating error. 
The process and end block is not executed" `
    -ForegroundColor Cyan
myfunc -ErrorAction Stop

The output is:




We see that for both ErrorActions Continue/SilentlyContinue that the process block is executed. When we use Stop then Write-Error becomes a terminating error and the pipeline is stopped.

Let us not dwell on that and move onto a function with some actual input:

# with input
function myfunc
{
    [cmdletbinding()]
    param(
        [Parameter(
            Position=0, 
            Mandatory=$true, 
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$true)
        ]
        $x
    )

    begin
    {
        # No errors in the begin block this time
        Write-Output 'begin block'
    }
    process
    {
        if($x -gt 2)
        {
            Write-Error "$x is too big to handle!"
        }
        # echo input
        Write-Output $x
    }
    end
    {
        Write-Output 'end block'
    }
}
Clear-Host
$VerbosePreference = "Continue"

Write-Host "-ErrorAction SilentlyContinue: the Write-Error in the process block is suppressed" `
    -ForegroundColor Cyan
@(1,2,3) | myfunc -ErrorAction SilentlyContinue

Write-Host "-ErrorAction Continue: The Write-Error in the process block is displayed,
but `$x is still echoed" `
    -ForegroundColor Cyan
@(1,2,3) | myfunc -ErrorAction Continue

Write-Host "-ErrorAction Stop: The Write-Error in the process block becomes a terminating error, 
`$x > 2 is NOT echoed" `
    -ForegroundColor Cyan
@(1,2,3) | myfunc -ErrorAction Stop

The output is:



Now we see that something uninteded is happening for both ErrorActions Continue/SilentlyContinue. 3 is echoed still. With Stop the story is as before, Write-Error becomes a terminating error and 3 is not echoed.

Now we basically just add a return statement:

# with input
function myfunc
{
    [cmdletbinding()]
    param(
        [Parameter(
            Position=0, 
            Mandatory=$true, 
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$true)
        ]
        $x
    )

    begin
    {
        # No errors in the begin block this time
        Write-Output 'begin block'
    }
    process
    {
        if($x -gt 2)
        {
            Write-Error "$x is too big to handle!"
            # continue on the pipeline. NOTE: continue does NOT continue but rather shuts down the pipeline completely
            return
        }
        # echo input
        Write-Output $x
    }
    end
    {
        Write-Output 'end block'
    }
}
Clear-Host
$VerbosePreference = "Continue"

Write-Host "-ErrorAction SilentlyContinue: the Write-Error in the process block is suppressed
(for both 3 and 4), and `$x > 2 is not echoed" `
    -ForegroundColor Cyan
@(1,2,3,4) | myfunc -ErrorAction SilentlyContinue

Write-Host "-ErrorAction Continue: The Write-Error in the process block is displayed
(twice, for both 3 and 4). `$x > 2 is not echoed" `
    -ForegroundColor Cyan
@(1,2,3,4) | myfunc -ErrorAction Continue
Write-Host 'The script keeps running' `
    -ForegroundColor Cyan

Write-Host "-ErrorAction Stop: The Write-Error in the process block becomes a terminating error,
'3' is NOT echoed. return is not exectuted hence the pipeline stops" `
    -ForegroundColor Cyan
@(1,2,3,4) | myfunc -ErrorAction Stop
Write-Host 'this is not reached' `
    -ForegroundColor Cyan

The output is:



We see that in all 3 cases that x greater than 2 is not echoed. Now ErrorAction Stop makes sense. We indicate that if the function fails for any input we do not wish to continue the script.

And we can add some error handling:

# with input
function myfunc
{
    [cmdletbinding()]
    param(
        [Parameter(
            Position=0, 
            Mandatory=$true, 
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$true)
        ]
        $x
    )

    begin
    {
        # No errors in the begin block this time
        Write-Output 'begin block'
    }
    process
    {
        try
        {
            if($x -gt 2)
            {
                # this puts the error into the $Error variable
                throw "$x is too big to handle!"

            }
            # echo input
            Write-Output $x
            }
        catch [System.Exception]
        {
            Write-Error $Error[0].Exception
            Write-Verbose "continue on the pipeline '$x'"
            return
        }
        Write-Verbose "continue on the pipeline '$x'"
    }
    end
    {
        Write-Output 'end block'
    }
}
Clear-Host
$VerbosePreference = "Continue"

Write-Host "-ErrorAction SilentlyContinue: the Write-Error in the process block is suppressed 
(for both 3 and 4), and `$x is not echoed" `
    -ForegroundColor Cyan
@(1,2,3,4) | myfunc -ErrorAction SilentlyContinue

Write-Host "-ErrorAction Continue: The Write-Error in the process block is displayed 
(twice, for both 3 and 4).`$x is not echoed" `
    -ForegroundColor Cyan
@(1,2,3,4) | myfunc -ErrorAction Continue
Write-Host 'The script keeps running' `
    -ForegroundColor Cyan

Write-Host "-ErrorAction Stop: The Write-Error in the process block becomes a terminating error, 
'3' is NOT echoed. return is not exectuted and the pipeline stops" `
    -ForegroundColor Cyan
@(1,2,3,4) | myfunc -ErrorAction Stop
Write-Host 'this is not reached' `
    -ForegroundColor Cyan

The output is:


I hope this helps understanding how some of the begin..process..end function works with regards to errors and error handling. I know I will be returning to this from time and again :D


tirsdag den 4. oktober 2016

ARM Template Tip: Names

Naming resources in ARM templates can be quite lengthy. This is an example of naming a network interface:

"name": "[concat(parameters('vmNamePrefix'), '-', padLeft(copyIndex(1), 2, '0'), variables('nicPostfix'), '-', padLeft(copyIndex(1), 2, '0'))]",

And we have to reference this at a later point for the virtual machine resource. If we then change the name, we will have to remember to change this reference also.

What we can do is to define the name in the variables section like this:

    "nic": {
      "name": "[concat(parameters('vmNamePrefix'), '-', padLeft('{0}', 4, '0'), variables('nicPostfix'), '-', padLeft('{0}', 4, '0'))]"
    }

(I like to group variables). And then reference this variable in the resource like:

"name": "[replace(variables('nic').name, '{0}', string(copyIndex(1)))]",

What I have done is to make {0} a placeholder and then replace it with the result from copyIndex(). We now have a central location to change the name if needed with no need to update any resources.

Would be cool if we had a template function for formatting:

"name": "[format(variables('nic').name, copyIndex(1), '-nic')]"

It would take the string as input and then a variable number of additional arguments. Ex.


"nic": {
   "name": "concat(parameters('vmNamePrefix'), '0{0}', '{1}')]"
}

would become({0} is replaced with the result from copyIndex(1) and {1} replaced with -nic):

"VM01-nic"

And it could be made more advanced, perhaps leaning on the good ol' sprintf.

torsdag den 29. september 2016

Logging webhooks using Azure Functions and OMS Log Analytics

We recently discussed webhooks internally at work and the question popped on how to maintain and log the activity. Webhooks normally have a limited timespan (could be years though), and they should generally be kept a secret even if they are accompanied by a token that authorizes the caller.

What better way to log the webhook calls than using OMS Log Analytics? Once the data is logged there you have a plethora of options on what to do with it. Go ask my colleague Stanislav.

I also wanted to try out the fairly new Azure Functions, which acts as a relay to Log Analytics Data Collector API. The webhook itself comes from an Azure Automation runbook.

I documented the entire solution on Github, and you can find the repository here - it takes you from A to Z on how to setup the various moving parts in Azure. I hope you can find some inspiration on how to manage your webhooks.

Søg i denne blog