Saving XML on remote machine using Invoke-Command - powershell

Basically, I'm loading the XML from my local machine, and then I'm trying to save it on a remote machine using Invoke-Command.
I know I can use Copy-item via UNC path, but it takes too long on some machines, and Invoke-Command is faster - I tested this already.
However, I think I'm passing the argument wrong?
The error I get is:
Method invocation failed because [System.String] does not contain a method named 'Save'.
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
+ PSComputerName : -
This is how I'm passing it:
foreach ($serverPath in $serverLocations) {
if ($null -ne $serverPath) {
$generatedPath = "$(Get-Location)\Generated.ManageSQLJobs.xml"
[Xml]$generatedFile = Get-Content $generatedPath
Write-Log "INFO" "Checking on $serverPath" $ExecutionLogFullPath
$testPath = Invoke-Command -ComputerName "$serverPath" -ArgumentList [Xml]$generatedFile -ScriptBlock {
param (
$value
)
Test-Path -Path "C:\AppData\MonitoringConfig\"
if (!$testPath) {
$destinationPath = New-Item -Path "C:\AppData\" -Name "MonitoringConfig" -ItemType Directory
}
if ($testPath) {
$destinationPath = "C:\AppData\MonitoringConfig"
#Write-Log "INFO" "Exists on $serverPath." $ExecutionLogFullPath
}
$GetPathToDeleteXML = "C:\AppData\MonitoringConfig\Generated.ManageSQLJobs.xml"
if (Test-Path -Path $GetPathToDeleteXML) {
Remove-Item -Path * #-Filter Generated.ManageSQLJobs.xml
}
$GetPathForXML = "C:\AppData\MonitoringConfig\Generated.ManageSQLJobs.xml"
$value.Save($GetPathForXML.fullname)
}
}
}

-ArgumentList [Xml]$generatedFile
should be (note the (...)):
-ArgumentList ([Xml]$generatedFile)
[Xml]$generatedFile isn't recognized as an expression, because when PowerShell parses in argument mode (commands with arguments, shell-style), an initial [ isn't special.
In effect, your argument is interpreted as an expandable string, i.e. as if you had passed
"[Xml]$generatedFile".
Therefore, $value in your remotely executed script block received a [string] instance, not an [xml] instance, and strings don't have a .Save() method, which explains the error message.
Enclosing your argument in (...) forces its interpretation as an expression.
See this answer for a comprehensive overview of how PowerShell parses unquoted tokens in argument mode.
A general caveat re passing complex objects as arguments to code executed remotely / in background jobs:
Arguments passed to remote / background script blocks must undergo XML-based serialization and deserialization, because they pass computer / process boundaries.
Only a limited set of known types are deserialized faithfully (deserialized as the original type), others are emulated.
While [xml] instances, [string] instances and .NET primitive types such as [int] are faithfully deserialized, most other types are not.
See this answer for more information.

Related

How to pass Powershell Class method or function return value to Set-Content -Path

class getVar {
static [string]pdConfigVarGetMethod($var = "") {
$configFilePath = "$Env:PD_SCRIPTS\pdConfigurationFile.ps1"
$configFileContent = Get-Content -Path $configFilePath
foreach ($line in $configFileContent) {
$lineVar = $line -split (':::')
if ($lineVar[0] -eq $var) {
return $lineVar[1]
}
}
return ""
}
}
$path = [getVar]::pdConfigVarGetMethod("PD_SCRIPTS")
Set-Location -Path $path
The above code works. What I want to understand is why this won't work:
Set-Location -Path [getVar]::pdConfigVarGetMethod("PD_SCRIPTS")
I used a class method because I learned that PS function return values have no contract. A class method's return value is loosly typed and can be relied upon to return only what I asked it to return with the proper type.
So why then doesn't the method call resolve into a string value that Set-Content -Path can understand???
I get the error:
Set-Location : A positional parameter cannot be found that accepts argument 'PD_SCRIPTS'.
At C:\Users\Paul.Defina\Documents\PowershellScripts\ScriptDir.ps1:11 char:1
+ Set-Location -Path [getVar]::pdConfigVarGetMethod("PD_SCRIPTS")
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Set-Location], ParameterBindingException
+ FullyQualifiedErrorId : PositionalParameterNotFound,Microsoft.PowerShell.Commands.SetLocationCommand
I expected:
Set-Location -Path [getVar]::pdConfigVarGetMethod("PD_SCRIPTS")
to work the same as:
$path = [getVar]::pdConfigVarGetMethod("PD_SCRIPTS")
Set-Location -Path $path
In argument parsing mode, [ is not a metacharacter, and therefore what in expression (parsing) mode is a type literal, such as [getVar], is not recognized as such and is simply interpreted verbatim.
Therefore, you must enclose your expression in parentheses - (...) - in order for it to be recognized as such:
Set-Location -Path ([getVar]::pdConfigVarGetMethod("PD_SCRIPTS"))
As an aside: -Path interprets its argument as a wildcard expression. If you know your path to be a literal one, it is more robust to use -LiteralPath.
As for what you tried:
Set-Location -Path [getVar]::pdConfigVarGetMethod("PD_SCRIPTS")
While [ isn't a metacharacter in argument mode, ( is, and -despite there being no space before (, PowerShell considers the (...) expression to be a separate argument, so that in effect you passed two arguments:
Verbatim [getVar]::pdConfigVarGetMethod
Verbatim PD_SCRIPTS, which is what the expression ("PD_SCRIPTS") evaluated to.
This unexpected extra argument caused Set-Location to report an error.
By contrast:
$path = [getVar]::pdConfigVarGetMethod("PD_SCRIPTS")
Set-Location -Path $path
This did work, because in the context of an assignment statement, [getVar] is recognized as a type literal, because the RHS of the assignment starting with a [ causes it to be parsed in expression mode.
In short:
In a given parsing context, it is the first token that determines which mode is entered:
If the first token of a statement is an unquoted name (symbolic identifier) - such as Set-Location - it is assumed to refer to a command, and argument mode is entered.
Otherwise, it is assumed to be an expression, and expression mode is entered - this therefore includes tokens that start with [, so that they're recognized as part of type literals.
You can create a new parsing context with (...), and also $(...) (primarily for use inside "...") and #(...).
See also:
The conceptual conceptual about_Parsing help topic.
This answer, which provides a concise summary.

Powershell variable is not transfering correctly in the Invoke-Command

I'm writing a script where in one part I want to export some settings of the SCVMM VM and set it in another. I have to run it from another machine, which doesn't have SCVMM installed so I have to call our VMM with Invoke-Command.
Unfortunately, variables I'm using in the code behave unexpectedly (don't want to say wrong, I assume it's by design). When they are used in parameter they don't transfer whole object that's in them, but just the Name.
$vm01 = Get-VM -Name VM01
$vm02 = Get-VM -Name VM02
$vm01name =$vm01.Name
$vm02name =$vm02.Name
$VMMparam = Invoke-Command –Computername VMM01 –ScriptBlock {
$VMMvm01=Get-SCVirtualMachine -VMMServer "VMM01.pandora.corp" -Name $using:vm01name
$vmcloud = $VMMvm01.Cloud
$vmos = $VMMvm01.OperatingSystem
$vmuserrole = $VMMvm01.UserRole
$vmowner = $VMMvm01.Owner
return $vmcloud,$vmos,$vmuserrole,$vmowner
}
$VMMcloud = $VMMparam[0]
$VMMos = $VMMparam[1]
$VMMuserrole = $VMMparam[2]
$VMMowner = $VMMparam[3]
Invoke-Command -ComputerName VMM01 -ScriptBlock {
if ($using:VMMcloud -eq $null){
Set-SCVirtualMachine -vm $using:vm02name -OperatingSystem $using:VMMos.Name
}
else{
Set-SCVirtualMachine -vm $using:vm02name -Cloud $using:VMMcloud -OperatingSystem $using:VMMos.Name -UserRole $using:VMMuserrole -Owner $using:VMMowner
}
}
This runs well until it's supposed to enter the Cloud object into the -cloud parameter. It ends in error:
Cannot bind parameter 'Cloud'. Cannot convert the "CLOUD01" value of type "Deserialized.Microsoft.SystemCenter.VirtualMachineManager.Cloud" to type "Microsoft.SystemCenter.VirtualMachineManager.Cloud".
+ CategoryInfo : InvalidArgument: (:) [Set-SCVirtualMachine], ParameterBindingException
+ FullyQualifiedErrorId : CannotConvertArgumentNoMessage,Microsoft.SystemCenter.VirtualMachineManager.Cmdlets.SetV
MCmdlet
+ PSComputerName : VMM01
If I call just the $using:VMMcloud variable inside of the Invoke-Command, it returns correctly. But when it's in parameter, just the Name value is returned. I tried it with arguments instead of prefixed variables, but same output.
Can you help me?
P.S. This is my first question in here. Hope the formatting is right and the problem described understandably. If not, ask away.

touch Function in PowerShell

I recently added a touch function in PowerShell profile file
PS> notepad $profile
function touch {Set-Content -Path ($args[0]) -Value ($null)}
Saved it and ran a test for
touch myfile.txt
error returned:
touch : The term 'touch' is not recognized as the name of a cmdlet, function,
script file, or operable program. Check the spelling of the name, or if a path
was included, verify that the path is correct and try again.
At line:1 char:1
+ touch myfile
+ ~~~~~
+ CategoryInfo : ObjectNotFound: (touch:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
With PowerShell there are naming conventions for functions. It is higly recommended to stick with that if only to stop getting warnings about it if you put those functions in a module and import that.
A good read about naming converntion can be found here.
Having said that, Powershell DOES offer you the feature of Aliasing and that is what you can see here in the function below.
As Jeroen Mostert and the others have already explained, a Touch function is NOT about destroying the content, but only to set the LastWriteTine property to the current date.
This function alows you to specify a date yourself in parameter NewDate, but if you leave it out it will default to the current date and time.
function Set-FileDate {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline = $true, Mandatory = $true, Position = 0)]
[string[]]$Path,
[Parameter(Mandatory = $false, Position = 1)]
[datetime]$NewDate = (Get-Date),
[switch]$Force
)
Get-Item $Path -Force:$Force | ForEach-Object { $_.LastWriteTime = $NewDate }
}
Set-Alias Touch Set-FileDate -Description "Updates the LastWriteTime for the file(s)"
Now, the function has a name PowerShell won't object to, but by using the Set-Alias you can reference it in your code by calling it touch
Here is a version that creates a new file if it does not exist or updates the timestamp if it does exist.
Function Touch-File
{
$file = $args[0]
if($file -eq $null) {
throw "No filename supplied"
}
if(Test-Path $file)
{
(Get-ChildItem $file).LastWriteTime = Get-Date
}
else
{
echo $null > $file
}
}
If you have a set of your own custom functions stored in a .ps1 file, you must first import them before you can use them, e.g.
Import-module .\MyFunctions.ps1 -Force
To avoid confusion:
If you have placed your function definition in your $PROFILE file, it will be available in future PowerShell sessions - unless you run . $PROFILE in the current session to reload the updated profile.
Also note that loading of $PROFILE (all profiles) can be suppressed by starting a session with powershell.exe -NoProfile (Windows PowerShell) / pwsh -NoProfile (PowerShell (Core)).
As Jeroen Mostert points out in a comment on the question, naming your function touch is problematic, because your function unconditionally truncates an existing target file (discards its content), whereas the standard touch utility on Unix-like platforms leaves the content of existing files alone and only updates their last-write (and last-access) timestamps.
See this answer for more information about the touch utility and how to implement equivalent behavior in PowerShell.

ValidateScript unexpectedly returning false when condition is true

I have a PowerShell function I'm writing to build and execute a variety of logman.exe commands for me so I don't have to reference the provider GUIDs and type up the command each time I want to capture from a different source. One of the parameters is the file name and I am performing some validation on the parameter. Originally I used -match '.+?\.etl$' to check that the file name had the .etl extension and additionally did some validation on the path. I later decided to remove the path validation but neglected to change the validation attribute to ValidatePattern.
What I discovered was that while it worked perfectly on the machine I was using to author and validate it, on my Server 2016 Core machine it seemed to misbehave when calling the function but that if I just ran the same check at the prompt it worked as expected.
The PowerShell:
[Parameter(ParameterSetName="Server", Mandatory=$true)]
[Parameter(ParameterSetName="Client", Mandatory=$true)]
[ValidateScript({$FileName -match '.+?\.etl$'}]
[string] $FileName = $null
The Output:
PS C:\Users\Administrator> Start-TBLogging -ServerLogName HTTPSYS -FileName ".\TestLog.etl"
PS C:\Users\Administrator> Start-TBLogging : Cannot validate argument on parameter 'FileName'. The "$FileName -match '.+?\.etl$'" validation script
for the argument with value ".\TestLog.etl" did not return a result of True. Determine why the validation script failed,
and then try the command again.
At line:1 char:50
+ Start-TBLogging -ServerLogName HTTPSYS -FileName ".\TestLog.etl"
+ ~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidData: (:) [Start-TBLogging], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Start-TBLogging
Trying it manually worked:
PS C:\Users\Administrator> $FileName = ".\TestLog.etl"
PS C:\Users\Administrator> $FileName -match '.+?\.etl$'
True
After changing the function to use ValidatePattern it works just fine everywhere but I was wondering if anyone could shed light on the discontinuity.
As Joshua Shearer points out in a comment on a question, you must use automatic variable $_ (or its alias form, $PSItem), not the parameter variable to refer to the argument to validate inside [ValidateScript({ ... })].
Therefore, instead of:
# !! WRONG: The argument at hand has NOT yet been assigned to parameter
# variable $FileName; by design, that assignment
# doesn't happen until AFTER (successful) validation.
[ValidateScript({ $FileName -match '.+?\.etl$' }]
[string] $FileName
use:
# OK: $_ (or $PSItem) represents the argument to validate inside { ... }
[ValidateScript({ $_ -match '.+?\.etl$' })]
[string] $FileName
As briantist points out in another comment on the question, inside the script block $FileName will have the value, if any, from the caller's scope (or its ancestral scopes).

Consume $args while also using parameter set names

Consider the following toy example script test.ps1:
Param(
[Parameter(ParameterSetName='readfile',Position=0,Mandatory=$True)]
[string] $FileName,
[Parameter(ParameterSetName='arg_pass',Mandatory=$True)]
[switch] $Ping
)
if ($Ping.isPresent) {
&$env:ComSpec /c ping $args
} else {
Get-Content $FileName
}
The desired effect would be that
.\test.ps1 FILE.TXT
displays the contents of FILE.TXT and
.\test.ps1 -Ping -n 5 127.0.0.1
pings localhost 5 times.
Unfortunately, the latter fails with the error
A parameter cannot be found that matches parameter name 'n'.
At line:1 char:18
+ .\test.ps1 -Ping -n 5 127.0.0.1
+ ~~
+ CategoryInfo : InvalidArgument: (:) [test.ps1], ParameterBindingException
+ FullyQualifiedErrorId : NamedParameterNotFound,test.ps1
This is just a minimal example, of course.
In general, I am looking for a way to introduce a [switch] parameter to my script that lives inside its own parameter set and when that switch is present, I want to consume all remaining arguments from the commandline and pass them on to another commandline application. What would be the way to do this in PowerShell?
You can use the ValueFromRemainingArguments parameter attribute. I would also recommend specifying a default parameter set name in CmdletBinding. Example:
[CmdletBinding(DefaultParameterSetName="readfile")]
param(
[parameter(ParameterSetName="readfile",Position=0,Mandatory=$true)]
[String] $FileName,
[parameter(ParameterSetName="arg_pass",Mandatory=$true)]
[Switch] $Ping,
[parameter(ParameterSetName="arg_pass",ValueFromRemainingArguments=$true)]
$RemainingArgs
)
if ( $Ping ) {
ping $RemainingArgs
}
else {
Get-Content $FileName
}
(Aside: I don't see a need for & $env:ComSpec /c. You can run commands in PowerShell without spawning a copy of cmd.exe.)