Powershell - Getting distinct values from a collection of hash tables - powershell-5.1

I'm using powershell version 5.1 for everything. The background of my issue is I'm using powershell DSC to create webapplications and app pools for various nodes. For every server except the last 2, every web application has a unique app pool so everything's worked fine. On the last 2 servers, several are sharing application pools due to memory concerns. I store a name reference to the given site in node data along with the service account, then in non-node data I put the more elaborate details (rt version, pool names, etc.) I have no issue joining up this data without using the join-object library. I gave the app pools that'd be reused redundant names so that I could ideally just use "Get-Unique" after sorting or pass in -Unique to the sort operation. This seems to be ignored with a collection of hash tables (probably because they're inherently unique, so I'm a dummy). I've gone over this so many times now casting types, using add-member, etc. my code is horrible, so the most recent version is below. THANK YOU for any valid input.
$a = #()
$JoinedObject = Foreach ($SiteName in $Node.Sites)
{
$ConfigurationData.Sites | Where-Object {$_.Name -eq $SiteName[0]} | Foreach-Object{
$test =$_ | Select-object $_.PoolConfigName, $_.PoolName, $_.PoolRtVer | Sort-Object -Unique
}
foreach($i in $test)
{
if ($a -notcontains $i)
{
#write-host $i #, $i.GetType()
$a = $a + $i
}
}
#write-host $a
#Write-host $test, $test.Gettype()
#write-host $a[0] | select -Unique
#write-host $_.PoolName
#write-host $_.PoolRtVer
}
The output still contains duplicates
#{ApEssRo=; EssWebServicesRO=; v4.0=} #{ApEssW=; EssWebServices=; v4.0=} #{ApEssRo=; EssWebServicesRO=; v4.0=} #{ApEssRo=; EssWebServicesRO=; v4.0=} #{ApEssRo=; EssWebServicesRO=; v4.0=} #{ApEssRo=; EssWeb
ServicesRO=; v4.0=} #{ApEssRo=; EssWebServicesRO=; v4.0=} #{ApEssRo=; EssWebServicesRO=; v4.0=} #{ApEssRo=; EssWebServicesRO=; v4.0=} #{ApEssRo=; EssWebServicesRO=; v4.0=} #{ApEssRo=; EssWebServicesRO=; v4
.0=} #{ApEssRo=; EssWebServicesRO=; v4.0=} #{ApEssRo=; EssWebServicesRO=; v4.0=} #{ApEssRo=; EssWebServicesRO=; v4.0=} #{ApEssRo=; EssWebServicesRO=; v4.0=} #{ApEssW=; EssWebServices=; v4.0=} #{ApEssOhr1=;
EssWebServicesOhr1=; v4.0=}

I figured I didn't want to leave this as an open question since I came up with a "solution" (meaning, not great after tons of trial/error).
I iterate bind the first loop into a fullcollection, then iterate that to cast it to PSCustomObject to easily access/manipulate. Then iterate over that to get the distinct values by a property name. Then iterate over that to use those explicit values.
I hope those better versed in PowerShell fundamentals don't get whiplash from shaking their head.
$FullSiteCollection = #(#())
$PoolCollection = #(#())
$JoinedObject = Foreach ($SiteName in $Node.Sites)
{
$ConfigurationData.Sites | Where-Object {$_.Name -eq $SiteName[0]} | Foreach-Object{
#$test =$_ | Select-object $_.PoolConfigName, $_.PoolName, $_.PoolRtVer | Sort-Object -Unique
$test =$_ | select #{ Label="RunAs";Expression={$SiteName[1]}},#{ Label="PoolName";Expression={$_.PoolName}},#{ Label="PoolConfigName";Expression={$_.PoolConfigName}},#{ Label="PoolRtVer";Expression={$_.PoolRtVer}}
}
$FullSiteCollection+=$test
}
foreach($i in $FullSiteCollection)
{
$i = [pscustomobject]#{
RunAs=$i.RunAs
PoolName=$i.PoolName
PoolConfigName=$i.PoolConfigName
PoolRtVer=$i.PoolRtVer
}
if ($PoolCollection.PoolName -notcontains $i.Poolname)
{$PoolCollection = $PoolCollection + $i}
}
foreach($Pool in $PoolCollection)
{
#xWebAppPool $Pool.PoolConfigName (yadda yadda)

Related

Powershell - mySQL Query, error every other run

I am working on a powershell script that needs some input from a mySQL database. For the life of me I can't tell what I've done wrong here.
Every other time I run this script, I get an error Exception calling "Open" with "0" argument(s): "Out of sync with server"[0]. So, the first run, it will pull the expected data and dump it on my screen, then on the next run I get that error. And the cycle just repeats. Here is my full code (right now its just a test query to pull then dump the data. If it matters, the mySQL server is running MariaDB 10.3.14 on a Ubuntu 18.04 host.
$error.Clear()
$sqlQuery = get-content -path "C:\querytext.sql" -Raw
$sqlUser = "myuser"
$sqlPass = "mypass"
$sqlHost = "myserver"
$sqlDB = "dbname"
$connectionString = "server= $sqlHost;port=3306;uid=$sqlUser;pwd=$sqlPass;database=$sqlDB"
Try{
$connection = New-Object MySql.data.MySqlClient.MySqlConnection
$connection.ConnectionString = $connectionString
$connection.Open()
$command = New-Object MySql.data.MySqlClient.MySqlCommand($sqlQuery,$connection)
$dataAdapter = New-Object MySql.data.MySqlClient.MySqlDataAdapter($command)
$dataSet = New-Object System.Data.DataSet
$dataAdapter.fill($dataSet, "data") | Out-Null
$command.Dispose()
$sqlResults = $dataSet.tables["data"]
}
Catch {
Write-Host "ERROR : Unable to run query : $query `n$Error[0]"
}
$connection.close()
$sqlResults | Format-Table
$sqlResults | ForEach-Object {
write-host $_.fname
}
Might I suggest using the SQL PS module:
https://learn.microsoft.com/en-us/sql/powershell/download-sql-server-ps-module?view=sql-server-2017
That page has installation instructions and its from Microsoft. Personally, the dotnet class you are using, it works, but its relatively difficult to work with.
Connecting to a DB is much simpler with this module and you do not have to worry about micromanaging connections.
Invoke-Sqlcmd -ServerInstance $sqlHost -Query $sqlQuery -Database $sqlDB -Username $sqlUser -Password $sqlPass
This will return a PS object like every other PS cmdlet.

My Function will Shut Down or Start Up Multiple VMs

I am writing this function to ask a user what VM they want to use, they will have to choose from their own Hyper-V list. After that, it will then ask if I want it on or off. I can get the selected VM to turn on or off when I select it. However, when I run the function it will try to go through all the VM's in Hyper-V. How can I get it so it can just turn on or off the VM I want?
(I am also going to go back and add an option to save the VM if it is on.)
Function get-ORS {
param ($R, $O, $S, $name, $off, $on)
$name= Read-Host "What VM would you like to turn on use?"
$IO= Read-Host "Would you like to turn on/off your VM?: [On] [Off]"
$R= Get-VM | where {$_.State -eq 'Running'}
$O= Get-VM | where {$_.State -eq 'Off'}
$S= Get-VM | Where {$_.State -eq 'saved'}
$Off= Get-VM | where {$_.state -eq 'Running'} | Stop-VM
$on= Get-VM | where {$_.State -eq 'Off'} | Start-VM
if ($IO -eq $on) {
try
{
Start-VM -Name $name
}
catch
{
echo ("Could not start" + $name)
}
}
elseif ($O){
try
{
Start-VM -Name $name
echo ("Starting "+ $name)
}
catch
{
echo "Could not start the VM."
}
}
elseif ($R){
try
{
echo "Your VM is currently running."
}
catch
{
echo "Something went wrong."
}
}
elseif ($S) {
try
{
Start-VM -Name $name
echo ("Starting your from a saved state " + $name)
}
catch
{
echo "Could not start the saved VM."
}
}
if ($IO -eq $off){
try
{
Stop-VM -Name $name
}
catch
{
echo ("Could not turn off " + $name)
}
}
}
There is a lot wrong with this function / script. Especially regarding the logic (use case) and construct side of this effort. It's really a Frankenstein this, meaning, maybe, you are copying and pasting from different sources. The formatting makes this hard to follow as well.
You really need to step back an approach this one use case at a time to make sure you are going down the right path.
You have params, then more prompts for the same information. You have a variable in an if statement that is not in your param or prompt section so it will always be empty.
You are specifically asking for all VMs, ...
$R = Get-VM | where {$_.State -eq 'Running'}
$O = Get-VM | where {$_.State -eq 'Off'}
$S = Get-VM | Where {$_.State -eq 'saved'}
$Off = Get-VM | where {$_.state -eq 'Running'} | Stop-VM
$on = Get-VM | where {$_.State -eq 'Off'} | Start-VM
... but you are saying you only want one, but in your prompts you are asking for two.
$name = Read-Host "What VM would you like to turn on use?"
$IO = Read-Host "Would you like to turn on/off your VM?: [On] [Off]"
You also have these as params, so that's just odd. Only put in the param block what you want to pass or prompt for.
Why do you have a pram block, then have a bunch of Read-Host blocks?
That is redundant. You can do the Read-Host in the Param block as well. So, that you can pass the params all at once, or if one forgets to, prompts occur.
param
(
$name = (Read-Host "What VM would you like to turn on use?"),
$IO = (Read-Host "Would you like to turn on/off your VM?: [On] [Off]")
)
$R = Get-VM | where {$_.State -eq 'Running'}
$O = Get-VM | where {$_.State -eq 'Off'}
$S = Get-VM | Where {$_.State -eq 'saved'}
$Off = Get-VM | where {$_.state -eq 'Running'} | Stop-VM
$on = Get-VM | where {$_.State -eq 'Off'} | Start-VM
You can also, use a validate set to ensure the user only can select an option vs having then type it in.
You can also just show a list of VM's for the use to select from, by using dynamic parameter set or using Out-GridView for the user to select from.
You have a very long if statement which just a candidate for a switch statement to make it more concise.
You are passing all your guest names in your if statements. Why, when you say you only want one.
This comes off as this is all new to you. Which is fine, but t is worth taking a few quick free online session before jumping in. If that is the case then, see these discussions and Q & A's.
See these resources.
PowerShell Resources
Helping out someone with PowerShell
And this...
MS PowerShell Docs
IMHO, you should approach this a different way to make it more concise, easier to understand, extend, maintain and troubleshoot.
What you have could really be reduced to just this, from what you show now. Again, there are many ways to do X or Y, this is just one way.
Function Get-Ors
{
[cmdletbinding()]
[Alias('ors')]
param
(
)
$VMName = ( Get-VM |
Select-Object -Property Name, State, Status |
Out-GridView -OutputMode Single -Title 'Select a target VM you want to use.')
$IO = ( 'On','Off'|
Out-GridView -OutputMode Single -Title 'Select On or Off to start or stop the target VM.')
switch ($IO)
{
On {
"Attempting to start $($VMName.Name)"
Start-VM -Name $VMName
}
Off {
"Attempting to stop $($VMName.Name)"
Stop-VM -Name $VMName.Name
}
default {Write-Warning -Message "$($VMName.Name) state could not be determined"}
}
}
PowerShell will auto generate a response based on current state if not valid for the request.
If you don't care for the built-in Out-GridView, you could construct and use messagebox or WinForm or WPF or use PowerShell other PoorMan's GUI approach, using the Show-Command cmdlet. The PoorMan's GUI approach is not customizable.

Mask password in JSON to be imported by PowerShell

I'm importing values from JSON to PowerShell to use it as a connection string to SQL.
Here is the current JSON file I've:
{
"Databases": [{
"InstanceName": "test-01\\ins01",
"DatabaseName": "test_logs",
"User": "dba",
"Password": "P#ssw0rd"
}
]
}
Since one of these values is a password. I'd like to know how can I mask the password in the JSON file in the meantime stay able to retrieve it in PowerShell?
As that was being said before, you will have to use the ConvertFrom-SecureString & ConvertTo-SecureString powershell cmdlets.
Around this, there is 2 methods:
First idea, as explained on this blog, is to directly store the secured password to your Json file, like this:
$secure = ConvertTo-SecureString -String 'P#$$w0rd' -AsPlainText -Force
$cred = New-Object -typename PSCredential -ArgumentList #('company\admin',$secure)
$cred | Select Username,#{Name="Password";Expression = { $_.password | ConvertFrom-SecureString }} | Convertto-Json
And then convert the secured password back from your JSON file:
$file = Get-Content -Path c:\temp\admin.json | ConvertFrom-Json
$secure = ConvertTo-SecureString $file.Password -ErrorAction Stop
But you will be abble to uncrypt the SecureString only on the same computer.
So the 2nd method is to use the ConvertTo-SecureString and specify an AES file, as it's well explained on this other blog.
So, basically you create the AES file:
$KeyFile = "C:\AES.key"
$Key = New-Object Byte[] 16 # You can use 16, 24, or 32 for AES
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($Key)
$Key | out-file $KeyFile
And you store the secured password in your json, like above but with the -Key option:
$cred | Select Username,#{Name="Password";Expression = { $_.password | ConvertFrom-SecureString }} -key $Key | Convertto-Json
And the same way to revert back the secured password:
$secure = ConvertTo-SecureString $file.Password -Key $key -ErrorAction Stop
Hope this help.

Extraneous data returned from Invoke-Command

I'm working with PowerShell to gather data from a list of remote servers which I then turn into a JSON object. Everything is working fine, but I get some really weird output that I can't seem to exclude.
I've tried piping the Invoke-Command results and excluding properties. I've also tried removing the items manually from the returned hash file, but I can't seem to make them go away.
What am I missing?
EDIT:
For the sake of figuring out what's wrong here is a simplified, but still broken, script:
$returnedServer = #{}
$pass = cat "C:\...\securestring.txt" | convertto-securestring
$mycred = new-object -typename System.Management.Automation.PSCredential -argumentlist "UserName",$pass
$s = #("xx.xxx.xxx.xxx","xx.xxx.xxx.xxx")
foreach($server in $s)
{
$returnedServer.$server += ,(Invoke-Command -ComputerName $server -ScriptBlock
{
1
}-credential $mycred | select -ExcludeProperty PSComputerName,RunSpaceID,PSShowComputerName)
$returnedServer| ConvertTo-Json
Which outputs:
{
"xx.xxx.xxx.xxx": [
{
"value": 1,
"PSComputerName": "xx.xxx.xxx.xxx",
"RunspaceId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"PSShowComputerName": xxxx
}
],
"xx.xxx.xxx.xxx": [
{
"value": 1,
"PSComputerName": "xx.xxx.xxx.xxx",
"RunspaceId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"",
"PSShowComputerName": xxxx
}
]
}
This post is really old, but I was unable to find an acceptable answer 6 years later, so I wrote my own.
$invokeCommandResults | ForEach-Object {
$_.PSObject.Properties.Remove('PSComputerName')
$_.PSObject.Properties.Remove('RunspaceId')
$_.PSObject.Properties.Remove('PSShowComputerName')
}
You need to use Select-Object to limit the result to just the properties you want to show up in the JSON output:
$returnedServers.$server += ,(Invoke-Command -ComputerName $server -ScriptBlock
{
...$serverHash = various look ups and calculations...
$serverHash
} | select PropertyA, PropertyB, ...)
For a more thorough answer you need to go into far more detail about your "various look ups and calculations" as well as the actual conversion to JSON.
After some testing, it seems the problem is the object type. I was able to get your test script to work by explicitly casting the returned result.
$returnedServer = #{}
$pass = cat "C:\...\securestring.txt" | convertto-securestring
$mycred = new-object -typename System.Management.Automation.PSCredential -argumentlist "UserName",$pass
$s = #("xx.xxx.xxx.xxx","xx.xxx.xxx.xxx")
foreach($server in $s)
{
$returnedServer.$server += ,[int](Invoke-Command -ComputerName $server -ScriptBlock {1} -credential $mycred)
}
$returnedServer| ConvertTo-Json
You could try this... instead of attempting to exclude extraneous property values, just be specific and "call" or "grab" the one(s) you want.
Quick Code Shortcut Tip! BTW, the Invoke-Command -Computer $server -Scriptbock {command} can be greatly simplified using: icm $server {command}
Now, getting back on track...
Using your original post/example, it appears that you are attempting to utilize one "value" by excluding all other values, i.e. -ExcludeProperty (which it is ultra-frustrating).
Let's start by removing and replacing the only exclusion section:
select -ExcludeProperty PSComputerName,RunSpaceID,PSShowComputerName
And instead, attempt to use one of the following:
1st Method: using the modified original command...
$returnedServer.$server += ,(Invoke-Command -ComputerName $server -ScriptBlock {1}-credential $mycred).value
2nd Method: using the "icm" version...
$returnedServer.$server += ,(icm $server {1} -credential $mycred).value
Essentially, you are "picking out" the value(s) you need (vs. excluding property values, which is, again, pretty frustrating when it does NOT work).
Related Example(s) follows:
Here is a typical system Powershell/WMIC command call:
icm ServerNameGoesHere {Get-CimInstance -ClassName win32_operatingsystem}
But what if I only want the "version" from the object glob:
(icm ServerNameGoesHere {Get-CimInstance -ClassName win32_operatingsystem}).version
But, hold on, now I only want the "lastbootuptime" from the object glob:
(icm ServerNameGoesHere {Get-CimInstance -ClassName win32_operatingsystem}).lastbootuptime
Indecisively, I want to be more flexible:
$a=icm ServerNameGoesHere {Get-CimInstance -ClassName win32_operatingsystem}
$a.version
$a.lastbootuptime
$a.csname
(Makes sense?)
Good luck,
~PhilC

Read SQL Agent Error Logs With Powershell

I can easily read the SQL Error Logs using the below code but cannot find a way to read the Agent Error Logs. Does anyone know if it is possible?
$sqlServer = new-object ("Microsoft.SqlServer.Management.Smo.Server") $server
$sqlServer.ReadErrorLog()
I've searched through the namespaces and there isn't anything obvious pointing me to the logs. I have tried this without success.
$sqlAgent = new-object ("Microsoft.SqlServer.Management.Smo.Agent") $server
$sqlAgent.ReadErrorLog()
#Pondlife was correct. For anyone else facing this problem, this worked for my purposes.
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null
$sqlServer = new-object ("Microsoft.SqlServer.Management.Smo.Server") $server
$jobServer = $sqlServer.JobServer;
$jobServer.ReadErrorLog() |
where { ($_.ErrorLevel -lt 3) -and ($_.LogDate -ge $(Get-Date).AddDays($EventLogDaysToReview)) } |
Format-Table -AutoSize -wrap