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