Powershell delete MSMQ remotely - powershell

I was wondering if it was possible to delete queues remotely via PowerShell? I have the following script:
cls
[Reflection.Assembly]::LoadWithPartialName("System.Messaging")
$computers = #("comp1","comp2","comp3");
foreach($computer in $computers) {
$messageQueues = [System.Messaging.MessageQueue]::GetPrivateQueuesByMachine($computer);
foreach ($queue in $messageQueues) {
$endpoint = [string]::Format("FormatName:DIRECT=OS:{0}\{1}", $computer, $queue.QueueName);
Write-Host $endpoint
[System.Messaging.MessageQueue]::Delete($endpoint);
}
}
This works fine, if I was running it on the machine whose queues I want to delete however when I run this remotely I get the error:
The specified format name does not support the requested operation. For example, a direct queue format name cannot be deleted.
Any ideas if this can be done?
EDIT
Oddly, I have figured I can remote onto the machine via PowerShell and execute a script block. However, I don't understand the difference between doing:
THIS:
$endpoint = [string]::Format("FormatName:DIRECT=OS:{0}\{1}", $computer, $queue.QueueName);
Invoke-Command -ComputerName $computer -ScriptBlock { [Reflection.Assembly]::LoadWithPartialName("System.Messaging"); [System.Messaging.MessageQueue]::Delete($endpoint) };
AND THIS:
Invoke-Command -ComputerName $computer -ScriptBlock { [Reflection.Assembly]::LoadWithPartialName("System.Messaging"); [System.Messaging.MessageQueue]::Delete("FormatName:DIRECT=OS:MY_SERVER\some.endpoint") };
The value of $endpoint is the same however, for some odd reason it doesn't like the variable approach though both values are identical. I tested this by setting $endpoint then calling delete. I get the error:
Exception calling "Delete" with "1" argument(s): "Invalid value for parameter path."
What I'm trying to say is if I hard code the value as part of the argument it works but assign it to a variable then invoke the method I get an error

For historic purposes if anyone else is experiencing this issue or is wondering how to delete queues remotely then please see below.
How do I delete private queues on a remote computer? It is possible to delete queues remotely. This can be achieved using the command Enable-PSRemoting -Force. Without this, you encounter the issue #JohnBreakWell indicated (see his link to MSDN).
The scope of variables when using Invoke-Command? The problem I found was the variables I declared were simply out of scope (script block was unable to see it). To rectify this, I simply did the following:
The important bit being the argument list and the use of param.
$computers = #("comp1","comp2");
foreach($computer in $computers) {
[Reflection.Assembly]::LoadWithPartialName("System.Messaging");
$messageQueues = [System.Messaging.MessageQueue]::GetPrivateQueuesByMachine($computer);
foreach ($queue in $messageQueues) {
$endpoint = [string]::Format("FormatName:DIRECT=OS:{0}\{1}", $computer, $queue.QueueName);
Enable-PSRemoting -Force
Invoke-Command -ComputerName $computer -ScriptBlock {
param ($computer, $endpoint)
[Reflection.Assembly]::LoadWithPartialName("System.Messaging");
[System.Messaging.MessageQueue]::Delete($endpoint)
}
} -ArgumentList $computer, $endpoint
}

You cannot delete a remote private queue.
You need to perform the operation locally to the queue.
From MQDeleteQueue:
Remarks
(2nd paragraph)
"Private queues registered on a remote computer ... cannot be deleted."

As Dr. Schizo mentioned, you'll need to execute
Enable-PSRemoting -Force
on the remote machine, but then, assuming you're using Server 2012 r2, it's as simple as:
Invoke-Command -ComputerName COMPUTERNAME { Get-MsmqQueue -Name QUEUENAME | Remove-MsmqQueue }

Related

[System.IO.Path]::GetTempPath() outputs local temp directory when called through Invoke-Command on a remote machine

I'm running PowerShell commands on a remote machine by the use of Invoke-Command -ComputerName. I'm trying to obtain the path of the temporary directory of the remote machine.
Depending on where I call [System.IO.Path]::GetTempPath() it either outputs the expected remote directory C:\Users\…\AppData\Local\Temp or my local temporary directory C:\temp.
This command is not working as expected:
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
Write-Output ([System.IO.Path]::GetTempPath())
}
# Outputs local directory 'C:\temp'
# Expected remote directory 'C:\Users\…\AppData\Local\Temp'
The problem can be reproduced with other commands than Write-Output, e. g. Join-Path.
Contrary, the following code samples all give the expected output of C:\Users\…\AppData\Local\Temp.
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
[System.IO.Path]::GetTempPath()
}
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
$tmp = [System.IO.Path]::GetTempPath(); Write-Output $tmp
}
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
Start-Sleep 1
Write-Output ([System.IO.Path]::GetTempPath())
}
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
Write-Output ([System.IO.Path]::GetTempPath())
Start-Sleep 1
}
Obviously Start-Sleep isn't a solution, but it seems to indicate some kind of timing problem.
Suspecting that the problem isn't limited to GetTempPath() I tried another user-related .NET API, which also unexpectedly outputs my local folder instead of the remote one:
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
Write-Output ([System.Environment]::GetFolderPath([Environment+SpecialFolder]::MyDocuments))
}
How can I use [System.IO.Path]::GetTempPath() and other .NET API in a PowerShell remote session in a predictable way?
Santiago Squarzon has found the relevant bug report:
GitHub issue #14511
The issue equally affects Enter-PSSession.
While a decision was made to fix the problem, that fix hasn't yet been made as of PowerShell 7.3.1 - and given that the legacy PowerShell edition, Windows PowerShell (versions up to v5.1, the latest and final version) will see security-critical fixes only, the fix will likely never be implemented there.
While the linked bug report talks about the behavior originally having been by (questionable) design, the fact that it only surfaces in very narrow circumstances (see below) implies that at the very least that original design intent's implementation was faulty.
The problem seems to be specific to a script block with the following characteristics:
containing a single statement
that is a cmdlet call (possibly with additional pipeline segments)
whose arguments involve .NET method calls, which are then unexpectedly performed on the caller's side.
Workaround:
Make sure that your remotely executing script block contains more than one statement.
A simple way to add a no-op dummy statement is to use $null++:
# This makes [System.IO.Path]::GetTempPath() *locally* report
# 'C:\temp\'
# *Remotely*, the *original* value should be in effect, even when targeting the
# same machine (given that the env. var. modification is process-local).
$env:TMP = 'C:\temp'
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
Write-Output ([System.IO.Path]::GetTempPath()); $null++ # <- dummy statement.
}
Other workarounds are possible too, such as enclosing the cmdlet call in (...) or inserting a dummy variable assignment
(Write-Output ($unused = [System.IO.Path]::GetTempPath()))
Your Start-Sleep workaround happened to work because by definition it too added another statement; but what that statement is doesn't matter, and there's no timing component to the bug.

New-CimSession Auto Downgrade to DCOM / WMI

One of the benefits of CIM is the "fan out" ability; e.g. running the following causes sessions to be created against all servers in parallel, rather than sequentially:
$myServers = 1..10 | %{'MyServer{0:00}.mydomain.example.com' -f $_}
$cimSessions = New-CimSession -ComputerName $myServers
Since some of our servers don't have WimRM enabled, I want to gracefully downgrade to using DCOM when WimRM isn't available / so we don't fail if we don't have to. As such I put a wrapper around the Get-CimSession that does something like this (simplified version):
function New-SoEgCimSession { #(soeg = Stack Overflow Exempli Gratia)
[CmdletBinding()]
Param (
[Parameter(Mantatory = $true, ValueFromPipeline = $true)]
[String]$ComputerName
)
Begin {
[Microsoft.Management.Infrastructure.Options.WSManSessionOptions]$WsmanSessionOption = New-CimSessionOption -Protocol Wsman
[Microsoft.Management.Infrastructure.Options.DComSessionOptions]$DcomSessionOption = New-CimSessionOption -Protocol Dcom
}
Process {
$session = New-CimSession -ComputerName $ComputerName -SessionOption $WsmanSessionOption
if ($null -eq $session) {
$session = New-CimSession -ComputerName $ComputerName -SessionOption $DcomSessionOption
}
$session
}
}
... Which I'd now invoke like so:
$cimSessions = $myServers | New-SoEgCimSession
My worry is that this now removes the "fan out" ability, since each call to New-CimSession is separate / only takes 1 server argument.
Question
Is this worry valid, or does New-CimSession work more like async/await in C#, in that whilst waiting on one CimSession to be created, control's handed back to the parent thread which can fetch the next session?
Additional Thoughts / follow up question
If my worry is valid, is there a cleaner way of approaching this? I've considered using workflows to wrap this logic, but that feels like reinventing something that New-CimSession provides by default.
I've also considered calling New-CimSession with the array of computers and -ErrorAction Stop, then use a catch block to process any failures; but it seems that only the first failure's details are included (i.e. catch {$_.Exception.OriginInfo} returns a single server name, even if multiple servers fail). I've also not tested with a large enough group of servers to know if the error also terminates the attempt to create new sessions; with a small data set it doesn't, but that may be because all sessions are successfully created before the errors with the non-winrm servers are detected.
My current idea is to compare original array's computer names with those in the created sessions to find those for which sessions weren't created, then attempt to create DCOM sessions for those remaining. However that still feels like a slightly hacky workaround / I suspect that my lack of experience with CIM means I'm missing a more obvious out-of-the-box solution...
You can try the following:
function New-SoEgCimSession {
[CmdletBinding()]
Param (
[Parameter(Mantatory, ValueFromPipeline)]
[String[]] $ComputerName
)
Begin {
$names = [Collections.Generic.List[string]]::new()
$WsmanSessionOption = New-CimSessionOption -Protocol Wsman
$DcomSessionOption = New-CimSessionOption -Protocol Dcom
}
Process {
$names.AddRange($ComputerName)
}
End {
# Try WSMan first. Successful sessions will be output,
# errors silently collected in variable $errs.
New-CimSession -ErrorVariable errs -ErrorAction SilentlyContinue -ComputerName $names -SessionOption $WsmanSessionOption
if ($errs.Count) {
# Errors occurred; try all failing computers with DCOM now.
# Successful sessions will be output and if there are still errors,
# they will surface.
$failedNames = $errs.OriginInfo.PSComputerName
New-CimSession -ComputerName $failedNames -SessionOption $DcomSessionOption
}
}
}
The computer names are collected in a list, and only after all have been collected does processing start, in the end block.
All names are passed to New-CimSession with the WSMan configuration first, suppressing display of any non-terminating errors (-ErrorAction SilentlyContinue), but collecting them in variable $errs (-ErrorVariable errs).
If any errors occurred, the computer names that caused the failure are then passed to New-CimSession with the DCOM configuration - no error handling is then attempted, meaning that any errors that still occur surface as non-terminating ones.
Finally, all successfully created sessions are output.
Generally, using -ErrorAction Stop always aborts a command on the first non-terminating error; if you want to collect them all for later analysis, use something like -ErrorAction SilentlyContinue -ErrorVariable errs, where self-chosen variable $errs receives a collection of all non-terminating errors that occurred.

PowerShell - Getting Return Value From Script Run on Remote Machine?

I'm just trying to get a script on a local machine run on a remote machine and have a result returned back to the local caller. What am I doing wrong? This is almost verbatim one of several examples I've seen, except they used -ScriptBlock {} to do it. Is this just not possible to achieve using the -FilePath option? I double-checked the local path. Both of the scripts are in the same folder on the same drive on the local machine.
This Caller gives me a blank result.
Local Caller:
Write-Output "Retrieving results from server call."
$returned=Invoke-Command -ComputerName $server -Credential $MyCredential -FilePath D:\scripts\runOnRemote.ps1
Write-Output $returned
The following produces 'DummyMessage' as expected
Alternate Local Caller:
$returned=Invoke-Command -ComputerName $server -Credential $MyCredential -ScriptBlock {
$toReturn = 'DummyMessage'
return $toReturn
}
But this doesn't.
Script trying to run on remote:
$returnMessage = 'DummyMessage'
return $returnMessage
Output:
Retrieving results from server call.
<nothing>
Vs.
Retrieving results from server call.
DummyMessage
The issue was resolved after returning from lunch. It was an ISE bug that just wasn't interpreting new code revisions after so many runs (hence my confusion!).

How to get return value from remote ps script?

I am using invoke-expression to call a remote script on another server. I was wondering how I can get the status code from that remote script to tell whether the script completes successfully or fails.
In fact, I am not sure whether invoke-expression will work for this return value, if it doesn't , is there any other way to work it around?
Invoke-Expression is not recommended. PSScriptAnalyzer will tell you not to use this if you can avoid this.
The typical way of getting a value from a remote system I'll demonstrate below. The value must be serializable.
$session = New-PSSession # the rest of the command to establish the session
$value = Invoke-Command -session $session -ScriptBlock {
# Do work to get value in say variable $returnValue
return $returnValue
}

OpenRemoteBaseKey() credentials

I'm attempting to use powershell to access a remote registry like so:
$reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("LocalMachine", $server)
$key = $reg.OpenSubkey($subkeyPath)
Depending on some factors that I'm not yet able to determine I either get
Exception calling "OpenSubKey" with "1" argument(s): "Requested registry access is not allowed."
Or
System.UnauthorizedAccessException: Attempted to perform an unauthorized operation.
at Microsoft.Win32.RegistryKey.Win32ErrorStatic(Int32 errorCode, String str)
at Microsoft.Win32.RegistryKey.OpenRemoteBaseKey(RegistryHive hKey, String machineName)
It seems pretty clear that this is because the user I'm running the powershell script as doesn't have the appropriate credentials to access the remote registry. I'd like to be able to supply a set of credentials to use for the remote registry access, but I can find no documentation anywhere of a way to do this. I'm also not clear on exactly where to specify which users are allowed to access the registry remotely.
Just thought I'd add my answer to anyone with this problem as well. It seems there is no way to add Credentials using RemoteRegistry. You can however use WMI to query a remote registry using alternative credentials as follows:
$reg = Get-WmiObject -List -Namespace root\default -ComputerName RemotePC -Credential "Domain\User" | Where-Object {$_.Name -eq "StdRegProv"}
From here you can call standard Registry methods. The below example will return the operating system.
$HKLM = 2147483650
$reg.GetStringValue($HKLM,"SOFTWARE\Microsoft\Windows NT\CurrentVersion","ProductName").sValue
Hope this helps someone :)
Are you running remote registry service? It is disabled by default and that must be causing the issue. Check the status of this service on all remote machines you are trying to access.
I couldn't comment directly on bentaylr's entry above, but I've taken what he contributed and added PSCredentials creation (figured out from here) to allow you to hard code credentials into the script.
Peace of mind disclaimer: Be careful when using plaintext credentials in a script. In my case, I'm using generic credentials on machines I'm launching. Depending on your case, you might consider creating an encrypted credential file to store the password in (see link above).
The credentials you use would need to be able to access the registry if you were logged into that user on the machine you are targeting.
$user = "Domain\Username"
$pass = ConvertTo-SecureString "Password" -AsPlainText -Force
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $user,$pass
$reg = Get-WmiObject -List -Namespace root\default -ComputerName $server -Credential $cred | Where-Object {$_.Name -eq "StdRegProv"}
$HKLM = 2147483650
$value = $reg.GetStringValue($HKLM,"Software\Microsoft\.NetFramework","InstallRoot").sValue
$key.OpenSubKey($subkeyName) opens the subkey in write protected mode,
$key.OpenSubKey($subkeyName,$true) opens it in writable mode
Therefore after $key.OpenSubKey($subkeyName,$true) you should be able to create a new subkey or value
If you try the same thing after $key.OpenSubKey($subkeyName) you will get "UnauthorizedAccessException"
PS C:\>$regKey.OpenSubKey
OverloadDefinitions
Microsoft.Win32.RegistryKey OpenSubKey(string name, **bool Writable**)
try
PS C:\>$key.OpenSubKey($subkeyName,**$true**)
http://msdn.microsoft.com/en-us/library/xthy8s8d%28v=vs.110%29.aspx
Came looking for the answer to your question, but in a little googling this morning I noticed that the first parameter is a type rather than a String... hope this helps:
$machine = "<Machine Name Goes Here>"
$type = [Microsoft.Win32.RegistryHive]::LocalMachine
$regkey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($type,$machine)
$subkey = $regKey.OpenSubKey($key)
foreach ($sub in $regKey.GetSubKeyNames()){$sub}
I wanted to first thank all for answers above really helpful, wanted to add that you can use Get-Credential command to collect credentials without having to hard code it in your script. I have written using the above suggestions into my script the following code and query:
$userCredentials = Get-Credential -Credential <domain\username>
$objReg = Get-WmiObject -List -Namespace root\default -ComputerName $server -Credential $userCredentials | Where-Object{$_.Name -eq "StdRegProv"}
$subKeyNames = $objReg.EnumKey($HKLM,"SOFTWARE\Microsoft\Updates\Microsoft .Net Framework 4.5.1").sNames
The above code returns all sub key names in the specified key so that I can determine installed updates other than OS which have been applied to a server. If you want to determine all collection possibilities with the $objReg variable then run:
$objReg | Get-Member
You will see a list of all possible queries which can be performed against the registry. Hope this helps!