Using PowerShell to Pause CenturyLink Cloud Virtual Servers

Summary

The following script will pause all servers in the specified group for the account alias ‘SXSW’. You can discover your Group ID by querying an existing server (see example towards the end of this blog post). Pausing your Virtual Servers in the CenturyLink Cloud merely saves their state (memory and disk) and can easily be resumed by Starting them (unlike shutting them down or turning them off which does not save the state of their memory). Pausing servers is similar to ‘sleep’ mode supported by most computers. This can be useful for reducing your CLC costs when virtual servers are not being used. This script reads the necessary BearerToken credentials from a text file (bearerToken.txt) that is created by the following script (keep in mind that this token has a life of only 2 weeks and will then need to be regenerated):

Note: All of these script samples are using PowerShell v4 (required)

Example: Save-BearerToken.ps1
if (!($psversiontable.PSVersion.Major -ge 4)) {
 Write-Host -ForegroundColor Red "Requires PowerShell v.4 or greater. Please install the Windows Management Framework 4 or above."
 exit
}

$psCreds = Get-Credential -Message "Use CLC Web Portal Credentials"
$creds = @{
 username = $psCreds.username
 password = $psCreds.GetNetworkCredential().Password
}
$creds = $creds | ConvertTo-Json

Write-Host -ForegroundColor "green" "Getting authentication 'bearerToken'..."
$logonUri = "https://api.ctl.io/v2/authentication/login"
$logonResponse = Invoke-RestMethod -Method Post -Uri $logonUri -Body $creds -ContentType "application/json"

Write-Host -ForegroundColor "green" "Account Summary:"
$logonResponse

Write-Host -ForegroundColor "green" "Bearer Token (2 week TTL):"
$logonResponse.bearerToken
$logonResponse.bearerToken | Set-Content "bearerToken.txt"
Example: Pause-Servers.ps1
$bearerTokenInput = Get-Content ".\bearerToken.txt"
$groupId = "7c3a1aee32241223a1aee32241979a28"
$accountAlias = "SXSW"
$bearerToken = " Bearer " + $bearerTokenInput
$header = @{}
$header["Authorization"] = $bearerToken

# Discover all servers in the specified group
$requestUri = "https://api.ctl.io/v2/groups/$accountAlias/$groupId"
$groupOutput = Invoke-RestMethod -Method GET -Headers $header -Uri $requestUri -ContentType "application/json" -SessionVariable "theSession"

$serverArray = @()
$serverPause = @()
foreach ($item in $groupOutput.links) {
  if ($item.rel -eq "server") {
    $serverName = $item.href.split("/")[($item.href.split("/").count - 1)]
    $serverArray = $serverArray + ($serverName)
  }
}

# Gather server status
$serverList = ""
foreach ($serverName in $serverArray) {
  Write-Host -ForegroundColor Green "Getting server properties for $serverName..."
  $requestUri = "https://api.ctl.io/v2/servers/$accountAlias/$serverName"
  $serverProperties = Invoke-RestMethod -Method GET -Headers $header -Uri $requestUri -ContentType "application/json" -SessionVariable "theSession"
  $powerState = $serverProperties.details.powerState
  Write-Host -ForegroundColor Green "  PowerState: $powerState"
  if ($serverProperties.details.powerState -eq "started") {
    Write-Host -ForegroundColor Yellow "  Pausing $serverName"
    $serverPause = $serverPause + ($serverName)
    $serverList = "$serverList, $serverName"
  }
}

$serverList = $serverList.TrimStart(", ")
if ($serverPause.Count -eq 1) {
  $servers = "[ ""$serverPause"" ]"
} else {
  $servers = $serverPause | ConvertTo-Json
}

Write-Host -ForegroundColor Green "Sending Server pause request for $serverList"
Write-Host "servers: $servers"
Write-Host "serverPause: $serverPause"
$requestUri = "https://api.ctl.io/v2/operations/$accountAlias/servers/pause"
$pauseRequest = Invoke-RestMethod -Method POST -Headers $header -Uri $requestUri -Body $servers -ContentType "application/json" -SessionVariable "theSession"

Write-Host "Request Output: $pauseRequest"

Getting Your Group ID From Your Server

The following sample script will retrieve your Group ID from an existing server. You will need to incorporate your specific server name in this script (we use ‘CA3TESTTEST01’).

Example: List-ServerGroup.ps1
if (!($psversiontable.PSVersion.Major -ge 4)) {
  Write-Host -ForegroundColor Red "Requires PowerShell v.4 or greater. Please install the Windows Management Framework 4 or above."
  exit
}
$dataCenter = "CA3"
$serverName = "CA3TESTTEST01"
$psCreds = Get-Credential -Message "Use CLC Web Portal Credentials"
$creds = @{
 username = $psCreds.username
 password = $psCreds.GetNetworkCredential().Password
}
$creds = $creds | ConvertTo-Json

$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"

Write-Host -ForegroundColor "green" "Getting authentication 'bearerToken'..."
$logonUri = "https://api.ctl.io/v2/authentication/login"
$logonResponse = Invoke-RestMethod -Method Post -Headers $headers -Uri $logonUri -Body $creds -ContentType "application/json" -SessionVariable "theSession"

$bearerToken = " Bearer " + $logonResponse.bearerToken
$accountAlias = $logonResponse.accountAlias

$headers.Add("Authorization",$bearerToken)

Write-Host -ForegroundColor "green" "Getting datacenter capabilities for $dataCenter..."
$RequestUri = "https://api.ctl.io/v2/datacenters/$accountAlias/CA3/deploymentCapabilities"
$deployCapabilities = Invoke-RestMethod -Method GET -Headers $headers -Uri $RequestUri -ContentType "application/json" -SessionVariable "theSession"

Write-Host -ForegroundColor "green" "This datacenter supports the following platform templates:"
$deployCapabilities.templates
Output:

CLC Output 3

Extra: Task Scheduler Rule

The following is a rule that can be imported into Task Scheduler to run the above script on a regular basis.

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Date>2015-06-19T10:56:50.2629119</Date>
<Author>CA3SXSWTEST01\Administrator</Author>
</RegistrationInfo>
<Triggers>
<CalendarTrigger>
<StartBoundary>2015-06-19T19:00:00</StartBoundary>
<Enabled>true</Enabled>
<ScheduleByDay>
<DaysInterval>1</DaysInterval>
</ScheduleByDay>
</CalendarTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<UserId>CA3SXSWTEST01\Administrator</UserId>
<LogonType>Password</LogonType>
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>false</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>P3D</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>powershell.exe</Command>
<Arguments>.\Pause-Servers.ps1</Arguments>
<WorkingDirectory>C:\Users\Administrator\Documents\Scripts</WorkingDirectory>
</Exec>
</Actions>
</Task>

Enjoy!

PowerShell and the CenturyLink Cloud API

Summary

CenturyLink has a powerful REST API for automating the provisioning and management of virtual servers in their environment. And since PowerShell v4 now has cmdlets that support interacting with REST APIs, it is an easy way to work with entities in the CenturyLink Cloud. The following post will cover the steps required (and sample code) to use PowerShell to provision a CenturyLink Cloud virtual server. Plain old Internet connectivity is all that’s required between PowerShell and CenturyLink, since their API is on the open Internet. The complete reference to CenturyLink’s Cloud REST API can be found here: https://www.ctl.io/api-docs/v2/

Steps:

  1. Get a free account for CenturyLink Cloud here: https://www.centurylinkcloud.com/
  2. Get your Account Alias
  3. Get your Group ID (Default Group)
  4. Query for the capabilities of your desired datacenter (you can find a list of datacenters from the CenturyLink Cloud Control Portal here: https://control.ctl.io/)
  5. Create your server!

Note: All of these script samples are using PowerShell v4 (required)

Get Your Account Alias

Once you get your CenturyLink account, you set your account alias to an abbreviation between 2 and 4 characters. You can also get your AccountAlias from the REST API logon response. You also need to get the BearerToken from the logon response to use as your authentication mechanism with communicating with the CenturyLink REST API (a bearer token has a life of 2 weeks, so don’t hard code it into your scripts).

Example: LogonResponse.ps1
if (!($psversiontable.PSVersion.Major -ge 4)) {
  Write-Host -ForegroundColor "red" "Requires PowerShell v.4 or greater. Please install the Windows Management Framework 4 or above."
  exit
}
 
$psCreds = Get-Credential -Message "Use CLC Web Portal Credentials"
$creds = @{
 username = $psCreds.username
 password = $psCreds.GetNetworkCredential().Password
}
$creds = $creds | ConvertTo-Json
 
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
 
Write-Host -ForegroundColor "green" "Getting authentication 'bearerToken'..."
$logonUrl = "https://api.ctl.io/v2/authentication/login"
$logonResponse = Invoke-RestMethod -Method Post -Headers $headers -Uri $logonUrl -Body $creds -ContentType "application/json" -SessionVariable "theSession"
 
Write-Host -ForegroundColor "green" "Account Summary:"
$logonResponse
 
Write-Host -ForegroundColor "green" "Bearer Token (2 week TTL):"
$logonResponse.bearerToken
Output:
PS-CLC Output 1

Find the Default Group for Your Server

This example is going to use your account’s datacenter “Default Group” as the destination for your new server, specifically for the datacenter ‘CA3’ (in Calgary, Canada). Any group can be used, but your account will have a “Default Group” for each datacenter your account can access. This group ID is required in order to identify the destination for your new server.

Example:  GetDefaultDatacenter.ps1
$psCreds = Get-Credential -Message "Use CLC Web Portal Credentials"
$creds = @{
 username = $psCreds.username
 password = $psCreds.GetNetworkCredential().Password
}
$creds = $creds | ConvertTo-Json

$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"

# Write-Host -ForegroundColor "green" "Getting authentication 'bearerToken'..."
$logonUrl = "https://api.ctl.io/v2/authentication/login"
$logonResponse = Invoke-RestMethod -Method Post -Headers $headers -Uri $logonUrl -Body $creds -ContentType "application/json" -SessionVariable "theSession"

$dataCenter = "CA3"
$bearerToken = " Bearer " + $logonResponse.bearerToken
$accountAlias = $logonResponse.accountAlias

$headers.Add("Authorization",$bearerToken)

Write-Host -ForegroundColor Green "Getting datacenter groups for $dataCenter..."
$requestURL = "https://api.ctl.io/v2/datacenters/$accountAlias/CA3?groupLinks=true"
$datacenterLinks = Invoke-RestMethod -Method GET -Headers $headers -Uri $requestURL -ContentType "application/json" -SessionVariable "theSession"
Write-Host "Available links:"
Write-Host $datacenterLinks.links

foreach ($link in $datacenterLinks.links) {
  if ($link.rel -eq "group") {
    Write-Host -ForegroundColor Green "Getting default datacenter group for group $($link.id)..."
    $requestURL = "https://api.ctl.io/v2/groups/$accountAlias/$($link.id)"
    $datacenterGroups = Invoke-RestMethod -Method GET -Headers $headers -Uri $requestURL -ContentType "application/json" -SessionVariable "theSession"
    foreach ($group in $datacenterGroups.Groups) {
      if ($group.type -eq "default") {
        Write-Host "Found group:"
        Write-Host $group
        Write-Host "`n"
        Write-Host -ForegroundColor Green "Default Group for $($dataCenter): $($group.id)"
      }
    }
  }
}
Output:

PS-CLC Output 2

Query for the Capabilities of Your Desired Datacenter

You can also get a list of available datacenters through the API with the following URI: https://api.ctl.io/v2/datacenters/yourAccountAlias. For this example we’re going to query CA3 (Canada – Toronto). As of this writing, the list includes: CA1, CA2, CA3, DE1, GB1, GB3, IL1, NY1, SG1, UC1, UT1, VA1, WA1. You will want to select the desired platform for your virtual server from the datacenter’s available templates as well as the ID of your destination network. If you don’t have any networks yet, the easiest way to create on is to first deploy a virtual server through the Control Portal web site. This can also be created through the API, but for the sake of brevity, we’ll use a network that already exists.

Partial Example (using the results from logonResponse.ps1): Query the Datacenter
$bearerToken = " Bearer " + $logonResponse.bearerToken
$accountAlias = $logonResponse.accountAlias
 
$headers.Add("Authorization",$bearerToken)
 
Write-Host -ForegroundColor "green" "Getting datacenter capabilities for $dataCenter..."
$RequestURL = "https://api.ctl.io/v2/datacenters/$accountAlias/CA3/deploymentCapabilities"
$deployCapabilities = Invoke-RestMethod -Method GET -Headers $headers -Uri $RequestURL -ContentType "application/json" -SessionVariable "theSession"
 
Write-Host -ForegroundColor "green" "This datacenter supports the following platform templates:"
$deployCapabilities.templates
 
Write-Host -ForegroundColor "green" "This datacenter supports the following networks:"
$deployCapabilities.deployableNetworks
Output

CLC Output 2

Finally, Create Your Server

After gathering the destination group (default group for this example), provisioning template (sourceServerId) and the destination network, you can use the following script to create your virtual server in the CenturyLink Cloud.

Note: There is a character limitation for the ‘name’ field (6 I believe but this example uses a 4 character name).

Example: CreateServer.ps1
$server = @{
  name = "test"
  description = "My test server"
  groupId = "7c39b8a1aee32241979a282241979a28"
  sourceServerId = "WIN2012R2DTC-64"
  isManagedOS = "false"
  networkId = "7J3aCCm0GGF5ev4cmh6U58279b6f056a"
  password = "SomePassword!"
  cpu = 2
  memoryGB = 4
  type = "standard"
  storageType = "standard"
}
$server = $server | ConvertTo-Json
 
$psCreds = Get-Credential -Message "Use CLC Web Portal Credentials"
$creds = @{
 username = $psCreds.username
 password = $psCreds.GetNetworkCredential().Password
}
$creds = $creds | ConvertTo-Json
 
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
 
Write-Host -ForegroundColor "green" "Getting authentication 'bearerToken'..."
$logonUrl = "https://api.ctl.io/v2/authentication/login"
$logonResponse = Invoke-RestMethod -Method Post -Headers $headers -Uri $logonUrl -Body $creds -ContentType "application/json" -SessionVariable "theSession"
 
$bearerToken = " Bearer " + $logonResponse.bearerToken
$accountAlias = $logonResponse.accountAlias
 
$headers.Add("Authorization",$bearerToken)
 
Write-Host -ForegroundColor "green" "Sending Server build request..."
$requestURL = "https://api.ctl.io/v2/servers/$accountAlias"
$buildRequest = Invoke-RestMethod -Method POST -Headers $headers -Uri $requestURL -Body $server -ContentType "application/json" -SessionVariable "theSession"
 
Write-Host -ForegroundColor "green" "Request Output:"
Write-Host -ForegroundColor "green" "Server Name:"
$buildRequest.server
Write-Host -ForegroundColor "green" "Is Queued?"
$buildRequest.isQueued
Write-Host -ForegroundColor "green" "Queue Status Links:"
$buildRequest.links
Output

PS-CLC Output 4

Enjoy!

Chef and PowerShell DSC Overview

Summary

PowerShell DSC is a very valuable extension to Chef Cookbooks for configuring and provisioning Windows Nodes for the following reasons:

  • It can be assumed that DSC Resources will be updated sooner, more stable, and more powerful than the equivalent Windows specific Chef Resources. Primarily because many of the DSC Resources are developed by Microsoft, for Microsoft platforms and applications.
  • Base DSC Resources (e.g. ‘file’, ‘registry’, ‘environment’, ‘script’, ‘feature’) are available by default to Chef Recipes for Nodes that have the Windows Management Framework v.4 (WMF 4) installed.
  • DSC includes a wealth of extension Resources that can simplify and stabilize a Chef recipe when installing or configuring Windows platform components.
  • Chef makes it easier to leverage DSC Resources than a traditional PowerShell DSC infrastructure for the following reasons:
    • No DSC server is required
    • DSC extension Resources don’t require implementation on the server (packaging and checksums need to be manually created for each extension Resource)
    • The Node doesn’t need to be configured for as a DSC Node (WMF v.4+ is still required on the Node)
    • DSC Resources can be executed on the fly by Chef-Client on the Node (rather than waiting for the COM objects to get exercised by the DSC Scheduled Tasks which can lead to inconsistent results when invoked manually)

Considerations:

  • Chef-Client v.12+ is required on the Node
  • WMF v.4+ is required on the Node
  • Using the Chef Resource ‘dsc_resource’ requires WMF v.5+ (this analysis is focusing on the Chef Resource ‘dsc_script’). WMF 5 won’t RTM until Windows 10, but will be available downlevel to Windows Server 2008 and beta versions are currently available down to Windows Server 2008 R2
  • Using DSC extension Resources requires DSC extension Resource management within the cookbook (e.g. by using the Chef Resource ‘remote_diretory’ in order to make sure DSC extension Resources are available on the Node when called by the Chef Recipe) where this is handled automatically be DSC Server infrastructure.

Chef Resource and DSC Resource Comparison

The following comparison is not meant to argue the benefit of using DSC Resources over Chef Resources, rather these examples are meant to demonstrate the difference in code complexity when performing Windows platform actions from Chef vs. the PowerShell DSC server. Considering whether or not to use DSC Resources might also include weighing the decision to develop recipes that are platform dependent since there are no plans for DSC Resources to include platforms other than Windows. In addition, these examples don’t include any Chef Resources other than the ‘powershell_script’ Resource. There are many Windows specific Chef resources that can also simplify Chef Recipes.

The following examples demonstrate enabling remote access via RDP to the Node.

Example: Leveraging the Chef ‘powershell_script’ Resource
# Enables remote desktop access to the server
powershell_script "enable-remote-desktop" do
  guard_interpreter :powershell_script
  code <<-EOH
    Set-ItemProperty -Path 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server' -name "fDenyTSConnections" -Value 0
    Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
    Set-ItemProperty -Path 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp' -name "UserAuthentication" -Value 1
    eventcreate /t INFORMATION /ID 2 /L APPLICATION /SO "Chef-Client" /D "Enabled Remote Desktop access"
  EOH
  only_if <<-EOH
    if ((Get-ItemProperty -Path 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server').fDenyTSConnections -ne "0") {
      $true
    } elseif ((Get-NetFirewallRule -Name "RemoteDesktop-UserMode-In-TCP").Enabled -ne "True") {
      $true
    } elseif ((Get-ItemProperty -Path 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp').UserAuthentication -ne "1") {
      $true
    } else {
      $false
    }
  EOH
end
Example: PowerShell DSC Script (Recipe) Using a DSC Resource
# Enables remote desktop access to the server via an experimental resource xRemoteDesktopAdmin
xRemoteDesktopAdmin EnableRDP
{
  Ensure = "Present"
  UserAuthentication = "Secure"
}
 
xFirewall AllowRDP
{
    Name = 'DSC - Remote Desktop Admin Connections'
    DisplayGroup = "Remote Desktop"
    Ensure = 'Present'
    State = 'Enabled'
    Access = 'Allow'
    Profile = ("Domain", "Private", "Public")
}
Example: Chef Recipe Using a DSC Resource from ‘dsc_script’
dsc_script "Configure Server" do
  imports 'xRemoteDesktopAdmin'
  imports 'xNetworking'
  code <<-SITESDSC
    # Enables remote desktop access to the server via an experimental resource xRemoteDesktopAdmin
    xRemoteDesktopAdmin EnableRemoteDesktop
    {
      Ensure = "Present"
      UserAuthentication = "Secure"
    }
 
    xFirewall AllowRDP
    {
        Name = 'DSC - Remote Desktop Admin Connections'
        DisplayGroup = "Remote Desktop"
        Ensure = 'Present'
        State = 'Enabled'
        Access = 'Allow'
        Profile = ("Domain", "Private", "Public")
    }
  SITESDSC
end
Example: Adding DSC Resource Extension Management in Chef Recipes

The following Chef Recipe snipit demonstrates how 2 DSC extension Resources are managed on the Node. The ‘remote_directory’ merely insures that the ‘xRemoteDesktopAdmin’ and ‘xNetworking’ resources are installed on the Node so they can be leveraged by the Chef Recipe.

remote_directory "C:\\Program Files\\WindowsPowerShell\\Modules\\xRemoteDesktopAdmin" do
  source "xRemoteDesktopAdmin"
  action :create
end
 
remote_directory "C:\\Program Files\\WindowsPowerShell\\Modules\\xNetworking" do
  source "xNetworking"
  action :create
end
Appendix: Full Chef Recipe ‘dsc_script.rb’

The following recipe performs actions on a Windows Server that fall under the topic of general server configuration.

# Requires Chef-Client v.12+
# Author: Otto Helweg
 
include_recipe 'ws2012r2::lcm_setup_dsc_script'
 
remote_directory "C:\\Program Files\\WindowsPowerShell\\Modules\\xRemoteDesktopAdmin" do
  source "xRemoteDesktopAdmin"
  action :create
end
 
remote_directory "C:\\Program Files\\WindowsPowerShell\\Modules\\xNetworking" do
  source "xNetworking"
  action :create
end
 
dsc_script "Configure Server" do
  imports 'xRemoteDesktopAdmin'
  imports 'xNetworking'
  code <<-SITESDSC
    # Leaves a timestamp indicating the last time this recipe was run on the node (this resource in intentionally not idempotent)
    Environment LeaveTimestamp
    {
      Ensure = "Present"
      Name = "DSCClientRun"
      Value = "Last PowerShell DSC run (UTC): " + (Get-Date).ToUniversalTime().ToString()
    }
 
    # Enables remote desktop access to the server via an experimental resource xRemoteDesktopAdmin
    xRemoteDesktopAdmin EnableRemoteDesktop
    {
      Ensure = "Present"
      UserAuthentication = "Secure"
    }
 
    xFirewall AllowRDP
    {
        Name = 'DSC - Remote Desktop Admin Connections'
        DisplayGroup = "Remote Desktop"
        Ensure = 'Present'
        State = 'Enabled'
        Access = 'Allow'
        Profile = ("Domain", "Private", "Public")
    }
 
    # Disables checking for updates
    Script DisableUpdates
    {
      SetScript = {
        $WUSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings
        $WUSettings.NotificationLevel = 1
        $WUSettings.save()
        eventcreate /t INFORMATION /ID 3 /L APPLICATION /SO "DSC-Client" /D "Disabled Checking for Updates"
      }
      TestScript = {
        $WUSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings
        if ($WUSettings.NotificationLevel -ne "1") {
          $false
        } else {
          $true
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    # Verifies Windows Remote Management is Configured or Configures it
    Script EnableWinrm
    {
      SetScript = {
        Set-WSManQuickConfig -Force -SkipNetworkProfileCheck
        eventcreate /t INFORMATION /ID 4 /L APPLICATION /SO "DSC-Client" /D "Enabled Windows Remote Management"
      }
      TestScript = {
        try{
          # Use to remove a listener for testing
          # Remove-WSManInstance winrm/config/Listener -selectorset @{Address="*";Transport="http"}
          Get-WsmanInstance winrm/config/listener -selectorset @{Address="*";Transport="http"}
          return $true
        } catch {
          #$wsmanOutput = "WinRM doesn't seem to be configured or enabled."
          return $false
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    # Installs the Applicaiton-Server Role
    Script InstallAppServer-LogEvent
    {
      SetScript = {
        eventcreate /t INFORMATION /ID 6 /L APPLICATION /SO "DSC-Client" /D "Installed Role: Applicaiton-Server"
      }
      TestScript = {
        if ((Get-WindowsFeature -Name Application-Server).Installed) {
          $true
        } else {
          $false
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    WindowsFeature InstallAppServer-Step1
    {
      Name = "Application-Server"
      Ensure = "Present"
      IncludeAllSubFeature = $true
    }
 
    WindowsFeature InstallAppServer-Step2
    {
      Name = "AS-Web-Support"
      Ensure = "Present"
      IncludeAllSubFeature = $true
      DependsOn = "[WindowsFeature]InstallAppServer-Step1"
    }
 
    # Disables Shutdown tracking (asking for a reason for shutting down the server)
    Script DisableShutdownTracking-LogEvent
    {
      SetScript = {
        eventcreate /t INFORMATION /ID 7 /L APPLICATION /SO "DSC-Client" /D "Disabled Shutdown Tracking"
      }
      TestScript = {
        if ((Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Reliability').ShutdownReasonOn -ne "0") {
          $false
        } else {
          $true
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    Registry DisableShutdownTracking
    {
      Ensure = "Present"
      Key = "HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Reliability"
      ValueName = "ShutdownReasonOn"
      ValueData = "0"
      ValueType = "Dword"
      Force = $true
    }
 
    # Disables automatic maintenance
    Script DisableAutomaticMaintenance
    {
      SetScript = {
        $taskList = @("Idle Maintenance","Maintenance Configurator","Manual Maintenance","Regular Maintenance")
        $schTasks = Get-ScheduledTask
        foreach ($task in $schTasks) {
          if (($task.TaskPath -eq "\\Microsoft\\Windows\\TaskScheduler\\") -and ($taskList.Contains($task.TaskName))) {
            Unregister-ScheduledTask -TaskName $task.TaskName -TaskPath "\\Microsoft\\Windows\\TaskScheduler\\" -Confirm:$false
          }
        }
        eventcreate /t INFORMATION /ID 8 /L APPLICATION /SO "DSC-Client" /D "Disables automatic maintenance"
      }
      TestScript = {
        $taskList = @("Idle Maintenance","Maintenance Configurator","Manual Maintenance","Regular Maintenance")
        $schTasks = Get-ScheduledTask
        foreach ($task in $taskList) {
          if ($schTasks.TaskName.Contains($task)) {
            $found = $true
          }
        }
        if ($found = $true) {
          $false
        } else {
          $true
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
  SITESDSC
end

Enjoy!

PowerShell DSC Script Example – Node Configuration

The following PowerShell DSC script was converted from a Chef recipe for Windows located here. It’s easy to see that there’s a lot of similarity to the logic that PowerShell DSC and Chef use regarding their resources and provisioning or configuring as node (server). PowerShell DSC adds an additional feature in their Script Resource called ‘GetScript’ which handles gathering information from the Node (not used in this example) for use elsewhere in the Script Resource (must be passed back as a hash). In addition, idempotency is managed by ‘TestScipt’ passing back either $true or $false, where $false will cause ‘SetScript’ to execute. Lastly, all variables must be defined within a Script Resource (most commonly a script block) and not outside of the resource, otherwise they will be evaluated and stored at the time of the MOF file generation.

The following script demonstrates some of the server configuration functionality via PowerShell’s new DSC service, and performs the following actions:

  1. Timestamp: A Windows Event is created and an environmental variable is updated to show the last time this recipe was executed on the server.
  2. Enable Remote Desktop: The Remote Desktop (RDP) functionality is enabled.
  3. Disable Windows Updates: Windows automatic updates are disabled (use with caution – unless you have another update process, don’t do this).
  4. Enable Windows Remote Management: The Windows Remote Management functionality is enabled.
  5. Install IIS: The Application Server and Web Server roles are installed and enabled.
  6. Disable Server Reboot Tracking: A minor registry key is added/updated to stop the default practice of asking for a reboot reason on Windows Server.

DSC Publishing Steps:

The following steps are taken to publish a MOF file that’s ready to be consumed by nodes:

  1. PowerShell DSC script is written (below) and executed.
  2. In the current working directory (important!) a MOF file is created, representing the resources called out in the DSC script.
  3. The MOF file is named after the ‘Node’, in a sub-directory named after the ‘Configuration’ call (e.g. in this case it would be named ‘.\ServerConfig\Anybody.mof’)
    The MOF file is copied to the PowerShell DSC server’s hosting directory.
  4. A sister CheckSum file is generated for the newly created MOF file.
  5. Nodes will the download the MOF file via a REST call, specifying a specific MOF file by GUID (a node can only be configured to download a single MOF file). This will be covered in another blog.

DSC Script:

Configuration ServerConfig
{
  Node Anybody
  {
    # Leaves a timestamp indicating the last time this script was run on the node (this resource is intentionally not idempotent)
    Script LeaveTimestamp
    {
      SetScript = {
        $currentTime = Get-Date
        $currentTimeString = $currentTime.ToUniversalTime().ToString()
        [Environment]::SetEnvironmentVariable("DSCClientRun","Last DSC-Client run (UTC): $currentTimeString","Machine")
        eventcreate /t INFORMATION /ID 1 /L APPLICATION /SO "DSC-Client" /D "Last DSC-Client run (UTC): $currentTimeString"
      }
      TestScript = {
        $false
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    # Enables remote desktop access to the server
    Registry EnableRDP-Step1
    {
      Ensure = "Present"
      Key = "HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Terminal Server"
      ValueName = "fDenyTSConnections"
      ValueData = "0"
      ValueType = "Dword"
      Force = $true
    }
 
    Registry EnableRDP-Step2
    {
      Ensure = "Present"
      Key = "HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp"
      ValueName = "UserAuthentication"
      ValueData = "1"
      ValueType = "Dword"
      Force = $true
    }
 
    Script EnableRDP
    {
      SetScript = {
        Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
        eventcreate /t INFORMATION /ID 2 /L APPLICATION /SO "DSC-Client" /D "Enabled Remote Desktop access"
      }
      TestScript = {
        if ((Get-NetFirewallRule -Name "RemoteDesktop-UserMode-In-TCP").Enabled -ne "True") {
          $false
        } else {
          $true
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    # Disables checking for updates
    Script DisableUpdates
    {
      SetScript = {
        $WUSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings
        $WUSettings.NotificationLevel = 1
        $WUSettings.save()
        eventcreate /t INFORMATION /ID 3 /L APPLICATION /SO "DSC-Client" /D "Disabled Checking for Updates"
      }
      TestScript = {
        $WUSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings
        if ($WUSettings.NotificationLevel -ne "1") {
          $true
        } else {
          $false
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    # Verifies Windows Remote Management is Configured or Configures it
    Script EnableWinrm
    {
      SetScript = {
        Set-WSManQuickConfig -Force -SkipNetworkProfileCheck
        eventcreate /t INFORMATION /ID 4 /L APPLICATION /SO "DSC-Client" /D "Enabled Windows Remote Management"
      }
      TestScript = {
        try{
          # Use to remove a listener for testing
          # Remove-WSManInstance winrm/config/Listener -selectorset @{Address="*";Transport="http"}
          Get-WsmanInstance winrm/config/listener -selectorset @{Address="*";Transport="http"}
          return $true
        } catch {
          #$wsmanOutput = "WinRM doesn't seem to be configured or enabled."
          return $false
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    # Installs the Applicaiton-Server Role
    Script InstallAppServer-LogEvent
    {
      SetScript = {
        eventcreate /t INFORMATION /ID 6 /L APPLICATION /SO "DSC-Client" /D "Installed Role: Applicaiton-Server"
      }
      TestScript = {
        if ((Get-WindowsFeature -Name Application-Server).Installed) {
          $true
        } else {
          $false
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    WindowsFeature InstallAppServer-Step1
    {
      Name = "Application-Server"
      Ensure = "Present"
      IncludeAllSubFeature = $true
    }
 
    WindowsFeature InstallAppServer-Step2
    {
      Name = "AS-Web-Support"
      Ensure = "Present"
      IncludeAllSubFeature = $true
      DependsOn = "[WindowsFeature]InstallAppServer-Step1"
    }
 
    # Disables Shutdown tracking (asking for a reason for shutting down the server)
    Script DisableShutdownTracking-LogEvent
    {
      SetScript = {
        eventcreate /t INFORMATION /ID 7 /L APPLICATION /SO "DSC-Client" /D "Disabled Shutdown Tracking"
      }
      TestScript = {
        if ((Get-ItemProperty -Path 'HKLM:\\SOFTWARE\Policies\Microsoft\Windows NT\Reliability').ShutdownReasonOn -ne "0") {
          $false
        } else {
          $true
        }
      }
      GetScript = {
        # Do Nothing
      }
    }
 
    Registry DisableShutdownTracking
    {
      Ensure = "Present"
      Key = "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT\Reliability"
      ValueName = "ShutdownReasonOn"
      ValueData = "0"
      ValueType = "Dword"
      Force = $true
    }
  }
}
 
# The MOF filename must be a GUID. It can be any unique GUID and can be generated by the following PowerShell command ([guid]::NewGuid()).
$guid = "45b51dc8-132c-4052-8e3b-479c73d4c9cc"
 
# Create the MOF file from the above PowerShell DSC script
ServerConfig
 
# Used to copy the newly generated MOF file in the Pull Server's publishing location
$mofFile = "c:\dscscripts\ServerConfig\Anybody.mof"
$mofPath = "C:\Program Files\WindowsPowerShell\DscService\Configuration"
$DSCMofFile = $mofPath + "\" + $guid + ".mof"
 
Copy-Item $mofFile -Destination $DSCMofFile
# Generate a CheckSum sister file for the MOF file

 New-DSCCheckSum $DSCMofFile -Force
After the DSC script is executed on the node, New Windows Events should be logged to the configured node (server):

DSC-Example-1

And the following web page should be viewable on the node (server):

DSC-Example-2

The following practices are leveraged in this recipe:
  • A time stamp is logged on the server in order to track every time the script is executed via both a Windows Event and a Machine Environmental Variable (the environmental variable doesn’t refresh until you log out and log back in – is CMD getting cached somewhere?). This might be helpful for troubleshooting issues by correlating with the recipe execution timestamp.
  • A Windows Event is logged every time a resource is executed on the node (server). Event IDs are unique to the resource function.
  • All resources (except the timestamp resource) are idempotent. In other words, logic is in place to insure the resource is not executed if it doesn’t need to be. For example, if IIS has already been installed, then there should be no attempt to install it again. This is a ‘best practice’ for writing DSC scripts.
The following PowerShell functionality is exercised by this recipe:
  • An Environmental Variable is created/updated
  • Events are created with the ‘eventcreate’ command. The PowerShell ‘New-Event’ is not used because for Windows Server (not client) you need to register your custom event source with the server, so ‘eventcreate’ is simpler.
  • A Registry Key is added/updated
  • The Enable-FirewallRule cmdlet is used
  • A COM object is accessed and/or updated
  • The Set-WsmanQuickConfig cmdlet is used
  • The Get-WsmanInstance cmdlet is used
  • The Add-WindowsFeature cmdlet is used

Idempotency:

Notice that the Script Resource implements PowerShell script logic (via ‘TestScript’) in order to make the resource follow idempotent best practices (most of the other resources manage this for you). The logic implemented in the Script Resource is a reasonable attempt to determine if action is necessary, while not covering every possible permutation that might exist for a misconfigured server. Also notice how either $true or $false is passed back to the resource from the test logic. And finally, the ‘SetScript’ block is not executed unless ‘$false’ is returned by the ‘TestScript’ logic.

Enjoy!

Configuring Windows with Chef and the PowerShell Resource

The PowerShell Resource in Chef is very powerful and versatile for configuring Windows. It essentially exposes all of the power of PowerShell to Chef. And since in PowerShell v.4, most Server Manager functionality is exposed via PowerShell cmdlets, there isn’t a lot you can’t configure in Windows with PowerShell.

The following recipe demonstrates some of the server configuration functionality via Chef’s PowerShell resource by performing the following actions:

  1. Timestamp: A Windows Event is created and an environmental variable is updated to show the last time this recipe was executed on the server.
  2. Enable Remote Desktop: The Remote Desktop (RDP) functionality is enabled.
  3. Disable Windows Updates: Windows automatic updates are disabled (use with caution – unless you have another update process, don’t do this).
  4. Enable Windows Remote Management: The Windows Remote Management functionality is enabled.
  5. Install IIS: The Application Server and Web Server roles are installed and enabled.

Chef Recipe:

# Leaves a timestamp indicating the last time this recipe was run on the node (this resource in intentionally not idempotent)
powershell_script "leave-timestamp" do
  code <<-EOH
    $currentTime = Get-Date
    $currentTimeString = $currentTime.ToUniversalTime().ToString()
    [Environment]::SetEnvironmentVariable("ChefClientRun","Last Chef-Client run (UTC): $currentTimeString","Machine")
    eventcreate /t INFORMATION /ID 1 /L APPLICATION /SO "Chef-Client" /D "Last Chef-Client run (UTC): $currentTimeString"
  EOH
end
 
# Enables remote desktop access to the server
powershell_script "enable-remote-desktop" do
  guard_interpreter :powershell_script
  code <<-EOH
    Set-ItemProperty -Path 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server' -name "fDenyTSConnections" -Value 0
    Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
    Set-ItemProperty -Path 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp' -name "UserAuthentication" -Value 1
    eventcreate /t INFORMATION /ID 2 /L APPLICATION /SO "Chef-Client" /D "Enabled Remote Desktop access"
  EOH
  only_if <<-EOH
    if ((Get-ItemProperty -Path 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server').fDenyTSConnections -ne "0") {
      $true
    } elseif ((Get-NetFirewallRule -Name "RemoteDesktop-UserMode-In-TCP").Enabled -ne "True") {
      $true
    } elseif ((Get-ItemProperty -Path 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp').UserAuthentication -ne "1") {
      $true
    } else {
      $false
    }
  EOH
end
 
# Disables checking for updates
powershell_script "disable-update-checking" do
  guard_interpreter :powershell_script
  code <<-EOH
    $WUSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings
    $WUSettings.NotificationLevel = 1
    $WUSettings.save()
    eventcreate /t INFORMATION /ID 3 /L APPLICATION /SO "Chef-Client" /D "Disabled Checking for Updates"
  EOH
  only_if <<-EOH
    $WUSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings
    if ($WUSettings.NotificationLevel -ne "1") {
      $true
    } else {
      $false
    }
  EOH
end
 
# Verifies Windows Remote Management is Configured or Configures it
powershell_script "check-winrm" do
  guard_interpreter :powershell_script
  code <<-EOH
    Set-WSManQuickConfig -Force -SkipNetworkProfileCheck
    eventcreate /t INFORMATION /ID 4 /L APPLICATION /SO "Chef-Client" /D "Enabled Windows Remote Management"
  EOH
  not_if <<-EOH
    # $wsmanOutput isn't used here, just a placeholder for possible use in the future
    try {
      # Use to remove a listener for testing
      # Remove-WSManInstance winrm/config/Listener -selectorset @{Address="*";Transport="http"
      $wsmanOutput = Get-WsmanInstance winrm/config/listener -selectorset @{Address="*";Transport="http"}
      $true
    } catch {
      $wsmanOutput = "WinRM doesn't seem to be configured or enabled."
      $false
    }
  EOH
end
 
# Installs the Application-Server Role
powershell_script "install-application-server" do
  guard_interpreter :powershell_script
  code <<-EOH
    $installResult = Add-WindowsFeature Application-Server,AS-Web-Support
    foreach ($result in $installResult) {
      if ($result.RestartNeeded -eq "Yes") {
        eventcreate /t INFORMATION /ID 5 /L APPLICATION /SO "Chef-Client" /D "Restart is required."
      }
    }
    eventcreate /t INFORMATION /ID 6 /L APPLICATION /SO "Chef-Client" /D "Installed Role: Applicaiton-Server"
  EOH
  not_if <<-EOH
    if ((Get-WindowsFeature -Name Application-Server).Installed) {
      $true
    } else {
      $false
    }
  EOH
 end
Your output should look something like this (although I ran this with Test-Kitchen rather than ‘chef-client’ directly):

Note: The check-winrm script did not execute because it was already configured on the node (server).

Chef-PowerShell-1

New Windows Events should be logged to the configured node (server):

Chef-PowerShell-2

And the following web page should be viewable on the node (server):

Chef-PowerShell-3

The following practices are leveraged in this recipe:

  • A time stamp is logged on the server in order to track every time the recipe is executed. This might be helpful for troubleshooting issues by correlating with the recipe execution timestamp.
  • A Windows Event is logged every time a resource is executed on the server. Event IDs are unique to the resource function.
  • All resources (except the timestamp resource) are idempotent. In other words, logic is in place to insure the resource is not executed if it doesn’t need to be. For example, if IIS has already been installed, then there should be no attempt to install it again. This is a ‘best practice’ for writing recipes.

The following PowerShell functionality is exercised by this recipe:

  • An Environmental Variable is created/updated
  • Events are created with the ‘eventcreate’ command. The PowerShell ‘New-Event’ is not used because for Windows Server (not client) you need to register your custom event source with the server, so ‘eventcreate’ is simpler.
  • A Registry Key is added/updated
  • The Enable-FirewallRule cmdlet is used
  •  A COM object is accessed and/or updated
  • The Set-WsmanQuickConfig cmdlet is used
  • The Get-WsmanInstance cmdlet is used
  • The Add-WindowsFeature cmdlet is used

Idempotency:

Notice that nearly all the resources implement PowerShell script logic in order to make their resources follow idempotent best practices. The logic implemented is a reasonable attempt to determine if action is necessary, while not covering every possible permutation that might exist for a misconfigured server. Also notice how either $true or $false is passed back to the resource. Both conditions need to be addressed by the Guard. Also notice where ‘not_if’ and ‘only_if’ are used.

Enjoy!

Trigger a PowerShell Script from a Windows Event

Note: Portions of this blog are taken from an old blog post titled “Reference the Event That Triggered Your Task”

This example will demonstrate both how to trigger ﴾launch﴿ a PowerShell script from a specific Windows Event, AND pass parameters to the PowerShell script from the Windows Event that triggered the script. For the purpose of this example, a test event will be generated using the built‐in EventCreate command‐line utility.

Background: The scenario behind this example was a need to clean up a file‐share after a specific Windows Event occurred. A specific Windows Event was logged upon the success of a file watermarking process. The event used in this example loosely follows the original event format.

The following steps will be demonstrated:

  1. Manually create the trigger event.
  2. Use Event Viewer to create an event triggered task from the above event.
  3. Modify the task to expose event details to the downstream script.
  4. Implement the PowerShell script to be triggered.
  5. Verify the setup.

Step 1: Create the trigger event using EventCreate ﴾it’s easier to go this route to generate a Scheduled Task for modification rather than trying to create one from scratch﴿. From the command prompt run:

eventcreate /T INFORMATION /SO SomeApplication /ID 1000 /L APPLICATION /D "<Params><Timestamp>2011-08-29T21:24:03Z</Timestamp><InputFile>C:\temp\Some Test File.txt</InputFile><Result>Success</Result></Params>"

Step 2: Use the Event Viewer “Attach Task to This Event…” feature to create the task.

Launch “Event Viewer” and find the event you created in Step 1. It should be located toward the top of the “Windows Logs\Application” Log. Once found, right‐click on the event and select “Attach Task to This Event…” then use the defaults for the first couple screens of the wizard.

PS-Trigger-1

Create a task to “Start a Program” with the following parameters:

  • Program/script: PowerShell.exe
  • Add arguments: .\TriggerScript.ps1 -eventRecordID $(eventRecordID) -eventChannel $(eventChannel)
  • Start in ﴾you might need to create this directory or alter the steps to use a directory of your choice﴿: c:\temp

PS-Trigger-2

Step 3: Modify the task to expose details about the trigger event and pass them to the PowerShell script

From within Task Scheduler, export the newly created task ﴾as an XML file﴿. Right‐click on the task “Application_SomeApplication_1000” in the “Event Viewer Tasks” folder, and select “Export…”.

PS-Trigger-3

Use Notepad ﴾or your text editor of choice ‐keep in mind the text editor must honor unicode which notepad does﴿ to add the Event parameters you which to pass along to your task. The event parameters below are the most useful for event identification. Notice the entire node

<ValueQueries>
     <Value name="eventChannel">Event/System/Channel</Value>
     <Value name="eventRecordID">Event/System/EventRecordID</Value>
     <Value name="eventSeverity">Event/System/Level</Value>
</ValueQueries>

See below:

PS-Trigger-5

From an Elevated Command Prompt, execute the following commands to delete the Trigger Task and recreate it with the newly modified exported Trigger Task ﴾I don’t believe there’s a way to modify an existing task using an updated XML file﴿. From a command prompt, run:

schtasks /delete /TN "Event Viewer Tasks\Application_SomeApplication_1000"
schtasks /create /TN "Event Viewer Tasks\Application_SomeApplication_1000" /XML Application_SomeApplication_1000.xml

Step 4: Implement the PowerShell script to be triggered by creating a script called “TriggerScript.ps1” below

Note: The script below is passed basic information about the event that triggered it. The script then queries the Windows Event Log to get more details about the event (the event payload). For this example, XML is used in the payload to separate the parameters, but any text can be passed as long as the script knows how to parse it. In addition, the “eventRecordID” that’s passed to the script should not be confused with the eventID of the event. The eventRecordID is a sequential number assigned to all events as they are logged to a specific channel. In addition, eventRecordIDs are only unique for a specific Channel (Log).

# Script Name: TriggerScript.ps1
# Usage Example (use a valid ID found via Event Viewer XML view of an event): powershell .\TriggerScript.ps1 eventRecordID 1 eventChannel Application
#
# Create a fake event for testing with the following command (from an elevated command prompt):
# eventcreate /T INFORMATION /SO SomeApplication /ID 1000 /L APPLICATION /D "<Params><Timestamp>20110829T21:24:03Z</Timestamp><InputFile>C:\temp\Some Test File.txt</InputFile><Result>Success</Result></Params>"
# Collects all named paramters (all others end up in $Args)
param($eventRecordID,$eventChannel)
$event = get-winevent -LogName $eventChannel -FilterXPath "<QueryList><Query Id='0' Path='$eventChannel'><Select Path='$eventChannel'>*[System [(EventRecordID=$eventRecordID)]]</Select></Query></QueryList>"
[xml]$eventParams = $event.Message
if ($eventParams.Params.TimeStamp) {
  [datetime]$eventTimestamp = $eventParams.Params.TimeStamp
  $eventFile = $eventParams.Params.InputFile

  $popupObject = new-object -comobject wscript.shell
  $popupObject.popup("RecordID: " + $eventRecordID + ", Channel: " + $eventChannel + ", Event Timestamp: " + $eventTimestamp + ", File: " + $eventFile)
}

Note: Besides executing a script, a task can display a popup directly or send an email. An email can be useful for catching infrequent events on your system or in your environment. And a task can be deployed via Group Policy Preferences.

Step 5: Verify the setup by generating another trigger event as in Step 1

eventcreate /T INFORMATION /SO SomeApplication /ID 1000 /L APPLICATION /D "<Params><Timestamp>2011-08-29T21:24:03Z</Timestamp><InputFile>C:\temp\Some Test File.txt</InputFile><Result>Success</Result></Params>"

You should see the following Popup window appear ﴾it might be hidden behind other Windows﴿:

PS-Trigger-4

Enjoy!

Quick and Dirty Reset of a Hyper-V VM with PowerShell

Note: Remember, this is a “Quick and Dirty” solution. In fact, because credentials are hard‐coded in the script, it is “Quick and Filthy”. But that tends to be the nature of IT‐Pro quick fixes, and I found it useful for a particular scenario. Just remember, this is merely being shown as an example and not a best practice.

Background: I had a problematic VM that would freeze over a period of time which required a “Hard Reset” to make it functional again. I wanted a way to reset the VM remotely, rather than from the console of the Hyper‐V parent.

The following steps will be taken to perform a hard reset on a VM:

  1. List ﴾enumerate﴿ the virtual machines on a Hyper‐V parent and identify the VM to reset by capturing its “Name” ﴾GUID﴿.
  2. Build a credential object that allows the execution of the RequestStateChange method ﴾typically Administrator credentials are required﴿.
  3. Execute the RequestStateChange method

List ﴾Enumerate﴿ the virtual machines on a Hyper‐V parent

PS> winrm enumerate wmi/root/virtualization/msvm_computersystem /r:<Hyper-V Parent> /u:<username> /p:<password>
     Msvm_ComputerSystem
     AssignedNumaNodeList = 0
     Caption = Virtual Machine
     CreationClassName = Msvm_ComputerSystem
     Description = Microsoft Virtual Machine
     ElementName = WSMANR2
     EnabledDefault = 2
     EnabledState = 2
     HealthState = 5
     InstallDate
     Datetime = 20110725T20:31:41Z
     Name = C4E916BB92D54A4097C10664FCC0123B
     NameFormat = null
     OnTimeInMilliseconds = 5774786
     OperationalStatus = 2
     OtherEnabledState = null
     PrimaryOwnerContact = null
     PrimaryOwnerName = null
     ProcessID = 2064
     RequestedState = 12
     ResetCapability = 1
     Status = null
     StatusDescriptions = Operating normally
     TimeOfLastConfigurationChange
          Datetime = 20110822T17:33:46.726979Z
     TimeOfLastStateChange
          Datetime = 20110822T17:33:46Z

Build a credential object ﴾this approach allows for the credentials to be hardcoded into a script, whereas the typical “get‐credential” cmdlet will not allow the password to be stored in the script and enforces the more secure method of manual entry of the password﴿.

PS> $password = convertto-securestring -String "<password>" -asplaintext -force
PS> $credential = new-object -typename System.Management.Automation.PSCredential -argumentlist "<username>",$password

Execute the RequestStateChange method on the VM ﴾acts like a Hard Reset for this example﴿

PS> # Various supported state changes: 2=Turns the VM on, 3=Turns the VM off, 10=A hard reset of the VM, 32768=Pauses the VM, 32769=Saves the state of the VM
PS> invoke-wsmanaction -action RequestStateChange -resourceuri wmi/root/virtualization/msvm_computersystem valueset @{RequestedState="10"} –selectorset @{Name="<VM Name/GUID from Above>"} -computername <HyperV Parent> -authentication default -credential $credential

Enjoy!

Sample PowerShell 2.0 Remoting Commands

The following are a list of commands that I demonstrated at TechEd 2010 in New Orleans. Actually I should say that I intended to demo these commands, but wasn’t able to complete the entire list due to a conference wide network outage. :‐﴾

Many of these commands were intended to run against a real world web server in the Internet ﴾http://wsman.msft.net﴿. For one command ﴾WS‐Man ID﴿, I’ll include the web server in the command syntax. Otherwise I’ll just use “<server name>” to specify the destination. In addition, my demo server is configured to respond to WS‐Management from the original port 80 ﴾rather than the new port 5985 which was changed in WinRM 2.0﴿.

Note: If your client or end points are not Windows7 or Windows Server 2008 R2, then you will need to have PowerShell 2.0 installed on both systems. You can get the bits as Windows updates from http://download.microsoft.com.

Note: All commands are intended to be executed from PowerShell 2.0 or the PowerShell Integrated Scripting Environment (ISE).

Test WS‐Man connectivity without authentication.

This is useful for making sure the network and the Windows Remote Manage service are operational and intentionally does not check credentials since that is usually another level of configuration can be tested on its own.

test-wsman –computername wsman.msft.net:80 –authentication none

Create a credential token to be used throughout the remaining commands.

$cred = get-credential <administrator account name on end point>

Test WS‐Man connectivity with credentials ﴾note version info is now displayed﴿.

As mentioned above, it is helpful to be able to isolate authentication when troubleshooting management connectivity issues.

test-wsman -computername <server name>:<port if other than 5985> -authentication default -credential $cred

Enumerate status for all Services.

This is merely using WS‐Man as the transport for accessing WMI providers. In the past, DCOM was the transport, but had many limitations due to its firewall unfriendly nature.

get-wsmaninstance -enumerate wmicimv2/win32_service -computername <server name>:<port if other than 5985> -authentication default -credential $cred

Enumerate status for IIS Service.

This demonstrates getting the state of a specific service ﴾or element﴿ by using the “selectorset” parameter.

get-wsmaninstance wmicimv2/win32_service -selectorset @{name="w3svc"} -computername <server name>:<port if other than 5985> -authentication default -credential $cred

Stop and Start the IIS Service.

Again, this is merely using WS‐Man as the transport in order to manipulate WMI methods that have been around since the dawn of time.

invoke-wsmanaction -action stopservice -resourceuri wmicimv2/win32_service -selectorset @{name="w3svc"} -computername <server name>:<port if other than 5985> -authentication default -credential $cred

This will verify the state of the stopped service.

get-wsmaninstance wmicimv2/win32_service -selectorset @{name="w3svc"} -computername <server name>:<port if other than 5985> -authentication default -credential $cred

Now restart the IIS service.

invoke-wsmanaction -action startservice -resourceuri wmicimv2/win32_service -selectorset @{name="w3svc"} -computername <server name>:<port if other than 5985> -authentication default -credential $cred

Store Output into an Object.

WMI instrumentation and actions are now very easy to automate with the addition of WS‐Man as a transport for remoting and PowerShell for scripting. The example here shows how the WMI information can be pulled into an object and properly formatted.

$operatingsystem = get-wsmaninstance -enumerate wmicimv2/win32_operatingsystem -computername <server name>:<port if other than 5985> -authentication default -credential $cred

Show the output for the Last Boot Time for the end point.

$operatingsystem.LastBootUpTime

Format this Boot Time data into a proper .Net DateTime object.

[datetime]$operatingsystem.LastBootUpTime.datetime

Query a VM Host

A good deal of Microsoft’s hypervisor’s ﴾Hyper‐V﴿ as well as VMWare’s hypervisor’s instrumentation and management is exposed via WMI. The following command displays characteristics of the Hyper‐V parent as well as all of its children.

get-wsmaninstance -enumerate wmi/root/virtualization/Msvm_computersystem -computername <server name>:<port if other than 5985> -authentication default -credential $cred

Create a persistent connection to a Remote System.

This is using WS‐Man to create a connection to the remote system, not PowerShell. Note that the port is not used in‐line with the “ComputerName”.

Connect-WSMan -computername <server name> -authentication default -credential $cred -port <port if other than 5985>

This will show the configuration of the remote system, including the listener.

cd wsman:

The connection does need to be ended.

disconnect-WSMan -computername wsman.msft.net

Create and use a PowerShell Remoting Session.

Using PowerShell for remote management is much more powerful as it allows for the scripts ﴾or script blocks﴿ to be passed within the connection rather than requiring them to exist on the remote computer.

Note: If the script or script block that is being passed to the remote computer is using any special modules, they will need to exist on the remote computer (modules are note passed with the script).

$wsman = new-pssession -computername <server name> -port <port if other than 5985> -authentication default -credential $cred

This merely shows how to remotely execute a single PowerShell command on a remote machine and that the output is returned as a formatted object with the remote machine’s meta data attached to the results.

invoke-command -session $wsman -scriptblock {getprocess}

We’ll then put the results into an object. Notice now nicely the data in the object is formatted. This is not the case when non‐PowerShell commands are executed remotely ﴾see below﴿.

$output = invoke-command -session $wsman -scriptblock {getprocess}

This shows how to tear down the remote session.

remove-pssession -session $wsman

Create and use a PowerShell Remoting Session on Several Servers

One of the most powerful features of PowerShell remoting is the ability to execute scripts on many servers simultaneously ﴾knows as “fan out”﴿. There is also the ability to throttle the number of servers that are simultaneously running scripts. The example below shows how to specify multiple servers within the command, but there are other ﴾more programmatic﴿ ways of doing this ﴾see “get‐help” for examples﴿.

$several = new-pssession -computername <server  name 1>,<server name 2>,<server name 3> -port <port if other than 5985> -authentication default -credential $cred
invoke-command -session $several -scriptblock {getprocess}
$output = invoke-command -session $several -scriptblock {getprocess}

The following example show how a “fan out” PowerShell command can also be executed in the background and monitored by using the “asjob” flag.

invoke-command -session $several -scriptblock {getprocess} asjob
get-job
receive-job -id <ID # listed from "getjob">

The following example shows how the output is formatted if the executed command is not a PowerShell script or cmdlet.

invoke-command -session $several -scriptblock {ipconfig /all}

When the results are placed in a PowerShell object, the object is essentially an array of single lines of text.

$output = invoke-command -session $several -scriptblock {ipconfig /all}
remove-pssession -session $several

Enter into a PSSession

The following examples show how to remote to a single end point and execute commands ﴾in this case the commands will stop and restart the web service﴿.

$wsman = new-pssession -computername <server name> -port <port if other than 5985> -authentication default -credential $cred
enter-pssession -session $wsman
net stop w3svc
net start w3svc
exit-possession
remove-pssession -session $wsman

Enjoy!

Hyper-V Virtual Identification

Since virtual machines can be easily moved between physical hosts (parents), it becomes important to track where virtual machines are physically residing for both asset management as well as troubleshooting purposes. The following post focuses on discovering the relationship between virtual hosts (parents) and the virtual machines (children) from both the perspective of the parent as well as the perspective of the child.

HyperV-ID-1

Note: The following examples use the WinRM and WinRS command-line utilities which shipped with Windows Vista and Windows Server 2008, but are also available as an Out-Of-Band install for Windows XP SP2+ and Windows Server 2003 SP1+ here.

Query the Parent:

Most commonly used for asset collection, this model gathers the names (and other virtual machine characteristics) of all the children running on a virtual host. This method queries the Hyper-V specific WMI provider/class by using the following command.

winrm enumerate wmi/root/virtualization/msvm_computersystem /r:<remote Hyper-V Host>

With the following sample output:

Msvm_ComputerSystem
     AssignedNumaNodeList = 0
     Caption = Virtual Machine
     CreationClassName = Msvm_ComputerSystem
     Description = Microsoft Virtual Machine
     ElementName = PROV-XP
     EnabledDefault = 2
     EnabledState = 2
     HealthState = 5
     InstallDate
          Datetime = 2008-07-01T21:47:02Z
     Name = 31F497F1-2437-4E89-8308-BE07FB5C14C2
     NameFormat = null
     OnTimeInMilliseconds = 432464839
     OperationalStatus = 2
     OtherEnabledState = null
     PrimaryOwnerContact = null
     PrimaryOwnerName = OTTOH-HOST\Administrator
     ProcessID = 2628
     RequestedState = 12
     ResetCapability = 1
     Status = null
     TimeOfLastConfigurationChange
          Datetime = 2008-07-30T17:07:06Z
     TimeOfLastStateChange
          Datetime = 2008-07-30T17:07:06Z

Query the Child:

Most commonly used for troubleshooting scenarios where a virtual machine is being evaluated and needs to be queried in order to determine its physical host (parent). The following command queries the registry on the child in order to determine its host (parent):

HyperV-ID-2

Remote Access Method #1 (the /f parameter merely structures the output in XML – handy for scripting, especially in PowerShell):

winrm invoke GetStringValue wmi/root/default/StdRegProv @{hDefKey="2147483650";sSubKeyName="Software\Microsoft\Virtual Machine\Guest\Parameters";sValueName="PhysicalHostNameFullyQualified"} /r:<Remote VM> /u:<Username> /p:<Password> /f:pretty

Remote Access Method #2:

winrs /r:<Remote VM> /u:<Username> /p:<Password> reg query "HKLM\Software\Microsoft\Virtual Machine\Guest\Parameters" /v PhysicalHostNameFullyQualified

Note: The first method demonstrates a powerful way to access the value of any registry key using the ‘StdRegProv’ WMI provider via WS-Man/WinRM for remote transport. Other registry hives can be accessed with the following hDefKey values: HKLM=2147483650, HKCU=2147483649, HKCR=2147483648, HKEY_USERS=2147483651.

Enjoy!

Quick and Dirty Large Scale Eventing for Windows

One of the least known yet most powerful management features to ship with Windows Vista and Windows Server 2008 is built‐in Event Forwarding which enables large scale health and state monitoring of a Windows environment ﴾assuming health and state can be determined from Windows Events ‐which they usually can﴿. Not only is this feature built into the latest versions of Windows, but it’s also available for down‐level OSs like Windows XP SP2+ and Windows Server 2003 SP1+ ﴾here﴿.

Note: True enterprise class Windows eventing is included with enterprise monitoring solutions like System Center Operations Manager.

This new Windows Event Forwarding ﴾also known as Windows Eventing 6.0﴿ is exceptional for the following reasons:

  1. Standards Based: No really! It leverages the DMTF WS‐Eventing standard which allows it to interoperate with other WS‐Man implementations ﴾see OpenWSMAN at SourceForge﴿.
  2. Agentless: Event Forwarding and Event Collection are included in the OS by default
  3. Down‐Level Support: Event Forwarding is available for Windows XP SP2+ and Windows Server 2003 SP1+
  4. Multi‐Tier: Forwarding architecture is very scalable where a “Source Computer” may forward to a large number of collectors and collectors may forward to collectors
  5. Scalable: Event Collection is very scalable ﴾available in Windows Vista as well﴿ where the collector can maintain subscriptions with a large number of “Source Computers” as well as process a large number of events per second
  6. Group Policy Aware: The entire model is configurable by Group Policy
  7. Schematized Events: Windows Events are now schematized and rendered in XML which enables many scripting and export scenarios
  8. Pre‐Rendering: Forwarded Windows Events can now be pre‐rendered on the Source Computer negating the need for local applications to render Windows Events
  9. Resiliency: Designed to enable mobile scenarios where laptops may be disconnected from the collector for extended periods of time without event loss ﴾except when logs wrap﴿ as well as leveraging TCP for guaranteed delivery
  10. Security: Certificate based encryption via Kerberos or HTTPS

This implementation will walk through the following example design where via Group Policy a domain computer group will be configured to forwarded Windows Events to a single collector:

Large-Eventing-1

Implementation steps are as follows:

  • Step 1: Create Event Forwarding Subscription
  • Step 2: Configure WinRM Group Policy
  • Step 3: Configure Event Forward Group Policy
  • Step 4: Test

Step 1: Create the Event Forwarding Subscription on the Event Collector

In the Windows Event Forwarding architecture, the subscription definition is held and maintained on the Collector in order to reduce the number of touch‐points in case a subscription needs to be created or modified. Creating the subscription is accomplished through the new Event Viewer user interface by selecting the ‘Create Subscription’ action when the ‘Subscriptions’ branch is highlighted. The Subscription may also be created via the “WECUTIL” command‐line utility.

Note: Both Windows Vista and Windows Server 2008 can be event collectors ﴾this feature is not supported for down‐level﴿. Although there are no built‐in limitations when Vista is a collector, Server 2008 will scale much better in high volume scenarios.

Large-Eventing-2

Although the above subscription is configured to leverage Group Policy, the subscription can be configured in a stand‐alone mode ﴾see the “Collector Initiated” option﴿. In addition, this subscription is designed to gather all events from the “Application” and “System” logs that have a level of “Critical”, “Error”, or “Warning”. This event scope can be expanded to gather all events from these logs or even add additional logs ﴾like the “Security” log﴿.

Lastly, the subscription is configured to forward events as quickly as possible with the advanced settings delivery option of “Minimize Latency”. The default setting of “Normal” would only forward events every 15 minutes ﴾which may be more desirable depending the the Collector and Source Computer resources﴿.

Large-Eventing-3

If Group Policy is not being used, configure the “Subscription type” to be “Collector Initiated”. In this case Source Computers will need to be manually added to the Subscription either through the Subscription configuration or the “WECUTIL” command‐line utility ﴾which can also be scripted using PowerShell, but that’s another topic﴿.

Note: In cases where there Source Computer is generating a large volume of forwarded events ﴾e.g. Security events from a Domain Controller﴿, use WECUTIL on the collector to disable event rendering Quick and Dirty Large Scale Eventing for Windows Otto for the subscription. The task of pre‐rendering an event on the source computer can be CPU intensive for a large number of events.

Step 2: Configure Group Policy to enable Windows Remote Management on the Source Computers ﴾clients﴿

Group Policy can be used to enable and configure Windows Remote Management ﴾WinRM or WSMan﴿ on the Source Computers. WinRM is required by Windows Event Forwarding as WS‐Man is the protocol used by WS‐Eventing. The following shows the Group Policy branch locations for configuring both WinRM and Event Forwarding:

Large-Eventing-4

The following GP setting will enable WinRM on the client as well as configure a Listener that will accept packets from ANY source.

Large-Eventing-5

Note: This Listener configuration should only be used in a trusted network environment. If the environment is not trusted ﴾like the Internet﴿, then configure only specific IP Addresses or ranges in the IPv4 and IPv6 filters.

To configure WinRM outside of Group Policy, run the following command on the Source Computer ﴾also see the above Note﴿:

winrm quickconfig

Step 3: Configure Group Policy to enable Windows Event Forwarding on the Source Computers

As with WinRM, Group Policy can be used to configure Source Computers ﴾Clients﴿ to forward events to a collector ﴾or set of collectors﴿. The policy is very simple. It merely tells the Source Computer to contact a specific FQDN ﴾Fully Qualified Domain Name﴿ or IP Address and request subscription specifics. All of the other subscription details are held on the Collector.

Large-Eventing-6

If Group Policy is not being used, then there is nothing to do here as the “Collector Initiated” Subscription will proactively reach out to the Source Computer.

Step 4: Test Event Forwarding

If all of the Event Forwarding components are functioning ﴾and there’s minimal network latency﴿, a test event created on the Source Computer should arrive in the Collector’s “Forwarded Events” log within 60 seconds. Create a test event with the following command:

eventcreate /id 999 /t error /l application /d "Test event."

Large-Eventing-7

This event should appear on the Collector as follows:

Large-Eventing-8

If the event doesn’t appear, perform the following troubleshooting steps:

Troubleshooting Step 1: Has Policy Been Applied to the Source Computer?

This can be forced by running the following command on the Source Computer:

gpupdate /force
Troubleshooting Step 2: Can the Collector Reach The Source Computer via WinRM?

Run the following command on the Collector

winrm id /r:<Source Computer> /a:none
Troubleshooting Step 3: Is the Collector Using the Right Credentials?

Run the following command on the Collector

winrm id /r:<Source Computer> /u:<username> /p:<password>

Note: These are the credentials defined in the Subscription on the Collector. The credentials don’t need to be in the local Administrators group on the Source Computer, they just need to be in the “Event Log Readers” group on the Source Computer ﴾local Administrators will also work﴿.

Troubleshooting Step 4: Has the Source Computer Registered with the Collector?

Run the following command on the Collector

wecutil gr <subscription name>

This will list all the registered Source Computers ﴾note if the Subscription is “Collector Initiated” then this will list all configured Source Computers﴿, their state ﴾from the Collector’s perspective﴿, and their last heartbeat time.

Enjoy!