MyRemoteManager.psm1

#
# Script module for module 'MyRemoteManager'
#

#Requires -Version 7.1
#Requires -PSEdition Core

using namespace System.Management.Automation


#region Enum
enum Scopes {
    # Scopes in which a connection can be invoked

    Undefined = 1
    Console = 2
    External = 3
}
#endregion Enums

#region Classes
class Item {
    # Name
    [ValidateLength(1, 50)]
    [ValidatePattern("^([a-zA-Z0-9_\-]+)$")]
    [string] $Name
    # Description
    [string] $Description

    [hashtable] Splat() {
        $Hashtable = [ordered] @{}
        foreach ($p in $this.PSObject.Properties) {
            $this.PSObject.Properties | ForEach-Object -Process {
                $Hashtable[$p.Name] = $p.Value
            }
        }
        return $Hashtable
    }
}
class Client : Item {
    # Executable
    [string] $Executable
    # Command template
    [string] $TokenizedArgs
    # Default port
    [UInt16] $DefaultPort
    # Default connection scope
    [Scopes] $DefaultScope
    # Does this client require a user
    [bool] $RequiresUser
    # Tokens
    static [string] $HostToken = "<host>"
    static [string] $PortToken = "<port>"
    static [string] $UserToken = "<user>"

    Client(
        [string] $Name,
        [string] $Executable,
        [string] $TokenizedArgs,
        [UInt16] $DefaultPort,
        [Scopes] $DefaultScope,
        [string] $Description
    ) {
        $this.Name = $Name
        $this.Executable = $Executable
        [Client]::ValidateTokenizedArgs($TokenizedArgs)
        $this.TokenizedArgs = $TokenizedArgs
        $this.DefaultPort = $DefaultPort
        $this.DefaultScope = $DefaultScope
        $this.RequiresUser = [Client]::UserTokenExists($TokenizedArgs)
        $this.Description = $Description
    }

    hidden static [void] ValidateTokenizedArgs(
        [string] $TokenizedArgs
    ) {
        @(
            [Client]::HostToken,
            [Client]::PortToken
        ) | ForEach-Object -Process {
            if ($TokenizedArgs -notmatch $_) {
                throw "The argument line does not contain the following token: {0}." -f $_
            }
        }
    }

    hidden static [bool] UserTokenExists([string] $TokenizedArgs) {
        return $TokenizedArgs -match [Client]::UserToken
    }

    [string] GenerateArgs(
        [string] $Hostname,
        [UInt16] $Port
    ) {
        return $this.TokenizedArgs.Replace(
            [Client]::HostToken, $Hostname
        ).Replace(
            [Client]::PortToken, $Port
        )
    }

    [string] GenerateArgs(
        [string] $Hostname,
        [UInt16] $Port,
        [string] $User
    ) {
        return $this.GenerateArgs($Hostname, $Port).Replace(
            [Client]::UserToken, $User
        )
    }

    [string] ToString() {
        return "{0}, Description: `"{1}`", Scope: {2}, Command: `"{3} {4}`"" -f (
            $this.Name,
            $this.Description,
            $this.DefaultScope,
            $this.Executable,
            $this.TokenizedArgs.Replace(
                "<port>", "<port:{0}>" -f $this.DefaultPort
            )
        )
    }
}
class Connection : Item {
    # Hostname
    [ValidatePattern("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$")]
    [string] $Hostname
    # Port
    [UInt16] $Port
    # Default client
    [string] $DefaultClient
    # Default user
    [string] $DefaultUser

    Connection(
        [String] $Name,
        [String] $Hostname,
        [UInt16] $Port,
        [string] $DefaultClient,
        [string] $DefaultUser,
        [string] $Description
    ) {
        $this.Name = $Name
        $this.Hostname = $Hostname.ToLower()
        $this.Port = $Port
        $this.DefaultClient = $DefaultClient
        $this.DefaultUser = $DefaultUser
        $this.Description = $Description
    }

    [bool] IsDefaultPort() {
        return $this.Port -eq 0
    }

    [string] ToString() {
        return "{0}, Description `"{1}`", Default client {2}, Target {3}:{4}" -f (
            $this.Name,
            $this.Description,
            $this.DefaultClient,
            $this.Hostname,
            $(
                if ($this.IsDefaultPort()) {
                    "default"
                }
                else {
                    $this.Port.ToString()
                }
            )
        )
    }
}
class Inventory {
    # Title for the inventory file
    [string] $Title = "MyRemoteManager inventory"
    # description for the inventory file
    [string] $Description = "MyRemoteManager inventory file where the connections and clients are stored"
    # Version of the inventory file
    [string] $Version = "0.1.0"
    # Path to the inventory file
    [string] $Path = [Inventory]::GetPath()
    # Collection of Clients
    [Client[]] $Clients
    # Collection of Connections
    [Connection[]] $Connections
    # Encoding for inventory file
    static [string] $Encoding = "utf-8"
    # Name of the environement variable to use a custom path to the inventory file
    static [string] $EnvVariable = "MY_RM_INVENTORY"

    static [string] GetPath() {
        foreach ($Target in @("Process", "User", "Machine")) {
            $Value = [System.Environment]::GetEnvironmentVariable(
                [Inventory]::EnvVariable,
                [System.EnvironmentVariableTarget]::"$Target"
            )
            if ($Value) { return $Value }
        }
        return Join-Path -Path $env:USERPROFILE -ChildPath "MyRemoteManager.json"
    }

    [void] ReadFile() {
        # Get content from the file
        $GetContentParams = @{
            Path        = $this.Path
            Raw         = $true
            Encoding    = [Inventory]::Encoding
            ErrorAction = "Stop"
        }
        try {
            $Items = Get-Content @GetContentParams | ConvertFrom-Json -AsHashtable
        }
        catch {
            throw "Cannot open inventory: {0}" -f $_.Exception.Message
        }

        # Check version of the inventory
        if ($Items.Version -ne $this.Version) {
            throw (
                "Version of the inventory file is not supported.",
                "Current version: `"{0}`", Expected version: `"{1}`"" -f (
                    $Items.Version, $this.Version
                )
            )
        }

        # Add every Client to inventory object
        foreach ($c in $Items.Clients) {
            $this.Clients += New-Object -TypeName Client -ArgumentList @(
                $c.Name,
                $c.Executable,
                $c.TokenizedArgs,
                $c.DefaultPort,
                $c.DefaultScope,
                $c.Description
            )
        }

        # Check if Client name duplicates exist
        if ($this.ClientNameDuplicatesExist()) {
            Write-Warning -Message ("Fix the inventory by renaming the duplicated client names in the inventory file: {0}" -f (
                    [Inventory]::GetPath()
                )
            )
        }

        # Add every Connection to inventory object
        foreach ($c in $Items.Connections) {
            $this.Connections += New-Object -TypeName Connection -ArgumentList @(
                $c.Name,
                $c.Hostname,
                $c.Port,
                $c.DefaultClient,
                $c.DefaultUser,
                $c.Description
            )
        }

        # Check if Connection name duplicates exist
        if ($this.ConnectionNameDuplicatesExist()) {
            Write-Warning -Message (
                "Fix the inventory by renaming the duplicated connection names in the inventory file: {0}" -f (
                    [Inventory]::GetPath()
                )
            )
        }
    }

    [void] SaveFile() {
        $Items = [ordered] @{
            Title       = $this.Title
            Description = $this.Description
            Version     = $this.Version
            Clients     = @()
            Connections = @()
        }

        foreach ($c in $this.Clients) {
            $Items.Clients += $c.Splat()
        }

        foreach ($c in $this.Connections) {
            $Connection = $c.Splat()
            $Items.Connections += $Connection
        }

        $Json = ConvertTo-Json -InputObject $Items -Depth 3

        $BackupPath = "{0}.backup" -f $this.Path
        if (Test-Path -Path $this.Path -PathType Leaf) {
            Copy-Item -Path $this.Path -Destination $BackupPath -Force
        }

        Set-Content -Path $this.Path -Value $Json -Encoding ([Inventory]::Encoding) -Force
    }

    hidden [bool] ClientNameDuplicatesExist() {
        $Duplicates = $this.Clients
        | Group-Object -Property Name
        | Where-Object -Property Count -GT 1

        if ($Duplicates) {
            $Duplicates | ForEach-Object -Process {
                Write-Warning -Message ("It exists more than one client named `"{0}`"." -f $_.Name)
            }
            return $true
        }
        return $false
    }

    hidden [bool] ConnectionNameDuplicatesExist() {
        $Duplicates = $this.Connections
        | Group-Object -Property Name
        | Where-Object -Property Count -GT 1

        if ($Duplicates) {
            $Duplicates | ForEach-Object -Process {
                Write-Warning -Message ("It exists more than one connection named `"{0}`"." -f $_.Name)
            }
            return $true
        }
        return $false
    }

    [Client] GetClient([string] $Name) {
        return $this.Clients | Where-Object -Property Name -EQ $Name
    }

    [Connection] GetConnection([string] $Name) {
        return $this.Connections | Where-Object -Property Name -EQ $Name
    }

    [bool] ClientExists([string] $Name) {
        return $this.GetClient($Name).Count -gt 0
    }

    [bool] ConnectionExists([string] $Name) {
        return $this.GetConnection($Name).Count -gt 0
    }

    [void] AddClient([Client] $Client) {
        if ($this.ClientExists($Client.Name)) {
            throw "Cannot add Client `"{0}`" as it already exists." -f $Client.Name
        }
        $this.Clients += $Client
    }

    [void] AddConnection([Connection] $Connection) {
        if ($this.ConnectionExists($Connection.Name)) {
            throw "Cannot add Connection `"{0}`" as it already exists." -f $Connection.Name
        }
        $this.Connections += $Connection
    }

    [void] RemoveClient([string] $Name) {
        $this.Clients = $this.Clients | Where-Object -Property Name -NE $Name
    }

    [void] RemoveConnection([string] $Name) {
        $this.Connections = $this.Connections | Where-Object -Property Name -NE $Name
    }
}
class ValidateClientName : ValidateArgumentsAttribute {
    [void] Validate(
        [System.Object] $Argument,
        [System.Management.Automation.EngineIntrinsics] $EngineIntrinsics
    ) {
        $Inventory = Import-Inventory
        if (-not $Inventory.ClientExists($Argument)) {
            Throw "Client `"{0}`" does not exist." -f $Argument
        }
    }
}
class ValidateConnectionName : ValidateArgumentsAttribute {
    [void] Validate(
        [System.Object] $Argument,
        [System.Management.Automation.EngineIntrinsics] $EngineIntrinsics
    ) {
        $Inventory = Import-Inventory
        if (-not $Inventory.ConnectionExists($Argument)) {
            Throw "Connection `"{0}`" does not exist." -f $Argument
        }
    }
}
class ValidateSetClientName : IValidateSetValuesGenerator {
    [string[]] GetValidValues() {
        try {
            $Inventory = Import-Inventory
            return $Inventory.Clients | ForEach-Object -Process { $_.Name }
        }
        catch {
            Write-Warning -Message $_.Exception.Message
        }
        return $null
    }
}
class ValidateSetConnectionName : IValidateSetValuesGenerator {
    [string[]] GetValidValues() {
        try {
            $Inventory = Import-Inventory
            return $Inventory.Connections | ForEach-Object -Process { $_.Name }
        }
        catch {
            Write-Warning -Message $_.Exception.Message
        }
        return $null
    }
}
#endregion Classes

#region Private functions
function Import-Inventory {

    <#
 
    .SYNOPSIS
    Import inventory.
 
    .DESCRIPTION
    Creates inventory object, reads inventory file and returns the object.
 
    .INPUTS
    None. You cannot pipe objects to New-DefaultClients.
 
    .OUTPUTS
    Inventory. Import-Inventory returns the inventory object.
 
    .EXAMPLE
    PS> Import-Inventory
    (Inventory)
 
    #>


    [OutputType([Inventory])]
    param ()

    process {
        $Inventory = New-Object -TypeName Inventory
        $Inventory.ReadFile()
    }

    end {
        $Inventory
    }
}
function New-DefaultClients {

    <#
 
    .SYNOPSIS
    Creates default clients.
 
    .DESCRIPTION
    Creates and returns Client objects with popular programs.
 
    .INPUTS
    None. You cannot pipe objects to New-DefaultClients.
 
    .OUTPUTS
    Client[]. New-DefaultClients returns an array of Client objects.
 
    .EXAMPLE
    PS> New-DefaultClients
    (Client[])
 
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Scope = "Function", Target = "*")]
    [OutputType([Client[]])]
    param ()

    begin {
        $DefaultClient = @()
    }

    process {
        # OpenSSH (Microsoft Windows feature)
        $DefaultClient += New-Object -TypeName Client -ArgumentList @(
            "OpenSSH",
            "C:\Windows\System32\OpenSSH\ssh.exe",
            "-l <user> -p <port> <host>",
            22,
            [Scopes]::Console,
            "OpenSSH (Microsoft Windows feature)"
        )

        # PuTTY using SSH protocol
        $DefaultClient += New-Object -TypeName Client -ArgumentList @(
            "PuTTY_SSH",
            "putty.exe",
            "-ssh -P <port> <user>@<host>",
            22,
            [Scopes]::External,
            "PuTTY using SSH protocol"
        )

        # Microsoft Remote Desktop
        $DefaultClient += New-Object -TypeName Client -ArgumentList @(
            "RD",
            "C:\Windows\System32\mstsc.exe",
            "/v:<host>:<port> /fullscreen",
            3389,
            [Scopes]::External,
            "Microsoft Remote Desktop"
        )
    }

    end {
        $DefaultClient
    }
}
#endregion Private functions

#region Public functions
function Add-MyRMClient {

    <#
 
    .SYNOPSIS
    Adds MyRemoteManager client.
 
    .DESCRIPTION
    Adds client entry to the MyRemoteManager inventory file.
 
    .PARAMETER Name
    Name of the client.
 
    .PARAMETER Executable
    Path to the executable program that the client uses.
 
    .PARAMETER Arguments
    String of Arguments to pass to the executable.
    The string should contain the required tokens.
    Please read the documentation of MyRemoteManager.
 
    .PARAMETER DefaultPort
    Network port to use if the connection has no defined port.
 
    .PARAMETER DefaultScope
    Default scope in which a connection will be invoked.
 
    .PARAMETER Description
    Short description for the client.
 
    .INPUTS
    None. You cannot pipe objects to Add-MyRMClient.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Add-MyRMClient -Name SSH -Executable "ssh.exe" -Arguments "-l <user> -p <port> <host>" -DefaultPort 22
 
    .EXAMPLE
    PS> Add-MyRMClient -Name MyCustomClient -Executable "client.exe" -Arguments "--hostname <host> --port <port>" -DefaultPort 666 -DefaultScope External -Description "My custom client"
 
    #>


    [OutputType([string])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Mandatory = $true,
            HelpMessage = "Name of the client."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [Parameter(
            Mandatory = $true,
            HelpMessage = "Path to the executable to run as client."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Executable,

        [Parameter(
            Mandatory = $true,
            HelpMessage = "Arguments as a tokenized string. Please, read the documentation to get the list of tokens."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Arguments,

        [Parameter(
            Mandatory = $true,
            HelpMessage = "Default port to connect to on the remote host."
        )]
        [ValidateNotNullOrEmpty()]
        [UInt16] $DefaultPort,

        [Parameter(
            HelpMessage = "Default scope in which a connection will be invoked."
        )]
        [ValidateNotNullOrEmpty()]
        [Scopes] $DefaultScope = [Scopes]::Console,

        [Parameter(
            HelpMessage = "Short description of the client."
        )]
        [string] $Description
    )

    begin {
        $ErrorActionPreference = "Stop"

        try {
            $Inventory = Import-Inventory
        }
        catch {
            Write-Error -Message (
                "Error inventory: {0}" -f $_.Exception.Message
            )
        }
    }

    process {
        try {
            $Client = New-Object -TypeName Client -ArgumentList @(
                $Name,
                $Executable,
                $Arguments,
                $DefaultPort,
                $DefaultScope,
                $Description
            )
        }
        catch {
            Write-Error -Message (
                "Cannot create new client: {0}" -f $_.Exception.Message
            )
        }

        if ($PSCmdlet.ShouldProcess(
                "Inventory file {0}" -f $Inventory.Path,
                "Add Client {0}" -f $Client.ToString()
            )
        ) {
            $Inventory.AddClient($Client)
            try {
                $Inventory.SaveFile()
                Write-Verbose -Message (
                    "Client `"{0}`" has been added to the inventory." -f $Name
                )
            }
            catch {
                Write-Error -Message (
                    "Cannot save inventory: {0}" -f $_.Exception.Message
                )
            }
        }
    }
}
function Add-MyRMConnection {

    <#
 
    .SYNOPSIS
    Adds MyRemoteManager connection.
 
    .DESCRIPTION
    Adds connection entry to the MyRemoteManager inventory file.
 
    .PARAMETER Name
    Name of the connection.
 
    .PARAMETER Hostname
    Name of the remote host.
 
    .PARAMETER Port
    Port to connect to on the remote host.
    If not set, it will use the default port of the client.
 
    .PARAMETER DefaultClient
    Name of the default client.
 
    .PARAMETER DefaultUser
    Default client to use to connect to the remote host.
 
    .PARAMETER Description
    Short description for the connection.
 
    .INPUTS
    None. You cannot pipe objects to Add-MyRMConnection.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Add-MyRMConnection -Name myconn -Hostname myhost -DefaultClient SSH
 
    .EXAMPLE
    PS> Add-MyRMConnection -Name myrdpconn -Hostname myhost -DefaultClient RDP -Description "My RDP connection"
 
    .EXAMPLE
    PS> Add-MyRMConnection -Name mysshconn -Hostname myhost -Port 2222 -DefaultClient SSH -DefaultUser myuser -Description "My SSH connection"
 
    #>


    [OutputType([string])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Mandatory = $true,
            HelpMessage = "Name of the connection."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [Parameter(
            Mandatory = $true,
            HelpMessage = "Name of the remote host."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Hostname,

        [Parameter(
            HelpMessage = "Port to connect to on the remote host."
        )]
        [UInt16] $Port,

        [Parameter(
            Mandatory = $true,
            HelpMessage = "Default client to use to connect to the remote host."
        )]
        [ValidateSet([ValidateSetClientName])]
        [ValidateClientName()]
        [string] $DefaultClient,

        [Parameter(
            HelpMessage = "Default user to use to connect to the remote host."
        )]
        [string] $DefaultUser,

        [Parameter(
            HelpMessage = "Short description of the connection."
        )]
        [string] $Description
    )

    begin {
        $ErrorActionPreference = "Stop"

        try {
            $Inventory = Import-Inventory
        }
        catch {
            Write-Error -Message (
                "Cannot open inventory: {0}" -f $_.Exception.Message
            )
        }
    }

    process {
        try {
            $Connection = New-Object -TypeName Connection -ArgumentList @(
                $Name,
                $Hostname,
                $Port,
                $DefaultClient,
                $DefaultUser,
                $Description
            )#
        }
        catch {
            Write-Error -Message (
                "Cannot create new connection: {0}" -f $_.Exception.Message
            )
        }

        if ($PSCmdlet.ShouldProcess(
                "Inventory file {0}" -f $Inventory.Path,
                "Add Connection {0}" -f $Connection.ToString()
            )
        ) {
            $Inventory.AddConnection($Connection)
            try {
                $Inventory.SaveFile()
                Write-Verbose -Message (
                    "Connection `"{0}`" has been added to the inventory." -f $Name
                )
            }
            catch {
                Write-Error -Message (
                    "Cannot save inventory: {0}" -f $_.Exception.Message
                )
            }
        }
    }
}
function Get-MyRMClient {

    <#
 
    .SYNOPSIS
    Gets MyRemoteManager clients.
 
    .DESCRIPTION
    Gets available clients from the MyRemoteManager inventory file.
    Clients can be filtered by their name.
 
    .PARAMETER Name
    Filters clients by name.
 
    .INPUTS
    None. You cannot pipe objects to Get-MyRMClient.
 
    .OUTPUTS
    PSCustomObject. Get-MyRMClient returns objects with details of the available clients.
 
    .EXAMPLE
    PS> Get-MyRMClient
    (objects)
 
    .EXAMPLE
    PS> Get-MyRMClient -Name "custom_*"
    (filtered objects)
 
    #>


    [OutputType([PSCustomObject[]])]
    [CmdletBinding()]
    param (
        [Parameter(
            HelpMessage = "Filter by client name."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Name = "*"
    )

    begin {
        $ErrorActionPreference = "Stop"

        try {
            $Inventory = Import-Inventory
        }
        catch {
            Write-Error -Message (
                "Error import inventory: {0}" -f $_.Exception.Message
            )
        }
    }

    process {
        $Clients = @()
        foreach ($c in $Inventory.Clients) {
            $Clients += [PSCustomObject] @{
                Name         = $c.Name
                Command      = "{0} {1}" -f $c.Executable, $c.TokenizedArgs
                DefaultPort  = $c.DefaultPort
                DefaultScope = $c.DefaultScope
                Description  = $c.Description
            }
        }
    }

    end {
        $Clients
        | Where-Object -Property Name -Like $Name
        | Sort-Object -Property Name
    }
}
function Get-MyRMConnection {

    <#
 
    .SYNOPSIS
    Gets MyRemoteManager connections.
 
    .DESCRIPTION
    Gets available connections from the MyRemoteManager inventory file.
    connections can be filtered by their name and/or client name.
 
    .PARAMETER Name
    Filters connections by name.
 
    .INPUTS
    None. You cannot pipe objects to Get-MyRMConnection.
 
    .OUTPUTS
    PSCustomObject. Get-MyRMConnection returns objects with details of the available connections.
 
    .EXAMPLE
    PS> Get-MyRMConnection
    (objects)
 
    .EXAMPLE
    PS> Get-MyRMConnection -Name "myproject_*" -Hostname "*.mydomain" -Client "*_myproject"
    (filtered objects)
 
    #>


    [OutputType([PSCustomObject[]])]
    [CmdletBinding()]
    param (
        [Parameter(
            HelpMessage = "Filter by connection name."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Name = "*",

        [Parameter(
            HelpMessage = "Filter by hostname."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Hostname = "*",

        [Parameter(
            HelpMessage = "Filter by client name."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Client = "*"
    )

    begin {
        $ErrorActionPreference = "Stop"

        try {
            $Inventory = Import-Inventory
        }
        catch {
            Write-Error -Message (
                "Error import inventory: {0}" -f $_.Exception.Message
            )
        }
    }

    process {
        $Connections = @()
        foreach ($c in $Inventory.Connections) {
            $Connections += [PSCustomObject] @{
                Name          = $c.Name
                Hostname      = $c.Hostname
                Port          = if ($c.IsDefaultPort) {
                    $Inventory.GetClient($c.DefaultClient).DefaultPort
                }
                else {
                    $c.Port
                }
                DefaultClient = $c.DefaultClient
                DefaultUser   = $c.DefaultUser
                Description   = $c.Description
            }
        }
    }

    end {
        $Connections
        | Where-Object -Property Name -Like $Name
        | Where-Object -Property Hostname -Like $Hostname
        | Where-Object -Property DefaultClient -Like $Client
        | Sort-Object -Property Name
    }
}
function Get-MyRMInventoryInfo {

    <#
 
    .SYNOPSIS
    Gets MyRemoteManager inventory information.
 
    .DESCRIPTION
    Gets detailed information about the MyRemoteManager inventory.
 
    .INPUTS
    None. You cannot pipe objects to Get-MyRMInventoryInfo.
 
    .OUTPUTS
    PSCustomObject. Get-MyRMInventoryInfo returns an object with detailed information.
 
    .EXAMPLE
    PS> Get-MyRMInventoryInfo
    (objects)
 
    #>


    [OutputType([PSCustomObject])]
    [CmdletBinding()]
    param ()

    begin {
        $Inventory = New-Object -TypeName Inventory
        $FileExists = $false
    }

    process {
        if (Test-Path -Path $Inventory.Path -PathType Leaf) {
            $Inventory.ReadFile()
            $FileExists = $true
        }

        $InventoryInfo = [PSCustomObject] @{
            Path                = $Inventory.Path
            EnvVariable         = [Inventory]::EnvVariable
            FileExists          = $FileExists
            NumberOfClients     = $Inventory.Clients.Count
            NumberOfConnections = $Inventory.Connections.Count
        }
    }

    end {
        $InventoryInfo
    }
}
function Invoke-MyRMConnection {

    <#
 
    .SYNOPSIS
    Invokes MyRemoteManager connection.
 
    .DESCRIPTION
    Invokes MyRemoteManager connection which is defined in the inventory.
 
    .PARAMETER Name
    Name of the connection.
 
    .PARAMETER Client
    Name of the client to use to initiate the connection.
 
    .PARAMETER User
    Name of the user to connect with.
 
    .PARAMETER Scope
    Scope in which the connection will be invoked.
 
    .INPUTS
    None. You cannot pipe objects to Invoke-MyRMConnection.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Invoke-MyRMConnection myconn
 
    .EXAMPLE
    PS> Invoke-MyRMConnection -Name myconn -Client SSH -User root -Scope Console
 
    #>


    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $true,
            HelpMessage = "Name of the connection."
        )]
        [ValidateSet([ValidateSetConnectionName])]
        [ValidateConnectionName()]
        [string] $Name,

        [Parameter(
            HelpMessage = "Name of the client to use to initiate the connection."
        )]
        [ValidateSet([ValidateSetClientName])]
        [ValidateClientName()]
        [Alias("c")]
        [string] $Client,

        [Parameter(
            HelpMessage = "Name of the user to connect with."
        )]
        [Alias("u")]
        [string] $User,

        [Parameter(
            HelpMessage = "Scope in which the connection will be invoked."
        )]
        [Alias("x")]
        [Scopes] $Scope
    )

    begin {
        $ErrorActionPreference = "Stop"

        try {
            $Inventory = Import-Inventory
        }
        catch {
            Write-Error -Message (
                "Error import inventory: {0}" -f $_.Exception.Message
            )
        }
    }

    process {
        $Invocation = @{}

        $Invocation.Connection = $Inventory.GetConnection($Name)
        Write-Debug -Message ("Invoke connection {0}" -f $Invocation.Connection.ToString())

        $Invocation.Client = if ($Client) {
            if (-not $Inventory.ClientExists($Client)) {
                Write-Error -Message (
                    "Cannot invoke connection with the specified client `"{0}`" because it does not exist." -f (
                        $Client
                    )
                )
            }
            $Inventory.GetClient($Client)
        }
        else {
            if (-not $Inventory.ClientExists($Invocation.Connection.DefaultClient)) {
                Write-Error -Message (
                    "Cannot invoke connection with the default client `"{0}`" because it does not exist." -f (
                        $Invocation.Connection.DefaultClient
                    )
                )
            }
            $Inventory.GetClient($Invocation.Connection.DefaultClient)
        }
        Write-Debug -Message ("Invoke connection with client {0}" -f $Invocation.Client.ToString())

        $Invocation.Port = if ($Invocation.Connection.IsDefaultPort()) {
            $Invocation.Client.DefaultPort
        }
        else {
            $Invocation.Connection.Port
        }
        Write-Debug -Message ("Invoke connection on port {0}" -f $Invocation.Port)

        $Invocation.Executable = $Invocation.Client.Executable
        $Invocation.Arguments = if ($Invocation.Client.RequiresUser) {
            if ($User) {
                $Invocation.Client.GenerateArgs(
                    $Invocation.Connection.Hostname,
                    $Invocation.Port,
                    $User
                )
            }
            elseif ($Invocation.Connection.DefaultUser) {
                $Invocation.Client.GenerateArgs(
                    $Invocation.Connection.Hostname,
                    $Invocation.Port,
                    $Invocation.Connection.DefaultUser
                )
            }
            else {
                Write-Error -Message "Cannot invoke connection: A user must be specified."
            }
        }
        else {
            $Invocation.Client.GenerateArgs(
                $Invocation.Connection.Hostname,
                $Invocation.Port
            )
        }
        $Invocation.Command = "{0} {1}" -f $Invocation.Executable, $Invocation.Arguments
        Write-Debug -Message ("Invoke connection with command `"{0}`"" -f $Invocation.Command)

        $Invocation.Scope = if ($Scope) {
            $Scope
        }
        else {
            $Invocation.Client.DefaultScope
        }
        Write-Debug -Message ("Invoke connection in scope `"{0}`"" -f $Invocation.Scope)

        if ($PSCmdlet.ShouldProcess($Invocation.Connection.ToString(), "Initiate connection")) {
            switch ($Invocation.Scope) {
                ([Scopes]::Console) {
                    Invoke-Expression -Command $Invocation.Command
                }
                ([Scopes]::External) {
                    Start-Process -FilePath $Invocation.Executable -ArgumentList $Invocation.Arguments
                }
                ([Scopes]::Undefined) {
                    Write-Error -Message "Cannot invoke connection: Scope is undefined."
                }
                default {
                    Write-Error -Message "Cannot invoke connection: Scope is unknown."
                }
            }
        }
    }
}
function New-MyRMInventory {

    <#
 
    .SYNOPSIS
    Creates MyRemoteManager inventory file.
 
    .DESCRIPTION
    Creates a new inventory file where MyRemoteManager saves items.
 
    .PARAMETER NoDefaultClients
    Does not add defaults clients to the new inventory.
 
    .PARAMETER Force
    Overwrites existing inventory file.
 
    .PARAMETER PassThru
    Indicates that the cmdlet sends items from the interactive window down the pipeline as input to other commands.
 
    .INPUTS
    None. You cannot pipe objects to New-MyRMInventory.
 
    .OUTPUTS
    System.Void. None.
        or if PassThru is set,
    System.String. New-MyRMInventory returns a string with the path to the created inventory.
 
    .EXAMPLE
    PS> New-MyRMInventory
 
    .EXAMPLE
    PS> New-MyRMInventory -NoDefaultClients
 
    .EXAMPLE
    PS> New-MyRMInventory -Force
 
    .EXAMPLE
    PS> New-MyRMInventory -PassThru
    C:\Users\MyUsername\MyRemoteManager.json
 
    .EXAMPLE
    PS> New-MyRMInventory -NoDefaultClients -Force -PassThru
    C:\Users\MyUsername\MyRemoteManager.json
 
    #>


    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            HelpMessage = "Do not add defaults clients."
        )]
        [switch] $NoDefaultClients,

        [Parameter(
            HelpMessage = "Overwrite existing inventory file."
        )]
        [switch] $Force,

        [Parameter(
            HelpMessage = "Indicates that the cmdlet sends items from the interactive window down the pipeline as input to other commands."
        )]
        [switch] $PassThru
    )

    begin {
        $ErrorActionPreference = "Stop"

        $Inventory = New-Object -TypeName Inventory
    }

    process {
        if ((Test-Path -Path $Inventory.Path -PathType Leaf) -and -not ($Force.IsPresent)) {
            Write-Error -ErrorAction Stop -Exception (
                [System.IO.IOException] "Inventory file already exists. Use `"-Force`" to overwrite it."
            )
        }

        if ($PSCmdlet.ShouldProcess($Inventory.Path, "Create inventory file")) {
            if (-not $NoDefaultClients.IsPresent) {
                New-DefaultClients | ForEach-Object -Process {
                    $Inventory.AddClient($_)
                }
            }
            try {
                $Inventory.SaveFile()
                Write-Verbose -Message (
                    "Inventory file has been created: {0}" -f $Inventory.Path
                )
            }
            catch {
                Write-Error -Message (
                    "Cannot save inventory: {0}" -f $_.Exception.Message
                )
            }
        }
    }

    end {
        if ($PassThru.IsPresent) {
            Resolve-Path $Inventory.Path | Select-Object -ExpandProperty Path
        }
    }
}
function Remove-MyRMClient {

    <#
 
    .SYNOPSIS
    Removes MyRemoteManager client.
 
    .DESCRIPTION
    Removes client entry from the MyRemoteManager inventory file.
 
    .PARAMETER Name
    Name of the client.
 
    .INPUTS
    None. You cannot pipe objects to Remove-MyRMClient.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Remove-MyRMClient SSH
 
    .EXAMPLE
    PS> Remove-MyRMClient -Name SSH
 
    #>


    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $true,
            HelpMessage = "Name of the client."
        )]
        [ValidateSet([ValidateSetClientName])]
        [ValidateClientName()]
        [string] $Name
    )

    begin {
        $ErrorActionPreference = "Stop"

        try {
            $Inventory = Import-Inventory
        }
        catch {
            Write-Error -Message (
                "Cannot open inventory: {0}" -f $_.Exception.Message
            )
        }
    }

    process {
        if ($PSCmdlet.ShouldProcess(
                "Inventory file {0}" -f $Inventory.Path,
                "Remove Client {0}" -f $Name
            )
        ) {
            $Inventory.RemoveClient($Name)

            try {
                $Inventory.SaveFile()
                Write-Verbose -Message (
                    "Client `"{0}`" has been removed from the inventory." -f $Name
                )
            }
            catch {
                Write-Error -Message (
                    "Cannot save inventory: {0}" -f $_.Exception.Message
                )
            }
        }
    }
}
function Remove-MyRMConnection {

    <#
 
    .SYNOPSIS
    Removes MyRemoteManager connection.
 
    .DESCRIPTION
    Removes connection entry from the MyRemoteManager inventory file.
 
    .PARAMETER Name
    Name of the connection.
 
    .INPUTS
    None. You cannot pipe objects to Remove-MyRMConnection.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Remove-MyRMConnection myconn
 
    .EXAMPLE
    PS> Remove-MyRMConnection -Name myconn
 
    #>


    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $true,
            HelpMessage = "Name of the connection."
        )]
        [ValidateSet([ValidateSetConnectionName])]
        [ValidateConnectionName()]
        [string] $Name
    )

    begin {
        $ErrorActionPreference = "Stop"

        try {
            $Inventory = Import-Inventory
        }
        catch {
            Write-Error -Message (
                "Cannot open inventory: {0}" -f $_.Exception.Message
            )
        }
    }

    process {
        if ($PSCmdlet.ShouldProcess(
                "Inventory file {0}" -f $Inventory.Path,
                "Remove Connection {0}" -f $Name
            )
        ) {
            $Inventory.RemoveConnection($Name)

            try {
                $Inventory.SaveFile()
                Write-Verbose -Message (
                    "Connection `"{0}`" has been removed from the inventory." -f $Name
                )
            }
            catch {
                Write-Error -Message (
                    "Cannot save inventory: {0}" -f $_.Exception.Message
                )
            }
        }
    }
}
function Set-MyRMInventoryPath {

    <#
 
    .SYNOPSIS
    Sets MyRemoteManager inventory path.
 
    .DESCRIPTION
    Sets the specific environment variable to overwrite default path to the MyRemoteManager inventory file.
 
    .PARAMETER Name
    Path to the inventory file.
    This path is set in a environment variable.
    Pass an empty string or null to reset to the default path.
 
    .PARAMETER Target
    Target scope where the environment variable will be saved.
 
    .INPUTS
    None. You cannot pipe objects to Set-MyRMInventoryPath.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Set-MyRMInventoryPath C:\MyCustomInventory.json
 
    .EXAMPLE
    PS> Set-MyRMInventoryPath -Path C:\MyCustomInventory.json
 
    #>


    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $true,
            HelpMessage = "Path to the inventory file."
        )]
        [AllowEmptyString()]
        [string] $Path,

        [Parameter(
            HelpMessage = "Target scope of the environment variable."
        )]
        [ValidateSet("Process", "User")]
        [string] $Target = "User"
    )

    begin {
        $EnvVar = [Inventory]::EnvVariable
    }

    process {
        if ($PSCmdlet.ShouldProcess(
                ("{0} environment variable {1}" -f $Target, $EnvVar),
                "Set value {0}" -f $Path
            )
        ) {
            [System.Environment]::SetEnvironmentVariable(
                $EnvVar,
                $Path,
                [System.EnvironmentVariableTarget]::"$Target"
            )
            Write-Verbose -Message ("{0} environment variable `"{1}`" has been set to `"{2}`"." -f $Target, $EnvVar, $Path)
        }
    }
}
function Test-MyRMConnection {

    <#
 
    .SYNOPSIS
    Tests MyRemoteManager connection.
 
    .DESCRIPTION
    Tests MyRemoteManager connection which is defined in the inventory.
 
    .PARAMETER Name
    Name of the connection.
 
    .INPUTS
    None. You cannot pipe objects to Test-MyRMConnection.
 
    .OUTPUTS
    System.String. Test-MyRMConnection returns a string with the status of the remote host.
 
    .EXAMPLE
    PS> Test-MyRMConnection myconn
    (status)
 
    .EXAMPLE
    PS> Test-MyRMConnection -Name myconn
    (status)
 
    #>


    [OutputType([string])]
    [CmdletBinding()]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $true,
            HelpMessage = "Name of the connection."
        )]
        [ValidateSet([ValidateSetConnectionName])]
        [ValidateConnectionName()]
        [string] $Name
    )

    begin {
        $Inventory = Import-Inventory
        $Status = "Unknown"
    }

    process {
        $Connection = $Inventory.GetConnection($Name)

        $Port = if ($Connection.IsDefaultPort()) {
            $Inventory.GetClient($Connection.DefaultClient).DefaultPort
        }
        else {
            $Connection.Port
        }

        $TestConnectionParams = @{
            TargetName     = $Connection.Hostname
            TcpPort        = $Port
            TimeoutSeconds = 3
            ErrorAction    = "Stop"
        }

        try {
            if (Test-Connection @TestConnectionParams) {
                Write-Information -MessageData (
                    "Connection {0} is up on port {1}." -f $Connection.ToString(), $Port
                ) -InformationAction Continue
                $Status = "Online"
            }
            else {
                Write-Error -Message (
                    "Connection: {0} is down on port {1}." -f $Connection.ToString(), $Port
                )
                $Status = "Offline"
            }
        }
        catch {
            Write-Error -Message $_.Exception.Message
            $Status = "CriticalFailure"
        }
    }

    end {
        $Status
    }
}
#endregion Public functions