Initial public release

This commit is contained in:
WebalexEU
2024-12-20 16:31:40 +01:00
commit 2becc990c9
12 changed files with 4943 additions and 0 deletions

571
src/WinBGP-API.ps1 Normal file
View File

@@ -0,0 +1,571 @@
###############################################################################
# #
# Name WinBGP-Api #
# #
# Description WinBGP API engine #
# #
# Notes Service is based on stevelee http listener example #
# (https://www.powershellgallery.com/packages/HttpListener) #
# #
# #
# Copyright (c) 2024 Alexandre JARDON | Webalex System. #
# All rights reserved.' #
# LicenseUri https://github.com/webalexeu/winbgp/blob/master/LICENSE #
# ProjectUri https://github.com/webalexeu/winbgp #
# #
###############################################################################
#Requires -version 5.1
# Based on
Param (
$Configuration=$false
)
$scriptVersion = '1.1.1'
# Create detailled log for WinBGP-API
# New-EventLog LogName Application Source 'WinBGP-API' -ErrorAction SilentlyContinue
# Logging function
function Write-Log {
<#
.Synopsis
Write-Log writes a message to a specified log file with the current time stamp.
.DESCRIPTION
The Write-Log function is designed to add logging capability to other scripts.
In addition to writing output and/or verbose you can write to a log file for
later debugging.
.NOTES
Created by: Jason Wasser @wasserja
Modified: 11/24/2015 09:30:19 AM
Changelog:
* Code simplification and clarification - thanks to @juneb_get_help
* Added documentation.
* Renamed LogPath parameter to Path to keep it standard - thanks to @JeffHicks
* Revised the Force switch to work as it should - thanks to @JeffHicks
To Do:
* Add error handling if trying to create a log file in a inaccessible location.
* Add ability to write $Message to $Verbose or $Error pipelines to eliminate
duplicates.
.PARAMETER Message
Message is the content that you wish to add to the log file.
.PARAMETER Path
The path to the log file to which you would like to write. By default the function will
create the path and file if it does not exist.
.PARAMETER Level
Specify the criticality of the log information being written to the log (i.e. Error, Warning, Informational)
.PARAMETER NoClobber
Use NoClobber if you do not wish to overwrite an existing file.
.EXAMPLE
Write-Log -Message 'Log message'
Writes the message to c:\Logs\PowerShellLog.log.
.EXAMPLE
Write-Log -Message 'Restarting Server.' -Path c:\Logs\Scriptoutput.log
Writes the content to the specified log file and creates the path and file specified.
.EXAMPLE
Write-Log -Message 'Folder does not exist.' -Path c:\Logs\Script.log -Level Error
Writes the message to the specified log file as an error message, and writes the message to the error pipeline.
.LINK
https://gallery.technet.microsoft.com/scriptcenter/Write-Log-PowerShell-999c32d0
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true)]
[ValidateNotNullOrEmpty()]
[Alias("LogContent")]
[string]$Message,
[Parameter(Mandatory=$false)]
[Alias('LogPath')]
[string]$Path,
[Parameter(Mandatory=$false)]
[ValidateSet("Error","Warning","Information")]
[string]$Level="Information",
[Parameter(Mandatory=$false)]
[string]$EventLogName='Application',
[Parameter(Mandatory=$false)]
[string]$EventLogSource='WinBGP',
[Parameter(Mandatory=$false)]
[string]$EventLogId=1006,
[Parameter(Mandatory=$false)]
[string]$EventLogCategory=0,
[Parameter(Mandatory=$false)]
[Array]$AdditionalFields=$null,
[Parameter(Mandatory=$false)]
[switch]$NoClobber
)
Begin
{
}
Process
{
#Create Log file only if Path is defined
if ($Path) {
# If the file already exists and NoClobber was specified, do not write to the log.
if ((Test-Path $Path) -AND $NoClobber) {
Write-Error "Log file $Path already exists, and you specified NoClobber. Either delete the file or specify a different name."
Return
}
# If attempting to write to a log file in a folder/path that doesn't exist create the file including the path.
elseif (!(Test-Path $Path)) {
Write-Verbose "Creating $Path."
$NewLogFile = New-Item $Path -Force -ItemType File
}
else {
# Nothing to see here yet.
}
# Format Date for our Log File
$FormattedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
# Write log entry to $Path
"$FormattedDate $Level $Message" | Out-File -FilePath $Path -Append
}
# Manage AdditionalFields (Not by default with PowerShell function)
if ($AdditionalFields) {
$EventInstance = [System.Diagnostics.EventInstance]::new($EventLogId, $EventLogCategory, $Level)
$NewEvent = [System.Diagnostics.EventLog]::new()
$NewEvent.Log = $EventLogName
$NewEvent.Source = $EventLogSource
[Array] $JoinedMessage = @(
$Message
$AdditionalFields | ForEach-Object { $_ }
)
$NewEvent.WriteEvent($EventInstance, $JoinedMessage)
} else {
#Write log to event viewer (Enabled by default)
Write-EventLog -LogName $EventLogName -Source $EventLogSource -EventId $EventLogId -EntryType $Level -Category $EventLogCategory -Message "$Message"
}
}
End
{
}
}
# Prometheus function
function New-PrometheusMetricDescriptor(
[Parameter(Mandatory = $true)][String] $Name,
[Parameter(Mandatory = $true)][String] $Type,
[Parameter(Mandatory = $true)][String] $Help,
[string[]] $Labels
) {
# Verification
if ($Name -notmatch "^[a-zA-Z_][a-zA-Z0-9_]*$") {
throw "Prometheus Descriptor Name '$($Name)' not valid"
}
foreach ($Label in $Labels) {
if ($Label -notmatch "^[a-zA-Z_][a-zA-Z0-9_]*$") {
throw "Prometheus Descriptor Label Name '$($Label)' not valid"
}
}
# return object
return [PSCustomObject]@{
PSTypeName = 'PrometheusMetricDescriptor'
Name = $Name
Help = $Help -replace "[\r\n]+", " " # Strip out new lines
Type = $Type
Labels = $Labels
}
}
function New-PrometheusMetric (
[Parameter(Mandatory = $true)][PSTypeName('PrometheusMetricDescriptor')] $PrometheusMetricDescriptor,
[Parameter(Mandatory = $true)][float] $Value,
[string[]] $Labels
) {
# Verification
if (($PrometheusMetricDescriptor.Labels).Count -ne $Labels.Count) {
throw "Metric labels are not matching the labels specified in the PrometheusMetricDescriptor provided"
}
# return object
return [PSCustomObject]@{
PSTypeName = 'PrometheusMetric'
Name = $PrometheusMetricDescriptor.Name
PrometheusMetricDescriptor = $PrometheusMetricDescriptor
Value = $Value
Labels = $Labels
}
}
function Export-PrometheusMetrics (
[PSTypeName('PrometheusMetric')][object[]] $Metrics
) {
$Lines = [System.Collections.Generic.List[String]]::new()
$LastDescriptor = $null
# Parse all metrics
foreach ($metric in $Metrics) {
if ($metric.PrometheusMetricDescriptor -ne $LastDescriptor) {
# Populate last descriptor
$LastDescriptor = $metric.PrometheusMetricDescriptor
$Lines.Add("# HELP $($LastDescriptor.Name) $($LastDescriptor.Help)")
$Lines.Add("# TYPE $($LastDescriptor.Name) $($LastDescriptor.Type)")
}
$FinalLabels = [System.Collections.Generic.List[String]]::new()
if (($metric.PrometheusMetricDescriptor.Labels).Count -gt 0) {
for ($i = 0; $i -lt ($metric.PrometheusMetricDescriptor.Labels).Count; $i++) {
$label = $metric.PrometheusMetricDescriptor.Labels[$i]
$value = ($metric.Labels[$i]).Replace("\", "\\").Replace("""", "\""").Replace("`n", "\n")
$FinalLabels.Add("$($label)=`"$($value)`"")
}
$StringLabels = $FinalLabels -join ","
$StringLabels = "{$StringLabels}"
} else {
$StringLabels = ""
}
$Lines.Add([String] $metric.Name + $StringLabels + " " + $metric.Value)
}
return $Lines -join "`n"
}
# Only processing if there is configuration
if ($Configuration) {
# Creating Prefixes variable
$ListenerPrefixes=@()
# Default Authentication Method/Group
$AuthenticationMethod='Anonymous'
$AuthenticationGroup='Administrators'
# Parsing all URIs
foreach ($item in $Configuration) {
[String] $Uri = $item.Uri
[System.Net.AuthenticationSchemes] $AuthMethod = $item.AuthenticationMethod
if ($AuthMethod -eq 'Negotiate') {
[String] $AuthGroup = $item.AuthenticationGroup
}
# Splitting Uri
[String]$Protocol = $Uri.Split('://')[0]
[String]$IP = $Uri.Split('://')[3]
[String]$Port = $Uri.Split('://')[4]
# TO IMPROVE
if ($IP -ne '127.0.0.1') {
$AuthenticationMethod=$AuthMethod
$AuthenticationGroup=$AuthGroup
}
# Manage certificate
$SSLConfigurationError=$false
if ($Protocol -eq 'https') {
# Populate variable
[String] $CertificateThumbprint = $item.CertificateThumbprint
# Managing cert on port
netsh http delete sslcert ipport="$($IP):$($Port)" | Out-Null
netsh http add sslcert ipport="$($IP):$($Port)" certhash="$CertificateThumbprint" appid='{00112233-4455-6677-8899-AABBCCDDEEFF}' | Out-Null
# Parsing netsh output to checck SSL configuration
$netshOutput=netsh http show sslcert ipport="$($IP):$($Port)" | Where-Object {($_.Split("`r`n")) -like '*IP:port*'}
if ($netshOutput -ne " IP:port : $($IP):$($Port)") {
Write-Log -Message "API failed - SSL configuration error" -Level Error
$SSLConfigurationError=$true
}
}
# If no SSL configuration error, checking
if ($SSLConfigurationError -ne $true) {
# Checking if listening port is available
if (Get-NetTCPConnection -LocalAddress $IP -LocalPort $Port -State Listen -ErrorAction SilentlyContinue) {
Write-Log -Message "Uri failed - Port '$($Port)' on IP '$($IP)' is already in use" -Level Error
} else {
# Adding / on Uri
$ListenerPrefixes+="$($Uri)/"
}
}
}
# If there is listener to start
if ($ListenerPrefixes) {
$listener = New-Object System.Net.HttpListener
# Adding listener
foreach ($ListenerPrefixe in $ListenerPrefixes) {
$listener.Prefixes.Add($ListenerPrefixe)
}
# Used previously when only one scheme was used
#$listener.AuthenticationSchemes = $AuthMethod
# Dynamic authentication schemes (TO IMPROVE)
$Listener.AuthenticationSchemeSelectorDelegate = { param($request)
# If local means Uri IP is 127.0.0.1
if ($request.IsLocal) {
# If local, we don't support authentication for now (TO IMPROVE)
return [System.Net.AuthenticationSchemes]::Anonymous
} else {
# TO IMPROVE
switch ( $AuthenticationMethod) {
'Negotiate' { $AuthenticationSchemes=[System.Net.AuthenticationSchemes]::IntegratedWindowsAuthentication }
'Anonymous' { $AuthenticationSchemes=[System.Net.AuthenticationSchemes]::Anonymous }
}
# Set default to anonymous
return $AuthenticationSchemes
}
}
# Starting listerner
$listener.Start()
# Output listeners
foreach ($ListenerPrefixe in $ListenerPrefixes) {
[String]$Protocol = $ListenerPrefixe.Split('://')[0]
[String]$IP = $ListenerPrefixe.Split('://')[3]
[String]$Port = $ListenerPrefixe.Split('://')[4]
Write-Log -Message "API started - Listening on '$($IP):$($Port)' (Protocol: $Protocol)"
}
while ($listener.IsListening) {
# Default return
$statusCode = [System.Net.HttpStatusCode]::OK
$commandOutput = [string]::Empty
$outputHeader = @{}
$context = $listener.GetContext()
$request = $context.Request
[string]$RequestHost=$request.RemoteEndPoint
$RequestHost=($RequestHost).split(':')[0]
# Manage authentication
$Authenticated=$false
if ($AuthenticationMethod -eq 'Negotiate') {
if ($request.IsAuthenticated) {
$RequestUser=$context.User.Identity.Name
if ($($context.User.IsInRole($AuthenticationGroup))) {
$Authenticated=$true
} else {
$statusCode = [System.Net.HttpStatusCode]::Forbidden
}
} else {
$statusCode = [System.Net.HttpStatusCode]::Unauthorized
}
} elseif ($AuthenticationMethod -eq 'Anonymous') {
$Authenticated=$true
$RequestUser='Anonymous'
}
# If local, we don't support authentication for now (TO IMPROVE)
if ($request.IsLocal -or $Authenticated) {
# Log every api request
# [string]$FullRequest = $request | Format-List * | Out-String
# Write-Log "API request received: $FullRequest" -EventLogSource 'WinBGP-API'
$FullPath=($request.RawUrl).substring(1)
$Path=$FullPath.Split('?')[0]
switch ($request.HttpMethod) {
'GET' {
if ($FullPath -eq 'api') {
$commandOutput = ConvertTo-Json -InputObject @{'message'='WinBGP API running'}
$statusCode = [System.Net.HttpStatusCode]::OK
} elseif ($FullPath -like 'api/*') {
$Path=$Path.replace('api/','')
$shortPath=$Path.Split('/')[0]
switch ($shortPath) {
'config' {
if (($Path -eq 'config') -or ($Path -eq 'config/')) {
$commandOutput = WinBGP -Config | ConvertTo-JSON
$outputHeader.Add('Content-Type', 'application/json')
$statusCode = [System.Net.HttpStatusCode]::OK
} else {
$SubConfig=$Path.Split('/')[1]
$commandOutput = (WinBGP -Config).$SubConfig
if ($commandOutput) {
$commandOutput = $commandOutput | ConvertTo-JSON
$outputHeader.Add('Content-Type', 'application/json')
$statusCode = [System.Net.HttpStatusCode]::OK
} else {
$statusCode = [System.Net.HttpStatusCode]::NotFound
}
}
}
'logs' {
$Last = $request.QueryString.Item("Last")
if (!($Last)) { $Last = 10 }
$commandOutput = WinBGP -Logs -Last $Last | Select-Object Index,TimeGenerated,@{Label='EntryType';Expression={($_.EntryType).ToString()}},Message,RouteName | ConvertTo-JSON
$outputHeader.Add('Content-Type', 'application/json')
$statusCode = [System.Net.HttpStatusCode]::OK
}
'peers' {
if (($Path -eq 'peers') -or ($Path -eq 'peers/')) {
$commandOutput = ConvertTo-Json -InputObject @(Get-BgpPeer | Select-Object PeerName,LocalIPAddress,LocalASN,PeerIPAddress,PeerASN,@{Label='ConnectivityStatus';Expression={$_.ConnectivityStatus.ToString()}}) #Using @() as inputobject to always return an array
$outputHeader.Add('Content-Type', 'application/json')
$statusCode = [System.Net.HttpStatusCode]::OK
} else {
$PeerName=$Path.Split('/')[1]
$commandOutput = Get-BgpPeer | Where-Object {$_.PeerName -eq $PeerName} | Select-Object PeerName,LocalIPAddress,PeerIPAddress,PeerASN,@{Label='ConnectivityStatus';Expression={$_.ConnectivityStatus.ToString()}}
if ($commandOutput) {
$commandOutput = $commandOutput | ConvertTo-JSON
$outputHeader.Add('Content-Type', 'application/json')
$statusCode = [System.Net.HttpStatusCode]::OK
} else {
$statusCode = [System.Net.HttpStatusCode]::NotFound
}
}
}
'router' {
$commandOutput = Get-BgpRouter | Select-Object BgpIdentifier,LocalASN,PeerName,PolicyName | ConvertTo-JSON
$statusCode = [System.Net.HttpStatusCode]::OK
}
'routes' {
if (($Path -eq 'routes') -or ($Path -eq 'routes/')) {
$commandOutput = ConvertTo-Json -InputObject @(WinBGP | Select-Object Name,Network,Status,@{Label='MaintenanceTimestamp';Expression={($_.MaintenanceTimestamp).ToString("yyyy-MM-ddTHH:mm:ss.fffK")}}) #Using @() as inputobject to always return an array
$outputHeader.Add('Content-Type', 'application/json')
$statusCode = [System.Net.HttpStatusCode]::OK
} else {
$RouteName=$Path.Split('/')[1]
$commandOutput = WinBGP | Where-Object {$_.Name -eq $RouteName} | Select-Object Name,Network,Status,@{Label='MaintenanceTimestamp';Expression={($_.MaintenanceTimestamp).ToString("yyyy-MM-ddTHH:mm:ss.fffK")}}
if ($commandOutput) {
$commandOutput = $commandOutput | ConvertTo-JSON
$outputHeader.Add('Content-Type', 'application/json')
$statusCode = [System.Net.HttpStatusCode]::OK
} else {
$statusCode = [System.Net.HttpStatusCode]::NotFound
}
}
}
'statistics' {
$commandOutput=(Invoke-CimMethod -ClassName "PS_BgpStatistics" -Namespace 'ROOT\Microsoft\Windows\RemoteAccess' -MethodName Get -OperationTimeoutSec 5).cmdletoutput | Select-Object PeerName,TcpConnectionEstablished,TcpConnectionClosed,@{Label='OpenMessage';Expression={$_.OpenMessage.CimInstanceProperties | Select-Object Name,Value}},@{Label='NotificationMessage';Expression={$_.NotificationMessage.CimInstanceProperties | Select-Object Name,Value}},@{Label='KeepAliveMessage';Expression={$_.KeepAliveMessage.CimInstanceProperties | Select-Object Name,Value}},@{Label='RouteRefreshMessage';Expression={$_.RouteRefreshMessage.CimInstanceProperties | Select-Object Name,Value}},@{Label='UpdateMessage';Expression={$_.UpdateMessage.CimInstanceProperties | Select-Object Name,Value}},@{Label='IPv4Route';Expression={$_.IPv4Route.CimInstanceProperties | Select-Object Name,Value}},@{Label='IPv6Route';Expression={$_.IPv6Route.CimInstanceProperties | Select-Object Name,Value}} | ConvertTo-JSON
$outputHeader.Add('Content-Type', 'application/json')
$statusCode = [System.Net.HttpStatusCode]::OK
}
'status' {
[string]$status = WinBGP -Status
$commandOutput = ConvertTo-Json -InputObject @{'service'=$status}
$outputHeader.Add('Content-Type', 'application/json')
$statusCode = [System.Net.HttpStatusCode]::OK
}
'version' {
$commandOutput = WinBGP -Version | ConvertTo-JSON
$outputHeader.Add('Content-Type', 'application/json')
$statusCode = [System.Net.HttpStatusCode]::OK
}
Default {
$statusCode = [System.Net.HttpStatusCode]::NotImplemented
}
}
} elseif ($FullPath -eq 'metrics') {
# Define WinBGP Prometheus metrics
$WinBGP_metrics=@()
# WinBGP peer status
$state_peerDescriptor=New-PrometheusMetricDescriptor -Name winbgp_state_peer -Type gauge -Help 'WinBGP Peers status' -Labels local_asn,local_ip,name,peer_asn,peer_ip,state
$peerStatus=@('connected','connecting','stopped')
# Try/catch to detect if BGP is configured properly
$BgpStatus=$null
try {
$peersCurrentStatus=Get-BgpPeer -ErrorAction SilentlyContinue | Select-Object PeerName,LocalIPAddress,LocalASN,PeerIPAddress,PeerASN,@{Label='ConnectivityStatus';Expression={$_.ConnectivityStatus.ToString()}}
}
catch {
#If BGP Router (Local) is not configured, catch it
$BgpStatus=($_).ToString()
}
if ($BgpStatus -eq 'BGP is not configured.') {
$peersCurrentStatus=$null
}
# Parse all peers and generate metric
foreach ($peerCurrentStatus in $peersCurrentStatus) {
foreach ($status in $peerStatus) {
$WinBGP_metrics+=New-PrometheusMetric -PrometheusMetricDescriptor $state_peerDescriptor -Value $(if ($status -eq $peerCurrentStatus.ConnectivityStatus) { 1 } else { 0 }) -Labels $peerCurrentStatus.LocalASN,$peerCurrentStatus.LocalIPAddress,$peerCurrentStatus.PeerName,$peerCurrentStatus.PeerASN,$peerCurrentStatus.PeerIPAddress,$status
}
}
# WinBGP route status
$state_routeDescriptor=New-PrometheusMetricDescriptor -Name winbgp_state_route -Type gauge -Help 'WinBGP routes status' -Labels family,maintenance_timestamp,name,network,state
$routeStatus=@('down','maintenance','up','warning')
# Silently continue as WinBGP is generating errors when BGP is not configured (TO REVIEW)
$routesCurrentStatus=(WinBGP -ErrorAction SilentlyContinue | Select-Object Name,Network,Status,@{Label='MaintenanceTimestamp';Expression={($_.MaintenanceTimestamp).ToString("yyyy-MM-ddTHH:mm:ss.fffK")}})
foreach ($routeCurrentStatus in $routesCurrentStatus) {
foreach ($status in $routeStatus) {
$WinBGP_metrics+=New-PrometheusMetric -PrometheusMetricDescriptor $state_routeDescriptor -Value $(if ($status -eq $routeCurrentStatus.Status) { 1 } else { 0 }) -Labels 'ipv4',$routeCurrentStatus.MaintenanceTimestamp,$routeCurrentStatus.Name,$routeCurrentStatus.Network,$status
}
}
# Return output
$commandOutput = Export-PrometheusMetrics -Metrics $WinBGP_metrics
# Add header
$outputHeader.Add('Content-Type', 'text/plain; version=0.0.4; charset=utf-8')
$statusCode = [System.Net.HttpStatusCode]::OK
} else {
$statusCode = [System.Net.HttpStatusCode]::NotImplemented
}
}
'POST' {
if ($FullPath -like 'api/*') {
$RouteName = $request.QueryString.Item("RouteName")
$Path=$Path.replace('api/','')
Write-Log "API received POST request '$Path' from '$RequestUser' - Source IP: '$RequestHost'" -AdditionalFields $RouteName
switch ($Path) {
'Reload' {
[string]$ActionOutput=WinBGP -Reload
$commandOutput = ConvertTo-Json -InputObject @{'output'=$ActionOutput}
$outputHeader.Add('Content-Type', 'application/json')
}
'StartMaintenance' {
[string]$ActionOutput=WinBGP -RouteName "$RouteName" -StartMaintenance
$commandOutput = ConvertTo-Json -InputObject @{'output'=$ActionOutput}
$outputHeader.Add('Content-Type', 'application/json')
}
'StartRoute' {
[string]$ActionOutput=WinBGP -RouteName "$RouteName" -StartRoute
$commandOutput = ConvertTo-Json -InputObject @{'output'=$ActionOutput}
$outputHeader.Add('Content-Type', 'application/json')
}
'StopMaintenance' {
[string]$ActionOutput=WinBGP -RouteName "$RouteName" -StopMaintenance
$commandOutput = ConvertTo-Json -InputObject @{'output'=$ActionOutput}
$outputHeader.Add('Content-Type', 'application/json')
}
'StopRoute' {
[string]$ActionOutput=WinBGP -RouteName "$RouteName" -StopRoute
$commandOutput = ConvertTo-Json -InputObject @{'output'=$ActionOutput}
$outputHeader.Add('Content-Type', 'application/json')
}
Default {
$statusCode = [System.Net.HttpStatusCode]::NotImplemented
}
}
switch ($commandOutput.output) {
'Success' { $statusCode = [System.Net.HttpStatusCode]::OK }
'WinBGP not ready' { $statusCode = [System.Net.HttpStatusCode]::InternalServerError }
}
} else {
$statusCode = [System.Net.HttpStatusCode]::NotImplemented
}
}
Default {
$statusCode = [System.Net.HttpStatusCode]::NotImplemented
}
}
}
$response = $context.Response
$response.StatusCode = $statusCode
foreach ($header in $outputHeader.Keys)
{
foreach ($headerValue in $outputHeader.$header)
{
$response.Headers.Add($header, $headerValue)
}
}
$buffer = [System.Text.Encoding]::UTF8.GetBytes($commandOutput)
$response.ContentLength64 = $buffer.Length
$output = $response.OutputStream
$output.Write($buffer,0,$buffer.Length)
$output.Close()
}
$listener.Stop()
} else {
Write-Log -Message "API failed - No Uri listener available" -Level Error
}
} else {
$OutputVersion=@{
'Version'=$scriptVersion
}
return $OutputVersion
}

1744
src/WinBGP-Engine.ps1 Normal file

File diff suppressed because it is too large Load Diff

522
src/WinBGP-HealthCheck.ps1 Normal file
View File

@@ -0,0 +1,522 @@
###############################################################################
# #
# Name WinBGP-HealthCheck #
# #
# Description WinBGP Routes HealthCheck #
# #
# Notes #
# #
# #
# Copyright (c) 2024 Alexandre JARDON | Webalex System. #
# All rights reserved.' #
# LicenseUri https://github.com/webalexeu/winbgp/blob/master/LICENSE #
# ProjectUri https://github.com/webalexeu/winbgp #
# #
###############################################################################
#Requires -version 5.1
Param (
$Route=$false
)
$scriptVersion = '1.2.1'
#Logging function
function Write-Log {
<#
.Synopsis
Write-Log writes a message to a specified log file with the current time stamp.
.DESCRIPTION
The Write-Log function is designed to add logging capability to other scripts.
In addition to writing output and/or verbose you can write to a log file for
later debugging.
.NOTES
Created by: Jason Wasser @wasserja
Modified: 11/24/2015 09:30:19 AM
Changelog:
* Code simplification and clarification - thanks to @juneb_get_help
* Added documentation.
* Renamed LogPath parameter to Path to keep it standard - thanks to @JeffHicks
* Revised the Force switch to work as it should - thanks to @JeffHicks
To Do:
* Add error handling if trying to create a log file in a inaccessible location.
* Add ability to write $Message to $Verbose or $Error pipelines to eliminate
duplicates.
.PARAMETER Message
Message is the content that you wish to add to the log file.
.PARAMETER Path
The path to the log file to which you would like to write. By default the function will
create the path and file if it does not exist.
.PARAMETER Level
Specify the criticality of the log information being written to the log (i.e. Error, Warning, Informational)
.PARAMETER NoClobber
Use NoClobber if you do not wish to overwrite an existing file.
.EXAMPLE
Write-Log -Message 'Log message'
Writes the message to c:\Logs\PowerShellLog.log.
.EXAMPLE
Write-Log -Message 'Restarting Server.' -Path c:\Logs\Scriptoutput.log
Writes the content to the specified log file and creates the path and file specified.
.EXAMPLE
Write-Log -Message 'Folder does not exist.' -Path c:\Logs\Script.log -Level Error
Writes the message to the specified log file as an error message, and writes the message to the error pipeline.
.LINK
https://gallery.technet.microsoft.com/scriptcenter/Write-Log-PowerShell-999c32d0
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true)]
[ValidateNotNullOrEmpty()]
[Alias("LogContent")]
[string]$Message,
[Parameter(Mandatory=$false)]
[Alias('LogPath')]
[string]$Path,
[Parameter(Mandatory=$false)]
[ValidateSet("Error","Warning","Information")]
[string]$Level="Information",
[Parameter(Mandatory=$false)]
[string]$EventLogName='Application',
[Parameter(Mandatory=$false)]
[string]$EventLogSource='WinBGP',
[Parameter(Mandatory=$false)]
[string]$EventLogId=1006,
[Parameter(Mandatory=$false)]
[string]$EventLogCategory=0,
[Parameter(Mandatory=$false)]
[Array]$AdditionalFields=$null,
[Parameter(Mandatory=$false)]
[switch]$NoClobber
)
Begin
{
}
Process
{
#Create Log file only if Path is defined
if ($Path) {
# If the file already exists and NoClobber was specified, do not write to the log.
if ((Test-Path $Path) -AND $NoClobber) {
Write-Error "Log file $Path already exists, and you specified NoClobber. Either delete the file or specify a different name."
Return
}
# If attempting to write to a log file in a folder/path that doesn't exist create the file including the path.
elseif (!(Test-Path $Path)) {
Write-Verbose "Creating $Path."
$NewLogFile = New-Item $Path -Force -ItemType File
}
else {
# Nothing to see here yet.
}
# Format Date for our Log File
$FormattedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
# Write log entry to $Path
"$FormattedDate $Level $Message" | Out-File -FilePath $Path -Append
}
# Manage AdditionalFields (Not by default with PowerShell function)
if ($AdditionalFields) {
$EventInstance = [System.Diagnostics.EventInstance]::new($EventLogId, $EventLogCategory, $Level)
$NewEvent = [System.Diagnostics.EventLog]::new()
$NewEvent.Log = $EventLogName
$NewEvent.Source = $EventLogSource
[Array] $JoinedMessage = @(
$Message
$AdditionalFields | ForEach-Object { $_ }
)
$NewEvent.WriteEvent($EventInstance, $JoinedMessage)
} else {
#Write log to event viewer (Enabled by default)
Write-EventLog -LogName $EventLogName -Source $EventLogSource -EventId $EventLogId -EntryType $Level -Category $EventLogCategory -Message "$Message"
}
}
End
{
}
}
#-----------------------------------------------------------------------------#
# #
# Function Add-IP #
# #
# Description Add IP address on the network card #
# #
# Arguments See the Param() block at the top of this script #
# #
# Notes #
# #
# History #
# #
#-----------------------------------------------------------------------------#
function Add-IP()
{
Param
(
[Parameter(Mandatory=$true)]
$Route
)
if($route.DynamicIpSetup) {
#Add IP
$IPAddress=$route.Network.split('/')[0]
$Netmask=$route.Network.split('/')[1]
#Add new IP (SkipAsSource:The addresses are not used for outgoing traffic and are not registered in DNS)
if ((Get-NetIPAddress -InterfaceAlias "$($route.Interface)").IPAddress -notcontains "$IPAddress"){
Write-Log "Add IP Address '$($route.Network)' on interface '$($route.Interface)'" -AdditionalFields @($route.RouteName)
New-NetIPAddress -InterfaceAlias "$($route.Interface)" -IPAddress $IPAddress -PrefixLength $Netmask -SkipAsSource:$true -PolicyStore ActiveStore
# Waiting IP to be mounted
while ((Get-NetIPAddress -InterfaceAlias "$($route.Interface)" -IPAddress $IPAddress).AddressState -eq 'Tentative'){}
if ((Get-NetIPAddress -InterfaceAlias "$($route.Interface)" -IPAddress $IPAddress).AddressState -eq 'Preferred') {
Write-Log "IP Address '$($route.Network)' on interface '$($route.Interface)' successfully added" -AdditionalFields @($route.RouteName)
$announce_route=$true
} elseif ((Get-NetIPAddress -InterfaceAlias "$($route.Interface)" -IPAddress $IPAddress).AddressState -eq 'Duplicate') {
$announce_route=$false
Remove-NetIPAddress -IPAddress $IPAddress -Confirm:$false
Write-Log "Duplicate IP - Unable to add IP Address '$($route.Network)' on interface '$($route.Interface)'" -Level Error -AdditionalFields @($route.RouteName)
Write-Log "Set ArpRetryCount to '0' to avoid this error" -Level Warning
} else {
$announce_route=$false
Write-Log "Unknown error - Unable to add IP Address '$($route.Network)' on interface '$($route.Interface)'" -Level Error -AdditionalFields @($route.RouteName)
}
} else {
# IP already there, announce route
$announce_route=$true
}
}
else {
# Always announce route
$announce_route=$true
Write-Log "IP Address '$($route.Network)' not managed by WinBGP Service" -Level Warning -AdditionalFields @($route.RouteName)
}
# Return status
return $announce_route
}
#-----------------------------------------------------------------------------#
# #
# Function remove-IP #
# #
# Description Remove IP address on the network card #
# #
# Arguments See the Param() block at the top of this script #
# #
# Notes #
# #
# History #
# #
#-----------------------------------------------------------------------------#
function Remove-IP()
{
Param
(
[Parameter(Mandatory=$true)]
$Route
)
if($route.DynamicIpSetup){
#Remove IP
$IPAddress=$route.Network.split('/')[0]
if ((Get-NetIPAddress -InterfaceAlias "$($route.Interface)").IPAddress -contains "$IPAddress"){
Write-Log "Remove IP Address '$($route.Network)' on interface '$($route.Interface)'" -AdditionalFields @($route.RouteName)
Remove-NetIPAddress -IPAddress $IPAddress -Confirm:$false
}
}
else {
Write-Log "IP Address '$($route.Network)' not managed by WinBGP Service" -Level Warning -AdditionalFields @($route.RouteName)
}
}
# IPC communication with WinBGP-Engine
Function Send-PipeMessage () {
Param(
[Parameter(Mandatory=$true)]
[String]$PipeName, # Named pipe name
[Parameter(Mandatory=$true)]
[String]$Message # Message string
)
$PipeDir = [System.IO.Pipes.PipeDirection]::Out
$PipeOpt = [System.IO.Pipes.PipeOptions]::Asynchronous
$pipe = $null # Named pipe stream
$sw = $null # Stream Writer
try {
$pipe = new-object System.IO.Pipes.NamedPipeClientStream(".", $PipeName, $PipeDir, $PipeOpt)
$sw = new-object System.IO.StreamWriter($pipe)
$pipe.Connect(1000)
if (!$pipe.IsConnected) {
throw "Failed to connect client to pipe $pipeName"
}
$sw.AutoFlush = $true
$sw.WriteLine($Message)
} catch {
Write-Log "Error sending pipe $pipeName message: $_" -Level Error
} finally {
if ($sw) {
$sw.Dispose() # Release resources
$sw = $null # Force the PowerShell garbage collector to delete the .net object
}
if ($pipe) {
$pipe.Dispose() # Release resources
$pipe = $null # Force the PowerShell garbage collector to delete the .net object
}
}
}
function Send-RouteControl {
param (
[Parameter(Mandatory=$true)]
[String]$RouteName, # Route Name
[Parameter(Mandatory=$true)]
[String]$Control # Control
)
$PipeStatus=$null
# Performing Action
try {
# Temporary
$pipeName='Service_WinBGP'
$Message="route $($RouteName) $($Control)"
Send-PipeMessage -PipeName $pipeName -Message $Message
}
catch {
$PipeStatus=($_).ToString()
}
if ($PipeStatus -like "*Pipe hasn't been connected yet*") {
return "WinBGP not ready"
} else {
# TO BE IMPROVED to get status
return "Success"
}
}
if ($Route) {
# Waiting for Pipe to be started before starting healtcheck (as healtcheck need the pipe to communicate)
# Temporary
$pipeName='Service_WinBGP'
while([System.IO.Directory]::GetFiles("\\.\\pipe\\") -notcontains "\\.\\pipe\\$($pipeName)") {
# Wait 1 seconds before checking again
Start-Sleep -Seconds 1
}
Write-Log -Message "HealthCheck Process started" -AdditionalFields @($Route.RouteName)
# Initialize variables
$rise_counter = 0
$fall_counter = 0
# Start a periodic timer
$timer = new-object System.Timers.Timer
$timer.Interval = ($route.Interval * 1000) # Milliseconds
$timer.AutoReset = $true # Make it fire repeatedly
Register-ObjectEvent $timer -EventName Elapsed -SourceIdentifier 'Timer'
$timer.start() # Must be stopped in the finally block
# Start a watchdog timer
$watchdog = new-object System.Timers.Timer
$watchdog.Interval = (30 * 1000) # Milliseconds
$watchdog.AutoReset = $true # Make it fire repeatedly
Register-ObjectEvent $watchdog -EventName Elapsed -SourceIdentifier 'Watchdog'
$watchdog.start() # Must be stopped in the finally block
if ($Route.WithdrawOnDown) {
$pos = ($Route.WithdrawOnDownCheck).IndexOf(":")
$check_method = ($Route.WithdrawOnDownCheck).Substring(0, $pos)
$check_name = ($Route.WithdrawOnDownCheck).Substring($pos+2)
switch($check_method) {
'service' {
# Service check from Json configuration
$check_expression="if ((Get-Service $check_name).Status -eq 'Running') {return `$true} else {return `$false}"
}
'process' {
# Process check from Json configuration
$check_expression="if ((Get-Process -ProcessName $check_name -ErrorAction SilentlyContinue).count -ge '1') {return `$true} else {return `$false}"
}
'tcp' {
# TCP port check from Json configuration
$host_to_check=$check_name.split(":")[0]
$port_to_check=$check_name.split(":")[1]
$check_expression="if ((Test-NetConnection $host_to_check -Port $port_to_check).tcptestsucceeded) {return `$true} else {return `$false}"
}
'cluster' {
# Cluster resource check from Json configuration
$check_expression="if ((Get-ClusterNode -Name `"$env:COMPUTERNAME`" -ErrorAction SilentlyContinue | Get-ClusterResource -Name `"$check_name`" -ErrorAction SilentlyContinue).State -eq 'Online') {return `$true} else {return `$false}"
}
'custom' {
# Custom check from Json configuration [Return status should be a Boolean (mandatory)]
$check_expression=$check_name
# Rewrite $check_name for logging
$check_name='check'
}
}
$check_method_name=(Get-Culture).textinfo.totitlecase($check_method.tolower())
$check_log_output="$check_method_name '$check_name'"
} else {
$check_log_output='WithdrawOnDown not enabled'
# Bypass Rise counter
$rise_counter=$Route.Rise
$rise_counter--
}
do {
$timer_event = Wait-Event # Wait for the next incoming event
if ($Route.WithdrawOnDown) {
# Default status is false
[bool]$check_status=$false
# Performing check
$check_status=Invoke-Expression -Command $check_expression
} else {
# Check always true as there is no check to perform
[bool]$check_status=$true
}
# Depending on the timer source
switch ($timer_event.SourceIdentifier) {
'Timer' {
# If check is OK
if ($check_status) {
# Create status log
if ($Route.WithdrawOnDown) {
$check_status_output="$check_log_output UP"
} else {
$check_status_output=$check_log_output
}
# Increment counter
$rise_counter++
# Waiting for rise threshold
if ($rise_counter -ge $Route.Rise) {
# Reset counter (only when rise has been reached)
$fall_counter=0
# Only when threshold is reached (Only once)
if ($rise_counter -eq $Route.Rise) {
# If route already announced
if ((Get-BgpCustomRoute).Network -contains "$($Route.Network)") {
Write-Log -Message "$check_status_output - Route already started" -AdditionalFields @($Route.RouteName)
} else {
if ($Route.WithdrawOnDown) {
Write-Log -Message "$check_status_output - Rise threshold reached" -AdditionalFields @($Route.RouteName)
}
Write-Log -Message "$check_status_output - Trigger route start" -AdditionalFields @($Route.RouteName)
# Call function to start BGP route
$output=Send-RouteControl -RouteName $Route.RouteName -Control 'start'
if ($output -ne 'Success') {
$rise_counter--
Write-Log -Message "Route start error - Trigger retry" -AdditionalFields @($Route.RouteName) -Level Error
}
}
}
} else {
Write-Log -Message "$check_status_output - Rise attempt: $rise_counter (Threshold: $($Route.Rise))" -AdditionalFields @($Route.RouteName)
}
# If check fail
} else {
# Create status log
if ($Route.WithdrawOnDown) {
$check_status_output="$check_log_output DOWN"
} else {
$check_status_output=$check_log_output
}
# Increment counter
$fall_counter++
# Waiting for fall threshold
if ($fall_counter -ge $Route.Fall) {
# Reset counter (only when fall has been reached)
$rise_counter=0
# Only when threshold is reached (Only once)
if ($fall_counter -eq $Route.Fall) {
# If route already unannounced
if ((Get-BgpCustomRoute).Network -notcontains "$($Route.Network)") {
Write-Log -Message "$check_status_output - Route already stopped" -AdditionalFields @($Route.RouteName)
} else {
if ($Route.WithdrawOnDown) {
Write-Log -Message "$check_status_output - Fall threshold reached" -AdditionalFields @($Route.RouteName)
}
Write-Log -Message "$check_status_output - Trigger route stop" -AdditionalFields @($Route.RouteName)
# Call function to stop BGP route
$output=Send-RouteControl -RouteName $Route.RouteName -Control 'stop'
if ($output -ne 'Success') {
$fall_counter--
Write-Log -Message "Route stop error - Trigger retry" -AdditionalFields @($Route.RouteName) -Level Error
}
}
}
} else {
Write-Log -Message "$check_status_output - Fall attempt: $fall_counter (Threshold: $($Route.Fall))" -AdditionalFields @($Route.RouteName)
}
}
}
'Watchdog' {
# If check is OK
if ($check_status) {
# Waiting for rise threshold
if ($rise_counter -gt $Route.Rise) {
# Announce the route from Json configuration if there is no route
if ((Get-BgpCustomRoute).Network -notcontains "$($Route.Network)")
{
Write-Log -Message "$check_status_output but route not started - Trigger route start (Watchdog)" -AdditionalFields @($Route.RouteName) -Level Warning
# Call function to start BGP route
$output=Send-RouteControl -RouteName $Route.RouteName -Control 'start'
if ($output -ne 'Success') {
Write-Log -Message "Route start error" -AdditionalFields @($Route.RouteName) -Level Error
}
} else {
# Checking IP is mounted properly
if($route.DynamicIpSetup) {
if (!(Get-NetIPAddress -IPAddress "$($route.Network.split('/')[0])")) {
Write-Log "Route announced but IP Address not mounted (Watchdog)" -AdditionalFields @($route.RouteName) -Level Warning
Add-IP $route
}
}
}
}
# If check fail
} else {
# Waiting for fall threshold
if ($fall_counter -gt $Route.Fall) {
# Stop Announce the route from Json configuration
if ((Get-BgpCustomRoute).Network -contains "$($Route.Network)")
{
Write-Log -Message "$check_status_output but route not stopped - Trigger route stop (Watchdog)" -AdditionalFields @($Route.RouteName)
# Call function to remove BGP route
$output=Send-RouteControl -RouteName $Route.RouteName -Control 'stop'
if ($output -ne 'Success') {
Write-Log -Message "Route stop error" -AdditionalFields @($Route.RouteName) -Level Error
}
} else {
# Checking IP is mounted properly
if($route.DynamicIpSetup) {
if (Get-NetIPAddress -IPAddress "$($route.Network.split('/')[0])") {
Write-Log "Route not announced but IP Address still mounted (Watchdog)" -AdditionalFields @($route.RouteName) -Level Warning
Remove-IP $route
}
}
}
}
}
}
}
$timer_event | Remove-Event # Flush the event from the queue
} while ($message -ne "exit")
# Stopping timers
$timer.stop()
$watchdog.stop()
} else {
$OutputVersion=@{
'Version'=$scriptVersion
}
return $OutputVersion
}

733
src/WinBGP.ps1 Normal file
View File

@@ -0,0 +1,733 @@
###############################################################################
# #
# Name WinBGP-CLI #
# #
# Description WinBGP CLI to manage WinBGP engine #
# #
# Notes Pipe control is based on JFLarvoire service example #
# (https://github.com/JFLarvoire/SysToolsLib) #
# #
# #
# Copyright (c) 2024 Alexandre JARDON | Webalex System. #
# All rights reserved.' #
# LicenseUri https://github.com/webalexeu/winbgp/blob/master/LICENSE #
# ProjectUri https://github.com/webalexeu/winbgp #
# #
###############################################################################
#Requires -version 5.1
<#
.SYNOPSIS
WinBGP CLI local management.
.DESCRIPTION
This script manage locally WinBGP.
.PARAMETER Start
Start the service.
.PARAMETER Stop
Stop the service.
.PARAMETER Restart
Stop then restart the service.
.PARAMETER Status
Get the current service status: Not installed / Stopped / Running
.PARAMETER Control
Send a control message to the service thread.
.PARAMETER Version
Display this script version and exit.
.EXAMPLE
# Setup the service and run it for the first time
C:\PS>.\PSService.ps1 -Status
Not installed
C:\PS>.\PSService.ps1 -Setup
C:\PS># At this stage, a copy of PSService.ps1 is present in the path
C:\PS>PSService -Status
Stopped
C:\PS>PSService -Start
C:\PS>PSService -Status
Running
C:\PS># Load the log file in Notepad.exe for review
C:\PS>notepad ${ENV:windir}\Logs\PSService.log
.EXAMPLE
# Stop the service and uninstall it.
C:\PS>PSService -Stop
C:\PS>PSService -Status
Stopped
C:\PS>PSService -Remove
C:\PS># At this stage, no copy of PSService.ps1 is present in the path anymore
C:\PS>.\PSService.ps1 -Status
Not installed
.EXAMPLE
# Configure the service to run as a different user
C:\PS>$cred = Get-Credential -UserName LAB\Assistant
C:\PS>.\PSService -Setup -Credential $cred
.EXAMPLE
# Send a control message to the service, and verify that it received it.
C:\PS>PSService -Control Hello
C:\PS>Notepad C:\Windows\Logs\PSService.log
# The last lines should contain a trace of the reception of this Hello message
#>
[CmdletBinding(DefaultParameterSetName='BGPStatus')]
Param(
[Parameter(ParameterSetName='Start', Mandatory=$true)]
[Switch]$Start, # Start the service
[Parameter(ParameterSetName='Stop', Mandatory=$true)]
[Switch]$Stop, # Stop the service
[Parameter(ParameterSetName='Restart', Mandatory=$true)]
[Switch]$Restart, # Restart the service
[Parameter(ParameterSetName='Status', Mandatory=$false)]
[Switch]$Status = $($PSCmdlet.ParameterSetName -eq 'Status'), # Get the current service status
[Parameter(ParameterSetName='Control', Mandatory=$true)]
[String]$Control = $null, # Control message to send to the service
[Parameter(ParameterSetName='Reload', Mandatory=$false)]
[Switch]$Reload = $($PSCmdlet.ParameterSetName -eq 'reload'), # Reload configuration
[Parameter(ParameterSetName='RouteName', Mandatory=$true)]
[ArgumentCompleter( {
param ( $CommandName,
$ParameterName,
$WordToComplete,
$CommandAst,
$FakeBoundParameters )
# Dynamically generate routes array
# TO BE IMPROVED - Set to static temporary
$configuration=Get-Content 'C:\Program Files\WinBGP\winbgp.json' | ConvertFrom-Json
[Array] $routes = ($configuration.routes).RouteName
return $routes
})]
[String]$RouteName = $null, # Select route to control
[Parameter(ParameterSetName='RouteName', Mandatory=$false)]
[Switch]$StartMaintenance, # Control message to send to the service
[Parameter(ParameterSetName='RouteName', Mandatory=$false)]
[Switch]$StopMaintenance, # Control message to send to the service
[Parameter(ParameterSetName='RouteName', Mandatory=$false)]
[Switch]$StartRoute, # Control message to send to the service
[Parameter(ParameterSetName='RouteName', Mandatory=$false)]
[Switch]$StopRoute, # Control message to send to the service
[Parameter(ParameterSetName='BGPStatus', Mandatory=$false)]
[Switch]$BGPStatus = $($PSCmdlet.ParameterSetName -eq 'BGPStatus'), # Get the current service status
[Parameter(ParameterSetName='Config', Mandatory=$false)]
[Switch]$Config = $($PSCmdlet.ParameterSetName -eq 'Config'), # Get the current configuration
[Parameter(ParameterSetName='Logs', Mandatory=$false)]
[Switch]$Logs = $($PSCmdlet.ParameterSetName -eq 'Logs'), # Get the last logs
[Parameter(ParameterSetName='Logs', Mandatory=$false)]
[Int]$Last = 20, # Define the last logs number
[Parameter(ParameterSetName='RestartAPI', Mandatory=$false)]
[Switch]$RestartAPI, # RestartAPI
[Parameter(ParameterSetName='Version', Mandatory=$true)]
[Switch]$Version # Get this script version
)
# Don't forget to increment version when updating engine
$scriptVersion = '1.0.1'
# This script name, with various levels of details
# Ex: PSService
$scriptFullName = 'C:\Program Files\WinBGP\WinBGP.ps1' # Ex: C:\Temp\PSService.ps1
# Global settings
$serviceName = "WinBGP" # A one-word name used for net start commands
$serviceDisplayName = "WinBGP"
$pipeName = "Service_$serviceName" # Named pipe name. Used for sending messages to the service task
$installDir = "${ENV:ProgramW6432}\$serviceDisplayName" # Where to install the service files
$configfile = "$serviceDisplayName.json"
$configdir = "$installDir\$configfile"
$FunctionCliXml="$installDir\$serviceDisplayName.xml" # Used to stored Maintenance variable
$logName = "Application" # Event Log name (Unrelated to the logFile!)
# If the -Version switch is specified, display the script version and exit.
if ($Version) {
return $scriptVersion
}
#-----------------------------------------------------------------------------#
# #
# Function Now #
# #
# Description Get a string with the current time. #
# #
# Notes The output string is in the ISO 8601 format, except for #
# a space instead of a T between the date and time, to #
# improve the readability. #
# #
# History #
# 2015-06-11 JFL Created this routine. #
# #
#-----------------------------------------------------------------------------#
Function Now {
Param (
[Switch]$ms, # Append milliseconds
[Switch]$ns # Append nanoseconds
)
$Date = Get-Date
$now = ""
$now += "{0:0000}-{1:00}-{2:00} " -f $Date.Year, $Date.Month, $Date.Day
$now += "{0:00}:{1:00}:{2:00}" -f $Date.Hour, $Date.Minute, $Date.Second
$nsSuffix = ""
if ($ns) {
if ("$($Date.TimeOfDay)" -match "\.\d\d\d\d\d\d") {
$now += $matches[0]
$ms = $false
} else {
$ms = $true
$nsSuffix = "000"
}
}
if ($ms) {
$now += ".{0:000}$nsSuffix" -f $Date.MilliSecond
}
return $now
}
#-----------------------------------------------------------------------------#
# #
# Function Log #
# #
# Description Log a string into the PSService.log file #
# #
# Arguments A string #
# #
# Notes Prefixes the string with a timestamp and the user name. #
# (Except if the string is empty: Then output a blank line.)#
# #
# History #
# 2016-06-05 JFL Also prepend the Process ID. #
# 2016-06-08 JFL Allow outputing blank lines. #
# #
#-----------------------------------------------------------------------------#
#Logging function
function Write-Log {
<#
.Synopsis
Write-Log writes a message to a specified log file with the current time stamp.
.DESCRIPTION
The Write-Log function is designed to add logging capability to other scripts.
In addition to writing output and/or verbose you can write to a log file for
later debugging.
.NOTES
Created by: Jason Wasser @wasserja
Modified: 11/24/2015 09:30:19 AM
Changelog:
* Code simplification and clarification - thanks to @juneb_get_help
* Added documentation.
* Renamed LogPath parameter to Path to keep it standard - thanks to @JeffHicks
* Revised the Force switch to work as it should - thanks to @JeffHicks
To Do:
* Add error handling if trying to create a log file in a inaccessible location.
* Add ability to write $Message to $Verbose or $Error pipelines to eliminate
duplicates.
.PARAMETER Message
Message is the content that you wish to add to the log file.
.PARAMETER Level
Specify the criticality of the log information being written to the log (i.e. Error, Warning, Informational)
.PARAMETER NoClobber
Use NoClobber if you do not wish to overwrite an existing file.
.EXAMPLE
Write-Log -Message 'Log message'
Writes the message to c:\Logs\PowerShellLog.log.
.EXAMPLE
Write-Log -Message 'Restarting Server.' -Path c:\Logs\Scriptoutput.log
Writes the content to the specified log file and creates the path and file specified.
.EXAMPLE
Write-Log -Message 'Folder does not exist.' -Path c:\Logs\Script.log -Level Error
Writes the message to the specified log file as an error message, and writes the message to the error pipeline.
.LINK
https://gallery.technet.microsoft.com/scriptcenter/Write-Log-PowerShell-999c32d0
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true)]
[ValidateNotNullOrEmpty()]
[Alias("LogContent")]
[string]$Message,
[Parameter(Mandatory=$false)]
[ValidateSet("Error","Warning","Information")]
[string]$Level="Information",
[Parameter(Mandatory=$false)]
[string]$EventLogName=$logName,
[Parameter(Mandatory=$false)]
[string]$EventLogSource=$serviceName,
[Parameter(Mandatory=$false)]
[string]$EventLogId=1006,
[Parameter(Mandatory=$false)]
[string]$EventLogCategory=0,
[Parameter(Mandatory=$false)]
[Array]$AdditionalFields=$null,
[Parameter(Mandatory=$false)]
[switch]$NoClobber
)
Begin
{
}
Process
{
# Manage AdditionalFields (Not by default with PowerShell function)
if ($AdditionalFields) {
$EventInstance = [System.Diagnostics.EventInstance]::new($EventLogId, $EventLogCategory, $Level)
$NewEvent = [System.Diagnostics.EventLog]::new()
$NewEvent.Log = $EventLogName
$NewEvent.Source = $EventLogSource
[Array] $JoinedMessage = @(
$Message
$AdditionalFields | ForEach-Object { $_ }
)
$NewEvent.WriteEvent($EventInstance, $JoinedMessage)
} else {
#Write log to event viewer (Enabled by default)
Write-EventLog -LogName $EventLogName -Source $EventLogSource -EventId $EventLogId -EntryType $Level -Category $EventLogCategory -Message "$Message"
}
}
End
{
}
}
#-----------------------------------------------------------------------------#
# #
# Function Send-PipeMessage #
# #
# Description Send a message to a named pipe #
# #
# Arguments See the Param() block #
# #
# Notes #
# #
# History #
# 2016-05-25 JFL Created this function #
# #
#-----------------------------------------------------------------------------#
Function Send-PipeMessage () {
Param(
[Parameter(Mandatory=$true)]
[String]$PipeName, # Named pipe name
[Parameter(Mandatory=$true)]
[String]$Message # Message string
)
$PipeDir = [System.IO.Pipes.PipeDirection]::Out
$PipeOpt = [System.IO.Pipes.PipeOptions]::Asynchronous
$pipe = $null # Named pipe stream
$sw = $null # Stream Writer
try {
$pipe = new-object System.IO.Pipes.NamedPipeClientStream(".", $PipeName, $PipeDir, $PipeOpt)
$sw = new-object System.IO.StreamWriter($pipe)
$pipe.Connect(1000)
if (!$pipe.IsConnected) {
throw "Failed to connect client to pipe $pipeName"
}
$sw.AutoFlush = $true
$sw.WriteLine($Message)
} catch {
Write-Log "Error sending pipe $pipeName message: $_" -Level Error
} finally {
if ($sw) {
$sw.Dispose() # Release resources
$sw = $null # Force the PowerShell garbage collector to delete the .net object
}
if ($pipe) {
$pipe.Dispose() # Release resources
$pipe = $null # Force the PowerShell garbage collector to delete the .net object
}
}
}
#-----------------------------------------------------------------------------#
# #
# Function Test-ConfigurationFile #
# #
# Description Test WinBGP configuration file #
# #
# Arguments See the Param() block at the top of this script #
# #
# Notes #
# #
# History #
# #
#-----------------------------------------------------------------------------#
function Test-ConfigurationFile()
{
Param
(
[Parameter(Mandatory=$false)]
$Path=$configdir
)
# Json validation
try {
$configuration = Get-Content -Path $Path | ConvertFrom-Json
$validJson = $true
} catch {
$validJson = $false
}
if ($validJson) {
$ValidConfig=$true
# Global
if ($configuration.global.Interval -isnot [Int32]) {$ValidConfig=$false}
if ($configuration.global.Timeout -isnot [Int32]) {$ValidConfig=$false}
if ($configuration.global.Rise -isnot [Int32]) {$ValidConfig=$false}
if ($configuration.global.Fall -isnot [Int32]) {$ValidConfig=$false}
if ($configuration.global.Metric -isnot [Int32]) {$ValidConfig=$false}
if ($configuration.global.Api -isnot [Boolean]) {$ValidConfig=$false}
# Api (Check only if Api is enabled)
if ($configuration.global.Api) {
if ($configuration.api -isnot [array]) {$ValidConfig=$false}
}
# Router
if ([string]::IsNullOrEmpty($configuration.router.BgpIdentifier)) {$ValidConfig=$false}
if ([string]::IsNullOrEmpty($configuration.router.LocalASN)) {$ValidConfig=$false}
# Peers
if ($configuration.peers -is [array]) {
foreach ($peer in $configuration.peers) {
if ([string]::IsNullOrEmpty($peer.PeerName)) {$ValidConfig=$false}
if ([string]::IsNullOrEmpty($peer.LocalIP)) {$ValidConfig=$false}
if ([string]::IsNullOrEmpty($peer.PeerIP)) {$ValidConfig=$false}
if ([string]::IsNullOrEmpty($peer.LocalASN)) {$ValidConfig=$false}
if ([string]::IsNullOrEmpty($peer.PeerASN)) {$ValidConfig=$false}
}
} else {
$ValidConfig=$false
}
# Routes
if ($configuration.routes -is [array]) {
foreach ($route in $configuration.routes) {
if ([string]::IsNullOrEmpty($route.RouteName)) {$ValidConfig=$false}
if ([string]::IsNullOrEmpty($route.Network)) {$ValidConfig=$false}
if ([string]::IsNullOrEmpty($route.Interface)) {$ValidConfig=$false}
if ($route.DynamicIpSetup -isnot [Boolean]) {$ValidConfig=$false}
if ($route.WithdrawOnDown -isnot [Boolean]) {$ValidConfig=$false}
# Only if WithdrawOnDown is enabled
if ($route.WithdrawOnDown) {
if ([string]::IsNullOrEmpty($route.WithdrawOnDownCheck)) {$ValidConfig=$false}
}
if ([string]::IsNullOrEmpty($route.NextHop)) {$ValidConfig=$false}
# Community
if ($route.Community -is [array]) {
# Parsing all Community
foreach ($community in $route.Community) {
if ([string]::IsNullOrEmpty($community)) {$ValidConfig=$false}
}
} else {
$ValidConfig=$false
}
}
} else {
$ValidConfig=$false
}
}
# If Json type and content are valid
if (($validJson) -and ($ValidConfig)) {
return $true
} else {
return $false
}
}
#-----------------------------------------------------------------------------#
# #
# Function Main #
# #
# Description Execute the specified actions #
# #
# Arguments See the Param() block at the top of this script #
# #
# Notes #
# #
# History #
# #
#-----------------------------------------------------------------------------#
# Identify the user name. We use that for logging.
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$currentUserName = $identity.Name # Ex: "NT AUTHORITY\SYSTEM" or "Domain\Administrator"
# Workaround for PowerShell v2 bug: $PSCmdlet Not yet defined in Param() block
$Status = ($PSCmdlet.ParameterSetName -eq 'Status')
if ($Start) { # The user tells us to start the service
Write-Verbose "Starting service $serviceName"
Write-Log -Message "Starting service $serviceName"
Start-Service $serviceName # Ask Service Control Manager to start it
return
}
if ($Stop) { # The user tells us to stop the service
Write-Verbose "Stopping service $serviceName"
Write-Log -Message "Stopping service $serviceName"
Stop-Service $serviceName # Ask Service Control Manager to stop it
return
}
if ($Restart) { # Restart the service
& $scriptFullName -Stop
& $scriptFullName -Start
return
}
if ($Status) { # Get the current service status
$spid = $null
$processes = @(Get-WmiObject Win32_Process -filter "Name = 'powershell.exe'" | Where-Object {
$_.CommandLine -match ".*$scriptCopyCname.*-Service"
})
foreach ($process in $processes) { # There should be just one, but be prepared for surprises.
$spid = $process.ProcessId
Write-Verbose "$serviceName Process ID = $spid"
}
# if (Test-Path "HKLM:\SYSTEM\CurrentControlSet\services\$serviceName") {}
try {
$pss = Get-Service $serviceName -ea stop # Will error-out if not installed
} catch {
"Not Installed"
return
}
$pss.Status
if (($pss.Status -eq "Running") -and (!$spid)) { # This happened during the debugging phase
Write-Error "The Service Control Manager thinks $serviceName is started, but $serviceName.ps1 -Service is not running."
exit 1
}
return
}
if ($Control) { # Send a control message to the service
Send-PipeMessage $pipeName $control
}
#Reload control
if ($Reload) {
$control='reload'
Send-PipeMessage $pipeName $control
# If Json is valid, reloading
if (Test-ConfigurationFile) {
return 'Success'
} else {
return "Configuration file '$($configdir)' is not valid"
}
}
# Restart API
if ($RestartAPI) {
$control='restart api'
Send-PipeMessage $pipeName $control
# Output message to be improved
return 'Success'
}
# Start/stop control or Maintenance control
if ($StartRoute -or $StopRoute -or $StartMaintenance -or $StopMaintenance) {
# Logging
Write-Log "Operation for route '$RouteName' triggered by '$currentUserName'"
# Read configuration
$configuration = Get-Content -Path $configdir | ConvertFrom-Json
$routeCheck=$null
$routeCheck=$configuration.routes | Where-Object {$_.RouteName -eq $RouteName}
# Start/stop control
if ($StartRoute -or $StopRoute) {
# START
if ($StartRoute) {
$control="route $RouteName start"
}
# STOP
if ($StopRoute) {
$control="route $RouteName stop"
}
}
# Maintenance control
if ($StartMaintenance -or $StopMaintenance) {
# START
if ($StartMaintenance) {
$control="maintenance $RouteName start"
}
# STOP
if ($StopMaintenance) {
$control="maintenance $RouteName stop"
}
}
if($routeCheck) {
$PipeStatus=$null
# Performing Action
try {
Send-PipeMessage $pipeName $control
}
catch {
$PipeStatus=($_).ToString()
}
if ($PipeStatus -like "*Pipe hasn't been connected yet*") {
return "WinBGP not ready"
} else {
# TO BE IMPROVED to get status
return "Success"
}
} else {
# Logging
Write-Log "Received control message: $control"
Write-Log "Control return: Route '$RouteName' not found" -Level Warning
return "Route '$RouteName' not found"
}
}
# Get the current BGP status
if ($BGPStatus) {
# Read configuration
$configuration = Get-Content -Path $configdir | ConvertFrom-Json
# Read maintenance
#If there is a maintenance, import it
if(Test-Path -Path $FunctionCliXml) {
#Import variable
$maintenance=Import-CliXml -Path $FunctionCliXml
} else {
#Otherwise, initialize variable
$maintenance = @{}
}
# Read BGP routes and policy (To optimize query)
$BGPRoutes=$null
$BGPPolicies=$null
try {
# Use CIM query to improve performance
$BGPRoutes=(Invoke-CimMethod -ClassName "PS_BgpCustomRoute" -Namespace 'ROOT\Microsoft\Windows\RemoteAccess' -MethodName Get).cmdletoutput.Network
$BGPPolicies=(Invoke-CimMethod -ClassName "PS_BgpRoutingPolicy" -Namespace 'ROOT\Microsoft\Windows\RemoteAccess' -MethodName Get).cmdletoutput.PolicyName
}
catch {
}
# Read IP Addresses (To optimize query)
$IPAddresses=(Get-NetIPAddress -AddressFamily IPv4).IPAddress
#Parse all routes
$Routes=@()
ForEach ($route in $configuration.routes) {
$RouteStatus=$null
$RouteStatusDetailled=$null
# Check if route is in maintenance mode
if ($maintenance.($route.RouteName)) {
$RouteStatus='maintenance'
# Check if route is up (Only if BGP service is configured)
} else {
if ($BGPRoutes -contains "$($route.Network)") {
# Check route policy
if ($BGPPolicies -contains "$($route.RouteName)") {
# Check IP
if ($route.DynamicIpSetup) {
if ($IPAddresses -contains "$($route.Network.split('/')[0])") {
$RouteStatus='up'
} else {
$RouteStatus='warning'
$RouteStatusDetailled='IP Address not mounted'
}
} else {
$RouteStatus='up'
}
} else {
$RouteStatus='warning'
$RouteStatusDetailled='No routing policy defined'
}
# Route down
} else {
# Check IP
if ($route.DynamicIpSetup) {
if ($IPAddresses -contains "$($route.Network.split('/')[0])") {
$RouteStatus='warning'
$RouteStatusDetailled='IP Address still mounted'
} else {
$RouteStatus='down'
}
} else {
$RouteStatus='down'
}
}
}
$RouteProperties=[PSCustomObject]@{
Name = $route.RouteName;
Network = $route.Network;
Status = $RouteStatus;
MaintenanceTimestamp = $maintenance.($route.RouteName);
RouteStatusDetailled = $RouteStatusDetailled;
}
# Add route to array
$Routes += $RouteProperties
}
# Select default properties to display
$defaultDisplaySet = 'Name','Network','Status','MaintenanceTimestamp'
$defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet)
$PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)
$Routes | Add-Member MemberSet PSStandardMembers $PSStandardMembers
return $Routes
}
if ($Config) {
# If Json is valid, reloading
if (Test-ConfigurationFile) {
$configuration = Get-Content -Path $configdir | ConvertFrom-Json
return $configuration
} else {
return "Configuration file '$($configdir)' is not valid"
}
}
if ($Logs) {
$EventLogs=Get-EventLog -LogName Application -Source WinBGP -Newest $Last | Select-Object Index,TimeGenerated,EntryType,Message,ReplacementStrings
$DisplayLogs=@()
foreach ($log in $EventLogs) {
if($log.ReplacementStrings -gt 1) {
$log | Add-Member -MemberType NoteProperty -Name 'RouteName' -Value $log.ReplacementStrings[1]
}
$log.PsObject.Members.Remove('ReplacementStrings')
$DisplayLogs+=$log
}
# Select default properties to display
$defaultDisplaySet = 'TimeGenerated','EntryType','Message','RouteName'
$defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet)
$PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)
$DisplayLogs | Add-Member MemberSet PSStandardMembers $PSStandardMembers
return $DisplayLogs
}

61
src/winbgp.json.example Normal file
View File

@@ -0,0 +1,61 @@
{
"global": {
"Interval": 5,
"Timeout": 1,
"Rise": 3,
"Fall": 2,
"Metric": 100,
"Api": true
},
"api": [
{
"Uri": "http://127.0.0.1:8888",
"AuthenticationMethod": "Anonymous"
}
],
"router": {
"BgpIdentifier": "YOUR_IP",
"LocalASN": "YOUR_ASN"
},
"peers": [
{
"PeerName": "Peer1",
"LocalIP": "YOUR_IP",
"PeerIP": "Peer1_IP",
"LocalASN": "YOUR_ASN",
"PeerASN": "Peer1_ASN"
},
{
"PeerName": "Peer2",
"LocalIP": "Peer2_IP",
"PeerIP": "10.136.21.75",
"LocalASN": "YOUR_ASN",
"PeerASN": "Peer1_ASN"
}
],
"routes": [
{
"RouteName": "mywinbgpservice.contoso.com",
"Network": "mywinbgpservice_IP/32",
"Interface": "Ethernet",
"DynamicIpSetup": true,
"WithdrawOnDown": true,
"WithdrawOnDownCheck": "service: W32Time",
"NextHop": "YOUR_IP",
"Community": [
"BGP_COMMUNITY"
]
},
{
"RouteName": "mysecondwinbgpservice.contoso.com",
"Network": "mysecondwinbgpservice_IP/32",
"Interface": "Ethernet",
"DynamicIpSetup": false,
"WithdrawOnDown": false,
"NextHop": "YOUR_IP",
"Community": [
"BGP_COMMUNITY"
]
}
]
}