If(Code -ne ISE){DontFret}

I’m behind the times, as usual…

I’ve been reluctant to give up my PowerShell ISE with ISEsteroids for years now, but I think it’s finally time to get onboard with VS code. It’s definitely a bit of a shift, so I thought that I’d add my thoughts to the chorus of users who’ve made the switch from ISE to Visual Studio Code.

I have an odd sense of humor, so I thought it would be fun to use ISE to download and install it’s replacement. Yes, I know it would’ve been faster to simply get it via browser, but did I mention my odd sense of humor…


$Uri="<a href="https://go.microsoft.com/fwlink/?Linkid=852157">https://go.microsoft.com/fwlink/?Linkid=852157</a>"

$download="$Home\Downloads\vscode_installer.exe"

Invoke-WebRequest -Uri $uri -OutFile $download

Start-Process -FilePath $download -ArgumentList "SP","/silent","/Log"

So after allowing UAC to run the file, we have a base install of VS code, that we can launch by simply typing the command ‘code‘ inside any Windows command interpreter.

2018-06-04 14_53_22-Untitled-1 — Visual Studio CodeSince I write pretty much exclusively for PowerShell, there’s a couple of things that I need to do right out of the gate to make this tool useful. First off, code is meant to be portable and to fit many needs, so there isn’t a ton installed out of the gate. Code handles this conundrum via Extensions. To add an extension, simply click Extensions in the Activity bar. This will open up the Extension marketplace. In the marketplace simply search for the desired extension, in this case PowerShell and hit install. Code will make you reload your session in order to make use of the newly added PowerShell features.

I’m almost exclusively going to be writing PowerShell, so I’d like this to be configured  as best I can for that purpose. Step 1, make PowerShell the default terminal. We can do this a couple of ways, although it looks like the folks on the Code team may have changed the default behavior since the last time I looked. But I digress…

We can get to our user settings from the File Menu -> Preferences -> Settings, however I want to use one of the powerful features of Code, the command palette. The command palette is a very dynamic and powerful tool in Code, but much has already been written on it, so no need to retread the same ground. After entering the palette by typing ctrl-shift-p , I simply start typing what it is that I’m looking for, in this case default shell, and IntelliSense figures out the rest. After selecting the setting I’d like to change, VS code kindly offers me some suggestions. Sure enough after selecting the PowerShell option for “Terminal: Select Default Shell” I see a new setting in my user settings json file.

 

Finally when I go to check out my terminal, viola PowerShell is my default:2018-06-04 15_50_19-settings.json — Visual Studio Code

Next up, I want to make sure that my default language for VS code is PowerShell. This time I manually edit my settings (File Menu -> Preferences -> Settings) and add the line for “files.defaultLanguage”: “powershell”

 The reason for this is that by default VS Code files are in a plain text format (*.txt, *.gitignore). I’m sure that’s great for a lot of folks, but for me I use PowerShell in a day to day operational role. I’m not always writing code for reuse, often I’m writing and executing ad hoc scripts to be run via my friend F8. By changing the default language, when I start up a new, untitled and yet to be saved script Code knows that I’d like it to be interpretted as PowerShell and they even put the pretty icon for PoSh in the tab for my script.
2018_06_05_16_25_28_Untitled_1_Visual_Studio_Code
Oh, you can also see that IntelliSense recognizes the language and provides context specific assistance as well. BTW for this configuration, I found that I needed to restart Code completely, not just reload the window for the defaultlanguage setting to take effect.

The last thing I need to customize to make this feel like home is to set the switch for “powershell.integratedConsole.focusConsoleOnExecute”: false. Code’s default behavior is to move context from the script selection to the console on execution. If you’ve been using ISE, you’re used to the context staying at the script selection. Setting this switch to false will replicate the ISE behavior of not changing focus.

At this point I have Code configured to feel familiar enough that I can start using it for some functions. If you just want to jump to the punchline, here’s my very simple settings.json file.

{
"terminal.integrated.shell.windows": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
"files.defaultLanguage": "powershell",
"powershell.integratedConsole.focusConsoleOnExecute": false
}

14727674df540bb3e5302bcb011cfb45_0As a long time ISESteroids user thought I’m having a really hard time making the shift from having a persistent Variables window to using the Code debugger. I’m not sure if I’m being a crotchety old man pounding away at the keyboard or if this will really be a road block for me making the shift permanently to Code. I guess time will tell…. In the meantime, if you’re new to using this tool, I’d urge you to pay attention to the tutorial Microsoft provides on install of VS code. It’s quite a solid walk through and will help with some of nuances of getting started.

If you have any additional tips and tricks on making the shift from ISE to Code, please reach out.

Until next time, happy writing.

Advertisements

Exchange Out of Office via PowerShell

I think we’ve all been in the situation where we take off on a lovely vacation and at some point in that first 24 hours you go “OH $^%# I forgot to set my out of office message!” We recently had that happen in a critical department at work and rather than deal with the ECP, I wanted to script this out as the situation is not abnormal…

So without further ado:

$user="username"
$enddate=get-date "April 22"
$inmessage="I am out of the office until $($enddate.toshortdatestring()). For immediate access, please contact my supervisor: $forwardaddress. Thank you and have a great day!"
$outmessage=$inmessage
$autoreply="Scheduled"

### if you would also like to setup a forward, change the value of $forward to true
$forward=$false
$forwardaddress="manager@domain.org"
$fork=$true

Set-MailboxAutoReplyConfiguration -Identity $user -InternalMessage $inmessage -ExternalMessage $outmessage -EndTime $enddate -AutoReplyState $autoreply

if($forward){
     Set-Mailbox $user -ForwardingAddress $fowardaddress -DeliverToMailboxAndForward $fork
}

lines 1-5 & 8-10: Set our variables because we don’t HARD-CODE. The first rule of Powershell club is that “YOU DON”T HARD-CODE VALUES!”  Actually this is ripe for converting to parameters in v2…
line 12: Here we come to the meat of the script, the Set-MailboxAutoReplyConfiguration cmdlet. As with most well formed cmdlets, once you find the relevant cmdlet, the parameters are pretty self explanatory… That being said, here’s a breakdown:

-Identity   This is the unique identifier for the mailbox. It accepts many value types that you can review here, but the major elements you’d pass in here are name, CN or email address.
-InternalMessage/-ExternalMessage   These are the messages that you’re sending out to your respective users. There are lots of examples about how to configure this parameter if you want to use HTML, but if you want to just add a simple text message, type away.
-EndTime   OK, I’m starting to feel dumb now, because this is pretty self-explanatory. This is when you want the Out Of Office message to expire. In my example script I use this as part of the message as well as a script parameter.
-AutoReplyState   You have three options here “Enabled” or send my OOO messages. “Disabled” or don’t send my OOO messages. “Scheduled” or send my OOO messages during the timeframe I already informed you about dummy!

line 14: On line 14 we check if the $forward variable is set to true. If not, have a good day! If it is true, we proceed to…

Line 15: We use a separate cmdlet here, Set-Mailbox. The Set-Mailbox cmdlet is used for many purposes related to general mailbox maintenance. There are dozens of parameters for this script, but in our case we use the parameters specifically to forward email messages on to the vacationing employees manager:

-ForwardingAddress   Where you are sending the emails to.
-DeliverToMailboxAndForward   If you want to forward emails AND deliver the messages to the original destination mailbox, set this to $true. If you’d like the mails to forward only, this value should be set to $false. IE. Messages are NOT delivered to the original mailbox if you set this to $false.

There is a lot you can do with Set-MailboxAutoReplyConfiguration, but when you run Get-MailboxAutoReplyConfiguration there ain’t a lot of options…

 

2018-04-18 11_16_25-mRemoteNG - confCons.xml - Exchange

In this final example we see that our message is set as we configured and the end date of our scheduled message has been set for our forgetful vacationer. As in previuos experiences (and blog posts) the Exchange PowerShell modules lay waste to what could have been a tedious problem.

Until next time…

CTVMUG – Ditch the UI

2018-03-01 21_33_09-ClipboardFirst off, if you attended my session at the CT Usercon, thank you for attending and engaging. If you weren’t there, I have to ask WHY WEREN’T YOU!?!

I honestly tried to jam a bit much content into the presentation, so as promised here is all of the code I presented today, complete with more detailed comments (after I get some rest). The code is way too much for one post, so I’m going put the code out now and expand on the posts more in the coming days.

Create a new vDS and add a server   (updated March 3, 2018)

Update from .msi based PowerCLI to module-based PowerCLI from the PowerShell Gallery (updated March 13, 2018)

PowerShell Building Blocks: increasingly larger samples of VM deployment (updated)

Building Blocks: modify disks via PowerCLI #1 (updated)

Building Blocks: PowerCLI HardDisk #2, introducing parameters and functions

Cleanup snapshots

Tag based SPBM policy

Checking SPBM compliance

Install programs via Invoke-VMScript (updated March 3, 2018)

Performance Reports via vCenter statistics

Get-EsxCli oh my! (updated March 2, 2018)

vRops & Remove-VM

Exchange Forward Rules via PowerShell

I’m really quite enamored with the Exchange PowerShell modules lately. Well to be honest, I’m just enamored with PowerShell in general… For someone who is a part time Exchange admin, the option to script out changes is really nice. I’d like to share a couple of examples of how simple managing forwarding rules is with PowerShell.

Recently we had someone go out on maternity leave and their manager wanted to keep up with their email. Reasonable request. Super easy to implement with PowerShell.

$ForwardingUser="OriginatingUsersSamAccountName"
$ForwardedToUser="DestinationUsersSamAccountName"

Set-Mailbox $ForwardingUser -ForwardingAddress $ForwardedToUser -DeliverToMailboxAndForward $true

It’s a pretty simple one-liner using Set-Mailbox. I was a little lazy on this one and used the positional parameter for -Identity to set the Originating user. Bad Scott, but I’ll fix it in a moment. Since in this case I’m forwarding within the organization, I use the -ForwardingAddress parameter with another SamAccountName, however if I needed to forward this externally I could just as easily use the -ForwardingSmtpAddress parameter. Lastly, I want both users to get copies of all messages so we set -DeliverToMailboxAndForward to $true.

Easy peasy, right?

In another recent example my employer recently merged with another entity. During our integration we wanted everyone to stay in communication. What we ended up doing in our domain was:

  • We created a contact entity in Exchange, pointed to their original domain’s SMTP address.
  • We then created their new Exchange accounts, and set the ForwardingAddress to be the aforementioned contact entity.

This solution worked great, but after the merger I didn’t really feel like going through and manually undoing all of that forwarding. Here’s the code I came up with to finish the job.

$MatchingDomain="NotForYou.org"

foreach($user in $(get-mailbox|where{$_.forwardingaddress})){
  if(Get-Contact $user.ForwardingAddress.DistinguishedName -ea Ignore){
    if( $($(get-contact $user.forwardingaddress.distinguishedname).windowsemailaddress).domain -eq $MatchingDomain ){
      Set-Mailbox $user -ForwardingAddress $null
    }
  }
}

I know that all of the entities have a ForwardingAddress attribute defined, so I use my ForEach loop to narrow down my list of users. Since I also know that all of the entities I’m looking for are configured as contacts, I leverage the get-contact cmdlet in the first IF statement to narrow down the field further. In the final if statement, I wrap the results in a number of expressions $() in order to do a comparison against $MatchingDomain. Finally we use the Set-Mailbox cmdlet again to set the -ForwardingAddress parameter to $null. As I expressed it to my team, the psuedo-code looks something like this:

If mailbox has ForwardingAddress -> If ForwardingAddress is a contact -> if domain of ForwardingAddress matches -> set-mailbox

Hope this helps someone. I’ll have more Exchange PowerShell posts soon.

Delete Windows Protected Files

If you’ve been following along in my continuing effort to prove that I’m in the wrong field, you may have noticed that my computer/lab blew up weeks before VMworld. Literally started smoking. Just this past week while attempting to upgrade my laptop before the requisite amount of coffee had been consumed, I blew up the raid array and ensured myself a fun Friday. Needless to say, I’ve been spending time with external disk enclosures recovering partitions.

Since I’m having so much fun I figured I’d upgrade my OTHER laptop’s hard drive. I’ve learned something of a lesson though and am least backing up my data first, which brings up the point of today’s post. The SSD I’m about to slap into this laptop was formerly a system drive, so I’d like to clean up all that garbage before I copy it off to other media.

2017-10-11 13_38_51-ClipboardNow the good folks at Microsoft know that 99 times out of 100 you shouldn’t be manually messing with the majority of the system, so removing the C:\Windows directory can be a bit of a challenge, even if that drive no longer is running your OS. It makes for an interesting conundrum though if you’re just trying to clean up a hard drive for reuse. There’s probably a more elegant way to solve this issue, but here’s how I went about it.

Now you know what happens if I try to just go in and change ownership or permissions: Windows tells me what it thinks of my storage skills. My quick way around around this is to use PsExec. Hopefully everyone knows Sysinternals by now. If you don’t, stop reading, go get Sysinternals and polish up your toolkit. So PsExec, although designed to run processes remotely can help you out locally as well. In my case I wanted to run my command as the system account to get this problem out of the way asap. We start off this fun by launching PowerShell via PSExec

psexec -i -s powershell.exe

The -i switch says you want to run in interactive mode, and the -s says to run as the LocalSystem account, which we can see here:

2017-10-11 17_04_28-Administrator_ C__Windows_System32_WindowsPowerShell_v1.0_powershell.exe

You may ask, why did you use PowerShell? Couldn’t you just use a cmd prompt? Well, sure, but cmd prompt is old, and PowerShell is awesome, so there you go. Now we are sitting in a PowerShell session running as the System account. We can use takeown.exe to set the local Administrator (/A) owner of all the files recursively (/R) without having to answer any prompts (/D Y)

takeown /F F:\Windows /A /R /D Y

I had to run this a couple of times.  It appears that there may be some funkiness with the recursion switch. I would also suggest that you output the results to a text file in case you have any issues.

Anyways… At this point we’ve got ownership taken care of, but we still need to grant ourselves access to the files using icacls, before we delete them. Since these are going away we don’t need to worry about security so we /grant the everyone group full permission and use the /T switch to tell it apply the permissions to all downstream files/directories

icacls.exe F:\Windows /grant everyone:F /T /C

And since we’re already in PowerShell let’s just take care of these badboys

remove-item F:\Windows -recurse -force

Blamo. No more Windows directory.

Next time I should really think of a less painful way to come up with a blog post

Take Your Next Step With PowerShell

Note: to hear me talk about this post, I’d like to invite you to visit the VMware Communities Roundtable podcast, episode #420. or here.

I just got back from Boston where I had a great time at the fall VMUG UserCon, presenting on PowerShell and PowerCLI. Regardless of what the title was, the message  behind the presentation was that everyone can become effective with PowerShell and hopefully some of these building blocks help you along the way.

One thing I hadn’t fully thought through before is that not everyone out there has development background. Talking about code with a room full of Systems Engineers finally helped me to realize this.  A couple of techniques came up in conversation that I know helped me take steps with my coding, and obviously hold interest for others who are getting started, so that’s what I’d like to talk about today. I’ve got a little bit of squirrel syndrome today and I see a shiny thing, so let’s go!

Expressions and sub-expressions

I’ve been using these for years, but I honestly had to go back and look up the name for this technique. Expressions harken back to mathematical expressions. Essentially by wrapping statements or an object in parentheses, you are telling the PowerShell engine to process the commands as a group. It’s essentially telling PowerShell about order of operations. Nothing too fancy about that.

What I find really fun with expressions is that they allow you to access properties of the group inline within your script. Take this code for example

$InputFile = "C:\Temp\linux_servers.json"

$Servers = $(Get-Content $InputFile -Raw | ConvertFrom-Json).Servers
 After reading in the contents of the json file, we directly reference the Servers array from the file, rather than individually parsing the elements of the file. Remember that PowerShell is all about objects, by wrapping Get-Content $InputFile -Raw | ConvertFrom-Json in a $( ) we can directly access the Servers attributes of the object represented within $( ).
Here’s another little example that takes it a bit farther. It’s long so I could use backticks (I’ll get to that in a few) to make it a little more readable, but I want you to see this code in all it’s glory!
 if($($agents.data[$i].hostname.tolower()).substring($agents.data[$i].hostname.tolower().indexof("\") + 1,2) -eq $serverData.Hostname.ToLower().substring(0,2))]
Super fun right? Not all that readable, so let’s break it down:
$($agents.data[$i].hostname.tolower()).substring(...)
We use the $i variable (getting updated in a loop we don’t have here) to index through an array of $agents.data[$i]. We are accessing the hostname element and use the tolower() method to cast it for comparison. Finally we use our handy dandy $() expression to take this output and then we can use that property to parse the substring. The point of this giant example is that you can take multiple different elements, do operations inline and use the output directly via expressions.
If I didn’t do this justice for you, please check out the PowerShell blog, SS64, or Mr. Jeff Hicks author of PowerShell Scripting and Toolmaking for more information.

Functions

I’ve been known to write dirty code. What I mean is that, I don’t always need the prettiest code, or the most denormalized or code that conforms to style guides. My whole reason for writing, is to make things more efficient and that sometimes results in “dirty code”. That being said, I have a growing love affair with functions. This isn’t a coding primer, so I’ll let the SS64 & Scripting guys teach you about the nuance of a function, but essentially it’s a named piece of code that you can call from some other piece of code. Why would you want to do this? It makes your code more modular. If your code is more modular, you can reuse it more places. If you can reuse it more places you can do more faster. If you can do more faster: PROFIT!

Anyways here’s an example I did for VMworld (with a touch of here-string help from my friends).  Here-strings… sounds like another post…

$cred=Get-Credential
$Guest = Get-VM -Name "Sql01"
$DiskSize = 40
$Disk = "Hard Disk 2"
$Volume = "D"

Invoke-VMScript -vm $Guest -ScriptText "Get-PSDrive -Name $volume" -ScriptType PowerShell -GuestCredential $cred

$objDisk = Get-HardDisk -VM $Guest -Name $disk
$objDisk | Set-HardDisk -CapacityGB $DiskSize -Confirm:$false
$scriptBlock = @"
echo rescan > c:\Temp\diskpart.txt
echo select vol $Volume >> c:\Temp\diskpart.txt
echo extend >> c:\Temp\diskpart.txt
diskpart.exe /s c:\Temp\diskpart.txt
"@
Invoke-VMScript-vm $Guest-ScriptText $scriptBlock-ScriptType BAT -GuestCredential $cred

Invoke-VMScript -vm $Guest -ScriptText "Get-PSDrive -Name $volume" -ScriptType PowerShell -GuestCredential $cred
This very simple code example will use invoke-vmscript to extend the drive on a windows machine. Now consider the following code:

if($Guest.Staaatus -ne "GuestToolsRunning" ){

Write-Host"Too bad so sad, you'll have to manually extend the OS partition. Maybe you should fix VMtools..."-BackgroundColor White -ForegroundColor DarkRed
}
else {
Set-OSvolume$Guest$Volume$Credential
}
}

### Function to Extend OS Volume
function Set-OSvolume{
Param(
$Guest,
[string[]]$Volume,
[System.Management.Automation.CredentialAttribute()]$Credential
)
$scriptBlock=@"
echo rescan > c:\Temp\diskpart.txt
echo select vol $Volume >> c:\Temp\diskpart.txt
echo extend >> c:\Temp\diskpart.txt
diskpart.exe /s c:\Temp\diskpart.txt
"@

Invoke-VMScript-vm $Guest-ScriptText $scriptBlock-ScriptType BAT -GuestCredential $cred>$null
$(Invoke-VMScript-vm $Guest-ScriptText "Get-PSDrive -Name $volume"-ScriptType PowerShell -GuestCredential $cred).ScriptOutput
}

Now that we’ve taken that same code snippet and turned into a function, our code got cleaner, more modular and something something profit. OHH did someone say cleaner code?

Splatting

Formatting scripts for presentation, or blogs, or community posts and actually making them readable can be a challenge especially when you start getting long one-liners. While prepping for PowerCLI 201 (cough, top 10 session, cough) Mr Luc and I had a bit of a debate around readability. I’ve historically been a fan of of the back-tick ` method to have a command extend beyond a single line.  Here’s what that looks like:

2017-10-18 12_50_39-VMworld 2017 SER2614BU - PowerCLI 201_ Getting More Out of VMware vSphere PowerC
“kyle, what was I thinking using back-ticks?”   “I dunno man, but I see Luc shaking his head over there”

I’m OK admitting when I’m wrong, and while back-ticks may be good in some situations, in others they just look like crap. So enters the splat, or as my wife likes to call it “splatting the gui”. I’m not going to try and give a deep dive on that concept, as there are already a number of great articles out there: Don Jones, author of the Month of Lunches, Rambling Cookie Monster, the googs. Just as a real quick reminder, splatting is a way in which you can specify both the parameter and it’s assigned value within a hashtable. By using this splat hashtable, you can dramatically simplify the code and it’s readability… as with all things, in the appropriate situation.

With that reminder out of the way, what I wanted to talk about is the when/where of splatting, but first I think this picture shows you what I’m talking about in terms of simplified code..

2017-10-18 13_00_27-VMworld 2017 SER2614BU - PowerCLI 201_ Getting More Out of VMware vSphere PowerC
“See how pretty this looks now Scott.”   “whoa”

Here’s what I’m finding with splats: while they make code more readable in a presentation or a blog post, you probably don’t want to try and use them while working out a problem or a new cmdlet. If you’re going to use them in your code, you’ll probably want to figure out your use case and parameters first, then back into using a splat. Likewise I would suggest knowing your audience and their experience level. While talking about this subject in Boston the other day, I got a few blank stares. Even if it’s pretty, the code still has to be usable and that means readable as well.

Hopefully these quick tips help!

It’s a bird, it’s a plane, it’s… Invoke-VMScript

I was at work today and a need came across my desk for a solution that requires SNMP. For some reason which I can’t fathom, SNMP is not installed as a service on the majority of the servers. Who do we turn to in tumultuous times like these? PowerShell and his mighty sidekick PowerCLI!

michael-corleone-pull-me-back-in-just-when-i-thought-i-was-out-they-pull-me-back-inFirst things first I wanted to know the scope of what I was dealing with. When I dove into this problem I had every intention of trying to broaden my horizons and move away from PowerCLI, but it’s so easy to get sucked back into what you know. Besides, I knew I was only targeting a couple of clusters, so it only made sense to go back to PowerCLI, right? Right???

If you ignore the ugly formatting, what I did below was load all of the VM’s I needed to target into an object and then iterate through each of them to make sure they were windows machines and that they were powered on. In hindsight I knew that I was probably going to use invoke-vmscript to get the job done, so I probably should have checked for vmTools status (ExtensionData.Guest.ToolsRunningStatus) while I was at it.  snmp1

So now we’ve got a nice neat little hashtable full of servers that need a little TLC. You’d think that we could immediately get rocking, but without going into details things unexpectedly got a little dodgy at this point. I mentioned earlier that I originally intended to try and break away from PowerCLI just to broaden my horizons. Unfortunately as an Infrastructure person you don’t always have the opportunity to do things the way you’d like, and you have to sacrifice elegance for just getting things done. Luckily as VMware admins when we need to get $hit done, we have a very handy and very powerful tool available to us and that is invoke-vmscript.

b436ea981cd43a8a244370d95fa3f343_super-troopers-better-fix-meow-super-troopers-meme_250-131If you’ve heard me talk, reviewed my scripts or spent any time around me you’d know that I think invoke-vmscript is the cat’s meow. It is without a doubt my favorite cmdlet as it lets you get away with some pretty awesome stuff. At it’s root, invoke-vmscript allows you to run a script via VMtools within the context of the local VM. Now this is different from PSexec or PowerShell remoting; you are actually running the a script within the local OS where VMtools and PowerCLI are just the mechanisms to enable this super hero activity.

Quick sidebar: With great power comes great responsibility. I said above that invoke-vmscript “lets you get away with some pretty awesome stuff.” Many people in this world just deploy VMtools and vCenter with default permissions and credentials. If you are a security person, you need to ensure that your roles and privileges are setup appropriately, of you could have exposure due to what you can accomplish with VMtools.

But I digress. We are here to get things done and at the center of it this whole exercise boils down to a one liner:

<strong>Invoke-VMScript -VM $client.Name -ScriptText "DISM /online /enable-feature /norestart /featurename:SNMP" -ScriptType Powershell

If you refer back to the original snip, we stored all of the servers into an array, which is being iterated through. We invoke the script targeting $client.name. The parameter for ScriptText is where we pass in the script that we would like to run on the remote system. In this case we are using the Microsoft DISM tool to add the SNMP feature to our Windows installation. Lastly is the parameter for ScriptType. You have three ScriptType options available to you as of today: Bat for you old school Windows Cats, Bash for the nix kittens and PowerShell for the up and coming cubs.

When you put it all together, here’s the code to get it done:

$serverset=$(get-cluster cluster1|Get-VM) + $(get-cluster cluster2|Get-VM)

$ArrRemediate=@()

foreach ($client in $serverset){

if($client.powerstate -eq "PoweredOn" -and $client.guestid.contains("windows")){

if(!$(get-service -ComputerName $client -Name SNMP -ea silentlycontinue)){
$ArrRemediate+=$client
Invoke-VMScript -VM $client.Name -ScriptText "DISM /online /enable-feature /norestart /featurename:SNMP" -ScriptType Powershell
get-service -ComputerName $client.Name -Name SNMP|Select-Object -Property name, status, starttype |ft

}

}

}

$ArrRemediate.Count

I hope for today you’ll excuse the formatting and less than efficient code, as the mission was to get things done. We achieved our mission and escaped certain doom due to our friendly neighborhood hero Invoke-VMScript. I hope to have a deeper expose into our masked super hero soon, but until then if you have any thoughts or would like to contribute to the conversation, please reach out.