PSOpt2.psm1

# psversion 5
$PrintServer = 'vps.opt2'
$sshKey = "$(split-path $MyInvocation.MyCommand.Source)\PSOpt2-key"
# Write-color $MyInvocation.MyCommand.Path,$MyInvocation.MyCommand.Definition,$MyInvocation.MyCommand.Source,$sshKey -foreGroundColor Red,Green

Class ProgressBarre {
    #requires -version 5
    [int]$Width = $host.ui.RawUI.WindowSize.Width-6
    [int]$CursorLeft = [Console]::CursorLeft
    [int]$CursorTop = [Console]::CursorTop
    [char]$block = 9632
    [char]$empty = 183

    [void]write([string]$Percent = '0~0~0'){
        $replace = ''
        ([float]$green, [float]$yellow, [float]$red, $none) = $Percent.split('~')
        if ($green -lt 0) {$green = 0} elseif ($green -gt 100) {$green = 100}
        if ($yellow -lt 0) {$yellow = 0} elseif ($yellow -gt 100) {$yellow = 100}
        if ($red -lt 0) {$red = 0} elseif ($red -gt 100) {$red = 100}
        $greenLenght = [int]($green / 100 * $this.Width)
        $yellowLenght = [int]($yellow / 100 * $this.Width)
        $redLenght = [int]($red / 100 * $this.Width)
        $noneLenght = $this.Width - $greenLenght - $yellowLenght - $redLenght
        if ($noneLenght -lt 0) {$noneLenght = 0} elseif ($noneLenght -gt $this.Width) {$noneLenght = $this.Width}
        $total = [math]::round($green + $yellow + $red, 1)
        $currentCursorLeft = [Console]::CursorLeft
        $currentCursorTop = [Console]::CursorTop
        try{
            [Console]::SetCursorPosition($this.CursorLeft, $this.CursorTop)
        } catch {
            Write-LogStep '!!!' error
        }
        Write-Color "$replace[", `
            ''.PadLeft($greenLenght, $this.block) , `
            ''.PadLeft($yellowLenght, $this.block) , `
            ''.PadLeft($redLenght, $this.block) , `
            ''.PadLeft($noneLenght, $this.empty) , `
            '] ' `
            -ForeGroundColor White, Green, Yellow, Red, gray, White `
            -NoNewline
        $percent = "$('{0:00.0}' -f $total)"
        try{
            [Console]::SetCursorPosition($this.Width - $percent.length - 2 , [Console]::CursorTop)
        } catch {
            Write-LogStep '!!!' error
        }
        Write-Host $percent -ForegroundColor Cyan -NoNewline
        try{
            [Console]::SetCursorPosition([Console]::CursorLeft + 1, [Console]::CursorTop)
        } catch {
            Write-LogStep '!!!' error
        }
        Write-Host '%' -NoNewline
        try{
            [Console]::SetCursorPosition($this.Width + 2, [Console]::CursorTop)
        } catch {
            Write-LogStep '!!!' error
        }
        try{
            [Console]::SetCursorPosition($currentCursorLeft, $currentCursorTop)
        } catch {
            Write-LogStep '!!!' error
        }
    }
    ProgressBarre(){
        $this.write(0)
    }
}

# https://app.apiary.io/coaxisasp/editor
function script:is-Opt2 {
    <#
        .SYNOPSIS
            Test si un object et bien de type Opt2
        .DESCRIPTION
            check la presence des proprietees
        .PARAMETER Opt2s
            Objets OPT2 de type Get-Opt2
        .EXAMPLE
            test si cet objet est valide
            is-Opt2 @{test=123}
        .EXAMPLE
            test si cet objet est valide
            get-opt2 | is-Opt2 @{test=123}
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>


    param (
        [Parameter(ValueFromPipeline = $true)]$Opt2s
    )
    process {
        foreach ($Opt2 in $Opt2s) {
            [bool]($Opt2.PSobject.Properties.name -match "id") -and `
            [bool]($Opt2.PSobject.Properties.name -match "hostname") -and `
            [bool]($Opt2.PSobject.Properties.name -match "ip") -and `
            [bool]($Opt2.PSobject.Properties.name -match "subnet") -and `
            [bool]($Opt2.PSobject.Properties.name -match "gateway") -and `
            [bool]($Opt2.PSobject.Properties.name -match "vlan") -and `
            [bool]($Opt2.PSobject.Properties.name -match "Container_id") -and `
            $Opt2.IP -match ('^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$')
        }
    }
}
function script:is-Opt2Tunnels {
    <#
        .SYNOPSIS
            Test si un object et bien de type Opt2 avec Tunnels
        .DESCRIPTION
            check la presence des proprietees
        .PARAMETER Opt2s
            Objets OPT2Tunnels de type Get-Opt2Tunnels
        .EXAMPLE
            test si cet objet est valide [NON]
            get-opt2 | is-Opt2Tunnels
        .EXAMPLE
            test si cet objet est valide [OUI]
            get-opt2tunnels | is-Opt2Tunnels
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>

    param (
        [Parameter(ValueFromPipeline = $true)]$Opt2s
    )
    process {
        foreach ($Opt2 in $Opt2s) {
            (is-Opt2 $Opt2) -and [bool]($Opt2.PSobject.Properties.name -match "Tunnels")
        }
    }
}
function script:is-Tunnels {
    <#
        .SYNOPSIS
            Test si un object et bien de type Tunnels
        .DESCRIPTION
            check la presence des proprietees
        .PARAMETER Tunnels
            Obj
        .EXAMPLE
            test si cet objet est valide [NON]
            get-opt2tunnels | is-Tunnels
        .EXAMPLE
            test si cet objet est valide [OUI]
            (get-opt2tunnels).tunnels | is-Tunnels
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>


    param (
        [Parameter(ValueFromPipeline = $true)] $Tunnels
    )
    process {
        foreach ($Tunnel in $Tunnels) {
            [bool]($Tunnel.PSobject.Properties.name -match "Name") -and `
            [bool]($Tunnel.PSobject.Properties.name -match "IP") -and `
            [bool]($Tunnel.PSobject.Properties.name -match "Channels") -and `
            [bool]($Tunnel.PSobject.Properties.name -match "Configs") -and `
            $Tunnel.IP -match ('^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$')
        }
    }
}
Function Get-Opt2 ([Parameter(ValueFromPipeline = $true)]$filter = '*', [switch]$details = $true) {
    <#
        .SYNOPSIS
            Recupere les Containers OPT2
        .DESCRIPTION
            Retourne les config des container
        .PARAMETER Filter
            Filtre de type hostname ou IP ou VLAN
        .EXAMPLE
            Tous ce qui ne sont pas a jour
            Get-Opt2 -details | ?{$_.version -ne 'v1.10.0'} | ft hostname,vlan,name,version,cont*
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>

    begin {
        $Containers = $null
        if ($details -and (Test-Path $sshKey)) {
            try {
                $SSHsession = New-SSHSession $PrintServer -Force -KeyFile $sshKey -Credential (New-Object System.Management.Automation.PSCredential("coaxis", $(new-object System.Security.SecureString))) -ea stop -wa 0 -ConnectionTimeout 1
                $cmd = "docker ps --no-trunc --format='{{.ID}}|{{.Names}}|{{.Image}}|{{.CreatedAt}}|{{.Status}}'"
                $Containers = (Invoke-SSHCommand -command $cmd -SessionId $SSHsession.SessionId -ea stop).output | %{
                    ($CONTAINER_ID,$NAMES,$IMAGE,$CREATED,$Uptime)=($_.split('|'))
                    [PSCustomObject]@{
                        $CONTAINER_ID = [PSCustomObject]@{
                            Container_id = $CONTAINER_ID
                            Name         = $NAMES
                            Image        = $IMAGE
                            Version      = $IMAGE.split(':')[-1]
                            Created      = $CREATED
                            Uptime       = $Uptime
                            # Inspect = $INSPECT
                        }
                    }
                }
                Write-LogStep "> SSH $PrintServer ", 'Listing des containers' ok
            } catch {
                Write-LogStep "> SSH $PrintServer ", $_ error
                # $SSHsession | Remove-SSHSession -ea 0 | Out-Null
            }
        }
    }
    process {
        if (Test-TcpPort $PrintServer -port 80 -ConfirmIfDown -Quick) {
            if($filter -is [string] -and $Filter -notmatch "\*" -and $filter -match '[A-Za-z]'){
                if ($filter -match '\.opt(|2)$') {
                    $filter = [System.Net.Dns]::GetHostByName("$filter").addressList.IPAddressToString
                } elseif([System.Net.Dns]::GetHostByName("$filter.opt2")) {
                    $filter = [System.Net.Dns]::GetHostByName("$filter.opt2").addressList.IPAddressToString
                } elseif([System.Net.Dns]::GetHostByName("$filter.opt")) {
                    $filter = [System.Net.Dns]::GetHostByName("$filter.opt").addressList.IPAddressToString
                }
                Write-LogStep '> Listing containers ', "Filtre = '$Filter'", 'par resolution DNS' wait
            } else {
                Write-LogStep '> Listing containers ', "Filtre = '$Filter'" wait
            }
            foreach ($Opt2 in ((Invoke-RestMethod "http://$PrintServer/api/daemons/?format=json").results  | ? {$_.ip -like $Filter -or $_.vlan -like $Filter -or $_.hostname -like $Filter})) {
                Write-LogStep "> Containers [$($Opt2.hostname)] ", "Vlan $($Opt2.vlan)", $Opt2.ip ok
                [PSCustomObject]@{
                    id           = $Opt2.id
                    hostname     = $Opt2.hostname
                    ip           = $Opt2.ip
                    subnet       = $Opt2.subnet
                    gateway      = $Opt2.gateway
                    vlan         = $Opt2.vlan
                    Container_id = $Opt2.Container_id
                    Name         = $Containers.($Opt2.Container_id).Name
                    #Image = $Containers.($Opt2.Container_id).Image
                    Version      = $Containers.($Opt2.Container_id).Version
                    Created      = $Containers.($Opt2.Container_id).Created
                    Uptime       = $Containers.($Opt2.Container_id).Uptime
                    IpPublic     = $(if($details){Invoke-Opt2Command $Opt2 -Commands "curl icanhazip.com --connect-timeout 1 --max-time 1 || curl -s http://checkip.dyndns.org --connect-timeout 1 --max-time 1 | sed -e 's/.*Current IP Address: //' -e 's/<.*$//' --connect-timeout 1 --max-time 1 || curl ipecho.net/plain --connect-timeout 1 --max-time 1 || echo 'NoWeb!'"})
                    DockerInspect = $(if($details){(Invoke-SSHCommand -SessionId $SSHsession.SessionId -command "docker inspect $($Containers.($Opt2.Container_id).Name)" -ea stop).output | ConvertFrom-Json})
                }
            }
        } else {
            Write-LogStep "> Http API $PrintServer ", 'Injoignable !' error
            throw "Impossible de joindre l'API Rest sur $PrintServer !"
        }
    }
    end {
        $SSHsession | Remove-SSHSession -ea 0 | Out-Null
    }
}
Function Get-Opt2Tunnels () {
    <#
        .SYNOPSIS
            Recupere la liste des Tunnels
        .DESCRIPTION
            Retourne la liste des Sites (Tunnels) d'un enssemble de Dockers
        .PARAMETER Opt2s
            Objets OPT2 de type Get-Opt2
            ou un filtre de type DNS, Pattern-Name, Pattern-IP ou Pattern-VLAN
        .PARAMETER Filter
            Filtre de type hostname ou IP ou Nom de site
        .EXAMPLE
            toute les conf de tous les Opt2
            Get-Opt2Tunnels
        .EXAMPLE
            toute les confs de acom
            Get-Opt2Tunnels -Opt2s acom
        .EXAMPLE
            toute les confs de acom
            Get-Opt2 -Opt2s acom | Get-Opt2Tunnels
        .EXAMPLE
            toute les confs dont le site contiend 'bdx'
            Get-Opt2Tunnels -filter bdx
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>

    param(
        [Parameter(ValueFromPipeline = $true)] $Opt2s = '*',
        $filter = '*',
        [switch]$details = $false
    )
    process {
        foreach ($Opt2 in $Opt2s) {
            if (!(is-Opt2 $Opt2)) {
                Write-LogStep "Verrification de l'oject [$Opt2]", "N'est pas un Object [Opt2] valide" warn
                return Get-Opt2 -filter $Opt2 -details:$details | Get-Opt2Tunnels -filter $filter -details:$details
            }
            if ((Test-TcpPort $Opt2.ip -port 80 -ConfirmIfDown).time) {
                try {
                    $apiSite = Invoke-RestMethod "http://$($Opt2.ip)/api/sites/" -ea stop
                    $Tunnels = $apiSite.results | Where-Object {
                        $_.id -like $Filter -or $_.hostname -like $Filter
                    } | ForEach-Object {
                        try {
                            $conf = (Invoke-RestMethod -Method Get -Uri "http://$($Opt2.ip)/api/config/$($_.id)" -ea 0)
                            $confs = [PSCustomObject]@{
                                BandwidthLimitation = [boolean] $conf.BandwidthLimitation
                                UploadLimit         = [int] $conf.UploadLimit
                                DownloadLimit       = [int] $conf.DownloadLimit
                            }
                        }
                        catch {
                            $confs = [PSCustomObject]@{
                                BandwidthLimitation = [boolean] $true
                                UploadLimit         = [int] 100
                                DownloadLimit       = [int] 10000
                            }
                        }
                        [PSCustomObject]@{
                            Name     = $_.id
                            IP       = $_.hostname
                            Channels = $_.Channels | % {
                                [PSCustomObject]@{
                                    Name  = $_.description
                                    IP    = $_.hostname
                                    Ports = $_.ports
                                }
                            }
                            Configs  = $confs
                            IpPublic = $(if($details){Invoke-OptBoxCommand -ip ($_.hostname) -Commands "curl icanhazip.com --connect-timeout 1 --max-time 1 || curl -s http://checkip.dyndns.org --connect-timeout 1 --max-time 1 | sed -e 's/.*Current IP Address: //' -e 's/<.*$//' --connect-timeout 1 --max-time 1 || curl ipecho.net/plain --connect-timeout 1 --max-time 1 || echo 'NoWeb!'"})
                        }
                    }
                    Write-LogStep '> Listing Tunnels ', $Opt2.hostname, "Filtre = '$Filter'" ok
                }
                catch {
                    Write-LogStep '> Listing Tunnels ', $Opt2.hostname, "Filtre = '$Filter'", $_ Error
                    $Tunnels = "$($Opt2.hostname)/api/sites/ ne repond pas correctement !"
                }
            }
            else {
                Write-LogStep '> Listing Tunnels ', $Opt2.hostname, "Filtre = '$Filter'" Error
                $Tunnels = "$($Opt2.hostname) ne repond pas !"
            }
            try{
                $Opt2 | Add-Member -MemberType NoteProperty -Name 'Tunnels' -Value $null -ea 0
            }catch{}
            $Opt2.tunnels = $Tunnels
            Write-LogStep "> Total filtered [$($opt2.hostname)] ", "$($opt2.tunnels.IP.count) Sites", "$($opt2.tunnels.channels.IP.count) Imprimantes" ok
            $Opt2
        }
    }
}
Function Export-Opt2Config ( [Parameter(ValueFromPipeline = $true)] $Opt2s = '*', $Path = '\\vdiv05.coaxis-asp.com\e$\Saves\OPT2.conf') {
    <#
        .SYNOPSIS
            Exporte dans un fichier les JSON tes tunnels et des imprimantes en OPT2
        .DESCRIPTION
            Retourne les chemin des fichier exporte
        .PARAMETER Opt2s
            Objets OPT2 de type Get-Opt2
            ou un filtre de type DNS, Pattern-Name, Pattern-IP ou Pattern-VLAN
        .PARAMETER Path
            Chemin de sauvegarde des JSON
        .EXAMPLE
            Exporte 1 seul site
            Get-Opt2Tunnels *shared2* -filter ed00 | Export-Opt2Config
        .EXAMPLE
            Exporte tous
            Get-Opt2Tunnels | Export-Opt2Config
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>

    process {
        foreach ($Opt2 in $Opt2s) {
            if (!(is-Opt2Tunnels $Opt2)) {
                Write-LogStep "Verrification de l'oject [$Opt2]", "N'est pas un Object [Opt2Tunnels] valide" warn
                return $Opt2 | Get-Opt2Tunnels -filter * -details | Export-Opt2Config -Path $Path
            }
            $dns = $Opt2.hostname -replace ('http:') -replace ('/')
            $outfile = "$path\$dns\SitesImps_$(get-date -Format 'yyyy-MM-dd').json"
            try {
                if (!(test-path "$path\$dns")) {
                    New-Item -Path "$path\$dns" -ItemType Directory -wa 0 -Force | out-null
                    Start-Sleep -m 150
                }
                $Opt2 | ConvertTo-Json -Depth 5 | Out-File $outfile -Encoding utf8
                $outfile
                Write-LogStep "[$($opt2.hostname)]", "$($opt2.tunnels.IP.count) Sites", "$($opt2.tunnels.channels.IP.count) Imprimantes" OK
            }
            catch {
                Write-LogStep "Recup. des Configs [$($Opt2.hostname)]", $_ error
            }
        }
    }
    end {
        Write-LogStep '','Fin des Exports !' ok
    }
}
function import-Opt2Config {
    <#
    .SYNOPSIS
        Genere les Site/Tunnels sur une interface OPT2
    .DESCRIPTION
        - creation du container s'il n'existe pas
        tente l'ajoute :
        - des tunnels
        - des imprimantes (respecet le port d'origine)
            - update les bandWith
    .PARAMETER Files
        Fichiers d'import valide ! chacun de ces fichiers sera lu pour creation avec l'API
    .EXAMPLE
        Importe une config complete
        import-Opt2Config .\SitesImps_2018-10-19.json
    .EXAMPLE
        restauration manuelle dans le fichier de conf du docker
        $toptex = Get-Content '\\vdiv05.coaxis-asp.com\e$\Saves\OPT2.conf\toptex.opt2\SitesImps_2019-05-06_toulouse.json' | ConvertFrom-Json
        $i=0
        ($toptex.Tunnels.Channels | %{
            "[$i]=`"L *:$($_.ports.listen):$($_.IP):$($_.ports.send) # $($_.Name)`""
            $i++
        }) -join(' ')
    .NOTES
        Alban LOPEZ 2018
        alban.lopez@gmail.com
        http://git/PowerTech/
    #>

    param(
        [Parameter(ValueFromPipeline = $true)]
        [ValidateScript( { $_ | Test-Path })]
        $Files = '\\vdiv05.coaxis-asp.com\e$\Saves\OPT2.conf\shared2.opt2\SitesImps_2018-10-00.json'
    )
    begin {
        $Header = @{
            'Content-Type' = 'application/json'
            'Accept'       = 'application/json'
        }
    }
    process {
        foreach ($File in $Files) {
            try {
                $Opt2 = (get-content $File | convertFrom-json)
                Write-LogStep "Configs [$($file | Split-Path -Leaf)]", $opt2.hostname, "$($opt2.tunnels.IP.count) Sites", "$($opt2.tunnels.channels.IP.count) Imprimantes" wait
                $WorkOnOpt2 = [PSCustomObject]@{
                    hostname = $Opt2.hostname
                    ip       = $Opt2.ip
                    subnet   = $Opt2.subnet
                    gateway  = $Opt2.gateway
                    vlan     = $Opt2.vlan
                    Container_id = $null
                } | New-Opt2

                $Opt2.id = $WorkOnOpt2.id
                $Opt2.Container_id = $WorkOnOpt2.Container_id

                if($wait){
                    $waitingNew = [progressBarre]::new()
                    $percent = $wait * 3
                    while ($percent -and !(Test-TcpPort $Opt2.ip -port 80 -timeout 50 -ConfirmIfDown).time) {
                        $percent--
                        Start-Sleep -Milliseconds 333
                        $waitingNew.write("~~$((($wait*3-$percent)/($wait*3))*100)")
                        #Write-ProgressBarreColor -Percents "~$((($wait*3-$percent)/($wait*3))*100)" -replaceLine
                    }
                    # $waitingNew.write("~30")
                    # $waitingNew.write("~40")
                    if ((Test-TcpPort $Opt2.ip -port 80 -timeout 50 -ConfirmIfDown).time){
                        # $waitingNew.write("~~80")
                        # $waitingNew.write("~~90")
                        $waitingNew.write("100")
                        # Write-ProgressBarreColor -Percents 100 -replaceLine
                    }else{
                        $waitingNew.write("~~100")
                        # Write-ProgressBarreColor -Percents "~~100" -replaceLine
                        throw "Impossible de joindre l'API [$($Opt2.ip):80], verifier le Routage du Vlan $($Opt2.vlan)"
                    }
                    Start-Sleep -s 1
                    Write-Host ''
                }

                $Opt2 | Add-Opt2Tunnels | Set-Opt2BandWidth | Add-Opt2Channels | out-null

                $Opt2 | Restart-Opt2 | Get-Opt2Tunnels
            }
            catch {
                Write-LogStep "BackEnd API request $($opt2.hostname)", $_ error
            }
        }
    }
}
function Add-Opt2Channels {
    <#
        .SYNOPSIS
            Ajoute un item dans un tunnel existant
        .DESCRIPTION
            parcoure tous les tunnels fournis et ajoute tous les items qui y sont specifie
        .PARAMETER Opt2s
            Objets OPT2Tunnels de type Get-Opt2Tunnels
            ou un filtre de type DNS, Pattern-Name, Pattern-IP ou Pattern-VLAN
            Par defaut Rien!
        .EXAMPLE
            recharge les imprimantes sur chaque sites de la sauvegarde
            Add-Opt2Channels -opt2s $sauvegardeOpt2
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>


    param (
        [ValidateNotNullOrEmpty()][Parameter(ValueFromPipeline = $true)] $Opt2s = $null
    )
    begin {
        $Header = @{
            'Content-Type' = 'application/json'
            'Accept'       = 'application/json'
        }
    }
    process {
        foreach ($Opt2 in $Opt2s) {
            if (!(is-Opt2Tunnels $Opt2)) {
                Write-LogStep "Verrification de l'oject [$Opt2]", "N'est pas un Object [Opt2Tunnels] valide" error
                return $null
                # return Get-Opt2 -detail:$false -filter $Opt2 | Set-Opt2BandWidth
            }
            foreach ($site in $Opt2.Tunnels) {
                if (!(is-Tunnels $site)) {
                    Write-LogStep "Verrification de l'oject [$site]", "N'est pas un Object [Tunnel] valide" error
                }
                else {
                    Write-LogStep "| |--> identification du Port ", 'Auto / Force' ok
                    $site.channels | ? {$_.name -and $_.ip} | ForEach-Object {

                        if ($_.ports.listen -or $_.ports.send){
                            $channel = [PSCustomObject]@{
                                site        = $site.Name
                                description = $_.name
                                hostname    = $_.ip
                                ports       = $_.ports
                            }
                        } else {
                            $channel = [PSCustomObject]@{
                                site        = $site.Name
                                description = $_.name
                                hostname    = $_.ip
                            }
                        }
                        try {
                            # ajout des imprimante en port automatique
                            $post = Invoke-RestMethod -Method Post -Uri "http://$($Opt2.ip)/api/printers/" -Headers $header -Body ($channel | convertto-json -Compress)
                            if ($post.cmd.exit_status -eq $true) {
                                Write-LogStep "| |--> Add Item [$($_.Name)] ", $_.ip, "[Tcp: $($_.ports.listen)]" ok
                            }
                            else {
                                Write-LogStep "| |--> Add Item [$($_.Name)] ", $_.ip, "[Tcp: $($_.ports.listen)]" error
                            }
                            Start-Sleep -m 250
                        }
                        catch {
                            Write-LogStep "| |--> Add Item [$($channel.description)] ", $_.Exception.Response.StatusDescription, $channel.hostname, "[Tcp: $($channel.ports.listen)]" error
                        }
                    }
                }
            }
            $Opt2
        }
    }
}
function Add-Opt2Tunnels {
    <#
        .SYNOPSIS
            Connecte un OptBox (tunnel) sur l'interface
        .DESCRIPTION
            supprime l'IP du KnownHost, ajoute un Site avec l'ensemble des configs
            Name, IP, UploadLimit, DownloadLimit...
        .PARAMETER Opt2s
            Objets OPT2Tunnels de type Get-Opt2Tunnels valide
            Par defaut Rien!
        .EXAMPLE
            Ajoute un Site a l'interface OPT2
            $opt2 | Add-Opt2Tunnels
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>

    param (
        [ValidateNotNullOrEmpty()][Parameter(ValueFromPipeline = $true)]
            $Opt2s = $null
    )
    begin {
        $Header = @{
            'Content-Type' = 'application/json'
            'Accept'       = 'application/json'
        }
    }
    process {
        foreach ($Opt2 in $Opt2s) {
            if (!(is-Opt2Tunnels $Opt2)) {
                Write-LogStep "Verrification de l'oject [$Opt2]", "N'est pas un Object [Opt2Tunnels] valide" warn
                return
            }
            foreach ($site in $Opt2.tunnels) {
                try {
                    Invoke-Opt2Command -Opt2s $Opt2 -Commands "ssh-keygen -f /home/mast/.ssh/known_hosts -R $($site.IP) && ssh-keygen -f /root/.ssh/known_hosts -R $($site.IP)" | out-null

                    $post = Invoke-RestMethod -Method Post -Uri "http://$($Opt2.ip)/api/sites/" -Headers $header -Body (
                        @{'id' = $site.name; 'hostname' = "$($site.IP)"} | convertto-json -Compress
                    )
                    if ($post.results[-1] -like '*failed*' -and $post.cmd.exit_status -eq $true ) {
                        Write-LogStep "|--> Add Tunnel [$($site.Name)]", "$($site.IP)", (($post.results | ? {$_ -match 'failed'}) -replace ('.* +\((.+ .*)\)', '$1!')) warn
                    }
                    elseif ($post.cmd.exit_status -eq $true) {
                        Write-LogStep "|--> Add Tunnel [$($site.Name)]", "$($site.IP)" ok
                    }
                    else {
                        if ((Test-TcpPort $site.ip -port 22 -ConfirmIfDown).time) {
                            Write-LogStep "|--> Add Tunnel [$($site.Name)]", "$($site.IP):22", 'Injoignable!', (($post.results | ? {$_ -match 'failed'}) -replace ('.* +\((.+ .*)\)', '$1!')) error
                        }
                        else {
                            Write-LogStep "|--> Add Tunnel [$($site.Name)]", "$($site.IP)", (($post.results | ? {$_ -match 'failed'}) -replace ('.* +\((.+ .*)\)', '$1!')) error
                        }
                    }
                    [PSCustomObject]@{
                        hostname = $Opt2.hostname
                        ip       = $Opt2.ip
                        subnet   = $Opt2.subnet
                        gateway  = $Opt2.gateway
                        vlan     = $Opt2.vlan
                        Container_id = $Opt2.Container_id
                        Tunnels = $site
                    }
                }
                catch {
                    Write-LogStep "|--> Add Tunnel $($Opt2.hostname) ", $site.Name, $site.IP, (((($_.ErrorDetails.Message | ConvertFrom-Json).results | ? {$_ -match 'failed'})[0]) -replace (' *failed *', ' [Failed!] '))  error
                }
            }
        }
    }
}
function Remove-Opt2Tunnels {
    <#
        .SYNOPSIS
            Supprime de l'interface un tunnel OptBox
        .DESCRIPTION
            Arrete le tunnel et le supprime ainsi que tous les channels qui s'y trouve
        .PARAMETER Opt2s
            Objets OPT2Tunnels de type Get-Opt2Tunnels valide, chacun des tunnels sera supprime
            Par defaut Rien!
        .EXAMPLE
            Add-Opt2Tunnels $opt2 -filter 'paris'
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
    #>

    param (
        [ValidateNotNullOrEmpty()][Parameter(ValueFromPipeline = $true)]
            $Opt2s = $null
    )
    begin {
        $Header = @{
            'Content-Type' = 'application/json'
            'Accept'       = 'application/json'
        }
    }
    process {
        foreach ($Opt2 in $Opt2s) {
            if (!(is-Opt2Tunnels $Opt2)) {
                Write-LogStep "Verrification de l'oject [$Opt2]", "N'est pas un Object [Opt2Tunnels] valide" warn
                return
            }
            foreach ($site in $Opt2.tunnels) {
                if (!(is-Tunnels $site)) {
                    Write-LogStep "Verrification de l'oject [$site]", "N'est pas un Object [Tunnel] valide" error
                } else {
                    try {
                        Invoke-RestMethod -Method Put -Uri "http://$($Opt2.ip)/api/sites/$($site.name)" -Headers $header -Body (
                                @{'action' = 'stop'; 'id' = $($site.name); 'hostname' = "$($site.IP)"} | convertto-json -Compress
                            ) | Out-Null
                        Start-Sleep -s 1
                        Invoke-RestMethod -Method Delete -Uri "http://$($Opt2.ip)/api/sites/$($site.name)" -Headers $header | Out-Null
                        Write-LogStep "|--> Suppression du site ","[$($site.Name)]", "$($site.IP)" ok
                    } catch {
                        Write-LogStep "|--> Suppression du site $($Opt2.hostname) ", $site.Name, $site.IP, (((($_.ErrorDetails.Message | ConvertFrom-Json).results | ? {$_ -match 'failed'})[0]) -replace (' *failed *', ' [Failed!] '))  error
                    }
                }
            }
            $Opt2 | Get-Opt2Tunnels
        }
    }
    end {
    }
}
function Restart-Opt2Tunnels {
    <#
        .SYNOPSIS
            Redemare un tunnel OptBox
        .DESCRIPTION
            Redemare le tunnel et les channels qui s'y trouve
        .PARAMETER Opt2s
            Objets OPT2Tunnels de type Get-Opt2Tunnels valide, chacun des tunnels sera supprime
            Par defaut Rien!
        .EXAMPLE
            Add-Opt2Tunnels $opt2 -filter 'paris'
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
    #>

    param (
        [ValidateNotNullOrEmpty()][Parameter(ValueFromPipeline = $true)]
            $Opt2s = $null
    )
    begin {
        $Header = @{
            'Content-Type' = 'application/json'
            'Accept'       = 'application/json'
        }
        $Tunnels = @()
    }
    process {
        foreach ($Opt2 in $Opt2s) {
            if (!(is-Opt2Tunnels $Opt2)) {
                Write-LogStep "Verrification de l'oject [$Opt2]", "N'est pas un Object [Opt2Tunnels] valide" warn
                return
            }
            foreach ($site in $Opt2.tunnels) {
                if (!(is-Tunnels $site)) {
                    Write-LogStep "Verrification de l'oject [$site]", "N'est pas un Object [Tunnel] valide" error
                } else {
                    try {
                        Invoke-RestMethod -Method Put -Uri "http://$($Opt2.ip)/api/sites/$($site.name)" -Headers $header -Body (
                                @{'action' = 'restart'; 'id' = $($site.name); 'hostname' = "$($site.IP)"} | convertto-json -Compress
                            ) | Out-Null
                        Write-LogStep "|--> Redemarage du site ","[$($site.Name)]", "$($site.IP)" ok
                        $Tunnels += ($Opt2 | Get-Opt2Tunnels -filter $site.Name).tunnels
                    } catch {
                        Write-LogStep "|--> Redemarage du site $($Opt2.hostname) ", $site.Name, $site.IP, (((($_.ErrorDetails.Message | ConvertFrom-Json).results | ? {$_ -match 'failed'})[0]) -replace (' *failed *', ' [Failed!] '))  error
                    }
                }
            }
        }
        $Opt2s.tunnels = $Tunnels
        $Opt2s
    }
    end {
    }
}
# if(($post = Invoke-RestMethod -Method Post -Uri "http://shared2.opt2/api/sites/" -Headers $header -Body (
# @{'id'= "abce231"; 'hostname'= "8.67.177.199"} | convertto-json -Compress
# ) -ea 0).exit_status -eq $true){'YES'}
function Set-Opt2BandWidth ([ValidateNotNullOrEmpty()][Parameter(ValueFromPipeline = $true)] $Opt2s = $null, [ValidateRange(10, 2500)]$Uploadlimit = $null) {
    <#
        .SYNOPSIS
            Change la vitessse d'un tunnel
        .DESCRIPTION
            Modifie la config de vitesse d'un site, puis l'applique
            retourne la config modifie
        .PARAMETER Opt2s
            Objets OPT2Tunnels de type Get-Opt2Tunnels
            ou un filtre de type DNS, Pattern-Name, Pattern-IP ou Pattern-VLAN
            Par defaut Rien!
        .PARAMETER UploadLimit
            bande passante de l'asp vers le local
            valeur valide entre : 10-2500
        .EXAMPLE
            change la bande passante du site scih bandol
            get-opt2Tunnels *scih* -filter *bandol* | Set-Opt2BandWidth 250
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>

    begin {
        $Header = @{
            'Content-Type' = 'application/json'
            'Accept'       = 'application/json'
        }
    }
    process {
        foreach ($Opt2 in $Opt2s) {
            if (!(is-Opt2Tunnels $Opt2)) {
                Write-LogStep "Verrification de l'oject [$Opt2]", "N'est pas un Object [Opt2Tunnels] valide" error
                return $null
                # return Get-Opt2 -detail:$false -filter $Opt2 | Set-Opt2BandWidth
            }
            $Opt2.Tunnels | % {
                try {
                    if (!(is-Tunnels $_)) {
                        Write-LogStep "Verrification de l'oject [$_]", "N'est pas un Object [Tunnel] valide" error
                    }
                    else {
                        if (!$Uploadlimit -and $_.Configs.UploadLimit -gt 10) {
                            $Uploadlimit = $_.Configs.UploadLimit
                        }
                        $realSpeed = Invoke-RestMethod -Method Put -Uri "http://$($Opt2.ip)/api/config/$($_.name)" -Headers $header -Body (@{
                                UploadLimit   = $Uploadlimit         # Case Sensitive
                                DownloadLimit = $Uploadlimit * 100   # Case Sensitive
                            } | convertto-json -Compress)
                        $_.Configs.UploadLimit = $realSpeed.UploadLimit
                        $_.Configs.DownloadLimit = $realSpeed.DownloadLimit
                        # il faut aussi redemarer le tunnels
                        Invoke-RestMethod -Method Put -Uri "http://$($Opt2.ip)/api/sites/$($_.name)" -Headers $header -Body (
                            @{'action' = 'restart'; 'id' = $($_.name); 'hostname' = "$($_.IP)"} | convertto-json -Compress
                        ) | Out-Null
                        Write-LogStep "|--> Change Speed ", $_.name,"$($_.Configs.UploadLimit) Ko/sec" ok
                    }
                }
                catch {
                    Write-LogStep "|--> Change Speed ", $_ Error
                }
            }
            $Opt2
        }
    }
}
function Update-Opt2 {
    <#
        .SYNOPSIS
            Mise a jour du containeur
        .DESCRIPTION
            Detecte la derniere version OPT2 presente sur l'hote et relance les OPT2 sur cette version
        .PARAMETER Opt2s
            Objets OPT2 de type Get-Opt2
            ou un filtre de type DNS, Pattern-Name, Pattern-IP ou Pattern-VLAN
        .PARAMETER Version
            Numero de version a utiliser, si il n'est pas fournis la mise a jour sera faire en derniere version
        .EXAMPLE
            mettre a jour un seul container
            Get-Opt2 *shared2* | Update-Opt2
        .EXAMPLE
            mettre a jour tous les containeurs
            Update-Opt2
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>

    param (
        [Parameter(ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        $Opt2s = $null,
        $Version = $null
    )
    begin {
        $Header = @{
            'Content-Type' = 'application/json'
            'Accept'       = 'application/json'
        }
        $Opt2Versions = (Invoke-RestMethod -Method GET -Uri "http://$PrintServer/api/daemon-versions/?format=json") | ? {
            $_ -match 'v\d\.\d+\.\d+'
        }
        if (!$Version -or $Opt2Versions -notcontains $Version) {
            # on cherche le meilleure version
            $Version = 'v' + (($Opt2Versions | % {
                        [version]($_.trimstart('v'))
                    } | Sort-Object -Descending -Unique)[0]).ToString()
            if ($Opt2Versions -notcontains $Version) {
                # si pas de version Numerique
                $Version = 'latest'
            }
        }
        Write-LogStep "Best Version [$($Version)] ", "Avalaible versions [$($Opt2Versions)]"
        # $BestOpt2Version = $Opt2Versions[0]
    }
    process {
        foreach ($Opt2 in $Opt2s) {
            if (!(is-Opt2 $Opt2)) {
                Write-LogStep "Verrification de l'oject [$Opt2]", "N'est pas un Object [Opt2] valide" warn
                return Get-Opt2 -detail:$false -filter $Opt2 | Update-Opt2
            }
            # mise a jour
            Invoke-RestMethod -Method POST -Uri "http://$PrintServer/api/containers/" -Headers $header -Body (@{
                    id      = $Opt2.id
                    action  = "upgrade"
                    version = $Version
                } | convertto-json -Compress) | Out-Null
            Start-Sleep -Seconds 1
            Write-LogStep "Update/Restart Container ", $Opt2.hostname ok
            $Opt2.vlan | Get-Opt2 -details
        }
    }
}
function New-Opt2 {
    <#
        .SYNOPSIS
            Genere un containeur
        .DESCRIPTION
            verrifi les config puis genere le container, retoune l'oject correspondant
        .PARAMETER Opt2s
            Objets OPT2 de type Get-Opt2
            Par defaut Rien!
        .EXAMPLE
            cree un nouveau containeur
            New-Opt2 -IpAddress 10.128.12.175 -maskCIDR 26 -Gateway 10.128.12.180 -Vlan 1350 -ClientName toto
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>

    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'NewDefined')]
        [ValidatePattern('^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$')]
        $IpAddress,
        [Parameter(Mandatory = $true, ParameterSetName = 'NewDefined')]
        [ValidateSet(16, 22, 23, 24, 25, 26, 27, 28)]
        [int]$maskCIDR,
        [Parameter(Mandatory = $true, ParameterSetName = 'NewDefined')]
        [ValidatePattern('^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$')]
        $Gateway,
        [Parameter(Mandatory = $true, ParameterSetName = 'NewDefined')]
        [ValidateRange(200, 4096)]
        [int]$Vlan,
        [Parameter(Mandatory = $true, ParameterSetName = 'NewDefined')]
        [string]$ClientName,

        [Parameter(ValueFromPipeline = $true, Mandatory = $true, ParameterSetName = 'AlreadyDefined')]
        [ValidateNotNullOrEmpty()]
        $Opt2s = $null
    )
    begin {
        $Header = @{
            'Content-Type' = 'application/json'
            'Accept'       = 'application/json'
        }
        $AlreadyExistOpt2s = Get-Opt2 -details:$false
        if (!$Opt2s) {
            $Opt2s = [pscustomobject][ordered]@{
                vlan     = $Vlan
                ip       = $IpAddress
                subnet   = "$IpAddress/$maskCIDR"
                gateway  = $gateway
                hostname = "http://$clientName.opt2"
                Container_id = $null
            }
        }
    }
    process {
        foreach ($Opt2 in $Opt2s) {
            try {
                if ($Opt2.ip -notmatch '^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$') {
                    Write-LogStep "IP [$($Opt2.ip)] ", 'Doit etre une IP valide' Error
                    $HasError = $Opt2
                }
                else {
                    $isUp = (ping $Opt2.ip -ports 0, 22, 80, 135, 445 -loop 3 -Intervale 250 -timeout 20).status | Sort-Object -Unique
                    if ($isUp.count -ne 1 -or $isUp -notlike '0%') {
                        Write-LogStep "Check IP [$($Opt2.ip)] ", 'Doit etre une libre !' Error
                        $HasError = $Opt2
                    }
                }
                if ($AlreadyExistOpt2s.vlan -contains $Opt2.vlan) {
                    Write-LogStep "Check Vlan [$($Opt2.vlan)] ", 'Il existe deja un Opt2 sur ce Vlan' Error
                    $HasError = $Opt2
                    Get-Opt2 $Opt2.vlan
                }
                elseif ($Opt2.vlan -gt 4096 -or $Opt2.vlan -lt 200) {
                    Write-LogStep "Check Vlan [$($Opt2.vlan)] ", 'Doit etre un numero de VLAN valide!' Error
                    $HasError = $Opt2
                }
                if ($Opt2.hostname -notlike 'http://*.opt2') {
                    Write-LogStep "Check hostname [$($Opt2.hostname)] ", 'Doit etre de la forme "http://*.opt2"' Error
                    $HasError = $Opt2
                }
                if ($Opt2.subnet -notmatch '^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/(16|22|23|24|25|26|27|28)$') {
                    Write-LogStep "Check Subnet [$($Opt2.subnet)] ", 'Doit etre de la forme "IP/CIDR"' Error
                    $HasError = $Opt2
                }
                if ($Opt2.gateway -notmatch '^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$') {
                    Write-LogStep "Check GateWay [$($Opt2.gateway)] ", 'Doit etre de la forme "IP"' Error
                    $HasError = $Opt2
                }

                if (!$HasError) {
                    $Opt2.hostname | New-Opt2DnsEntry -IpAddress $Opt2.IP
                    $Opt2 | Add-Member -MemberType NoteProperty -Name 'client_id' -Value '96da4115-9578-4272-acb9-de32283d4574'
                    $NewOpt2 = $Opt2 | ConvertTo-Json -Compress
                    if ((Invoke-WebRequest -Uri "http://$PrintServer/api/daemons/" -Method Post -Body $NewOpt2 -Headers $Header).StatusCode -eq 201) {
                        Write-LogStep "Creation [$($Opt2.hostname)] ", "Vlan $($Opt2.vlan)", "IP $($Opt2.ip)", "SubNet $($Opt2.subnet)", "Gateway $($Opt2.Gateway)" ok
                    }
                    Get-Opt2 $Opt2.vlan
                }
            }
            catch {
                Write-LogStep "BackEnd API request $($opt2.hostname)", $_ error
            }
        }
    }
}
function New-Opt2DnsEntry {
    <#
        .SYNOPSIS
            Ajoute une entree DNS dans la zone OPT2
        .DESCRIPTION
            ajoute une entré DNS pour un nouvel OPT2 et teste la resolution
        .PARAMETER Name
            Nom pour l'entre de type A
        .PARAMETER IpAddress
            Address IP pour l'entre de type A
        .PARAMETER Wait
            Temps d'attente pour validation
        .EXAMPLE
            cree une nouvelle entre dns sur le split pour duoconseils
            New-Opt2DnsEntry -Names duoconseils -IpAddress 10.128.12.175
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>

    param (
        [Parameter(ValueFromPipeline = $true,Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        $Names = $null,
        [Parameter( Mandatory = $true)]
        [ValidatePattern('^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$')]
        $IpAddress = $null,
        $wait = 10
    )
    begin{
        $SplitDns = [PSCustomObject]@{
            IP = '172.16.1.1'
            Credential = Get-CredentialByRegistry -ntAccountName 'Berlin8'
        }
    }
    process {
        foreach ($Name in $Names) {
            $Name = $name.ToLower().replace('.opt2','').replace('http://','')
            try {
                Write-LogStep "Add DNS on splitDns [$($SplitDns.IP)] ", "${name}.opt2", $IpAddress wait
                if ($wait) {
                    # $script:CursorLeft = $script:CursorTop = $null
                    $percent = $wait *3
                    $percent--
                    # Write-ProgressBarreColor -Percents "~$((($wait*3-$percent)/($wait*3))*100)" -replaceLine
                }
                start-job -ScriptBlock {
                    param($SplitDns,$name,$IpAddress)
                    Invoke-WmiMethod -ComputerName $SplitDns.IP -Credential $SplitDns.Credential -Path win32_process -Name create -ArgumentList "dnscmd /recordadd opt2 $Name A $ipaddress"
                } -arg $SplitDns,$name,$IpAddress | Out-Null
                if ($wait) {
                    $waitingDns = [progressBarre]::new()
                    while ($percent -and !(Resolve-DnsName -Name "${name}.opt2" -Server $SplitDns.ip -Type A -ea 0)) {
                        $percent--
                        Start-Sleep -Milliseconds 333
                        $waitingDns.write("~$((($wait*3-$percent)/($wait*3))*100)")
                        # Write-ProgressBarreColor -Percents "~$((($wait*3-$percent)/($wait*3))*100)" -replaceLine
                    }
                    while ($percent -and !([System.Net.Dns]::GetHostByName("${name}.opt2").addressList.IPAddressToString -contains $IpAddress)) {
                        $percent = $percent - 3
                        Start-Sleep -Milliseconds 1000
                        $waitingDns.write("~$((($wait*3-$percent)/($wait*3))*100)")
                        # Write-ProgressBarreColor -Percents "$((($wait*3-$percent)/($wait*3))*100)" -replaceLine
                        Clear-DnsClientCache
                    }
                    if ([System.Net.Dns]::GetHostByName("${name}.opt2").addressList.IPAddressToString -contains $IpAddress){
                        [PSCustomObject]@{ $IpAddress = "${name}.opt2" } 
                        $waitingDns.write("100")
                        # Write-ProgressBarreColor -Percents 100 -replaceLine
                    }else{
                        $waitingDns.write("~~100")
                        # Write-ProgressBarreColor -Percents "~~100" -replaceLine
                    }
                    Write-Host ''
                }
                Write-LogStep "Add DNS on splitDns [$($SplitDns.IP)] ", "${name}.opt2", $IpAddress, 'Type A ' ok
            }
            catch {
                Write-LogStep "Add DNS on splitDns [$($SplitDns.IP)] ", $_ error
            }
        }
    }
}

# function remove-Opt2 ($opt2s){
# <#
# .SYNOPSIS
# [Descriptif en quelques mots]
# .DESCRIPTION
# [Descriptif en quelques lignes]
    # .PARAMETER Opt2s
    # Objets OPT2 de type Get-Opt2
    # ou un filtre de type DNS, Pattern-Name, Pattern-IP ou Pattern-VLAN
    # Par defaut *
# .EXAMPLE
# remove-Opt2
# .NOTES
# Alban LOPEZ 2018
# alban.lopez@gmail.com
# http://git/PowerTech/
# #>
# begin{
# $Header = @{
# 'Content-Type' = 'application/json'
# 'Accept' = 'application/json'
# }
# }
# # # Invoke-WebRequest -Uri 'http://$PrintServer/api/networks/' -Method GET # authentification requise
# # GET http://$PrintServer/api/networks/
# # # Invoke-WebRequest -Uri 'http://$PrintServer/api/networks/a3e9466b70cd543f7783f7328da8709ff7194651774369fea5b6b62e013c53d1/' -Method DELETE -Headers $Header # authentification requise
# # DELETE http://$PrintServer/api/networks/a3e9466b70cd543f7783f7328da8709ff7194651774369fea5b6b62e013c53d1/
# }
function Restart-Opt2 {
    <#
        .SYNOPSIS
            Redemare un containeur
        .DESCRIPTION
            Retourne les propriete du containneur opt apres le reboot
        .PARAMETER Opt2s
            Objets OPT2Tunnels de type Get-Opt2Tunnels
            ou un filtre de type DNS, Pattern-Name, Pattern-IP ou Pattern-VLAN
            Par defaut Rien!
        .EXAMPLE
            Redemare le shared2
            Get-Opt2 *shared2* | Restart-Opt2
        .EXAMPLE
            Update-Opt2
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>

    param (
        [Parameter(ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        $Opt2s = $null,
        [ValidateRange(0, 60)][int]$wait = 15
    )
    begin {
        $Header = @{
            'Content-Type' = 'application/json'
            'Accept'       = 'application/json'
        }
    }
    process {
        foreach ($Opt2 in $Opt2s) {
            if (!(is-Opt2 $Opt2)) {
                Write-LogStep "Verrification de l'oject [$Opt2]", "N'est pas un Object [Opt2] valide" warn
                return Get-Opt2 -detail:$false -filter $Opt2 | restart-Opt2 -wait $wait
            }
            try {
                $Opt2Restarted = (Invoke-RestMethod -Method POST -Uri "http://$PrintServer/api/containers/" -Headers $header -Body (@{
                    id     = $Opt2.id
                    action = "restart"
                } | convertto-json -Compress))
                if($wait){
                    $waitingRestart = [progressBarre]::new()
                    #Write-LogStep 'Redemarage Docker ',$Opt2.hostname wait
                    # $script:CursorLeft = $script:CursorTop = $null
                    $percent = $wait * 3
                    while ($percent -and !(Test-TcpPort $Opt2.ip -port 80 -timeout 50 -ConfirmIfDown).time) {
                        $percent--
                        Start-Sleep -Milliseconds 333
                        $waitingRestart.write("~$((($wait*3-$percent)/($wait*3))*100)")
                        #Write-ProgressBarreColor -Percents "~$((($wait*3-$percent)/($wait*3))*100)" -replaceLine
                    }
                    if ((Test-TcpPort $Opt2.ip -port 80 -timeout 50 -ConfirmIfDown).time){
                        $waitingRestart.write("100")
                        # Write-ProgressBarreColor -Percents 100 -replaceLine
                    }else{
                        $waitingRestart.write("~~100")
                        #Write-ProgressBarreColor -Percents "~~100" -replaceLine
                    }
                    Write-Host ''
                    Start-Sleep -Seconds 1
                }
                $Opt2.id = $Opt2Restarted.ID
                $Opt2.container_id = $Opt2Restarted.container_id
                $Opt2
            }
            catch {
                Write-Object $opt2
                Write-LogStep '',$_ Error
            }
        }
    }
}
function Restart-OptBox {
    <#
        .SYNOPSIS
            Redemare un boitier distant
        .DESCRIPTION
            se connecte en ssh et demande un reboot au boitier
        .PARAMETER IpAddresses
            Addresse IP du boitier local
        .PARAMETER cred
            Credential du boitier
        .PARAMETER Wait
            Temps d'attente pour validation
        .EXAMPLE
            redemade le RPi
            '10.48.50.7', '10.48.50.7' | Restart-OptBox
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>

    param (
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
            [ValidatePattern('^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$')]
                [string] $IpAddresses,
        $cred = (Get-CredentialByRegistry 'coaxis'),
        $wait = 60
        )
    begin{
        $waitingOptBox = @()
    }
    process {
        foreach ($IpAddress in $IpAddresses) {
            try {
                $cmd = 'sudo reboot'
                $SSHsession = New-SSHSession $IpAddress -Credential $cred -Force -wa 0 -ea stop
                $stream = $SSHsession.Session.CreateShellStream("PS-SSH", 0, 0, 0, 0, 100)
                Invoke-SSHStreamExpectSecureAction -ShellStream $stream -Command $cmd -ExpectString "[sudo] password for $($cred.UserName):" -SecureAction $cred.Password | Out-Null
                Write-LogStep "OptBox SSH request [$IpAddresses] ", $cmd, 'Wait 1 min' ok
                if($wait){
                    $waitingOptBox += [PSCustomObject]@{
                        IP = $IpAddress
                        Waiting = [progressBarre]::new()
                        isUp = $false
                    }
                    Write-Host ''
                }
            } catch {
                Write-LogStep "OptBox SSH request [$IpAddresses] ", "Echec '$cmd' !",$_ error
            }
            $SSHsession | Remove-SSHSession -ea 0 | Out-Null
        }

    }
    end{
        if($wait){
            Start-Sleep -Seconds 1
            $percent = $wait
            while ($percent -and $waitingOptBox.isUp -contains $false) {
                $percent--
                $zero = Get-Date
                foreach ($box in $waitingOptBox){
                    if (!(Test-TcpPort $IpAddress -port 80 -timeout 100).time){
                        $box.waiting.write("~$((($wait-$percent)/($wait))*100)")
                    } elseif ((Test-TcpPort $IpAddress -port 80 -timeout 100 -ConfirmIfDown).time){
                        $box.waiting.write("100")
                        $box.isUp = $true
                        Invoke-RestMethod -Method Get -Uri "http://$IpAddress/libs/system.php" | %{[PSCustomObject]@{
                            HostName = $_.hostname
                            Uptime = $_.Uptime
                            IP = ((Invoke-RestMethod -Method Get -Uri "http://$IpAddress/libs/network.php") | ?{$_.ip -match $IpAddress}).ip -split('<br>')
                        }}
                    }else{
                        $box.waiting.write("~~100")
                    }
                }
                Start-Sleep -Milliseconds (1000-((get-date)-$zero).TotalMilliseconds) -ea 0
            }
            Write-Host ''
        }
    }
}
function Invoke-OptBoxCommand {
    <#
        .SYNOPSIS
            Execute une commande sur le boitier OptBox local
        .DESCRIPTION
            chacune des commandes sera executé dans l'orde
            retourne les resultats
        .PARAMETER IpAddresses
            Addresse IP du boitier local
        .PARAMETER cred
            Credential du boitier
        .PARAMETER Commands
            liste de commande a executer
        .EXAMPLE
            recherche l'IP publique du boitier OptBox
            Invoke-OptBoxCommand -ip '10.232.11.188' -commands "curl icanhazip.com --connect-timeout 1 --max-time 1"
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
            [ValidatePattern('^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$')]
                [string] $IpAddresses,
        $cred = (Get-CredentialByRegistry 'coaxis'),
        [string[]]$Commands = @(
            "curl icanhazip.com || curl -s http://checkip.dyndns.org | sed -e 's/.*Current IP Address: //' -e 's/<.*$//' || curl ipecho.net/plain"
        )
    )
    begin {
    }
    process {
        foreach ($IpAddress in $IpAddresses) {
            if((Test-TcpPort $IpAddress -port 22 -ConfirmIfDown).time){
                try {
                    $SSHsession = New-SSHSession $IpAddress -Credential $cred -Force -wa 0 -ea stop
                    $Commands | %{
                        (Invoke-SSHCommand -command $_ -SessionId $SSHsession.SessionId -ea stop).output
                        Write-LogStep "|--> SSH [$IpAddress] ", $_ ok
                    }
                }catch{
                    Write-LogStep "|--> SSH [$IpAddress] ",$_ Error
                    'Error!'
                }
                $SSHsession | Remove-SSHSession -ea 0 | Out-Null
            } else {
                'Injoignable!'
            }
        }
    }
    end {
    }
}
function Invoke-Opt2Command {
    <#
        .SYNOPSIS
            Execute une serie de commande dans les dockers
        .DESCRIPTION
            chacune des commandes sera executé dans l'orde
            retourne les resultats
        .PARAMETER Opt2s
            Objets OPT2 de type Get-Opt2
            ou un filtre de type DNS, Pattern-Name, Pattern-IP ou Pattern-VLAN
            Par defaut Rien!
        .PARAMETER Commands
            liste de commande a executer
        .EXAMPLE
            Affiche les Boitiers OPT2 Conu dasn le ssh know_host
            Invoke-Opt2Command shared2 -command 'ssh-keygen -l -f /home/mast/.ssh/known_hosts'
        .EXAMPLE
            retourne les process qui utilise un port specifique
            Invoke-Opt2Command shared2 -command 'ps -eo command | grep 9112:'
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>


    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
            [ValidateNotNullOrEmpty()]
                $Opt2s = $null,
        [string[]]$Commands = @(
            "ssh-keygen -l -f /home/mast/.ssh/known_hosts"
            # "ssh-keygen -f /home/mast/.ssh/known_hosts -R $IpAddress && ssh-keygen -f /root/.ssh/known_hosts -R $IpAddress"
        )
    )
    process {
        if ((Test-Path $sshKey)) {
            foreach ($Opt2 in $Opt2s) {
                if (!(is-Opt2 $Opt2)) {
                    Write-LogStep "Verrification de l'oject [$Opt2]", "N'est pas un Object [Opt2] valide",'Recherche...' warn
                    return Get-Opt2 -detail:$false -filter $Opt2 | Invoke-Opt2Command -Commands $Commands
                }
                try {
                    $SSHsession = New-SSHSession $PrintServer -Force -KeyFile $sshKey -Credential (New-Object System.Management.Automation.PSCredential("coaxis", $(new-object System.Security.SecureString))) -ea stop -wa 0
                    
                    $Docker_ID = (($Opt2.Container_id)[0..12]) -join ('')
                    $Commands | %{
                        (Invoke-SSHCommand -command "docker exec $Docker_ID sh -c `"$_`"" -SessionId $SSHsession.SessionId -ea stop).output # | out-null | ? {$_} | ForEach-Object {
                        Write-LogStep "|--> SSH [$($opt2.hostname)]", $_ ok

                    }
                }
                catch {
                    Write-LogStep "|--> SSH [$($opt2.hostname)] ", $_ error
                }
                $SSHsession | Remove-SSHSession -ea 0 | Out-Null
            }
        } else {
            Write-LogStep "|--> SSH ",'PrivateKey Missing' error
        }
    }
    end {
    }
}
function Repair-OptBox  {
    <#
        .SYNOPSIS
            Refait l'association SSH des tunnel
        .DESCRIPTION
            supprime l'IP du KnownHost, ajoute un faux Site et le supprime
        .PARAMETER Opt2s
            Objets OPT2 de type Get-Opt2
            ou un filtre de type DNS, Pattern-Name, Pattern-IP ou Pattern-VLAN
            Par defaut *
        .PARAMETER Filter
            Filtre IP ou Nom d'un Site/OptBox
        .EXAMPLE
            Repair-OptBox *shared2* -filter paris
        .NOTES
            Alban LOPEZ 2018
            alban.lopez@gmail.com
            http://git/PowerTech/
    #>

    [Alias('Replace-OptBox')]
    param(
        [ValidateNotNullOrEmpty()][Parameter(ValueFromPipeline = $true)] $Opt2s = '*',
        $filter = '*'
        )
    begin {
        $Header = @{
            'Content-Type' = 'application/json'
            'Accept'       = 'application/json'
        }
    }
    process {
        foreach ($Opt2 in $Opt2s) {
            if (!(is-Opt2Tunnels $Opt2)) {
                Write-LogStep "Verrification de l'oject [$Opt2]", "N'est pas un Object [Opt2Tunnels] valide" warn
                return Get-Opt2Tunnels $opt2 -filter $filter -detail:$false | Repair-OptBox
            }
            foreach ($site in $Opt2.tunnels) {
                try {
                    $tmpOpt2 = [PSCustomObject]@{ # un object valide !
                        hostname = $Opt2.hostname
                        ip       = $Opt2.ip
                        subnet   = $Opt2.subnet
                        gateway  = $Opt2.gateway
                        vlan     = $Opt2.vlan
                        Container_id = $Opt2.Container_id
                        Tunnels  = [PSCustomObject]@{
                            name     = "tmp-$((get-md5 (get-date))[0..10] -join(''))"
                            ip       = $Site.ip
                            channels = $null
                            Configs  = $null
                        }
                    } | Add-Opt2Tunnels

                    if ($tmpOpt2) {
                        $true
                        Invoke-RestMethod -Method Put -Uri "http://$($TmpOpt2.ip)/api/sites/$($TmpOpt2.Tunnels[0].name)" -Headers $header -Body (
                            @{'action' = 'stop'; 'id' = $($TmpOpt2.Tunnels[0].name); 'hostname' = "$($site.IP)"} | convertto-json -Compress
                        ) | Out-Null
                        Start-Sleep -s 1
                        Invoke-RestMethod -Method Delete -Uri "http://$($TmpOpt2.ip)/api/sites/$($TmpOpt2.Tunnels[0].name)" -Headers $header | Out-Null
                        Write-LogStep "|--> Acouplement : Opt2 <=> Box ","[$($site.Name)]", "$($site.IP)" ok
                        
                        [PSCustomObject]@{ # un object valide !
                            hostname = $Opt2.hostname
                            ip       = $Opt2.ip
                            subnet   = $Opt2.subnet
                            gateway  = $Opt2.gateway
                            vlan     = $Opt2.vlan
                            Container_id = $Opt2.Container_id
                            Tunnels  = $site
                        } | Restart-Opt2Tunnels
                    }
                }
                catch {
                    Write-LogStep "|--> Acouplement : Opt2 <=> Box ", $TmpOpt2.hostname, $site.Name, $site.IP, (((($_.ErrorDetails.Message | ConvertFrom-Json).results | ? {$_ -match 'failed'})[0]) -replace (' *failed *', ' [Failed!] '))  error
                }
            }
        }
    }
}
function Test-Opt2Channels {
    <#
        .SYNOPSIS
            [Descriptif en quelques mots]
        .DESCRIPTION
            [Descriptif en quelques lignes]
        .PARAMETER hostName
            
        .PARAMETER Port
            
        .PARAMETER Tunnels
            
        .PARAMETER Channels
            
        .EXAMPLE
            Test-Opt2Channels
        .NOTES
            Alban LOPEZ 2019
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>


    [alias('Test-PrinterPort')]
    <#
        .SYNOPSIS
            Test avance d'un port Tcp
        .DESCRIPTION
            effectu une requette JetDirect sur un port OPT2 et verifie qu'il est bien unique
        .PARAMETER IP
            HostName ou Addresse IP
        .PARAMETER Port
            Numero de Port Tcp
        .EXAMPLE
            teste tous les port du boitier OPTBox '10.208.1.251' chez toptex
            (Get-Opt2Tunnels toptex -filter 10.208.1.251) | Test-Opt2Channels | Out-GridView
        .NOTES
            Alban LOPEZ 2019
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>


    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true, ParameterSetName = 'Tcp')]
        [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true, ParameterSetName = 'Channels')]
        [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true, ParameterSetName = 'Tunnels')]
        [alias('Address','HostAddress','IP')]
            [string]$hostName,
        [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true, ParameterSetName = 'Tcp')]
        [alias('PortNumber')]
            [int]$Port,
        [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true, ParameterSetName = 'Tunnels')]
            $Tunnels,
        [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true, ParameterSetName = 'Channels')]
            $Channels
        # [string]$Query = "@PJL INFO ID`n", # n'est pas pris en charge, a cause du 'ParameterSetName'
        # [int]$timeOut = 250 # n'est pas pris en charge, a cause du 'ParameterSetName'
    )
    begin {
        [string]$Query = "@PJL INFO ID`n" # n'est pas pris en charge comme parametre, a cause du 'ParameterSetName'
        [int]$timeOut = 200               # n'est pas pris en charge comme parametre, a cause du 'ParameterSetName'
    }
    process {
        # Write-Object $PSCmdlet.ParameterSetName -backGroundColor Red
        # $PSBoundParameters | Write-Object -fore Magenta -backGroundColor Gray
        switch($PSCmdlet.ParameterSetName){
            'Tcp' {
                $status = [PSCustomObject]@{
                    Listening = $null
                    Query = [PSCustomObject]@{
                        Accepted = $Null
                        Reply = $Null
                        Error = $Null
                    }
                    Conflit = $null
                }
                Write-LogStep 'Connectivite JetDirect ',"${HostName}:${Port}" Wait
                $timeMs = (Measure-Command {
                    $tcpClient = New-Object System.Net.Sockets.TCPClient
                    $connect = $tcpClient.BeginConnect($HostName, $Port, $null, $null)
                    write-verbose "${HostName}:${Port} Connecting..."
                    $connect.AsyncWaitHandle.WaitOne($timeOut, $false) | Out-Null
                }).TotalMilliseconds
                if ($tcpClient.Connected) {
                    $status.Listening = $true
                    $timeMs += (Measure-Command {
                        $stream = $TcpClient.GetStream() # pour vider la sortie std avant de faire une demande
                        write-verbose "${HostName}:${Port} Query : $($Query.trim("`n"))"
                        $data = [System.Text.Encoding]::ASCII.GetBytes($Query)
                        $stream.Write($data, 0, $data.Length) | Out-Null
                        $status.Query.Accepted = $true
                        $step = 200
                        $i = ($timeOut * 10) / $step
                        # $waitTcp = [progressBarre]::new()
                        # $waitTcp.write("~~")
                        While ([int64]$tcpClient.Available -le 0 -and $i-- -gt 0) {
                            Start-Sleep -Milliseconds $step
                            # $waitTcp.write("~$((($timeOut * 20) - ($i * $step)) / ($timeOut * 20) * 100)")
                        }
                        # write-host
                    }).TotalMilliseconds
                    If ([int64]$tcpClient.Available -gt 0) {
                        # $waitTcp.write("100")
                        $stringBuilder = New-Object Text.StringBuilder
                        try {
                            $timeMs += (Measure-Command {
                                $stream = $TcpClient.GetStream()
                                $bindResponseBuffer = New-Object Byte[] -ArgumentList $tcpClient.Available
                                [Int]$response = $stream.Read($bindResponseBuffer, 0, $bindResponseBuffer.count)  
                                $Null = $stringBuilder.Append(($bindResponseBuffer | ForEach-Object {[char][int]$_}) -join '')
                            }).TotalMilliseconds
                            $status.Query.Reply = $stringBuilder.Tostring().split("`r`n`f")[2]
                            Write-Verbose "Read reply = $($status.Query.value)`n$($stringBuilder.Tostring())"
                        } catch {
                            $status.Query.Error = $_
                        }
                    } elseif($i -gt 0) {
                        $status.Query.Error = 'Reply is Empty!'
                        $status.Query.Reply = $true
                        # $waitTcp.write("~100~")
                    } else {
                        $status.Query.Error = 'Reply TimeOut!'
                        $status.Query.Reply = $false
                        # $waitTcp.write("~~100")
                    }
                }
                write-verbose "${HostName}:${Port} > Reply ${timeMs}ms > $status !"
                $tcpClient.Close() | Out-Null
                $tcpClient.Dispose() | Out-Null

                $command = $msg = $count = $null
                if (($Site = (Invoke-RestMethod "http://$HostName/api/sites/" -ea 0).results)) {
                    $Listening = Invoke-Opt2Command $HostName -command "ps -eo command | grep autossh | grep :${port}:" | ?{$_ -match 'coaxis@'} | ?{$_} | %{
                        Write-Verbose $_
                        [pscustomobject]@{
                            Tunnels   = ($_ -split 'coaxis@')[-1]
                            Channels  = $_ -split(' -L ') -split(' ') | ?{$_ -match "\*\:${port}\:.+\:[0-9]+"}
                            # Commands = $command
                        }
                    }
                    if($Listening.Channels.count -gt 1){
                        $status.Conflit = $true
                        Write-LogStep "Conflit Tcp Port [${hostName}:$port] ","Restart-Opt2 -opt $hostName" Error
                    } elseif(!$Listening.Channels){
                        $status.Query.Error = "Aucun Tunnel n'ecoute sur le Port:${port}"
                    }
                }
                [PSCustomObject]@{
                    IP        = $HostName
                    Port      = $Port
                    Status    = $status
                    Time      = [math]::round($timeMs,1)
                    Listening = $Listening
                }
            }
            'Tunnels' {
                $Tunnels | %{
                    Write-LogStep 'Connectivite Tunnels ',"$HostName $($Tunnels.IP)" Warn
                    [PSCustomObject]@{
                        HostName = ($hostName -split('//'))[1]
                        Channels = $_.channels
                    }
                } | Test-Opt2Channels
            }
            'Channels' {
                $Channels | %{
                    Write-LogStep 'Connectivite Channels ',"$HostName $($Channels.IP) $($Channels.Ports.listen)" Warn
                    [PSCustomObject]@{
                        HostName = $HostName
                        Port = $_.Ports.listen
                    }
                 } | Test-Opt2Channels
            }
        }
    }
    end {}
}

if (Get-Module PsWrite) {
    # Export-ModuleMember -Function Convert-RdSession, Get-RdSession
    Write-LogStep 'Chargement du module ',$PSCommandPath ok
} else {
    function Script:Write-logstep {
        param ( [string[]]$messages, $mode, $MaxWidth, $EachLength, $prefixe, $logTrace )
        Write-Verbose "$($messages -join(',')) [$mode]"
    }
}


<# Creation d'un port + Imp
    # Powershell V4+ Windows 2012R2
    # Creaton du port
    Add-PrinterPort -Name "GZ_%vps%:%port%_%imp%" -PrinterHostAddress "%vps%" -PortNumber %port%
    # Creaton du port de secour en direct
    Add-PrinterPort -Name "DIRECT_%imp%" -PrinterHostAddress "%imp%"
    # Install du driver
    if (Get-PrinterDriver -Name "MS Publisher Color Printer") {
        Write-Host "Pilote Generique deja present"
    } else {
        Write-Host "Installation du pilote generique : MS Publisher Color Printer"
        Add-PrinterDriver "MS Publisher Color Printer"
    }
    # Creation de l’objet imprimante
    Add-Printer -name "%name%" -PortName "GZ_%vps%:%port%_%imp%" -Location "%site%" -Comment "GZ par tunnel SSH (%UTC%)" -DriverName "MS Publisher Color Printer"
#>

function Rename-Opt2Item {
    <#
        .SYNOPSIS
            [Descriptif en quelques mots]
        .DESCRIPTION
            [Descriptif en quelques lignes]
        .PARAMETER Opt2s
            
        .EXAMPLE
            Rename-Opt2Item
        .NOTES
            Alban LOPEZ 2019
            alban.lopez@gmail.com
            http://git/PowerTech/
        #>


    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true)] $Opt2s = $null
    )
    begin {
        # sed -i 's/original/new/g' file.txt
    }
    process {
        foreach ($Opt2 in $Opt2s) {
            
        }
    }
    end {}
}