From eb592c25116c0bc20f603b2a62c55ed6dce6ff29 Mon Sep 17 00:00:00 2001 From: Dominic Robinson <65237317+drobinson-moj@users.noreply.github.com> Date: Tue, 13 Feb 2024 13:53:29 +0000 Subject: [PATCH] DSOS-2601: powershell fixes for win2012 (#556) * support pwsh 4 * fix * comments * Fix * add user-data script * fix * test * fix * fix * test * test * Fix * fix * fix * fix * Fixes * fix * Readme fix * fix * readme * fix * bump timeout * Fix * fix * more debug * fix --- .../Modules/ModPlatformAD/ModPlatformAD.psd1 | 6 +- .../ModPlatformAD/ModPlatformADComputer.psm1 | 4 +- .../ModPlatformAD/ModPlatformADConfig.psm1 | 69 ++++++++++++++----- .../ModPlatformADCredential.psm1 | 38 +++++----- powershell/README.md | 6 +- powershell/Scripts/Invoke-UserDataScript.ps1 | 21 ++++++ .../ModPlatformAD/Join-ModPlatformAD.ps1 | 11 +-- powershell/Scripts/Run-GitScript.ps1 | 20 ++++-- .../HmppsDomainServicesTest.ps1 | 1 + 9 files changed, 126 insertions(+), 50 deletions(-) create mode 100644 powershell/Scripts/Invoke-UserDataScript.ps1 create mode 100644 powershell/Scripts/UserDataScripts/HmppsDomainServicesTest.ps1 diff --git a/powershell/Modules/ModPlatformAD/ModPlatformAD.psd1 b/powershell/Modules/ModPlatformAD/ModPlatformAD.psd1 index 7296284e7..548357798 100644 --- a/powershell/Modules/ModPlatformAD/ModPlatformAD.psd1 +++ b/powershell/Modules/ModPlatformAD/ModPlatformAD.psd1 @@ -3,7 +3,7 @@ # # Generated by: Ministry of Justice # -# Generated on: 09/02/2024 +# Generated on: 12/02/2024 # @{ @@ -12,7 +12,7 @@ # RootModule = '' # Version number of this module. -ModuleVersion = '1.0.0.1' +ModuleVersion = '1.0.0.2' # Supported PSEditions # CompatiblePSEditions = @() @@ -33,7 +33,7 @@ Copyright = '(c) 2024 Crown Copyright (Ministry of Justice)' Description = 'Modernisation Platform ModPlatformAD module' # Minimum version of the PowerShell engine required by this module -PowerShellVersion = '5.1.0.0' +PowerShellVersion = '4.0' # Name of the PowerShell host required by this module # PowerShellHostName = '' diff --git a/powershell/Modules/ModPlatformAD/ModPlatformADComputer.psm1 b/powershell/Modules/ModPlatformAD/ModPlatformADComputer.psm1 index 3a75ac685..01899c144 100644 --- a/powershell/Modules/ModPlatformAD/ModPlatformADComputer.psm1 +++ b/powershell/Modules/ModPlatformAD/ModPlatformADComputer.psm1 @@ -32,8 +32,8 @@ function Rename-ModPlatformADComputer { $ErrorActionPreference = "Stop" - $Token = Invoke-RestMethod -TimeoutSec 2 -Headers @{"X-aws-ec2-metadata-token-ttl-seconds"=3600} -Method PUT -Uri http://169.254.169.254/latest/api/token - $InstanceId = Invoke-RestMethod -TimeoutSec 2 -Headers @{"X-aws-ec2-metadata-token" = $Token} -Method GET -Uri http://169.254.169.254/latest/meta-data/instance-id + $Token = Invoke-RestMethod -TimeoutSec 10 -Headers @{"X-aws-ec2-metadata-token-ttl-seconds"=3600} -Method PUT -Uri http://169.254.169.254/latest/api/token + $InstanceId = Invoke-RestMethod -TimeoutSec 10 -Headers @{"X-aws-ec2-metadata-token" = $Token} -Method GET -Uri http://169.254.169.254/latest/meta-data/instance-id $TagsRaw = aws ec2 describe-tags --filters "Name=resource-id,Values=$InstanceId" $Tags = "$TagsRaw" | ConvertFrom-Json diff --git a/powershell/Modules/ModPlatformAD/ModPlatformADConfig.psm1 b/powershell/Modules/ModPlatformAD/ModPlatformADConfig.psm1 index 2b12cf13a..b4916deb0 100644 --- a/powershell/Modules/ModPlatformAD/ModPlatformADConfig.psm1 +++ b/powershell/Modules/ModPlatformAD/ModPlatformADConfig.psm1 @@ -5,8 +5,8 @@ function Get-ModPlatformADConfig { Retrieve appropriate AD config for the given Modernisation Platform environment. .DESCRIPTION - Either pass in the doman name as a parameter, or derive the AD configuration - from EC2 tags (environment-name or domain-name). + Either pass in the domain name as a parameter, or derive the AD configuration + from EC2 tags (environment-name or domain-name). EC2 requires permissions to get tags and the aws cli. .PARAMETER DomainNameFQDN @@ -38,7 +38,6 @@ function Get-ModPlatformADConfig { ) "SecretAccountName" = "hmpps-domain-services-test" "SecretName" = "/microsoft/AD/azure.noms.root/shared-passwords" - "SecretRoleName" = "EC2HmppsDomainSecretsRole" "DomainNameFQDN" = "azure.noms.root" "DomainNameNetbios" = "AZURE" "DomainJoinUsername" = "svc_join_domain" @@ -51,37 +50,71 @@ function Get-ModPlatformADConfig { "planetfm-production", "corporate-staff-rostering-preproduction", "corporate-staff-rostering-production" - ) + ) "SecretAccountName" = "hmpps-domain-services-production" "SecretName" = "/microsoft/AD/azure.hmpp.root/shared-passwords" - "SecretRoleName" = "EC2HmppsDomainSecretsRole" "DomainNameFQDN" = "azure.hmpp.root" "DomainNameNetbios" = "HMPP" "DomainJoinUsername" = "svc_join_domain" } } - if ($DomainNameFQDN -and $ModPlatformADConfigs.ContainsKey($DomainNameFQDN)) { - return $ModPlatformADConfigs.[string]$DomainNameFQDN - } - $Token = Invoke-RestMethod -TimeoutSec 2 -Headers @{"X-aws-ec2-metadata-token-ttl-seconds"=3600} -Method PUT -Uri http://169.254.169.254/latest/api/token - $InstanceId = Invoke-RestMethod -TimeoutSec 2 -Headers @{"X-aws-ec2-metadata-token" = $Token} -Method GET -Uri http://169.254.169.254/latest/meta-data/instance-id + $ModPlatformADSecretRoleName = @{ + "EC2HmppsDomainSecretsRole" = @{ + "EnvironmentNameTags" = @( + "hmpps-domain-services-development", + "hmpps-domain-services-test", + "hmpps-domain-services-preproduction", + "hmpps-domain-services-production", + "planetfm-development", + "planetfm-test", + "planetfm-preproduction", + "planetfm-production", + "corporate-staff-rostering-development", + "corporate-staff-rostering-test" + "corporate-staff-rostering-preproduction", + "corporate-staff-rostering-production" + ) + } + } + + $Token = Invoke-RestMethod -TimeoutSec 10 -Headers @{"X-aws-ec2-metadata-token-ttl-seconds"=3600} -Method PUT -Uri http://169.254.169.254/latest/api/token + $InstanceId = Invoke-RestMethod -TimeoutSec 10 -Headers @{"X-aws-ec2-metadata-token" = $Token} -Method GET -Uri http://169.254.169.254/latest/meta-data/instance-id $TagsRaw = aws ec2 describe-tags --filters "Name=resource-id,Values=$InstanceId" $Tags = "$TagsRaw" | ConvertFrom-Json $DomainNameTag = ($Tags.Tags | Where-Object {$_.Key -eq "domain-name"}).Value $EnvironmentNameTag = ($Tags.Tags | Where-Object {$_.Key -eq "environment-name"}).Value - if ($DomainNameTag -and $ModPlatformADConfigs.containsKey($DomainNameTag)) { - return $ModPlatformADConfigs.[string]$DomainNameTag + $Key = $null + if ($DomainNameFQDN) { + $Key = $DomainNameFQDN + } elseif ($DomainNameTag) { + $Key = $DomainNameTag + } else { + foreach ($Config in $ModPlatformADConfigs.GetEnumerator() ) { + if ($Config.Value["EnvironmentNameTags"].Contains($EnvironmentNameTag)) { + $Key = $Config.Key + break + } + } } - - foreach ($Config in $ModPlatformADConfigs.GetEnumerator() ) { - if ($Config.Value["EnvironmentNameTags"].Contains($EnvironmentNameTag)) { - return $Config.Value + if ($Key) { + if ($ModPlatformADConfigs.ContainsKey($Key)) { + $ConfigCopy = $ModPlatformADConfigs[$Key].Clone() + foreach ($Config in $ModPlatformADSecretRoleName.GetEnumerator() ) { + if ($Config.Value["EnvironmentNameTags"].Contains($EnvironmentNameTag)) { + $ConfigCopy["SecretRoleName"] = $Config.Key + break + } + } + return $ConfigCopy + } else { + Write-Error "No matching configuration for domain ${Key}" } } - - Write-Error "No matching configuration for environment-name $EnvironmentNameTag" + else { + Write-Error "No matching configuration for environment-name ${EnvironmentNameTag}" + } } Export-ModuleMember -Function Get-ModPlatformADConfig diff --git a/powershell/Modules/ModPlatformAD/ModPlatformADCredential.psm1 b/powershell/Modules/ModPlatformAD/ModPlatformADCredential.psm1 index 95913d94a..b72e110db 100644 --- a/powershell/Modules/ModPlatformAD/ModPlatformADCredential.psm1 +++ b/powershell/Modules/ModPlatformAD/ModPlatformADCredential.psm1 @@ -6,7 +6,7 @@ function Get-ModPlatformADCredential { .DESCRIPTION Using configuration returned from Get-ModPlatformADConfig, this function - assumes a role to access a secret containing the password of the + optionally assumes a role to access a secret containing the password of the domain join username. EC2 requires permissions to join the given role, a SSM parameter containing account IDs, and the aws cli. @@ -37,23 +37,27 @@ function Get-ModPlatformADCredential { $SecretAccountId = $AccountIds.[string]$ModPlatformADConfig.SecretAccountName $SecretName = $ModPlatformADConfig.SecretName $SecretArn = "arn:aws:secretsmanager:eu-west-2:${SecretAccountId}:secret:${SecretName}" - $AccountId = aws sts get-caller-identity --query Account --output text - $SecretRoleName = $ModPlatformADConfig.SecretRoleName - $RoleArn = "arn:aws:iam::${AccountId}:role/${SecretRoleName}" - $Session = "ModPlatformADConfig-$env:COMPUTERNAME" - $CredsRaw = aws sts assume-role --role-arn "${RoleArn}" --role-session-name "${Session}" - $Creds = "$CredsRaw" | ConvertFrom-Json - $Tmp_AWS_ACCESS_KEY_ID = $env:AWS_ACCESS_KEY_ID - $Tmp_AWS_SECRET_ACCESS_KEY = $env:AWS_SECRET_ACCESS_KEY - $Tmp_AWS_SESSION_TOKEN = $env:AWS_SESSION_TOKEN - $env:AWS_ACCESS_KEY_ID = $Creds.Credentials.AccessKeyId - $env:AWS_SECRET_ACCESS_KEY = $Creds.Credentials.SecretAccessKey - $env:AWS_SESSION_TOKEN = $Creds.Credentials.SessionToken - $SecretValueRaw = aws secretsmanager get-secret-value --secret-id "${SecretArn}" --query SecretString --output text + if ($ModPlatformADConfig.ContainsKey("SecretRoleName")) { + $AccountId = aws sts get-caller-identity --query Account --output text + $SecretRoleName = $ModPlatformADConfig.SecretRoleName + $RoleArn = "arn:aws:iam::${AccountId}:role/${SecretRoleName}" + $Session = "ModPlatformADConfig-$env:COMPUTERNAME" + $CredsRaw = aws sts assume-role --role-arn "${RoleArn}" --role-session-name "${Session}" + $Creds = "$CredsRaw" | ConvertFrom-Json + $Tmp_AWS_ACCESS_KEY_ID = $env:AWS_ACCESS_KEY_ID + $Tmp_AWS_SECRET_ACCESS_KEY = $env:AWS_SECRET_ACCESS_KEY + $Tmp_AWS_SESSION_TOKEN = $env:AWS_SESSION_TOKEN + $env:AWS_ACCESS_KEY_ID = $Creds.Credentials.AccessKeyId + $env:AWS_SECRET_ACCESS_KEY = $Creds.Credentials.SecretAccessKey + $env:AWS_SESSION_TOKEN = $Creds.Credentials.SessionToken + $SecretValueRaw = aws secretsmanager get-secret-value --secret-id "${SecretArn}" --query SecretString --output text + $env:AWS_ACCESS_KEY_ID = $Tmp_AWS_ACCESS_KEY_ID + $env:AWS_SECRET_ACCESS_KEY = $Tmp_AWS_SECRET_ACCESS_KEY + $env:AWS_SESSION_TOKEN = $Tmp_AWS_SESSION_TOKEN + } else { + $SecretValueRaw = aws secretsmanager get-secret-value --secret-id "${SecretArn}" --query SecretString --output text + } $SecretValue = "$SecretValueRaw" | ConvertFrom-Json - $env:AWS_ACCESS_KEY_ID = $Tmp_AWS_ACCESS_KEY_ID - $env:AWS_SECRET_ACCESS_KEY = $Tmp_AWS_SECRET_ACCESS_KEY - $env:AWS_SESSION_TOKEN = $Tmp_AWS_SESSION_TOKEN $DomainJoinUsername = $ModPlatformADConfig.DomainJoinUsername $DomainJoinPassword = $SecretValue.$DomainJoinUsername if (-not $DomainJoinPassword) { diff --git a/powershell/README.md b/powershell/README.md index deaab32cc..ccc8fdf72 100644 --- a/powershell/README.md +++ b/powershell/README.md @@ -33,7 +33,7 @@ Get-Help Get-ModPlatformADConfig Put scripts in a `powershell/Scripts` folder. For example, active directory related scripts in `powershell/Scripts/ModPlatformAD` -### Naming +### Naming Be consistent. Pascal case (capitalize the first letter of each word) except keywords and operators which are in lower case. @@ -61,8 +61,10 @@ tasks: Set-Location -Path ([System.IO.Path]::GetTempPath()) $GitBranch = "main" $Script = "ModPlatformAD/Join-ModPlatformAD.ps1" + $ScriptArgs = @{"NewHostname" = "tag:Name"} + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # since powershell 4 uses Tls1 as default Invoke-WebRequest "https://raw.githubusercontent.com/ministryofjustice/modernisation-platform-configuration-management/${GitBranch}/powershell/Scripts/Run-GitScript.ps1" -OutFile "Run-GitScript.ps1" - . ./Run-GitScript.ps1 $Script -GitBranch $GitBranch + . ./Run-GitScript.ps1 $Script -ScriptArgs $ScriptArgs -GitBranch $GitBranch ``` This example downloads the wrapper script and executes it with an example script. diff --git a/powershell/Scripts/Invoke-UserDataScript.ps1 b/powershell/Scripts/Invoke-UserDataScript.ps1 new file mode 100644 index 000000000..427d5198e --- /dev/null +++ b/powershell/Scripts/Invoke-UserDataScript.ps1 @@ -0,0 +1,21 @@ +<# +.SYNOPSIS + Get server-type tag and run associated UserDataScript +#> + +$ErrorActionPreference = "Stop" +$Token = Invoke-RestMethod -TimeoutSec 10 -Headers @{"X-aws-ec2-metadata-token-ttl-seconds"=3600} -Method PUT -Uri http://169.254.169.254/latest/api/token +$InstanceId = Invoke-RestMethod -TimeoutSec 10 -Headers @{"X-aws-ec2-metadata-token" = $Token} -Method GET -Uri http://169.254.169.254/latest/meta-data/instance-id +$TagsRaw = aws ec2 describe-tags --filters "Name=resource-id,Values=$InstanceId" +$Tags = "$TagsRaw" | ConvertFrom-Json +$ServerTypeTag = ($Tags.Tags | Where-Object {$_.Key -eq "server-type"}).Value +$UserDataScript = "${ServerTypeTag}.ps1" + +Set-Location -Path "UserDataScripts" +if (-not $ServerTypeTag) { + Write-Error "Missing or blank server-type tag" +} elseif (-not (Get-ChildItem $UserDataScript -ErrorAction SilentlyContinue)) { + Write-Error "Could not find $UserDataScript" +} else { + . ./$UserDataScript +} diff --git a/powershell/Scripts/ModPlatformAD/Join-ModPlatformAD.ps1 b/powershell/Scripts/ModPlatformAD/Join-ModPlatformAD.ps1 index 53dc7d7cb..fc05d538e 100644 --- a/powershell/Scripts/ModPlatformAD/Join-ModPlatformAD.ps1 +++ b/powershell/Scripts/ModPlatformAD/Join-ModPlatformAD.ps1 @@ -1,10 +1,10 @@ <# .SYNOPSIS - Join computer to appropriate Mod Platform domain + Optionally rename and join computer to appropriate Mod Platform domain .DESCRIPTION - Either pass in the doman name as a parameter, or derive the AD configuration - from EC2 tags (environment-name or domain-name). + By default the script derives the hostname from the Name tag. Or specify NewHostname parameter. + By default derives the AD configuration from EC2 tags (environment-name or domain-name), or specify DomainNameFQDN parameter. EC2 requires permissions to get tags and the aws cli. Exits with 3010 if reboot required and script requires re-running. For use in SSM docs @@ -17,6 +17,7 @@ [CmdletBinding()] param ( + [string]$NewHostname = "tag:Name", [string]$DomainNameFQDN, [string]$AccountIdsSSMParameterName = "account_ids" ) @@ -27,7 +28,9 @@ $ErrorActionPreference = "Stop" $ADConfig = Get-ModPlatformADConfig -DomainNameFQDN $DomainNameFQDN $ADCredential = Get-ModPlatformADCredential -ModPlatformADConfig $ADConfig -AccountIdsSSMParameterName $AccountIdsSSMParameterName -if (Rename-ModPlatformADComputer -NewHostname "tag:Name" -ModPlatformADCredential $ADCredential) { +$Renamed = Rename-ModPlatformADComputer -NewHostname $NewHostname -ModPlatformADCredential $ADCredential +if ($Renamed) { + Write-Output "Renamed computer to ${Renamed}" exit 3010 # triggers reboot if running from SSM Doc } if (Add-ModPlatformADComputer -ModPlatformADConfig $ADConfig -ModPlatformADCredential $ADCredential) { diff --git a/powershell/Scripts/Run-GitScript.ps1 b/powershell/Scripts/Run-GitScript.ps1 index 8974c2ab0..edf6ff4bf 100644 --- a/powershell/Scripts/Run-GitScript.ps1 +++ b/powershell/Scripts/Run-GitScript.ps1 @@ -6,7 +6,11 @@ Clone repo, configure modules and run given powershell script .PARAMETER Script - Relative path of script from Modules/Scripts directory + Optionally provide a script to run. + Specify relative path of script from Modules/Scripts directory + +.PARAMETER ScriptArgs + Optionally provide arguments to the script in hashtable format .PARAMETER GitBranch Git branch to checkout, e.g. main @@ -15,11 +19,12 @@ Optionally specify location to clone repo, otherwise temp dir is used .EXAMPLE - Run-GitScript.ps1 ModPlatformAD/Join-ModPlatformAD + Run-GitScript.ps1 -Script "ModPlatformAD/Join-ModPlatformAD" -ScriptArgs @{"DomainNameFQDN": "azure.noms.root"} #> param ( - [Parameter(Mandatory=$true)][string]$Script, + [string]$Script, + [hashtable]$ScriptArgs, [string]$GitBranch = "main", [string]$GitCloneDir ) @@ -56,4 +61,11 @@ $ModulePath = Join-Path (Join-Path $GitCloneDir $GitRepo) (Join-Path "powershell if (-not $env:PSModulePath.Split(";").Contains($ModulePath)) { $env:PSModulePath = "${env:PSModulePath};${ModulePath}" } -. powershell/Scripts/$Script +if ($Script) { + $RelativeScriptDir = Split-Path -Parent $Script + $ScriptFilename = Split-Path -Leaf $Script + Set-Location -Path "powershell/Scripts/$RelativeScriptDir" + . ./$ScriptFilename @ScriptArgs +} else { + Set-Location -Path powershell/Scripts +} diff --git a/powershell/Scripts/UserDataScripts/HmppsDomainServicesTest.ps1 b/powershell/Scripts/UserDataScripts/HmppsDomainServicesTest.ps1 new file mode 100644 index 000000000..a77110465 --- /dev/null +++ b/powershell/Scripts/UserDataScripts/HmppsDomainServicesTest.ps1 @@ -0,0 +1 @@ +. ../ModPlatformAD/Join-ModPlatformAD.ps1