Azure.ToolKit.IAAS.Management.psm1

Set-StrictMode -Version Latest

<#
.SYNOPSIS
Install the winrm cert for the azure vm onto the local machine
 
.DESCRIPTION
Install the winrim cert for the azure vm onto the local machine to enable powershell remoting from the local machine
 
.PARAMETER VMName
The name of the vm
 
.PARAMETER ServiceName
The name of the cloud service
#>

function Install-AzureVMWinRMCert
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)][string]$VMName,
        [Parameter(Mandatory = $true)][string]$ServiceName
    )
    
    Write-Output "Installing WinRM Certificate from VM: $VMName onto local machine"

    $VM = Get-AzureVM -name $vmName -ServiceName $serviceName -ErrorAction SilentlyContinue

    if($null -eq $VM)
    {
        throw "VM: $vmName does not exist in cloud service: $serviceName"
    }

    $winRMCert = ($VM| select-object -ExpandProperty vm).DefaultWinRMCertificateThumbprint
    
    if ($null -eq $winRMCert)
    {
        throw "Default WinRM Certificate Thumbprint has no value"
    }
    else
    {
        Write-Output "Default WinRM Certificate: $winRMCert found on VM: $VMName"
    }

    $installedCert = Get-Item Cert:\LocalMachine\Root\$winRMCert -ErrorAction SilentlyContinue

    if ($null -eq $installedCert)
    {
        $certTempFile = [IO.Path]::GetTempFileName()
        $AzureX509cert = Get-AzureCertificate -ServiceName $($VM.serviceName) -Thumbprint $winRMCert -ThumbprintAlgorithm sha1
        $AzureX509cert.Data | Out-File $certTempFile
 
        # Target The Cert That Needs To Be Imported
        $CertToImport = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $certTempFile
 
        $store = New-Object System.Security.Cryptography.X509Certificates.X509Store "Root", "LocalMachine"
        $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
        $store.Add($CertToImport)
        $store.Close() 
        Remove-Item $certTempFile
        Write-Output  "WinRM Certificate installed onto local machine"
    }
    else
    {
        Write-Output  "WinRM Certificate already present on local machine"
    }
}

<#
.SYNOPSIS
Copy a file from your local machine to an azure vm
 
.DESCRIPTION
Copy a file from your local machine to an azure vm
 
.PARAMETER VMName
The name of the vm
 
.PARAMETER ServiceName
The name of the cloud service
 
.PARAMETER Credential
PSCredential with perms to connect remotely to the target vm
 
.PARAMETER LocalFile
The full path to the file to copy
 
.PARAMETER OverWrite
Overwrite the remote file if it already exists
 
.PARAMETER ChunkSizeMB
The chunk size (default 5 MB) used to stream the file across to the vm
 
.PARAMETER RemoteFile
The full path to the file on the azure vm
 
.EXAMPLE
$password = ConvertTo-SecureString -String "password" -AsPlainText -Force
$credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList 'username', $password
 
Copy-FileToAzureVM -VMName 'myVM' -ServiceName 'myCloudService' -Credential $credential -LocalFile "C:\myFile.txt" -RemoteFile "C:\Documents\myFile.txt"
#>

function Copy-FileToAzureVM
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$VMName,
        [Parameter(Mandatory = $true)][string]$ServiceName,
        [Parameter(Mandatory = $true)][PSCredential]$Credential,
        [Parameter(Mandatory = $true)][string]$LocalFile,
        [Parameter(Mandatory = $true)][string]$RemoteFile,
        [boolean]$OverWrite = $true,
        [int]$ChunkSizeMB = 5
    )

    # The plan is is to open a remote connection to the vm and copy the file across through the remote connection in 1MB chunks
    # A powershell limitation limits this approach to 10 MB - hence we chunk it over in 1MB chunks
    Write-Output "Copy file $LocalFile to vm: $VMName"
    $session = $null
    $localFileStream = $null

    Install-AzureVMWinRMCert -VMName $VMName -ServiceName $ServiceName
    $winRmUri = Get-AzureWinRMUri -ServiceName $ServiceName -Name $VMName
    $session = New-PSSession -ConnectionUri $winRmUri.ToString() -Credential $Credential 

    try 
    {
        # Open local file
        [IO.FileStream]$localFileStream = [IO.File]::OpenRead($LocalFile)

        # Open remote file on the server - handle remote errors
        Invoke-Command -Session $Session -ScriptBlock {
            Param
            (
                $RemoteFile, 
                $OverWrite
            )
            $remoteException = $null
            try 
            {
                if(-not $OverWrite)
                {
                    if(Test-Path $RemoteFile)
                    {
                        throw "File $RemoteFile already exists on the remote machine, overwrite disabled"
                    }
                }
                $dir = [IO.Path]::GetDirectoryName($RemoteFile)
                if (-not (Test-Path $dir))
                {
                    Write-Output "Creating directory $dir"
                    New-Item $dir -type directory -Force
                }
                [IO.FileStream]$remoteFileStream = [IO.File]::OpenWrite($RemoteFile)
            }
            catch 
            {
                $remoteException = $_
            }            
        } -ArgumentList $RemoteFile, $OverWrite
        $remoteException = Invoke-Command -Session $Session -ScriptBlock { $remoteException }
        if($null -ne $remoteException) 
        {
            throw $remoteException
        }

        # Copy file in chunks - handle remote errors
        $chunkSize = $ChunkSizeMB * 1024 * 1024
        [byte[]]$contentchunk = New-Object byte[] $chunkSize
        $bytesread = 0
        Write-Output "Starting file transfer"
        while (($bytesread = $localFileStream.Read( $contentchunk, 0, $chunkSize )) -ne 0)
        {
            $percent = $localFileStream.Position / $localFileStream.Length        
            Invoke-Command -Session $Session -ScriptBlock {
                Param($data, $bytes)
                $remoteException = $null
                try 
                {
                    $remoteFileStream.Write( $data, 0, $bytes )
                }
                catch
                {
                    $remoteException = $_
                }
            } -ArgumentList $contentchunk,$bytesread
            $remoteException = Invoke-Command -Session $Session -ScriptBlock { $remoteException }
            if($null -ne $remoteException)
            {
                throw "Error during file transfer: $remoteException"
            }
            Write-Output ("Sent {0} bytes: transfer {1:P2} complete, " -f $bytesread, $percent)
        }
        Write-Output "File transfer complete"
    }
    catch
    {
        throw $_
    }
    finally
    {
        if($localFileStream -ne $null) {
            $localFileStream.Close()
        }

        Invoke-Command -Session $Session -ScriptBlock {
            if($null -ne $remoteFileStream) {
                $remoteFileStream.Close()
            }
        }

        if($null -ne $session) {
            Remove-PSSession $session
        }
    }
}

<#
.SYNOPSIS
Execute the given script on the azure vm
 
.DESCRIPTION
Execute the given script on the azure vm
 
.PARAMETER VMName
The name of the vm
 
.PARAMETER ServiceName
The name of the cloud service
 
.PARAMETER Credential
PSCredential with perms to connect remotely to the target vm
 
.PARAMETER ScriptBlock
The powershell script to execute
 
.PARAMETER ArgumentList
An array of arguments to pass into the script block
 
.EXAMPLE
$password = ConvertTo-SecureString -String "password" -AsPlainText -Force
$credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList 'username', $password
 
$ScriptBlock = {
    Param
    (
        [int]$diskNumber = 0
    )
    (Get-Disk -Number $diskNumber).FriendlyName
}
$ArgumentList = @(0)
 
Invoke-RemoteScriptOnAzureVM -VMName 'myVM' -ServiceName 'myCloudService' -Credential $credential -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList
#>

function Invoke-RemoteScriptOnAzureVM
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$VMName,
        [Parameter(Mandatory = $true)][string]$ServiceName,
        [Parameter(Mandatory = $true)][PSCredential]$Credential,
        [Parameter(Mandatory = $true)][ScriptBlock]$ScriptBlock,
        [array]$ArgumentList
    )

    Install-AzureVMWinRMCert -VMName $VMName -ServiceName $ServiceName
    $winRmUri = Get-AzureWinRMUri -ServiceName $ServiceName -Name $VMName

    Write-Output "Executing script on vm: $VMName"
    $script = $ScriptBlock.ToString()
    Write-Output "Script: $script"

    Invoke-Command -ConnectionUri $winRmUri.ToString() -Credential $Credential -ScriptBlock $ScriptBlock `
        -ArgumentList $ArgumentList
}

<#
.SYNOPSIS
Install an msi on an azure vm
 
.DESCRIPTION
Install an msi on an azure vm. The msi is download onto the vm and then remotley executed
 
.PARAMETER VMName
The name of the vm
 
.PARAMETER ServiceName
The name of the cloud service
 
.PARAMETER Credential
PSCredential with perms to connect remotely to the target vm
 
.PARAMETER ProductName
The name of the product being installed
 
.PARAMETER MsiUrl
The url to download the msi from
 
.PARAMETER InstallDirectory
The local install directory on the target vm, defaults to 'C:\Installs'
 
.EXAMPLE
$password = ConvertTo-SecureString -String "password" -AsPlainText -Force
$credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList 'username', $password
$msiUrl = "http://download.octopusdeploy.com/octopus/Octopus.Tentacle.2.5.8.447-x64.msi"
Install-MsiToAzureVMFromUrl -VMName 'myVM' -ServiceName 'myCloudService' -Credential $credential -ProductName 'Octopus Tentacle' -MsiUrl $msiUrl -InstallDirectory 'C:/Installs'
#>

function Install-MsiToAzureVMFromUrl
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$VMName,
        [Parameter(Mandatory = $true)][string]$ServiceName,
        [Parameter(Mandatory = $true)][PSCredential]$Credential,
        [Parameter(Mandatory = $true)][string]$ProductName,
        [Parameter(Mandatory = $true)][string]$MsiUrl,
        [string]$InstallDirectory = 'C:\Installs'
    )

    Write-Output "Installing $MsiUrl onto $VMName"

    Install-AzureVMWinRMCert -VMName $VMName -ServiceName $ServiceName
    $winRmUri = Get-AzureWinRMUri -ServiceName $ServiceName -Name $VMName
    $session = New-PSSession -ConnectionUri $winRmUri.ToString() -Credential $Credential            
    
    Write-Output "Install $ProductName onto vm: $VMName service: $serviceName"
    Invoke-Command -Session $session -ScriptBlock {
        param
        (
            $ProductName,
            $MsiUrl,
            $InstallDirectory
        )

        if (-not (Test-Path $InstallDirectory))
        {
            Write-Output "Creating directory $InstallDirectory"
            New-Item $InstallDirectory -type directory
        }
        $url = $MsiUrl
        $file = Join-Path $InstallDirectory "$ProductName.msi"
        $logFile = Join-Path $InstallDirectory "$ProductName.log"
        if (Test-Path $file)
        {
            Write-Output "Deleting exisiting file: $file"
            Remove-Item $file
        }
        Write-Output "Download msi: $url to $file"
        $webclient = New-Object System.Net.WebClient
        $webclient.DownloadFile($url,$file)

        Write-Output "Running msi: $file"
        & cmd /c msiexec /i $file /L*v $logFile /quiet 
    } -ArgumentList $ProductName, $MsiUrl, $InstallDirectory

    $exitCode = Invoke-Command -session $session -ScriptBlock {$LASTEXITCODE}

    Remove-PSSession $session

    if($exitCode -eq 0)
    {
        Write-Output "Product $ProductName successfully installed"
    }
    else
    {
        Write-Error "Product $ProductName was not successfully installed, exited with error code: $exitCode. See install log at $InstallDirectory on target vm"
    }
}

<#
.SYNOPSIS
Install an msi on an azure vm
 
.DESCRIPTION
Install an msi on an azure vm. The msi is copied onto the vm from a local file location and then remotley executed
 
.PARAMETER VMName
The name of the vm
 
.PARAMETER ServiceName
The name of the cloud service
 
.PARAMETER Credential
PSCredential with perms to connect remotely to the target vm
 
.PARAMETER ProductName
The name of the product being installed
 
.PARAMETER LocalFile
The local path to the msi
 
.PARAMETER InstallDirectory
The local install directory on the target vm, defaults to 'C:\Installs'
 
.EXAMPLE
$password = ConvertTo-SecureString -String "password" -AsPlainText -Force
$credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList 'username', $password
Install-MsiToAzureVMFromFile -VMName 'myVM' -ServiceName 'myCloudService' -Credential $credential -ProductName 'Azure Powershell' -LocalFile 'C:/azure-powershell.2.0.1.msi' -InstallDirectory 'C:/Installs'
#>

function Install-MsiToAzureVMFromFile
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$VMName,
        [Parameter(Mandatory = $true)][string]$ServiceName,
        [Parameter(Mandatory = $true)][PSCredential]$Credential,
        [Parameter(Mandatory = $true)][string]$ProductName,
        [Parameter(Mandatory = $true)][string]$LocalFile,
        [string]$InstallDirectory = 'C:\Installs'
    )

    Write-Output "Installing $LocalFile onto $VMName"

    # First copy the file to the VM
    $remoteFile = Join-Path $InstallDirectory "$ProductName.msi"
    $remoteLogFile = Join-Path $InstallDirectory "$ProductName.log"
    Copy-FileToAzureVM -VMName $VMName -ServiceName $ServiceName -Credential $Credential -LocalFile $LocalFile -RemoteFile $remoteFile

    # Remotley Install it
    $winRmUri = Get-AzureWinRMUri -ServiceName $ServiceName -Name $VMName
    $session = New-PSSession -ConnectionUri $winRmUri.ToString() -Credential $Credential            
    
    Write-Output "Install $ProductName onto vm: $VMName service: $serviceName"
    Invoke-Command -Session $session -ScriptBlock {
        param
        (
            $ProductName,
            $remoteFile,
            $remoteLogFile
        )

        Write-Output "Running msi: $remoteFile"
        & cmd /c msiexec /i $remoteFile /L*v $remoteLogFile /quiet 
    } -ArgumentList $ProductName, $remoteFile, $remoteLogFile

    $exitCode = Invoke-Command -session $session -ScriptBlock {$LASTEXITCODE}

    Remove-PSSession $session

    if($exitCode -eq 0)
    {
        Write-Output "Product $ProductName successfully installed"
    }
    else
    {
        Write-Error "Product $ProductName was not successfully installed, exited with error code: $exitCode. See install log at $InstallDirectory on target vm"
    }
}

<#
.SYNOPSIS
Return the .NET version installed on an azure vm
 
.DESCRIPTION
Return the .NET version installed on an azure vm
 
.PARAMETER VMName
The name of the vm
 
.PARAMETER ServiceName
The name of the cloud service
 
.PARAMETER Credential
PSCredential with perms to connect remotely to the target vm
 
.EXAMPLE
$password = ConvertTo-SecureString -String "password" -AsPlainText -Force
$credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList 'username', $password
Get-AzureVMDotNetVersion -VMName 'myVM' -ServiceName 'myCloudService' -Credential $credential
#>

function Get-AzureVMDotNetVersion
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$VMName,
        [Parameter(Mandatory = $true)][string]$ServiceName,
        [Parameter(Mandatory = $true)][PSCredential]$Credential
    )

    # See https://msdn.microsoft.com/library/hh925568(v=vs.110).aspx
    $scriptBlock = {
        $prop = Get-ItemProperty -Path "hklm:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\" -ErrorAction SilentlyContinue
        if($null -ne $prop)
        {
            $release = $prop.Release
            switch ($release)
            {
                "378389" { $version = ".NET 4.5" }
                "378675" { $version = ".NET 4.5.1" }
                "378758" { $version = ".NET 4.5.1" }
                "379893" { $version = ".NET 4.5.2" }
                "393297" { $version = ".NET 4.6" }
                "393295" { $version = ".NET 4.6" }
                "394254" { $version = ".NET 4.6.1" }
                "394271" { $version = ".NET 4.6.1" }
                "394802" { $version = ".NET 4.6.2" }
                "394806" { $version = ".NET 4.6.2" }
                default  { $version = "Unknown .NET version" }
            }
        }
        else
        {
            $version =  "Unable to determine of .NET Framework version"
        }
    }

    Install-AzureVMWinRMCert -VMName $VMName -ServiceName $ServiceName > $null
    $winRmUri = Get-AzureWinRMUri -ServiceName $ServiceName -Name $VMName 
    $session = New-PSSession -ConnectionUri $winRmUri.ToString() -Credential $Credential
    
    Invoke-Command -Session $session -ScriptBlock $scriptBlock

    $version = Invoke-Command -Session $session -ScriptBlock {$version}

    Remove-PSSession $session

    $result = New-Object �TypeName PSObject �Prop (@{'Name'=$VMName; 'ServiceName' = $ServiceName; 'DotNET_Version' = $version})
    $result
}

<#
.SYNOPSIS
Sets the win update settings on the target vm
 
.DESCRIPTION
Sets the win update settings on the target vm
 
.PARAMETER VMName
The name of the vm
 
.PARAMETER ServiceName
The name of the cloud service
 
.PARAMETER Settings
Winupdate setting ("NoCheck", "CheckOnly", "DownloadOnly", "Install",)
 
.EXAMPLE
$password = ConvertTo-SecureString -String "password" -AsPlainText -Force
$credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList 'username', $password
 
Set-WindowsUpdateOnAzureVM -VMName 'myVM' -ServiceName 'myCloudService' -Credential $credential -Setting "NoCheck"
#>


function Set-WindowsUpdateOnAzureVM
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$VMName,
        [Parameter(Mandatory = $true)][string]$ServiceName,
        [Parameter(Mandatory = $true)][PSCredential]$Credential,
        [Parameter(Mandatory = $true)]
        [ValidateSet("NoCheck", "CheckOnly", "DownloadOnly", "Install",ignorecase=$true)]$Setting
    )

    # NoCheck: $AuOptions = 1
    # CheckOnly: $AuOptions = 2
    # DownloadOnly: $AuOptions = 3
    # Install: $AuOptions = 4

    $auOptions = 0
    switch($Setting)
    {
        "NoCheck"
        {
            $auOptions = 1    
        }
        "CheckOnly"
        {
            $auOptions = 2    
        }
        "DownloadOnly"
        {
            $auOptions = 3    
        }
        "Install"
        {
            $auOptions = 4    
        }
        default
        {
            throw "Unknown winupdate setting $Setting"
        }
    }

    $scriptBlock = {
        param
        (
            [int]$auOptions
        )
        $Key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update" 
        Set-ItemProperty -Path $Key -Name "AUOptions" -Value $auOptions -Force -Confirm:$false
        Set-ItemProperty -Path $Key -Name "CachedAUOptions" -Value $auOptions -Force -Confirm:$false
    }

    Write-Output "Disbale windows update on vm: $VMName"
    Invoke-RemoteScriptOnAzureVM -VMName $VMName -ServiceName $ServiceName -Credential $Credential `
        -ScriptBlock $scriptBlock -ArgumentList @($auOptions)
}

#Export-ModuleMember 'Install-AzureVMWinRMCert'
Export-ModuleMember 'Copy-FileToAzureVM'
Export-ModuleMember 'Invoke-RemoteScriptOnAzureVM'
Export-ModuleMember 'Install-MsiToAzureVMFromUrl'
Export-ModuleMember 'Install-MsiToAzureVMFromFile'
Export-ModuleMember 'Get-AzureVMDotNetVersion'
Export-ModuleMember 'Set-WindowsUpdateOnAzureVM'