Initial public release
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Service builder executable outputs
|
||||
service/*.exe
|
||||
|
||||
# Installer outputs
|
||||
builder/*.msi
|
||||
builder/*.wixpdb
|
||||
21
LICENCE
Normal file
21
LICENCE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Webalex System
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
31
README
Normal file
31
README
Normal file
@@ -0,0 +1,31 @@
|
||||
# WinBGP
|
||||
|
||||
WinBGP Service
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Windows Server 2016, 2019, 2022, 2025
|
||||
|
||||
## Installation
|
||||
|
||||
Standard MSI installation.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Coder or not, you can contribute to the project! We welcome all contributions.
|
||||
|
||||
### For Users
|
||||
|
||||
If you don't code, you still sit on valuable information that can make this project even better. If you experience that the
|
||||
product does unexpected things, throw errors or is missing functionality, you can help by submitting bugs and feature requests.
|
||||
Please see the issues tab on this project and submit a new issue that matches your needs.
|
||||
|
||||
### For Developers
|
||||
|
||||
If you do code, we'd love to have your contributions. Please read the [Contribution guidelines](CONTRIBUTING.md) for more information.
|
||||
You can either help by picking up an existing issue or submit a new one if you have an idea for a new feature or improvement.
|
||||
|
||||
## Links
|
||||
|
||||
- [Semantic Versioning 2.0.0](https://semver.org/)
|
||||
28
builder/build.ps1
Normal file
28
builder/build.ps1
Normal file
@@ -0,0 +1,28 @@
|
||||
[CmdletBinding()]
|
||||
Param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[String] $Version,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateSet("amd64", "arm64")]
|
||||
[String] $Arch = "amd64"
|
||||
)
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# The MSI version is not semver compliant, so just take the numerical parts
|
||||
$MsiVersion = $Version -replace '^v?([0-9\.]+).*$','$1'
|
||||
|
||||
# Set working dir to this directory, reset previous on exit
|
||||
Push-Location $PSScriptRoot
|
||||
Trap {
|
||||
# Reset working dir on error
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
|
||||
Write-Verbose "Creating winbgp-${Version}-${Arch}.msi"
|
||||
$wixArch = @{"amd64" = "x64"; "arm64" = "arm64"}[$Arch]
|
||||
|
||||
Invoke-Expression "wix build -arch $wixArch -o .\WinBGP-$($Version)-$($Arch).msi .\files.wxs .\main.wxs -d ProductName=WinBGP -d Version=$($MsiVersion) -ext WixToolset.Firewall.wixext -ext WixToolset.UI.wixext -ext WixToolset.Util.wixext"
|
||||
|
||||
Write-Verbose "Done!"
|
||||
Pop-Location
|
||||
60
builder/files.wxs
Normal file
60
builder/files.wxs
Normal file
@@ -0,0 +1,60 @@
|
||||
<!--
|
||||
~ Copyright 2024 The Prometheus Authors
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<Wix xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util"
|
||||
xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
<Fragment>
|
||||
<DirectoryRef Id="APPLICATIONFOLDER">
|
||||
<Component Transitive="yes">
|
||||
<File Id="winbgp_service" Source="..\service\WinBGP-Service.exe" KeyPath="yes" Vital="yes" Checksum="yes" />
|
||||
<ServiceInstall
|
||||
Id="InstallExporterService"
|
||||
Name="WinBGP"
|
||||
DisplayName="WinBGP Engine"
|
||||
Description="The BGP swiss army knife of networking on Windows"
|
||||
ErrorControl="normal"
|
||||
Start="auto"
|
||||
Type="ownProcess"
|
||||
Vital="yes"
|
||||
>
|
||||
<util:ServiceConfig
|
||||
ResetPeriodInDays="1"
|
||||
FirstFailureActionType="restart"
|
||||
SecondFailureActionType="restart"
|
||||
ThirdFailureActionType="restart"
|
||||
RestartServiceDelayInSeconds="60"
|
||||
/>
|
||||
<ServiceDependency Id="RemoteAccess" />
|
||||
</ServiceInstall>
|
||||
<ServiceControl
|
||||
Id="MyServiceControl"
|
||||
Name="WinBGP"
|
||||
Remove="both"
|
||||
Stop="both"
|
||||
Wait="yes" />
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="ApplicationFiles">
|
||||
<ComponentRef Id="winbgp_service" />
|
||||
<ComponentRef Id="winbgp_engine" />
|
||||
<ComponentRef Id="winbgp_healthcheck" />
|
||||
<ComponentRef Id="winbgp_api" />
|
||||
<ComponentRef Id="winbgp_config" />
|
||||
<ComponentRef Id="winbgp_config_example" />
|
||||
<ComponentRef Id="winbgp_cli" />
|
||||
</ComponentGroup>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
217
builder/main.wxs
Normal file
217
builder/main.wxs
Normal file
@@ -0,0 +1,217 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<?ifndef Version?>
|
||||
<?error Version must be defined?>
|
||||
<?endif?>
|
||||
<?if $(sys.BUILDARCH) = x64 or $(sys.BUILDARCH) = arm64 ?>
|
||||
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
|
||||
<?else ?>
|
||||
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
|
||||
<?endif?>
|
||||
|
||||
<!--
|
||||
~ Copyright 2024 The Prometheus Authors
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<Wix xmlns:fw="http://wixtoolset.org/schemas/v4/wxs/firewall"
|
||||
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui"
|
||||
xmlns="http://wixtoolset.org/schemas/v4/wxs"
|
||||
>
|
||||
<Package UpgradeCode="0889d60f-67d5-4bf5-8918-2d5e810f888d" Name="$(var.ProductName)" Version="$(var.Version)"
|
||||
Manufacturer="Webalex System" Language="1033" Scope="perMachine">
|
||||
<SummaryInformation Manufacturer="Webalex System" Description="$(var.ProductName) $(var.Version) installer" />
|
||||
|
||||
<CustomAction Id="GenerateConfig"
|
||||
Execute="deferred"
|
||||
Impersonate="no"
|
||||
Directory="TARGETDIR"
|
||||
ExeCommand="powershell.exe -ExecutionPolicy Bypass -Command "
|
||||
Write-Host 'Generating initial configuration file (winbgp.json)'; 

|
||||
$config=Get-Content -Path '[APPLICATIONFOLDER]\winbgp.json' | ConvertFrom-Json; 

|
||||
$localIP=(Get-WmiObject -Class Win32_NetworkAdapterConfiguration | Where-Object {$_.IPEnabled -eq $true}).IPAddress[0]; 

|
||||
$config.router=@([PSCustomObject]@{BgpIdentifier=$localIP;LocalASN='64512'}); 

|
||||
$config.peers=@(); 

|
||||
$config.routes=@(); 

|
||||
$config=$config | ConvertTo-Json; 

|
||||
Set-Content -Value $config -Path '[APPLICATIONFOLDER]\winbgp.json' 

|
||||
""
|
||||
Return="ignore"
|
||||
/>
|
||||
|
||||
<CustomAction Id="EnableWindowsFeature"
|
||||
Execute="deferred"
|
||||
Impersonate="no"
|
||||
Directory="TARGETDIR"
|
||||
ExeCommand="powershell.exe -ExecutionPolicy Bypass -Command "if ((Get-WindowsFeature -Name 'Routing').InstallState -ne 'Installed') { Write-Host 'Adding routing feature (WinBGP prerequisite)'; Add-WindowsFeature -Name 'Routing' -IncludeAllSubFeature }""
|
||||
Return="check"
|
||||
/>
|
||||
|
||||
<CustomAction Id="EnableWindowsFeatureRSAT"
|
||||
Execute="deferred"
|
||||
Impersonate="no"
|
||||
Directory="TARGETDIR"
|
||||
ExeCommand="powershell.exe -ExecutionPolicy Bypass -Command "if ((Get-WindowsFeature -Name 'RSAT-RemoteAccess-PowerShell').InstallState -ne 'Installed') { Write-Host 'Adding RSAT routing feature (WinBGP prerequisite)'; Add-WindowsFeature -Name 'RSAT-RemoteAccess-PowerShell' }""
|
||||
Return="check"
|
||||
/>
|
||||
|
||||
<!-- ExeCommand="dism.exe /online /enable-feature /featurename:RasRoutingProtocols /featurename:RemoteAccessPowerShell /all" -->
|
||||
|
||||
<CustomAction Id="EnableRouting"
|
||||
Execute="deferred"
|
||||
Impersonate="no"
|
||||
Directory="TARGETDIR"
|
||||
ExeCommand="powershell.exe -ExecutionPolicy Bypass -Command "if ((Get-RemoteAccess).RoutingStatus -ne 'Installed') { Write-Host 'Enabling routing (WinBGP prerequisite)'; Install-RemoteAccess -VpnType RoutingOnly }""
|
||||
Return="check"
|
||||
/>
|
||||
|
||||
<CustomAction Id="RemoveConfig"
|
||||
Execute="deferred"
|
||||
Impersonate="no"
|
||||
Directory="TARGETDIR"
|
||||
ExeCommand="powershell.exe -ExecutionPolicy Bypass -Command "
|
||||
Write-Host 'Removing configuration file (winbgp.json)'; 

|
||||
Remove-Item -Path '[APPLICATIONFOLDER]\winbgp.json' 

|
||||
""
|
||||
Return="ignore"
|
||||
/>
|
||||
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action="GenerateConfig" After="InstallFiles" Condition="NOT Installed AND NOT WIX_UPGRADE_DETECTED AND NOT WIX_DOWNGRADE_DETECTED" />
|
||||
<Custom Action="EnableWindowsFeature" After="InstallFiles" Condition="&Features=3 AND NOT Installed" />
|
||||
<Custom Action="EnableWindowsFeatureRSAT" After="EnableWindowsFeature" Condition="&Features=3 AND NOT Installed" />
|
||||
<Custom Action="EnableRouting" After="EnableWindowsFeatureRSAT" Condition="&Features=3 AND NOT Installed" /> <!-- TO CHECK to use Condition="MAINTENANCE" for upgrade -->
|
||||
<ScheduleReboot After="InstallFinalize" Condition="&Features=3 AND NOT Installed" /> <!-- TO CHECK <Condition>REBOOT_REQUIRED</Condition> -->
|
||||
<Custom Action='RemoveConfig' Before='RemoveFiles' Condition='Installed AND (REMOVE="ALL") AND NOT (WIX_UPGRADE_DETECTED OR UPGRADINGPRODUCTCODE)' />
|
||||
</InstallExecuteSequence>
|
||||
|
||||
<Media Id="1" Cabinet="winbgp.cab" EmbedCab="yes" />
|
||||
<MajorUpgrade Schedule="afterInstallInitialize" DowngradeErrorMessage="A later version of [ProductName] is already installed. Setup will now exit." AllowSameVersionUpgrades="yes" />
|
||||
|
||||
|
||||
<Property Id="ARPHELPLINK" Value="https://github.com/webalexeu/winbgp/issues" />
|
||||
<Property Id="ARPSIZE" Value="9000" />
|
||||
<Property Id="ARPURLINFOABOUT" Value="https://github.com/pwebalexeu/winbgp" />
|
||||
<!--<Property Id="ARPNOMODIFY" Value="0" />-->
|
||||
<!--<Property Id="ARPNOREPAIR" Value="1" />-->
|
||||
<Property Id="START_MENU_FOLDER" Value="0" />
|
||||
<Property Id="NOSTART" Value="0" />
|
||||
|
||||
<Feature
|
||||
Id="DefaultFeature"
|
||||
Level="1"
|
||||
Title="$(var.ProductName) $(var.Version)"
|
||||
Description="The binary and configuration files for $(var.ProductName)"
|
||||
Display="expand"
|
||||
ConfigurableDirectory="APPLICATIONFOLDER"
|
||||
AllowAdvertise="no"
|
||||
InstallDefault="local"
|
||||
AllowAbsent="no"
|
||||
>
|
||||
|
||||
<ComponentGroupRef Id="ApplicationFiles" />
|
||||
<ComponentGroupRef Id="CG_FirewallException" />
|
||||
<ComponentGroupRef Id="PATH" />
|
||||
<!-- Subfeature to install required windows features -->
|
||||
<Feature
|
||||
Id="Features"
|
||||
Level="1"
|
||||
Title="Routing features"
|
||||
Description="Enable required windows features for routing capabilities"
|
||||
Display="expand"
|
||||
AllowAdvertise="no"
|
||||
InstallDefault="local"
|
||||
AllowAbsent="yes">
|
||||
</Feature>
|
||||
</Feature>
|
||||
|
||||
<UI Id="FeatureTree">
|
||||
<ui:WixUI Id="WixUI_FeatureTree" />
|
||||
<UIRef Id="WixUI_ErrorProgressText" />
|
||||
|
||||
|
||||
<!-- skip the license agreement dialog; higher Order takes priority (weird) -->
|
||||
|
||||
<Publish
|
||||
Condition="NOT Installed"
|
||||
Dialog="WelcomeDlg"
|
||||
Control="Next"
|
||||
Event="NewDialog"
|
||||
Value="CustomizeDlg"
|
||||
Order="10"/>
|
||||
<Publish
|
||||
Condition="NOT Installed"
|
||||
Dialog="CustomizeDlg"
|
||||
Control="Back"
|
||||
Event="NewDialog"
|
||||
Value="WelcomeDlg"
|
||||
Order="10"/>
|
||||
|
||||
|
||||
<!--CustomPropertyDlg-->
|
||||
</UI>
|
||||
|
||||
<!-- InstallLocation key -->
|
||||
<CustomAction Id="SetInstallLocation" Property="ARPINSTALLLOCATION" Value="[APPLICATIONFOLDER]" />
|
||||
|
||||
<StandardDirectory Id="ProgramFiles64Folder">
|
||||
<Directory Id="APPLICATIONFOLDER" Name="WinBGP">
|
||||
<Component Id="winbgp_engine" Guid="a7b5748a-16c9-4594-85ad-de032fb3f39c">
|
||||
<File Id="winbgp_engine" Source="..\src\WinBGP-Engine.ps1" KeyPath="yes" Vital="yes" Checksum="yes" />
|
||||
</Component>
|
||||
<Component Id="winbgp_healthcheck" Guid="8a356315-8307-4508-b19d-00eb05e59428">
|
||||
<File Id="winbgp_healthcheck" Source="..\src\WinBGP-HealthCheck.ps1" KeyPath="yes" Vital="yes" Checksum="yes" />
|
||||
</Component>
|
||||
<Component Id="winbgp_api" Guid="77de0ab1-0e53-4967-9469-142f49b397a4">
|
||||
<File Id="winbgp_api" Source="..\src\WinBGP-API.ps1" KeyPath="yes" Vital="yes" Checksum="yes" />
|
||||
</Component>
|
||||
<Component Id="winbgp_config" Guid="03063c39-1063-4126-b974-5cad888d5804" NeverOverwrite="yes" Permanent="yes" >
|
||||
<File Id="winbgp_config" Name="winbgp.json" Source="..\src\winbgp.json.example" KeyPath="yes" Checksum="yes" />
|
||||
</Component>
|
||||
<Component Id="winbgp_config_example" Guid="b3ee9ef1-0d91-43a4-a5a4-a9074a579fb3" >
|
||||
<File Id="winbgp_config_example" Source="..\src\winbgp.json.example" KeyPath="yes" Checksum="yes" />
|
||||
</Component>
|
||||
<Component Id="winbgp_cli" Guid="b74d4be4-3de4-4006-a271-b56d078d2bcc">
|
||||
<File Id="winbgp_cli" Source="..\src\WinBGP.ps1" KeyPath="yes" Vital="yes" Checksum="yes" />
|
||||
</Component>
|
||||
</Directory>
|
||||
</StandardDirectory>
|
||||
<ComponentGroup Id="CG_FirewallException">
|
||||
<Component Directory="APPLICATIONFOLDER" Id="C_FirewallException" Guid="9f522655-ac0e-42d2-a512-a7b19ebec7f7">
|
||||
<fw:FirewallException
|
||||
Id="APIEndpoint"
|
||||
Name="$(var.ProductName) - API (TCP-In)"
|
||||
Description="Inbound rule for $(var.ProductName) API HTTP endpoint. [TCP]"
|
||||
Program="[#winbgp.exe]"
|
||||
Port="8888"
|
||||
Protocol="tcp">
|
||||
</fw:FirewallException>
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
|
||||
<ComponentGroup Id="PATH">
|
||||
<Component Directory="APPLICATIONFOLDER" Id="AddToPath" Guid="bd53afb3-eeb6-4eb8-979e-42a6fee3b84b" KeyPath="yes">
|
||||
<!-- Modify system PATH variable -->
|
||||
<Environment Id="AddToSystemPathEnv"
|
||||
Action="set"
|
||||
Name="Path"
|
||||
Value="[APPLICATIONFOLDER]"
|
||||
Permanent="no"
|
||||
Part="last"
|
||||
System="yes"
|
||||
/>
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
|
||||
</Package>
|
||||
</Wix>
|
||||
949
service/WinBGP-Service.ps1
Normal file
949
service/WinBGP-Service.ps1
Normal file
@@ -0,0 +1,949 @@
|
||||
###############################################################################
|
||||
# #
|
||||
# File name PSService.ps1 #
|
||||
# #
|
||||
# Description A sample service in a standalone PowerShell script #
|
||||
# #
|
||||
# Notes The latest PSService.ps1 version is available in GitHub #
|
||||
# repository https://github.com/JFLarvoire/SysToolsLib/ , #
|
||||
# in the PowerShell subdirectory. #
|
||||
# Please report any problem in the Issues tab in that #
|
||||
# GitHub repository in #
|
||||
# https://github.com/JFLarvoire/SysToolsLib/issues #
|
||||
# If you do submit a pull request, please add a comment at #
|
||||
# the end of this header with the date, your initials, and #
|
||||
# a description of the changes. Also update $scriptVersion. #
|
||||
# #
|
||||
# The initial version of this script was described in an #
|
||||
# article published in the May 2016 issue of MSDN Magazine. #
|
||||
# https://msdn.microsoft.com/en-us/magazine/mt703436.aspx #
|
||||
# This updated version has one major change: #
|
||||
# The -Service handler in the end has been rewritten to be #
|
||||
# event-driven, with a second thread waiting for control #
|
||||
# messages coming in via a named pipe. #
|
||||
# This allows fixing a bug of the original version, that #
|
||||
# did not stop properly, and left a zombie process behind. #
|
||||
# The drawback is that the new code is significantly longer,#
|
||||
# due to the added PowerShell thread management routines. #
|
||||
# On the other hand, these thread management routines are #
|
||||
# reusable, and will allow building much more powerful #
|
||||
# services. #
|
||||
# #
|
||||
# Dynamically generates a small PSService.exe wrapper #
|
||||
# application, that in turn invokes this PowerShell script. #
|
||||
# #
|
||||
# Some arguments are inspired by Linux' service management #
|
||||
# arguments: -Start, -Stop, -Restart, -Status #
|
||||
# Others are more in the Windows' style: -Setup, -Remove #
|
||||
# #
|
||||
# The actual start and stop operations are done when #
|
||||
# running as SYSTEM, under the control of the SCM (Service #
|
||||
# Control Manager). #
|
||||
# #
|
||||
# To create your own service, make a copy of this file and #
|
||||
# rename it. The file base name becomes the service name. #
|
||||
# Then implement your own service code in the if ($Service) #
|
||||
# {block} at the very end of this file. See the TO DO #
|
||||
# comment there. #
|
||||
# There are global settings below the script param() block. #
|
||||
# They can easily be changed, but the defaults should be #
|
||||
# suitable for most projects. #
|
||||
# #
|
||||
# Service installation and usage: See the dynamic help #
|
||||
# section below, or run: help .\PSService.ps1 -Detailed #
|
||||
# #
|
||||
# Debugging: The Log function writes messages into a file #
|
||||
# called C:\Windows\Logs\PSService.log (or actually #
|
||||
# ${env:windir}\Logs\$serviceName.log). #
|
||||
# It is very convenient to monitor what's written into that #
|
||||
# file with a WIN32 port of the Unix tail program. Usage: #
|
||||
# tail -f C:\Windows\Logs\PSService.log #
|
||||
# #
|
||||
# History #
|
||||
# 2015-07-10 JFL jf.larvoire@hpe.com created this script. #
|
||||
# 2015-10-13 JFL Made this script completely generic, and added comments #
|
||||
# in the header above. #
|
||||
# 2016-01-02 JFL Moved the Event Log name into new variable $logName. #
|
||||
# Improved comments. #
|
||||
# 2016-01-05 JFL Fixed the StartPending state reporting. #
|
||||
# 2016-03-17 JFL Removed aliases. Added missing explicit argument names. #
|
||||
# 2016-04-16 JFL Moved the official repository on GitHub. #
|
||||
# 2016-04-21 JFL Minor bug fix: New-EventLog did not use variable $logName.#
|
||||
# 2016-05-25 JFL Bug fix: The service task was not properly stopped; Its #
|
||||
# finally block was not executed, and a zombie task often #
|
||||
# remained. Fixed by using a named pipe to send messages #
|
||||
# to the service task. #
|
||||
# 2016-06-05 JFL Finalized the event-driven service handler. #
|
||||
# Fixed the default command setting in PowerShell v2. #
|
||||
# Added a sample -Control option using the new pipe. #
|
||||
# 2016-06-08 JFL Rewrote the pipe handler using PSThreads instead of Jobs. #
|
||||
# 2016-06-09 JFL Finalized the PSThread management routines error handling.#
|
||||
# This finally fixes issue #1. #
|
||||
# 2016-08-22 JFL Fixed issue #3 creating the log and install directories. #
|
||||
# Thanks Nischl. #
|
||||
# 2016-09-06 JFL Fixed issue #4 detecting the System account. Now done in #
|
||||
# a language-independent way. Thanks A Gonzalez. #
|
||||
# 2016-09-19 JFL Fixed issue #5 starting services that begin with a number.#
|
||||
# Added a $ServiceDescription string global setting, and #
|
||||
# use it for the service registration. #
|
||||
# Added comments about Windows event logs limitations. #
|
||||
# 2016-11-17 RBM Fixed issue #6 Mangled hyphen in final Unregister-Event. #
|
||||
# 2017-05-10 CJG Added execution policy bypass flag. #
|
||||
# 2017-10-04 RBL rblindberg Updated C# code OnStop() routine fixing #
|
||||
# orphaned process left after stoping the service. #
|
||||
# 2017-12-05 NWK omrsafetyo Added ServiceUser and ServicePassword to the #
|
||||
# script parameters. #
|
||||
# 2017-12-10 JFL Removed the unreliable service account detection tests, #
|
||||
# and instead use dedicated -SCMStart and -SCMStop #
|
||||
# arguments in the PSService.exe helper app. #
|
||||
# Renamed variable userName as currentUserName. #
|
||||
# Renamed arguments ServiceUser and ServicePassword to the #
|
||||
# more standard UserName and Password. #
|
||||
# Also added the standard argument -Credential. #
|
||||
# #
|
||||
###############################################################################
|
||||
#Requires -version 5.1
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
A sample Windows service, in a standalone PowerShell script.
|
||||
|
||||
.DESCRIPTION
|
||||
This script demonstrates how to write a Windows service in pure PowerShell.
|
||||
It dynamically generates a small PSService.exe wrapper, that in turn
|
||||
invokes this PowerShell script again for its start and stop events.
|
||||
|
||||
.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 Setup
|
||||
Install the service.
|
||||
Optionally use the -Credential or -UserName arguments to specify the user
|
||||
account for running the service. By default, uses the LocalSystem account.
|
||||
Known limitation with the old PowerShell v2: It is necessary to use -Credential
|
||||
or -UserName. For example, use -UserName LocalSystem to emulate the v3+ default.
|
||||
|
||||
.PARAMETER Credential
|
||||
User and password credential to use for running the service.
|
||||
For use with the -Setup command.
|
||||
Generate a PSCredential variable with the Get-Credential command.
|
||||
|
||||
.PARAMETER UserName
|
||||
User account to use for running the service.
|
||||
For use with the -Setup command, in the absence of a Credential variable.
|
||||
The user must have the "Log on as a service" right. To give him that right,
|
||||
open the Local Security Policy management console, go to the
|
||||
"\Security Settings\Local Policies\User Rights Assignments" folder, and edit
|
||||
the "Log on as a service" policy there.
|
||||
Services should always run using a user account which has the least amount
|
||||
of privileges necessary to do its job.
|
||||
Three accounts are special, and do not require a password:
|
||||
* LocalSystem - The default if no user is specified. Highly privileged.
|
||||
* LocalService - Very few privileges, lowest security risk.
|
||||
Apparently not enough privileges for running PowerShell. Do not use.
|
||||
* NetworkService - Idem, plus network access. Same problems as LocalService.
|
||||
|
||||
.PARAMETER Password
|
||||
Password for UserName. If not specified, you will be prompted for it.
|
||||
It is strongly recommended NOT to use that argument, as that password is
|
||||
visible on the console, and in the task manager list.
|
||||
Instead, use the -UserName argument alone, and wait for the prompt;
|
||||
or, even better, use the -Credential argument.
|
||||
|
||||
.PARAMETER Remove
|
||||
Uninstall the service.
|
||||
|
||||
.PARAMETER Service
|
||||
Run the service in the background. Used internally by the script.
|
||||
Do not use, except for test purposes.
|
||||
|
||||
.PARAMETER SCMStart
|
||||
Process Service Control Manager start requests. Used internally by the script.
|
||||
Do not use, except for test purposes.
|
||||
|
||||
.PARAMETER SCMResume
|
||||
Process Service Control Manager resume requests. Used internally by the script.
|
||||
Do not use, except for test purposes.
|
||||
|
||||
.PARAMETER SCMStop
|
||||
Process Service Control Manager stop requests. Used internally by the script.
|
||||
Do not use, except for test purposes.
|
||||
|
||||
.PARAMETER SCMSuspend
|
||||
Process Service Control Manager suspend requests. Used internally by the script.
|
||||
Do not use, except for test purposes.
|
||||
|
||||
.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='Version')]
|
||||
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='Setup', Mandatory=$true)]
|
||||
[Parameter(ParameterSetName='Setup2', Mandatory=$true)]
|
||||
[Switch]$Setup, # Install the service
|
||||
|
||||
[Parameter(ParameterSetName='Setup', Mandatory=$true)]
|
||||
[String]$UserName, # Set the service to run as this user
|
||||
|
||||
[Parameter(ParameterSetName='Setup', Mandatory=$false)]
|
||||
[String]$Password, # Use this password for the user
|
||||
|
||||
[Parameter(ParameterSetName='Setup2', Mandatory=$false)]
|
||||
[System.Management.Automation.PSCredential]$Credential, # Service account credential
|
||||
|
||||
[Parameter(ParameterSetName='Remove', Mandatory=$true)]
|
||||
[Switch]$Remove, # Uninstall the service
|
||||
|
||||
[Parameter(ParameterSetName='Build', Mandatory=$true)]
|
||||
[Switch]$Build, # Run the service (Internal use only)
|
||||
|
||||
[Parameter(ParameterSetName='Version', Mandatory=$true)]
|
||||
[Switch]$Version # Get this script version
|
||||
)
|
||||
|
||||
# Don't forget to increment version when updating service
|
||||
$serviceVersion = '1.1.1.1'
|
||||
|
||||
# This script name, with various levels of details
|
||||
$argv0 = Get-Item $MyInvocation.MyCommand.Definition
|
||||
$script = $argv0.basename # Ex: PSService
|
||||
$scriptName = $argv0.name # Ex: PSService.ps1
|
||||
$scriptFullName = $argv0.fullname # Ex: C:\Temp\PSService.ps1
|
||||
|
||||
# Global settings
|
||||
$serviceName = "WinBGP" # A one-word name used for net start commands
|
||||
$serviceDisplayName = "WinBGP Engine"
|
||||
$ServiceDescription = "The BGP swiss army knife of networking on Windows"
|
||||
$installDir = "$($ENV:ProgramW6432)\WinBGP" # Where to install the service files
|
||||
$scriptCopy = "$installDir\$scriptName"
|
||||
$exeName = "$serviceName-Service.exe"
|
||||
$exeFullName = "$installDir\$exeName"
|
||||
# Remove file log
|
||||
#$logDir = "${ENV:programfiles}\WinBGP\Logs" # Where to log the service messages
|
||||
#$logFile = "$logDir\$serviceName.log"
|
||||
$logName = "Application" # Event Log name (Unrelated to the logFile!)
|
||||
# Note: The current implementation only supports "classic" (ie. XP-compatble) event logs.
|
||||
# To support new style (Vista and later) "Applications and Services Logs" folder trees, it would
|
||||
# be necessary to use the new *WinEvent commands instead of the XP-compatible *EventLog commands.
|
||||
# Gotcha: If you change $logName to "NEWLOGNAME", make sure that the registry key below does not exist:
|
||||
# HKLM\System\CurrentControlSet\services\eventlog\Application\NEWLOGNAME
|
||||
# Else, New-EventLog will fail, saying the log NEWLOGNAME is already registered as a source,
|
||||
# even though "Get-WinEvent -ListLog NEWLOGNAME" says this log does not exist!
|
||||
|
||||
# If the -Version switch is specified, display the script version and exit.
|
||||
if ($Version) {
|
||||
return $serviceVersion
|
||||
}
|
||||
|
||||
#-----------------------------------------------------------------------------#
|
||||
# #
|
||||
# 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 $source #
|
||||
# #
|
||||
# Description C# source of the PSService.exe stub #
|
||||
# #
|
||||
# Arguments #
|
||||
# #
|
||||
# Notes The lines commented with "SET STATUS" and "EVENT LOG" are #
|
||||
# optional. (Or blocks between "// SET STATUS [" and #
|
||||
# "// SET STATUS ]" comments.) #
|
||||
# SET STATUS lines are useful only for services with a long #
|
||||
# startup time. #
|
||||
# EVENT LOG lines are useful for debugging the service. #
|
||||
# #
|
||||
# History #
|
||||
# 2017-10-04 RBL Updated the OnStop() procedure adding the sections #
|
||||
# try{ #
|
||||
# }catch{ #
|
||||
# }finally{ #
|
||||
# } #
|
||||
# This resolved the issue where stopping the service would #
|
||||
# leave the PowerShell process -Service still running. This #
|
||||
# unclosed process was an orphaned process that would #
|
||||
# remain until the pid was manually killed or the computer #
|
||||
# was rebooted #
|
||||
# #
|
||||
#-----------------------------------------------------------------------------#
|
||||
|
||||
# Overwrite for builder
|
||||
$scriptCopy= "$installDir\WinBGP-Engine.ps1"
|
||||
|
||||
$scriptCopyCname = $scriptCopy -replace "\\", "\\" # Double backslashes. (The first \\ is a regexp with \ escaped; The second is a plain string.)
|
||||
$source = @"
|
||||
using System;
|
||||
using System.ServiceProcess;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices; // SET STATUS
|
||||
using System.ComponentModel; // SET STATUS
|
||||
using System.Reflection; // SET STATUS
|
||||
|
||||
[assembly: AssemblyVersion("$serviceVersion")] // SET VERSION
|
||||
|
||||
public enum ServiceType : int { // SET STATUS [
|
||||
SERVICE_WIN32_OWN_PROCESS = 0x00000010,
|
||||
SERVICE_WIN32_SHARE_PROCESS = 0x00000020,
|
||||
}; // SET STATUS ]
|
||||
|
||||
public enum ServiceState : int { // SET STATUS [
|
||||
SERVICE_STOPPED = 0x00000001,
|
||||
SERVICE_START_PENDING = 0x00000002,
|
||||
SERVICE_STOP_PENDING = 0x00000003,
|
||||
SERVICE_RUNNING = 0x00000004,
|
||||
SERVICE_CONTINUE_PENDING = 0x00000005,
|
||||
SERVICE_PAUSE_PENDING = 0x00000006,
|
||||
SERVICE_PAUSED = 0x00000007,
|
||||
}; // SET STATUS ]
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)] // SET STATUS [
|
||||
public struct ServiceStatus {
|
||||
public ServiceType dwServiceType;
|
||||
public ServiceState dwCurrentState;
|
||||
public int dwControlsAccepted;
|
||||
public int dwWin32ExitCode;
|
||||
public int dwServiceSpecificExitCode;
|
||||
public int dwCheckPoint;
|
||||
public int dwWaitHint;
|
||||
}; // SET STATUS ]
|
||||
|
||||
public enum Win32Error : int { // WIN32 errors that we may need to use
|
||||
NO_ERROR = 0,
|
||||
ERROR_APP_INIT_FAILURE = 575,
|
||||
ERROR_FATAL_APP_EXIT = 713,
|
||||
ERROR_SERVICE_NOT_ACTIVE = 1062,
|
||||
ERROR_EXCEPTION_IN_SERVICE = 1064,
|
||||
ERROR_SERVICE_SPECIFIC_ERROR = 1066,
|
||||
ERROR_PROCESS_ABORTED = 1067,
|
||||
};
|
||||
|
||||
public class Service_$serviceName : ServiceBase { // $serviceName may begin with a digit; The class name must begin with a letter
|
||||
private System.Diagnostics.EventLog eventLog; // EVENT LOG
|
||||
private ServiceStatus serviceStatus; // SET STATUS
|
||||
|
||||
public const int SERVICE_ACCEPT_PRESHUTDOWN = 0x100; // Preshutdown
|
||||
public const int SERVICE_CONTROL_PRESHUTDOWN = 0xf; // Preshutdown
|
||||
|
||||
public Service_$serviceName() {
|
||||
ServiceName = "$serviceName";
|
||||
CanStop = true;
|
||||
CanShutdown = true;
|
||||
CanPauseAndContinue = true;
|
||||
AutoLog = true;
|
||||
|
||||
// PreShutdown Section
|
||||
FieldInfo acceptedCommandsFieldInfo = typeof(ServiceBase).GetField("acceptedCommands", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (acceptedCommandsFieldInfo == null)
|
||||
{
|
||||
throw new ApplicationException("acceptedCommands field not found");
|
||||
}
|
||||
int value = (int)acceptedCommandsFieldInfo.GetValue(this);
|
||||
acceptedCommandsFieldInfo.SetValue(this, value | SERVICE_ACCEPT_PRESHUTDOWN);
|
||||
// End PreShutdown Section
|
||||
|
||||
eventLog = new System.Diagnostics.EventLog(); // EVENT LOG [
|
||||
if (!System.Diagnostics.EventLog.SourceExists(ServiceName)) {
|
||||
System.Diagnostics.EventLog.CreateEventSource(ServiceName, "$logName");
|
||||
}
|
||||
eventLog.Source = ServiceName;
|
||||
eventLog.Log = "$logName"; // EVENT LOG ]
|
||||
EventLog.WriteEntry(ServiceName, "$exeName $serviceName()"); // EVENT LOG
|
||||
}
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError=true)] // SET STATUS
|
||||
private static extern bool SetServiceStatus(IntPtr handle, ref ServiceStatus serviceStatus);
|
||||
|
||||
protected override void OnStart(string [] args) {
|
||||
EventLog.WriteEntry(ServiceName, "$exeName OnStart() // Entry. Starting script '$scriptCopyCname' -SCMStart"); // EVENT LOG
|
||||
// Set the service state to Start Pending. // SET STATUS [
|
||||
// Only useful if the startup time is long. Not really necessary here for a 2s startup time.
|
||||
serviceStatus.dwServiceType = ServiceType.SERVICE_WIN32_OWN_PROCESS;
|
||||
serviceStatus.dwCurrentState = ServiceState.SERVICE_START_PENDING;
|
||||
serviceStatus.dwWin32ExitCode = 0;
|
||||
serviceStatus.dwWaitHint = 2000; // It takes about 2 seconds to start PowerShell
|
||||
SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS ]
|
||||
// Start a child process with another copy of this script
|
||||
try {
|
||||
Process p = new Process();
|
||||
// Redirect the output stream of the child process.
|
||||
p.StartInfo.UseShellExecute = false;
|
||||
p.StartInfo.RedirectStandardOutput = true;
|
||||
p.StartInfo.FileName = "PowerShell.exe";
|
||||
p.StartInfo.Arguments = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMStart"; // Works if path has spaces, but not if it contains ' quotes.
|
||||
p.Start();
|
||||
// Read the output stream first and then wait. (To avoid deadlocks says Microsoft!)
|
||||
string output = p.StandardOutput.ReadToEnd();
|
||||
// Wait for the completion of the script startup code, that launches the -Service instance
|
||||
p.WaitForExit();
|
||||
if (p.ExitCode != 0) throw new Win32Exception((int)(Win32Error.ERROR_APP_INIT_FAILURE));
|
||||
// Success. Set the service state to Running. // SET STATUS
|
||||
serviceStatus.dwCurrentState = ServiceState.SERVICE_RUNNING; // SET STATUS
|
||||
} catch (Exception e) {
|
||||
EventLog.WriteEntry(ServiceName, "$exeName OnStart() // Failed to start $scriptCopyCname. " + e.Message, EventLogEntryType.Error); // EVENT LOG
|
||||
// Change the service state back to Stopped. // SET STATUS [
|
||||
serviceStatus.dwCurrentState = ServiceState.SERVICE_STOPPED;
|
||||
Win32Exception w32ex = e as Win32Exception; // Try getting the WIN32 error code
|
||||
if (w32ex == null) { // Not a Win32 exception, but maybe the inner one is...
|
||||
w32ex = e.InnerException as Win32Exception;
|
||||
}
|
||||
if (w32ex != null) { // Report the actual WIN32 error
|
||||
serviceStatus.dwWin32ExitCode = w32ex.NativeErrorCode;
|
||||
} else { // Make up a reasonable reason
|
||||
serviceStatus.dwWin32ExitCode = (int)(Win32Error.ERROR_APP_INIT_FAILURE);
|
||||
} // SET STATUS ]
|
||||
} finally {
|
||||
serviceStatus.dwWaitHint = 0; // SET STATUS
|
||||
SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS
|
||||
EventLog.WriteEntry(ServiceName, "$exeName OnStart() // Exit"); // EVENT LOG
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnContinue() {
|
||||
EventLog.WriteEntry(ServiceName, "$exeName OnContinue() // Entry. Starting script '$scriptCopyCname' -SCMResume"); // EVENT LOG
|
||||
// Set the service state to Continue Pending. // SET STATUS [
|
||||
// Only useful if the startup time is long. Not really necessary here for a 2s startup time.
|
||||
serviceStatus.dwServiceType = ServiceType.SERVICE_WIN32_OWN_PROCESS;
|
||||
serviceStatus.dwCurrentState = ServiceState.SERVICE_CONTINUE_PENDING;
|
||||
serviceStatus.dwWin32ExitCode = 0;
|
||||
serviceStatus.dwWaitHint = 2000; // It takes about 2 seconds to start PowerShell
|
||||
SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS ]
|
||||
// Start a child process with another copy of this script
|
||||
try {
|
||||
Process p = new Process();
|
||||
// Redirect the output stream of the child process.
|
||||
p.StartInfo.UseShellExecute = false;
|
||||
p.StartInfo.RedirectStandardOutput = true;
|
||||
p.StartInfo.FileName = "PowerShell.exe";
|
||||
p.StartInfo.Arguments = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMResume"; // Works if path has spaces, but not if it contains ' quotes.
|
||||
p.Start();
|
||||
// Read the output stream first and then wait. (To avoid deadlocks says Microsoft!)
|
||||
string output = p.StandardOutput.ReadToEnd();
|
||||
// Wait for the completion of the script startup code, that launches the -Service instance
|
||||
p.WaitForExit();
|
||||
if (p.ExitCode != 0) throw new Win32Exception((int)(Win32Error.ERROR_APP_INIT_FAILURE));
|
||||
// Success. Set the service state to Running. // SET STATUS
|
||||
serviceStatus.dwCurrentState = ServiceState.SERVICE_RUNNING; // SET STATUS
|
||||
} catch (Exception e) {
|
||||
EventLog.WriteEntry(ServiceName, "$exeName OnContinue() // Failed to resume $scriptCopyCname. " + e.Message, EventLogEntryType.Error); // EVENT LOG
|
||||
// Change the service state back to Paused. // SET STATUS [
|
||||
serviceStatus.dwCurrentState = ServiceState.SERVICE_PAUSED;
|
||||
Win32Exception w32ex = e as Win32Exception; // Try getting the WIN32 error code
|
||||
if (w32ex == null) { // Not a Win32 exception, but maybe the inner one is...
|
||||
w32ex = e.InnerException as Win32Exception;
|
||||
}
|
||||
if (w32ex != null) { // Report the actual WIN32 error
|
||||
serviceStatus.dwWin32ExitCode = w32ex.NativeErrorCode;
|
||||
} else { // Make up a reasonable reason
|
||||
serviceStatus.dwWin32ExitCode = (int)(Win32Error.ERROR_APP_INIT_FAILURE);
|
||||
} // SET STATUS ]
|
||||
} finally {
|
||||
serviceStatus.dwWaitHint = 0; // SET STATUS
|
||||
SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS
|
||||
EventLog.WriteEntry(ServiceName, "$exeName OnContinue() // Exit"); // EVENT LOG
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void StopSCM()
|
||||
{
|
||||
// Start a child process with another copy of ourselves
|
||||
try {
|
||||
Process p = new Process();
|
||||
// Redirect the output stream of the child process.
|
||||
p.StartInfo.UseShellExecute = false;
|
||||
p.StartInfo.RedirectStandardOutput = true;
|
||||
p.StartInfo.FileName = "PowerShell.exe";
|
||||
p.StartInfo.Arguments = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMStop"; // Works if path has spaces, but not if it contains ' quotes.
|
||||
p.Start();
|
||||
// Read the output stream first and then wait. (To avoid deadlocks says Microsoft!)
|
||||
string output = p.StandardOutput.ReadToEnd();
|
||||
// Wait for the PowerShell script to be fully stopped.
|
||||
p.WaitForExit();
|
||||
if (p.ExitCode != 0) throw new Win32Exception((int)(Win32Error.ERROR_APP_INIT_FAILURE));
|
||||
// Success. Set the service state to Stopped. // SET STATUS
|
||||
serviceStatus.dwCurrentState = ServiceState.SERVICE_STOPPED; // SET STATUS
|
||||
} catch (Exception e) {
|
||||
EventLog.WriteEntry(ServiceName, "$exeName StopSCM() // Failed to stop $scriptCopyCname.", EventLogEntryType.Error); // EVENT LOG
|
||||
throw e; // SET STATUS ]
|
||||
} finally {
|
||||
serviceStatus.dwWaitHint = 0; // SET STATUS
|
||||
SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS
|
||||
}
|
||||
}
|
||||
|
||||
private void SuspendSCM()
|
||||
{
|
||||
// Start a child process with another copy of ourselves
|
||||
try {
|
||||
Process p = new Process();
|
||||
// Redirect the output stream of the child process.
|
||||
p.StartInfo.UseShellExecute = false;
|
||||
p.StartInfo.RedirectStandardOutput = true;
|
||||
p.StartInfo.FileName = "PowerShell.exe";
|
||||
p.StartInfo.Arguments = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMSuspend"; // Works if path has spaces, but not if it contains ' quotes.
|
||||
p.Start();
|
||||
// Read the output stream first and then wait. (To avoid deadlocks says Microsoft!)
|
||||
string output = p.StandardOutput.ReadToEnd();
|
||||
// Wait for the PowerShell script to be fully stopped.
|
||||
p.WaitForExit();
|
||||
if (p.ExitCode != 0) throw new Win32Exception((int)(Win32Error.ERROR_APP_INIT_FAILURE));
|
||||
// Success. Set the service state to Suspended. // SET STATUS
|
||||
serviceStatus.dwCurrentState = ServiceState.SERVICE_PAUSED; // SET STATUS
|
||||
} catch (Exception e) {
|
||||
EventLog.WriteEntry(ServiceName, "$exeName SuspendSCM() // Failed to suspend $scriptCopyCname.", EventLogEntryType.Error); // EVENT LOG
|
||||
throw e; // SET STATUS ]
|
||||
} finally {
|
||||
serviceStatus.dwWaitHint = 0; // SET STATUS
|
||||
SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnStop() {
|
||||
EventLog.WriteEntry(ServiceName, "$exeName OnStop() // Entry"); // EVENT LOG
|
||||
try {
|
||||
this.StopSCM();
|
||||
base.OnStop();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
EventLog.WriteEntry(ServiceName, "$exeName OnStop() // Fail. " + e.Message, EventLogEntryType.Error); // EVENT LOG
|
||||
throw e;
|
||||
}
|
||||
EventLog.WriteEntry(ServiceName, "$exeName OnStop() // Exit"); // EVENT LOG
|
||||
}
|
||||
|
||||
protected override void OnPause()
|
||||
{
|
||||
EventLog.WriteEntry(ServiceName, "$exeName OnPause() // Entry"); // EVENT LOG
|
||||
try {
|
||||
this.SuspendSCM();
|
||||
base.OnPause(); // This will set the service status to "Paused"
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
EventLog.WriteEntry(ServiceName, "$exeName OnPause() // Fail. " + e.Message, EventLogEntryType.Error); // EVENT LOG
|
||||
throw e;
|
||||
}
|
||||
EventLog.WriteEntry(ServiceName, "$exeName OnPause() // Exit"); // EVENT LOG
|
||||
}
|
||||
|
||||
protected override void OnCustomCommand(int command)
|
||||
{
|
||||
if (command == SERVICE_CONTROL_PRESHUTDOWN)
|
||||
{
|
||||
EventLog.WriteEntry(ServiceName, "$exeName OnPreshutdown() // Entry"); // EVENT LOG
|
||||
try{
|
||||
this.StopSCM();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
EventLog.WriteEntry(ServiceName, "$exeName OnPreshutdown() // Fail. " + e.Message, EventLogEntryType.Error); // EVENT LOG
|
||||
throw e;
|
||||
}
|
||||
EventLog.WriteEntry(ServiceName, "$exeName OnPreshutdown() // Exit"); // EVENT LOG
|
||||
}
|
||||
base.OnCustomCommand(command);
|
||||
}
|
||||
|
||||
protected override void OnShutdown() {
|
||||
// * NOP *
|
||||
}
|
||||
|
||||
public static void Main() {
|
||||
System.ServiceProcess.ServiceBase.Run(new Service_$serviceName());
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------#
|
||||
# #
|
||||
# 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"
|
||||
|
||||
# Remove file log
|
||||
# if ($Setup) {Log ""} # Insert one blank line to separate test sessions logs
|
||||
# Log $MyInvocation.Line # The exact command line that was used to start us
|
||||
|
||||
# The following commands write to the event log, but we need to make sure the PSService source is defined.
|
||||
New-EventLog -LogName $logName -Source $serviceName -ea SilentlyContinue
|
||||
|
||||
# Workaround for PowerShell v2 bug: $PSCmdlet Not yet defined in Param() block
|
||||
$Status = ($PSCmdlet.ParameterSetName -eq 'Status')
|
||||
|
||||
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 ($Setup) { # Install the service
|
||||
# Check if it's necessary
|
||||
try {
|
||||
$pss = Get-Service $serviceName -ea stop # Will error-out if not installed
|
||||
# Check if this script is newer than the installed copy.
|
||||
if ((Get-Item $scriptCopy -ea SilentlyContinue).LastWriteTime -lt (Get-Item $scriptFullName -ea SilentlyContinue).LastWriteTime) {
|
||||
Write-Verbose "Service $serviceName is already Installed, but requires upgrade"
|
||||
& $scriptFullName -Remove
|
||||
throw "continue"
|
||||
} else {
|
||||
Write-Verbose "Service $serviceName is already Installed, and up-to-date"
|
||||
}
|
||||
exit 0
|
||||
} catch {
|
||||
# This is the normal case here. Do not throw or write any error!
|
||||
Write-Debug "Installation is necessary" # Also avoids a ScriptAnalyzer warning
|
||||
# And continue with the installation.
|
||||
}
|
||||
if (!(Test-Path $installDir)) {
|
||||
New-Item -ItemType directory -Path $installDir | Out-Null
|
||||
}
|
||||
# Copy the service script into the installation directory
|
||||
if ($ScriptFullName -ne $scriptCopy) {
|
||||
Write-Verbose "Installing $scriptCopy"
|
||||
Copy-Item $ScriptFullName $scriptCopy
|
||||
}
|
||||
# Generate the service .EXE from the C# source embedded in this script
|
||||
try {
|
||||
Write-Verbose "Compiling $exeFullName"
|
||||
Add-Type -TypeDefinition $source -Language CSharp -OutputAssembly $exeFullName -OutputType ConsoleApplication -ReferencedAssemblies "System.ServiceProcess" -Debug:$false
|
||||
} catch {
|
||||
$msg = $_.Exception.Message
|
||||
Write-error "Failed to create the $exeFullName service stub. $msg"
|
||||
exit 1
|
||||
}
|
||||
# Register the service
|
||||
Write-Verbose "Registering service $serviceName"
|
||||
if ($UserName -and !$Credential.UserName) {
|
||||
$emptyPassword = New-Object -Type System.Security.SecureString
|
||||
switch ($UserName) {
|
||||
{"LocalService", "NetworkService" -contains $_} {
|
||||
$Credential = New-Object -Type System.Management.Automation.PSCredential ("NT AUTHORITY\$UserName", $emptyPassword)
|
||||
}
|
||||
{"LocalSystem", ".\LocalSystem", "${env:COMPUTERNAME}\LocalSystem", "NT AUTHORITY\LocalService", "NT AUTHORITY\NetworkService" -contains $_} {
|
||||
$Credential = New-Object -Type System.Management.Automation.PSCredential ($UserName, $emptyPassword)
|
||||
}
|
||||
default {
|
||||
if (!$Password) {
|
||||
$Credential = Get-Credential -UserName $UserName -Message "Please enter the password for the service user"
|
||||
} else {
|
||||
$securePassword = ConvertTo-SecureString $Password -AsPlainText -Force
|
||||
$Credential = New-Object -Type System.Management.Automation.PSCredential ($UserName, $securePassword)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#Find if exeFullName contain withespace (Security issue with Unquoted Service Path) # TO IMPROVE - Create PR on GitHub
|
||||
if ($exeFullName -match "\s") { $exeFullName = "`"$exeFullName`""}
|
||||
|
||||
if ($Credential.UserName) {
|
||||
Write-Log -Message "Configuring the service to run as $($Credential.UserName)"
|
||||
# TO IMPROVE - Add variable to manage DependsOn and Create PR on GitHub
|
||||
$pss = New-Service -Name $serviceName -BinaryPathName $exeFullName -DisplayName $serviceDisplayName -Description $ServiceDescription -StartupType Automatic -Credential $Credential -DependsOn 'RemoteAccess'
|
||||
} else {
|
||||
Write-Log -Message "Configuring the service to run by default as LocalSystem"
|
||||
# TO IMPROVE - Add variable to manage DependsOn and Create PR on GitHub
|
||||
$pss = New-Service -Name $serviceName -BinaryPathName $exeFullName -DisplayName $serviceDisplayName -Description $ServiceDescription -StartupType Automatic -DependsOn 'RemoteAccess'
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if ($Build) { # Install the service
|
||||
# Generate the service .EXE from the C# source embedded in this script
|
||||
|
||||
# Overwrite for builder
|
||||
$exeFullName=".\$exeName"
|
||||
|
||||
try {
|
||||
Write-Verbose "Compiling $exeFullName"
|
||||
Add-Type -TypeDefinition $source -Language CSharp -OutputAssembly $exeFullName -OutputType ConsoleApplication -ReferencedAssemblies "System.ServiceProcess" -Debug:$false
|
||||
} catch {
|
||||
$msg = $_.Exception.Message
|
||||
Write-error "Failed to create the $exeFullName service stub. $msg"
|
||||
exit 1
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if ($Remove) { # Uninstall the service
|
||||
# Check if it's necessary
|
||||
try {
|
||||
$pss = Get-Service $serviceName -ea stop # Will error-out if not installed
|
||||
} catch {
|
||||
Write-Verbose "Already uninstalled"
|
||||
return
|
||||
}
|
||||
Stop-Service $serviceName # Make sure it's stopped
|
||||
# In the absence of a Remove-Service applet, use sc.exe instead.
|
||||
Write-Verbose "Removing service $serviceName"
|
||||
$msg = sc.exe delete $serviceName
|
||||
if ($LastExitCode) {
|
||||
Write-Error "Failed to remove the service ${serviceName}: $msg"
|
||||
exit 1
|
||||
} else {
|
||||
Write-Verbose $msg
|
||||
}
|
||||
# Remove the installed files
|
||||
if (Test-Path $installDir) {
|
||||
foreach ($ext in ("exe", "pdb", "ps1")) {
|
||||
$file = "$installDir\$serviceName.$ext"
|
||||
if (Test-Path $file) {
|
||||
Write-Verbose "Deleting file $file"
|
||||
Remove-Item $file
|
||||
}
|
||||
}
|
||||
if (!(@(Get-ChildItem $installDir -ea SilentlyContinue)).Count) {
|
||||
Write-Verbose "Removing directory $installDir"
|
||||
Remove-Item $installDir
|
||||
}
|
||||
}
|
||||
# Remove file log
|
||||
# Log "" # Insert one blank line to separate test sessions logs
|
||||
return
|
||||
}
|
||||
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