├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── clean.ps1 ├── doc └── setup.md ├── globals.ps1 ├── installer └── patroni.iss ├── make.ps1 └── src ├── etcd.yaml ├── etcd_service.xml ├── install.ps1 ├── patroni.yaml ├── patroni_service.xml ├── patronictl.bat ├── uninstall.ps1 ├── vip.yaml └── vip_service.xml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | # Maintain dependencies for GitHub Actions 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 5 | 6 | name: Upload Release Asset 7 | 8 | jobs: 9 | build: 10 | name: Upload Release Asset 11 | runs-on: windows-latest 12 | steps: 13 | - uses: actions/setup-python@v6 14 | with: 15 | python-version: '3.13.5' 16 | 17 | - name: Checkout code 18 | uses: actions/checkout@v5 19 | 20 | - name: Build project 21 | shell: pwsh 22 | run: | 23 | .\make.ps1 24 | 25 | - name: Release Artifacts 26 | uses: softprops/action-gh-release@v2 27 | with: 28 | files: | 29 | *.zip 30 | *.tar.gz 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | 8 | test-windows: 9 | if: true # false to skip job during debug 10 | name: Build assets 11 | runs-on: windows-latest 12 | steps: 13 | - uses: actions/setup-python@v6 14 | with: 15 | python-version: '3.13.5' 16 | 17 | - name: Check out code 18 | uses: actions/checkout@v5 19 | 20 | - name: Test 21 | shell: pwsh 22 | run: | 23 | .\make.ps1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pes 2 | *.zip 3 | *.exe -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | PostgreSQL License 2 | 3 | Copyright (c) 2018-2023, CYBERTEC PostgreSQL International GmbH 4 | 5 | Permission to use, copy, modify, and distribute this software and its 6 | documentation for any purpose, without fee, and without a written agreement is 7 | hereby granted, provided that the above copyright notice and this paragraph 8 | and the following two paragraphs appear in all copies. 9 | 10 | IN NO EVENT SHALL CYBERTEC PostgreSQL International GmbH BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, 11 | SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING 12 | OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF CYBERTEC PostgreSQL International GmbH 13 | HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 14 | 15 | CYBERTEC PostgreSQL International GmbH SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 16 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 17 | PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, 18 | AND CYBERTEC PostgreSQL International GmbH HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 19 | ENHANCEMENTS, OR MODIFICATIONS. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 2 | [![Release](https://img.shields.io/github/release/cybertec-postgresql/patroni-windows-packaging.svg)](https://github.com/cybertec-postgresql/patroni-windows-packaging/releases/latest) 3 | [![Github All Releases](https://img.shields.io/github/downloads/cybertec-postgresql/patroni-windows-packaging/total?style=flat-square)](https://github.com/cybertec-postgresql/patroni-windows-packaging/releases) 4 | 5 | # patroni-windows-packaging 6 | 7 | Automate installing and launching of Patroni under Windows 8 | 9 | ## Install 10 | 11 | Download the archive from [Releases](https://github.com/cybertec-postgresql/patroni-windows-packaging/releases) page. 12 | To install from zip, please, check the [Setup Guide](doc/setup.md). 13 | 14 | ## Authors 15 | 16 | [Pavlo Golub](https://github.com/pashagolub) and [Julian Markwort](https://github.com/markwort) 17 | -------------------------------------------------------------------------------- /clean.ps1: -------------------------------------------------------------------------------- 1 | # Set the environment variables 2 | . .\globals.ps1 3 | 4 | Remove-Item -Recurse -Force "$MD", "patroni" -ErrorAction SilentlyContinue 5 | Remove-Item -Force ` 6 | "*.zip", ` 7 | "*.exe", ` 8 | "$env:TEMP\etcd.zip", ` 9 | "$env:TEMP\pes.zip", ` 10 | "$env:TEMP\micro.zip", ` 11 | "$env:TEMP\vip.zip", ` 12 | "$env:TEMP\pgsql.zip", ` 13 | "$env:TEMP\patroni.zip" ` 14 | -ErrorAction SilentlyContinue -------------------------------------------------------------------------------- /doc/setup.md: -------------------------------------------------------------------------------- 1 | # PES (Patroni Environment Setup) on Windows 2 | 3 | The package consists of: 4 | 5 | - [patroni](https://github.com/zalando/patroni) HA 6 | - [etcd](https://github.com/etcd-io/etcd) distributed key-value store 7 | - [vip-manager](https://github.com/cybertec-postgresql/vip-manager) virtual IP manager 8 | - [PostgreSQL](https://www.postgresql.org/) database itself 9 | - [python](https://www.python.org/) runtime and packages 10 | - [micro](https://github.com/zyedidia/micro) console editor 11 | 12 | While the processes within `patroni` itself do not change much when running it under Windows, most challenges come from creating an environment in which patroni can run. 13 | 14 | - `patroni` needs to be able to run PostgreSQL, this can only be done by unprivileged users. 15 | - That unprivileged user in turn needs to be able to run Patroni, thus is needs access to Python. 16 | - `patroni` needs to run as soon as the machine is booted up, without requiring anybody to log in and start anything. 17 | - For that services are used under Windows. Since `etcd`, `patroni`, and `vip-manager` are not native Windows services, it is wise to use `[WinSW](https://github.com/winsw/winsw/)` wrapper for that. 18 | 19 | # Installing all the things 20 | 21 | You can choose two installation methods: 22 | 23 | 1. by Installer (.exe) 24 | 2. by unzipping (.zip) and running a PowerShell Script. 25 | 26 | > [!IMPORTANT] 27 | > Both installation methods need to be run with **Administrator** privileges. Powershell v7+ is a requirement. 28 | 29 | In either case, you will need to install everything into a path that can be made accessible to the unprivileged user that will later be used to run `patroni` and PostgreSQL. 30 | 31 | This rules out any Paths that are below `C:\Users` 32 | 33 | We recommend installing everything into a directory directly at the root of `C:\`, e.g. `C:\PES\` . The PostgreSQL data dir can still be located in another location, but this will also need to be made accessible to the user running PostgreSQL. 34 | 35 | The PowerShell Script `install.ps1` needs to be run with special Execution Policy because it is not signed by us. You can verify the contents beforehand. 36 | To change the Execution Policy only for the execution of the script: 37 | 38 | ```powershell 39 | cd C:\PES 40 | powershell.exe -ExecutionPolicy Bypass 41 | .\install.ps1 42 | REM waiting... 43 | exit 44 | ``` 45 | 46 | During the installation, the script or the installer will try to create a new user `pes` and assign a randomly chosen password. This password will be printed on the screen, so make sure to note it down somewhere. Don't worry if you forget this password. You can check it in the `patroni\patroni_service.xml` file. 47 | 48 | Afterward, the script or installer will make sure to grant access to the installation directory to the newly created user. 49 | 50 | Should any of this user-creating or access-granting fail to work, here are the commands you can use (and adapt) yourself to fix it: 51 | 52 | ```powershell 53 | REM add a user with a password: 54 | net user username password /ADD 55 | 56 | REM change the password only: 57 | net user username newpassword 58 | ``` 59 | 60 | Even though a new user was just created, all remaining setup tasks need to be performed as an **Administrator**, primarily to register the Services. 61 | 62 | Because PostgreSQL cannot be run by a "superuser", Patroni, and subsequently PostgreSQL is run by the `pes` user. Consequently, the user needs to be able to access the pgsql binaries, patroni configuration, patroni script and so on. 63 | 64 | ```powershell 65 | REM grant full access to pes user: 66 | icacls C:\PES\ /q /c /t /grant pes:F 67 | ``` 68 | 69 | Now is also a good time to add the Firewall rules that etcd, Patroni, and PostgreSQL need to function. Make sure the program paths match up with your system, especially if you're running a different Python install location. 70 | 71 | ```powershell 72 | netsh advfirewall firewall add rule name="etcd" dir=in action=allow program="C:\PES\etcd\etcd.exe" enable=yes 73 | 74 | netsh advfirewall firewall add rule name="postgresql" dir=in action=allow program="C:\PES\pgsql\bin\postgres.exe" enable=yes 75 | 76 | netsh advfirewall firewall add rule name="python" dir=in action=allow program="C:\Program Files\Python38\python.exe" enable=yes 77 | ``` 78 | 79 | # Setup etcd 80 | 81 | From the base directory `C:\PES\`, go into the `etcd` directory and create a file `etcd.yaml`. 82 | 83 | ```yaml 84 | name: 'win1' 85 | data-dir: win1.etcd 86 | heartbeat-interval: 100 87 | election-timeout: 1000 88 | listen-peer-urls: http://0.0.0.0:2380 89 | listen-client-urls: http://0.0.0.0:2379 90 | initial-advertise-peer-urls: http://192.168.178.88:2380 91 | advertise-client-urls: http://192.168.178.88:2379 92 | initial-cluster: win1=http://192.168.178.88:2380,win2=http://192.168.178.89:2380,win3=http://192.168.178.90:2380 93 | initial-cluster-token: 'etcd-cluster' 94 | initial-cluster-state: 'new' 95 | enable-v2: true 96 | ``` 97 | 98 | The config file above is for three-node etcd clusters, which is the minimum recommended size. 99 | You can go through and replace the IP-addresses in `initial-advertise-peer-urls`, `advertise-client-urls`, and `initial-cluster` to match those of your three cluster members-to-be. 100 | The mapping `name=url` in the `initial-cluster` value needs to contain the matching `name` and `initial-advertise-peer-urls` of your cluster members. 101 | 102 | When you're done adapting the above `etcd.conf` to your needs, copy it over to the other cluster members and change the name, and IP addresses or hostnames there accordingly. 103 | 104 | To make sure that `etcd` can be run after boot, we need to create a Windows Service. Windows Services require the executable to behave in a particular fashion and to react to certain signals, all of which `etcd` cannot do. The simplest option is to use a wrapper that behaves in this fashion and in turn, launches `etcd` for us. One such wrapper (and the best option it seems) is [WinSW](https://github.com/winsw/winsw). 105 | 106 | A copy of the `winsw.exe` executable is renamed `etcd_service.exe` and an accompanying `etcd_service.xml` config file is created. The config contains details on where to find the `etcd` executable, where the config file (`etcd.conf`) is located, and where the logs should go. 107 | 108 | The next version of WinSW will allow to provide YAML configuration files. 109 | 110 | ## etcd service installation 111 | 112 | ```powershell 113 | etcd_service.exe install 114 | ``` 115 | 116 | Will register the service that will later launch `etcd` automatically for us. 117 | 118 | Apart from the messages on screen, you can check that the service is installed with: 119 | 120 | ```powershell 121 | sc.exe qc etcd 122 | ``` 123 | 124 | You should see that the start type for this service is set to auto, which means "start the service automatically after booting up". 125 | 126 | Now that the service is installed, we need to create 127 | 128 | ## etcd service running 129 | 130 | Having installed the service, you can start it manually: 131 | 132 | ```powershell 133 | > etcd_service.exe start 134 | or 135 | > net start etcd 136 | or 137 | > sc.exe start etcd 138 | ``` 139 | 140 | You will need to go through the etcd Setup on all three hosts in order to successfully bootstrap the etcd cluster. Only after that you will be able to continue with the setup of Patroni. 141 | 142 | ## etcd checking 143 | 144 | You can first take a look at `C:\PES\etcd\log\etcd_service.err.log`. If something went wrong during the installing or starting of the service already, the messages about that will be in `C:\PES\etcd\log\etcd_service.wrapper.log`. 145 | 146 | If there are no critical errors in those files, you can check if the etcd cluster is working allright, assuming that you've started all other etcd cluster members: 147 | 148 | ```powershell 149 | C:\PES\etcd\etcdctl cluster-health 150 | ``` 151 | 152 | ```powershell 153 | PS C:\PES\etcd> .\etcdctl cluster-health 154 | member 21f8508fe1bed56a is healthy: got healthy result from http://192.168.178.96:2379 155 | member 381962e0d76a93eb is healthy: got healthy result from http://192.168.178.97:2379 156 | member 49a65bc5e0e3e0ea is healthy: got healthy result from http://192.168.178.98:2379 157 | cluster is healthy 158 | ``` 159 | 160 | This should list all of your etcd cluster members and indicate that they are all working. 161 | 162 | If you receive any timeout errors or similar, something during the bootstrap went wrong. 163 | 164 | If you figured out the error that was preventing successful bootstrap of the cluster, it is best practice to 1. stop all etcd members 2. remove all etcd data directories 3. fix the error 4. start all etcd members. 165 | 166 | Some changes to the config (mainly those involving the initial cluster members and cluster name) will be ignored if the data dir has already been initialized. 167 | 168 | # Setup Patroni 169 | 170 | Warning: Do not begin setting up Patroni if your etcd cluster does not yet contain all cluster members, check `C:\PES\etcd\etcdctl cluster-health` to make sure. Otherwise you will have multiple Patroni instances who are not aware of their peers and will bootstrap on their own. 171 | 172 | From the base directory `C:\PES\`, go into the `patroni` directory and create (or edit) a file `patroni.yaml`. 173 | 174 | ```yaml 175 | scope: pgcluster 176 | namespace: /service/ 177 | name: win1 178 | 179 | restapi: 180 | listen: 0.0.0.0:8008 181 | connect_address: 192.168.178.88:8008 182 | 183 | etcd: 184 | hosts: 185 | - 192.168.178.88:2379 186 | - 192.168.178.89:2379 187 | - 192.168.178.90:2379 188 | 189 | bootstrap: 190 | dcs: 191 | ttl: 30 192 | loop_wait: 10 193 | retry_timeout: 10 194 | maximum_lag_on_failover: 1048906 195 | postgresql: 196 | use_pg_rewind: true 197 | use_slots: true 198 | parameters: 199 | logging_collector: true 200 | log_directory: log 201 | log_filename: postgresql.log 202 | wal_keep_segments: 50 203 | pg_hba: 204 | - host replication replicator 0.0.0.0/0 md5 205 | - host all all 0.0.0.0/0 md5 206 | 207 | initdb: 208 | - encoding: UTF8 209 | - data-checksums 210 | 211 | postgresql: 212 | listen: 0.0.0.0:5432 213 | connect_address: 192.168.178.88:5432 214 | data_dir: C:/PES/pgsql/pgcluster_data 215 | bin_dir: C:/PES/pgsql 216 | authentication: 217 | replication: 218 | username: replicator 219 | password: reptilefluid 220 | superuser: 221 | username: postgres 222 | password: snakeoil 223 | 224 | tags: 225 | nofailover: false 226 | noloadbalance: false 227 | clonefrom: false 228 | nosync: false 229 | ``` 230 | 231 | Under Windows one should double backslash path delimiter when used in patroni configuration, since it used as an escape character. To resolve the ambiguity we highly recommend to replace all backslashes with slashes in a folder names, e.g. 232 | `data_dir: C:/PES/pgsql/pgcluster_data` 233 | 234 | If you're running different Patroni clusters on top of the same etcd cluster, make sure to set a different `scope` (often reffered to as cluster name) for the different Patroni clusters. 235 | 236 | Change the `name` (this is the name of a member within the cluster `scope` ) to your liking; This name needs to be different for each cluster member. 237 | Setting the `name` to the hostname is often a good starting point. 238 | 239 | Replace the IP address in `restapi.connect_address` with the host's own IP address or hostname. This address will be used for communication from other Patroni members to this one. 240 | 241 | Replace the IP addresses in the `etcd.hosts` list to match the IP addresses or hostnames of your etcd cluster. 242 | 243 | Change the IP address in the `postgresql.listen` section to the host's own IP address or hostname. This address will be used when Patroni needs to pull a backup from the primary or to create the Streaming Replication connections. If Streaming Replication and backups should use a dedicated NIC put the IP address registered on that NIC here. 244 | 245 | If you intend to create a Patroni cluster from a preexisting PostgreSQL cluster, stop that cluster and put the location of that cluster's data directory into the `postgresql.data_dir` variable. If the PostgreSQL version of the preexisting cluster is different, change the `postgresql.bin_dir` accordingly. Make sure that the `pes` user can access both of those directories. 246 | 247 | For a full list of configuration items and their description, please refer to the Patroni [Documentation](https://patroni.readthedocs.io/en/latest/SETTINGS.html). 248 | 249 | When you're done adapting the above `patroni.yaml` to your needs, copy it over to the other cluster members and change the name, and IP addresses or hostnames there accordingly. 250 | 251 | The creation of the Patroni Service and start is similar to the procedure for `etcd`. 252 | The major difference is that Patroni needs to be run as the `pes` user. For this reason, the `patroni_service.xml` contains the user name and password. 253 | 254 | ## patroni service installation 255 | 256 | Create the service: 257 | 258 | ```powershell 259 | C:\PES\patroni\patroni_service.exe install 260 | ``` 261 | 262 | Check the service: 263 | 264 | ```powershell 265 | sc.exe qc patroni 266 | ``` 267 | 268 | You should see that the start type for this service is set to auto, which means "start the service automatically after booting up". 269 | 270 | ## patroni service running 271 | 272 | Start the service: 273 | 274 | ```powershell 275 | > patroni_service.exe start 276 | or 277 | > net start patroni 278 | or 279 | > sc.exe start patroni 280 | ``` 281 | 282 | It is recommended to start Patroni on one host first and check that it bootstrapped as expected, before starting the remaining cluster members. This is not to avoid race conditions, because Patroni can handle those fine. This recommendation is given mainly to make it easier to troubleshoot problems as soon as they arise. 283 | 284 | ## Check Patroni 285 | 286 | You can first take a look at `C:\PES\patroni\log\patroni_service.err.log`. If something went wrong during the installing or starting of the service already, the messages about that will be in `C:\PES\patroni\log\patroni_service.wrapper.log`. 287 | 288 | If the `patroni_service.err.log` contains messages like "starting PostgreSQL failed" or similar, check the PostgreSQL log as well, which should be located in `C:\PES\pgsql\pgcluster_data\log\`. 289 | 290 | If there are no critical errors in those files, you can check if the Patroni cluster is working allright: 291 | 292 | ```powershell 293 | C:\PES\patronictl list 294 | ``` 295 | 296 | ```powershell 297 | PS C:\PES\patroni> python patronictl.py -c patroni.yaml list 298 | + Cluster: pgcluster (6865748196457585920) --+----+-----------+ 299 | | Member | Host | Role | State | TL | Lag in MB | 300 | +--------+----------------+--------+---------+----+-----------+ 301 | | win1 | 192.168.178.96 | | running | 2 | 0 | 302 | | win2 | 192.168.178.97 | | running | 2 | 0 | 303 | | win3 | 192.168.178.98 | Leader | running | 2 | | 304 | +--------+----------------+--------+---------+----+-----------+ 305 | ``` 306 | 307 | This should list all of your Patroni cluster members and indicate that they are all working. 308 | 309 | If you are bootstrapping the cluster for the first time and the first cluster member did not yet show up, check the logs. 310 | 311 | If there are cluster members that display "Start failed" in their status field, you need to examine the logs on those machines first. 312 | 313 | # Setup vip-manager 314 | 315 | From the base directory `C:\PES\`, go into the `vip-manager` directory and create a file `vip-manager.yaml`. 316 | 317 | ```powershell 318 | # time (in milliseconds) after which vip-manager wakes up and checks if it needs to register or release ip addresses. 319 | interval: 1000 320 | 321 | # the etcd or consul key which vip-manager will regularly poll. 322 | key: "/service/pgcluster/leader" 323 | # if the value of the above key matches the trigger-value (often the hostname of this host), vip-manager will try to add the virtual ip address to the interface specified in Iface 324 | nodename: "win2" 325 | 326 | ip: 192.168.178.123 # the virtual ip address to manage 327 | mask: 24 # netmask for the virtual ip 328 | iface: "Ethernet 2" #interface to which the virtual ip will be added 329 | 330 | endpoint_type: etcd # etcd or consul 331 | # a list that contains all DCS endpoints to which vip-manager could talk. 332 | endpoints: 333 | - http://192.168.178.96:2379 334 | - http://192.168.178.97:2379 335 | - http://192.168.178.98:2379 336 | 337 | # how often things should be retried and how long to wait between retries. (currently only affects arpClient) 338 | retry_num: 2 339 | retry_after: 250 #in milliseconds 340 | ``` 341 | 342 | Change the `trigger-key` to match what the concatenation of these values from the patroni.yaml gives: ` + "/" + + "/leader"` . Patroni store the current leader name in this key. 343 | 344 | Change the `trigger-value` to the `name` in the `patroni.yaml` of this host. 345 | 346 | Change `ip`, `netmask`, `interface` to the virtual IP that will be managed and the appropriate netmask, as well as the networking interface on which the virtual IP should be registered. 347 | 348 | Change the `endpoints` list to the list of all your etcd cluster members. Do not forget the protocol prefrix: `http://` here. 349 | 350 | ## vip-manager service installation 351 | 352 | The creation of the vip-manager Service and start is similar to the procedure for etcd. 353 | Create the service: 354 | 355 | ```powershell 356 | C:\PES\vip-manager\vip_service install 357 | ``` 358 | 359 | Check the service: 360 | 361 | ```powershell 362 | sc.exe qc vip-manager 363 | ``` 364 | 365 | You should see that the start type for this service is set to auto, which means "start the service automatically after booting up". 366 | 367 | ## vip-manager service running 368 | 369 | Start the service: 370 | 371 | ```powershell 372 | > vip_service.exe start 373 | or 374 | > net start vip-manager 375 | or 376 | > sc start vip-manager 377 | ``` 378 | 379 | ## Check vip-manager 380 | 381 | You can first take a look at `C:\PES\vip-manager\log\vip_service.err.log`. If something went wrong during the installing or starting of the service already, the messages about that will be in `C:\PES\vip-manager\log\vip_service.wrapper.log`. 382 | 383 | When vip-manager is working as expected, it should log messages like ... 384 | 385 | ```powershell 386 | 2020/08/28 01:24:36 reading config from C:\PES\vip-manager\vip-manager.yaml 387 | 2020/08/28 01:24:36 IP address 192.168.178.123/24 state is false, desired false 388 | 2020/08/28 01:24:36 IP address 192.168.178.123/24 state is false, desired true 389 | 2020/08/28 01:24:36 Configuring address 192.168.178.123/24 on Ethernet 2 390 | 2020/08/28 01:24:36 IP address 192.168.178.123/24 state is true, desired true 391 | 2020/08/28 01:24:46 IP address 192.168.178.123/24 state is true, desired true 392 | 2020/08/28 01:24:56 IP address 192.168.178.123/24 state is true, desired true 393 | 2020/08/28 01:25:06 IP address 192.168.178.123/24 state is true, desired true 394 | 2020/08/28 01:25:16 IP address 192.168.178.123/24 state is true, desired true 395 | 2020/08/28 01:25:26 IP address 192.168.178.123/24 state is true, desired true 396 | 2020/08/28 01:25:36 IP address 192.168.178.123/24 state is true, desired true 397 | ``` 398 | 399 | # Check Patroni cluster is working as expected 400 | 401 | - Trigger a couple of switchovers (`patronictl switchover `) and observe (using `patronictl -w` that the demoted primary comes back up as a replica and clears its rewind state (i.e. switches to the new primary's timeline). Observe vip-manager log to make sure it is succesfully dropping the VIP on the old primary and registering it on the new primary. 402 | - Trigger a reinit of a replica (`patronictl reinit `). 403 | - Reboot your machines at least once to check if all the services are starting as expected. 404 | -------------------------------------------------------------------------------- /globals.ps1: -------------------------------------------------------------------------------- 1 | $MD = "PES" 2 | $VCREDIST_REF = "https://aka.ms/vs/17/release/vc_redist.x64.exe" 3 | $ETCD_REF = "https://github.com/etcd-io/etcd/releases/download/v3.5.21/etcd-v3.5.21-windows-amd64.zip" 4 | $PATRONI_REF = "https://github.com/patroni/patroni/archive/refs/tags/v4.0.6.zip" 5 | $MICRO_REF = "https://github.com/zyedidia/micro/releases/download/v2.0.14/micro-2.0.14-win64.zip" 6 | $WINSW_REF = "https://github.com/winsw/winsw/releases/download/v2.12.0/WinSW.NET461.exe" 7 | $VIP_REF = "https://github.com/cybertec-postgresql/vip-manager/releases/download/v4.0.0/vip-manager_4.0.0_Windows_x86_64.zip" 8 | $PGSQL_REF = "https://get.enterprisedb.com/postgresql/postgresql-17.5-1-windows-x64-binaries.zip" 9 | $PYTHON_REF = "https://www.python.org/ftp/python/3.13.5/python-3.13.5-amd64.exe" 10 | # one should change python version in github action workflows when changed here 11 | 12 | $SEVENZIP = "C:\Program Files\7-Zip\7z.exe" 13 | 14 | function Expand-ZipFile { 15 | param ( 16 | [string]$zipFilePath, 17 | [string]$destinationPath 18 | ) 19 | if (Test-Path $SEVENZIP) { 20 | & $SEVENZIP x "$zipFilePath" -o"$destinationPath" 21 | Start-Sleep -Seconds 5 22 | } 23 | else { 24 | Expand-Archive -Path "$zipFilePath" -DestinationPath "$destinationPath" 25 | } 26 | Remove-Item -Force "$zipFilePath" -ErrorAction Ignore 27 | } 28 | 29 | function Compress-ToZipFile { 30 | param ( 31 | [string]$sourcePath, 32 | [string]$destinationPath 33 | ) 34 | if (Test-Path $SEVENZIP) { 35 | & $SEVENZIP a "$destinationPath" -y "$sourcePath" 36 | } 37 | else { 38 | Compress-Archive -Path "$sourcePath" -DestinationPath "$destinationPath" 39 | } 40 | } 41 | 42 | function Start-Bootstrapping { 43 | Write-Host "`n--- Start bootstrapping ---" -ForegroundColor blue 44 | & ./clean.ps1 45 | New-Item -ItemType Directory -Path $MD 46 | Copy-Item "src\*.bat" $MD 47 | Copy-Item "src\*.ps1" $MD 48 | Copy-Item "doc" "$MD\doc" -Recurse 49 | Write-Host "`n--- End bootstrapping ---" -ForegroundColor green 50 | } 51 | 52 | function Get-VCRedist { 53 | Write-Host "`n--- Download VCREDIST ---" -ForegroundColor blue 54 | Invoke-WebRequest -Uri $VCREDIST_REF -OutFile "$MD\vc_redist.x64.exe" 55 | Write-Host "`n--- VCREDIST downloaded ---" -ForegroundColor green 56 | } 57 | 58 | function Get-ETCD { 59 | Write-Host "`n--- Download ETCD ---" -ForegroundColor blue 60 | Invoke-WebRequest -Uri $ETCD_REF -OutFile "$env:TEMP\etcd.zip" 61 | Expand-ZipFile "$env:TEMP\etcd.zip" "$MD" 62 | Rename-Item "$MD\etcd-*" "etcd" 63 | Copy-Item "src\etcd.yaml" "$MD\etcd" 64 | Write-Host "`n--- ETCD downloaded ---" -ForegroundColor green 65 | } 66 | 67 | function Get-Micro { 68 | Write-Host "`n--- Download MICRO ---" -ForegroundColor blue 69 | Invoke-WebRequest -Uri $MICRO_REF -OutFile "$env:TEMP\micro.zip" 70 | Expand-ZipFile "$env:TEMP\micro.zip" "$MD" 71 | Rename-Item "$MD\micro-*" "micro" 72 | Write-Host "`n--- MICRO downloaded ---" -ForegroundColor green 73 | } 74 | 75 | function Get-VIPManager { 76 | Write-Host "`n--- Download VIP-MANAGER ---" -ForegroundColor blue 77 | Invoke-WebRequest -Uri $VIP_REF -OutFile "$env:TEMP\vip.zip" 78 | Expand-ZipFile "$env:TEMP\vip.zip" "$MD" 79 | Rename-Item "$MD\vip-manager*" "vip-manager" 80 | Remove-Item "$MD\vip-manager\*.yml" -ErrorAction Ignore 81 | Copy-Item "src\vip.yaml" "$MD\vip-manager" 82 | Write-Host "`n--- VIP-MANAGER downloaded ---" -ForegroundColor green 83 | } 84 | 85 | function Get-PostgreSQL { 86 | Write-Host "`n--- Download POSTGRESQL ---" -ForegroundColor blue 87 | # Use prompt for credentials if auth is required 88 | # if (-not $PGSQL_CREDENTIAL) { 89 | # $global:PGSQL_CREDENTIAL = Get-Credential -Message "Enter credentials for PostgreSQL download" 90 | # } 91 | Invoke-WebRequest -Uri $PGSQL_REF -OutFile "$env:TEMP\pgsql.zip" -Credential $PGSQL_CREDENTIAL 92 | Expand-ZipFile "$env:TEMP\pgsql.zip" "$MD" 93 | Remove-Item -Recurse -Force "$MD\pgsql\pgAdmin 4", "$MD\pgsql\symbols" -ErrorAction Ignore 94 | Write-Host "`n--- POSTGRESQL downloaded ---" -ForegroundColor green 95 | } 96 | 97 | function Get-Patroni { 98 | Write-Host "`n--- Download PATRONI ---" -ForegroundColor blue 99 | Invoke-WebRequest -Uri $PATRONI_REF -OutFile "$env:TEMP\patroni.zip" 100 | Expand-ZipFile "$env:TEMP\patroni.zip" "$MD" 101 | Rename-Item "$MD\patroni-*" "patroni" 102 | Remove-Item "$MD\patroni\postgres?.yml" -ErrorAction Ignore 103 | Copy-Item "src\patroni.yaml" "$MD\patroni" 104 | Write-Host "`n--- PATRONI downloaded ---" -ForegroundColor green 105 | } 106 | 107 | function Update-PythonAndPIP { 108 | Write-Host "`n--- Update Python and PIP installation ---" -ForegroundColor blue 109 | $PYTHON = "python.exe" 110 | $PIP = "pip3.exe" 111 | 112 | if (-Not $env:RUNNER_TOOL_CACHE) { 113 | Write-Host "Running on a local machine builder" -ForegroundColor Yellow 114 | $PYTHON = "$env:ProgramFiles\Python313\python.exe" 115 | $PIP = "$env:ProgramFiles\Python313\Scripts\pip3.exe" 116 | } 117 | 118 | Write-Host "Loading the Python installation..." -ForegroundColor Blue 119 | Invoke-WebRequest -Uri $PYTHON_REF -OutFile "python-install.exe" 120 | Start-Process -FilePath "python-install.exe" -ArgumentList "/quiet InstallAllUsers=1 PrependPath=1 Include_test=0 Include_launcher=0" -Wait 121 | 122 | & $PYTHON -m pip install --upgrade pip 123 | 124 | Write-Host "Python version is:" -ForegroundColor Green 125 | & $PYTHON --version 126 | 127 | Write-Host "PIP version is:" -ForegroundColor Green 128 | & $PIP --version 129 | 130 | $global:PYTHON = $PYTHON 131 | $global:PIP = $PIP 132 | 133 | Move-Item "python-install.exe" "$MD" 134 | Write-Host "`n--- Python and PIP installation updated ---" -ForegroundColor green 135 | } 136 | 137 | function Get-PatroniPackages { 138 | Write-Host "`n--- Download PATRONI packages ---" -ForegroundColor blue 139 | Set-Location "$MD\patroni" 140 | & $PIP download -r requirements.txt -d .patroni-packages 141 | & $PIP download pip pip_install setuptools wheel cdiff psycopg psycopg-binary -d .patroni-packages 142 | Set-Location -Path "..\.." 143 | Write-Host "`n--- PATRONI packages downloaded ---" -ForegroundColor green 144 | } 145 | 146 | function Get-WinSW { 147 | Write-Host "`n--- Download WINSW ---" -ForegroundColor blue 148 | Invoke-WebRequest -Uri $WINSW_REF -OutFile "$MD\patroni\patroni_service.exe" 149 | Copy-Item "src\patroni_service.xml" "$MD\patroni" 150 | Copy-Item "$MD\patroni\patroni_service.exe" "$MD\etcd\etcd_service.exe" -Force 151 | Copy-Item "src\etcd_service.xml" "$MD\etcd" 152 | Copy-Item "$MD\patroni\patroni_service.exe" "$MD\vip-manager\vip_service.exe" -Force 153 | Copy-Item "src\vip_service.xml" "$MD\vip-manager" 154 | Write-Host "`n--- WINSW downloaded ---" -ForegroundColor green 155 | } 156 | 157 | function Export-Assets { 158 | Write-Host "`n--- Prepare archive ---" -ForegroundColor blue 159 | Compress-ToZipFile "$MD" "$MD.zip" 160 | Write-Host "`n--- Archive compressed ---" -ForegroundColor green 161 | } 162 | -------------------------------------------------------------------------------- /installer/patroni.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define MyAppName "Patroni Environment Setup" 5 | #define MyAppInstallDir "PES" 6 | #define MyAppVersion "v240516" 7 | #define MyAppPublisher "CYBERTEC PostgreSQL International GmbH" 8 | #define MyAppURL "https://www.cybertec-postgresql.com/" 9 | 10 | [Setup] 11 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 12 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 13 | AppId={{4D29D928-999E-4D56-9896-76DEB4F259EC} 14 | AppName={#MyAppName} 15 | AppVersion={#MyAppVersion} 16 | ;AppVerName={#MyAppName} {#MyAppVersion} 17 | AppPublisher={#MyAppPublisher} 18 | AppPublisherURL={#MyAppURL} 19 | AppSupportURL={#MyAppURL} 20 | AppUpdatesURL={#MyAppURL} 21 | DefaultDirName={sd}\{#MyAppInstallDir} 22 | DefaultGroupName={#MyAppName} 23 | AllowNoIcons=yes 24 | LicenseFile=..\LICENSE 25 | ; Uncomment the following line to run in non administrative install mode (install for current user only.) 26 | ; PrivilegesRequired=lowest 27 | OutputDir=.. 28 | OutputBaseFilename=Patroni-Env-Setup 29 | Compression=lzma 30 | SolidCompression=yes 31 | WizardStyle=modern 32 | ArchitecturesInstallIn64BitMode="x64" 33 | AppCopyright=CYBERTEC PostgreSQL International GmbH 34 | 35 | [Languages] 36 | Name: "english"; MessagesFile: "compiler:Default.isl" 37 | 38 | [Files] 39 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 40 | Source: "..\PES\*"; DestDir: "{app}"; Flags: ignoreversion createallsubdirs recursesubdirs 41 | 42 | [Icons] 43 | Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" 44 | 45 | [Run] 46 | Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\install.ps1"""; WorkingDir: "{app}"; Flags: waituntilterminated 47 | 48 | [UninstallRun] 49 | Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\uninstall.ps1"""; WorkingDir: "{app}"; Flags: waituntilterminated 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /make.ps1: -------------------------------------------------------------------------------- 1 | # Stop execution on any error 2 | $ErrorActionPreference = "Stop" 3 | 4 | # Set the environment variables and load the functions 5 | . .\globals.ps1 6 | 7 | Start-Bootstrapping 8 | Get-VCRedist 9 | Get-ETCD 10 | Get-Micro 11 | Get-VIPManager 12 | Get-PostgreSQL 13 | Get-Patroni 14 | Update-PythonAndPIP 15 | Get-PatroniPackages 16 | Get-WinSW 17 | Export-Assets 18 | Write-Host "`n--- PACKAGING FINISHED ---" -ForegroundColor green 19 | -------------------------------------------------------------------------------- /src/etcd.yaml: -------------------------------------------------------------------------------- 1 | name: 'win1' 2 | data-dir: win1.etcd 3 | heartbeat-interval: 100 4 | election-timeout: 1000 5 | listen-peer-urls: http://0.0.0.0:2380 6 | listen-client-urls: http://0.0.0.0:2379 7 | initial-advertise-peer-urls: http://192.168.178.88:2380 8 | advertise-client-urls: http://192.168.178.88:2379 9 | initial-cluster: win1=http://192.168.178.88:2380,win2=http://192.168.178.89:2380,win3=http://192.168.178.90:2380 10 | initial-cluster-token: 'etcd-cluster' 11 | initial-cluster-state: 'new' 12 | # enable-v2: true -------------------------------------------------------------------------------- /src/etcd_service.xml: -------------------------------------------------------------------------------- 1 | 2 | etcd 3 | etcd 4 | Distributed reliable key-value store 5 | %BASE%\etcd.exe 6 | --config-file=%BASE%\etcd.yaml 7 | %BASE%\log 8 | 9 | -------------------------------------------------------------------------------- /src/install.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 7.0 2 | #Requires -RunAsAdministrator 3 | 4 | Write-Host "--- Installing VC++ 2015-2019 redistributable ---" -ForegroundColor blue 5 | Start-Process -FilePath .\vc_redist.x64.exe -ArgumentList "/install /quiet /norestart" -NoNewWindow -Wait 6 | Write-Host "--- VC++ 2015-2019 redistributable installed ---`n" -ForegroundColor green 7 | 8 | Write-Host "--- Installing Python runtime ---" -ForegroundColor blue 9 | Start-Process -FilePath .\python-install.exe -ArgumentList "/quiet InstallAllUsers=1 PrependPath=1 Include_test=0 Include_launcher=0" -NoNewWindow -Wait 10 | 11 | # Update Path variable with installed Python 12 | $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") 13 | 14 | # update pip and pipe output to stdout to avoid parallel execution 15 | Set-Location 'patroni' 16 | python.exe -m pip install --upgrade --no-index --find-links .patroni-packages pip | Out-Default 17 | pip3.exe install --no-index --find-links .patroni-packages pip_install 18 | pip3.exe install --no-index --find-links .patroni-packages setuptools 19 | pip3.exe install --no-index --find-links .patroni-packages wheel 20 | Write-Host "--- Python runtime installed ---`n" -ForegroundColor green 21 | 22 | Write-Host "--- Installing Patroni packages ---" -ForegroundColor blue 23 | pip3.exe install --no-index --find-links .patroni-packages -r requirements.txt 24 | pip3.exe install --no-index --find-links .patroni-packages psycopg psycopg-binary 25 | pip3.exe install --no-index --find-links .patroni-packages cdiff 26 | Set-Location '..' 27 | Write-Host "--- Patroni packages installed ---`n" -ForegroundColor green 28 | 29 | $userName = "pes" 30 | $out = Get-LocalUser -Name $userName -ErrorAction SilentlyContinue 31 | if($null -eq $out) 32 | { 33 | Write-Host "--- Adding local user '$userName' for patroni service ---" -ForegroundColor blue 34 | $Password = ("a".."z")+("A".."Z") | Get-Random -Count 4 35 | $Password += ("!"..".") | Get-Random -Count 2 36 | $Password += ("0".."9") | Get-Random -Count 2 37 | $Password = [Security.SecurityElement]::Escape(-join($Password)) 38 | 39 | $SecurePassword = ConvertTo-SecureString $Password -AsPlainText -Force 40 | New-LocalUser $userName -Password $SecurePassword -Description "Patroni service account" 41 | $ConfFile = 'patroni\patroni_service.xml' 42 | (Get-Content $ConfFile) -replace '12345', $Password | Out-File -encoding ASCII $ConfFile 43 | Write-Host "--- Patroni user '$userName' added ---`n" -ForegroundColor green 44 | } 45 | else 46 | { 47 | Write-Host "--- WARNING: Patroni user '$userName' already exists! ---" -ForegroundColor red 48 | Write-Host "--- Please, set a correct password in 'patroni\patroni_service.xml'! ---`n" -ForegroundColor red 49 | } 50 | 51 | Write-Host "--- Installing Etcd service ---" -ForegroundColor blue 52 | etcd\etcd_service.exe install | Out-Default 53 | Write-Host "--- Etcd service sucessfully installed ---" -ForegroundColor green 54 | 55 | Write-Host "--- Installing patroni service ---" -ForegroundColor blue 56 | patroni\patroni_service.exe install | Out-Default 57 | Write-Host "--- Patroni service sucessfully installed ---" -ForegroundColor green 58 | 59 | Write-Host "--- Installing vip-manager service ---" -ForegroundColor blue 60 | vip-manager\vip_service.exe install | Out-Default 61 | Write-Host "--- vip-manager service sucessfully installed ---" -ForegroundColor green 62 | 63 | $workDir = (Get-Location).tostring() 64 | $python = (Get-Command python.exe).Source 65 | 66 | # grant access to PES directory 67 | Write-Host "--- Grant access to working directory ---" -ForegroundColor blue 68 | icacls $workDir /q /c /t /grant $userName:F 69 | Write-Host "--- Access to working directory granted ---" -ForegroundColor green 70 | 71 | Write-Host "--- Enabling Etcd, Postgres and patroni (via python) to listen to incomming traffic ---" -ForegroundColor blue 72 | netsh advfirewall firewall add rule name="etcd" dir=in action=allow program="$workDir\etcd\etcd.exe" enable=yes 73 | netsh advfirewall firewall add rule name="postgresql" dir=in action=allow program="$workDir\pgsql\bin\postgres.exe" enable=yes 74 | netsh advfirewall firewall add rule name="python" dir=in action=allow program="$python" enable=yes 75 | Write-Host "--- Firewall rules sucessfully installed ---" -ForegroundColor green 76 | 77 | Write-Host "--- Installation sucessfully finished ---" -ForegroundColor green 78 | -------------------------------------------------------------------------------- /src/patroni.yaml: -------------------------------------------------------------------------------- 1 | scope: pgcluster 2 | namespace: /service/ 3 | name: win1 4 | 5 | restapi: 6 | listen: 0.0.0.0:8008 7 | connect_address: 192.168.178.88:8008 8 | 9 | etcd3: 10 | hosts: 11 | - 192.168.178.88:2379 12 | - 192.168.178.89:2379 13 | - 192.168.178.90:2379 14 | 15 | bootstrap: 16 | dcs: 17 | ttl: 30 18 | loop_wait: 10 19 | retry_timeout: 10 20 | maximum_lag_on_failover: 1048906 21 | postgresql: 22 | use_pg_rewind: true 23 | use_slots: true 24 | parameters: 25 | logging_collector: true 26 | log_directory: log 27 | log_filename: postgresql.log 28 | wal_keep_segments: 50 29 | pg_hba: 30 | - host replication replicator 0.0.0.0/0 md5 31 | - host all all 0.0.0.0/0 md5 32 | 33 | initdb: 34 | - encoding: UTF8 35 | - data-checksums 36 | 37 | postgresql: 38 | listen: 0.0.0.0:5432 39 | connect_address: 192.168.178.88:5432 40 | data_dir: ../pgsql/data 41 | bin_dir: ../pgsql/bin 42 | authentication: 43 | replication: 44 | username: replicator 45 | password: reptilefluid 46 | superuser: 47 | username: postgres 48 | password: snakeoil 49 | 50 | tags: 51 | nofailover: false 52 | noloadbalance: false 53 | clonefrom: false 54 | nosync: false 55 | -------------------------------------------------------------------------------- /src/patroni_service.xml: -------------------------------------------------------------------------------- 1 | 2 | patroni 3 | Patroni HA Windows Service 4 | Patroni high-availability solution using Python and etcd 5 | python.exe 6 | %BASE%\patroni.py %BASE%\patroni.yaml 7 | true 8 | 9 | %BASE%\log 10 | 11 | pes 12 | 12345 13 | true 14 | 15 | -------------------------------------------------------------------------------- /src/patronictl.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | SETLOCAL 4 | 5 | REM Set here your console [sic!] favorite editor 6 | SET EDITOR=micro\micro.exe 7 | 8 | python.exe patroni\patronictl.py -c patroni\patroni.yaml %* 9 | -------------------------------------------------------------------------------- /src/uninstall.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 7.0 2 | #Requires -RunAsAdministrator 3 | 4 | Write-Host "--- Uninstalling Etcd service ---" -ForegroundColor blue 5 | etcd\etcd_service.exe uninstall | Out-Default 6 | Write-Host "--- Etcd service sucessfully uninstalled ---" -ForegroundColor green 7 | 8 | Write-Host "--- Uninstalling patroni service ---" -ForegroundColor blue 9 | patroni\patroni_service.exe uninstall | Out-Default 10 | Write-Host "--- Patroni service sucessfully uninstalled ---" -ForegroundColor green 11 | 12 | Write-Host "--- Uninstalling vip-manager service ---" -ForegroundColor blue 13 | vip-manager\vip_service.exe uninstall | Out-Default 14 | Write-Host "--- vip-manager service sucessfully uninstalled ---" -ForegroundColor green 15 | 16 | Write-Host "--- Disabling Etcd, Postgres and patroni firewall rules ---" -ForegroundColor blue 17 | netsh advfirewall firewall delete rule name="etcd" 18 | netsh advfirewall firewall delete rule name="postgresql" 19 | netsh advfirewall firewall delete rule name="python" 20 | Write-Host "--- Firewall rules sucessfully deleted ---" -ForegroundColor green 21 | 22 | Write-Host "--- Uninstallation sucessfully finished ---" -ForegroundColor green -------------------------------------------------------------------------------- /src/vip.yaml: -------------------------------------------------------------------------------- 1 | # time (in milliseconds) after which vip-manager wakes up and checks if it needs to register or release ip addresses. 2 | interval: 1000 3 | 4 | # the etcd or consul key which vip-manager will regularly poll. 5 | key: "/service/pgcluster/leader" 6 | # if the value of the above key matches the trigger-value (often the hostname of this host), vip-manager will try to add the virtual ip address to the interface specified in Iface 7 | nodename: "win2" 8 | 9 | ip: 192.168.178.123 # the virtual ip address to manage 10 | mask: 24 # netmask for the virtual ip 11 | iface: "Ethernet 2" #interface to which the virtual ip will be added 12 | 13 | endpoint_type: etcd # etcd or consul 14 | # a list that contains all DCS endpoints to which vip-manager could talk. 15 | endpoints: 16 | - http://192.168.178.96:2379 17 | - http://192.168.178.97:2379 18 | - http://192.168.178.98:2379 19 | 20 | # how often things should be retried and how long to wait between retries. (currently only affects arpClient) 21 | retry_num: 2 22 | retry_after: 250 #in milliseconds -------------------------------------------------------------------------------- /src/vip_service.xml: -------------------------------------------------------------------------------- 1 | 2 | vip-manager 3 | vip-manager 4 | Manager for a virtual IP based on state kept in etcd or Consul 5 | %BASE%\vip-manager.exe 6 | --config %BASE%\vip.yaml 7 | %BASE%\log 8 | 9 | --------------------------------------------------------------------------------