Skip to content

Instantly share code, notes, and snippets.

@grenade
Last active August 29, 2015 14:27
Show Gist options
  • Select an option

  • Save grenade/2cfb23deaf2331b3535f to your computer and use it in GitHub Desktop.

Select an option

Save grenade/2cfb23deaf2331b3535f to your computer and use it in GitHub Desktop.
<powershell><#
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
.Synopsis
Bootstrap an AWS EC2 instance with cloud-tools (https://github.com/mozilla/build-cloud-tools)
.Description
This script will do the following when run:
- log output to the puppet mailing list, to enable debugging
- check and/or correct the hostname if required
- check and/or correct the primary dns suffix
- check and/or correct the nxlog aggregator configuration
- ensure only a single puppet run on the golden instance
- handle reboots and shutdowns (eg: after hostname changes or puppet runs)
.Notes
All parameters have sensible defaults and only need to be provided when running outside of the cloud-tools environment (eg: testing).
If running outside of cloud tools, ensure that you provide sensible values for: hostname, domain & aggregator.
Double angle brackets used in this script are intentional and used to escape the bracket when python pre-processing replaces single bracket tokens.
.Parameter hostname
Defines the hostname of the target instance.
When run by cloud-tools, the hostname token will be replaced before the script is uploaded to the target instance.
Defaults to: '{{hostname}}', if not defined.
.Parameter domain
Defines the primary DNS suffix of the target instance.
When run by cloud-tools, the domain token will be replaced before the script is uploaded to the target instance.
Defaults to: '{{domain}}', if not defined.
.Parameter puppetServer
Defines the hostname of the puppet server.
When run by cloud-tools, the puppet_server token will be replaced before the script is uploaded to the target instance.
Defaults to: '{{puppet_server}}', if not defined.
.Parameter aggregator
Defines the hostname of the log aggregation server.
When run by cloud-tools, the region_dns_atom token will be replaced before the script is uploaded to the target instance.
Defaults to: 'log-aggregator.srv.releng.{{region_dns_atom}}.mozilla.com', if not defined.
.Parameter logDir
Defines the local directory on the target host where log files will be generated and kept.
The directory will be created if it does not exist.
Defaults to: 'C:\log', if not defined.
.Parameter smtpServer
Defines the smtp server that will relay log messages.
Defaults to: 'smtp.mail.scl3.mozilla.com', if not defined.
.Parameter logSender
Defines the email sender (from address) for log reports generated by this script.
Defaults to an address comprised of the username from environment variable and the hostname and domain arguments mentioned above.
.Parameter logRecipients
Defines the email recipient list for log reports generated by this script.
Accepts a string, if only one recipient, or a string array, if more than one.
Defaults to: '[email protected]', if not defined.
.Example
PS>.\userscript.ps1 -hostname b-2008-ec2-something -domain build.releng.use1.mozilla.com -aggregator log-aggregator.srv.releng.use1.mozilla.com
#>
param (
# accept tokens from cloud-tools (change tokens to actual values if running outside cloud-tools)
[string] $hostname = '{hostname}',
[string] $domain = '{domain}',
#[string] $puppetServer = '{{puppet_server}}',
[string] $aggregator = 'log-aggregator.srv.releng.{region_dns_atom}.mozilla.com',
# runtime configuration
[string] $logDir = ('{{0}}\log' -f $env:SystemDrive),
[string] $smtpServer = 'smtp.mail.scl3.mozilla.com',
[string] $logSender = ('{{0}}@{{1}}.{{2}}' -f $env:USERNAME, $hostname, $domain),
[string[]] $logRecipients = @('[email protected]')
)
# runtime configuration
$runLog = [IO.Path]::Combine($logDir, 'userdata-run.log')
$puppetLog = [IO.Path]::Combine($logDir, 'puppet-agent-run.log')
$ec2Log = ('{{0}}\Amazon\Ec2ConfigService\Logs\Ec2ConfigLog.txt' -f $env:ProgramFiles)
$smtpServer = $smtpServer
function Download-Module {{
<#
.Synopsis
Downloads a psm1 module file to the local modules folder
.Description
The local module will use the psm1 filename, minus extension, as the module name
.Parameter url
The full url to the userdata module.
.Parameter modulesPath
The full path to the local modules folder.
#>
param (
[string] $url,
[string] $modulesPath = ('{{0}}\Modules' -f $pshome)
)
$filename = $url.Substring($url.LastIndexOf('/') + 1)
$moduleName = [IO.Path]::GetFileNameWithoutExtension($filename)
New-Item -ItemType Directory -Force -Path ('{{0}}\{{1}}' -f $modulesPath, $moduleName)
(New-Object Net.WebClient).DownloadFile($url, ('{{0}}\{{1}}\{{2}}' -f $modulesPath, $moduleName, $filename))
}}
# download and import the Ec2UserdataUtils module
# todo: swap hardcoded puppet url when puppet_server token is being correctly populated (spot and golden)
#Download-Module -url ('http://{{0}}/repos/powershell/Ec2UserdataUtils.psm1' -f $puppetServer)
Download-Module -url 'http://releng-puppet2.srv.releng.scl3.mozilla.com/repos/powershell/Ec2UserdataUtils.psm1'
Import-Module Ec2UserdataUtils
# create the log directory if it doesn't exist
New-Item -ItemType Directory -Force -Path $logDir
Disable-PuppetService
$hostnameCorrect = (Is-HostnameSetCorrectly -hostnameExpected $hostname)
$domainCorrect = (Is-DomainSetCorrectly -domainExpected $domain)
if ($hostnameCorrect -and $domainCorrect) {{
Write-Log -message ('correct hostname ({{0}}) detected' -f $hostname) -severity 'INFO'
Write-Log -message ('correct domain ({{0}}) detected' -f $domain) -severity 'INFO'
if (($hostname.Contains("-spot-")) -or (Has-PuppetRunSuccessfully -puppetLog $puppetLog)) {{
Write-Log -message 'skipping puppet agent run, prior run detected' -severity 'INFO'
if (Is-AggregatorConfiguredCorrectly -aggregator "$aggregator") {{
Write-Log -message 'correct log aggregator config detected' -severity 'INFO'
}} else {{
Set-Aggregator -aggregator "$aggregator"
}}
}} else {{
if (Test-Path $puppetLog) {{
Write-Log -message 'incomplete puppet run detected' -severity 'INFO'
}}
# todo: swap hardcoded puppet url when puppet_server token is being correctly populated (golden)
#Run-Puppet -hostname $hostname -domain $domain -puppetServer $puppetServer -logdest $puppetLog
Run-Puppet -hostname $hostname -domain $domain -logdest $puppetLog
Send-Log -logfile $puppetLog -subject ('Puppet Agent Report for {{0}}.{{1}}' -f $hostname, $domain) -to $logRecipients -from $logSender
Stop-ComputerWithDelay -reason 'userdata golden puppet run complete'
}}
}} else {{
if ($hostnameCorrect -ne $true) {{ Set-Hostname -hostname $hostname }}
if ($domainCorrect -ne $true) {{ Set-Domain -domain $domain }}
Stop-ComputerWithDelay -reason 'host name changed' -restart
}}
Enable-UserdataPersist
Send-Log -logfile $runLog -subject ('UserData Run Report for {{0}}.{{1}}' -f $hostname, $domain) -to $logRecipients -from $logSender
Move-Item -path $runLog -destination ([IO.Path]::Combine($logDir, ('userdata-run-{{0}}.log' -f [DateTime]::Now.ToString("yyyyMMdd-HHmm"))))
Send-Log -logfile $ec2Log -subject ('EC2 Config Report for {{0}}.{{1}}' -f $hostname, $domain) -to $logRecipients -from $logSender
Move-Item -path $ec2Log -destination ([IO.Path]::Combine($logDir, ('ec2config-run-{{0}}.log' -f [DateTime]::Now.ToString("yyyyMMdd-HHmm"))))
</powershell>
<persist>true</persist>
<#
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
.Synopsis
Utility functions for bootstraping an AWS EC2 instance with cloud-tools (https://github.com/mozilla/build-cloud-tools)
#>
function Write-Log {
<#
.Synopsis
Logs to the userdata run log file, with timestamps.
.Parameter message
The body of the log message
.Parameter severity
The severity of the message, to enable filtering in log aggregators or reporting.
.Parameter path
The full path to the log file.
#>
param (
[string] $message,
[string] $severity = 'INFO',
[string] $path = ('{0}\log\userdata-run.log' -f $env:SystemDrive)
)
if (Test-Path $path) {
Add-Content -Path $path ('{0} [{1}] {2}' -f [DateTime]::Now.ToString("yyyy-MM-dd HH:mm:ss"), $severity, $message)
} else {
switch ($severity)
{
'DEBUG' { Write-Host ('{0} [{1}] {2}' -f [DateTime]::Now.ToString("yyyy-MM-dd HH:mm:ss"), $severity, $message) -ForegroundColor 'DarkGray' }
'ERROR' { Write-Host ('{0} [{1}] {2}' -f [DateTime]::Now.ToString("yyyy-MM-dd HH:mm:ss"), $severity, $message) -ForegroundColor 'Red' }
default { Write-Host ('{0} [{1}] {2}' -f [DateTime]::Now.ToString("yyyy-MM-dd HH:mm:ss"), $severity, $message) }
}
}
}
function Send-Log {
<#
.Synopsis
Mails the specified logfile to the configured recipient(s)
.Parameter logfile
The full path to the log file to be mailed.
.Parameter subject
The subject line of the message.
.Parameter to
The recipient(s) of the message.
.Parameter from
The sender of the message.
.Parameter smtpServer
The smtp server that relays log messages.
#>
param (
[string] $logfile,
[string] $subject,
[string] $to,
[string] $from = ('{0}@{1}.{2}' -f $env:USERNAME, $env:COMPUTERNAME, $env:USERDOMAIN),
[string] $smtpServer = 'smtp.mail.scl3.mozilla.com'
)
Send-MailMessage -To $to -Subject $subject -Body ([IO.File]::ReadAllText($logfile)) -SmtpServer $smtpServer -From $from
}
function Enable-UserdataPersist {
<#
.Synopsis
Sets Ec2ConfigService Ec2HandleUserData to enabled in config.
.Description
Modifies Ec2ConfigService config file and logs settings at time of check.
.Parameter ec2SettingsFile
The full path to the config file for Ec2ConfigService.
#>
param (
[string] $ec2SettingsFile = "C:\Program Files\Amazon\Ec2ConfigService\Settings\Config.xml"
)
$modified = $false;
[xml]$xml = (Get-Content $ec2SettingsFile)
foreach ($plugin in $xml.DocumentElement.Plugins.Plugin) {
Write-Log -message ('plugin state of {0} read as: {1}, in: {2}' -f $plugin.Name, $plugin.State, $ec2SettingsFile) -severity 'DEBUG'
if ($plugin.Name -eq "Ec2HandleUserData") {
if ($plugin.State -ne "Enabled") {
Write-Log -message ('changing state of Ec2HandleUserData plugin from: {0} to: Enabled, in: {1}' -f $plugin.State, $ec2SettingsFile) -severity 'INFO'
$plugin.State = "Enabled"
$modified = $true;
}
}
}
if ($modified) {
Write-Log -message ('granting full access to: System, on: {0}' -f $ec2SettingsFile) -severity 'DEBUG'
$icaclsArgs = @($ec2SettingsFile, '/grant', 'System:F')
& 'icacls' $icaclsArgs
$xml.Save($ec2SettingsFile)
}
Write-Log -message ('granting read access to: Everyone, on: {0}' -f $ec2SettingsFile) -severity 'DEBUG'
$icaclsArgs = @($ec2SettingsFile, '/grant', 'Everyone:R')
& 'icacls' $icaclsArgs
Write-Log -message ('removing all inherited permissions on: {0}' -f $ec2SettingsFile) -severity 'DEBUG'
$icaclsArgs = @($ec2SettingsFile, '/inheritance:r')
& 'icacls' $icaclsArgs
Write-Log -message ('removing access for: root, on: {0}' -f $ec2SettingsFile) -severity 'DEBUG'
$icaclsArgs = @($ec2SettingsFile, '/remove:g', 'root')
& 'icacls' $icaclsArgs
Write-Log -message ('removing access for: Administrators, on: {0}' -f $ec2SettingsFile) -severity 'DEBUG'
$icaclsArgs = @($ec2SettingsFile, '/remove:g', 'Administrators')
& 'icacls' $icaclsArgs
Write-Log -message ('removing access for: Users, on: {0}' -f $ec2SettingsFile) -severity 'DEBUG'
$icaclsArgs = @($ec2SettingsFile, '/remove:g', 'Users')
& 'icacls' $icaclsArgs
Write-Log -message ('removing access for: System, on: {0}' -f $ec2SettingsFile) -severity 'DEBUG'
$icaclsArgs = @($ec2SettingsFile, '/remove:g', 'System')
& 'icacls' $icaclsArgs
}
function Stop-ComputerWithDelay {
<#
.Synopsis
Shuts down the computer and optionally restarts, logging a reason to the event log.
.Parameter reason
The reason for the shutdown or reboot.
.Parameter delayInSeconds
The time delay in seconds before shutting down
.Parameter restart
Whether or not to restart after shutdown
#>
param (
[string] $reason,
[int] $delayInSeconds = 10,
[switch] $restart
)
Write-Log -message ('shutting down with reason: {0}' -f $reason) -severity 'INFO'
if ($restart) {
$stopArgs = @('-r', '-t', $delayInSeconds, '-c', $reason, '-f', '-d', 'p:4:1')
} else {
$stopArgs = @('-s', '-t', $delayInSeconds, '-c', $reason, '-f', '-d', 'p:4:1')
}
& 'shutdown' $stopArgs
}
function Does-FileContain {
<#
.Synopsis
Determine if a file contains the specified string
.Parameter needle
The string to search for.
.Parameter haystack
The full path to the file to be checked.
#>
param (
[string] $haystack,
[string] $needle
)
if (((Get-Content $haystack) | % { $_ -Match "$needle" }) -Contains $true) {
return $true
} else {
return $false
}
}
function Has-PuppetRunSuccessfully {
<#
.Synopsis
Determine if a successful puppet run has completed
.Parameter puppetLog
The full path to the puppet log file.
#>
param (
[string] $puppetLog
)
if ((Test-Path $puppetLog) -and (Does-FileContain -haystack $puppetLog -needle "Puppet (notice): Finished catalog run")) {
return $true
} else {
return $false
}
}
function Disable-Service {
<#
.Synopsis
Stops and disables a windows service
.Parameter serviceName
the name of the service to be disabled
#>
param (
[string] $serviceName
)
Write-Log -message ('stopping and disabling service: {0}' -f $serviceName) -severity 'INFO'
Get-Service $serviceName | Stop-Service -PassThru | Set-Service -StartupType disabled
}
function Disable-WindowsUpdate {
<#
.Synopsis
Stops and disables the windows update service
#>
$autoUpdateSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings
if ($autoUpdateSettings.NotificationLevel -ne 1) {
Write-Log -message 'disabling Windows Update notifications' -severity 'INFO'
$autoUpdateSettings.NotificationLevel=1
$autoUpdateSettings.Save()
} else {
Write-Log -message 'detected disabled Windows Update notifications' -severity 'DEBUG'
}
Disable-Service -serviceName 'wuauserv'
}
function Disable-PuppetService {
<#
.Synopsis
Stops and disables the puppet service and deletes the RunPuppet scheduled task
#>
Disable-Service -serviceName 'puppet'
Write-Log -message 'deleting RunPuppet scheduled task' -severity 'INFO'
$schtasksArgs = @('/delete', '/tn', 'RunPuppet', '/f')
& 'schtasks' $schtasksArgs
}
function Run-Puppet {
<#
.Synopsis
Runs the puppet agent
.Description
Runs the puppetization vbscript
Runs the puppet agent in cli mode, logging to an output file
Deletes the RunPuppet scheduled task
.Parameter hostname
The hostname of the instance, required for facter env vars.
.Parameter domain
The domain of the instance, required for facter env vars.
#>
param (
[string] $hostname,
[string] $domain,
[string] $puppetServer = 'puppet',
[string] $logdest
)
Write-Log -message 'setting environment variables' -severity 'INFO'
[Environment]::SetEnvironmentVariable("FACTER_domain", "$domain", "Process")
[Environment]::SetEnvironmentVariable("FACTER_hostname", "$hostname", "Process")
[Environment]::SetEnvironmentVariable("FACTER_fqdn", ("$hostname.$domain"), "Process")
[Environment]::SetEnvironmentVariable("COMPUTERNAME", "$hostname", "Machine")
[Environment]::SetEnvironmentVariable("USERDOMAIN", "$domain", "Machine")
Write-Log -message 'running puppetization script' -severity 'INFO'
#todo: log and mail output from vbs script
cscript.exe ('{0}\Puppetlabs\puppet\var\puppettize_TEMP.vbs' -f $env:ProgramData)
Write-Log -message ('running puppet agent, logging to: {0}' -f $logdest) -severity 'INFO'
$puppetArgs = @('agent', '--test', '--detailed-exitcodes', '--server', $puppetServer, '--logdest', $logdest)
& 'puppet' $puppetArgs
Write-Log -message 'deleting RunPuppet scheduled task (again)' -severity 'INFO'
$schtasksArgs = @('/delete', '/tn', 'RunPuppet', '/f')
& 'schtasks' $schtasksArgs
}
function Is-HostnameSetCorrectly {
<#
.Synopsis
Determines if the hostname is correctly set
.Parameter hostnameExpected
The expected hostname of the instance.
#>
param (
[string] $hostnameExpected
)
$hostnameActual = [System.Net.Dns]::GetHostName()
if ("$hostnameExpected" -ieq "$hostnameActual") {
return $true
} else {
Write-Log -message ('net dns hostname: {0}, expected: {1}' -f $hostnameActual, $hostnameExpected) -severity 'DEBUG'
Write-Log -message ('computer name env var: {0}, expected: {1}' -f $env:COMPUTERNAME, $hostnameExpected) -severity 'DEBUG'
return $false
}
}
function Set-Hostname {
<#
.Synopsis
Sets the hostname
.Description
- Sets the COMPUTERNAME environment variable at the machine level
- Renames the computer
- Adds the new hostname to the sysprep file, to prevent sysprep from reverting the hostname on reboot
.Parameter hostname
The required new hostname of the instance.
#>
param (
[string] $hostname
)
[Environment]::SetEnvironmentVariable("COMPUTERNAME", "$hostname", "Machine")
(Get-WmiObject Win32_ComputerSystem).Rename("$hostname")
Write-Log -message ('hostname set to: {0}' -f $hostname) -severity 'INFO'
$sysprepFile = ('{0}\Amazon\Ec2ConfigService\sysprep2008.xml' -f $env:ProgramFiles)
[xml] $xml = Get-Content($sysprepFile)
foreach ($settings in $xml.DocumentElement.settings) {
if ($settings.pass -eq "specialize") {
foreach ($component in $settings.component) {
if ($component.name -eq "Microsoft-Windows-Shell-Setup") {
if (-not $component.ComputerName) {
$computerNameElement = $xml.CreateElement("ComputerName")
$computerNameElement.AppendChild($xml.CreateTextNode("$hostname"))
$component.AppendChild($computerNameElement)
Write-Log -message ('computer name inserted to: {0}' -f $sysprepFile) -severity 'DEBUG'
} else {
$component.ComputerName.value = "$hostname"
Write-Log -message ('computer name updated in: {0}' -f $sysprepFile) -severity 'DEBUG'
}
}
}
}
}
$xml.Save($sysprepFile)
}
function Is-DomainSetCorrectly {
<#
.Synopsis
Determines if the primary dns suffix is correctly set
.Parameter domainExpected
The expected primary dns suffix of the instance.
#>
param (
[string] $domainExpected
)
$primaryDnsSuffix = (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\" -Name "NV Domain")."NV Domain"
if ("$domainExpected" -ieq "$primaryDnsSuffix") {
return $true
} else {
Write-Log -message ('nv domain registry key: {0}, expected: {1}' -f $primaryDnsSuffix, $domainExpected) -severity 'DEBUG'
return $false
}
}
function Set-Domain {
<#
.Synopsis
Set the primary DNS suffix (for FQDN)
.Parameter domain
The required new primary DNS suffix of the instance.
#>
param (
[string] $domain
)
[Environment]::SetEnvironmentVariable("USERDOMAIN", "$domain", "Machine")
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\' -Name 'Domain' -Value "$domain"
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\' -Name 'NV Domain' -Value "$domain"
Write-Log -message ('Primary DNS suffix set to: {0}' -f $domain) -severity 'INFO'
}
# determines if the log aggregator is correctly set
function Is-AggregatorConfiguredCorrectly {
<#
.Synopsis
Determines if the log aggregator is correctly set
.Parameter aggregator
The fqdn of the log aggregator for the current aws region.
#>
param (
[string] $aggregator
)
$conf = ('{0}\nxlog\conf\nxlog_target_aggregator.conf' -f ${env:ProgramFiles(x86)})
if ((Test-Path $conf) -and (Does-FileContain -haystack $conf -needle $aggregator)) {
return $true
} else {
return $false
}
}
function Set-Aggregator {
<#
.Synopsis
Sets the fqdn of the log aggregator for the current aws region.
.Description
Modifies the nxlog configuration file to point to the specified log aggregator and restarts the nxlog service.
.Parameter aggregator
The fqdn of the log aggregator for the current aws region.
#>
param (
[string] $aggregator
)
$conf = ('{0}\nxlog\conf\nxlog_target_aggregator.conf' -f ${env:ProgramFiles(x86)})
if (Test-Path $conf) {
(Get-Content $conf) |
Foreach-Object { $_ -replace "(Host [^ ]*)", "Host $aggregator" } |
Set-Content $conf
Restart-Service nxlog
Write-Log -message "log aggregator set to: $aggregator" -severity 'INFO'
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment