IntelliTect.AzureRm.psm1
function New-AzureRmVirtualMachine { <# .SYNOPSIS Creates a new Azure RM virtual machine .DESCRIPTION Creates a new virtual machine, and other resources if needed - Resource Group, Storage Account, Virtual Network, Public IP Address, Domain Name Label, Network Interface. Defaults can be set for inputs by using Set-AzureRmDefault. .EXAMPLE New-AzureRmVirtualMachine -VMName myVmName .EXAMPLE New-AzureRmVirtualMachine -VMName myVmName -ResourceGroupName newResourceGroup -DomainNameLabel mydomain .PARAMETER VMName Name of the virtual machine. REQUIRED .PARAMETER Inputs When scripting menu inputs can be provided. $inputs = New-AzureRmVmInputs $inputs.Location = "westus"" $inputs.SubscriptionId = "<subscription id>" ... New-AzureRmVirtualMachine -VMName myVmName -Inputs $inputs .PARAMETER ResourceGroupName Resource group to create resources in. DEFAULT: none - If creating a new resource group it must be specified in the parameter. - If not specified you can only choose from existing resource groups. - Can specify a default with Set-AzureRmDefault -ResourceGroupName <resourcegroupname> .PARAMETER VirtualNetworkName Name of virtual network. Will be created if it doesn't exist. DEFAULT: $ResourceGroupName - Can specify a default with Set-AzureRmDefault -VirtualNetworkName <virtualnetworkname> .PARAMETER DomainNameLabel Domain name to point at your public IP address. DEFAULT: none - If a domain name is desired then it must be specified on the command line .NOTES Default values can be specified for: Location ResourceGroupName SubscriptionId VMImagePublisher VMImageOffer VMImageSku StorageAccountType VMSize OperatingSystem #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "")] param ( [Parameter(Mandatory = $true)] [string]$VmName, [AzureRmVmInputs]$Inputs = (New-Object AzureRmVmInputs), [string]$ResourceGroupName = $null, [string]$VirtualNetworkName = $null, [string]$DomainNameLabel = "", # Implementing our own -whatif and -confirm # Not all of the calls to Azure implement these switches, plus so much of # the script is dependent on return values from previous commands [switch]$WhatIf, [switch]$Confirm ) Set-StrictMode -Version Latest $context = Assert-AzureRmSession # Process overrides from command line if ($ResourceGroupName) { $Inputs.ResourceGroupName = $ResourceGroupName } if ($VirtualNetworkName) { $Inputs.VirtualNetworkName = $VirtualNetworkName } $Inputs.DomainNameLabel = $DomainNameLabel # Choose a subscription ... and switch context to it if different than current Get-AzureRmSubscriptionMenu $Inputs | Out-Null if ($context.Subscription.SubscriptionId -ne $Inputs.SubscriptionId) { if (!(Confirm-ScriptShouldContinue $Confirm "Continuing will change your current Azure context to the selected subscription.")) { return } Write-Information "Switching Azure context to selected subscription ..." -InformationAction Continue Set-AzureRmContext -SubscriptionId $Inputs.SubscriptionId | Out-Null } # Choose a resource group Write-Information "Retrieving resource groups ..." -InformationAction Continue $resourceGroups = {Get-AzureRmResourceGroup -WarningAction SilentlyContinue | Sort-Object ResourceGroupName | ` Select-Object -ExpandProperty ResourceGroupName} Get-InputFromMenu $Inputs "ResourceGroupName" "Select Resource Group" $resourceGroups $null $null $true if (!$Inputs.ResourceGroupName) { return } # Choose an image sku Get-AzureRmVmImageSkuMenu $Inputs | Out-Null # And a VM size Get-AzureRmVmSizeMenu $Inputs | Out-Null # Choose a storage account Write-Information "Retrieving storage accounts ..." -InformationAction Continue $storageAccounts = {Get-AzureRmStorageAccount | Where-Object { $_.ResourceGroupName -eq $Inputs.ResourceGroupName } | ` Sort-Object StorageAccountName | Select-Object -ExpandProperty StorageAccountName} $defaultStorageAccountName = $Inputs.ResourceGroupName.ToLower() -Replace "[^0-9a-z]", "" $defaultStorageAccountName += (Get-Date).Ticks $defaultStorageAccountName = $defaultStorageAccountName.Substring(0, [System.Math]::Min(24, $defaultStorageAccountName.Length)) Get-InputFromMenu $Inputs "StorageAccountName" "Select Storage Account" $storageAccounts $defaultStorageAccountName # If storage account doesn't exist then get additional info $storageAccount = Get-AzureRmStorageAccount | Where-Object { $_.StorageAccountName -eq $Inputs.StorageAccountName } if (!$storageAccount) { $storageAccountTypes = @("Standard_LRS, Locally Redundant Storage", "Standard_ZRS, Zone Redundant Storage", "Standard_GRS, Geo Redundant Storage", "Standard_RAGRS, Read-Access Geo Redundant Storage", "Premium_LRS, Locally Redundant Storage") Get-InputFromMenu $Inputs "StorageAccountType" "Select Storage Account Type" {$storageAccountTypes} $null "Please Note: Selected type must be available for selected VM size." $Inputs.StorageAccountType = $Inputs.StorageAccountType.Substring(0, $Inputs.StorageAccountType.IndexOf(",")) } # Get the virtual network and network security group Write-Information "Retrieving virtual networks ..." -InformationAction Continue $virtualNetworks = {Get-AzureRmVirtualNetwork | Where-Object { $_.ResourceGroupName -eq $Inputs.ResourceGroupName } | ` Sort-Object Name | Select-Object -ExpandProperty Name} Get-InputFromMenu $Inputs "VirtualNetworkName" "Select Virtual Network" $virtualNetworks $Inputs.ResourceGroupName Write-Information "Retrieving security groups ..." -InformationAction Continue $securityGroups = {Get-AzureRmNetworkSecurityGroup | Where-Object { $_.ResourceGroupName -eq $Inputs.ResourceGroupName } | ` Sort-Object Name | Select-Object -ExpandProperty Name} Get-InputFromMenu $Inputs "NetworkSecurityGroup" "Select Network Security Group" $securityGroups $Inputs.ResourceGroupName # We run different commands based on the OS, and have no way to figure it out from the image $osChoices = @("Linux", "Windows") Get-InputFromMenu $Inputs "OperatingSystem" "Select Operating System" {$osChoices} $null "Please Note: Selected operating system must match the VM image selected." # The VM will need its admin credentials set $Inputs.AdminCredentials = (Get-Credential -UserName "vmadmin" -Message "Enter the username and password of the admin account for the new VM") # If a domain name label is supplied, then test that it isn't in use Assert-DomainNameIsAvailable $Inputs.DomainNameLabel $Inputs.Location if (!$WhatIf -and !(Confirm-ScriptShouldContinue $Confirm "Continuing will add resources to your current Azure subscription.")) { return } if ($WhatIf) { Write-Information "WhatIf: Virtual machine $($VmName) would be created in resource group $($Inputs.ResourceGroupName) in location $($Inputs.Location)" ` -InformationAction Continue Write-Information "The following inputs were entered" -InformationAction Continue $Inputs return } # Now start creating things and setting them up # If the resource group doesn't exist, then create it $checkResourceGroup = Get-AzureRmResourceGroup | Where-Object { $_.ResourceGroupName -eq $Inputs.ResourceGroupName } if (!$checkResourceGroup) { Write-Information -MessageData "Creating new resource group" -InformationAction Continue New-AzureRmResourceGroup -Name $Inputs.ResourceGroupName -Location $Inputs.Location | Out-Null } # If the storage account doesn't exist, then create it if (!$storageAccount) { Write-Information -MessageData "Creating new storage account" -InformationAction Continue New-AzureRmStorageAccount -Name $Inputs.StorageAccountName -ResourceGroupName $Inputs.ResourceGroupName ` -SkuName $Inputs.StorageAccountType -Location $Inputs.Location | Out-Null } # If the virtual network doesn't exist, then create it $vnet = $null $checkVirtualNetwork = Get-AzureRmVirtualNetwork | Where-Object { $_.Name -eq $Inputs.VirtualNetworkName } if (!$checkVirtualNetwork) { Write-Information -MessageData "Creating new virtual network" -InformationAction Continue $defaultSubnet = New-AzureRmVirtualNetworkSubnetConfig -Name "defaultSubnet" -AddressPrefix "10.0.2.0/24" $vnet = New-AzureRmVirtualNetwork -Name $Inputs.VirtualNetworkName -ResourceGroupName $Inputs.ResourceGroupName ` -Location $Inputs.Location -AddressPrefix "10.0.0.0/16" -Subnet $defaultSubnet -WarningAction SilentlyContinue } # Create, if needed, the network security group and add a default RDP rule $nsg = $null $securityGroupName = @{$true = $Inputs.ResourceGroupName; $false = $Inputs.NetworkSecurityGroup}[$Inputs.NetworkSecurityGroup -eq ""] if ($Inputs.NetworkSecurityGroup -ne "") { $nsg = Get-AzureRmNetworkSecurityGroup -ResourceGroupName $Inputs.ResourceGroupName | Where-Object { $_.Name -eq $Inputs.NetworkSecurityGroup } } if (!$nsg) { Write-Information -MessageData "Creating security group and rule" -InformationAction Continue $nsg = New-AzureRMNetworkSecurityGroup -ResourceGroupName $Inputs.ResourceGroupName -Name $securityGroupName -Location $Inputs.Location -WarningAction SilentlyContinue $nsg | Add-AzureRmNetworkSecurityRuleConfig -WarningAction SilentlyContinue -Name 'Allow_RDP' -Priority 1000 -Protocol TCP -Access Allow ` -SourceAddressPrefix * -SourcePortRange * -DestinationAddressPrefix * -DestinationPortRange 3389 -Direction Inbound | ` Set-AzureRmNetworkSecurityGroup | Out-Null } # Create the public IP and NIC if (!$vnet) { $vnet = Get-AzureRmVirtualNetwork -Name $Inputs.VirtualNetworkName -ResourceGroupName $Inputs.ResourceGroupName } $ticks = (Get-Date).Ticks.ToString() $ticks = $ticks.Substring($ticks.Length - 5, 5) $nicName = "$($Inputs.ResourceGroupName)$($ticks)" if ($Inputs.DomainNameLabel -ne "") { Write-Information -MessageData "Creating new public IP address with domain name" -InformationAction Continue $pip = New-AzureRmPublicIpAddress -WarningAction SilentlyContinue -Name $nicName -ResourceGroupName $Inputs.ResourceGroupName ` -DomainNameLabel $Inputs.DomainNameLabel -Location $Inputs.Location -AllocationMethod Dynamic } else { Write-Information -MessageData "Creating new public IP address WITHOUT domain name" -InformationAction Continue $pip = New-AzureRmPublicIpAddress -WarningAction SilentlyContinue -Name $nicName -ResourceGroupName $Inputs.ResourceGroupName ` -Location $Inputs.Location -AllocationMethod Dynamic } Write-Information -MessageData "Creating new network interface" -InformationAction Continue $nic = New-AzureRmNetworkInterface -WarningAction SilentlyContinue -Name $nicName -ResourceGroupName $Inputs.ResourceGroupName ` -Location $Inputs.Location -PublicIpAddressId $pip.Id -SubnetId $vnet.Subnets[0].Id -NetworkSecurityGroupId $nsg.Id # Create the VM configuration Write-Information -MessageData "Creating VM configuration" -InformationAction Continue $vm = New-AzureRmVMConfig -VMName $VMName -VMSize $Inputs.VMSize # Add additional info to the configuration if ($Inputs.OperatingSystem -eq "Windows") { $vm = Set-AzureRmVMOperatingSystem -VM $vm -Windows -ComputerName $VMName -Credential $Inputs.AdminCredentials -ProvisionVMAgent -EnableAutoUpdate } else { $vm = Set-AzureRmVMOperatingSystem -VM $vm -Linux -ComputerName $VMName -Credential $Inputs.AdminCredentials } $vm = Set-AzureRmVMSourceImage -VM $vm -PublisherName $Inputs.VMImagePublisher -Offer $Inputs.VMImageOffer -Skus $Inputs.VMImageSku -Version "latest" $vm = Add-AzureRmVMNetworkInterface -VM $vm -Id $nic.Id # Create the disk $diskName = "$($VMName)OSDisk" $storage = Get-AzureRmStorageAccount -ResourceGroupName $Inputs.ResourceGroupName -Name $Inputs.StorageAccountName $osDisk = $storage.PrimaryEndpoints.Blob.ToString() + "vhds/" + $diskName + ".vhd" if ($Inputs.OperatingSystem -eq "Windows") { $vm = Set-AzureRmVMOSDisk -VM $vm -Name $diskName -VhdUri $osDisk -CreateOption fromImage } else { $vm = Set-AzureRmVMOSDisk -VM $vm -Name $diskName -VhdUri $osDisk -CreateOption fromImage } # And finally create the VM itself Write-Information -MessageData "Creating the VM ... this will take some time ..." -InformationAction Continue New-AzureRmVM -ResourceGroupName $Inputs.ResourceGroupName -Location $Inputs.Location -VM $vm Write-Information -MessageData "VM Created successfully" -InformationAction Continue } function Enable-RemotePowerShellOnAzureRmVm { <# .SYNOPSIS Remotely configures an Azure RM virtual machine to enable Powershell remoting. Returns a command that can be used to connect to the remote VM. Much of this script came from a blog post by Marcus Robinson http://www.techdiction.com/2016/02/12/powershell-function-to-enable-winrm-over-https-on-an-azure-resource-manager-vm/ .DESCRIPTION Generates a script locally, then uploads it to blob storage, where it is then installed as a custom script extension and run on the VM. Opens the appropriate port in the network security group rules. . .EXAMPLE Enable-RemotePowerShellOnAzureRmVm -VMName myvm -ResourceGroupName myvirtualmachines .PARAMETER VMName Name of the virtual machine. REQUIRED .PARAMETER ResourceGroupName Name of the resource group. REQUIRED .PARAMETER DnsName Name of the computer that will be connecting. Used in name of certificate and in WinRM listener. DEFAULT: $env:ComputerName .PARAMETER SourceAddressPrefix Prefix of source IP addresses in network security group rule. DEFAULT: * #> [CmdletBinding()] [OutputType([string])] Param ( [parameter(Mandatory=$true)] [string]$VMName, [parameter(Mandatory=$true)] [string]$ResourceGroupName, [parameter()] [string]$DNSName = $env:COMPUTERNAME, [parameter()] [string]$SourceAddressPrefix = "*" ) [string]$scriptName = "ConfigureWinRM_HTTPS.ps1" [string]$extensionName = "EnableWinRM_HTTPS" [string]$blobContainer = "scripts" [string]$securityRuleName = "WinRM_HTTPS" # Define a temporary file in the users TEMP directory Write-Information -MessageData "Creating script locally that we'll upload to the storage account" -InformationAction Continue [string]$file = $env:TEMP + "\" + $scriptName # Create the file containing the PowerShell { # POWERSHELL TO EXECUTE ON REMOTE SERVER BEGINS HERE param([string]$DNSName) # Force all network locations that are Public to Private Get-NetConnectionProfile | Where-Object { $_.NetworkCategory -eq "Public" } | ` ForEach-Object { Set-NetConnectionProfile -InterfaceIndex $_.InterfaceIndex -NetworkCategory Private } # Ensure PS remoting is enabled, although this is enabled by default for Azure VMs Enable-PSRemoting -Force # Create rule in Windows Firewall, if it's not already there if ((Get-NetFirewallRule | Where-Object { $_.Name -eq "WinRM HTTPS" }).Count -eq 0) { New-NetFirewallRule -Name "WinRM HTTPS" -DisplayName "WinRM HTTPS" -Enabled True -Profile Any -Direction Inbound -Action Allow -LocalPort 5986 -Protocol TCP } # Create Self Signed certificate and store thumbprint, if it doesn't already exist $thumbprint = (Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object { $_.Subject -eq "CN=$DNSName" } | Select-Object -First 1).Thumbprint if (!$thumbprint) { $thumbprint = (New-SelfSignedCertificate -DnsName $DNSName -CertStoreLocation Cert:\LocalMachine\My).Thumbprint } # Run WinRM configuration on command line. DNS name set to computer hostname, you may wish to use a FQDN $cmd = "winrm create winrm/config/Listener?Address=*+Transport=HTTPS @{Hostname=""$DNSName""; CertificateThumbprint=""$thumbprint""}" cmd.exe /C $cmd # POWERSHELL TO EXECUTE ON REMOTE SERVER ENDS HERE } | out-file -width 1000 $file -force # Get the VM we need to configure Write-Information -MessageData "Getting information needed to find and update the blob storage with the new script" -InformationAction Continue $vm = Get-AzureRmVM -ResourceGroupName $ResourceGroupName -Name $VMName # Get storage account name $storageaccountname = $vm.StorageProfile.OsDisk.Vhd.Uri.Split('.')[0].Replace('https://','') # get storage account key $key = ((Get-AzureRmStorageAccountKey -Name $storageaccountname -ResourceGroupName $ResourceGroupName) | ` Where-Object { $_.KeyName -eq "key1" }).Value # create storage context $storagecontext = New-AzureStorageContext -StorageAccountName $storageaccountname -StorageAccountKey $key # create a container called scripts if ((Get-AzureStorageContainer -Context $storagecontext | Where-Object { $_.Name -eq $blobContainer}).Count -eq 0) { New-AzureStorageContainer -Name $blobContainer -Context $storagecontext | Out-Null } #upload the file Set-AzureStorageBlobContent -Container $blobContainer -File $file -Blob $scriptName -Context $storagecontext -force | Out-Null # Create custom script extension from uploaded file Write-Information -MessageData "Create and run a script extension from our uploaded script" -InformationAction Continue Set-AzureRmVMCustomScriptExtension -ResourceGroupName $ResourceGroupName -VMName $VMName -Name $extensionName ` -Location $vm.Location -StorageAccountName $storageaccountname -StorageAccountKey $key -FileName $scriptName ` -ContainerName $blobContainer -RunFile $scriptName -Argument $DNSName | Out-Null # Get the name of the first NIC in the VM Write-Information -MessageData "Create a new security rule that will allow us to connect remotely" -InformationAction Continue $nic = Get-AzureRmNetworkInterface -ResourceGroupName $ResourceGroupName -Name (Get-AzureRmResource -ResourceId $vm.NetworkInterfaceIDs[0]).ResourceName # Get the network security group attached to the NIC $nsg = Get-AzureRmNetworkSecurityGroup -ResourceGroupName $ResourceGroupName -Name (Get-AzureRmResource -ResourceId $nic.NetworkSecurityGroup.Id).Name # Add the new NSG rule, and update the NSG if (($nsg.SecurityRules | Where-Object { $_.Name -eq "WinRM_HTTPS" }).Length -eq 0) { $nsg | Add-AzureRmNetworkSecurityRuleConfig -Name $securityRuleName -Priority 1100 -Protocol TCP -Access Allow ` -SourceAddressPrefix $SourceAddressPrefix -SourcePortRange * -DestinationAddressPrefix * -DestinationPortRange 5986 -Direction Inbound | ` Set-AzureRmNetworkSecurityGroup | Out-Null } # get the NIC public IP $ip = Get-AzureRmPublicIpAddress -ResourceGroupName $ResourceGroupName -Name (Get-AzureRmResource -ResourceId $nic.IpConfigurations[0].PublicIpAddress.Id).ResourceName "To connect to the VM using the IP address while bypassing certificate checks use the following command:" "Enter-PSSession -ComputerName $($ip.IpAddress) -Credential <admin_username> -UseSSL -SessionOption (New-PsSessionOption -SkipCACheck -SkipCNCheck)" } function Get-AzureRmSubscriptionMenu { <# .SYNOPSIS Displays a menu for selecting an Azure subscription. Returns chosen subscription id. .PARAMETER Inputs When scripting menu inputs can be provided. - This menu will set the value of $Inputs.SubscriptionId $inputs = New-AzureRmVmInputs $inputs.Location = "westus" ... Get-AzureRmSubscriptionMenu -Inputs $inputs .EXAMPLE Get-AzureRmSubscriptionMenu #> [CmdletBinding()] param ( [AzureRmVmInputs]$inputs = (New-Object AzureRmVmInputs) ) Write-Information "Retrieving subscriptions ..." -InformationAction Continue $subscriptions = Get-AzureRmSubscription -WarningAction SilentlyContinue | ` Sort-Object SubscriptionName | ` Select-Object @{ Name = "Subscription"; Expression = { "$($_.SubscriptionName) [$($_.SubscriptionId)]" } } | ` Select-Object -ExpandProperty Subscription Get-InputFromMenu $inputs "SubscriptionId" "Select Subscription" { $subscriptions } $sub = $inputs.SubscriptionId -Replace "]" $inputs.SubscriptionId = $sub.Split("[")[1] $inputs.SubscriptionId } function Get-AzureRmVmImageSkuMenu { <# .SYNOPSIS Displays a menu for selecting an Azure VM image sku. - Returns the name of the chosen SKU. - Calling this menu will also call the Location, VMImagePublisher and VMImageOffer menus if needed. .PARAMETER Inputs When scripting menu inputs can be provided. - This menu will set the value of $Inputs.VMImageSku $inputs = New-AzureRmVmInputs $inputs.Location = "westus" $inputs.VMImagePublisher = "RedHat" ... Get-AzureRmVmImageSkuMenu -Inputs $inputs .EXAMPLE Get-AzureRmVmImageSkuMenu #> [CmdletBinding()] param ( [AzureRmVmInputs]$inputs = (New-Object AzureRmVmInputs) ) Get-AzureRmVmImageOfferMenu $inputs | Out-Null Write-Information "Retrieving VM image SKUs ..." -InformationAction Continue $skus = {Get-AzureRmVMImageSku -WarningAction SilentlyContinue -Location $inputs.Location -PublisherName $inputs.VMImagePublisher -Offer $inputs.VMImageOffer ` | Sort-Object Skus | Select-Object -ExpandProperty Skus} Get-InputFromMenu $inputs "VMImageSku" "Select VM Image Sku" $skus Get-AzureRmVMImageSku -WarningAction SilentlyContinue -Location $inputs.Location -PublisherName $inputs.VMImagePublisher -Offer $inputs.VMImageOffer $inputs.VMImageSku } function Get-AzureRmVmImageOfferMenu { <# .SYNOPSIS Displays a menu for selecting an Azure VM image offer. - Returns the name of the chosen offer. - Calling this menu will also call the Location and VMImagePublisher menus if needed. .PARAMETER Inputs When scripting menu inputs can be provided. - This menu will set the value of $Inputs.VMImageOffer $inputs = New-AzureRmVmInputs $inputs.Location = "westus" $inputs.VMImagePublisher = "RedHat" ... Get-AzureRmVmImageOfferMenu -Inputs $inputs .EXAMPLE Get-AzureRmVmImageOfferMenu #> [CmdletBinding()] param ( [AzureRmVmInputs]$inputs = (New-Object AzureRmVmInputs) ) Get-AzureRmVmImagePublisherMenu $inputs | Out-Null Write-Information "Retrieving VM image offers ..." -InformationAction Continue $offers = {Get-AzureRmVMImageOffer -WarningAction SilentlyContinue -Location $inputs.Location -PublisherName $inputs.VMImagePublisher ` | Sort-Object Offer | Select-Object -ExpandProperty Offer} Get-InputFromMenu $inputs "VMImageOffer" "Select VM Image Offer" $offers $inputs.VMImageOffer } function Get-AzureRmVMImagePublisherMenu { <# .SYNOPSIS Displays a menu for selecting an Azure VM image publisher. - Returns the name of the chosen publisher. - Calling this menu will also call the Location menu if needed. .PARAMETER Inputs When scripting menu inputs can be provided. - This menu will set the value of $Inputs.VMImagePublisher $inputs = New-AzureRmVmInputs $inputs.Location = "westus" ... Get-AzureRmVmImagePublisherMenu -Inputs $inputs .EXAMPLE Get-AzureRmVmImagePublisherMenu #> [CmdletBinding()] param ( [AzureRmVmInputs]$inputs = (New-Object AzureRmVmInputs) ) Get-AzureRmLocationMenu $inputs | Out-Null Write-Information "Retrieving VM image publishers ..." -InformationAction Continue $publishers = {Get-AzureRmVMImagePublisher -WarningAction SilentlyContinue -Location $inputs.Location ` | Sort-Object PublisherName | Select-Object -ExpandProperty PublisherName} Get-InputFromMenu $inputs "VMImagePublisher" "Select VM Image Publisher" $publishers $inputs.VMImagePublisher } function Get-AzureRmLocationMenu { <# .SYNOPSIS Displays a menu for selecting an Azure location. - Returns the name of the chosen location. .PARAMETER Inputs When scripting menu inputs can be provided. - This menu will set the value of $Inputs.Location $inputs = New-AzureRmVmInputs ... Get-AzureRmLocationMenu -Inputs $inputs .EXAMPLE Get-AzureRmLocationMenu #> [CmdletBinding()] param ( [AzureRmVmInputs]$inputs = (New-Object AzureRmVmInputs) ) Write-Information "Retrieving locations ..." -InformationAction Continue $locations = Get-AzureRmLocation -WarningAction SilentlyContinue | Sort-Object DisplayName | Select-Object -ExpandProperty Location Get-InputFromMenu $inputs "Location" "Select Location" { $locations } $inputs.Location } function Get-AzureRmVmSizeMenu { <# .SYNOPSIS Displays a menu for selecting an Azure VM size. - Returns the name of the chosen size. - Calling this menu will also call the Location menu if needed. .PARAMETER Inputs When scripting menu inputs can be provided. - This menu will set the value of $Inputs.VMSize $inputs = New-AzureRmVmInputs $inputs.Location = "westus" ... Get-AzureRmVmSizeMenu -Inputs $inputs .EXAMPLE Get-AzureRmVmSizeMenu #> [CmdletBinding()] param ( [AzureRmVmInputs]$Inputs = (New-Object AzureRmVmInputs) ) Get-AzureRmLocationMenu $inputs | Out-Null Write-Information "Retrieving VM sizes ..." -InformationAction Continue $vmSizes = { Get-AzureRmVMSize -Location $Inputs.Location | Sort-Object Name | Select-Object @{ Label = "Name"; Expression = { ` "$($_.Name.PadRight(25))Cores = $($_.NumberOfCores.ToString().PadLeft(2)); Memory = $(($_.MemoryInMb / 1024).ToString().PadLeft(4)) GB; OS Disk = $(($_.OSDiskSizeInMB / 1024).ToString().PadLeft(4)) GB" }} | ` Select-Object -ExpandProperty Name } Get-InputFromMenu $Inputs "VMSize" "Select VM Size" $vmSizes if ($inputs.VMSize.Length -gt 25) { $inputs.VMSize = $inputs.VMSize.Substring(0, 25).TrimEnd() } $inputs.VMSize } function Assert-AzureRmSession { <# .SYNOPSIS Test if an Azure RM session exists, and throw an error if it doesn't. .DESCRIPTION Confirms an Azure RM session exists by checking for Get-AzureRmContext, and throws an error if $null is returned. .EXAMPLE Assert-AzureRmSession #> try { $context = Get-AzureRmContext } catch { throw "Commands in this module require an Azure session. Please use Add-AzureRmAccount before continuing" } $context } function Get-AzureRmDefault { <# .SYNOPSIS Load defaults for AzureRm commands. .DESCRIPTION Defaults are loaded from a JSON file in the profile folder. .EXAMPLE $defaults = Get-AzureRmDefault #> if (Test-Path $script:FilePath) { $jsonObj = (Get-Content $script:FilePath) | ConvertFrom-Json if ($jsonObj.PSObject.Properties -match "AzureRmDefaults") { return $jsonObj.AzureRmDefaults } } return @{} } function Set-AzureRmDefault { <# .SYNOPSIS Update AzureRm defaults .DESCRIPTION Saves defaults for IntelliTect.AzureRm commands to a JSON file in the profile folder. .EXAMPLE Set-AzureRmDefault -Location westus .EXAMPLE Set-AzureRmDefault -RemoveLocation #> [CmdletBinding()] param ( [string]$Location = $null, [string]$ResourceGroupName = $null, [string]$SubscriptionId = $null, [string]$VMImagePublisher = $null, [string]$VMImageOffer = $null, [string]$VMImageSku = $null, [string]$StorageAccountType = $null, [string]$VMSize = $null, [string]$OperatingSystem = $null, [switch]$RemoveLocation, [switch]$RemoveResourceGroupName, [switch]$RemoveSubscriptionId, [switch]$RemoveVMImagePublisher, [switch]$RemoveVMImageOffer, [switch]$RemoveVMImageSku, [switch]$RemoveStorageAccountType, [switch]$RemoveVMSize, [switch]$RemoveOperatingSystem ) $cache = $CachedDefaults function setDefaultProperty($name, $value) { if ($value) { if (!($cache.PSObject.Properties -match $name)) { $cache | Add-Member -MemberType NoteProperty -Name $name -Value $null } $cache.$name = $value } } function removeProperty($name, $remove) { if ($remove) { if ($cache.PSObject.Properties -match $name) { $cache.PSObject.Properties.Remove($name) } } } setDefaultProperty "Location" $Location setDefaultProperty "ResourceGroupName" $ResourceGroupName setDefaultProperty "SubscriptionId" $SubscriptionId setDefaultProperty "VMImagePublisher" $VMImagePublisher setDefaultProperty "VMImageOffer" $VMImageOffer setDefaultProperty "VMImageSku" $VMImageSku setDefaultProperty "StorageAccountType" $StorageAccountType setDefaultProperty "VMSize" $VMSize setDefaultProperty "OperatingSystem" $OperatingSystem removeProperty "Location" $RemoveLocation removeProperty "ResourceGroupName" $RemoveResourceGroupName removeProperty "SubscriptionId" $RemoveSubscriptionId removeProperty "VMImagePublisher" $RemoveVMImagePublisher removeProperty "VMImageOffer" $RemoveVMImageOffer removeProperty "VMImageSku" $RemoveVMImageSku removeProperty "StorageAccountType" $RemoveStorageAccountType removeProperty "VMSize" $RemoveVMSize removeProperty "OperatingSystem" $RemoveOperatingSystem $jsonObj = @{} if (Test-Path $script:FilePath) { $jsonObj = (Get-Content $script:FilePath) | ConvertFrom-Json } $jsonObj.AzureRmDefaults = $cache ($jsonObj | ConvertTo-Json) | Out-File $script:FilePath $CachedDefaults = Get-AzureRmDefault } ## Class definitions class AzureRmVmInputs { [string]$SubscriptionId [string]$ResourceGroupName [string]$Location [string]$VMImagePublisher [string]$VMImageOffer [string]$VMImageSku [string]$StorageAccountName [string]$StorageAccountType [string]$DomainNameLabel [string]$VirtualNetworkName [string]$VMSize [string]$NetworkSecurityGroup [PSCredential]$AdminCredentials [string]$OperatingSystem } function New-AzureRmVmInputs { <# .SYNOPSIS Generates a new instance of AzureRmVmInputs. .DESCRIPTION Useful for providing scripted inputs to the IntelliTect.AzureRm commands. .EXAMPLE $inputs = New-AzureRmVmInputs $inputs.Location = "westus" ... Get-AzureRmVmImagePublisher -Inputs $inputs #> return [AzureRmVmInputs]::new() } ## Private functions and variables function Assert-DomainNameIsAvailable([string]$domainNameLabel = "", [string]$location = "") { <# .SYNOPSIS Verifies that a given domain name is available for a location. .PARAMETER domainNameLabel Domain name to verify. .PARAMETER location Azure RM location in which to check the domain name. .EXAMPLE Assert-DomainNameIsAvailable "mydomain" "westus" #> if ($domainNameLabel -eq "" -or $location -eq "") { return } Write-Information "Verifying domain name is available ..." -InformationAction Continue $message = $null $domainOk = Test-AzureRmDnsAvailability -DomainQualifiedName $domainNameLabel -Location $location -ErrorAction SilentlyContinue if (!$?) { $message = "Test-AzureRmDnsAvailability failed with DomainNameLabel = $domainNameLabel" } elseif ( $domainOk -eq $false) { $message = "DomainNameLabel ($domainNameLabel) failed when tested for uniqueness." } if ($message) { throw $message } return } function Confirm-ScriptShouldContinue([bool]$confirm, [string]$message, [string]$continueMessage = $null) { <# .SYNOPSIS Prompt the user to determine if the script should continue. .PARAMETER confirm If false, then don't do the confirmation. Allows for passing value of -Confirm in. .PARAMETER message Message displayed with the confirmation prompt. .PARAMETER continueMessage Override the default description for the continue option. .EXAMPLE Confirm-ScriptShouldContinue $true "This will mess up your stuff" "If you continue, your stuff will be messed up" #> $confirmTitle = "Continue?" if (!$continueMessage) { $continueMessage = "Script will proceed which will result in changes to your Azure resources." } $confirmOptions = [System.Management.Automation.Host.ChoiceDescription[]]( ` (New-Object System.Management.Automation.Host.ChoiceDescription "&Continue", ` $continueMessage), ` (New-Object System.Management.Automation.Host.ChoiceDescription "&Stop", ` "Stop the script at this point.")) if (!$confirm) { return $true } $confirmResult = $host.UI.PromptForChoice($confirmTitle, $message, $confirmOptions, 1) return ($confirmResult -eq 0) } function Get-CachedDefaultValue([string]$propertyName) { <# .SYNOPSIS Wrapper for Get-AzureRmDefault, but uses cached values. - Returns default value for property. .DESCRIPTION If the provided property doesn't exist in the stored defaults null will be returned. .PARAMETER propertyName If Set-AzureRmDefault has been used for this property, then the stored value is returned. .EXAMPLE Set-AzureRmDefault -Location "westus" Get-CachedDefaultValue("Location") #> if (!$CachedDefaults) { $CachedDefaults = Get-AzureRmDefault } if ($CachedDefaults.PSObject.Properties -match $propertyName) { $CachedDefaults.$propertyName } else { $null } } function Get-MenuSelection([int]$selectionCount, [string]$prompt = "Please enter your selection") { <# .SYNOPSIS After a menu has been displayed this function is called to get the user's selection. - Returns menu value associated with the selection. .PARAMETER selectionCount How many menu selections are displayed. .PARAMETER prompt Prompt to display when asking for their selection. .EXAMPLE ... used internally by Get-InputFromMenu #> $validSelection = $false $itemSelected = $false if ($OriginalMenuSelections.Count -ne $CurrentMenuSelections.Count) { $prompt += " (** to restore original menu items)" } else { $prompt += " (enter a partial value to filter menu items)"} do { $selection = Read-Host $prompt if ($selection -in 1..$selectionCount) { $validSelection = $true $itemSelected = $true } elseif ($selection -eq "**") { $validSelection = $true } elseif (($CurrentMenuSelections | Where-Object { $_.ToLower().Contains($selection.ToLower()) }).Count -gt 0) { $validSelection = $true } } while (!$validSelection) @{ ItemSelected = $itemSelected; Selection = $selection } } ## StackOverflow gems function Invoke-MenuMaker { <# .SYNOPSIS Displays a list of menu selections, along with an optional Title and Note .PARAMETER title Displayed above the menu. .PARAMETER note Displayed above the menu, but below the title. .PARAMETER selections List of menu choices. .PARAMETER subSelections Indicates a subset of an original list of selections is being displayed. .EXAMPLE ... used internally by Get-InputFromMenu #> param ( [string]$title = $null, [string]$note = $null, [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string[]]$selections, [bool]$subSelections = $false ) if (!$subSelections) { $script:OriginalMenuSelections = $selections } $script:CurrentMenuSelections = $selections $width = ($selections | Where-Object { $_.Length } | Sort-Object Length -Descending | Select-Object -First 1).Length if ($title -or $note) { $widthArray = @($width) if ($title) { $widthArray += $title.Length } if ($note) { $widthArray += $note.Length } $width = $widthArray | Sort-Object -Descending | Select-Object -First 1 } $buffer = if (($width * 1.5) -gt 200) { (200 - $width) / 2 } else { $width / 4 } if ($buffer -gt 4) { $buffer = 4 } $buffer = [int]$buffer $maxWidth = $buffer * 2 + $width + 5 $menu = "" $menu += "╔" + "═" * $maxWidth + "╗`n" if ($title) { $menu += "║" + " " * [Math]::Floor(($maxWidth - $title.Length) / 2) + $title + " " * [Math]::Ceiling(($maxWidth - $title.Length) / 2) + "║`n" $menu += "╟" + "─" * $maxWidth + "╢`n" } if ($note) { $menu += "║" + " " * [Math]::Floor(($maxWidth - $note.Length) / 2) + $note + " " * [Math]::Ceiling(($maxWidth - $note.Length) / 2) + "║`n" $menu += "╟" + "─" * $maxWidth + "╢`n" } for ($i = 1; $i -le $selections.Count; $i++) { $item = "$i`. ".PadRight(5) $menu += "║" + " " * $buffer + $item + $selections[$i - 1] + " " * ($maxWidth - $buffer - $item.Length - $selections[$i - 1].Length) + "║`n" } $menu += "╚" + "═" * $maxWidth + "╝`n" Write-Information $menu -InformationAction Continue } function Get-InputFromMenu([AzureRmVmInputs]$inputs, [string]$property, [string]$prompt, [ScriptBlock]$selectionScript, ` [string]$default = $null, [string]$note = $null, [bool]$confirmSingle = $false) { <# .SYNOPSIS Displays a menu and prompts the user for input. - Menu is not displayed: - If the property is already set on the provided $inputs. - If a default has been set for this property. - If $selectionScript only returns one option. - User will need to confirm - If $selectionScript returns no values and $default is provided. - An error is thrown if no $default is provided. .PARAMETER inputs AzureRmVmInputs instance to add the selection to. .PARAMETER property Name of the property on inputs that will be set. .PARAMETER prompt Prompt displayed to the user. .PARAMETER selectionScript Once a decision is made to display the menu, this script will provide the selections. - Won't be evaluated unless the menu will be displayed. .PARAMETER default If selectionScript returns no values then default will be used. .PARAMETER note Passed to Invoke-MenuMaker .PARAMETER confirmSingle Determines if user will be prompted if selectionScript returns only one value. .EXAMPLE $inputs = New-AzureRmVmInputs $locations = Get-AzureRmLocation -WarningAction SilentlyContinue | Sort-Object DisplayName | Select-Object -ExpandProperty Location Get-InputFromMenu $inputs "Location" "Select Location" { $locations } #> if (!$inputs.$property) { $inputs.$property = Get-CachedDefaultValue $property if (!$inputs.$property) { $selections = &$selectionScript if ($selections -isnot [System.Array]) { if (($selections -eq "" -or $null -eq $selections) -and !$default) { throw "No $($property) values found for supplied inputs."} elseif (($selections -eq "" -or $null -eq $selections) -and $default) { $inputs.$property = $default Write-Information "Using default value for $property - $($inputs.$property)" -InformationAction Continue } else { $inputs.$property = $selections Write-Information "Using single available value for $property - $($inputs.$property)" -InformationAction Continue if ($confirmSingle) { if (!(Confirm-ScriptShouldContinue $true "Single value available for $property - $($inputs.$property)." "Continuing will use the only available value.")) { $inputs.$property = $null } } } } else { $selectedItem = $null $subSelections = $false do { Invoke-MenuMaker -Title $prompt -Selections $selections -SubSelections $subSelections -Note $note $selection = Get-MenuSelection $selections.Count if ($selection.ItemSelected) { $selectedItem = $selection.Selection } else { if ($selection.Selection -eq "**") { $selections = $OriginalMenuSelections $subSelections = $false } else { $selections = $selections | Where-Object { $_.ToLower().Contains($selection.Selection.ToLower()) } $subSelections = $true } } } while (!$selectedItem) if ($selections -isnot [System.Array]) { $inputs.$property = $selections } else { $inputs.$property = $selections[$selectedItem - 1] } } } else { Write-Information "Using cached value for $property - $($inputs.$property)" -InformationAction Continue } } else { Write-Information "Using provided input for $property - $($inputs.$property)" -InformationAction Continue } } ## Set up defaults $FilePath = (Split-Path -Path $profile) + "\IntelliTectUserSettings.json" $CachedDefaults = $null $CurrentMenuSelections = $null $OriginalMenuSelections = $null Export-ModuleMember -Function New-AzureRmVirtualMachine Export-ModuleMember -Function Enable-RemotePowerShellOnAzureRmVm Export-ModuleMember -Function Get-AzureRmDefault Export-ModuleMember -Function Set-AzureRmDefault Export-ModuleMember -Function Get-AzureRmSubscriptionMenu Export-ModuleMember -Function Get-AzureRmLocationMenu Export-ModuleMember -Function Get-AzureRmVmImagePublisherMenu Export-ModuleMember -Function Get-AzureRmVmImageOfferMenu Export-ModuleMember -Function Get-AzureRmVmImageSkuMenu Export-ModuleMember -Function New-AzureRmVmInputs Export-ModuleMember -Function Get-AzureRmVmSizeMenu |