From d22d8b25877d0afe61a8db4c5291b49c141cdda9 Mon Sep 17 00:00:00 2001 From: Adrian Vladu Date: Tue, 9 Sep 2014 04:59:26 +0300 Subject: [PATCH] HOT for MSSQL Server with unit tests It deploys an MSSQL Server instance. The MSSQL ISO file is copied from an SMB share. The unit tests for the powershell module(user data scripts) are written using Pester 3.0 Change-Id: If2b024d7717e2846bb199e950c314ce9c9d778e7 Partially-Implements: blueprint windows-instances --- hot/Windows/MSSQLServer/MSSQL.ps1 | 41 ++ hot/Windows/MSSQLServer/MSSQL.psm1 | 214 ++++++++++ hot/Windows/MSSQLServer/MSSQL.yaml | 173 ++++++++ hot/Windows/MSSQLServer/Tests/MSSQL.Tests.ps1 | 205 ++++++++++ .../MSSQLServer/heat-powershell-utils.psm1 | 369 ++++++++++++++++++ 5 files changed, 1002 insertions(+) create mode 100755 hot/Windows/MSSQLServer/MSSQL.ps1 create mode 100755 hot/Windows/MSSQLServer/MSSQL.psm1 create mode 100755 hot/Windows/MSSQLServer/MSSQL.yaml create mode 100755 hot/Windows/MSSQLServer/Tests/MSSQL.Tests.ps1 create mode 100755 hot/Windows/MSSQLServer/heat-powershell-utils.psm1 diff --git a/hot/Windows/MSSQLServer/MSSQL.ps1 b/hot/Windows/MSSQLServer/MSSQL.ps1 new file mode 100755 index 00000000..5470bcc6 --- /dev/null +++ b/hot/Windows/MSSQLServer/MSSQL.ps1 @@ -0,0 +1,41 @@ +#ps1_sysnative + +# Copyright 2014 Cloudbase Solutions Srl +# +# 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. + +$ErrorActionPreference = 'Stop' + +$moduleName = "MSSQL.psm1" +$cfnFolder = "C:\cfn" +$modulePath = Join-Path $cfnFolder $moduleName +Import-Module -Name $modulePath -DisableNameChecking -Force + + +$mssqlServiceUsername="mssql-service-username" +$mssqlServicePassword = "mssql-service-user-password" +$mssqlSaPassword = "mssql-sa-password" +$mssqlFeatures = "mssql-features" +$mssqlInstanceName = "mssql-instance-name" +$mssqlIsoUNCPath = "mssql_iso_unc_path" +$mssqlWaitConditionEndpoint = "mssql_wait_condition_endpoint" +$mssqlWaitConditionToken = "mssql_wait_condition_token" + +Install-MSSQL -MssqlServiceUsername $mssqlServiceUsername ` + -MssqlServicePassword $mssqlServicePassword ` + -MssqlSaPassword $mssqlSaPassword ` + -MssqlFeatures $mssqlFeatures ` + -MssqlInstanceName $mssqlInstanceName ` + -MssqlIsoUNCPath $mssqlIsoUNCPath ` + -MssqlWaitConditionEndpoint $mssqlWaitConditionEndpoint ` + -MssqlWaitConditionToken $MssqlWaitConditionToken \ No newline at end of file diff --git a/hot/Windows/MSSQLServer/MSSQL.psm1 b/hot/Windows/MSSQLServer/MSSQL.psm1 new file mode 100755 index 00000000..9aad00a1 --- /dev/null +++ b/hot/Windows/MSSQLServer/MSSQL.psm1 @@ -0,0 +1,214 @@ +#ps1_sysnative + +# Copyright 2014 Cloudbase Solutions Srl +# +# 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. + +$ErrorActionPreference = 'Stop' + +$modulePath = "heat-powershell-utils.psm1" +$currentLocation = Split-Path -Parent $MyInvocation.MyCommand.Path +$fullPath = Join-Path $currentLocation $modulePath +Import-Module -Name $fullPath -DisableNameChecking -Force + +$heatTemplateName = "MSSQL" +$sqlLogFile = Join-Path ${ENV:ProgramFiles} -ChildPath ` + "\Microsoft SQL Server\110\Setup Bootstrap\Log\Summary.txt" + +function Log { + param( + $message + ) + LogTo-File -LogMessage $message -Topic $heatTemplateName + Log-HeatMessage $message +} + +function Install-RequiredFeatures { + param() + + $windowsFeatures = @('NET-Framework-Core') + $installStatus = Install-WindowsFeatures $windowsFeatures + if ($installStatus.Reboot -eq $true) { + Log "System will reboot to finish install updates." + ExitFrom-Script $rebootAndReexecuteCode + } +} + +function Add-MSSQLUser { + param( + $mssqlServiceUsername, + $mssqlServicePassword + ) + + Log "Adding the MSSQL user." + Add-WindowsUser $mssqlServiceUsername $mssqlServicePassword +} + +function Copy-FilesLocal { + param( + $From + ) + + $copyLocal = ${ENV:Temp} + $fileName = Split-Path -Path $From -Leaf + $localPath = Join-Path -Path $copyLocal $fileName + Copy-Item -Force -Recurse -Path $From -Destination $localPath + Log ("Local iso path:" + $localPath) + return $localPath +} + +function Get-MSSQLParameters { + param( + $MssqlServiceUsername, + $MssqlServicePassword, + $MssqlSaPassword, + $MssqlFeatures, + $MssqlInstanceName + ) + + $parameters = "/ACTION=install " + $parameters += "/Q " + $parameters += "/IACCEPTSQLSERVERLICENSETERMS=1 " + $parameters += "/INSTANCENAME=$MssqlInstanceName " + $parameters += "/FEATURES=$MssqlFeatures " + $parameters += "/SQLSYSADMINACCOUNTS=Admin " + $parameters += "/UpdateEnabled=1 " + $parameters += "/AGTSVCSTARTUPTYPE=Automatic " + $parameters += "/BROWSERSVCSTARTUPTYPE=Automatic " + $parameters += "/SECURITYMODE=SQL " + $parameters += "/SAPWD=$MssqlSaPassword " + $parameters += "/SQLSVCACCOUNT=.\$MssqlServiceUsername " + $parameters += "/SQLSVCPASSWORD=$MssqlServicePassword " + $parameters += "/SQLSVCSTARTUPTYPE=Automatic " + $parameters += "/NPENABLED=1 " + $parameters += "/TCPENABLED=1 /ERRORREPORTING=1" + + return $parameters +} + +function Get-MSSQLError { + param() + + $sqlErrorString = "Failed: see details below" + $errorsCount = (Select-String $sqlErrorString -Path $sqlLogFile).Length + if ($errorsCount -ne 0) { + Log "MSSQL log file has an error." + return $true + } + + return $false +} + +function Add-NetRules { + param() + Open-Port 80 "TCP" "HTTP" + Open-Port 443 "TCP" "HTTPS" + Open-Port 1434 "UDP" "SQL Browser" + Open-Port 135 "TCP" "SQL Debugger/RPC" + Open-Port 5355 "UDP" "Link Local Multicast Name Resolution" + + netsh.exe firewall set multicastbroadcastresponse ENABLE + + $sqlServerBinaryPath = Join-Path ${ENV:ProgramFiles} -ChildPath ` + "Microsoft SQL Server\MSSQL11.MSSQL\MSSQL\Binn\sqlservr.exe" + New-NetFirewallRule -DisplayName "Allow TCP Sql Server Ports" ` + -Direction Inbound -Action Allow -EdgeTraversalPolicy Allow ` + -Protocol UDP -LocalPort 100-65000 -Program $sqlServerBinaryPath + New-NetFirewallRule -DisplayName "Allow TCP Sql Server Ports" ` + -Direction Inbound -Action Allow -EdgeTraversalPolicy Allow ` + -Protocol TCP -LocalPort 100-65000 -Program $sqlServerBinaryPath +} + +function Install-MSSQLInternal { + param( + $MssqlServiceUsername, + $MssqlServicePassword, + $MssqlSaPassword, + $MssqlFeatures, + $MssqlInstanceName, + $MssqlIsoUNCPath, + $MssqlWaitConditionEndpoint, + $MssqlWaitConditionToken + ) + + Log "Started MSSQL instalation." + + Install-RequiredFeatures + + Add-MSSQLUser $MssqlServiceUsername $MssqlServicePassword + + $localIsoPath = Copy-FilesLocal $MssqlIsoUNCPath + Log "MSSQL ISO Mount." + $iso = Mount-DiskImage -PassThru $localIsoPath + + $isoSetupPath = (Get-Volume -DiskImage $iso).DriveLetter + ":\setup.exe" + if (Test-Path $sqlLogFile) { + Remove-Item $sqlLogFile -Force + } + $parameters = Get-MSSQLParameters ` + -MssqlServiceUsername $mssqlServiceUsername ` + -MssqlServicePassword $mssqlServicePassword ` + -MssqlSaPassword $mssqlSaPassword ` + -MssqlFeatures $mssqlFeatures ` + -MssqlInstanceName $mssqlInstanceName + ExecuteWith-Retry -Command { + param($isoSetupPath, $parameters) + Start-Process -Wait -FilePath $isoSetupPath ` + -ArgumentList $parameters + } -Arguments @($isoSetupPath, $parameters) + + Dismount-DiskImage -ImagePath $iso.ImagePath + Remove-Item $localIsoPath + + if ((Get-MSSQLError) -eq $true) { + throw "Failed to install MSSQL Server." + } + Add-NetRules + + $successMessage = "Finished MSSQL instalation." + Log $successMessage + Send-HeatWaitSignal -Endpoint $MssqlWaitConditionEndpoint ` + -Message $successMessage -Success $true ` + -Token $MssqlWaitConditionToken +} + +function Install-MSSQL { + param( + $MssqlServiceUsername, + $MssqlServicePassword, + $MssqlSaPassword, + $MssqlFeatures, + $MssqlInstanceName, + $MssqlIsoUNCPath, + $MssqlWaitConditionEndpoint, + $MssqlWaitConditionToken + ) + + try { + Install-MSSQLInternal -MssqlServiceUsername $mssqlServiceUsername ` + -MssqlServicePassword $mssqlServicePassword ` + -MssqlSaPassword $mssqlSaPassword ` + -MssqlFeatures $mssqlFeatures ` + -MssqlInstanceName $mssqlInstanceName ` + -MssqlIsoUNCPath $mssqlIsoUNCPath ` + -MssqlWaitConditionEndpoint $mssqlWaitConditionEndpoint ` + -MssqlWaitConditionToken $MssqlWaitConditionToken + } catch { + Log "Failed to install template." + Send-HeatWaitSignal -Endpoint $MssqlWaitConditionEndpoint ` + -Message $_.Exception.Message -Success $false ` + -Token $MssqlWaitConditionToken + } +} + +Export-ModuleMember -Function * -ErrorAction SilentlyContinue \ No newline at end of file diff --git a/hot/Windows/MSSQLServer/MSSQL.yaml b/hot/Windows/MSSQLServer/MSSQL.yaml new file mode 100755 index 00000000..aba38c3b --- /dev/null +++ b/hot/Windows/MSSQLServer/MSSQL.yaml @@ -0,0 +1,173 @@ +heat_template_version: 2013-05-23 + +description: > + Deploys a Microsoft SQL Server instance. + +parameters: + key_name: + description: Name of an existing keypair to encrypt the Admin password + type: string + + flavor: + description: Id or name of an existing flavor + type: string + default: m1.small + + image: + description: Id or name of an existing Windows image + type: string + + public_network_id: + type: string + description: > + ID of an existing public network where a floating IP will be + allocated. + + private_network_id: + type: string + description: Id of an existing private network + + mssql_service_username: + type: string + default: mssqluser + description: > + The username under which the mssql service runs + + mssql_service_user_password: + type: string + hidden: true + constraints: + - length: { min: 8, max: 64 } + - allowed_pattern: "(?=^.{6,255}$)((?=.*\\d)(?=.*[A-Z])(?=.*[a-z])|(?=.*\\d)(?=.*[^A-Za-z0-9])(?=.*[a-z])|(?=.*[^A-Za-z0-9])(?=.*[A-Z])(?=.*[a-z])|(?=.*\\d)(?=.*[A-Z])(?=.*[^A-Za-z0-9]))^.*" + description: > + The password of the user under which the mssql service runs + + mssql_sa_password: + type: string + hidden: true + description: > + The password of the sa mssql user + + mssql_features: + type: string + default: > + SQLENGINE,ADV_SSMS + description: > + The mssql features to be installed + + mssql_instance: + type: string + default: mssql + description: > + The mssql instance name + + mssql_iso_unc_path: + type: string + description: > + The mssql iso file UNC path. It can be a local path or a network path. + + mssql_max_timeout: + type: number + default: 3600 + description: > + The maximum allowed time for the mssql instalation to finish. + +resources: + server_port: + type: OS::Neutron::Port + properties: + network_id: { get_param: private_network_id } + + server_floating_ip: + type: OS::Neutron::FloatingIP + depends_on: server_port + properties: + floating_network_id: { get_param: public_network_id } + port_id: { get_resource: server_port } + + utils_module: + type: OS::Heat::SoftwareConfig + properties: + group: ungrouped + config: { get_file: heat-powershell-utils.psm1 } + + mssql_module: + type: OS::Heat::SoftwareConfig + properties: + group: ungrouped + config: { get_file: MSSQL.psm1 } + + mssql_main: + type: OS::Heat::SoftwareConfig + properties: + group: ungrouped + config: + str_replace: + template: { get_file: MSSQL.ps1 } + params: + mssql-service-username: + { get_param: mssql_service_username } + mssql-service-user-password: + { get_param: mssql_service_user_password } + mssql-sa-password: + { get_param: mssql_sa_password } + mssql-features: + { get_param: mssql_features } + mssql-instance-name: + { get_param: mssql_instance } + mssql_iso_unc_path: + { get_param: mssql_iso_unc_path } + mssql_wait_condition_endpoint: + { get_attr: [ mssql_wait_condition_handle, endpoint ] } + mssql_wait_condition_token: + { get_attr: [ mssql_wait_condition_handle, token ] } + + mssql_init: + type: OS::Heat::MultipartMime + depends_on: mssql_wait_condition_handle + properties: + parts: + [ { + filename: "heat-powershell-utils.psm1", + subtype: "x-cfninitdata", + config: { get_resource: utils_module } + }, + { + filename: "MSSQL.psm1", + subtype: "x-cfninitdata", + config: { get_resource: mssql_module } + }, + { + filename: "cfn-userdata", + subtype: "x-cfninitdata", + config: { get_resource: mssql_main } + } + ] + + mssql: + type: OS::Nova::Server + depends_on: [ server_port, mssql_init ] + properties: + image: { get_param: image } + flavor: { get_param: flavor } + key_name: { get_param: key_name } + networks: + - port: { get_resource: server_port } + user_data_format: RAW + user_data: { get_resource: mssql_init } + + mssql_wait_condition: + type: OS::Heat::WaitCondition + depends_on: mssql_wait_condition_handle + properties: + count: 1 + handle: { get_resource: mssql_wait_condition_handle } + timeout: { get_param: mssql_max_timeout } + + mssql_wait_condition_handle: + type: OS::Heat::WaitConditionHandle + +outputs: + mssql_server_public_ip: + description: The MSSQL public IP address + value: { get_attr: [ server_floating_ip, floating_ip_address ] } diff --git a/hot/Windows/MSSQLServer/Tests/MSSQL.Tests.ps1 b/hot/Windows/MSSQLServer/Tests/MSSQL.Tests.ps1 new file mode 100755 index 00000000..3cefcabe --- /dev/null +++ b/hot/Windows/MSSQLServer/Tests/MSSQL.Tests.ps1 @@ -0,0 +1,205 @@ +#ps1_sysnative + +# Copyright 2014 Cloudbase Solutions Srl +# +# 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. + +$moduleName = "MSSQL" +$modulePath = "../MSSQL.psm1" + +$currentLocation = Split-Path -Parent $MyInvocation.MyCommand.Path +$fullPath = Join-Path $currentLocation $modulePath + +Remove-Module -Name $moduleName -ErrorAction SilentlyContinue +Import-Module -Name $fullPath -DisableNameChecking -Force + +InModuleScope $moduleName { + Describe "Test Add MSSQL User" { + Context "On success" { + Mock Log { return $true } -Verifiable + Mock Add-WindowsUser { return $true } -Verifiable + + Add-MSSQLUser "fakeuser" "fakepassword" + + It "should verify caled all mocks" { + Assert-VerifiableMocks + } + } + } +} + +InModuleScope $moduleName { + Describe "Test Add Net Rules" { + Context "On success" { + Mock netsh.exe { return $true } -Verifiable + Mock New-NetFirewallRule { return $true } -Verifiable + + Add-NetRules + + It "should verify caled all mocks" { + Assert-VerifiableMocks + } + } + } +} + +InModuleScope $moduleName { + Describe "Test Get MSSQL Error" { + Context "With errors" { + $fakeErrors = @("err1") + Mock Select-String { return $fakeErrors } -Verifiable + Mock Log { return $true } -Verifiable + + $result = Get-MSSQLError + + It "should return true" { + $result | Should Be $true + } + + It "should verify caled all mocks" { + Assert-VerifiableMocks + } + } + Context "No errors" { + $fakeErrors = @() + Mock Select-String { return $fakeErrors } -Verifiable + + $result = Get-MSSQLError + + It "should return false" { + $result | Should Be $false + } + + It "should verify caled all mocks" { + Assert-VerifiableMocks + } + } + } +} + +InModuleScope $moduleName { + Describe "Test Install Required Features" { + Context "No reboot" { + $fakeStatus = @{"Reboot"=$false} + Mock Install-WindowsFeatures { return $fakeStatus } -Verifiable + + Install-RequiredFeatures + + It "should verify caled all mocks" { + Assert-VerifiableMocks + } + } + Context "With reboot" { + $fakeStatus = @{"Reboot"=$true} + Mock Install-WindowsFeatures { return $fakeStatus } -Verifiable + Mock ExitFrom-Script { return $true } -Verifiable + Mock Log { return $true } -Verifiable + + Install-RequiredFeatures + + It "should verify caled all mocks" { + Assert-VerifiableMocks + } + } + } +} + +InModuleScope $moduleName { + Describe "Test Install internal MSSQL" { + $mssqlServiceUsername="mssql-service-username"; + $mssqlServicePassword = "mssql-service-user-password"; + $mssqlSaPassword = "mssql-sa-password"; + $mssqlFeatures = "mssql-features"; + $mssqlInstanceName = "mssql-instance-name"; + $mssqlIsoUNCPath = "mssql_iso_unc_path"; + $mssqlWaitConditionEndpoint = "mssql_wait_condition_endpoint"; + $mssqlWaitConditionToken = "mssql_wait_condition_token"; + Context "On success" { + + $cimInstance = New-CimInstance -ClassName "MSFT_DiskImage" ` + -Namespace "root/Microsoft/Windows/Storage" ` + -ClientOnly ` + -Property @{"ImagePath"="fakePath"} + + Mock Log { return $true } -Verifiable + Mock Install-RequiredFeatures { return $true } -Verifiable + Mock Add-MsSQLUser { return $true } -Verifiable + Mock Copy-FilesLocal { return $true } -Verifiable + Mock Mount-DiskImage { return $cimInstance } -Verifiable + Mock Get-Volume { return $true } -Verifiable + Mock Test-Path { return $true } -Verifiable + Mock Remove-Item { return $true } -Verifiable + Mock Get-MSSQLParameters { return $true } -Verifiable + Mock ExecuteWith-Retry { return $true } -Verifiable + Mock Dismount-DiskImage { return $true } -Verifiable + Mock Get-MssqlError { return $false } -Verifiable + Mock Add-NetRules { return $true } -Verifiable + Mock Send-HeatWaitSignal { return $true } -Verifiable + + $result = Install-MSSQLInternal -MssqlServiceUsername $mssqlServiceUsername ` + -MssqlServicePassword $mssqlServicePassword ` + -MssqlSaPassword $mssqlSaPassword ` + -MssqlFeatures $mssqlFeatures ` + -MssqlInstanceName $mssqlInstanceName ` + -MssqlIsoUNCPath $mssqlIsoUNCPath ` + -MssqlWaitConditionEndpoint $mssqlWaitConditionEndpoint ` + -MssqlWaitConditionToken $MssqlWaitConditionToken + + It "should succeed" { + $result | Should Be $true + } + + It "should verify caled all mocks" { + Assert-VerifiableMocks + } + } + + Context "On failure internal" { + + Mock Install-MSSQLInternal { throw } -Verifiable + Mock Log { return $true } -Verifiable + Mock Send-HeatWaitSignal { return $true } -Verifiable + + Install-MSSQL -MssqlServiceUsername $mssqlServiceUsername ` + -MssqlServicePassword $mssqlServicePassword ` + -MssqlSaPassword $mssqlSaPassword ` + -MssqlFeatures $mssqlFeatures ` + -MssqlInstanceName $mssqlInstanceName ` + -MssqlIsoUNCPath $mssqlIsoUNCPath ` + -MssqlWaitConditionEndpoint $mssqlWaitConditionEndpoint ` + -MssqlWaitConditionToken $MssqlWaitConditionToken + + It "should verify caled all mocks" { + Assert-VerifiableMocks + } + } + + Context "On succes internal" { + + Mock Install-MSSQLInternal { return $true } -Verifiable + + $result = Install-MSSQL -MssqlServiceUsername $mssqlServiceUsername ` + -MssqlServicePassword $mssqlServicePassword ` + -MssqlSaPassword $mssqlSaPassword ` + -MssqlFeatures $mssqlFeatures ` + -MssqlInstanceName $mssqlInstanceName ` + -MssqlIsoUNCPath $mssqlIsoUNCPath ` + -MssqlWaitConditionEndpoint $mssqlWaitConditionEndpoint ` + -MssqlWaitConditionToken $MssqlWaitConditionToken + + It "should be successful" { + Assert-VerifiableMocks + } + } + } +} \ No newline at end of file diff --git a/hot/Windows/MSSQLServer/heat-powershell-utils.psm1 b/hot/Windows/MSSQLServer/heat-powershell-utils.psm1 new file mode 100755 index 00000000..786f6dae --- /dev/null +++ b/hot/Windows/MSSQLServer/heat-powershell-utils.psm1 @@ -0,0 +1,369 @@ +#ps1_sysnative + +# Copyright 2014 Cloudbase Solutions Srl +# +# 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. + +$rebotCode = 1001 +$reexecuteCode = 1002 +$rebootAndReexecuteCode = 1003 + +function Log-HeatMessage { + param( + [string]$Message + ) + + Write-Host $Message +} + +function ExitFrom-Script { + param( + [int]$ExitCode + ) + + exit $ExitCode +} + +function ExecuteWith-Retry { + param( + [ScriptBlock]$Command, + [int]$MaxRetryCount=10, + [int]$RetryInterval=3, + [array]$Arguments=@() + ) + + $currentErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = "Continue" + + $retryCount = 0 + while ($true) { + try { + $res = Invoke-Command -ScriptBlock $Command ` + -ArgumentList $Arguments + $ErrorActionPreference = $currentErrorActionPreference + return $res + } catch [System.Exception] { + $retryCount++ + if ($retryCount -gt $MaxRetryCount) { + $ErrorActionPreference = $currentErrorActionPreference + throw $_.Exception + } else { + Write-Error $_.Exception + Start-Sleep $RetryInterval + } + } + } +} + +function Execute-Command { + param( + [ScriptBlock]$Command, + [array]$Arguments=@(), + [string]$ErrorMessage + ) + + $res = Invoke-Command -ScriptBlock $Command -ArgumentList $Arguments + if ($LASTEXITCODE -ne 0) { + throw $ErrorMessage + } + return $res +} + +function Install-WindowsFeatures { + param( + [Parameter(Mandatory=$true)] + [array]$Features, + [int]$RebootCode=$rebootAndReexecuteCode + ) + + $winVer = (Get-WmiObject -class Win32_OperatingSystem).Version.Split('.') + $isWinServer2008R2 = (($winVer[0] -eq 6) -and ($winVer[1] -eq 1)) + if ($isWinServer2008R2 -eq $true) { + Import-Module ServerManager + } + + $rebootNeeded = $false + foreach ($feature in $Features) { + if ($isWinServer2008R2 -eq $true) { + $state = ExecuteWith-Retry -Command { + Add-WindowsFeature -Name $feature -ErrorAction Stop + } + } else { + $state = ExecuteWith-Retry -Command { + Install-WindowsFeature -Name $feature -ErrorAction Stop + } + } + if ($state.Success -eq $true) { + if ($state.RestartNeeded -eq 'Yes') { + $rebootNeeded = $true + } + } else { + throw "Install failed for feature $feature" + } + } + + if ($rebootNeeded -eq $true) { + ExitFrom-Script $RebootCode + } +} + +function CopyFrom-SambaShare { + param( + [Parameter(Mandatory=$true)] + [string]$SambaDrive, + [Parameter(Mandatory=$true)] + [string]$SambaShare, + [Parameter(Mandatory=$true)] + [string]$SambaFolder, + [Parameter(Mandatory=$true)] + [string]$FileName, + [Parameter(Mandatory=$true)] + [string]$Destination + ) + + $p = New-PSDrive -Name $SambaDrive -Root $SambaShare -PSProvider FileSystem + $samba = ($SambaDrive + ":\\" + $SambaFolder) + $localPath = "$Destination\$FileName" + if (!(Test-Path $localPath)) { + Copy-Item "$samba\$FileName" $Destination -Recurse + } + return $localPath + +} + +function Unzip-File { + param( + [Parameter(Mandatory=$true)] + [string]$ZipFile, + [Parameter(Mandatory=$true)] + [string]$Destination + ) + + $shellApp = New-Object -ComObject Shell.Application + $zipFileNs = $shellApp.NameSpace($ZipFile) + $destinationNs = $shellApp.NameSpace($Destination) + $destinationNs.CopyHere($zipFileNs.Items(), 0x4) +} + +function Download-File { + param( + [Parameter(Mandatory=$true)] + [string]$DownloadLink, + [Parameter(Mandatory=$true)] + [string]$DestinationFile + ) + + $webclient = New-Object System.Net.WebClient + ExecuteWith-Retry -Command { + $webclient.DownloadFile($DownloadLink, $DestinationFile) + } +} + +# Get-FileHash for Powershell versions less than 4.0 (SHA1 algorithm only) +function Get-FileSHA1Hash { + [CmdletBinding()] + param( + [parameter(Mandatory=$true)] + [string]$Path, + [string]$Algorithm = "SHA1" + ) + + process + { + if ($Algorithm -ne "SHA1") { + throw "Unsupported algorithm: $Algorithm" + } + $fullPath = Resolve-Path $Path + $f = [System.IO.File]::OpenRead($fullPath) + $sham = $null + try { + $sham = New-Object System.Security.Cryptography.SHA1Managed + $hash = $sham.ComputeHash($f) + $hashSB = New-Object System.Text.StringBuilder ` + -ArgumentList ($hash.Length * 2) + foreach ($b in $hash) { + $sb = $hashSB.AppendFormat("{0:x2}", $b) + } + return [PSCustomObject]@{ Algorithm="SHA1"; + Hash=$hashSB.ToString().ToUpper(); + Path=$fullPath } + } + finally { + $f.Close() + if($sham) { + $sham.Clear() + } + } + } +} + +function Check-FileIntegrityWithSHA1 { + param( + [Parameter(Mandatory=$true)] + [string]$File, + [Parameter(Mandatory=$true)] + [string]$ExpectedSHA1Hash + ) + + if ($PSVersionTable.PSVersion.Major -lt 4) { + $hash = (Get-FileSHA1Hash -Path $File).Hash + } else { + $hash = (Get-FileHash -Path $File -Algorithm "SHA1").Hash + } + if ($hash -ne $ExpectedSHA1Hash) { + throw ("SHA1 hash not valid for file: $filename. " + + "Expected: $ExpectedSHA1Hash Current: $hash") + } +} + +function Install-Program { + param( + [Parameter(Mandatory=$true)] + [string]$DownloadLink, + [Parameter(Mandatory=$true)] + [string]$DestinationFile, + [Parameter(Mandatory=$true)] + [string]$ExpectedSHA1Hash, + [Parameter(Mandatory=$true)] + [string]$Arguments, + [Parameter(Mandatory=$true)] + [string]$ErrorMessage + ) + + Download-File $DownloadLink $DestinationFile + Check-FileIntegrityWithSHA1 $DestinationFile $ExpectedSHA1Hash + + $p = Start-Process -FilePath $DestinationFile ` + -ArgumentList $Arguments ` + -PassThru ` + -Wait + if ($p.ExitCode -ne 0) { + throw $ErrorMessage + } + + Remove-Item $DestinationFile +} + +function Set-IniFileValue { + [CmdletBinding()] + param( + [parameter(Mandatory=$true, ValueFromPipeline=$true)] + [string]$Key, + [parameter()] + [string]$Section = "DEFAULT", + [parameter(Mandatory=$true)] + [string]$Value, + [parameter(Mandatory=$true)] + [string]$Path + ) + + process + { + $Source = @" + using System; + using System.Text; + using System.Runtime.InteropServices; + + namespace PSCloudbase + { + public sealed class Win32IniApi + { + [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] + public static extern uint GetPrivateProfileString( + string lpAppName, + string lpKeyName, + string lpDefault, + StringBuilder lpReturnedString, + uint nSize, + string lpFileName); + + [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool WritePrivateProfileString( + string lpAppName, + string lpKeyName, + StringBuilder lpString, // Don't use string, as Powershell replaces $null with an empty string + string lpFileName); + + [DllImport("Kernel32.dll")] + public static extern uint GetLastError(); + } + } +"@ + Add-Type -TypeDefinition $Source -Language CSharp + $retVal = [PSCloudbase.Win32IniApi]::WritePrivateProfileString($Section, $Key, $Value, $Path) + if (!$retVal -and [PSCloudbase.Win32IniApi]::GetLastError()) { + throw "Cannot set value in ini file: " + [PSCloudbase.Win32IniApi]::GetLastError() + } + } +} + +function LogTo-File { + param( + $LogMessage, + $LogFile="C:\cfn\userdata.log", + $Topic="general" + ) + + $date = Get-Date + $fullMessage = "$Date | $Topic | $LogMessage" + Add-Content -Path $logFile -Value $fullMessage +} + +function Open-Port($port, $protocol, $name) { + netsh.exe advfirewall firewall add rule name=$name dir=in action=allow ` + protocol=$protocol localport=$port +} + +function Add-WindowsUser { + param( + [parameter(Mandatory=$true)] + [string]$Username, + [parameter(Mandatory=$true)] + [string]$Password + ) + + NET.EXE USER $Username $Password '/ADD' +} + +function Send-HeatWaitSignal { + param( + [parameter(Mandatory=$true)] + [string]$Endpoint, + [parameter(Mandatory=$true)] + [string]$Token, + $Message, + $Success=$true + ) + + $statusMap = @{ + $true="SUCCESS"; + $false="FAILURE" + } + + $heatMessage = @{ + "status"=$statusMap[$Success]; + "reason"="Configuration script has been executed."; + "data"=$Message; + } + $headers = @{ + "X-Auth-Token"=$Token; + "Accept"="application/json"; + "Content-Type"= "application/json"; + } + $heatMessageJSON = $heatMessage | ConvertTo-JSON + $result = Invoke-RestMethod -Method POST -Uri $Endpoint ` + -Body $heatMessageJSON -Headers $headers +} + +Export-ModuleMember -Function *