Veeam Plugin for PoshBot chat bot

Vor kurzem bin ich auf das Chat Bot Projekt PoshBot gestoßen, dabei handelt es sich um einen in PowerShell verfassten Slack Bot. Da ich diese Art des operativen Vorgehens schon länger spannend finde, habe ich mich daran gesetzt durch mein eigenes Veeam Plugin for PoshBot die Funktionalität etwas zu erweitern. Denn nur durch relevante Befehle und Funktionen wird ein solches Tool vom ‚Spielzeug‘ auch  zu einem echten Hilfsmittel.
Das PoshBot Projekt macht es einem durch seine gute Dokumentation wirklich sehr einfach einen Einstieg zu finden. Innerhalb von weniger Stunden war ich in der Lage die ChatBot App in meinen Slack Channel einzubinden und den ersten Befehl meines eigenen Plugins aufzurufen.

Veeam Plugin for PoshBot - Get-VeeamRepositories

Mittlerweile habe ich noch zwei weitere Kommandos hinzugefügt und es folgen hoffentlich bald noch viele weitere.

Veeam Plugin for PoshBot - Get-VeeamJobSessions

Veeam Plugin for PoshBot - Get-VeeamJobs

Update 30.11.2017:

Veeam Plugin for PoshBot - Start-VeeamQuickBackup

Wie Starte ich PoshBot

Ich habe meinen Einstieg in das Thema durch den tollen Artikel von Warren F. gefunden. In diesem Artikel werden unter anderem verschiedene Wege aufgeführt wie man den PoshBot Prozess startet (ist auch in der ReadTheDocs Doku des Projekts zu finden). Ich habe mir der Einfachheit halber eine PowerShell Funktion geschrieben um den Chat Bot in meiner Konfiguration nach Bedarf zu starten. In der Produktion ist sicher die Implementation als Dienst sinnvoller.

Allgemeine Voraussetzungen:

Veeam Plugin Voraussetzungen:

  • Veeam PowerShell Snapin
  • User hat Vollzugriff im Veeam Backup & Replication Server
    • Egal ob manuell gestartet oder als Dienst ausgeführt
  • Nicht zwingend erforderlich aber ratsam wäre es, PoshBot direkt auf dem Veeam Backup & Replication Server auszuführen
    • dadurch erspart man sich bei den Kommandos den optionalen Parameter  –brserver
function Start-VeeamPoshBot ($Token) {
    Import-Module -Name PoshBot
    $botParams = @{
        Name = 'veeambot'
        BotAdmins = @('vmarkus_k')
        CommandPrefix = '!'
        LogLevel = 'Verbose'
        BackendConfiguration = @{
            Name = 'SlackBackend'
            Token = $Token
        }
        AlternateCommandPrefixes = 'bender', 'hal'
    }

    $myBotConfig = New-PoshBotConfiguration @botParams

    Start-PoshBot -Configuration $myBotConfig

}

Input Parameter für die Funktion ist dann nur noch der Slack API Token für den Bot (vorausgesetzt das PoshBot Modul ist bereits Installiert).

Start-VeeamPoshBot -Token '<Your API Token>'

Mein Veeam Plugin for PoshBot

In dem Artikel von Warren F wird auch auf die Entwicklung von Plugin für PoshBot eingegangen. Ich fand jedoch den Abschnitt Developing Plugins der Projekt Dokumentation etwas übersichtlicher.

Was mich nun wirklich begeistert hat an dem PoshBot Konzept ist, dass man seine bestehenden PowerShell Funktionen mit wenigen Anpassungen zu einem PoshBot Plugin umwandeln kann. Genau das habe ich auch mit meinem PRTG Veeam Advanced Sensor gemacht indem ich dieses Skript in zwei PowerShell Funktionen aufgeteilt habe: Get-VeeamRepositoriesGet-VeeamJobSessions und Get-VeeamJobs. Sehr schnell war ich aber mit dem Detailgrad der Ausgabe von Get-VeeamJobSessions unzufrieden und habe mit Hilfe des PowerShell-VeeamAllStats Moduls von Timothy Dewin meine Funktion erweitert.

Get-VeeamRepositories

Die Zeilen 13-16 und 94-105 sind die einzigen PoshBot spezifischen Ergänzungen die aus einer normalen PowerShell Funktion ein PoshBot Kommando mit Aliasen, Rechten und übersichtlicher Ausgabe macht

function Get-VeeamRepositories {
        <#
        .SYNOPSIS
        Get Veeam Repositories
        .EXAMPLE
        !Get-VeeamRepositories
        .EXAMPLE
        !Repos
        .EXAMPLE
        !VeeamRepositories
        #>

        [PoshBot.BotCommand(
                Aliases = ('Repos', 'VeeamRepositories'),
                Permissions = 'read'
        )]
        [cmdletbinding()]
        param(
                [Parameter(Position=0, Mandatory=$false)]
                [string] $BRHost = "localhost"
        )

        #region: Functions
        Function Get-vPCRepoInfo {
                [CmdletBinding()]
                param (
                        [Parameter(Position=0, ValueFromPipeline=$true)]
                        [PSObject[]]$Repository
                )
                Begin {
                        $outputAry = @()
                        Function New-RepoObject {param($name, $repohost, $path, $free, $total)
                        $repoObj = New-Object -TypeName PSObject -Property @{
                                        Target = $name
                                        RepoHost = $repohost
                                        Storepath = $path
                                        StorageFree = [Math]::Round([Decimal]$free/1GB,2)
                                        StorageTotal = [Math]::Round([Decimal]$total/1GB,2)
                                        FreePercentage = [Math]::Round(($free/$total)*100)
                                }
                                Return $repoObj | Select-Object Target, RepoHost, Storepath, StorageFree, StorageTotal, FreePercentage
                        }
                }
                Process {
                        Foreach ($r in $Repository) {
                                # Refresh Repository Size Info
                                try {
                                        [Veeam.Backup.Core.CBackupRepositoryEx]::SyncSpaceInfoToDb($r, $true)
                                }
                                catch {
                                        Write-Debug "SyncSpaceInfoToDb Failed"
                                }

                                If ($r.HostId -eq "00000000-0000-0000-0000-000000000000") {
                                        $HostName = ""
                                }
                                Else {
                                        $HostName = $($r.GetHost()).Name.ToLower()
                                }
                                $outputObj = New-RepoObject $r.Name $Hostname $r.Path $r.info.CachedFreeSpace $r.Info.CachedTotalSpace
                        }
                        $outputAry += $outputObj
                }
                End {
                        $outputAry
                }
        }
        #endregion

        #region: Start BRHost Connection
        Connect-VBRServer -Server $BRHost
        #endregion

        #region: Collect and filter Repos
        [Array]$repoList = Get-VBRBackupRepository | Where-Object {$_.Type -ne "SanSnapshotOnly"}
        [Array]$scaleouts = Get-VBRBackupRepository -scaleout
        if ($scaleouts) {
                foreach ($scaleout in $scaleouts) {
                        $extents = Get-VBRRepositoryExtent -Repository $scaleout
                        foreach ($ex in $extents) {
                                $repoList = $repoList + $ex.repository
                        }
                }
        }
        #endregion

        #region: Build Report
        $RepoReport = $repoList | Get-vPCRepoInfo | Select-Object       @{Name="Repository Name"; Expression = {$_.Target}},
                                                                        @{Name="Host"; Expression = {$_.RepoHost}},
                                                                        @{Name="Path"; Expression = {$_.Storepath}},
                                                                        @{Name="Free (GB)"; Expression = {$_.StorageFree}},
                                                                        @{Name="Total (GB)"; Expression = {$_.StorageTotal}},
                                                                        @{Name="Free (%)"; Expression = {$_.FreePercentage}}
        forEach ($Repo in $RepoReport) {

                $r = [pscustomobject]@{
                        Name = $($Repo.'Repository Name')
                        'Free (%)' = $($Repo.'Free (%)')
                        'Free (GB)' = $($Repo.'Free (GB)')
                        'Total (GB)' =  $($Repo.'Total (GB)')
                        'Host' =  $($Repo.'Host')
                        'Path' =  $($Repo.'Path')
                    }

                New-PoshBotCardResponse -Title "$($Repo.'Repository Name'):" -Text ($r | Format-List -Property * | Out-String)
        }
        #endregion
}

Veeam Plugin for PoshBot - Get-VeeamRepositories Full

Get-VeeamJobSessions

function Get-VeeamJobSessions {
        <#
        .SYNOPSIS
        Get last Veeam Job Sessions
        .EXAMPLE
        !Get-VeeamJobSessions
        .EXAMPLE
        !Get-VeeamJobSessions --brhost <your BR Host>
        .EXAMPLE
        !JobSessions
        .EXAMPLE
        !VeeamJobSessions
        #>

        [PoshBot.BotCommand(
                Aliases = ('JobSessions', 'VeeamJobSessions'),
                Permissions = 'read'
        )]
        [cmdletbinding()]
        param(
                [Parameter(Position=0, Mandatory=$false)]
                        [string] $BRHost = "localhost"
        )

        #region: Start BRHost Connection
        Connect-VBRServer -Server $BRHost
        #endregion

        #region: Collect Sessions
        $Stats = (Get-VeeamAllStat).jobsessions
        #endregion

        #region: Build Report
        forEach ($Stat in $Stats) {

                                $r = [pscustomobject]@{
                                        JobName = $Stat.name
                                        JobType = $Stat.type
                                        HasRan = $Stat.hasRan
                                        JobStatus = $Stat.status
                                        CreationTime  = $Stat.creationTime
                                        EndTime  = $Stat.endTime
                                        VMsTotal = $Stat.vmstotal
                                        VMsSuccess = $Stat.vmssuccess
                                        VMsWarning = $Stat.vmswarning
                                        VMsError  = $Stat.vmserror
                                    }

                                New-PoshBotCardResponse -Title "Job '$($Stat.name)' Stats:" -Text ($r | Format-List -Property * | Out-String)
                                }
        #endregion
}

In meinem Veeam Plugin for PoshBot verwendete Klassen und Funktionen aus dem genialen PowerShell-VeeamAllStats Modul von Timothy Dewin:

class VeeamAllStatJobSessionVM {
    $name
    $status;
    $starttime;
    $endtime;
    $size;
    $read;
    $transferred;
    $duration;
    $details;
    VeeamAllStatJobSessionVM() {}
    VeeamAllStatJobSessionVM($name,$status,$starttime,$endtime,$size,$read,$transferred,$duration,$details) {
        $this.name = $name
        $this.status = $status;
        $this.starttime = $starttime;
        $this.endtime = $endtime;
        $this.size = $size;
        $this.read = $read;
        $this.transferred = $transferred;
        $this.duration  = $duration;
        $this.details = $details
    }
}

class VeeamAllStatJobSession {
    $name = "<no name>"
    $type = "<no type>";
    $description = "";
    $status = "<not run>";
    [System.DateTime]$creationTime='1970-01-01 00:00:00';
    [System.DateTime]$endTime='1970-01-01 00:00:00';
    [System.TimeSpan]$duration
    $processedObjects=0;
    $totalObjects=0
    $totalSize=0
    $backupSize=0
    $dataRead=0
    $transferredSize=0
    $dedupe=0
    $compression=0
    $details=""
    $vmstotal=0
    $vmssuccess=0
    $vmswarning=0
    $vmserror=0
    $allerrors=@{}
    [bool]$hasRan=$true
    [VeeamAllStatJobSessionVM[]]$vmsessions= @()
    VeeamAllStatJobSession() { }
    VeeamAllStatJobSession($name,$type,$description) {
        $this.name = $name
        $this.type = $type;
        $this.description =$description;
    }
    VeeamAllStatJobSession($name,$type,$description,$status,$creationTime,$endtime,$processedObjects,$totalObjects,$totalSize,$backupSize,$dataRead,$transferredSize,$dedupe,$compression,$details) {
        $this.name = $name
        $this.type = $type;
        $this.description =$description;
        $this.status = $status;
        $this.creationTime=$creationTime;
        $this.endTime=$endTime;
        $this.processedObjects=$processedObjects;
        $this.totalObjects=$totalObjects
        $this.totalSize=$totalSize
        $this.backupSize=$backupSize
        $this.dataRead=$dataRead
        $this.transferredSize=$transferredSize
        $this.dedupe=$dedupe
        $this.compression=$compression
        $this.details=$details
        $this.duration=$endTime-$creationTime
    }
}

class VeeamAllStatJobMain {
    $versionVeeam
    $server
    $serverString
    [VeeamAllStatJobSession[]]$jobsessions
    VeeamAllStatJobMain () {
        $this.jobsessions = @()
    }
}



function Get-VeeamAllStatJobSessionVMs {
    param(
        $session,
        [VeeamAllStatJobSession]$statjobsession
    )

    $tasks = $session.GetTaskSessions()

    foreach($task in $tasks) {
        $s = $task.status
        $vm = [VeeamAllStatJobSessionVM]::new(
            $task.Name,
            $s,
            $task.Progress.StartTime,
            $task.Progress.StopTime,
            $task.Progress.ProcessedSize,
            $task.Progress.ReadSize,
            $task.Progress.TransferedSize,
            $task.Progress.Duration,
            $task.GetDetails())

        if ($s -ieq "success") {
            $statjobsession.vmssuccess += 1
        } elseif ($s -ieq "warning" -or $s -ieq "pending" -or $s -ieq "none") {
            $statjobsession.vmswarning +=1
        } else {
            $statjobsession.vmserror += 1
        }
        if ($vm.details -ne "") {
            $statjobsession.allerrors[$task.Name]=$vm.details
        }
        $statjobsession.vmsessions += $vm
        $statjobsession.vmstotal+=1
    }
}

function Get-VeeamAllStatJobSession {
    param(
        $job,
        $session
    )
    $statjob = [VeeamAllStatJobSession]::new(
        $job.Name,
        $session.JobType,
        $job.Description,
        $session.Result,
        $session.CreationTime,
        $session.EndTime,
        $session.Progress.ProcessedObjects,
        $session.Progress.TotalObjects,
        $session.Progress.TotalSize,
        $session.BackupStats.BackupSize,
        $session.Progress.ReadSize,
        $Session.Progress.TransferedSize,
        $session.BackupStats.GetDedupeX(),
        $session.BackupStats.GetCompressX(),
        $session.GetDetails()
    )
    Get-VeeamAllStatJobSessionVMs -session $session -statjobsession $statjob

    if ($session.Result -eq "None" -and $session.JobType -eq "BackupSync") {
        if($session.State -eq "Idle" -and $statjob.vmserror -eq 0 -and $statjob.vmswarning -eq 0 -and $statjob.allerrors.count -eq 0 -and $statjob.details -eq ""  -and $session.EndTime -gt $session.CreationTime ) {
            if ($session.Progress.Percents -eq 100) {
                $statjob.Status="Success"
            }
        }
    }

    return $statjob
}

function Get-VeeamAllStatJobSessions {
    param(
        [VeeamAllStatJobMain]$JobMain
    )

    $allsessions = Get-VBRBackupSession
    $allorderdedsess = $allsessions | Sort-Object -Property CreationTimeUTC -Descending
    $jobs = get-vbrjob

    foreach ($Job in $Jobs) {
        $lastsession = $allorderdedsess | ? { $_.jobid -eq $Job.id } | select -First 1
        if ($lastsession -ne $null) {
           $JobMain.jobsessions += Get-VeeamAllStatJobSession -job $job -session $lastsession
        } else {
           $s = [VeeamAllStatJobSession]::new($job.Name,$job.type,$job.description)
           $s.hasRan = $false
           $JobMain.jobsessions += $s
        }
    }

}

function Get-VeeamAllStatServerVersion {
    param(
        [VeeamAllStatJobMain]$JobMain
    )
    $versionstring = "Unknown Version"

    $pssversion = (Get-PSSnapin VeeamPSSnapin -ErrorAction SilentlyContinue)
    if ($pssversion -ne $null) {
        $versionstring = ("{0}.{1}" -f $pssversion.Version.Major,$pssversion.Version.Minor)
    }

    $corePath = Get-ItemProperty -Path "HKLM:\Software\Veeam\Veeam Backup and Replication\" -Name "CorePath" -ErrorAction SilentlyContinue
    if ($corePath -ne $null) {
        $depDLLPath = Join-Path -Path $corePath.CorePath -ChildPath "Packages\VeeamDeploymentDll.dll" -Resolve -ErrorAction SilentlyContinue
        if ($depDLLPath -ne $null -and (Test-Path -Path $depDLLPath)) {
            $file = Get-Item -Path $depDLLPath -ErrorAction SilentlyContinue
            if ($file -ne $null) {
                $versionstring = $file.VersionInfo.ProductVersion
            }
        }
    }

    $servsession = Get-VBRServerSession
    $JobMain.versionVeeam = $versionstring
    $JobMain.server = $servsession.server
    $JobMain.serverString = ("Server {0} : Veeam Backup & Replication {1}" -f $servsession.server,$versionstring)
}

function Get-VeeamAllStat {
    $report = [VeeamAllStatJobMain]::new()

    Get-VeeamAllStatServerVersion -JobMain $report
    Get-VeeamAllStatJobSessions -JobMain $report
    return $report
}

function Get-HumanDataSize {
 param([double]$numc)
 $num = $numc+0
 $trailing= "","K","M","G","T","P","E"
 $i=0
 while($num -gt 1024 -and $i -lt 6) {
  $num= $num/1024
  $i++
 }
 return ("{0:f1} {1}B" -f $num,$trailing[$i])
}

function Get-HumanDate {
    param([DateTime]$t)
    return $t.toString("yyyy-MM-dd HH:mm:ss")
}

function Get-HumanDuration {
    param([System.TimeSpan]$d)
    return ("{0:D2}:{1:D2}:{2:D2}" -f ($d.Hours+($d.Days*24)),$d.Minutes,$d.Seconds)
}

Veeam Plugin for PoshBot - Get-VeeamJobSessions

Detailansicht eines Jobs:

Veeam Plugin for PoshBot - Get-VeeamJobSessions Full

Der Aufruf mit dem alternativen PoshBot Kommando und dem Alias für Get-VeeamSessions aus dem Veeam Plugin for PoshBot sieht in meinem Falls so aus:

Veeam Plugin for PoshBot - bender, JobSessions

Get-VeeamJobs

function Get-VeeamJobs {
        <#
        .SYNOPSIS
        Get Veeam Jobs
        .EXAMPLE
        !Get-VeeamJobs
        .EXAMPLE
        !Get-VeeamJobs --brhost <your BR Host>
        .EXAMPLE
        !Jobs
        .EXAMPLE
        !VeeamJobs
        #>

        [PoshBot.BotCommand(
                Aliases = ('Jobs', 'VeeamJobs'),
                Permissions = 'read'
        )]
        [cmdletbinding()]
        param(
                [Parameter(Position=0, Mandatory=$false)]
                [string] $BRHost = "localhost"
        )

        #region: Start BRHost Connection
        Connect-VBRServer -Server $BRHost
        #endregion

        #region: Collect Jobs
        [Array] $Jobs = Get-VBRJob
        #endregion

        #region: Build Report
        $report = @()
        forEach ($Job in $Jobs) {

                $object = [pscustomobject]@{
                        Name = $Job.Name
                        JobType = $Job.JobType
                    }
                $report += $object
        }
        New-PoshBotCardResponse -Title "Veeam Jobs:" -Text ($report | Format-List -Property * | Out-String)
        #endregion
}

Veeam Plugin for PoshBot - Get-VeeamJobs

Start-VeeamQuickBackup

function Start-VeeamQuickBackup {
    <#
    .SYNOPSIS
    STart Veeam Quick Backup
    .EXAMPLE
    !Start-VeeamQuickBackup --ViEntity <VM Name>
    .EXAMPLE
    !Start-VeeamQuickBackup --ViEntity <VM Name> --brhost <your BR Host>
    .EXAMPLE
    !VuickBackup --ViEntity <VM Name>
    .EXAMPLE
    !VeeamQuickBackup --ViEntity <VM Name>
    #>

    [PoshBot.BotCommand(
        Aliases = ('QuickBackup', 'VeeamQuickBackup'),
        Permissions = 'write'
    )]
    [CmdletBinding()]
        param(
            [Parameter(Mandatory=$true, Position=0)]
            [ValidateNotNullorEmpty()]
            [String]$ViEntity,
            [Parameter(Mandatory=$false, Position=1)]
            [ValidateNotNullorEmpty()]
            [string] $BRHost = "localhost"
        )

    #region: Load PSSnapIn
    if ( (Get-PSSnapin -Name VeeamPSSnapin -ErrorAction SilentlyContinue) -eq $null ) {
        Add-PSsnapin VeeamPSSnapin
    }
    #endregion

    #region: Start BRHost Connection
    Connect-VBRServer -Server $BRHost
    #endregion

    if ($VeeamVm = Find-VBRViEntity -Name $ViEntity) {

        $Result = Start-VBRQuickBackup -VM $VeeamVm -Wait

        if ($Result) {

            #region: Build Report
            $report = @()
            $object = [pscustomobject]@{
                JobName = $Result.JobName
                Result = $Result.Result
                CreationTime = $Result.CreationTime
                EndTime = $Result.EndTime
                }
            $report += $object
            New-PoshBotCardResponse -Title "Veeam Quickbackup Result:" -Text ($report | Format-List -Property * | Out-String)
            #endregion
        }

        if ($Result.Result -eq "Failed") {
            Throw "Quick Backup Failed. See Veeam B&R Console for more details!"
        }

    }
    else {
        Throw "No ViEntity '$ViEntity' found!"
    }

}

Veeam Plugin for PoshBot - Start-VeeamQuickBackup

Install Veeam Plugin for PoshBot

Um mein Veeam Plugin for PoshBot ganz unkompliziert zur Verfügung zu stellen habe ich es mit dem entsprechenden Tag in der PowerShell Gallery veröffentlicht. Alle verfügbaren Plugins lassen sich mit dem Kommando !find-plugin ganz einfach auflisten und mit !install-plugin auch direkt installieren.

Veeam Plugin for PoshBot - Install-Plugin

Nach der Installation steht auch die Hilfe zu dem Plugin zur Verfügung. Besonders interessant sind hierbei auch die Aliase für die Kommandos, damit ist die Eingabe (besonders am Smartphone) doch etwas flotter.

Veeam Plugin for PoshBot - Plugin Help

Ebenso kann die Hilfe für die einzelnen Kommandos des Plugins angezeigt werden und auch die notwendigen Rechte geprüft werden, dazu aber später mehr.

Veeam Plugin for PoshBot - Command Help

Mit dem Slack Befehl !help poshbot.veeam:Get-VeeamJobSessions –detailed kann die vollständige Hilfe für das Kommando, inklusive Beispielen aufgerufen werden.

Die Ausführung der Kommandos des Plugins ist dank Access Control nochmal eine andere Sache:

Veeam Plugin for PoshBot - Not authorized

Nachdem der Slack User der passenden Gruppe zugeordnet wurde, klappt es aber auch mit den Kommandos:

Veeam Plugin for PoshBot - Authorized

Die Gruppe habe ich vorab mit den entsprechenden Befehlen vorbereitet und die Rechte für das Veeam Plugin for PoshBot vergeben:

!New-Group veeamadmins
!New-Role veeamadmins
!Add-GroupRole veeamadmins veeamadmins

!Add-RolePermission veeamadmins poshbot.veeam:read
!Add-RolePermission veeamadmins poshbot.veeam:write

Weiterentwicklung Veeam Plugin for PoshBot

Wie so viele meiner Projekte ist auch dieses im Status „Work in Progress“ und ich würde mich über etwas Input freuen welche Funktionen praktisch oder hilfreich wären.

Ich bin dankbar über jeden Kommentar zu diesem Beitrag bzw. Issue oder Enhancement Request in dem dazugehörigen  GitHub Projekt.

Leave a Reply