I'm having issues with the way an array loses its formatting when it is emailed and viewed in outlook 2013
The formatted array looks like this in PowerShell
vServer State Connection
------- ----- ----------
vServer-LB-1 UP 0
vServer-LB-2 DOWN 0
vServer-LB-3 UP 0
vServer-LB-4 UP 0
vServer-LB-5 UP 0
vServer-LB-6 UP 2
This is how I formatted the array (I have tried to email the unformatted array, but it is still wrong)
$formatserver = #{Expression={$_.name};Label="vServer";width=48}, `
#{Expression={$_.state};Label="State";width=17}, `
#{Expression={$_.establishedconn};Label="Connection"}
$Array = $server | Format-Table $formatserver
However, when emailed (not quite like this, but its not formatted correctly).
vServer State Connection
------- ----- ----------
vServer-LB-1 UP 0
vServer-LB-2 DOWN 0
vServer-LB-3 UP 0
vServer-LB-4 UP 0
vServer-LB-5 UP 0
vServer-LB-6 UP 2
Here is the code for emailing
$from = 'Reporting <Support#Me.com>'
$to = 'me#me.com'
$subject = 'Report'
$body = $Array | Out-String
$smtpServer = 'mail.me.com'
$msg = New-Object Net.Mail.MailMessage($from, $to, $subject, $body)
#$msg.IsBodyHTML = $true
$smtp = New-Object Net.Mail.SmtpClient($smtpServer)
$smtp.Send($msg)
Please note I have tried many combinations of | out-stringand $msg.IsBodyHTML = $true
You can use the <pre> tag in HTML to keep the spacing in your emails.
$body = $Array | Out-String | %{"<pre>"+$_+"</pre>"}
Make sure that you set IsBodyHTML as $true.
Note: Table formats a limited to the buffers of your PowerShell Shell/Console. So if your table width is more that the buffer width on your Shell/Console the table will not be shown in full.
To get round this you can set your shell buffer at the start of your script with the following:
$pshost = get-host
$pswindow = $pshost.ui.rawui
$newsize = $pswindow.buffersize
$newsize.height = 3000
$newsize.width = 1500
$pswindow.buffersize = $newsize
Or go to File >> Properties on your Shell/Console, and change the following property.
Related
I have a 100 column table in sql server and I want to make it so not all of the columns need to be passed in the file to load. I have assigned column names in a table that then compares the columns in a hash table to find matching columns. I then create the code based on the match for the array I want to use to insert the data from the file. The problem is, it doesn't like calling the one variable to create the custom object.
I store the following below in a array. (up to a 100 of these, few below for sample (notice sqlcolumn2 is skipped for example)).
sqlcolumn1 = if ([string]::IsNullOrEmpty($obj.P1) -eq $true) {$null} else {"$obj.P1"}
sqlcolumn3 = if ([string]::IsNullOrEmpty($obj.P2) -eq $true) {$null} else {"$obj.P2"}
sqlcolumn4 = if ([string]::IsNullOrEmpty($obj.P3) -eq $true) {$null} else {"$obj.P3"}
sqlcolumn5 = if ([string]::IsNullOrEmpty($obj.P4) -eq $true) {$null} else {"$obj.P4"}
Here is the array:
foreach($line in $Final)
{
$DataRow = "$($line."TableColumnName") = if ([string]::IsNullOrEmpty(`$obj.$($line."PName")) -eq `$true) {`$null} else {`"`$obj.$($line."PName")`"}"
$DataArray += $DataRow
}
I then try to add it to a final array where I would want this to be looped through for each row of data after which I would perform the insert from the array. Even though the "string" value in the array above is correct if it were hand coded, I can't get it to recognize the rows and run.
foreach ($obj in $data2)
{
$test = [PSCustomObject] #{
$DataArray = Invoke-Expression $DataArray
}
If I just type $DataArray, it doesn't like this because it wants the = sign which I already have built into the string.
Is what I am trying to do even possible.
I was attempting to template out various different ways we receive this data, where some people send us 30 of the 100 columns, other more or less, and no one person using the exact columns to cut down on individual scripts for everything.
Adding more code:
Function ArrayCompare() {
[CmdletBinding()]
PARAM(
[Parameter(Mandatory=$True)]$Array1,
[Parameter(Mandatory=$True)]$A1Match,
[Parameter(Mandatory=$True)]$Array2,
[Parameter(Mandatory=$True)]$A2Match)
$Hash = #{}
foreach ($Data In $Array1) {
$Hash[$Data.$A1Match] += ,$Data
}
foreach ($Data In $Array2) {
$Hash[$Data.$A2Match] += ,$Data
}
foreach ($KeyValue In $Hash.GetEnumerator()){
$Match1, $Match2 = $KeyValue.Value.Where( {$_.$A1Match}, 'Split')
[PSCustomObject]#{
MatchValue = $KeyValue.Key
A1Matches = $Match1.Count
A2Matches = $Match2.Count
TablePosition = [int]$Match2.TablePosition
TableColumnName = $Match2.TableColumnName
# PName is the P(##) that is a generic ascending column value back to import-excel module. ColumnA = P1, ColumnB = P2 etc..until no data is detected. Allows flexibility and not having to know how many columns there are
PName = $Match1.Name}
}
}
$Server = 'ServerName'
$Catalog = 'DBName'
$DestinationTable = 'ImportIntoTableName'
$FileIdentifierID = 10
$FileName = 'Test.xlsx'
$FilePath = 'C:\'
$FullFilePath = $FilePath + $FileName
$data = Import-Excel -Path $FullFilePath -NoHeader -StartRow 1 # Import-
Excel Module for working with xlsx excel files
$data2 = Import-Excel -Path $ullFilePath -NoHeader -StartRow 2 # Import-
Excel Module for working with xlsx excel files
$ExpectedHeaderArray = #()
$HeaderArray = #()
$DataArray = #()
$HeaderDetect = #()
$HeaderDetect = $data | Select-Object -First 1 # Header Row In File
$HeaderDetect |
ForEach-Object {
$ColumnValue = $_
$ColumnValue |
Get-Member -MemberType *Property |
Select-Object -ExpandProperty Name |
ForEach-Object {
$HeaderValues = [PSCustomObject]#{
Name = $_
Value = $ColumnValue.$_}
$HeaderArray += $HeaderValues
}
}
# Query below provides a list of all expected file headers and the table
column name they map to
$Query = "SELECT TableColumnName, FileHeaderName, TablePosition FROM
dbo.FileHeaders WHERE FileIdentifierID = $($FileIdentifierID)"
$ds = Invoke-Sqlcmd -ServerInstance $Server -Database $Catalog -Query $Query
-OutputAs DataSet
$ExpectedHeaderArray = foreach($Row in $ds.Tables[0].Rows)
{
new-object psObject -Property #{
TableColumnName = "$($row.TableColumnName)"
FileHeaderName = "$($row.FileHeaderName)"
TablePosition = "$($row.TablePosition)"
}
}
#Use Function Above
#Bring it together so we know what P(##) goes with which header in file/mapped to table column name
$Result = ArrayCompare -Array1 $HeaderArray -A1Match Value -Array2 $ExpectedHeaderArray -A2Match FileHeaderName
$Final = $Result | sort TablePosition
foreach($Line in $Final)
{
$DataRow = "$($Line."TableColumnName") = if ([string]::IsNullOrEmpty(`$obj.$($Line."PName")) -eq `$true) {`$null} else {`"`$obj.$($Line."PName"))`"}"
$DataArray += $DataRow
}
# The output below is what the code inside the last array would be that I would use to import into excel.
# The goal is to be dynamic and match headers in the file to the stored header value and import into a table (mapped from header column to table column name)
# The reason for this is before I was here, there were many different "versions" of a layout that was given out. In the end, it is all one in the same
# but some send all 100 columns, some only send a handful, some send 80 etc. I am trying to have everything flow through here vs. 60+ pieces of code/stored procedures/ssis packs
Write-Output $DataArray
# Output Sample -- Note how in the sample, P2 and subsequent skip SQLColumn2 because P2 maps to the header value of position 3 in the sql table and each after is one off.
# In this example, SqlColumn2 would not be populated
# SqlColumn1 = if ([string]::IsNullOrEmpty($obj.P1) -eq $true) {$null} else {"$obj.P1"}
# SqlColumn3 = if ([string]::IsNullOrEmpty($obj.P2) -eq $true) {$null} else {"$obj.P2"}
# SqlColumn4 = if ([string]::IsNullOrEmpty($obj.P3) -eq $true) {$null} else {"$obj.P3"}
# SqlColumn5 = if ([string]::IsNullOrEmpty($obj.P4) -eq $true) {$null} else {"$obj.P4"}
# I know this doesn't work. This is where I'm stuck, how to build an array now off of this output from above
foreach ($obj in $data2)
{
$test = [PSCustomObject] #{
$DataArray = Invoke-Expression $DataArray}
}
I'm gong to re-state your question first, just to make sure I understand it properly (it's possible I don't!)...
You've got an excel file that looks something like this:
+---+---------+---------+---------+
| | A | B | C |
+---+---------+---------+---------+
| 1 | HeaderA | HeaderB | HeaderC |
+---+---------+---------+---------+
| 2 | Value P | Value Q | Value R |
+---+---------+---------+---------+
| 3 | Value S | Value T | Value U |
+---+---------+---------+---------+
You've also got a database table which looks like this:
+---------+---------+---------+---------+
+ ColumnW | ColumnX | ColumnY | ColumnZ |
+---------+---------+---------+---------+
+ ....... | ....... | ....... | ....... |
+---------+---------+---------+---------+
and a column mapping table like this (note, ColumnX isn't mapped in this example):
+-----------------+----------------+---------------+
| TableColumnName | FileHeaderName | TablePosition |
+-----------------+----------------+---------------+
| ColumnW | HeaderA | 1 |
+-----------------+----------------+---------------+
| ColumnY | HeaderB | 2 |
+-----------------+----------------+---------------+
| ColumnZ | HeaderC | 3 |
+-----------------+----------------+---------------+
You want to insert the values from the spreadsheet into the database table, using the data in your mapping table so you get this:
+---------+---------+---------+---------+
+ ColumnW | ColumnX | ColumnY | ColumnZ |
+---------+---------+---------+---------+
+ Value P | null | Value Q | Value R |
+---------+---------+---------+---------+
+ Value S | null | Value T | Value U |
+---------+---------+---------+---------+
So let's load the spreadsheet (letting the header row generate meaningful property names this time):
$data = Import-Excel -Path ".\MySpreadsheet.xlsx";
write-host ($data | ft | out-string);
# HeaderA HeaderB HeaderC
# ------- ------- -------
# Value P Value Q Value R
# Value S Value T Value U
and get your column mapping data (I'm programmatically creating an in-memory dataset, but you obviously read yours from your database instead):
$mappings = new-object System.Data.DataTable;
$null = $mappings.Columns.Add("TableColumnName", [string]);
$null = $mappings.Columns.Add("FileHeaderName", [string]);
$null = $mappings.Columns.Add("TablePosition", [int]);
#(
#{ "TableColumnName"="ColumnW"; "FileHeaderName"="HeaderA"; "TablePosition"=1 },
#{ "TableColumnName"="ColumnY"; "FileHeaderName"="HeaderB"; "TablePosition"=2 },
#{ "TableColumnName"="ColumnZ"; "FileHeaderName"="HeaderC"; "TablePosition"=3 }
) | % {
$row = $mappings.NewRow();
$row.TableColumnName = $_.TableColumnName;
$row.FileHeaderName = $_.FileHeaderName;
$row.TablePosition = $_.TablePosition;
$mappings.Rows.Add($row);
}
$ds = new-object System.Data.DataSet;
$ds.Tables.Add($mappings);
write-host ($ds.Tables[0] | ft | out-string)
# TableColumnName FileHeaderName TablePosition
# --------------- -------------- -------------
# ColumnW HeaderA 1
# ColumnY HeaderB 2
# ColumnZ HeaderC 3
Now we can build the "mapped" objects:
$values = #();
foreach( $row in $data )
{
$properties = [ordered] #{};
foreach( $mapping in $mappings )
{
$properties.Add($mapping.TableColumnName, $row."$($mapping.FileHeaderName)");
}
$values += new-object PSCustomObject -Property $properties;
}
write-host ($values | ft | out-string)
# ColumnW ColumnY ColumnZ
# ------- ------- -------
# Value P Value Q Value R
# Value S Value T Value U
The tricksy bit is $properties.Add($mapping.TableColumnName, $row."$($mapping.FileHeaderName)"); - basically, you can access object properties in PowerShell using a dotted string literal or variable (I'm not sure of the exact feature name) - e.g.
PS> $myValue = new-object PSCustomObject -Property #{ "aaa"="bbb"; "ccc"="ddd" }
PS> $myValue."aaa"
bbb
PS> $myProperty = "aaa"
PS> $myValue.$myProperty
"bbb"
so $row."$($mapping.FileHeaderName)" is an expression that evaluates to the value of the property of $row named in $mapping.FileHeaderName.
And then finally you can insert the objects into your database using your existing process...
Note that I couldn't quite work out what your ArrayCompare is actually doing so it's possible the above doesn't solve your problem 100%, but it's hopefully close enough that you can either work the difference out yourself, or leave a comment with where it differs from your desired solution.
Hope this helps.
Code:
$arr1 = "" | select blabla,blabla2
$arr2 = "" | select blabla3,blabla4
$arrtotal = #()
$arrtotal += $arr1
$arrtotal += $arr2
$arrtotal
Printout:
blabla blabla2
However, when attempting to print both cells individually (not one after the other but simply selecting in PS ISE and hitting F8):
$arrtotal[0]
blabla blabla2
$arrtotal[1]
blabl3 blabla4
EDITED:
I would have expected both array columns to be printed when printing $arrtotal. Not just one of them. Further more it's unclear to me why printing them individually works but one after the other i.e "$arrtotal[0];$arrtotal[1]" does not.
EDIT2:
This is my original code.
All it does is query Sparkpost's API in order to build a custom HTML report.
$test = (Invoke-WebRequest "https://api.sparkpost.com/api/v1/metrics/deliverability?metrics=count_injected,count_sent,count_bounce,count_accepted&from=2016-01-01T08:00&to=2016-04-25T08:00" -Headers #{"Authorization"="xxxxxxxxxxxxx";"Content-Type"= "application/json"}).content | ConvertFrom-Json
$fill1 = "" | select EmailsReceived,EmailsSent,EmailsBounced
$fill1.EmailsReceived = $test.results.count_injected
$fill1.EmailsSent = $test.results.count_accepted
$fill1.EmailsBounced = $test.results.count_bounce
$fill2 = "" | select DeliveredPrecentage,BouncesPrecentage
$fill2.DeliveredPrecentage = [math]::round($test.results.count_accepted/$test.results.count_injected*100,2)
$fill2.BouncesPrecentage = [math]::round(($test.results.count_bounce)/$test.results.count_accepted*100,2)
$arr = #()
$arr += , $fill1
$arr += , $fill2
My problem is that I cant simply convert $arr into an HTML file like I've done numerous times before.
$arr
EmailsReceived EmailsSent EmailsBounced
107 107 12
On the other hand
$arr | Format-List
EmailsReceived : 107 EmailsSent : 107 EmailsBounced : 12
DeliveredPrecentage : 100 BouncesPrecentage : 11.21
I'd like to make an HTML out of everything so I can send it via email later. How can I pipe it all?
Its because $arr1and $arr2are two different PSCustomObjects. You can print the whole arrayin a list using the Format-List cmdlet:
$arrtotal | Format-List
Output:
blabla :
blabla2 :
blabla :
blabla2 :
Answer to your edit:
This looks like a single record for me, try this:
$record = [PsCustomObject]#{
EmailsReceived = $test.results.count_injected
EmailsSent = $test.results.count_accepted
EmailsBounced = $test.results.count_bounce
DeliveredPrecentage = [math]::round($test.results.count_accepted/$test.results.count_injected*100,2)
BouncesPrecentage = [math]::round(($test.results.count_bounce)/$test.results.count_accepted*100,2)
}
$record | convertto-html | out-file file.html
Note: I also changed the logic to create the object to a more well known approach using a PsCustomObject typecast on a hashtable.
Hi Firstly I have WinPE 4 and Powershell version 4 running other powershell scripts fine in my environment.
The issue is with displaying Array information.. On windows 7 running PS Version 4 this works great...
I have a Function rounding the size off which we dont need in this example.
$a = #()
$diskdrive = gwmi win32_diskdrive
foreach($drive in $diskdrive){
$InterFaceType = "$($Drive.InterfaceType)"
If(!($InterFaceType -eq "USB")){
$Size = "$($drive.size)"
$DriveModel = "`nDrive: $($drive.deviceid.substring(4)) Model: $($drive.model)"
$Size = Get-OptimalSize $Size
$Result = $DriveModel + " " + $Size
$a += $Result
}
}
$a = $a | Sort-Object
$Drive1 = $a[0]
$Drive2 = $a[1]
$Drive3 = $a[2]
$Drive4 = $a[3]
$Drive5 = $a[4]
$Drive6 = $a[5]
$Drive1 = $Drive1.trim()
$Drive2 = $Drive2.trim()
$Drive3 = $Drive3.trim()
$Drive4 = $Drive4.trim()
$Drive5 = $Drive5.trim()
$Drive6 = $Drive6.trim()
In Windows 7 $Drive1,2,3,4,5,6 will display the following info about the Drive detected.
Drive: PHYSICALDRIVE0 Model: ST3500418AS 465.76 GB
In WinPE $Drive1,2,3,4,5,6 Are Blank...
However $Result ( which i add to my array ) is not. As soon as i take that $Result and add to the $a array in WinPE its like it clears it?
I need to loop through the for each and add each pass to an array and this is how you do it, but as WinPE is acting differently I am looking at creating a brand new dynamic variable with each pass.
For eg.
Take first pass call $Result1, then $Result2 and $result3 etc.
Maybe if I use an actual Variable ( like the $Result which appears to display in WinPE) i can work round the odd behaviour of WinPE??
Any help appreciated.
Just to add the behaviour is reminiscent of the string not being trimmed as when compiling the code on Windows 7 it was doing the same until i added the trim to the $drive variables. However i have already trimmed the variables and in WinPE it still will not show when called. Do i have to trim further? just an idea
for eg.... i was doing this.
If($a[0]){
$Radio1.BackColor = [System.Drawing.Color]::’Transparent’
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 500
$System_Drawing_Size.Height = 15
$Radio1.Size = $System_Drawing_Size
$Radio1.TabIndex = 5
$Radio1.Text = $Drive1
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 27
$System_Drawing_Point.Y = 260
$Radio1.Location = $System_Drawing_Point
$Radio1.DataBindings.DefaultDataSourceUpdateMode = 0
$Radio1.Name = "Radio1"
$form1.Controls.Add($Radio1)
}
Using $a and not an array this is what happens.
If i do this
$msg = [System.Windows.Forms.MessageBox]
$msg::Show($a[0])
I get nothing in winpe.
If i do this...
$msg = [System.Windows.Forms.MessageBox]
$msg::Show($a)
it outputs the information, however i cannot use just $a as that is only the last pass in the loop..
All this is fine in windows 7..
How can i create a New variable on each pass without an Array?
So $Result1, $Result2 etc then i can call the actual variable for the info not the array index? that might work?
Adding my suggestion as an Answer so the question can be resolved.
Try changing $a += $Results to [Array]$a = $a + $Results and see if that behaves better. It sounds like it isn't setting up $a as an array so it isn't iterating new records, it's just resetting it.
I have encountered a new problem and I don't even know where exactly to start explaining. I will try my best, if something is unclear just ask me please.
I have an Excel workbook with informations (multiple rows) about DNS records - pretty similar to the powershell DNS syntax. e.g:
HostName RecordType TimeStamp TimeToLive RecordData
# A 0 00:05:00 127.0.0.1
I read them as arrays with the following little code - not very fast, but it works!:
#Read Excel
$row = [int]2
do {
if ($Sheet4.Cells.Item($Row,1).Text) {$ZoneName += $Sheet4.Cells.Item($Row,1).Text}
$HostName += $Sheet4.Cells.Item($Row,2).Text
$RecordType += $Sheet4.Cells.Item($Row,3).Text
$TimeStamp += $Sheet4.Cells.Item($Row,4).Text
$TimeToLive += $Sheet4.Cells.Item($Row,5).Text
$RecordData += $Sheet4.Cells.Item($Row,6).Text
$row = $row + [int] 1
} until (!$Sheet4.Cless.Item($row,2))
Now I have 6 arrays all stuck with information in different arrays, but all with the same amount of lines.
And now the tricky (atleast for me!) part:
I would like to stuff those 6 arrays into some special array I do not know, or in some sort of table I do not know how to create.
Why?
Because I want to compare those lines to this code ($Records to be specific):
$ZoneNames = (Get-DnsServerZone -ComputerName $DnsServer).zonename
$ZoneNames | foreach {$Records = (Get-DnsServerResourceRecord -ComputerName $DnsServer -ZoneName $_)}
$Records[0] would show me this (e.g.):
HostName RecordType Timestamp TimeToLive RecordData
-------- ---------- --------- ---------- ----------
# A 0 00:05:00 127.0.0.1
BUT: If I go deeper: $Records[0].RecordData:
IPv4Address PSComputerName
----------- --------------
127.0.0.1
So I would need to recreate this (above) sort of hierarchy to compare them (If I am right?).
I have tried it with a table like this (didn't work):
#Create Table object
$table = New-Object system.Data.DataTable “$ExcelRecords”
#Define Columns
$col2 = New-Object system.Data.DataColumn HostName,([string])
$col3 = New-Object system.Data.DataColumn RecordType,([string])
$col4 = New-Object system.Data.DataColumn TimeStamp,([string])
$col5 = New-Object system.Data.DataColumn TimeToLive,([string])
$col6 = New-Object system.Data.DataColumn RecordData,([string])
#Add the Columns
$table.columns.add($col2)
$table.columns.add($col3)
$table.columns.add($col4)
$table.columns.add($col5)
$table.columns.add($col6)
#Create a row
$r = $table.NewRow()
#Enter data in the row
$r.HostName = $HostName[$counter]
$r.RecordType = $RecordType[$counter]
$r.TimeStamp = $TimeStamp[$counter]
$r.TimeToLive = $TimeToLive[$counter]
$r.RecordData = $RecordData[$counter]
$RecordData
#Add the row to the table
$table.Rows.Add($r)
Tried comparing like this (didn't work):
if ($records[0] -like $table[0]) {write-host "works"}
This did work:
if ($records[0].hostname -like $table[0].hostname) {write-host "works"}
works
This did not (I guess this is the root of my problems):
if ($Records[0].RecordData -like $table[0].RecordData) {write-host "works"}
My main objective:
Check if there are Records on the DNS-Server, which aren't stated in the Excel sheet and delete them from the DNS-Server!
If you read through all the text, thanks for doing that! Appreciate every help.
Thanks in advance!
I'd start by creating an array of PS objects from your spreadsheet data.
#Read Excel
$row = [int]2
$DNS_Records =
do {
if ($Sheet4.Cells.Item($Row,1).Text) {
New-Object PSObject -Property #{
ZoneName = $Sheet4.Cells.Item($Row,1).Text
HostName = $Sheet4.Cells.Item($Row,2).Text
RecordType = $Sheet4.Cells.Item($Row,3).Text
TimeStamp = $Sheet4.Cells.Item($Row,4).Text
TimeToLive = $Sheet4.Cells.Item($Row,5).Text
RecordData = $Sheet4.Cells.Item($Row,6).Text
}
}
} until (!$Sheet4.Cless.Item($row,2))
I am trying to build a powershell script which collects system up-time information from multiple system and then e-mails it. I want to format the e-mail so I can structure it in a sensible way and later add more information about the systems.
Today I have this script below which collect the uptime information about a list of systems and then passes these information in a array to the SendMail function which includes the array in an e-mail. However the current output is like this.
Uptime list: Restarted 02/19/2013 04:04:52 MyServer01 Computer Uptime: 23 days and 9 hours Restarted 02/15/2013 04:17:40 MyServer02 Computer Uptime: 27 days and 9 hours
I would like to have the output or rather the e-mail text more like:
System | Restarted date/time | Uptime
MyServer01 02/17/2013 04:04:52 25 days and 9 hours
MyServer02 02/17/2013 04:04:52 25 days and 9 hours
How can I format the text in the e-mail or do I have to do that before I pass the array to the e-mail function.
The script itself:
`
function SendMail([string]$arg1){
#SMTP server name
$smtpServer = "mysmtp"
#Creating a Mail object
$msg = new-object Net.Mail.MailMessage
#Creating SMTP server object
$smtp = new-object Net.Mail.SmtpClient($smtpServer)
#Email structure
$msg.From = "my#overtherainbow.ddd"
$msg.ReplyTo = "noreply#overtherainbow.ddd"
$msg.To.Add("my#overtherainbow.ddd")
$msg.subject = "System uptime"
$msg.body = ("List og servers and uptime:" + $arg1 )
#Sending email
$smtp.Send($msg)}
function CheckUptime($listofservers){
$k =#()
foreach($s in $listofservers){
$Booted = Get-WmiObject -Class Win32_OperatingSystem -Computer $s
$Calc = [DateTime]::Now - $Booted.ConvertToDateTime($Booted.LastBootUpTime)
$k += " Restarted " +
$Booted.ConvertToDateTime($Booted.LastBootUpTime) +
" " + $s + " " +
" Computer Uptime: " +
$Calc.days + " " +
"days and", $Calc.hours + " hours"
}
return $k
}
#Defining list og servers
$listofservers = "MyServer01", "MyServer02"
#Calling function
$message = CheckUptime($serverlist)
SendMail($message)
`
Use Format-Table to format the data as desired, and pipe that into Out-String to convert into a single string containing the formatted data.
Then append that to your email content.
This relies on the data gathering replacing building a single string with building a custom object with separate fields for each different column.
Eg (incomplete, but should give you the idea)
$serverData = $serverList | Foreach-Object { GetServerData $_ } |
Format-Table Name,
#(l='Boot Time'; e={$_.BootTime.ToLocalTime()}},
#(l='Uptime'; e={FormatUptime([DateTime]::UtcNow - $_.BootTime}} |
Out-String
$emailBody += $serverData
where the referenced functions are already defined, along the lines of:
function GetServerData {
params([string]$name)
$os = Get-WmiObject -Class Win32_OperatingSystem -Computer $name
$bootTimeLocal = $os.ConvertToDateTime($os.LastBootUpTime)
new-object PSObject -property #{
Name = $name;
# Keep everything in UTC to avoid confusion in calcs,
# TODO FIX: Use the remote machine's timezone for this....
BootTime = $bootTimeLocal.ToUniversalTime()
}
}
and
function FormatUptime {
params([TimeSpan]$time)
...
}
Summary:
Use Format-* cmdlets to format, and if not immediately passing to output (and the default Out-Default appended to the end of every pipeline without an Out-*) then convert to a string with Out-String.
Keep data types in their native type as long as possible. Do not convert to human readable forms until you have to. Thus information is not lost.
Make use of custom objects and properties (especially NoteProperties) to create your own object "types" to keep related information together.
First, it shows little effort when your sample is not even a working sample. Variable typos etc.
As a solution, why don't you create custom objects containing the data for each server and output it as an array?. Something like:
PS > function CheckUptime($listofservers){
$res = #()
foreach($s in $listofservers){
$boot = Get-WmiObject win32_operatingsystem -ComputerName $s | % { $_.ConvertToDateTime($_.LastBootUpTime) }
$uptime = [DateTime]::Now - $boot
$res += New-Object psobject -Property #{
Server = (Get-Culture).TextInfo.ToTitleCase($s)
BootTime = $boot
Uptime = "{0} days and {1} hours" -f $uptime.Days, $uptime.Hours
}
}
#Output
$res
}
$myservers = "localhost", "localhost"
$str = "List of servers and uptime:`r`n"
$str += CheckUptime $myservers | Out-String
PS > $str
List of servers and uptime:
Server Uptime BootTime
------ ------ --------
Localhost 8 days and 6 hours 06.03.2013 09:33:37
Localhost 8 days and 6 hours 06.03.2013 09:33:37