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
Related
I got the following variable $listofusers which returns the below objects in two columns:
SourceUser DestinationUser
---------- ---------------
username1#legacy.company.corp username1#modern.company.corp
username2#legacy.company.corp username2#modern.company.corp
username3#legacy.company.corp username3#modern.company.corp
username4#legacy.company.corp username4#modern.company.corp
I now need to process this list of users in a foreach loop. I have tried so far the following but without luck yet:
$Results = ForEach ($User in $listofusers) {
Write-Host "Processing SourceUser $($User.SourceUser)"
Write-Host "Processing DestinationUser $($User.DestinationUser)"
#Assign the content to variables
$SourceUsers = $User.SourceUser
$DestinationUsers = $User.DestinationUser
}
It only returns me the last line of the objects:
$SourceUsers
RETURN ONLY: username4#legacy.company.corp
$DestinationUsers
RETURN ONLY: username4#modern.company.corp
How can I add all the objects in the variable $listofusers for further processing?
UPDATE:
I am trying to achieve the following that's why I have broken the association in listofusers
$SourceUser = #()
$DestinationUser = #()
$Results = ForEach ($User in $listofusers)
{
Write-Host "Processing SourceUser $($User.SourceUser)"
Write-Host "Processing DestinationUser $($User.DestinationUser)"
#Assign the content to variables
$SourceUser += $User.SourceUser
$DestinationUser += $User.DestinationUser
#Cannot get that variables working yet
$sourceusername, $sourcedomain = $SourceUser -split ("#")
$DestinationUsername, $destinationDomain = $DestinationUser -split ("#")
$SourceAccount = Get-ADUser $sourceusername -server $sourcedomain -Properties objectSid
$TargetAccount = Get-ADUser $DestinationUsername -Server $destinationDomain
}
Is there any better way to achieve that and get those variables to that point?
NEW UPDATE:
The purpose of the script would be to achieve the following cmdlets for processing ad objects:
#get the objectSid of the source account
$objectSid = $SourceAccount.objectSid
#copy source account objectSid to target account msExchMasterAccountSid
$TargetAccount | Set-ADUser -Replace #{"msExchMasterAccountSid"=$objectSid}
#enable targetaccount
$TargetAccount | Enable-ADAccount
#disable the source account
$SourceAccount | Disable-ADAccount
#move the migrated user into prod OU
$TargetAccount | Move-ADObject -TargetPath "ou=test,dc=contoso,dc=com"
Thanks
here is a demo of the concept i was trying to get across. [grin] it keeps the association of the objects in your CSV in the original object for as long as possible. the code has NOT been tested since i have no AD access.
what it does ...
fakes reading in a CSV file
when you are ready to use real data, replace the entire "region" with a call to Import-CSV.
iterates thru the list
builds a splat of the parameters for the AD calls
see Get-Help about_Splatting for more info on that wonderfully useful idea.
calls Get-AdUser with each to the Source/Target user data sets
stores the above
uses the stored account info to ...
== replace the .objectSid of the Target account
== enable the Target account
== disable the Source account
== Move the Target account to the desired OU
the hard coded OU could be set with a variable to make this a tad more flexible. however, this seems to be a one-off operation - so there is likely no benefit.
if you want to add logging, do so in the same loop.
there is no error handling, either. that likely should be added with a try/catch around each AD call & logging of both success and failure.
the code ...
#region >>> fake reading in a CSV file
# in real life, use Import-CSV
$UserList = #'
SourceUser, DestUser
ABravo#Old.com, ABravo#NewDomain.com
BCharlie#Old.com, BCharlie#NewDomain.com
CDelta#Old.com, CDelta#NewDomain.com
DEcho#Old.com, DEcho#NewDomain.com
EFoxtrot#Old.com, EFoxtrot#NewDomain.com
'# | ConvertFrom-Csv
#endregion >>> fake reading in a CSV file
ForEach ($UL_Item in $UserList)
{
Write-Host 'Processing ...'
Write-Host (' SourceUser {0}' -f $UL_Item.SourceUser)
Write-Host (' DestinationUser {0}' -f $UL_Item.DestUser)
Write-Host '__ Source Account __'
$GADU_Params_1 = [ordered]#{
Identity = $UL_Item.SourceUser.Split('#')[0]
Server = $UL_Item.SourceUser.Split('#')[1]
Properties = 'objectSid'
}
$SourceAccount = Get-ADUser #GADU_Params_1
Write-Host '__ Target Account __'
$GADU_Params_2 = [ordered]#{
Identity = $UL_Item.DestUser.Split('#')[0]
Server = $UL_Item.DestUser.Split('#')[1]
}
$TargetAccount = Get-ADUser #GADU_Params_2
Write-Host 'Making changes ...'
# all these piped objects are slower than making _direct_ calls
# however, i don't have any way to test the code, so i can't use what likely is faster
# something like >>>
# Set-AdUser -Identity $TargetAccount -Replace #{
# 'msExchMasterAccountSid' = $objectSid
# }
# note that i also replaced the unneeded _double_ quotes with the safer _single_ quotes
$TargetAccount |
Set-AdUser -Replace #{
'msExchMasterAccountSid' = $SourceAccount.objectSid
}
$TargetAccount |
Enable-AdAccount
$SourceAccount |
Disable-AdAccount
$TargetAccount |
Move-AdObject -TargetPath 'ou=test,dc=contoso,dc=com'
Write-Host '=' * 30
Write-Host ''
}
no output shown since i can't actually run this AD stuff. [grin]
$SourceUsers and $DestinationUsers contain only the last ones becasue youa re replacing the value on each foreach iteration.
if you want it to separate the properties try this:
$SourceUsers = $User | select SourceUser -ExpandProperty SourceUser
$DestinationUsers = $User | select DestinationUser -ExpandProperty DestinationUser
That will create a collection of only those strings. you wont be able to access those values by property anymore, meaning that is a simple String[] after the -ExpandProperty.
$SourceUsers = #()
$DestinationUsers = #()
$Results = ForEach ($User in $listofusers) {
Write-Host "Processing SourceUser $($User.SourceUser)"
Write-Host "Processing DestinationUser $($User.DestinationUser)"
#Assign the content to variables
$SourceUsers += $User.SourceUser
$DestinationUsers += $User.DestinationUser
}
$SourceUsers = #() and $DestinationUsers = #() creates two empty
arrays which we will use in the loop
+= is an assignment operator which enables us to assign more than
one value to a variable. According to the documentation: Increases
the value of a variable by the specified value, or appends the
specified value to the existing value.
I am trying to gather data from eventlogs of logons, disconnect, logoff etc... this data will be stored in a csv format.
This is the script i am working which got from Microsoft Technet and i have modified to meet my requirement. Script is working as it should be but there is looping going on which i can't figure out how it should be stopped.
$ServersToQuery = Get-Content "C:\Users\metho.HOME\Desktop\computernames.txt"
$cred = "home\Administrator"
$StartTime = "September 19, 2018"
#$Yesterday = (Get-Date) - (New-TimeSpan -Days 1)
foreach ($Server in $ServersToQuery) {
$LogFilter = #{
LogName = 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational'
ID = 21, 23, 24, 25
StartTime = (Get-Date).AddDays(-1)
}
$AllEntries = Get-WinEvent -FilterHashtable $LogFilter -ComputerName $Server -Credential $cred
$AllEntries | Foreach {
$entry = [xml]$_.ToXml()
$Output += New-Object PSObject -Property #{
TimeCreated = $_.TimeCreated
User = $entry.Event.UserData.EventXML.User
IPAddress = $entry.Event.UserData.EventXML.Address
EventID = $entry.Event.System.EventID
ServerName = $Server
}
}
}
$FilteredOutput += $Output | Select TimeCreated, User, ServerName, IPAddress, #{Name='Action';Expression={
if ($_.EventID -eq '21'){"logon"}
if ($_.EventID -eq '22'){"Shell start"}
if ($_.EventID -eq '23'){"logoff"}
if ($_.EventID -eq '24'){"disconnected"}
if ($_.EventID -eq '25'){"reconnection"}
}
}
$Date = (Get-Date -Format s) -replace ":", "-"
$FilePath = "$env:USERPROFILE\Desktop\$Date`_RDP_Report.csv"
$FilteredOutput | Sort TimeCreated | Export-Csv $FilePath -NoTypeInformation
Write-host "Writing File: $FilePath" -ForegroundColor Cyan
Write-host "Done!" -ForegroundColor Cyan
#End
First time when i run the script, it runs fine and i get the csv output as it should be. When i run the script again than a new CSV is created (as it should be) but the same event log enteries are created twice and run it again than three enteries are created for the same event. This is very strange as a new csv is created each time and i dont not have -append switch for export-csv configured.
$FilteredOutput = #()
$Output = #()
I did try adding these two lines in above script as i read somewhere that it is needed if i am mixing multiple variables into a array (i do not understand this so applogies if i get this wrong).
Can someone please help me this, more importantly, I need to understand this as it is good to know for my future projects.
Thanks agian.
mEtho
It sounds like the$Output and $FilteredOutput variables aren't getting cleared when you run the script subsequent times (nothing in the current script looks to do that), so the results are just getting appended to these variables each time.
As you've already said, you could add these to the top of your script:
$FilteredOutput = #()
$Output = #()
This will initialise them as empty arrays at the beginning, which will ensure they start empty as well as make it possible for them to be appended to (which happens at the script via +=). Without doing this on the first run the script likely failed, so I assume you must have done this in your current session at some point for it to be working at all.
I am trying to write a script to scan for Sql Maintenance Task failures - see script below. I appear to be unable to process more than 100 entries using EnumHistory(). Does anyone have a way around this?
Param(
[int]$days="30" # this hardly matters since EnumJobHistory is limited to 100 rows :-(
)
#http://powershell.com/cs/blogs/tobias/archive/2010/01/13/cancelling-a-pipeline.aspx
filter Stop-Pipeline([scriptblock]$condition = {$true})
{$_
if (& $condition) {continue}
}
cls
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null
$instances = Get-Content "DailyMaintenanceMMCServerList.txt"
#loop through each instance
foreach ($instance in $instances)
{
# Create an SMO connection to the instance
$srv = New-Object ('Microsoft.SqlServer.Management.Smo.Server') $instance
$instance
#get all the jobs on the server
$jobs = $srv.JobServer.Jobs
# Avoid exception on some servers?
if (!$jobs.Count)
{
continue
}
#go through each job and find failures in the job history
$jobs | % {
do
{ $job = $_; $count = 0;
$_.EnumHistory() |
Stop-Pipeline { $_.Rundate -lt [datetime]::Today.AddDays(-$days) } |
#? {$_.Message -notlike "*succeeded*" } |
% { " " + ++$count + " " + $job.Name + " " + $_.RunDate + " " + ($_.Message).Substring(0,20) }
} while ($false)
}
}
As pointed out by Ben Thul, the maximum number of rows of history kept is configured by server instance:
Check how much history the Agent is configured to keep. In powershell, you can get this from the MaximumJobHistoryRows in the JobServer object. Or right click on the agent in SSMS and look at "history". My guess is that it's only configured to keep 100 per job.
the script below reads my outlook emails but how do I access the output. I'm new too Powershell and I'm still getting used to certain things. I just want to get the body of 10 unread outlook emails and store them in an Array called $Body.
$olFolderInbox = 6
$outlook = new-object -com outlook.application;
$ns = $outlook.GetNameSpace("MAPI");
$inbox = $ns.GetDefaultFolder($olFolderInbox)
#checks 10 newest messages
$inbox.items | select -first 10 | foreach {
if($_.unread -eq $True) {
$mBody = $_.body
#Splits the line before any previous replies are loaded
$mBodySplit = $mBody -split "From:"
#Assigns only the first message in the chain
$mBodyLeft = $mbodySplit[0]
#build a string using the –f operator
$q = "From: " + $_.SenderName + ("`n") + " Message: " + $mBodyLeft
#create the COM object and invoke the Speak() method
(New-Object -ComObject SAPI.SPVoice).Speak($q) | Out-Null
}
}
This may not be a factor here, since you're looping through only ten elements, but using += to add elements to an array is very slow.
Another approach would be to output each element within the loop, and assign the results of the loop to $body. Here's a simplified example, assuming that you want $_.body:
$body = $inbox.items | select -first 10 | foreach {
if($_.unread -eq $True) {
$_.body
}
}
This works because anything that is output during the loop will be assigned to $body. And it can be much faster than using +=. You can verify this for yourself. Compare the two methods of creating an array with 10,000 elements:
Measure-Command {
$arr = #()
1..10000 | % {
$arr += $_
}
}
On my system, this takes just over 14 seconds.
Measure-Command {
$arr = 1..10000 | % {
$_
}
}
On my system, this takes 0.97 seconds, which makes it over 14 times faster. Again, probably not a factor if you are just looping through 10 items, but something to keep in mind if you ever need to create larger arrays.
define $body = #(); before your loop
Then just use += to add the elements
Here's another way:
$body = $inbox.Items.Restrict('[Unread]=true') | Select-Object -First 10 -ExpandProperty Body
I am creating a reporting script in powercli for our vSphere.
I am trying to make an array that i can export to csv.
The problem i am having is i need to have multiple rows to input into the array, not just the same one over and over.
When i run in current format all that happens is the outermost row reports and none of the internal ones
Here is a mockup of what i want the script to do.
$report = #()
foreach($D in $data){
$row = "" | select Datacenter, datacenterInfo
$row.datacenter = "kam1"
$row.datacenterInfo = "12"
$report += $row
foreach($F in $D){
$row2 = "" | select folder, folderInfo
$row2.folder = "test"
$row2.folderInfo = "34"
$report += $row2
foreach($VM in $F){
$row3 = "" | select VM, VMInfo
$row3.VM = "test"
$row3.VMInfo = "56"
$report += $row3
}
}
}
$report | export-csv -path c:\temp\testy.csv -NoTypeInformation
Any Help Appreciated
data would hopefully come out as a csv in excel while looking like so
datacenter / datacenterinfo
test / 12
folder / folderinfo
test / 34
VM / VMInfo
test / 56
The real script would be detailing hundreds of VM's and folder though.
I know this problem and have no real Solution for it.
But as workaround you could use
"$row3.VM \ $row3.VMInfo" | Out-File -append -path "c:\temp\testy.csv"
in your inner for each, and write the data manually as csv.