Kabompo.Logging.ps1

#
# Kabompo.Logging.ps1
#

function Register-KabompoLogging
{
    <#
    .SYNOPSIS
        Starts logging of KABOMPO module.
    .DESCRIPTION
        Initializes the logging to the KABOMPO logfile in the subdirectory "log". Please be sure, that the executing user has write access to this folder.
        The log file will be generated as a CMTrace (see Microsoft SCCM) compatible file.
    .PARAMETER LoggingPath
        Optional. Path of the folder where the log file is located. Default is "$($PSScriptRoot)\log".
        Fallback is "$($env:TEMP)\KABOMPO" (if path is not accessible or due to invalid characters given)
    .PARAMETER Name
        Optional. Base name of the log file. Default is "kabompo"
    .PARAMETER Extension
        Optional. Extension of the log file without a leading dot. Default is "log"
    .PARAMETER MaxSize
        Optional. Maximum size (bytes) of a log file at the time of the registration. Default: 5000000 - Min: 200 - Max: 2147483647 (i.e. 2GB)
        If a file "$LoggingPath\$Name.$Extension" exists and its size exceeds $MaxSize, then it will be renamed according to the pattern
        "$Name_[1-9][0-9]*.$Extension" - i.e. the next free number is added to the base name of the log file.
        Note: No size check is done during the logging, only at the begin of the logging
    .PARAMETER MaxAge
        Optional. All log files (i.e. matching the pattern "$LoggingPath\*.log") older than $MaxAge days will be deleted prior to the registrating
        of the current log file. Default: 60 - Min: 1 - Max: 32676 (i.e. more than 89 years)
    .PARAMETER LogDetailLevel
        Optional. The log's detail level. Default: 1 - Allowed values: 0 = Debug, 1 = Normal, 2 = Minimalistic
        Warnings and Errors will be logged whatever detail level is set. Logs of type 1 ("Normal") will be logged only if the
        log level matches the log's detail level (i.e. if the log level equals or exceeds the log's detail level).
    .EXAMPLE
        Register-Logging
    .EXAMPLE
        Register-Logging -Name "MyLog" -Extension "log" -MaxSize 5000000 -MaxAge 60
    .EXAMPLE
        Register-Logging -Name "MyLog" -Extension "log" -LogDetailLevel 2
    .NOTES
    .LINK
        https://marketplace.matrix42.com
    #>


    Param
    (
        [Parameter(Mandatory=$false)]
        [string]$LoggingPath="$($PSScriptRoot)\log", #path to logging file (just the folder)

        [Parameter(Mandatory=$false)]
        [string]$Name="kabompo", #basename of the logging file

        [Parameter(Mandatory=$false)]
        [string]$Extension="log", #extension of the logging file

        [Parameter(Mandatory=$false)]
        [ValidateRange([uint32]200,[uint32]2147483647)] #max 2^31-1 bytes (or 2GB)
        [uint32]$MaxSize=5000000, #max log file size at start of logging - not checked during logging!

        [Parameter(Mandatory=$false)]
        [ValidateRange(1,32767)] #max 2^15-1 days (or more than 89 years)
        [int]$MaxAge=60, #remove all log files older than $MaxAge days

        [Parameter(Mandatory=$false)]
        [ValidateSet(0,1,2)] #log detail level: 0 = Debug, 1 = Normal, 2 = Minimalistic
        [int] $LogDetailLevel=0 #logging will be done for every message with log level greater or equal this detail level
    )


    try {
        # should we do a verbose registration of logging module
        $DoVerbose = ($PSBoundParameters['Verbose'] -eq $true)

        # remove the current logfile store
        $global:kabompoLog = $null

        if ([string]::IsNullOrWhiteSpace($LoggingPath)) {
            $LoggingPath = "$($PSScriptRoot)\log"
        }
        
        # Check if path exist
        if(!(Test-Path "$LoggingPath")) {
            try {
                $logPath = (New-Item -Path "$LoggingPath" -ItemType directory -ErrorAction Stop).FullName
            }
            catch {
                $logPath = (New-Item -Path "$($env:TEMP)" -Name KABOMPO -ItemType directory -Force -ErrorAction Stop).FullName 
            }
        }
        else { $logPath = "$LoggingPath" }

        # define the absolute path of the logfile
        $logFile = "{0}\{1}.{2}" -f $logPath, $Name, $Extension

        # get files based on lastwrite filter and specified folder
        $Files = @(Get-ChildItem $logPath -Filter "*.$Extension" -ErrorAction Stop | where { $_.LastWriteTime -le ((Get-Date).AddDays(-$MaxAge))})

        # Delete all files older than $MaxAge days
        foreach ($File in $Files) { 
            Remove-Item $File.FullName | Out-Null 
        }

        # Is logfile size more than size $MaxSize bytes
        $log = @(Get-ChildItem $logPath -Filter "$Name.$Extension" -ErrorAction Stop | where { $_.Length -ge $MaxSize })
        if($log -ne $null) {
            # Find the next free logfile-name
            $logCount=1
            while ($targetLogName -eq $null) 
            { 
                $testLogName = "{0}_{1}.{2}" -f $Name, $logCount, $Extension
                if(!(Test-Path "$logPath\$testLogName"))
                    { $targetLogName = $testLogName } 
                else 
                    { $logCount++ }
            }
            # Rename logfile
            Rename-Item -Path $logFile -NewName $targetLogName
        }

        # Define logfile-name
        $global:kabompoLog = $logFile


        # Set LogLevel
        $global:LogDetailLevel = $LogDetailLevel


        # do an initial logging
        Add-KabompoLogLine -Message "----------------- New logfile worker initialized. Start logging ----------------- " -Component Register-KabompoLogging -Type 1 -Verbose:$DoVerbose
    }
    catch
    {
        throw $_.Exception
    }
}

function Add-KabompoLogLine
{
    <#
    .SYNOPSIS
        Add a line to the KABOMPO logfile.
    .DESCRIPTION
        Add a line to the registered KABOMPO logfile in a CMTrace (see Microsoft SCCM) compatible format.
        Please be sure, that the executing user has write access to file.
    .PARAMETER Message
        Mandatory. The message describing the event being logged. May be given as $null or empty string
        if -ErrorMessage parameter is given.
        Note: If neither $Message nor $ErrorMessage is given ($null or empty string) no logging will be done.
    .PARAMETER ErrorMessage
        Optional. Meant for errors - overwrites parameter -Type with value 3 (Error) if not null and not empty.
        If both $Message and $ErrorMessage are given then the CMTrace message is build from both parameters
        according to the pattern "$Message. Details: $ErrorMessage".
        Note: If neither $Message nor $ErrorMessage is given ($null or empty string) no logging will be done.
    .PARAMETER Component
        Optional. Fills the "component" field of the CMTrace log. Component "KABOMPO" is used if $Component is
        $null or empty.
    .PARAMETER Type
        Mandatory. Fills the "type" field of the CMTrace log (representing 1 for "Normal", 2 for "Warning" and
        3 for "Error"). The type will be overwritten by 3 ("Error") if a non-empty -ErrorMessage parameter is
        provided.
    .PARAMETER LogLevel
        Optional. Log level of the message provided. Default: 1 - Allowed values: 0 = Debug, 1 = Normal, 2 = Minimalistic
        The log level is only obeyed for events of type 1 ("Normal"), all other types (2 for "Warning"
        and 3 for "Error") are always logged. Logs of type 1 ("Normal") will be logged only if the
        log level matches the log's detail level (i.e. if the log level equals or exceeds the log's detail level).
    .PARAMETER Context
        Optional. Fills the "context" field of the CMTrace log. Default: "" (i.e. empty string)
    .PARAMETER Thread
        Optional. Fills the "thread" field of the CMTrace log. Default: "" (i.e. empty string)
    .PARAMETER File
        Optional. Fills the "file" field of the CMTrace log. Default: "" (i.e. empty string)
    .PARAMETER Verbose
        Optional. The bound PS parameter is used to do an additional output to the host. Some pretty printing
        is done by using $global:LogIndentationStep for defining indentation steps (i.e. a user of this logging
        tool as to maintain $global:LogIndentationStep him- or herself for pretty printing)
    .EXAMPLE
        Add-KabompoLogLine -Message "Some message" -Component My-CurrentFunction -Type 1
    .EXAMPLE
        Add-KabompoLogLine -Message "Some message" -ErrorMessage "My detailed error message" -Component My-CurrentFunction -Type 3
    .EXAMPLE
        Add-KabompoLogLine -Message "Also output to host" -ErrorMessage "My detailed error message" -Component My-CurrentFunction -Type 3 -Verbose
    .NOTES
    .LINK
        https://marketplace.matrix42.com
    #>

    Param 
    (
        [Parameter(Mandatory=$true)]
        [string]$Message,

        [Parameter(Mandatory=$false)]
        [string]$ErrorMessage="", #ErrorMessage overwrites Type (i.e. if set the logging type will always be 3 (Error - red))

        [Parameter(Mandatory=$false)]
        [string]$Component="",

        [Parameter(Mandatory=$true)]
        [ValidateSet(1,2,3)] #Type: 1 = Normal, 2 = Warning (yellow), 3 = Error (red)
        [int]$Type,

        [Parameter(Mandatory=$false)]
        [ValidateSet(0,1,2)] #Log Level: 0 = Debug, 1 = Normal, 2 = Minimalistic
        [int]$LogLevel = 1,

        [Parameter(Mandatory=$false)]
        [string]$Context="",

        [Parameter(Mandatory=$false)]
        [string]$Thread="",

        [Parameter(Mandatory=$false)]
        [string]$File=""
    )

    # Exit when logging is disabled for unit testing
    if($global:LogDetailLevel -eq 99) { 
        return 
    }

    # Exit function when given loglevel is less than configured loglevel
    if($LogLevel -lt $global:LogDetailLevel -and  $Type -eq 1) { 
        return 
    }

    # Exit function if idention level is too high for minimalistic loglevel
    if($global:logIndention -ge 1 -and $global:LogDetailLevel -eq 2 -and $Type -eq 1) { 
        return 
    }

    # Exit function if idention level is too high for normal loglevel
    if($global:logIndention -ge 2 -and $global:LogDetailLevel -eq 1 -and $Type -eq 1) { 
        return 
    }

    # exit function if logging is switched off
    if($LogLevel -eq 3) { 
        return 
    }

    # Exit function if no message is given
    if([string]::IsNullOrEmpty($Message) -and [string]::IsNullOrEmpty($ErrorMessage)) { 
        return 
    }
    
    # Use Errormessage as message when message is empty
    elseif([string]::IsNullOrEmpty($Message) -and ![string]::IsNullOrEmpty($ErrorMessage)) { 
        $Message = $ErrorMessage 
    }
    
    # Combine Errormessage and Message when both are given
    elseif(![string]::IsNullOrEmpty($Message) -and ![string]::IsNullOrEmpty($ErrorMessage)) {
        $Message = "{0}. Details: {1}" -f $Message, $ErrorMessage 
    }

    # Generate Date and Time string for CMTrace and Console Log
    $TimeString = Get-Date -Format "HH:mm:ss.ffffff"
    $DateString = Get-Date -Format "MM-dd-yyyy"

    # If errormessage is given, set message type to error
    if (![string]::IsNullOrEmpty($ErrorMessage)) {
        $Type = 3
    }

    # Set default comonent string when not given
    if ([string]::IsNullOrEmpty($Component)) {
        $Component = "KABOMPO"
    }
 
    # Configure Log Message output string
    $LogMessage = "<![LOG[{0}]LOG]!><time=""{1}"" date=""{2}"" component=""{3}"" context=""{4}"" type=""{5}"" thread=""{6}"" file=""{7}"">" -f $Message, $TimeString, $DateString, $Component, $Context, $Type, $Thread, $File
    $LogMessage | Out-File -Append -Encoding UTF8 -FilePath $global:kabompoLog

    # Write output also to host when enabled and PS-Host is Console or ISE
    if($PSBoundParameters['Verbose'] -eq $true) { 
        Write-KabompoLogToHost -Message $Message -Component $Component -Type $Type -DateString $DateString -TimeString $TimeString 
    }
}

function Write-KabompoLogToHost
{
    <#
    .SYNOPSIS
        Output a line to the Powershell console.
    .DESCRIPTION
        Output a line to the Powershell console. This function is just for internal use and not published in the module.
    .PARAMETER Message
        Mandatory. The message as provided to the CMTrace log entry
    .PARAMETER Component
        Mandatory. The component as provided to the "component" field of the CMTrace log entry
    .PARAMETER Type
        Mandatory. The type as provided to the "type" field of the CMTrace log entry
    .PARAMETER DateString
        Mandatory. The date as provided to the "date" field of the CMTrace log entry
    .PARAMETER TimeString
        Mandatory. The date as provided to the "time" field of the CMTrace log entry
    .EXAMPLE
        Write-KabompoLogToHost -Message "Some message" -Component My-CurrentFunction -Type 1 -DateString 2016-05-06 -TimeString 12:39:33.698623
    .NOTES
    .LINK
        https://marketplace.matrix42.com
    #>

    Param 
    (
        [Parameter(Mandatory=$true)]
        [string]$Message,

        [Parameter(Mandatory=$true)]
        [string]$Component,

        [Parameter(Mandatory=$true)]
        [ValidateSet(1,2,3)] #Type: 1 = Normal, 2 = Warning (yellow), 3 = Error (red)
        [int]$Type,

        [Parameter(Mandatory=$true)]
        [string]$DateString,

        [Parameter(Mandatory=$true)]
        [string]$TimeString

    )

    #$indent = (37 - $Component.Length) + ($global:logIndention * 2)
    $indent = (45 - $Component.Length) + ($global:logIndention * 2)
    # Output information line to the console with the normal powershell console colors
    $indentMessage = $Message.PadLeft($Message.Length + $indent, " ")
    if($Type -eq 1) { 
        $HostMessage = "{0}_{1} I [{2}] {3}" -f $DateString, $TimeString, $Component, $indentMessage
        Write-Verbose -Message $HostMessage
    }
    # Output warning line to the console in yellow
    elseif($Type -eq 2) { 
        $HostMessage = "{0}_{1} W [{2}] {3}" -f $DateString, $TimeString, $Component, $indentMessage
        Write-Verbose -Message $HostMessage
    }
    # Output error line to the console in red
    elseif($Type -eq 3) { 
        $HostMessage = "{0}_{1} E [{2}] {3}" -f $DateString, $TimeString, $Component, $indentMessage
        Write-Verbose -Message $HostMessage
    }

}

# SIG # Begin signature block
# MIINJAYJKoZIhvcNAQcCoIINFTCCDRECAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUKfDCANjLFhJmTncE4VPiC+yJ
# yL2gggpZMIIE+DCCA+CgAwIBAgIQXWihGXfS4Od7/8YeI88pIDANBgkqhkiG9w0B
# AQsFADB/MQswCQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRp
# b24xHzAdBgNVBAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxMDAuBgNVBAMTJ1N5
# bWFudGVjIENsYXNzIDMgU0hBMjU2IENvZGUgU2lnbmluZyBDQTAeFw0xNTAyMDMw
# MDAwMDBaFw0xODA0MDMyMzU5NTlaMGAxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIEwZI
# ZXNzZW4xEjAQBgNVBAcTCUZyYW5rZnVydDEVMBMGA1UEChQMTWF0cml4IDQyIEFH
# MRUwEwYDVQQDFAxNYXRyaXggNDIgQUcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
# ggEKAoIBAQDHZNuFnBatHdyRnMUJ2Y1P6k49MjD04t9S7ukWj8upP/B6wPRYzzQf
# 6QVMPKzMhNTPfIXaIEzi/jhT0jjwnXC0O9hF2rwTpDRnVOnVJemaqd2XsSDVEBUY
# yhH7QZ82Zwi1nnSgo69WL694L3f74ge6bRiRIJ/IoZawj+74NL/9B93kGKH89sew
# VtqPoDwSklJmzc86Qlgw6X/WXenHw8n6k/htEIjkHpiE6iGkDZp1gAPBERIV/qi/
# HyIZpvsO3g9RvcWEDvRvq6ZsIPfAvtlOnVWPrvik96pEDugHKPtvyjuAQtJtxw42
# zsYbB7lQxCo7khjhuZKYrzjv/l79BnC1AgMBAAGjggGNMIIBiTAJBgNVHRMEAjAA
# MA4GA1UdDwEB/wQEAwIHgDArBgNVHR8EJDAiMCCgHqAchhpodHRwOi8vc3Yuc3lt
# Y2IuY29tL3N2LmNybDBmBgNVHSAEXzBdMFsGC2CGSAGG+EUBBxcDMEwwIwYIKwYB
# BQUHAgEWF2h0dHBzOi8vZC5zeW1jYi5jb20vY3BzMCUGCCsGAQUFBwICMBkMF2h0
# dHBzOi8vZC5zeW1jYi5jb20vcnBhMBMGA1UdJQQMMAoGCCsGAQUFBwMDMFcGCCsG
# AQUFBwEBBEswSTAfBggrBgEFBQcwAYYTaHR0cDovL3N2LnN5bWNkLmNvbTAmBggr
# BgEFBQcwAoYaaHR0cDovL3N2LnN5bWNiLmNvbS9zdi5jcnQwHwYDVR0jBBgwFoAU
# ljtT8Hkzl699g+8uK8zKt4YecmYwHQYDVR0OBBYEFAAbz15M7iRhPipVPcQDgb2D
# MWA5MBEGCWCGSAGG+EIBAQQEAwIEEDAWBgorBgEEAYI3AgEbBAgwBgEBAAEB/zAN
# BgkqhkiG9w0BAQsFAAOCAQEAScMz5dR4/qoFzikf7Dj15lRfGWNY1IZ50GPxhhqo
# aMM8Vb8oyO3Z0J9bRuX/gwV80NaaKuC1MX2b/5yVLqTpZXvVAO4uwxNq5/BXywi5
# 7Wb6V2ld9Fey9TsSQHZLwbyYua1+yPGAnI7hcc/I9TAsHXIjx0kclwJm8FYMUh8+
# NnEnt7PTV3Z0ytUsR/oS7Kq5TYNhxKUpTP2vZfdYnM7yf4oglandpdsA0tJATc2r
# Gw/tdUEqOyynzPG5OI49SDkypSdFTdEnN70CwrHUcIplwY3Ro7hWWtHA4drT2bgh
# 1KsFyPjq3rDQD6xWAIuiFlBAwnr1DzdGFi2jMZfKLWNpXTCCBVkwggRBoAMCAQIC
# ED141/l2SWCyYX308B7KhiowDQYJKoZIhvcNAQELBQAwgcoxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1
# c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDIwMDYgVmVyaVNpZ24sIEluYy4gLSBG
# b3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNpZ24gQ2xhc3Mg
# MyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc1MB4X
# DTEzMTIxMDAwMDAwMFoXDTIzMTIwOTIzNTk1OVowfzELMAkGA1UEBhMCVVMxHTAb
# BgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8wHQYDVQQLExZTeW1hbnRlYyBU
# cnVzdCBOZXR3b3JrMTAwLgYDVQQDEydTeW1hbnRlYyBDbGFzcyAzIFNIQTI1NiBD
# b2RlIFNpZ25pbmcgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCX
# gx4AFq8ssdIIxNdok1FgHnH24ke021hNI2JqtL9aG1H3ow0Yd2i72DarLyFQ2p7z
# 518nTgvCl8gJcJOp2lwNTqQNkaC07BTOkXJULs6j20TpUhs/QTzKSuSqwOg5q1PM
# IdDMz3+b5sLMWGqCFe49Ns8cxZcHJI7xe74xLT1u3LWZQp9LYZVfHHDuF33bi+Vh
# iXjHaBuvEXgamK7EVUdT2bMy1qEORkDFl5KK0VOnmVuFNVfT6pNiYSAKxzB3JBFN
# YoO2untogjHuZcrf+dWNsjXcjCtvanJcYISc8gyUXsBWUgBIzNP4pX3eL9cT5Dio
# hNVGuBOGwhud6lo43ZvbAgMBAAGjggGDMIIBfzAvBggrBgEFBQcBAQQjMCEwHwYI
# KwYBBQUHMAGGE2h0dHA6Ly9zMi5zeW1jYi5jb20wEgYDVR0TAQH/BAgwBgEB/wIB
# ADBsBgNVHSAEZTBjMGEGC2CGSAGG+EUBBxcDMFIwJgYIKwYBBQUHAgEWGmh0dHA6
# Ly93d3cuc3ltYXV0aC5jb20vY3BzMCgGCCsGAQUFBwICMBwaGmh0dHA6Ly93d3cu
# c3ltYXV0aC5jb20vcnBhMDAGA1UdHwQpMCcwJaAjoCGGH2h0dHA6Ly9zMS5zeW1j
# Yi5jb20vcGNhMy1nNS5jcmwwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMD
# MA4GA1UdDwEB/wQEAwIBBjApBgNVHREEIjAgpB4wHDEaMBgGA1UEAxMRU3ltYW50
# ZWNQS0ktMS01NjcwHQYDVR0OBBYEFJY7U/B5M5evfYPvLivMyreGHnJmMB8GA1Ud
# IwQYMBaAFH/TZafC3ey78DAJ80M5+gKvMzEzMA0GCSqGSIb3DQEBCwUAA4IBAQAT
# hRoeaak396C9pK9+HWFT/p2MXgymdR54FyPd/ewaA1U5+3GVx2Vap44w0kRaYdtw
# b9ohBcIuc7pJ8dGT/l3JzV4D4ImeP3Qe1/c4i6nWz7s1LzNYqJJW0chNO4LmeYQW
# /CiwsUfzHaI+7ofZpn+kVqU/rYQuKd58vKiqoz0EAeq6k6IOUCIpF0yH5DoRX9ak
# JYmbBWsvtMkBTCd7C6wZBSKgYBU/2sn7TUyP+3Jnd/0nlMe6NQ6ISf6N/SivShK9
# DbOXBd5EDBX6NisD3MFQAfGhEV0U5eK9J0tUviuEXg+mw3QFCu+Xw4kisR93873N
# Q9TxTKk/tYuEr2Ty0BQhMYICNTCCAjECAQEwgZMwfzELMAkGA1UEBhMCVVMxHTAb
# BgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8wHQYDVQQLExZTeW1hbnRlYyBU
# cnVzdCBOZXR3b3JrMTAwLgYDVQQDEydTeW1hbnRlYyBDbGFzcyAzIFNIQTI1NiBD
# b2RlIFNpZ25pbmcgQ0ECEF1ooRl30uDne//GHiPPKSAwCQYFKw4DAhoFAKB4MBgG
# CisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcC
# AQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYE
# FEYCLdg9LjmFoMhhRdNYYETG8G1LMA0GCSqGSIb3DQEBAQUABIIBADPL17EyNroJ
# A/Yq87pht26AHX0oF4f6o/AQlMC1rATtvUKgfJNeA3RuSRAMeW/gSp3JXH4eH2xV
# eN77a5HR+WXLVarPKGCOJCncWl8D/wnPAkqNcUcQMbAHm0T9o2FAkpzKsDyrVNaP
# L7uC9FuaDnsBVO6fRijsrM9LznBwqp2FJyZUpuVFn1pn4rG8pvAzItNNcbvpYsto
# yohiFmuXH5u0tRgL7NNbaMXlx17k6eOzUgsmkEy0KE//7N5gAscZp8lYtnfzXJOr
# 1qPvHcKANrX+NlEy9sa0x+sTH6HqW3JlC0sLrNj8YhAJq4jKwvaNTAqty4AgRLJE
# Tjfi3MVFnx8=
# SIG # End signature block