I'm looking to use a single host server to maintain a PowerShell script, with global variables, that can be interpreted and ran on several other devices in the network cluster.
On the main host I'd like to specifically be able to maintain a list of variables for IP addresses of each other device that I want to run the scripts against, but then how I want to run the script is something I'm having a hard time determining. There are several things I need to do to each other machine in the cluster (change the computer name, modify the time zone and time, configure the network adapters.... there's a decent list of stuff). The commandlets to do the functions on the individual machines is no problem... I have all of that written out and tested. I just don't what my options are for where that script is stored. Preferably, I think I'd like to declare all of the variables for everything that needs to be done on all machines, at the top of the file on the main host. Then I would like to break down everything that needs to be done to each host on the same file, on the main host. I know it will get a little messy, but that would make maintaining the cmdlets for each device much easier, especially when it comes to testing and making changes. Am I trying to do the impossible here??
I learned about using ENABLE-PSSESSION as well as INVOKE-COMMAND, but each seem to have their own challenges. With Enable-PSSession I cannot seem to find a way to wait for the script to connect to each host before it moves on to the next line. I've tried piping in Out-Null, as well as adding a Start-Sleep line. I don't want to have to manually connect to each host and then manually run the list of commands against each host. Invoke-Command doesn't seem to let me break out the SCRIPTBLOCK section into multiple lines.
Is there any suggestion for the best method of accomplishing the desire to run the script from the main host, that performs all of my cmdlets on multiple machines, without any additional human interaction??
Thanks so much!!
-Andrew
EDIT: I found that I can break the ScriptBlock line (contrary to what I thought didn't work yesterday). Here is basically what I'm trying to accomplish, though of course the below does not work when calling the variables from the top of the file:
#Edit These Variables
$NewName_Server2 = "Server2"
$NewName_Server3 = "Server3"
$NewName_Server4 = "Server4"
$IPAddress_Server2 = "10.10.10.2"
$IPAddress_Server3 = "10.10.10.3"
$IPAddress_Server4 = "10.10.10.4"
$TimeZone = "US Eastern Standard Time"
#Do Not Edit These Variables
$Server2 = "192.168.1.2"
$Server3 = "192.168.1.3"
$Server4 = "192.168.1.4"
#Configure Server 2
Invoke-Command -ComputerName $Server2 -ArgumentList $local -ScriptBlock {
Rename-Computer -NewName $NewName_Server2
New-NetIPAddress -InterfaceAlias "Wired Ethernet Connection" -IPv4Address $IPAddress_Server2
Set-TimeZone -ID $TimeZone
Restart-Computer -Force
}
#Configure Server 3
Invoke-Command -ComputerName $Server3 -ArgumentList $local -ScriptBlock {
Rename-Computer -NewName $NewName_Server3
New-NetIPAddress -InterfaceAlias "Wired Ethernet Connection" -IPv4Address $IPAddress_Server3
Set-TimeZone -ID $TimeZone
Restart-Computer -Force
}
#Configure Server 4
Invoke-Command -ComputerName $Server3 -ArgumentList $local -ScriptBlock {
Rename-Computer -NewName $NewName_Server3
New-NetIPAddress -InterfaceAlias "Wired Ethernet Connection" -IPv4Address $IPAddress_Server4
Set-TimeZone -ID $TimeZone
Restart-Computer -Force
}
You can use the using scope to access local variables. I don't know what $local is. Nice try.
$a = 'hi'
invoke-command comp001,comp002 { $using:a }
hi
hi
The other way is using a param, not well documented. Passing arrays is more tricky.
invoke-command comp001,comp002 { param($b) $b } -args $a
Related
I am trying a write an application to fetch the version of the application installed on remote machines. There is a need to query many remote servers and get the version of the application and show it on the dashboard.
Powershell WMI takes too long to get the information. I am looking for something lot faster.
The app will read remote server information like IP, Username, and password from a config file and fetch the data.
Any help is really appreciated.
It sounds like you want to take a closer look at Powershell Sessions.
There are at least two ways to approach in from there, one is using Invoke-Command in combination with the -ComputerName attribute, possibly along with -Authentication or -Credential. -ScriptBlock contains the code you want to run.
Invoke-Command -ComputerName "computername.domain.local" -ScriptBlock { ... }
I assume from "the application" that your concern is one application, and not every application. Then you should be able to tell the version by running Get-Item on the executable, then look at either VersionInfo.ProductVersion or VersionInfo.FileVersion, whichever is more relevant to your case.
To access one of them, you could use something like:
$version = (Get-Item "path-to-executable\executable.exe').VersionInfo.ProductVersion
To find out which attributes are relevant to your executable, you can run
Get-Item "executable.exe" | Select -ExpandProperty VersionInfo | Format-List *
Combining these techniques, you could try something like this.
# this is a dummy array for example purposes
$computers = #(#{'ip' = '127.0.0.1'; 'username' = 'admin'; 'password' = 'password'})
foreach($computer in $computers)
{
# creating a PSCredential object from plain text passwords is not a good practice, but I'm assuming here that's what you've got to work with
$credentials = [System.Management.Automation.PSCredential]::new($computer.username, (ConvertTo-SecureString -String $computer.password -AsPlainText -Force))
# fetch versioninfo info from remote computer
$versioninfo = Invoke-Command -ComputerName $computer.ip -Credential $credentials -ScriptBlock { Get-Item "executable.exe" | Select -ExpandProperty VersionInfo
if ($versioninfo.ProductVersion -ne '3.1.2414.0')
{
# do something if product version isn't 3.1.2414.0
}
if ($versioninfo.ProductVersionRaw.Major -lt 5)
{
# do something if product version major part is less than 5 (true for 1.5.5.5 but false for 5.1.1.1)
}
}
If you want to run several commands on the client computers, use New-PSSession and pass the session along to every call to Invoke-Command, otherwise you'd lose time and resources opening a new session every time.
Here's an example on how that could be achieved:
$session = New-PSSession -ComputerName $computer.ip -Credential $credentials
$versioninfo = Invoke-Command -Session $session -ScriptBlock { # do something }
if ($versioninfo.ProductVersion -lt 1)
{
Invoke-Command -Session $session -ScriptBlock { # do something else }
}
Remove-PSSession -Session $session
You might also want to check out the using: scope modifier if you find a need to pass variables along to the remote computer, which would make $localvariable visible at the remote computer with $using:localvariable (readonly)
If time is still a concern after this (especially with tcp timeouts on offline hosts), then threading is the next topic you'd want to look into.
As far as I know, my code is compatible with Powershell v3.1, but I recommend using no less than v5, especially on the machine running the script.
This should be enough information to send you on your way. Good luck. :)
I have a handful of computers on thier own VLAN seperated by a firewall. Since I need to remote to the machines I have to make sure the times dont drift so I setup my own time script for them that matches the time with my script server. The problem I run into is that the users who utilize those machines require a different timezone for all of their proprietary software to have correct timestamps. Obviously if there is a difference in timezones between my script server, domain controller, or end machine remoting becomes almost impossible. I know I can add secondary timezone clock manually but I wanted to make sure I could manage those remotely as well.
I searched high and low for an article showing me how to create a secondary time zone system clock but was unsuccessful. I wanted to alter what I currently have to simply add another clock with a different timezone. I can alter it after that. I have included something similar to the script I have in use.
$computers = Get-Content -path "C:\Path"
$date = Get-Date
$timezone = Get-TimeZone
Foreach ($computer in $computers){
if(Test-Connection -computername $computer){
$session = new-PSSession -Computername $computer
$change = $null
$change = Invoke-Command -Session $session -scriptblock {
Set-Date -date $using:date
tzutil /s "Pacific Standard Time"
}
if($change){Write-host "Sucessfully changed $computer to $change"}
remove-PSSession -computername $computer
}
}
I'm working on a script to copy a folder from a UNC path to a local server. I'm remotely running my script through an interactive session and utilizing Invoke-Command -ScriptBlock like so:
Invoke-Command -ComputerName MyServer -ScriptBlock $Script
This is the script to do the copying:
$script {
try {
New-PSDrive -Name MyDrive -PSProvider FileSystem -Root \\uncpathserver\e$\SourceCode\ -Credential Contoso\me
Copy-Item -Path \\uncpathserver\e$\SourceCode\* -Destination E:\Inetpub\Target -Recurse -Force
}
catch {
Write-Host "Failed to copy!"
}
}
It is failing and throwing my catch block every time. I can't seem to figure out what I am missing to get this to work - it seems so simple and I hope I'm not missing something blatantly obvious.
EDIT:
I was able to get it to work by now just running the script from my local PC instead of from a server. I'm calling the file copy out of $script block now as well. This is what the new code looks like:
$MyServers= #("server-01", "server-02")
foreach ($server in $MyServers)
{
$TargetSession = New-PSSession -ComputerName $server -Credential
contoso\me
Copy-Item -ToSession $TargetSession -Path C:\Source\TheCode\ -
Destination "E:\InetPub\wherethecodegoes" -Recurse -Force
}
Everything else I'm doing inside my $script block (which has been omitted here for troubleshooting sake) is working A-OK. I do have to enter my credentials for each server, but due to the small nature of servers I'm working with, that isn't a deal breaker.
Sounds like a 'kerberos double hop' problem.
Short-Answer
Avoid the problem. From your system, setup two PSdrives. Then copy \\uncpathserver\e$\SourceCode\ to \\RemoteIISserver\E$\Inetpub\Target\
Long-Answer
From your system (System A), you are remotely executing a script (on System B) that will copy a remote folder (from System C).
It should work, but it doesn't. This is because when you (specifically, your account) from System A, remotely connects to System B, then asks System C for something, 'System C' doesn't trust you.
A quick google of the problem will show a myriad of ways around this issue, however;
Not all methods are secure (example: CredSSP)
Not all methods will work on your version of Windows (which is...?)
Not all methods will work with PowerShell
One secure method that does work with PowerShell leverages delegation.
This can be a bit daunting to setup, and I suggest you read-up on this thoroughly.
## Module 'ActiveDirectory' from RSAT-AD-PowerShell Windows feature required.
$ServerA = $Dnv:COMPUTERNAME
$ServerB = Get-ADComputer -Identity ServerB
$ServerC = Get-ADComputer -Identity ServerC
Delegate 'Server B' to access 'Server C';
# Set the resource-based Kerberos constrained delegation
Set-ADComputer -Identity $ServerC -PrincipalsAllowedToDelegateToAccount $ServerB
# Confirm AllowedToActOnBehalfOfOtherIdentity.Access is correct (indirectly).
Get-ADComputer -Identity $ServerC -Properties PrincipalsAllowedToDelegateToAccount
Wait about 15 minutes for 'Server B' to sync-up (or just reboot it).
You can force this with the following (Note: $Cred should contain your credentials);
Invoke-Command -ComputerName $ServerB.Name -Credential $cred -ScriptBlock {
klist purge -li 0x3e7
}
Run a test-hop;
Invoke-Command -ComputerName $ServerB.Name -Credential $cred -ScriptBlock {
Test-Path \\$($using:ServerC.Name)\C$
Get-Process lsass -ComputerName $($using:ServerC.Name)
Get-EventLog -LogName System -Newest 3 -ComputerName $($using:ServerC.Name)
}
The downside is you have to setup every remote remote-target (every 'Server C') this way. But the upside is that it's secure.
I already have code written to connect to another computer in my house and pull music files from the C drive. However, I am trying to find out how to keep this code, but modify it in a way that I can use it to run code on the second computer, then save it to a text file.
foreach ($server in Get-Content .\serverList.txt){
psexec \\$server -u username-p password cmd /c dir c:\*.mp3 /s > c:\Powershell\$server.txt
}
You could write a book on PowerShell Remoting (several people have) but it's reasonably straightforward.
On both computers run Enable-PSRemoting to configure all the settings. Then on the originating computer (the one making the remote call) run Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' (if you are security conscious replace the * with the IP of the remote PC).
Then you can run the all-powerful Invoke-Command to do all sorts of awesome stuff remotely. Unless you're on a domain or there's an identical user on the remote PC you'll need to provide credentials which means either prompting for them or saving them, but if I go into too much detail we'll both be here all day. Pretty easy to find the answers on Google.
$cred = Get-Credential
foreach ($server in Get-Content .\serverList.txt) {
Invoke-Command $server -Credential $cred -ScriptBlock { Get-ChildItem C:\*.mp3 -Recurse } | Out-File C:\Powershell\$server.txt
}
I am wondering which is best practice considering both examples will probably work. Using the built in help examples I have written a script to install windows features on remote servers. Here is my code:
$servers = ('server1', 'server2', 'server3', 'server4')
ForEach ($server in $servers) {
Install-WindowsFeature -Name Desktop-Experience -ComputerName $server -IncludeAllSubFeature -IncludeManagementTools -Restart
}
Would the above be preferred OR should I wrap the "Install-WindowsFeature ..." in an "Invoke-Command" block like the following?
Invoke-Command -ComputerName server1, server2, server3, server4 -command {
Install-WindowsFeature -Name Desktop-Experience -ComputerName $server -IncludeAllSubFeature -IncludeManagementTools -Restart
}
Thanks for your insight!
Personally I would use the latter (directly call Install-WindowsFeature -ComputerName $server rather than do a separate Invoke-Command) in this case for the following reasons:
You may be hard-coding the feature names now, but in the future you may want to put those in a variable. If you put them in a variable, you'll have to pass it as a parameter into the Invoke-Command's script block. This is entirely possible, but more work.
By using your own loop, you can write progress messages, logging, etc.
You gain nothing by using Invoke-Command in this case because you're running a single command on the remote computer (as opposed to running multiple commands with -ComputerName parameters vs. running multiple commands inside the script block).