PowerShell Converting $null into 0 - powershell

I have a pretty straight-forward function for grabbing SQL results:
function RunSqlCommand($sql)
{
$connection = $null
$command = $null
try
{
$connectionString = "data source=localhost; integrated security=true"
$connection = New-Object System.Data.SqlClient.SqlConnection($connectionString)
$command = $connection.CreateCommand()
$command.CommandText = $sql
$adapter = New-Object System.Data.SqlClient.SqlDataAdapter($command)
$dataSet = New-Object System.Data.DataSet
$adapter.Fill($dataSet)
$results = $dataSet.Tables | Select-Object -ExpandProperty "Rows"
return $results
}
finally
{
if ($command -ne $null) { $command.Dispose() }
if ($connection -ne $null) { $connection.Dispose() }
}
}
Whenever there are no results, the $results variable is $null. However, when I inspect the return value in the calling method, it magically becomes 0.
Is PowerShell doing something behind the scenes? I really do want to return $null to represent "no results".

$adapter.Fill() returns the number of rows added or refreshed in the dataset.
To fix, you can do this:
[void]$adapter.Fill($dataSet)
or
$adapter.Fill($dataset) | out-null

Related

Parameterize a debug variable and insert it into the rest of the functions

$isDebug currently exists as a variable in the CleanStaleDevices function,
I need to make the variable into a optional parameter with a default value of $false.
Add the same parameter to the remaining functions.
I would like to set $isDebug as a variable at the initial function call, where CleanStaleDevices is called, then you can pass the value you set down the chain to the other functions. However, if you want to isolate debugging to an individual function you can override.
I tried converting $idDebug to a function itself but it didn't behave the way I was expecting
`function CleanStaleDevices($numDays) {
##Set debug flag
$isDebug = $true
#######Begin
Connect-AzureAD
###Get-AzureADDevice to get list of devices stale by 3 months and make sure devices aren't autopilot managed###
$dt = (Get-Date).AddDays($numDays)
$devices = Get-AzureADDevice -All:$true | Where-Object { $_.ApproximateLastLogonTimeStamp -le $dt } | select-object -Property AccountEnabled, DeviceId, DeviceOSType, DeviceOSVersion, DisplayName, DeviceTrustType, IsManaged, ApproximateLastLogonTimestamp
$fileNameDate = Get-Date -format "yyyyddMM"
$fileNameMiddle = "_AAD_Stale_Devices-"
##Get Intune devices
$intuneDevicesFileName = "$($fileNameDate)$($fileNameMiddle)Intune.csv"
$devices | ? { $_.IsManaged -eq $true } | Export-Csv c:\temp\$intuneDevicesFileName -NoTypeInformation
#######OUTSIDE ACTION#######
##Use Invoke-Device script to retire devices from generated CSV
##https://www.niallbrady.com/2017/08/23/getting-started-with-microsoft-graph-and-using-powershell-to-automate-things-in-intune/
#######END##################
$confirmation = Read-Host "Have you retired the detected Intune devices (y/n)?"
if ($confirmation -eq 'y') {
$hybridDevicesFileName = "$($fileNameDate)$($fileNameMiddle)Hybrid.csv"
$devices | ? { $_.DeviceTrustType -eq "ServerAd" } | Export-Csv c:\temp\$hybridDevicesFileName -NoTypeInformation
#######OUTSIDE ACTION#######
##Use DisableADComputersFromCSV.ps1 to disable the devices generated in the hybrid device csv
#######END##################
$confirmation = Read-Host "Have you disabled the on prem devices and ran a directory sync (y/n)?"
if ($confirmation -eq 'y') {
# proceed
###We don't have to open a new connection for each loop of the devices
$Connection = New-Object System.Data.SQLClient.SQLConnection
$serverName = "XXXXXXX"
$databaseName = "XXXXXXX"
$Connection.ConnectionString = "server='$serverName';database='$databaseName';trusted_connection=true;"
$Connection.Open()
if ($isDebug -eq $true) {
##debug array
$devicesweredisabled = #()
}
##Sort on DeviceTrustType so that AD Joined devices are disabled before AD registered
$devices = $devices | Sort-Object DeviceTrustType
foreach ($device in $devices) {
#if device is enabled
if ($device.AccountEnabled -eq $true) {
if ($isDebug -eq $true) {
##write to debug array
$devicesweredisabled += $device
Write-Host "Disable device" $device.DeviceId $device.DisplayName $device.AccountEnabled
}
else {
##don't disable Hybrid devices since they were disabled earlier
if ($_.DeviceTrustType -ne "ServerAd")
{
Write-Host "DISABLE " $device.DisplayName "with trust type" $device.DeviceTrustType
#$device.AccountEnabled = $false
}
##Check to see if device is in table already, if so check how long its been there
$disabledDateTimeValue = CheckIfDeviceISInTable $device.DeviceId $Connection
if ($null -eq $disabledDateTimeValue) {
##Device hasn't been added to table
WriteDisabledAADDevice $device.DeviceId $device.DisplayName $isDebug $Connection
}
}
}
else {
#device is not enabled
##Check to see if device is in table already, if so check how long its been there
$disabledDateTimeValue = CheckIfDeviceISInTable $device.DeviceId $Connection
if ($null -eq $disabledDateTimeValue) {
##Device is already disabled so we need to add it to the table for time tracking
WriteDisabledAADDevice $device.DeviceId $device.DisplayName $isDebug $Connection
}
else {
$numDaysSince = (Get-Date) - [datetime]$disabledDateTimeValue | % days
if ($numDaysSince -ge 90) {
if ($isDebug -eq $true) {
Write-Host "DEVICE WAS DELETED" $device.DeviceId $device.DisplayName $device.AccountEnabled
}
else {
Write-Host "DELETED..."
#device has been in table for 90 days so delete
##delete device
}
}
}
}
}
$Connection.Close()
if ($isDebug -eq $true) {
$devicesweredisabled | export-csv c:\temp\devicelist-olderthan-x-days-disabledtest.csv -NoTypeInformation
}
}
else{
Exit
}
}
else{
Exit
}
}
function WriteDisabledAADDevice {
Param(
[parameter(position = 3)]
$conn,
[parameter(position = 2)]
$logOnly,
[parameter(position = 1)]
$displayName,
[parameter(position = 0)]
$deviceId
)
$newId = (New-Guid).Guid
$disableDateTime = (Get-Date)
$Command = New-Object System.Data.SQLClient.SQLCommand
$Command.Connection = $conn
$tableName = "DisabledAADDevice"
$insertquery = "
INSERT INTO $tableName
([DisabledAADDeviceId],[AADDeviceId],[DiplayName],[DisabledDateTime])
VALUES
('$newId','$deviceId','$displayName','$disableDateTime')"
if ($logOnly -eq $true) {
Write-Host $insertquery
}
else {
$Command.CommandText = $insertquery
$Command.ExecuteNonQuery()
}
}
function CheckIfDeviceISInTable {
Param(
[parameter(position = 1)]
$conn,
[parameter(position = 0)]
$deviceId
)
$Command = New-Object System.Data.SQLClient.SQLCommand
$Command.Connection = $conn
$tableName = "DisabledAADDevice"
$selectQuery = "
Select [DisabledDateTime] FROM $tableName
WHERE [AADDeviceId] = '$deviceId'"
$Command.CommandText = $selectQuery
$resultValue = $Command.ExecuteScalar()
return $resultValue
}
CleanStaleDevices(-365)`

Parallel run for this particular powershell script

I am in the process of re-writing the script below to be able to run in parallel, as can be seen in the code, an array of servers is passed to the script, and then it loads it onto a hash table, loops through each server at a time to do the deployment, for each server there are files to execute in a particular order (see array of files). Looking at the structure, I feel workspace is the way to go here but I could be wrong.
Where the performance gains can be seen in my opinion or having the code such that multiple servers can be executed at thesame time rather than waiting for each server to complete and move onto the next one. foreach parallel
I ran a test to call a function declared outside a workspace, it worked.Is this good practice to call a function declared outside a workspace ? I ask this because I would like to reuse some functions outside the workspace, or is it generally better to put all the code in the workspace even ones that are not intended for parallel workloads i.e one off calls to the code. ?
The below is the code I am testing with.
Function Check-Instance-Connection{
param
(
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
$sql_server,
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=1)]
$db_name
)
try
{
#Return extra useful info by using custom objects
$check_outcome = "" | Select-Object -Property log_date, stage, status, error_message
$check_outcome.log_date = (Get-Date)
$check_outcome.stage = 'Ping SQL instance for $sql_server'
#test connection for a sql instance
$connectionstring = "Data Source=$sql_server;Integrated Security =true;Initial Catalog=$db_name;Connect Timeout=5;"
$sqllconnection = New-Object System.Data.SqlClient.SqlConnection $connectionstring
$sqllconnection.Open();
$check_outcome.status = $true
$check_outcome.error_message = ''
return $check_outcome
}
Catch
{
$check_outcome.status = $false
$check_outcome.error_message = $_.Exception.Message
return $check_outcome
}
finally{
$sqllconnection.Close();
}
}
$file_list = #("deployment_1.sql","deployment_2.sql","deployment_3.sql","deployment_4.sql","deployment_5.sql")
$x = (1,"Server1",3,1),(4,"Server2",6,2),(3,"Server3",4,3)
$k = 'serverid','servername','locationid','appid' # key names correspond to data positions in each array in $x
$h = #{}
For($i=0;$i -lt $x[0].length; $i++){
$x |
ForEach-Object{
[array]$h.($k[$i]) += [string]$_[$i]
}
}
$folder = "F:\Files\"
$database_name = "Test"
foreach ($server_id in $all_server_ids)
{
$severid = $h["serverid"][$all_server_ids.indexof($server_id)]
$servername = $h["servername"][$all_server_ids.indexof($server_id)]
$locationid = $h["locationid"][$all_server_ids.indexof($server_id)]
$message = 'ServerID {0} has a servername of {1} and a location id of {2}' -f $server_id, $h["servername"][$all_server_ids.indexof($server_id)],$h["locationid"][$all_server_ids.indexof($server_id)]
Write-Output $message
Write-Output "This $severid and this $servername and this $locationid"
foreach ($file in $file_list)
{
$is_instance_ok = Check-Instance-Connection $servername $database_name
if ($is_instance_ok.check_outcome -eq $true){
invoke-sqlcmd -ServerInstance "$servername" -inputfile $folder$file -Database "$database_name" -Querytimeout 60 -OutputSqlErrors $true -ConnectionTimeout 10 -ErrorAction Continue -Errorvariable generated_error | Out-Null
}
}
}
Thanks, I did a lot more research and looked at a lot of examples on how workflows work. This is what I have come up with.
Workflow RunExecution
{
Function Check-Instance-Connection{
param
(
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
$sql_server,
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=1)]
$db_name
)
try
{
#Return extra useful info by using custom objects
$check_outcome = "" | Select-Object -Property log_date, stage, status, error_message
$check_outcome.log_date = (Get-Date)
$check_outcome.stage = 'Ping SQL instance for $sql_server'
#test connection for a sql instance
$connectionstring = "Data Source=$sql_server;Integrated Security =true;Initial Catalog=$db_name;Connect Timeout=5;"
$sqllconnection = New-Object System.Data.SqlClient.SqlConnection $connectionstring
$sqllconnection.Open();
$check_outcome.status = $true
$check_outcome.error_message = ''
return $check_outcome
}
Catch
{
$check_outcome.status = $false
$check_outcome.error_message = $_.Exception.Message
return $check_outcome
}
finally{
$sqllconnection.Close();
}
}
$file_list = #("deployment_1.sql","deployment_2.sql","deployment_3.sql","deployment_4.sql","deployment_5.sql")
$x = (1,"server1\DEV3",3,1),(4,"serer1\DEV2",6,2),(3,"serer2\DEV1",4,3)
$k = 'serverid','servername','locationid','appid'
$h = #{}
For($i=0;$i -lt $x[0].length; $i++){
$x |
ForEach-Object{
[array]$h.($k[$i]) += [string]$_[$i]
}
}
$folder = "C:\Temp\"
$database_name = "Test"
$all_server_ids = $h['serverid']
foreach -parallel ($server_id in $all_server_ids)
{
$severid = $h["serverid"][$all_server_ids.indexof($server_id)]
$servername = $h["servername"][$all_server_ids.indexof($server_id)]
$locationid = $h["locationid"][$all_server_ids.indexof($server_id)]
foreach ($file in $file_list)
{
# $check_fine = $is_instance_ok.check_outcome
# if ($check_fine = $true){
invoke-sqlcmd -ServerInstance "$servername" -inputfile $folder$file -Database "$database_name" -Querytimeout 60 -OutputSqlErrors $true -ConnectionTimeout 10 -ErrorAction Continue
write-output "invoke-sqlcmd -ServerInstance $servername -inputfile $folder$file -Database $database_name -Querytimeout 60 -OutputSqlErrors $true -ConnectionTimeout 10 -ErrorAction Continue "
# }
}
}
}
RunExecution

Access datarow from a workflow

I have a script which look like below:
function Invoke-SQL {
param(
[string] $dataSource = ".\MSSQLSERVER",
[string] $database = "master",
[string] $sqlCommand = $(throw "Please specify a query.")
)
$connectionString = "Data Source=$dataSource; Integrated Security=True; Initial Catalog=$database; Connect Timeout=100"
$connection = new-object system.data.SqlClient.SQLConnection($connectionString)
$command = new-object system.data.sqlclient.sqlcommand($sqlCommand,$connection)
Try {
$connection.Open()
} catch {
return, $null
}
$adapter = New-Object System.Data.sqlclient.sqlDataAdapter $command
$dataset = New-Object System.Data.DataSet
$adapter.Fill($dataSet) | Out-Null
$connection.Close()
$dataSet.Tables
}
[string]$SQL = "select CmsSrvName from CmsServerList_VM"
$DbInstances = Invoke-SQL "DBSRV" "Test" $SQL
workflow wf1{
Param([System.Data.DataTable]$instance)
ForEach -Parallel -ThrottleLimit 10 ($i in $instance) {
InlineScript{
$t = $using:i
Write-Verbose "$t['CmsSrvName']"
}
}
}
wf1 -Verbose $DbInstances
Output:
VERBOSE: [localhost]:System.Data.DataRow['CmsSrvName']
VERBOSE: [localhost]:System.Data.DataRow['CmsSrvName']
VERBOSE: [localhost]:System.Data.DataRow['CmsSrvName']
The output is not what I expected, it just print out the type name not the value. How can I access the DataRow value in a workflow?(in Powershell 5)
Thanks in advance for the help

Powershell DirectorySearcher Null Output

I'm writing a powershell script that searches for users inside an Active Directory OU and allows me to reset passwords by choosing matches from a list. I found a Tutorial that uses the System.DirectoryServices.DirectoryEntry and System.DirectoryServices.DirectorySearcher, and modified it like so:
$objDomain = New-Object System.DirectoryServices.DirectoryEntry("LDAP:\\[REDACTED]")
##ReadSTDIN
$strSearch = Read-Host -Prompt "Search"
$strCat = "(&(objectCategory=User)(Name=*" + $strSearch + "*))"
## Search Object
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.SearchRoot = $objDomain
$objSearcher.PageSize = 1000
$objSearcher.Filter = $strCat
$objSearcher.SearchScope = "Subtree"
#Load Required Properties into the dynObjLink
$objSearcher.PropertiesToLoad.Add("name")
$objSearcher.PropertiesToLoad.Add("userPrincipalName")
$objSearcher.PropertiesToLoad.Add("SamAccountName")
##Magical Search Function
$colResults = $objSearcher.FindAll()
$colResults.PropertiesLoaded
#for every returned userID add them to a table
ForEach ($objResult in $colResults)
{$a++
$objResult.count
$objItem = $objResult.Properties
$objItem.name
$objItem.userPrincipalName
$results.Add($a, $objItem.name + $objItem.userPrincipalName + $objItem.SamAccountName)
}
#Print Table
$results | Format-Table -AutoSize
This works well enough, but when it prints data I can only get the "first name" value of anything that comes back. Everything else becomes NULL and I can't figure out why.
Name Value
---- -----
3 {James3 [REDACTED], $null, $null}
2 {James2 [REDACTED], $null, $null}
1 {James1 [REDACTED], $null, $null}
I've tried different kinds of authentication and manipulating values, but the DirectorySearcher object only seems to collect the "name" value of any record it returns, no matter what I load into it. Help?
Here's a bit shorter (and PowerShell v2-compatible) way of doing this:
#requires -version 2
param(
[Parameter(Mandatory=$true)]
[String] $SearchPattern
)
$searcher = [ADSISearcher] "(&(objectClass=user)(name=$SearchPattern))"
$searcher.PageSize = 1000
$searcher.PropertiesToLoad.AddRange(#("name","samAccountName","userPrincipalName"))
$searchResults = $searcher.FindAll()
if ( $searchResults.Count -gt 0 ) {
foreach ( $searchResult in $searchResults ) {
$properties = $searchResult.Properties
$searchResult | Select-Object `
#{Name = "name"; Expression = {$properties["name"][0]}},
#{Name = "sAMAccountName"; Expression = {$properties["samaccountname"][0]}},
#{Name = "userPrincipalName"; Expression = {$properties["userprincipalname"][0]}}
}
}
$searchResults.Dispose()
Note that there's no need to build a list and output afterwards. Just output each search result. Put this code in a script file and call it:
PS C:\Scripts> .\Searcher.ps1 "*dyer*"
If you omit the parameter, PowerShell will prompt you for it (because the parameter is marked as mandatory).
try using Properties matching to the PropertiesToLoad
$entry = new-object -typename system.directoryservices.directoryentry -ArgumentList $LDAPServer, "ldap", "esildap"
$entry.Path="LDAP://OU=childOU,OU=parentOU,DC=dc1,DC=dc2"
$searcher = new-object -typename system.directoryservices.directorysearcher -ArgumentList $entry
$searcher.PropertiesToLoad.Add('samaccountname')
$searcher.PropertiesToLoad.Add('mail')
$searcher.PropertiesToLoad.Add('displayname')
$objs = $searcher.findall()
foreach($data in $objs)
{
$samaccountname = $data.properties['samaccountname'][0] + ''
$mail = $data.properties['mail'][0] + ''
$displayname = $data.properties['displayname'][0] + ''
}
when accessing the properties of the resultset you get a System.DirectoryServices.ResultPropertyValueCollection type for each property
to get a string value for passing to a database the property value access the zero index of the object

Populating a datagridview

I ma working on a form that will search all connected drives for PST files.
I can get it working with the following command:-
Get-PSDrive -PSProvider "filesystem"|%{get-childitem $_.root -include *.pst -r}|select name, directoryname, #{name="Size (GB)";expression ={"{0:N2}" -f ($_.length/1GB)}}
The only problem is it takes about 45 minutes to run through all the drives and finish the search. I was thinking of trying to speed it up by using the windows search index.
I have got this....
function Searchindex{
$query="SELECT System.ItemName, system.ItemPathDisplay, System.ItemTypeText, System.Size FROM SystemIndex where system.itemtypetext = 'outlook data file'"
$objConnection = New-Object -ComObject adodb.connection
$objrecordset = New-Object -ComObject adodb.recordset
$objconnection.open("Provider=Search.CollatorDSO;Extended Properties='Application=Windows';")
$objrecordset.open($query, $objConnection)
$array=#()
Try { $objrecordset.MoveFirst() }
Catch [system.exception] { "no records returned" }
do
{
Write-host ($objrecordset.Fields.Item("System.ItemName")).value `
($objrecordset.Fields.Item("System.ItemPathDisplay")).value `
($objrecordset.Fields.Item("System.ITemTypeText")).value `
($objrecordset.Fields.Item("System.Size")).value
if(-not($objrecordset.EOF)) {$objrecordset.MoveNext()}
} Until ($objrecordset.EOF)
$objrecordset.Close()
$objConnection.Close()
$objrecordset = $null
$objConnection = $null
[gc]::collect()
}
this outputs the details to the screen in a few seconds which is perfect but I can't work out how to display it in a datagrid view.
I am using primal form to create the forms.
Once the data is populated in the datagridview I want to be able to select records and copy them to a new location
Can anyone help?
TIA
Andy
I'm not familiar with DataGridView but I feel if you had an object you would be better capable to manipulate it.
function Searchindex{
$query="SELECT System.ItemName, system.ItemPathDisplay, System.ItemTypeText, System.Size FROM SystemIndex where system.itemtypetext = 'outlook data file'"
$objConnection = New-Object -ComObject adodb.connection
$objrecordset = New-Object -ComObject adodb.recordset
$objconnection.open("Provider=Search.CollatorDSO;Extended Properties='Application=Windows';")
$objrecordset.open($query, $objConnection)
$array=#()
Try { $objrecordset.MoveFirst() }
Catch [system.exception] { "no records returned" }
do
{
$array += [pscustomobject]#{
Name = ($objrecordset.Fields.Item("System.ItemName")).value
Path = ($objrecordset.Fields.Item("System.ItemPathDisplay")).value
TypeText = ($objrecordset.Fields.Item("System.ITemTypeText")).value
Size = ($objrecordset.Fields.Item("System.Size")).value
}
If(-not($objrecordset.EOF)) {$objrecordset.MoveNext()}
} Until ($objrecordset.EOF)
$objrecordset.Close()
$objConnection.Close()
$objrecordset = $null
$objConnection = $null
[gc]::collect()
$array
}
This will send out a custom PowerShell object array. You already had the variable $array initialized. We just needed to populate it.
Then you could use something like this to filter out the files you are looking for.
Searchindex | Out-GridView -PassThru
After hitting Ok it will only output the records selected.
DataGridView
with multiselect and return
$global:results = #()
#...searchindex function is here ....
$form = New-Object System.Windows.Forms.Form
$form.Size = New-Object System.Drawing.Size(900,600)
$dataGridView = New-Object System.Windows.Forms.DataGridView
$dataGridView.Size=New-Object System.Drawing.Size(800,400)
$dataGridView.SelectionMode = 'FullRowSelect'
$dataGridView.MultiSelect = $true
$go = New-Object System.Windows.Forms.Button
$go.Location = New-Object System.Drawing.Size(300,450)
$go.Size = New-Object System.Drawing.Size(75,23)
$go.text = "Select"
$form.Controls.Add($go)
$form.Controls.Add($dataGridView)
$arraylist = New-Object System.Collections.ArrayList
$arraylist.AddRange((Searchindex))
$dataGridView.DataSource = $arraylist
$dataGridView.Columns[0].width = 240
$go.Add_Click(
{
$dataGridView.SelectedRows| ForEach-Object{
$global:results += [pscustomobject]#{
Name = $dataGridView.Rows[$_.Index].Cells[0].Value
Path = $dataGridView.Rows[$_.Index].Cells[1].Value
TypeText = $dataGridView.Rows[$_.Index].Cells[2].Value
Size = $dataGridView.Rows[$_.Index].Cells[3].Value
}
$form.Close()
}
})
$form.ShowDialog()
$global:results
There is a lot to cover here but look at the examples and let me know how this works for you. It will return all selected rows back as objects in the global variable $global:results. It needs to be global as output does not persist outside the $go.Add_Click. The searchindex function is there but omitted in the second code sample to save space.