Initial public release
This commit is contained in:
571
src/WinBGP-API.ps1
Normal file
571
src/WinBGP-API.ps1
Normal 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
1744
src/WinBGP-Engine.ps1
Normal file
File diff suppressed because it is too large
Load Diff
522
src/WinBGP-HealthCheck.ps1
Normal file
522
src/WinBGP-HealthCheck.ps1
Normal 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
733
src/WinBGP.ps1
Normal 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
61
src/winbgp.json.example
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user