I'm playing with SMO and tried to use it to change the database owners to sa. The code is
# To simplify our discussing, let's say we have a function Get-SMOServer
$s = Get-SMOServer -Instance myserver\myinstance
$s.databases | ?{$_.owner -ne "sa"} | %{$_.setowner("sa", $true)}
At this point, when I check the database owners from SSMS, the owners were already changed. However, if I check it from $s.databases, I still got the old data, until I do something like:
$s.databases | %{$_.refresh()}
Then I can get the correct result from $s.databases.
I checked the SMO objects and found many of them have a refresh() function. My question is, should I call refresh() every time I modified some object? How to find all object types that have a refresh() member?
Thanks
You can look at the SMO assembly. I'm using SMO which ships with 2008 R2:
$assm = add-type -AssemblyName "Microsoft.SqlServer.Smo, Version=10.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91" -EA Stop -PassThru
$assm | %{$hasRefreshMethod = $null; $hasRefreshMethod = $_.GetMethods() | ?{$_.name -eq "Refresh"}; new-object psobject -property #{Name=$_.Name; HasRefreshMethod=$($hasRefreshMethod -ne $null)}}
Related
Below is the exact code that I am having trouble with.
A brief description:
I am trying to set up a PowerShell class that will hold objects of different types for easy access. I've done this numerous times in C#, so I thought it would be fairly straight forward. The types wanted are [System.Printing] and WMI-Objects.
Originally I had tried to write the class directly to my PowerShell profile for easy usage, but my profile fails to load when I have to class code in it. Saying that it can’t find the type name "System.Printing.PrintServer", or any other explicitly listed types.
After that failed, I moved it to its own specific module and then set my profile to import the module on open. However, even when stored in its own module, if I explicitly list a .NET type for any of the properties, the entire module fails to load. Regardless of whether I have added or imported the type / dll.
The specific problem area is this:
[string]$Name
[System.Printing.PrintServer]$Server
[System.Printing.PrintQueue]$Queue
[System.Printing.PrintTicket]$Ticket
[System.Management.ManagementObject]$Unit
[bool]$IsDefault
When I have it set to this, everything "kind of" works, but then all my properties have the _Object type, which is not helpful.
[string]$Name
$Server
$Queue
$Ticket
$Unit
$IsDefault
Add-Type -AssemblyName System.Printing
Add-Type -AssemblyName ReachFramework
Class PrinterObject
{
[string]$Name
[System.Printing.PrintServer]$Server
[System.Printing.PrintQueue]$Queue
[System.Printing.PrintTicket]$Ticket
[System.Management.ManagementObject]$Unit
[bool]$IsDefault
PrinterObject([string]$Name)
{
#Add-Type -AssemblyName System.Printing
#Add-Type -AssemblyName ReachFramework
$this.Server = New-Object System.Printing.PrintServer -ArgumentList [System.Printing.PrintSystemDesiredAccess]::AdministrateServer
$this.Queue = New-Object System.Printing.PrintQueue (($this.Server), ($this.Server.GetPrintQueues() |
Where-Object {$_.Name -match $Name} | Select-Object -ExpandProperty Name))
$this.Ticket = $this.Queue.UserPrintTicket
$this.Unit = Get-WmiObject -Query "SELECT * FROM Win32_Printer WHERE Name LIKE `"%$Name%`""
}
PrinterObject([string]$Name, [bool]$IsNetwork)
{
#Add-Type -AssemblyName System.Printing
#Add-Type -AssemblyName ReachFramework
if($IsNetwork -eq $true) {
$this.Server = New-Object System.Printing.PrintServer ("\\Server")
$this.Queue = New-Object System.Printing.PrintQueue (($this.Server), ($this.Server.GetPrintQueues() |
Where-Object {$_.Name -match $Name} | Select-Object -ExpandProperty Name))
$this.Ticket = $this.Queue.UserPrintTicket
$this.Unit = Get-WmiObject -Query "SELECT * FROM Win32_Printer WHERE Name LIKE `"%$Name%`""
}
else {
$This.Server = New-Object System.Printing.PrintServer -argumentList [System.Printing.PrintSystemDesiredAccess]::AdministrateServer
$this.Queue = New-Object System.Printing.PrintQueue (($this.Server), ($this.Server.GetPrintQueues() |
Where-Object {$_.Name -match $Name} | Select-Object -ExpandProperty Name))
$this.Ticket = $this.Queue.UserPrintTicket
$this.Unit = Get-WmiObject -Query "SELECT * FROM Win32_Printer WHERE Name LIKE `"%$Name%`"" }
}
[void]SetPrintTicket([int]$Copies, [string]$Collation, [string]$Duplex)
{
$this.Ticket.CopyCount = $Copies
$this.Ticket.Collation = $Collation
$this.Ticket.Duplexing = $Duplex
$this.Queue.Commit()
}
[Object]GetJobs($Option)
{
if($Option -eq 1) { return $this.Queue.GetPrintJobInfoCollection() | Sort-Object -Property JobIdentifier | Select-Object -First 1}
else { return $this.Queue.GetPrintJobInfoCollection() }
}
static [Object]ShowAllPrinters()
{
Return Get-WmiObject -Class Win32_Printer | Select-Object -Property Name, SystemName
}
}
Every PowerShell script is completely parsed before the first statement in the script is executed. An unresolvable type name token inside a class definition is considered a parse error. To solve your problem, you have to load your types before the class definition is parsed, so the class definition has to be in a separate file. For example:
Main.ps1:
Add-Type -AssemblyName System.Printing
Add-Type -AssemblyName ReachFramework
. $PSScriptRoot\Class.ps1
Class.ps1:
using namespace System.Management
using namespace System.Printing
Class PrinterObject
{
[string]$Name
[PrintServer]$Server
[PrintQueue]$Queue
[PrintTicket]$Ticket
[ManagementObject]$Unit
[bool]$IsDefault
}
The other possibility would be embed Class.ps1 as a string and use Invoke-Expression to execute it. This will delay parsing of class definition to time where types is available.
Add-Type -AssemblyName System.Printing
Add-Type -AssemblyName ReachFramework
Invoke-Expression #'
using namespace System.Management
using namespace System.Printing
Class PrinterObject
{
[string]$Name
[PrintServer]$Server
[PrintQueue]$Queue
[PrintTicket]$Ticket
[ManagementObject]$Unit
[bool]$IsDefault
}
'#
To complement PetSerAl's helpful answer, which explains the underlying problem and contains effective solutions, with additional background information:
To recap:
As of PowerShell 7.3.1, a PowerShell class definition can only reference .NET types that have already been loaded into the session before the script is invoked.
Because class definitions are processed at parse time of a script, rather than at runtime, Add-Type -AssemblyName calls inside a script execute too late for the referenced assemblies' types to be known to any class definitions inside the same script.
A using assembly statement should solve this problem, but currently doesn't:
using assembly should be the parse-time equivalent of an Add-Type (analogous to the relationship between using module and Import-Module), but this hasn't been implemented yet, because it requires extra work to avoid the potential for undesired execution of arbitrary code when an assembly is loaded.
Implementing a solution has been green-lighted in GitHub issue #3641, and the necessary work is being tracked as part of GitHub issue #6652 - but it is unclear when this will happen, given that the issue hasn't received attention in several years.
A better solution (than just invoking the entire class in a string) would be to just create your objects and pass them to the class as parameters. For example, this runs fine:
Add-Type -AssemblyName PresentationCore,PresentationFramework
class ExampleClass {
$object
ExampleClass ($anotherClass) {
$this.object = $anotherClass
}
[void] Show () {
$this.object::Show('Hello')
}
}
$y = [ExampleClass]::new([System.Windows.MessageBox])
$y.Show()
However, if you were to do something like this, you can expect Unable to find type [System.Windows.MessageBox].
Add-Type -AssemblyName PresentationCore,PresentationFramework
class ExampleClass2 {
$object
ExampleClass () {
$this.object = [System.Windows.MessageBox]
}
[void] Show () {
$this.object::Show('Hello')
}
}
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.
Currently working on making a new report that will be generated with PowerShell. Using PowerShell to build a HTML email. I have one other report working fine but ran into an unexpected issue on this one.
The below code is just s sample from the script I am still building. Still adding pieces to the script but testing it as I move forward. I added a Test-Connection to see if a computer was responding or not and lost the ability to build an array.
My final goal with this report is to import a list of names from a file and then loop over all of the computers to see if they are pinging and gather some information from them using Get-WMIObject, etc.
The below code will replicate the issue I am having but I am not sure how to solve it. I've narrowed down the issue to when Test-Connection returns 'False'. On line 26 I am filtering for just results that returned a 'False' on Test-Connection to save them into its own array so that I can use that array in a different part of my code to build the HTML table/HTML to send out the email.
Only the flipside, if I tell it to look for only 'True', it will save into the array without issue.
This is the error that PowerShell is giving when doing filtering by 'False'.
Cannot convert value "#{Computer_Name=Computer1; Ping_Status=False}" to type "System.Collections.ArrayList". Error: "Cannot convert the "#{Computer_Name=Computer1 Ping_Status=False}" value of type "Selected.System.Management.Automation.PSCustomObject" to type "System.Collections.ArrayList"."
Please let me know if there is any other information that I can provide. I've been stuck on this one for a while. Co-workers are even say this is a weird one.
Is there something unique about the way Test-Connection return a 'False'?
CLS
[string]$ErrorActionPreference = "Continue"
[System.Collections.ArrayList]$Names = #(
"Computer1"
"Computer2"
)
[System.Collections.ArrayList]$WMI_Array = #()
[System.Collections.ArrayList]$Ping_Status_False = #()
foreach ($Name in $Names) {
[bool]$Ping_Status = Test-Connection $Name -Count 1 -Quiet
$WMI_Array_Object = [PSCustomObject]#{
'Computer_Name' = $Name
'Ping_Status' = $Ping_Status
}
$WMI_Array.Add($WMI_Array_Object) | Out-Null
}
$WMI_Array | Format-Table
[System.Collections.ArrayList]$Ping_Status_False = $WMI_Array | Where-Object {$_.Ping_Status -eq $false} | Select-Object Computer_Name, Ping_Status
$Ping_Status_False
The problem is not Test-Connection but that this statement
$WMI_Array | Where-Object {$_.Ping_Status -eq $false} | Select-Object Computer_Name, Ping_Status
produces just a single result. Which is not an array, and can thus not be converted to an ArrayList. The behavior is identical when you filter for $_.PingStatus -eq $true with just a single matching object, so I suspect that you had either more than one successfully pinged host or none at all when you tested that condition and it didn't throw the same error.
You could mitigate the problem by wrapping the statement in the array subexpression operator:
[Collections.ArrayList]$Ping_Status_False = #($WMI_Array |
Where-Object {$_.Ping_Status -eq $false} |
Select-Object Computer_Name, Ping_Status)
Or, you could simply drop all the pointless type-casting from your code:
$ErrorActionPreference = "Continue"
$Names = 'Computer1', 'Computer2'
$WMI_Array = foreach ($Name in $Names) {
[PSCustomObject]#{
'Computer_Name' = $Name
'Ping_Status' = [bool](Test-Connection $Name -Count 1 -Quiet)
}
}
$WMI_Array | Where-Object { -not $_.Ping_Status }
I want to generate an import script for a MSSQL DB via Powershell (related to this question).
I tried doing this:
#Set-ExecutionPolicy RemoteSigned
$DB_NAME = "<<dbName>>"
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.SMO") | Out-Null
$srv = new-object "Microsoft.SqlServer.Management.SMO.Server" "<<server>>"
$conContext = $srv.ConnectionContext
$conContext.LoginSecure = $false
$conContext.Login = "<<user>>"
$conContext.Password = "<<password>>"
$srv = new-object Microsoft.SqlServer.Management.Smo.Server($conContext)
$srv.SetDefaultInitFields([Microsoft.SqlServer.Management.SMO.View], "IsSystemObject")
$db = $srv.databases[$DB_NAME]
$scripter = new-object "Microsoft.SqlServer.Management.Smo.Scripter" $srv
$scripter.Options.ScriptSchema = $false
$scripter.Options.ScriptData = $true
$scripter.Options.ScriptDrops = $false
$scripter.Script($db)
But executing this throws an error:
"This method does not support scripting data"
I also tried to set the output file option but this doesn't change anything.
Can you tell me what I did wrong?
Thanks!
Per the error, Scripter.Script does not support scripting data. This is documented. What isn't documented is what you're supposed to use instead, but it's EnumScript:
$scripter.EnumScript(#($db.Tables))
You must pass the tables, since simply scripting the database will yield nothing (as, technically, the database itself contains no data, its tables do).
(The #() forcibly converts the Tables collection to an array, since that's what EnumScript expects.)
I have a namespace that I'd like to use to validate the existence of a WMI object before I run a Get-WmiObject against it further down in the code. For example, I want to throw the namespace for SSRS 2012 at it and if it doesn't exist on the machine, then I'll try the next namespace for SSRS 2008 R2.
Is there a way to check for the class's existence, by just guessing a namespace, without throwing an error if it does not exist?
I don't want to have to use a try-catch as the solution. I'd like to know a way that I can get a simple boolean result that tells me whether the class exists in this namespace.
I don't want to have to use SilentlyContinue as the solution either.
This will be executed from a Powershell job step in a SQL Agent job. This sometimes handles errors differently than pure Powershell, and is the reason for my concern about #'s 1 and 2 above.
You can use the -Class, -List, and -Namespace parameters of the Get-WmiObject cmdlet to see if a single class exists in the specified namespace:
$class = Get-WmiObject -Class 'Win32_BIOS' -List -Namespace 'root\cimv2';
$classExists = $class -ne $null;
Here's an alternative (but slower) method from an earlier revision of my answer:
$class = Get-WmiObject -List -Namespace 'root\cimv2' `
| Where-Object { $_.Name -eq 'Win32_BIOS'; };
$classExists = $class -ne $null;
Going back to my original answer, here's a third option that, in my testing, does not throw any errors if either the namespace or the class is invalid:
$class = Get-WmiObject -List | Where-Object {
$_.__NAMESPACE -eq 'root\cimv2' -and $_.__CLASS -eq 'Win32_BIOS';
};
$classExists = $class -ne $null;
Note that $_.Name and $_.__CLASS are effectively synonyms. In each of these code snippets, $class will contain a ManagementClass instance for the class you searched for, if found, otherwise $null.