Multithreading foreach loop when using invoke-vmscript in powershell/powercli - powershell

I have a script that modifies network info of certain VMs ,however it run sequentially . Since there are hundred of VMs , I want to all VMs to execute the command simultaneously in order to save time . Also how do I add invoke-vmscript to the scriptblock with external variables. I did try few things but doesnt seem to working well. Script is run on a system using powercli 10 on linux and no additional modules are allowed to be installed
Below is the code , any help would be much appreciated
$test=get-vm -name testvm* |sort-object
$fip='192.168.10.41'
$f=$fip.Split(".")|select -first 3
$s=[system.String]::join(".",$f)
$l=$fip.Split(".")|select -last 1
foreach ($vm in $test) {
Write-Host "$vm $s.$l"
do {start-sleep -s 2; write-host " $vm not ready yet" ; $tstatus=(get-vm
$vm).extensiondata.Guest.ToolsStatus;}while($tstatus -ne "toolsOk");
invoke-vmscript -ScriptText "sed -i '14,20 d' /etc/network/interfaces" -
ScriptType bash -VM $vm -GuestUser admin -GuestPassword Welcome!}
$ipscript = "sed -i -e 's/address *.*.*.*/address $s.$l/g'
/etc/network/interfaces"
write-host " Adding IP "
invoke-vmscript -ScriptText $ipscript -ScriptType bash -VM $vm -GuestUser
$GuestUserName admin -GuestPassword Welcome!
Start-Sleep -s 2
Restart-VM -VM $vm -RunAsync -Confirm:$false
$l=[int]$l+1
}

Related

PWSH | Parallelize Invoke-VMScript

I'm trying to parallelize a portion of a bigger PWSH script, basically I need to cycle through a list of VMs and execute some commands on them, and to speed up the process, I need this to be executed in parallel:
#Utils.ps1 contains all the functions
.(".\src\utils.ps1")
# List of VMs on which we execute the space reclaim
$VMs = Get-Content .\vmnames.txt
# Check if directory ./Results exists, if not it will be created
CheckDirResults
# Check if dir temp exists, if not it will be created
CheckDirTemp
# Asks for vCenter credentials
GetVServerCred
# Asks for VM credentials
GetVMCred
# Connects to vCenter
ConnectVcenter
# Commands that will be executed on VMs on line 94 (Invoke-VMScript)
$InvokeScript = #'
#! /bin/bash
pwd
'#
foreach ($vm in $VMs) {
$vms = Get-VM -Name $vm
$vms | ForEach-Object -Parallel {
Write-Output "Testing on $_"
Invoke-VMScript -VM $_ -GuestCredential $VMCred -ScriptText $InvokeScript -Confirm:$false
}
}
I also tried to simply execute the Invoke directly on $VMs like this:
$VMs | ForEach-Object -Parallel {
Write-Output "Testing on $_"
Invoke-VMScript -VM $_ -GuestCredential $VMCred -ScriptText $commands
}
In both cases, the Invoke-VMScript can't either connect to the VMs or can't find the chunk of code to execute on the VMs.
Errors:
"test.ps1" 44L, 934C 36,0-1 81%
---- ---- ----
evl6800756.sys.ntt.eu 443 VSPHERE.LOCAL\Administrator
Testing on euczdrpoc03
Testing on euczdrpoc30
Invoke-VMScript:
Line |
3 | Invoke-VMScript -VM $_ -GuestCredential $VMCred -ScriptText $comma …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| 1/17/2023 10:42:02 AM Invoke-VMScript You are not currently connected to any servers. Please connect first using a Connect cmdlet.
Invoke-VMScript:
Line |
3 | Invoke-VMScript -VM $_ -GuestCredential $VMCred -ScriptText $comma …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| 1/17/2023 10:42:02 AM Invoke-VMScript Value cannot be found for the mandatory parameter ScriptText
Invoke-VMScript:
Line |
3 | Invoke-VMScript -VM $_ -GuestCredential $VMCred -ScriptText $comma …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| 1/17/2023 10:42:02 AM Invoke-VMScript You are not currently connected to any servers. Please connect first using a Connect cmdlet.
Invoke-VMScript:
Line |
3 | Invoke-VMScript -VM $_ -GuestCredential $VMCred -ScriptText $comma …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| 1/17/2023 10:42:02 AM Invoke-VMScript Value cannot be found for the mandatory parameter ScriptText
The $using: scope modifier inside a -Parallel script-block lets you refer to variables assigned a value outside that script-block. Examples in your question are not complete enough to say for certain, but hopefully this explains at least some of the difficulty.
So for example within the parallel block, try $using:commands instead of your $commands. This bit from reference doc could be more helpfully spelled out, IMHO.
Use the $using: keyword to pass variable references to the running
script.
That's demonstrated more clearly in one of the reference examples, in this blog post, and this answer.
Edit: sounds like the parallel runspaces do not inherit the vCenter connection. The open proposal for a -UseCurrentState switch to transfer the current runspace state to -Parallel loop iterations might help in your situation. Meanwhile could try passing -Server $using:DefaultVIServer to relevant commands inside the parallel block. LucD's comment that PowerCLI is not threadsafe may also be worth researching alongside the note in the reference doc:
The ForEach-Object -Parallel parameter set runs script blocks in parallel on separate process threads. The $using: keyword allows passing variable references from the cmdlet invocation thread to each running script block thread. Since the script blocks run in different threads, the object variables passed by reference must be used safely. Generally it is safe to read from referenced objects that don't change. But if the object state is being modified then you must used thread safe objects, such as .NET System.Collection.Concurrent types
I managed to do it this way:
# Commands that will be executed on VMs on line 94 (Invoke-VMScript)
$commands = #'
#! /bin/bash
hostname
'#
# Loads the VMs names from the file vmnames.txt
$vms = Get-Content ".\vmnames.txt"
# Uses the content of the file vmnames.txt to get the VM objects
$vmnames = Get-VM -Name $vms
# Executes the commands on the VMs
$vmnames | ForEach-Object -Parallel {
# Loads the variables in the parallel session
$commands = $using:commands
$VMCred = $using:VMCred
Invoke-VMScript -VM $_ -GuestCredential $VMCred -ScriptText $commands
}
For some reasons, piping directly $vms (so the content of the file to ForEach-Object -Parallel won't pass correctly the value (in this case the VM name), while using the Get-VM -Name $vms will do.
Then, once this is solved the ForEach-Object -Parallel will finally get the correct VM name assigned to $_.

Setting the VirtualHardDiskPath when adding a virtual machine through powershell to Hyper-V

I'm adding a vm through powershell to Hyper-V. The add is working but it's setting the config/xml files on the same drive as the vhdx file.
I am setting the $config and then running my new-vm.
$config= Get-VMHost | Select-Object VirtualMachinePath
I end up with this:
#{VirtualMachinePath=F:\vmconfigs}
This is how I'm adding the vm:
New-VM -Name $name -MemoryStartupBytes 8192MB -VirtualHardDiskPath $config -Path $driv\vm -Generation 2 -SwitchName (Get-VMSwitch).Name
If I run it without the -VirtualHardDiskPath, it places the configs in a folder on the same drive as the vhdx file. Of course, it will not run with the way it's set with the path added since it is not formatted correctly.
You can see here that my default is f:\vmconfigs but it's not using that folder when I manually add it.
So, I have two questions. First, how do I get the VirtualMachinePath correctly. Second, why isn't it putting the configs in the default folder (f:\vmconfigs) if I do not set it with powershell at the command line? If I add it through the interface, it is correct.
Thanks!
EDIT
This is what happens:
Even though the virtual machine path is f:\vmconfigs
My current command:
New-VM -Name $name -MemoryStartupBytes 8192MB -Path $driv\vm -Generation 2 -SwitchName (Get-VMSwitch).Name
I wasn't using -path correctly. I ended up with this:
# ---------------- vhdx File Locations -----------------
# the main virtual machine to be added
$sysprep= "C:\SysPrep\sysprep2019.vhdx"
# the 'd: drive' that is to be added
$sysprep2= "C:\SysPrep\sysprep2019_2Drive.vhdx"
# ---------------- Hardware Settings -----------------
# number of processors to allocate
$numprocs= 6
# startup memory, defaults to 8gb
$startupmem= 8192MB
Write-Output "Creating the new virtual machine $name."
New-VM -Name $name -MemoryStartupBytes $startupmem -Generation 2 -SwitchName (Get-VMSwitch).Name
Write-Output "Adding $sysprep to the new virtual machine."
Add-VMHardDiskDrive -VMName $name -Path $newfile
if($secdrive -match 'y')
{
Write-Output "Adding second drive to guest operating system."
Add-VMHardDiskDrive -VMName $name -Path $newfile2
Write-Output "$sysprep2 has been attached."
}
# set number of processors
Write-Output "Setting the number of processors to $numprocs"
Set-VMProcessor $name -Count $numprocs
Granted, this is only part of my script. So, I create the VM first in Hyper-V then add the drives to it.

Invoke-Command doesn't return to local machine but software is installed

The tl;dr: I'm puzzled as to why my script isn't returning back to the deployment machine. Does anyone have an idea why?
I was provided an EXE and a couple arguments to install some software. I'm using PowerShell's Start-Process to run it. The deployment machine is Win Server 2012 domain controller, logged in and being run as domain admin. The test machines are two Windows 10 Pro domained machines. All three have PS 5.1.
Being a silent install, there is no user interaction required. If I run the exact command locally, via RDP, or in a pssession, with just c:\Software.exe /silent /arg2removed, it installs and returns as expected.
The script runs fine up to a point. Nothing happens after Start-Process inside Invoke-Command 's -ScriptBlock. In a separate PowerShell window, I can use Enter-PSSession for each of the two client machines, and Get-Service and Get-Process both show the software's service and background processes, respectively. I can Ctrl+c on the deployment machine and get back to a prompt. No errors are reported at any time.
Here's the Start-Process chunk. I've read the help and it doesn't sound like I'm missing anything that would allow the ScriptBlock to finish. If I prepend Start-Process with Write-Host (like we all do), it echoes the command that would run and I get back to a command prompt on the deployment machine.
# Start the installer.
Start-Process `
-FilePath "C:\${using:SrcExe}" `
-ArgumentList "/SILENT", "/arg2removed" `
-WorkingDirectory C:\ `
-Wait `
-Verbose `
-ErrorAction SilentlyContinue `
-ErrorVariable InstallErrors
Here's most of the script. The only items before Invoke-Command are where I set up $ComputersToInstallOn, enter the credentials (yes I'm sure they're correct), and supply the path to the EXE.
Invoke-Command `
-ComputerName $ComputersToInstallOn `
-Credential $Creds `
-Verbose `
-ErrorAction SilentlyContinue `
-ErrorVariable InvokeCommErrors `
-ScriptBlock {
# Get and print the destination machine's hostname
$ThisMachine = Get-Content Env:\COMPUTERNAME ; $ThisMachine
# Print the current date and time
Get-Date
# Check if Sentinel processes are running. If not, assume it's not installed.
$S1Procs = get-process sentinel*
if([string]::IsNullOrEmpty($S1Procs)) {
# Sentinel isn't installed. Continue.
# Map a drive letter to $SrcFolder. Not theoretically necessary but Start-Process complains when copying with the UNC path directly.
New-PSDrive `
-Name S `
-PSProvider FileSystem `
-Credential ${using:Creds} `
-Root ${using:SrcFolder} `
-verbose
# List remote folder
Get-ChildItem S:\
# Copy the $SrcExe to C:\
Copy-Item `
-Path "S:\${using:SrcExe}" `
-Destination C:\ `
-Verbose `
-ErrorAction Stop `
-ErrorVariable CopyErrors
# Unmount drive
Remove-PSDrive S -verbose
# Verify EXE exists locally
Get-ChildItem -Path C:\${using:SrcExe}
# If there were copy errors, abort.
if ($CopyErrors) {
Write-Host "There was an error copying '${using:SrcExe}' to $ThisMachine. Aborting."
exit 1 } else {
# All good so far. Continue to install.
Write-Host "$(Get-Date -UFormat '%Y%m%d %H:%M:%S') : Starting install on ${ThisMachine}. You may need to Ctrl+C to return to the local machine. Check processes on each machine though."
# Start the installer.
Start-Process `
-FilePath "C:\${using:SrcExe}" `
-ArgumentList "/SILENT", "/arg2removed" `
-WorkingDirectory C:\ `
-Wait `
-Verbose `
-ErrorAction SilentlyContinue `
-ErrorVariable InstallErrors
# ScriptBlock doesn't seem to make it to anything after Start-Process.
# Remove the EXE.
Remove-Item "C:\${using:SrcExe}" -Verbose -ErrorAction SilentlyContinue
exit 0
# Get-Process -Name Sentinel*
# echo "Sleeping. Now would be the time to abort."
# Start-Sleep 15
}
} else {
Write-Host "Sentinel appears to be installed and running."
$S1Procs
Get-Service -Name Sentinel* | Where-Object { $_.Status -match "Running" }
exit 0
}
}
if($InvokeCommErrors){
Write-Host "There were some errors."
}
EDIT: Added some requested info.

Setting VLAN from CSV File in PowerCLI Script

I am trying to compile a PowerCLI script to help automate the creation of VMs from a CSV file's data. Currently the script I am running looks like this:
# Specify vCenter Server, vCenter Server username and vCenter Server user password
write-host “Please specify vCenter Server and enter credentials” -foreground green
$vc = read-Host "Connect to which vCenter Server?"
write-host “Connecting to vCenter Server $vc” -foreground green
$CSVPath = "$ScriptRoot\PROD_VMRequestTEST.xlsx.csv"
Connect-VIServer -Server $vc
$CSVFile = Import-Csv -Path $CSVPath
Import-Csv -Path $CSVPath
# Specify vCenter Server Virtual Machine & Templates folder
$Folder = “Discovered virtual machine”
#
# Specify the vSphere Cluster
$Cluster = 'vSphere 5.5 RND'
$esx = Get-Cluster $Cluster | Get-VMHost -State connected
Import-Csv -Path $CSVPath | %{
Write-Host “Creation of VM $_.Name initiated” -ForegroundColor green
New-VM -Name $_.Name -VMHost ($esx | Get-Random) -Location $Folder
Write-Host “Power On of the VM $_.name initiated” -ForegroundColor green
Start-VM -VM $_.Name -confirm:$false -RunAsync
}
I have encountered a few examples online dealing with setting the VLAN via PowerCLI but none of them seem to relate directly to my example of using the CSV file to determine the VLAN.
One example I tried using with my script was:
$VMhost = Get-vmhost $_.VMHost
$PortGroup = Get-VirtualPortgroup -name $_.VLAN -VMhost $VMhost
However, I get errors relating to the -Name argument in that line.
On the script context, I believe the error is causing because one of the columns value seems empty(in your case its Name). I can see that you are getting all the values in a foreach loop. I would recommend you to create a sample csv, insert only 2 set of data and see if it works. This way atleast you can narrow down the issue.
Hope this helps...!!!

VMware PowerCLI Variables in ScriptText

Hello I'm having a bit of problem with the Invoke-VMScript cmdlet
I have made a script that creates a virtual Windows 7 machine and then some powershell script executes on the machine for example renaming the computer to the correct name.
But if I run
Invoke-VMScript -ScriptText {(Get-Wmiobject -Class Win32_ComputerSystem).Rename($strName)}
The $strName variable doesn't get resolved, anyone have any idea on how to do this?
Your haven't correctly scoped your variable. Here's a simple experiment you can try in your console:
PS C:\> $test = "Resolve!"
PS C:\> $test
Resolve!
# This scriptblock will not resolve the variable.
PS C:\> {$test}
$test
# This scriptblock will resolve the variable.
PS C:\> [scriptblock]::Create($test)
Resolve!
Invoke-VMScript's documentation suggests you should pass -ScriptText as a string instead of a ScriptBlock. So in this case, we can mimic example 2:
$string = "(Get-Wmiobject -Class Win32_ComputerSystem).Rename($strName)"
Invoke-VMScript -ScriptText $string
Variables enclosed by " " will be resolved.
I don't have a guest handy for renaming, but following LucD, here's a working example using variables to build up input for scriptText:
$svcName = "vmtools"
$guestScript = 'get-wmiObject win32_service | where {$_.name -eq "' + $svcName + '"}'
Invoke-VMScript -vm myVMname -scriptText $guestScript
#Rename Computer command line script
$renamecomputer = "wmic path win32_computersystem where ""Name='%computername%'"" CALL rename name='$VM'"
#Send command line script to GuestOS
Invoke-VMScript -VM $VM -GuestUser $GU -GuestPassword $GP -ScriptType Bat -ScriptText $renamecomputer
This is what i use for my renaming script.
Works well
I got this working:
Invoke-VMScript -VM $vm -ScriptText "(Get-WmiObject win32_computersystem).rename(""${vmName}"")"