Friday, May 1, 2015

Change iDRAC DNS Name Dynamically

Today I'd like to share a quick script I threw together to dynamically update the iDRAC DNS Name using the Hostname reported to the iDRAC from OMSA or iDRAC Service Module installed on the host operating system.

Here's the scenario:  We have a customer running running Ubuntu server, but OpenManage Server Administrator (OMSA) and OpenManage Essentials (OME) aren't validated with Ubuntu.  While there is a Debian port of OMSA, a critical service for system updates (dsm_sa_shrsvc part of srvadmin-cm package) is not available.  We have also found that if we discover both the host and the iDRAC that they correlate, but system updates are not displayed due to the missing inventory.

The decision was made to go completely out-of-band, but the iDRAC DNS names were all configured with the default idrac-SVCTAG convention.  This makes it very difficult to tell which iDRAC belongs to which server.  The solution?  Let's change it to <hostname>-iDRAC instead!

Since I've been experimenting with manipulating the iDRAC and Lifecycle Controller via PowerShell using WS-Man for a while now, I decided I would put the necessary pieces together to create a fast and streamlined method to: 

1. Query the iDRAC for the OS hostname and make sure it's not null (updated by OMSA services)
2. Query the iDRAC for current iDRAC hostname
3. Make sure the current iDRAC hostname is default convention
4. Change the hostname to <hostname>-iDRAC
5. Commit the change
6. Pull the new iDRAC hostname after the configuration change

I already have a couple helper functions I've written up (and actually threw in a .psm1 and installed them as cmdlets on my system) that will help out with this.

The first one is "New-iDRACSession":

Function New-iDRACSession {
param (
[Parameter (Mandatory = $true)][string] $racUser,
[Parameter (Mandatory = $true)][string] $racPass,
[Parameter (Mandatory = $true)][string] $racIP
)

# WSMAN Cmdlet Session
$secPass = ConvertTo-SecureString "$racPass" -AsPlainText -Force
$credentials = New-Object -typename System.Management.Automation.PSCredential -argumentlist ("$racUser", $secPass)
$wsmanOptions = New-WSManSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$wsmanArgs = @{Authentication="Basic";ConnectionURI="https://$racIP/wsman";SessionOption=$wsmanOptions;Credential=$credentials}

# Test Session
$SystemView = Get-WSManInstance -Enumerate @wsmanArgs -ResourceURI "http://schemas.dell.com/wbem/wscim/1/cim-schema/2/DCIM_SystemView"
if ($SystemView.Manufacturer -ne $null) {
$wsmanArgs
}
else {
throw "Unable to connect to the iDRAC, check iDRAC IP and/or credentials and try again."
}
}

This function allows you to create a session that you can use with the other helper function that we'll use to query the iDRAC.  You'll notice that this function is basically a wrapper to create a credential for the PowerShell wsman cmdlets, though this does validate that you can connect to the iDRAC before it returns the credential object.

The next helper function is "Get-iDRACClass":

Function Get-iDRACClass {
param (
[Parameter (Mandatory = $true)][hashtable] $Session,
[Parameter (Mandatory = $true)][string] $Class,
[Parameter (Mandatory = $false)][string] $Instance
)
if ($Instance -eq [String]::Empty) {
Get-WSManInstance @Session -Enumerate -ResourceURI "http://schemas.dell.com/wbem/wscim/1/cim-schema/2/$Class"
}
if ($Instance -ne [String]::Empty) {
$InstanceID = @{InstanceID="$Instance"}
Get-WSManInstance @Session -ResourceURI "http://schemas.dell.com/wbem/wscim/1/cim-schema/2/$ClassName" -SelectorSet $InstanceID
}
}

This function uses the session you created (and saved to a variable) to query iDRAC Classes with just their name, and not the entire resource URI/URL.

Now that we have our helper functions, we can write the function that will actually orchestrate all of the work:

Function Set-DNSRacNameFromHostname {
param (
[Parameter (Mandatory = $true)]$racIP,
[Parameter (Mandatory = $true)]$racUser,
[Parameter (Mandatory = $true)]$racPass
)
# Create session and enum classes
$idrac = New-iDRACSession -racUser $racUser -racPass $racPass -racIP $racIP
$idraccardview = Get-iDRACClass -Session $idrac -Class DCIM_iDRACCardView
$systemview = Get-iDRACClass -Session $idrac -Class DCIM_SystemView
if ($idraccardview.DNSRacName -like "idrac-*" -and $systemview.Hostname -ne "") {
$iDRACCardService = "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/DCIM_iDRACCardService"
$selectFilter = @{SystemCreationClassName="DCIM_ComputerSystem";CreationClassName="DCIM_iDRACCardService";SystemName="DCIM:ComputerSystem";Name="DCIM:iDRACCardService"}
$hostname = @{Target="iDRAC.Embedded.1";AttributeName="NIC.1#DNSRacName";AttributeValue="$($systemview.hostname)-iDRAC"}
$commitSet = @{Target="iDRAC.Embedded.1";ScheduledStartTime="TIME_NOW"}
Invoke-WSManAction @idrac -ResourceURI $iDRACCardService -SelectorSet $selectFilter -Action SetAttribute -ValueSet $hostname
Invoke-WSManAction @idrac -ResourceURI $iDRACCardService -SelectorSet $selectFilter -Action CreateTargetedConfigJob -ValueSet $commitSet
}
elseif ($systemview.Hostname -eq "") {
Write-Host "There is no reported hostname... aborting!"
#throw "There is no reported hostname... aborting!"
}
elseif ($idraccardview.DNSRacName -notlike "idrac-*") {
Write-Host "The iDRAC has been changed from `"idrac-SVCTAG`" naming convention... aborting!"
#throw "The iDRAC has been changed from `"idrac-SVCTAG`" naming convention... aborting!"
}
else {
throw "Unhandled exception"
}
sleep 1
Write-Host "New DNSRacName set to $((Get-iDRACClass -Session $idrac -Class DCIM_iDRACCardView).DNSRacName)"
}

Stepping through the function, we go ahead and establish our iDRAC credential, then we go ahead and enumerate the two classes where we can quickly grab the two properties we want.  After that, we make sure that we're using the default iDRAC naming convention, and that the hostname isn't empty before doing any work.

We'll be performing configuration changes with Invoke-WSManAction, and I like to lay everything out in variables so those commands are a bit easier to read.  We first define our resourceURI where the methods we'll be invoking live ($iDRACCardService).  Then we provide the values for -SelectorSet ($selectFilter).  Now we need the value sets.  In one, we're changing the hostname, the other we're committing.  $hostname's attribute value handles the work of changing the name to <hostname>-iDRAC with substitution (AttributeValue="$($systemview.hostname)-iDRAC"), while $commitSet is just handling targeting and time requirements.  Finally we have our two Invoke-WSManAction commands for SetAttribute and CreateTargetedConfigJob that are run, then we wait a second and return the newly configured iDRAC Hostname that is on the iDRAC after configuration.

This script can easily be run through a foreach loop to target multiple systems, or you can use OME and the Generic Command line Remote Task type to apply to multiple discovered iDRACs.  This also gives you job scheduling/management without having to write up a PowerShell function to manage it for you.

To do this in OME, you'd need to: 
1. Set-ExecutionPolicy Unrestricted (if you haven't already)
2. Create your command line task
3. Set to Generic Command
4. Enter "powershell.exe" for Command
5. Enter the path to script and arguments + tokens for Arguments (c:\test\Set-DNSRacNameFromHostname.ps1 -racIP $RAC_IP -racUser $USERNAME -racPass $PASSWORD)
6. Check Output to file and specify a location for script output to be logged (you'll have multiple instances of the script running, so be sure to check Append)
7. Enter the iDRAC username in $USERNAME field
8. Enter the iDRAC password in $PASSWORD field


9. Select your iDRACs in Task Target



10. Set to Run now or schedule a time for execution
11. Enter account credentials (account needs to have local administrator rights on the OME server)


I'd say that about wraps up this episode of tinkering.  For easy reference I am adding the script below:

Thursday, April 30, 2015

Building a Hard Drive Report for Controller 0 in Dell OpenManage Server Administrator


I was recently asked a question on if there was a way to capture a couple pieces of data from the hard drives attached to the internal PERC RAID controller.  The first thought was that maybe we could pull this from one of the classes in the root/cimv2/dell WMI namespace available with the OpenManage Server Administrator (OMSA) installation.

We soon found out that the data available from those classes did not provide the necessary data (we wanted to capture the vendor and model of the drives), but we could get that information from the OMSA CLI using "omreport storage pdisk controller=0".


This came with it's own set of challenges:  We don't display the actual vendor since these are Dell validated drives, and you're spending a whole lot of time looking for the information you need.

Being that I'm one of the world's worst "lazy" people and I'll work hard to automate things, I decided to do some experimenting to see if I could quickly pull the key information we need using PowerShell.

First up, I really don't want to spend time parsing text if I can help it, and I know that "omreport" will take an argument to export the data to XML.  PowerShell likes XML... so I set off to work looking at the output of the "omreport storage pdisk controller=0 -fmt xml" command.


As luck would have it, the XML actually presents the disk vendor as DiskProductVendor.  That makes getting just the data we need a whole lot easier.  After casting the xml output into a PowerShell object and working out where it was addressed, I put together a command to pull that data and submitted it for review:

$test = [xml] $(omreport storage pdisk controller=0 -fmt xml) ; $test.OMA.ArrayDisks.DCStorageObject.DiskProductVendor ; $test.OMA.ArrayDisks.DCStorageObject.ProductID

type                                                        #text
----                                                        -----
astring                                                     SEAGATE
astring                                                     SEAGATE
astring                                                     ST9500620SS
astring                                                     ST9500620SS

Of course, the next question was how we could run it against a bunch of systems.  Since one of the assumptions I was given to work with is that OMSA would be installed, and the systems are in the same domain, we can use the "Invoke-Command" cmdlet with the "-ComputerName" and "-ScriptBlock" switches to invoke omreport remotely.  With that, getting the data becomes as simple as creating a foreach statement and piping the hostnames into it.

$servers = "R620-1","R620-2"
$servers | foreach { $test = $(Invoke-Command -Computer $_ -ScriptBlock { omreport storage pdisk controller=0 -fmt xml }) ; $($test.OMA.ArrayDisks.DCStorageObject.DiskProductVendor) ; $($test.OMA.ArrayDisks.DCStorageObject.ProductID) }

type                                                        #text
----                                                        -----
astring                                                     SEAGATE
astring                                                     SEAGATE
astring                                                     ST9500620SS
astring                                                     ST9500620SS
astring                                                     SEAGATE
astring                                                     SEAGATE
astring                                                     ST9500620SS
astring                                                     ST9500620SS

Success!  The data we wanted was actually pulled.  To increase the scale you could easily create a text file with a list of servers, and then just populate $servers using Get-Content ($servers = Get-Content .\servers.txt).

Now, I'm a bit picky about how data is presented.  This is still very ugly to look at, and you don't even know which drives belong to which server.  What if you wanted to know the physical drive position?  With these thoughts in mind, I set out to make it better, though it did take a little experimentation.  Cramming it together like a one-liner wasn't going to cut it.

The next step for me was to get this data sorted out and in an order I could create a PSObject and display the data in a table.  While I was at it, I grabbed the DeviceID (which correlates to drive bay):

$test.OMA.ArrayDisks.DCStorageObject | foreach {echo "Disk $($_.DeviceID.'#text')";$_.DiskProductVendor.'#text'
; $_.ProductID.'#text'}
Disk 0
SEAGATE
ST9500620SS
Disk 1
SEAGATE
ST9500620SS


Now this is definitely something we can work with!  I decided to go ahead and grab the output of hostname from the system we're executing against, though I probably could have just assigned $_ to $hostname in the outer foreach loop.  It wasn't necessary to define variables for $diskID, $vendor, and $model, it was just my way to keep myself from chasing down parsing errors because I missed a single or double quote somewhere.

Here's what I ended up with:

$report = ($servers | foreach {
$hostname = $(Invoke-Command -Computer $_ -ScriptBlock { hostname })
$pdisks = [xml] $(Invoke-Command -Computer $_ -ScriptBlock { omreport storage pdisk controller=0 -fmt xml })
@($pdisks.OMA.ArrayDisks.DCStorageObject | foreach {
$diskID = "Disk $($_.DeviceID.'#text')"
$vendor = $($_.DiskProductVendor.'#text')
$model = $($_.ProductID.'#text')
[pscustomobject]@{"Hostname"=$hostname;
  "Disk ID"=$diskID;
  "Vendor"=$vendor;
  "Model"=$model}
})
})
$report


Hostname Disk ID Vendor  Model
-------- ------- ------  -----
R620-1   Disk 0  SEAGATE ST9500620SS
R620-1   Disk 1  SEAGATE ST9500620SS
R620-2   Disk 0  SEAGATE ST9500620SS
R620-2   Disk 1  SEAGATE ST9500620SS



I think we can all agree that's a lot easier on the eyes!  And since this is a PSObject, we can export that to a CSV with "$report | Export-Csv <file>" like so:

$report | Export-Csv .\export.csv
type .\export.csv
#TYPE System.Management.Automation.PSCustomObject
"Hostname","Disk ID","Vendor","Model"
"R620-1","Disk 0","SEAGATE","ST9500620SS"
"R620-1","Disk 1","SEAGATE","ST9500620SS"
"R620-2","Disk 0","SEAGATE","ST9500620SS"
"R620-2","Disk 1","SEAGATE","ST9500620SS"


And with that, my tinkering on this subject is complete.