Friday, July 7, 2017

AWSPowerShell Module - Unable to load type from assembly

Greetings,

We are recently starting to get involved with AWS at the company where I work, so of course I started looking at the AWSPowerShell module, and following the documentation it lead me to install the module, and then run the Set-AWSCredential cmdlet to save my credentials into a profile.

When I entered the cmdlet I received the following error:

Get-AWSCredential : Could not load type Amazon.Runtime.CredentialManagement.CredentialProfile" from assembly "AWSSDK.Core, Version=3.3.0.0, Culture=neutral, PublicKeyToken=885c28607f98e604".
At line:1 char:1
+ Get-AWSCredential
+ ~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Get-AWSCredential], TypeLoadException
    + FullyQualifiedErrorId : System.TypeLoadException,Amazon.PowerShell.Common.GetCredentialCmdlet


While trying different things out I received this error on a number of other cmdlets as well. (For example, Get-AWSCredential, Get-EC2Instance, etc) It seemed that pretty much anything that required credentials would give me that error.

I started googling the error, but there was nothing, not one post that I found that might lead me to anything. That was a very scary moment, so I started troubleshooting :
  1. I redid my steps on a different machine to see if I had the same issue.
  2. I loaded up an AWS Instance and ran the cmdlet and it worked! What the hell!? 
    1. I thought maybe it is my server image? Maybe some configuration? So I took that module and loaded it on my server and that worked. What? Well I looked further into that module and found out that the AMI had a really outdated powershell module, 3.3.8.0 to be exact and the most recent update is 3.3.120.0; Now I couldn't stay with that old version! I'd be missing out on a lot of cool new stuff, so I took that one out and continued with my quest.
  3. I was running out of options so I jumped on a chat with AWS to see if they could help; and he recommended trying the last version, as the newest version had just been released today, so maybe there was a bug.
    1. I removed the SDK for .NET and all of the Windows tools and related files, then rebooted and tried installing directly from psGallery without installing the SDK. 
    2. Then again with the SDK and not the Install-Package from psGallery. exit
    3. I went back 3 versions, and nothing. 
Nothing was working. There had to be something else wrong. It couldn't be that two of my machines were messing up at the same time, right?

At this point I was getting pretty desperate, so I went onto PowerShell and loaded up the assembly and tried to load the type manually, to my surprise it also failed. I tried looking for the type, maybe there was some typo? Nothing. The Namespace Amazon.Runtime.CredentialManager could not be found. 

I asked the AWS rep about it, I was still on the chat with him, and he pointed me to the source and the namespace and the class were indeed public, so there is no reason why i shouldn't be able to see it, but they weren't there! What is going on here? A few more hours went by trying different things on different machines we ended the chat with escalating to the dev team to try to get someone with even more knowledge to see if they could help, but I was determined. I just felt like I was close to a breakthrough. 

I started looking at common development issues for when a type couldn't be loaded from a dll, and that was a bust too.

I still had my console open with the assembly loaded, below is the code I used to load the assembly, and that's when i noticed something

[System.Reflection.Assembly]::LoadFile('C:\Program Files (x86)\AWS Tools\PowerShell\AWSPowerShell\AWSSDK.Core.dll'); 

In the results for the Fullname property of the assembly, the Location didn't match the path that I was giving when loading the assembly.




Now I probably saw this results about a million times, and never thought anything of it. Then it I wondered Why? Why is the Location, not the same? So I followed the path to find a different AWSSDK.Core.dll file with an old version. 

Here are both dll files details side by side:

So now I'm sitting here wondering what the heck is this file doing there, and why is the cmdlet trying to read that file instead of the one that I'm passing it? I bust out google and i search for the path where the dll was location: C:\Windows\Microsoft.NET\assembly\GAC_MSIL

This lead me to find what the GAC was, Global Assembly Cache, and how it is .NET uses it. Well now what to do? How do I update this file? Well it turns out you can't, you can uninstall the application and reinstall it, something I had already done multiple times, or you can manually remove the files so I renamed the folder.

I then restarted PowerShell, Imported the AWSPowerShell module, tried the cmdlet and boom! Great Success.

Now the kicker is that I don't know how those old dll files were cached. We work mostly out of Terminal Servers, so i'm not the only one that uses these machines, so maybe someone installed the AWS SDK back a while ago, but as you can see the last modified date on that file is 6/6/2017, and the kicker is that I checked if the AWS SDK/Module was installed before attempting to install it and it wasn't there, so who knows exactly what happened. I'm going to drop a bug report in the source code but not very hopeful that anything will come from it.

Anyways, I wanted to share my fun adventure on my first day playing with AWS and PowerShell. Lets hope not every day is like today, or maybe that they are i don't know I had fun.

Kind regards,
Me


Update: Since I wrote this article i have found in a few occasions old assemblies on the GAC. I'm not sure what is causing this though as the assemblies that we are running with the powershell tools are up to date. This has been particularly notorious with the AWSSDK.S3.dll assembly. 

Friday, March 11, 2016

SCSM View Manager

Hello everyone,

I was recently asked to create a tool that will allow our SCSM Administrators to more quickly and efficiently manage the Views and folders on SCSM, particularly speaking Cireson Views/Folders. The root cause was that they are on the plans of restructuring the company’s structure and the effort would take a long time with the built in methods.

My first thought was to create PowerShell scripts to automate this task, but they asked me if it was possible to create a GUI application for this instead. I was a bit hesitant at first because with GUIs you are normally bound to have to baby sit the user a lot more than with a cmdlet where the user normally has to perform their own research before being able to do much. Never the less I agreed, and here are the fruits of this work.



The application works by gathering the information from the exported Management Pack XML file, and making changes to it. It is fully developed in PowerShell working with PowerShell Studio 2015 (now 2016 edition) and I’ll get the source code into GitHub later this week.

The application has different features:

Folder Features:

  • Create a new Folder: this function is currently configured to create a Cireson folder but this can be easily modified or even a new function can be added to determine if you want to make a Cireson folder or a regular folder. (It goes without saying that you do need to have the Cireson suite installed or it won’t work) 
  • Create a new folder from template: this function will prompt you for a new name, and then will search for and clone the folder named ‘TemplateFolder’ as well as all subfolders and subitems. The function will then attempt to replace “TemplateFolder” with the new provided name on all of the created items. If a ‘TemplateFolder’ is not found then the function will fail and an error will be send out. 
    • Current Template folder example: 
  • Rename Folder: This function will prompt you for a new name and rename the name of the folder in the DisplayString XML Element 
  • Delete Folder: This function will delete the currently selected folder, and all of its subfolders and views, from the XML file. 
    • Removed Elements: 
      • <Categories> 
      • <Folder> 
      • <FolderItems> 
      • <ImageReference> 
      • <DisplayString> 
  • Clone Folder: This function will clone the folder into the same parent folder along with all of its subfolders and views.

View Features:

  • Copy View: This function will prompt you for a new name and copy the view with the new name and all of its properties to the same parent location. 
  • Delete View: This function will delete the currently selected view from the XML File. 
    • Removed Elements: 
      • <Categories> 
      • <View> 
      • <FolderItem> 
      • <ImageReference> 
      • <DisplayString> 
  • Rename View: This function will prompt you for a new name, and rename the View in the DisplayString XML Element.

Other Features:

  • The program also allows you for you to be able to drag and drop folders and views to any location. 

Requirements: 

  • This programs requires that you have Windows PowerShell 4.0 or above installed.
All of the changes that are performed are automatically saved to the XML file, so make sure you make a backup before you start making changes if you are not able to quickly export it.

I hope you’ll let me know what you think, if you have any other ideas, issues or concerns.

Download path:
https://www.dropbox.com/s/zmhvikam8snjv81/SCSM%20View%20Manager.exe?dl=0

I have made a new respository for this on GitHub that you can find here:
https://github.com/Jonatan-B/SCSMViewManager

Kind regards,
Me.

Wednesday, March 9, 2016

SCOM Deleting Inactive (Grayed) servers from CSV

Hello everyone,

We have been cleaning out our SCOM environment from servers that have been decommissioned from the environment but have not yet been deleted. This turned out to be a quite a bother because you have to one by one find the inactive (grayed) server in the Managed Servers list, right click and delete.

Of course this called for some PowerShell to get the job done faster, but I couldn't find any cmdlets that would help me get this done. My google-fu took me to this article where I was able to gather the servers that were inactive.

Acquired source code:

#Import the OperationsManager module
Import-Module OperationsManager

#Define the output file
$file=”C:\Temp\GreyAgents.txt”

#Get the agent class and the each object which is in a grey state
$agent = Get-SCClass -name “Microsoft.SystemCenter.Agent"
$objects = Get-SCOMMonitoringObject -class:$agent | where {$_.IsAvailable –eq $false}

#Export each objet into a file
foreach($object in $objects){
    $object.DisplayName+ “,”+ $object.HealthState | Out-File $file -Append


* I had to fix a typo in the original code

Now that I had the objects I had to find out how to remove them, and I found my solution in this msdn article

Acquired source code:

# Summary
#     Delete an agent hosted by a non-existent computer.
# Params
#     AgentNames - An array of strings that contain the FQDN of agents to delete.
# Returns
#     None
function global:Delete-Agent([System.String[]] $agentNames)
{
    $NoAgentsErrorMsg = "`nNo agent names specified. Please specify the FQDN for each agent you want to delete.`n";


    if ($agentNames -eq $null)
    {
        Write-Host $NoAgentsErrorMsg;
        return;
    }

    $administration = (get-item .).ManagementGroup.GetAdministration();

    $agentManagedComputerType = [Microsoft.EnterpriseManagement.Administration.AgentManagedComputer];

    $genericListType = [System.Collections.Generic.List``1]
    $genericList = $genericListType.MakeGenericType($agentManagedComputerType)

    $agentList = new-object $genericList.FullName

    foreach ($agentName in $agentNames)
    {
        $agent = Get-Agent | where {$_.PrincipalName -eq $agentName}

        if ($agent -eq $null)
        {
            $msg =  "Agent '{0}' not found." -f $agentName
            Write-Host $msg;
        }
        else
        {
            $agentList.Add($agent);
        }
    }

    if ($agentList.Count -eq 0)
    {
        Write-Host $NoAgentsErrorMsg;
        return;
    }

    $genericReadOnlyCollectionType = [System.Collections.ObjectModel.ReadOnlyCollection``1]
    $genericReadOnlyCollection = $genericReadOnlyCollectionType.MakeGenericType($agentManagedComputerType)

    $agentReadOnlyCollection = new-object $genericReadOnlyCollection.FullName @(,$agentList);


    $msg = "`nDeleting {0} agents:`n" -f $agentReadOnlyCollection.Count;
    Write-Host $msg;
    foreach ($agent in $agentReadOnlyCollection)
    {
        Write-Host $agent.PrincipalName;
    }

    $administration.DeleteAgentManagedComputers($agentReadOnlyCollection);
}



However, this isn't quite what I was looking for as I wanted something to remove the servers from the CSV file that I have, and so I had to modify the script and this is the end result:

# Get the Class Object
$agentClass = Get-SCClass -Name "Microsoft.SystemCenter.Agent" -ComputerName SCOMSERVERS[blankifyourrunfromManagementServer]
# Get the MonitoringObject based on the class, and filter out only the available ones and the ones that have the HealthState not as Uninitialzied
# This will prevent that new servers will be removed.
$inactiveObjects = Get-SCOMMonitoringObject -Class $agentClass -ComputerName SCOMSERVERS[blankifyourrunfromManagementServer] | ? IsAvailable -eq $false | ? HealthState -ne "Uninitialized"

# Import your CSV File, my column name is "InactiveServers" and I added the Unique key to ensure no repeats are added which would cause an issue later.
$csv = (Import-Csv C:\temp\SCOMInactiveServers.csv | select * -Unique)

# Createa a new GenericList
$genericListType = [System.Collections.Generic.List``1]
$genericList = $genericListType.MakeGenericType([Microsoft.EnterpriseManagement.Administration.AgentManagedComputer])
$agentList = New-Object $genericList.FullName

# Get the ManagementGroup Object
$managementGroup = Get-SCOMManagementGroup -ComputerName SCOMSERVERS[blankifyourrunfromManagementServer]

# Get the Administration Property from the ManagementGroup Object
$Administration = $managementGroup.Administration

foreach($server in $csv.InactiveServers) # Loop through the servers in your CSV file
{
    # Create a new report object
    $reportObject = New-Object psobject
    # Add a member property to the object that will represent the name
    $reportObject | Add-Member -MemberType NoteProperty -Name "InactiveServers" -Value $server
    if($inactiveObjects.DisplayName -contains $server) # Check if the server is found in the inactive objects
    {
        $reportObject | Add-Member -MemberType NoteProperty -Name "Status" -Value "Inactive"
        # If the server is found in the inactive list then we do want to remove it
        # Get the SCOMAgent object and add it to the Generic List created above
        $agent = Get-SCOMAgent -DNSHostName $server -ComputerName SCOMSERVERS[blankifyourrunfromManagementServer]
        $agentList.Add($agent)
    } # endif
    else # If it is not found that means that the server is now back to being active and we don't want to remove it anymore
    {
         # Edit #1 
         $agent = Get-SCOMAgent -DNSHostName $server -ComputerName wp-scom-ms-01
        if($agent -eq $null)
        {
            $reportObject | Add-Member -MemberType NoteProperty -Name "Status" -Value "Offline"
        }
        else
        {
            $reportObject | Add-Member -MemberType NoteProperty -Name "Status" -Value "Active"

        } 
    } # endelse

    # Export the results of the query to a CSV file.
    $reportObject | Export-Csv C:\temp\SCOMInactiveServers.csv -Append
} # endloop

# Create a ReadOnly Collection
<#
    Truth be told I'm not 100% sure why this is needed, and I did tried it without it and got some errors,
    but then found out it was because of the "Unique" value on line 8 and wasn't able to test again
#>

$genericReadOnlyCollectionType = [System.Collections.ObjectModel.ReadOnlyCollection``1]
$genericReadOnlyCollection = $genericReadOnlyCollectionType.MakeGenericType([Microsoft.EnterpriseManagement.Administration.AgentManagedComputer])
$agentReadOnlyCollection = New-Object $genericReadOnlyCollection.FullName @(,$agentList)

# Print out a message with the number of computers that will be removed
$msg = "`nDeleting {0} agents:`n" -f $agentReadOnlyCollection.Count;
Write-Host $msg;

# Removes the computers from SCOM.
$Administration.DeleteAgentManagedComputers($agentReadOnlyCollection)



The comments pretty much explain the logic that I followed but in a TL;DR version: The script will gather all of the currently inactive servers and then check that the servers in your list are indeed inactive, and export a report of it, before sending them to be removed.

Do let me know if you find this helpful or if you have any feedback, questions or concerns.

Kind regards,
Me.

Edit:
I made a change for reporting purposes on the Else on line 35. This was mainly because it was showing "active" for servers that were active or that no longer exist on the SCOM Environment.

Saturday, February 6, 2016

Windows 10 Disabled services

Hello everyone!

My sister in law brought me her computer to be fixed last night as her WiFi was not working. Upon inspection I quickly noticed that her audio was also not working. I started the built in troubleshooter in hopes that Windows might be able to quickly fix it, but it returned an error stating that the Troubleshooting service was disabled ( I can't remember the exact name ) I moved to check out the EventLog to see if any error logs had been raised and I was greeted with a nice little error stating that the EventLog service was disabled. At this time I decided it was time to look at the services and find out what was going on. 

I opened the service manager and to my surprise everything, with just a very few exceptions, was disabled, not stopped or paused, but disabled. Well no wonder nothing was working right? So I asked her what changes had been made to her computer, and she told me that one of her neighbors had used it to install AVG because she told him she didn't have an Antivirus. 

I've never seen AVG, as crappy as it might have become now, do that. I went ahead and uninstalled it in hopes that this might resolve the issue. Unfortunately it did not. I looked around and I wasn't able to find a solution on this from Google, and wasn't able to find a one click solution either. 

I was able to find a list of all of the main Windows 10 services but nothing that I could use to quickly export and import, so I exported all of the services from my Windows 10 computer by using the following PowerShell command:  

   Get-Service | select Name, StartType | Export-Csv C:\temp\services.csv -nottypeinformation

Then moved the file to her computer using a thumb drive and "imported" it using:

   Import-CSV C:\users\UserProfile\Desktop\services.csv | %{Set-Service -Name $_.Name -StartupType $_.StartType } 

I rebooted her computer and now most of the services were then back working as they should have. I did have to turn on a few more manually but over all it was functional again. The only thing not working still was her WiFi, but I reinstalled her driver and that fixed that issue.

Well I hope that this might help someone having this issue out there. Also if someone has a fresh Windows 10 install and would like to provide a CSV file with their services ( My import returned quite a few errors because I had services that her computer did not have ) then I can upload it here. 

Kind regards,
Me.


Friday, January 15, 2016

SCOM Migration

Hello everyone! 

We are currently in the process of migrating our all of the servers from one domain to our 'main' domain and as part of this project we are migrating the servers from their current SCOM environment to ours. This is normally very simple to be done through the SCOM Management Console; however, I have found that about 25% of the servers that I have been migrating are failing either during the uninstall, or the installation of the new client. 

The failures are due to a number of things, because of permission issues, or ports not being opened, or just because the server doesn't feel like cooperating. Either way in order to make my work easier I created two functions to remotely install and uninstall the agent using Invoke-command and msiexec.exe and would like to share this scripts. 

<#
       .SYNOPSIS
              Install SCOM Agent in a remote computer using MSIExec

       .DESCRIPTION
              This function will Install the SCOM Agent on a remote computer using msiexec.exe. The function will require that you provide the name of the computer, and if different to modify the path where the SCOM Agent folder is located.

       .PARAMETER  computerName
              The computer where you wish to uninstall the SCOM Agent from.

       .PARAMETER  agentPath
              The script will use the MOMAgent.msi to install the SCOM Agent, therefor we'll need to copy the file over to the server.
              This is currently done by copying the folder where the file is located. The folder will be copied to the C drive under the folder
              name SCOMAgent and will clean it up after the installation is completed.

              You can hard code this parameter so that it will always use a default location under the 'Begin' section.

       .EXAMPLE
              PS C:\> Install-SCOMAgentMsiExec -ComputerName SERVERNAME

       .EXAMPLE
              PS C:\> Install-SCOMAgentMsiExec -ComputerName SERVERNAME -agentPath C:\agentPath\Folder
                     * Note: This is the folder where it is located, not the msi file itself.

       .INPUTS
              System.String, [System.String]

       .OUTPUTS
              System.String
#>
function Install-SCOMAgentMsiExec
{
       [CmdletBinding()]
       param (
              [Parameter(Position = 0, Mandatory = $true)]
              [System.String]$computerName,
              [Parameter(Position = 0, Mandatory = $false)]
              [System.String]$agentPath
       )
       begin
       {
              if ($agentPath.Length -eq 0 -or $agentPath -eq $null)
              {
                     $msiPath = # Default path here
              }
             
              $serverAgentPath = "\\$($computerName)\c$\SCOMAgent"
       } # endbegin
       process
       {
              if (-not (Test-Connection -ComputerName $computerName -Count 1 -Quiet)) # Testing if Computer is online.
              {
                     Write-Error -Message "$computerName cannot be pinged." -Category ResourceUnavailable -ErrorAction Stop
              }
              else
              {
                     Write-Verbose -Message "$computerName is accepting pings."
              } # endelse

              if (-not(Get-Service HealthService -ComputerName $computerName -ErrorAction SilentlyContinue)) # Ensure that the HealthService doesn't exists.
              {
                     # Install the Agent if the service is not found.
                     Write-Verbose -Message "The HealthService service is not installed. Installing."
                    
                     if (-not (Test-Path -Path $msiPath)) # Test to ensure the Agent Path exists.
                     {
                           Write-Error -Message "Unable to find the location $msiPath." -Category ObjectNotFound -ErrorAction Stop
                     } # endif
                    
                     try
                     {
                           if (Test-Path -Path $serverAgentPath) # Test if the destination folder exists.
                           {
                                  Write-Verbose -Message "The destination folder already exists. Ensure no files are missing."
                                  # If the Path exists, we'll compare the files to not have to re-write data.
                                  $sourceFiles = Get-ChildItem $msiPath
                                  $destinationFiles = Get-ChildItem $serverAgentPath
                                 
                                  if (-not (Compare-Object -ReferenceObject $sourceFiles -DifferenceObject $destinationFiles)) # Test if the folders are the same, and not missing files.
                                  {
                                         # If the folders do not match delete the current folder.
                                         Write-Verbose -Message "The folders do not match completely."
                                         Remove-Item $serverAgentPath -Recurse
                                         # Copy the correct Agent data into the local folder.
                                         Write-Verbose "Copying files from $msiPath to $serverAgentPath"
                                         Copy-Item $msiPath -Destination $serverAgentPath -Recurse -Force
                                  } # endif
                                  else
                                  {
                                         Write-Verbose -Message "The folders are identical. Proceeding."
                                  } # endelese
                           }
                           else
                           {
                                  # If the folder doesn't exist copy the Agent data into the local drive.
                                  Write-Verbose "Copying files from $msiPath to $serverAgentPath"
                                  Copy-Item $msiPath -Destination $serverAgentPath -Recurse -Force
                           }
                     } # endtry
                     catch
                     {
                           Write-Error -Message $Error[0].Exception -ErrorAction Stop
                     } # endcatch
                    
                     Write-Verbose -Message "Uninstalling the SCOM Agent."
                     Invoke-Command -ComputerName $computerName -ScriptBlock {
                           Start-Process "C:\Windows\System32\msiexec.exe" -ArgumentList "/i C:\SCOMAgent\MOMAgent.msi /qn USE_SETTINGS_FROM_AD=0 USE_MANUALLY_SPECIFIED_SETTINGS=1 MANAGEMENT_GROUP=[GROUPNAME] MANAGEMENT_SERVER_DNS=[SERVERNAME] SECURE_PORT=[PORT] AcceptEndUserLicenseAgreement=1" -Wait
                     }
                    
                     # We look in the computers eventlog to find the MsiExec log for the installation in the last 5 minutes.
                     Write-Verbose -Message "Confirming that the installation has been a success."
                     $eventLog = Get-EventLog -ComputerName $computerName -LogName Application -InstanceId 11707 -Newest 1 -Message "Product: Microsoft Monitoring Agent -- Installation operation completed successfully." -After $((Get-Date).AddMinutes(-5))
                    
                     if ($eventLog -ne $null) # Check if the EventLog was found.
                     {
                           Write-Verbose -Message "The HealthService service has been installed."
                     } # endif
                     else
                     {
                           Write-Error -Message "The installation failed." -Category NotInstalled -ErrorAction Stop
                     } # endcatch
              } # endif
              else
              {
                     Write-Error -Message "The HealthService is already installed." -Category InvalidOperation -RecommendedAction "Uninstall the agent before proceeding." -ErrorAction Stop
              } # endelse
       } # endprocess
       end
       {
              Write-Verbose -Message "Removing the Agent folder."
              Remove-Item $serverAgentPath -Recurse
       } # endEnd
} # endfunction



<#
       .SYNOPSIS
              Uninstall SCOM Agent in a remote computer using MSIExec

       .DESCRIPTION
              This function will Uninstall the SCOM Agent on a remote computer using msiexec.exe. The function will require that you provide the
              name of the computer, and if different to modify the path where the SCOM Agent folder is located.

       .PARAMETER  computerName
              The computer where you wish to uninstall the SCOM Agent from.

       .PARAMETER  agentPath
              The script will use the MOMAgent.msi to uninstall the SCOM Agent, therefor we'll need to copy the file over to the server.
              This is currently done by copying the folder where the file is located. The folder will be copied to the C drive under the folder
              name SCOMAgent and will clean it up after the uninstallation is completed.

              You can hard code this parameter so that it will always use a default location under the 'Begin' section.

       .EXAMPLE
              PS C:\> Uninstall-SCOMAGentMsiExec -ComputerName SERVERNAME

       .EXAMPLE
              PS C:\> Uninstall-SCOMAGentMsiExec -ComputerName SERVERNAME -agentPath C:\agentPath\Folder
                     * Note: This is the folder where it is located, not the msi file itself.

       .INPUTS
              System.String, [System.String]

       .OUTPUTS
              System.String
#>
function Uninstall-SCOMAgentMsiExec
{
       [CmdletBinding()]
       param (
              [Parameter(Position = 0, Mandatory = $true)]
              [System.String]$computerName,
              [Parameter(Position = 0, Mandatory = $false)]
              [System.String]$agentPath
       )
       begin
       {
              if ($agentPath.Length -eq 0 -or $agentPath -eq $null)
              {
                     $msiPath = # Default path here
              }
             
              $serverAgentPath = "\\$($computerName)\c$\SCOMAgent"
       } # endbegin
       process
       {
              if (-not (Test-Connection -ComputerName $computerName -Count 1 -Quiet)) # Testing if Computer is online.
              {
                     Write-Error -Message "$computerName cannot be pinged." -Category ResourceUnavailable -ErrorAction Stop
              } # endif
              else
              {
                     Write-Verbose -Message "$computerName is accepting pings."
              } # endelse

              if (Get-Service HealthService -ComputerName $computerName -ErrorAction Stop) # Check if the HealthService exists.
              {
                     Write-Verbose -Message "The HealthService service is installed. Proceeding with uninstallation."
                    
                     if (-not (Test-Path -Path $msiPath)) # Test to ensure the Agent Path exists.
                     {
                           Write-Error -Message "Unable to find the location $msiPath." -Category ObjectNotFound -ErrorAction Stop
                     } # endif
                    
                     try
                     {
                           if (Test-Path -Path $serverAgentPath) # Test if the destination folder exists.
                           {
                                  Write-Verbose -Message "The destination folder already exists. Ensure no files are missing."
                                  # If the Path exists, we'll compare the files to not have to re-write data.
                                  $sourceFiles = Get-ChildItem $msiPath
                                  $destinationFiles = Get-ChildItem $serverAgentPath
                                 
                                  if (-not (Compare-Object -ReferenceObject $sourceFiles -DifferenceObject $destinationFiles)) # Test if the folders are the same, and not missing files.
                                  {
                                         # If the folders do not match delete the current folder.
                                         Write-Verbose -Message "The folders do not match completely."
                                         Remove-Item $serverAgentPath -Recurse
                                         # Copy the correct Agent data into the local folder.
                                         Write-Verbose "Copying files from $msiPath to $serverAgentPath"
                                         Copy-Item $msiPath -Destination $serverAgentPath -Recurse -Force
                                  } # endif
                                  else
                                  {
                                         Write-Verbose -Message "The folders are identical. Proceeding."
                                  } # endelese
                           }
                           else
                           {
                                   # If the folder doesn't exist copy the Agent data into the local drive.
                                  Write-Verbose "Copying files from $msiPath to $serverAgentPath"
                                  Copy-Item $msiPath -Destination $serverAgentPath -Recurse -Force
                           }
                     } # endtry
                     catch
                     {
                           Write-Error -Message $Error[0].Exception -ErrorAction Stop
                     } # endcatch
                    
                     Write-Verbose -Message "Uninstalling the SCOM Agent."
                     Invoke-Command -ComputerName $computerName -ScriptBlock { Start-Process "C:\Windows\System32\msiexec.exe" -ArgumentList "/x C:\SCOMAgent\MOMAgent.msi /qb" -Wait }
                    
                     # We look in the computers eventlog to find the MsiExec log for the uninstallation in the last 5 minutes.
                     Write-Verbose -Message "Confirming that the uninstallation has been a success."
                     $eventLog = Get-EventLog -ComputerName $computerName -LogName Application -InstanceId 11724 -Newest 1 -Message "Product: Microsoft Monitoring Agent -- Removal completed successfully." -After $((Get-Date).AddMinutes(-5))
                    
                     if ($eventLog -ne $null) # Check if the EventLog was found.
                     {
                           Write-Verbose -Message "The HealthService service has been uninstalled."
                     } # endif
                     else
                     {
                           Write-Error -Message "The uninstallation failed." -Category NotInstalled -ErrorAction Stop
                     } # endcatch
              } # endif
              else
              {
                     Write-Error -Message "The HealthService is not installed." -Category InvalidOperation -ErrorAction Stop
              } # endelse
       } # endprocess
       end
       {
              Write-Verbose -Message "Removing the Agent folder."
              Remove-Item $serverAgentPath -Recurse
       } # endEnd
} # endfunction



The main thing to notice here is that the script will need to have access to the location where the msi SCOM Installation file is located, and local administrator rights to the server. 

The script will then copy the files to the local drive of the server and run the msi file. Once the installation runs it will check for a successful installation/uninstall eventlog and delete the data that it copied to the C drive. 

I hope that this might help someone out there, and please let me know if you have any questions, feedback or concerns. 

Kind regards,
Me.