Unexpected variable type returned by Receive-Job - sql-server

I'm trying to execute the Invoke-Sqlcmd command (from the SqlServer module) to run a query as a different AD user. I know there's the -Credential argument, but that doesn't seem to work.
Thus, I thought using Start-Job might be an option, as shown in the snippet below.
$username = 'dummy_domain\dummy_user'
$userpassword = 'dummy_pwd' | ConvertTo-SecureString -AsPlainText -Force
$credential = New-Object -TypeName System.Management.Automation.PSCredential ($username, $password)
$job = Start-Job -ScriptBlock {Import-Module SqlServer; Invoke-Sqlcmd -query "exec sp_who" -ServerInstance 'dummy_mssql_server' -As DataSet} -Credential $credential
$data = Receive-Job -Job $job -Wait -AutoRemoveJob
However, when looking at the variable type that the job returned, it isn't what I expected.
> $data.GetType().FullName
System.Management.Automation.PSObject
> $data.Tables[0].GetType().FullName
System.Collections.ArrayList
If I run the code in the ScriptBlock directly, these are the variable types that PS returns:
> $data.GetType().FullName
System.Data.DataSet
> $data.Tables[0].GetType().FullName
System.Data.DataTable
I tried casting the $data variable to [System.Data.DataSet], which resulted in the following error message:
Cannot convert value "System.Data.DataSet" to type "System.Data.DataSet".
Error: "Cannot convert the "System.Data.DataSet" value of type
"Deserialized.System.Data.DataSet" to type "System.Data.DataSet"."
Questions:
Is there a better way to run SQL queries under a different AD account, using the Invoke-Sqlcmd command?
Is there a way to get the correct/expected variable type to be returned when calling Receive-Job?
Update
When I run $data.Tables | Get-Member, one of the properties returned is:
Tables Property Deserialized.System.Data.DataTableCollection {get;set;}

Is there a way to get the correct/expected variable type to be returned when calling Receive-Job?
Due to using a background job, you lose type fidelity: the objects you're getting back are method-less emulations of the original types.
Manually recreating the original types is not worth the effort and may not even be possible - though perhaps working with the emulations is enough.
Update: As per your own answer, switching from working with System.DataSet to System.DataTable resulted in serviceable emulations for you.[1]
See the bottom section for more information.
Is there a better way to run SQL queries under a different AD account, using the Invoke-Sqlcmd command?
You need an in-process invocation method in order to maintain type fidelity, but I don't think that is possible with arbitrary commands if you want to impersonate another user.
For instance, the in-process (thread-based) alternative to Start-Job - Start-ThreadJob - doesn't have a -Credential parameter.
Your best bet is therefore to try to make Invoke-SqlCmd's -Credential parameter work for you or find a different in-process way of running your queries with a given user's credentials.
Serialization and deserialization of objects in background jobs / remoting / mini-shells:
Whenever PowerShell marshals objects across process boundaries, it employs XML-based serialization at the source, and deserialization at the destination, using a format known as CLI XML (Common Language Infrastructure XML).
This happens in the context of PowerShell remoting (e.g., Invoke-Command calls with the
-ComputerName parameter) as well as in background jobs (Start-Job) and so-called mini-shells (which are implicitly used when you call the PowerShell CLI from inside PowerShell itself with a script block; e.g., powershell.exe { Get-Item / }).
This deserialization maintains type fidelity only for a limited set of known types, as specified in MS-PSRP, the PowerShell Remoting Protocol Specification. That is, only instances of a fixed set of types are deserialized as their original type.
Instances of all other types are emulated: list-like types become [System.Collections.ArrayList] instances, dictionary types become [hasthable] instances, and other types become method-less (properties-only) custom objects ([pscustomobject] instances), whose .pstypenames property contains the original type name prefixed with Deserialized. (e.g., Deserialized.System.Data.DataTable), as well as the equally prefixed names of the type's base types (inheritance hierarchy).
Additionally, the recursion depth for object graphs of non-[pscustomobject] instances is limited to 1 level - note that this includes instance of PowerShell custom classes, created with the class keyword: That is, if an input object's property values aren't instance of well-known types themselves (the latter includes single-value-only types, including .NET primitive types such as [int], as opposed to types composed of multiple properties), they are replaced by their .ToString() representations (e.g., type System.IO.DirectoryInfo has a .Parent property that is another System.IO.DirectoryInfo instance, which means that the .Parent property value serializes as the .ToString() representation of that instance, which is its full path string); in short: Non-custom (scalar) objects serialize such that property values that aren't themselves instances of well-known types are replaced by their .ToString() representation; see this answer for a concrete example.
By contrast, explicit use of CLI XML serialization via Export-Clixml defaults to a depth of 2 (you can specify a custom depth via -Depth and you can similarly control the depth if you use the underlying System.Management.Automation.PSSerializer type directly).
Depending on the original type, you may be able to reconstruct instances of the original type manually, but that is not guaranteed.
(You can get the original type's full name by calling .pstypenames[0] -replace '^Deserialized\.' on a given custom object.)
Depending on your processing needs, however, the emulations of the original objects may be sufficient.
[1] Using System.DataTable results in usable emulated objects, because you get a System.Collections.ArrayList instance that emulates the table, and custom objects with the original property values for its System.DataRow instances. The reason this works is that PowerShell has built-in logic to treat System.DataTable implicitly as an array of its data rows, whereas the same doesn't apply to System.DataSet.

I can't say for question 2 as I've never used the job commands but when it comes to running the Invoke-Sqlcmd I always make sure that the account that runs the script has the correct access to run the SQL.
The plus to this is that you don't need to store the credentials inside the script, but is usually a moot point as the scripts are stored out of reach of most folks, although some bosses can be nit picky!
Out of curiosity how do the results compare if you pipe them to Get-Member?

For those interested, below is the code I implemented. Depending on whether or not $credential is passed, Invoke-Sqlcmd will either run directly, or using a background job.
I had to use -As DataTables instead of -As DataSet, as the latter seems to have issues with serialisation/deserialisation (see accepted answer for more info).
function Exec-SQL($server, $database, $query, $credential) {
$sqlData = #()
$scriptBlock = {
Param($params)
Import-Module SqlServer
return Invoke-Sqlcmd -ServerInstance $params.server -Database $params.database -query $params.query -As DataTables -OutputSqlErrors $true
}
if ($PSBoundParameters.ContainsKey("credential")) {
$job = Start-Job -ScriptBlock $scriptBlock -Credential $credential -ArgumentList $PSBoundParameters
$sqlData = Receive-Job -Job $job -Wait -AutoRemoveJob
} else {
$sqlData = & $scriptBlock -params $PSBoundParameters
}
return $sqlData
}

Related

How to work with an object array in powershell [duplicate]

This is how my current script looks like:
$cpu = Get-WmiObject win32_processor | select LoadPercentage
logwrite $cpu #this fuction writes $cpu into a .txt file
The output of the file is:
#{LoadPercentage=4}
I want it to be only the number so that I can make calculations.
qbanet359's helpful answer uses direct property access (.LoadPercentage) on the result object, which is the simplest and most efficient solution in this case.
In PowerShell v3 or higher this even works with extracting property values from a collection of objects, via a feature called member-access enumeration.
E.g., ((Get-Date), (Get-Date).AddYears(-1)).Year returns 2019 and 2018 when run in 2019, which are the .Year property values from each [datetime] instance in the array.
In cases where you do want to use Select-Object (or its built-in alias, select), such as when processing a large input collection item by item:
To use Select-Object to extract a single property value, you must use -ExpandProperty:
Get-WmiObject win32_processor | Select-Object -ExpandProperty LoadPercentage
Background:
Select-Object by default creates custom objects ([pscustomobject] instances[1]
) that have the properties you specify via the -Property parameter (optionally implicitly, as the 1st positional argument).
This applies even when specifying a single property[2], so that select LoadPercentage (short for: Select-Object -Property LoadPercentage) creates something like the following object:
$obj = [pscustomobject] #{ LoadPercentage = 4 } # $obj.LoadPercentage yields 4
Because you use Add-Content to write to your log file, it is the .ToString() string representation of that custom object that is written, as you would get if you used the object in an expandable string (try "$([pscustomobject] #{ LoadPercentage = 4 })").
By contrast, parameter -ExpandProperty, which can be applied to a single property only, does not create a custom object and instead returns the value of that property from the input object.
Note: If the value of that property happens to be an array (collection), its elements are output individually; that is, you'll get multiple outputs per input object.
[1] Strictly speaking, they're [System.Management.Automation.PSCustomObject] instances, whereas type accelerator [pscustomobject], confusingly, refers to type [System.Management.Automation.PSObject], for historical reasons; see this GitHub issue.
[2] There's a hotly debated request on GitHub to change Select-Object's default behavior with only a single property; while the discussion is interesting, the current behavior is unlikely to change.
That is a pretty simple fix. Instead of selecting the LoadPercentage when running Get-WmiObject, just select the property when calling your function. This will write only the number to your log file.
$cpulogpath = "C:\Monitoring\$date.csv"
function logwrite
{
param ([string]$logstring)
add-content $cpulogpath -value $logstring
}
$cpu = Get-WmiObject win32_processor #don't select the property here
logwrite $cpu.LoadPercentage #select it here

Why is my ArrayList throwing a "PSCustomObject doesn't contain a method for 'foreach'" error when the ArrayList contains <= 1 item, & only in PS 5?

I have the following script I wrote using PowerShell 5 that utilizes the Active Directory and Join-Object PowerShell modules to get a list of all AD Groups and their users (along with some additional properties per user like their manager and title):
$ADGroupsList = #(Get-ADGroup -Filter * -Properties * | Select-Object DistinguishedName,CN,GroupCategory,Description | Sort-Object CN)
#I'm using an ArrayList here so that later on I can use the .Add() method to avoid costly += operations.
$ADUsersList = New-Object -TypeName "System.Collections.ArrayList"
$ADUsersList = [System.Collections.ArrayList]#()
$Record = [ordered] #{
"Group Name" = ""
"Employee Name" = ""
"Title"= ""
"Manager" = ""
}
foreach ($Group in $ADGroupsList) {
$ArrayofMembers = #(Get-ADGroupMember -Identity $Group.DistinguishedName | Where-Object { $_.objectClass -eq "user" })
#Loop through each member in the list of members from above
foreach ($Member in $ArrayofMembers) {
#Get detailed user info about the current user like title and manager that aren't available from Get-ADGroupMember
$User = #(Get-ADUser -Identity $Member -Properties name,title,manager | Select-Object Name, Title, #{Label="Manager";Expression={(Get-ADUser (Get-ADUser $Member -Properties Manager).Manager).Name}})
#Specifies what values to apply to each property of the $Record object
$Record."Group Name" = $Group.CN
$Record."Employee Name" = $Member.Name
$Record."Title" = $User.Title
$Record."Manager" = $User.Manager
#Put all the stored information above in a 'copy' record
$objRecord = New-Object PSObject -property $Record
#Add that copy to the existing data in the ADUsersList object
[void]$ADUsersList.Add($objRecord)
}
#Using Join-Object here to enable me to use SQL-like JOINs
Join-Object -Left $ADUsersList -Right $ADGroupsList -LeftJoinProperty "Group Name" -RightJoinProperty "CN" -Type AllInLeft -LeftMultiMode DuplicateLines -RightMultiMode DuplicateLines -ExcludeRightProperties DistinguishedName | Export-Csv ("C:\ADReports\" + $Group.CN + " Report.csv") -NoTypeInformation
$ADUsersList.Clear()
}
Here's the output I expect (columns may be out of order, but column ordering isn't important):
My code works great for most groups, but for groups that have only one member (or none), I get an error:
Join-Object : Method invocation failed because [System.Management.Automation.PSCustomObject] does not contain a method named 'ForEach'.
At C:\GetADGroups&Users.ps1:54 char:5
+ Join-Object -Left $ADUsersList -Right $ADGroupsList -LeftJoinProp ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (ForEach:String) [Join-Object], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound,Join-Object
At first, I thought it was because I read arrays/arraylists with one entry get turned into scalars. But a knee-jerk wrapping of every object I can think of in #() didn't resolve the issue. In fact, if I wrap the $objRecord assignment (New-Object PSObject -property $Record) in #() to convert it to an array, it writes the Member Properties of $ADUsersList to the Join-Object line instead of the contents of $ADUsersList, resulting in this:
Is there somewhere I've missed an array/arraylist getting converted to a scalar? Why is the code above throwing an error for groups with <= 1 entries?
Compounding my curiosity, PowerShell 7 (possibly 6, too) doesn't seem to care about this issue; it doesn't throw the error at all (instead it just outputs the appropriate single-value/blank CSV). Normally I'd just wipe my hands and say PS 7 is required, but I'd like to get this working in PowerShell 5, or at least understand what is causing the issue.
Googling led me to several related articles & questions, including:
Method Invocation .Foreach failed, System.Object doesn't contain a method named 'foreach' this one's specific to PowerShell v2 (I'm running v5)
Method invocation failed because [System.Management.Automation.PSObject] doesn't contain a method named 'op_Addition' this one seems only tangentially related. Incidentally it's where I read that arrays with one item output as scalars, as I mentioned earlier.
It does appear that scalars lack the .ForEach() & .Where() methods in 5.1. The additional of the methods is probably just an enhancement newer version, certainly 7 not sure about 6. I'm sure that's documented somewhere.
I can't really test your code but it doesn't look like there's anywhere that could be flipping to a scalar. To help guarantee ArrayList collections through out you can type constrain the variables like [Collections.ArrayList]$Var = #() This may end up being more practical than hunting for an implementing #() throughout.
Something that stands out is the error seems to come from Join-Object I only found a single invocation of .ForEach() on line 820 of Join-Object.ps1 My guess is it's this line or similar elsewhere in the module combined with the 5.1 runtime environment.
If you can manually modify that to a traditional | ForEach-Object {...} might be telling. And/or you can wrap $result like #($Result) right before the .ForEach() is invoked.
Really interested to see what you come up with. I see you've already posted an issue with the author. Please post back if you get a reply. Thanks.

How do I sort an array of objects by one of their property values in Powershell?

For example, I have a variable, which returns the line with several arrays:
#{sourceDSAcn=B; LastSyncResult=0} #{sourceDSAcn=A; LastSyncResult=9} #{sourceDSAcn=C; LastSyncResult=0} #{sourceDSAcn=M; Last SyncResult=10}
I want to sort this line alphabetically by one of parameters. In this case - by sourceDSAcn, so result must be like that:
#{sourceDSAcn=A; LastSyncResult=9} #{sourceDSAcn=B; LastSyncResult=0} #{sourceDSAcn=C; LastSyncResult=0} #{sourceDSAcn=M; Last SyncResult=10}
How can I do that?
Your output format suggests two things:
The objects aren't arrays, but custom objects ([pscustomobject] instances).
You've used the Write-Host cmdet to print these objects to the host (display), which results in the hashtable-literal-like representation shown in your question (see this answer).
If, instead, you want the usual rich display formatting you get by default - while still sending the output to the host only rather than to the success output stream - you can use the Out-Host cmdlet.
Conversely, to produce data output to the pipeline, use either the Write-Output cmdlet or, preferably, PowerShell's implicit output feature, as shown below; for more information, see this answer.
In order to sort (custom) objects by a given property, simply pass the name of that property to
Sort-Object's (positionally implied) -Property parameter, as Mathias R. Jessen helpfully suggests:
# Using $variable by itself implicitly sends its value through the pipeline.
# It is equivalent to: Write-Output $variable | ...
$variable | Sort-Object sourceDSAcn # same as: ... | Sort-Object -Property sourceDSAcn

How to have a Powershell validatescript parameter pulling from an array?

I have a module with a lot of advanced functions.
I need to use a long list of ValidateSet parameters.
I would like to put the whole list of possible parameters in an array and then use that array in the functions themselves.
How can I pull the list of the whole set from an array?
New-Variable -Name vars3 -Option Constant -Value #("Banana","Apple","PineApple")
function TEST123 {
param ([ValidateScript({$vars3})]
$Fruit)
Write-Host "$Fruit"
}
The problem is that when I use the function it doesn't pull the content from the constant.
TEST123 -Fruit
If I specify the indexed value of the constant then it works.
TEST123 -Fruit $vars3[1]
It returns Apple.
You are misunderstanding how ValidateScript ...
ValidateScript Validation Attribute
The ValidateScript attribute specifies a script that is used to
validate a parameter or variable value. PowerShell pipes the value to
the script, and generates an error if the script returns $false or if
the script throws an exception.
When you use the ValidateScript attribute, the value that is being
validated is mapped to the $_ variable. You can use the $_ variable to refer to the value in the script.
... works. As the others have pointed out thus far. You are not using a script you are using a static variable.
To get what I believe you are after, you would do it, this way.
(Note, that Write- is also not needed, since output to the screen is the default in PowerShell. Even so, avoid using Write-Host, except for in targeted scenarios, like using color screen output. Yet, even then, you don't need it for that either. There are several cmdlets that can be used, and ways of getting color with more flexibility. See these listed MS powershelgallery.com modules)*
Find-Module -Name '*Color*'
Tweaking your code you posted, and incorporating what Ansgar Wiechers, is showing you.
$ValidateSet = #('Banana','Apple','PineApple') # (Get-Content -Path 'E:\Temp\FruitValidationSet.txt')
function Test-LongValidateSet
{
[CmdletBinding()]
[Alias('tlfvs')]
Param
(
[Validatescript({
if ($ValidateSet -contains $PSItem) {$true}
else { throw $ValidateSet}})]
[String]$Fruit
)
"The selected fruit was: $Fruit"
}
# Results - will provide intellisense for the target $ValidateSet
Test-LongValidateSet -Fruit Apple
Test-LongValidateSet -Fruit Dog
# Results
The selected fruit was: Apple
# and on failure, spot that list out. So, you'll want to decide how to handle that
Test-LongValidateSet -Fruit Dog
Test-LongValidateSet : Cannot validate argument on parameter 'Fruit'. Banana Apple PineApple
At line:1 char:29
Just add to the text array / file but this also means, that file has to be on every host you use this code on or at least be able to reach a UNC share to get to it.
Now, you can use the other documented "dynamic parameter validate set". the Lee_Daily points you to lookup, but that is a bit longer in the tooth to get going.
Example:
function Test-LongValidateSet
{
[CmdletBinding()]
[Alias('tlfvs')]
Param
(
# Any other parameters can go here
)
DynamicParam
{
# Set the dynamic parameters' name
$ParameterName = 'Fruit'
# Create the dictionary
$RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
# Create the collection of attributes
$AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
# Create and set the parameters' attributes
$ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
$ParameterAttribute.Mandatory = $true
$ParameterAttribute.Position = 1
# Add the attributes to the attributes collection
$AttributeCollection.Add($ParameterAttribute)
# Generate and set the ValidateSet
$arrSet = Get-Content -Path 'E:\Temp\FruitValidationSet.txt'
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)
# Add the ValidateSet to the attributes collection
$AttributeCollection.Add($ValidateSetAttribute)
# Create and return the dynamic parameter
$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
$RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
return $RuntimeParameterDictionary
}
begin
{
# Bind the parameter to a friendly variable
$Fruit = $PsBoundParameters[$ParameterName]
}
process
{
# Your code goes here
$Fruit
}
}
# Results - provide intellisense for the target $arrSet
Test-LongValidateSet -Fruit Banana
Test-LongValidateSet -Fruit Cat
# Results
Test-LongValidateSet -Fruit Banana
Banana
Test-LongValidateSet -Fruit Cat
Test-LongValidateSet : Cannot validate argument on parameter 'Fruit'. The argument "Cat" does not belong to the set "Banana,Apple,PineApple"
specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.
At line:1 char:29
Again, just add to the text to the file, and again, this also means, that file has to be on every host you use this code on or at least be able to reach a UNC share to get to it.
I am not sure exactly what your use case is, but another possibility if you're using PowerShell 5.x, or newer, is to create a class, or if you're using an older version you could embed a little C# in your code to create an Enum that you can use:
Add-Type -TypeDefinition #"
public enum Fruit
{
Strawberry,
Orange,
Apple,
Pineapple,
Kiwi,
Blueberry,
Raspberry
}
"#
Function TestMe {
Param(
[Fruit]$Fruit
)
Write-Output $Fruit
}

Selecting certain properties from an object in PowerShell

The ADSI query works fine, it returns multiple users.
I want to select the 'name' and 'email' from each object that is returned.
$objSearcher = [adsisearcher] "()"
$objSearcher.searchRoot = [adsi]"LDAP://dc=admin,dc=domain,dc=co,dc=uk"
$objSearcher.Filter = "(sn=Smith)"
$ADSearchResults = $objSearcher.FindAll()
$SelectedValues = $ADSearchResults | ForEach-Object { $_.properties | Select -property mail, name }
$ADSearchResults.properties.mail gives me the email address
When I omit the 'select -properties' it will return all the properties, but trying to select certain properties comes back with nothing but empty values.
Whenever working with ADSI I find it easier to expand the objects returned using .GetDirectoryEntry()
$ADSearchResults.GetDirectoryEntry() | ForEach-Object{
$_.Name
$_.Mail
}
Note: that doing it this way gives you access to the actual object. So it is possible to change these values and complete the changes with something like $_.SetInfo(). That was meant to be a warning but would not cause issues simply reading values.
Heed the comment from Bacon Bits as well from his removed answer. You should use Get-Aduser if it is available and you are using Active Directory.
Update from comments
Part of the issue is that all of these properties are not string but System.DirectoryServices.PropertyValueCollections. We need to get that data out into a custom object maybe? Lets have a try with this.
$SelectedValues = $ADSearchResults.GetDirectoryEntry() | ForEach-Object{
New-Object -TypeName PSCustomObject -Property #{
Name = $_.Name.ToString()
Mail = $_.Mail.ToString()
}
}
This simple approach uses each objects toString() method to break the data out of the object. Note that while this works for these properties be careful using if for other and it might not display the correct results. Experiment and Debug!
Have you tried adding the properties?
$objSearcher.PropertiesToLoad.Add("mail")
$objSearcher.PropertiesToLoad.Add("name")

Resources