Compare commits
34 Commits
c859738b0f
...
applier
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c4e5a9363 | ||
|
|
3445bb57a4 | ||
|
|
35e3dc14b7 | ||
|
|
afe5dc0597 | ||
|
|
6fb43736f8 | ||
|
|
5612667a58 | ||
|
|
63585671dc | ||
|
|
ffa0a77f5e | ||
|
|
852ada3904 | ||
|
|
b4652c6d93 | ||
|
|
e106cbac47 | ||
|
|
1534002c6b | ||
|
|
04258ad46d | ||
|
|
67df74968b | ||
|
|
2916432c34 | ||
|
|
cb23d562eb | ||
|
|
10cd60af2c | ||
|
|
0d77432a92 | ||
|
|
ff7d69fa0a | ||
|
|
b577404611 | ||
|
|
4b0a8c9899 | ||
|
|
4e139d02f3 | ||
|
|
641d0f0190 | ||
|
|
6879fa85dd | ||
|
|
ac13ffbdb1 | ||
|
|
9a7003c46f | ||
|
|
ab5f1e43c0 | ||
|
|
8f7c65d8ca | ||
|
|
4ba8c59fd8 | ||
|
|
80058fceed | ||
|
|
78a6f413aa | ||
|
|
19b0354e85 | ||
|
|
55700429de | ||
|
|
e7c9a93702 |
14
.github/workflows/black.yml
vendored
14
.github/workflows/black.yml
vendored
@@ -1,14 +0,0 @@
|
||||
name: Lint
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: psf/black@stable
|
||||
with:
|
||||
options: "--check --verbose --diff"
|
||||
src: ". vppcfg"
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -6,11 +6,6 @@ __pycache__
|
||||
dist
|
||||
vppcfg.egg-info
|
||||
.pybuild
|
||||
debian/vppcfg
|
||||
debian/vppcfg.*.log
|
||||
debian/vppcfg.*.debhelper
|
||||
debian/.debhelper
|
||||
debian/files
|
||||
debian/vppcfg.substvars
|
||||
.*.~undo-tree~
|
||||
.eggs
|
||||
.venv/
|
||||
|
||||
218
LICENSE
218
LICENSE
@@ -1,22 +1,202 @@
|
||||
Copyright (c) 2021, Pim van Pelt <pim@ipng.nl>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
||||
|
||||
10
Makefile
10
Makefile
@@ -1,22 +1,26 @@
|
||||
VERSION=0.0.6
|
||||
VERSION=$(shell python3 -c "import sys; sys.path.insert(0, '.'); from vppcfg._version import __version__; print(__version__)")
|
||||
VPPCFG:=vppcfg
|
||||
PYTHON?=python3
|
||||
PIP?=pip
|
||||
PIP_DEPENDS=build yamale netaddr pylint
|
||||
PIP_DEPENDS+=argparse pyyaml ipaddress black
|
||||
PIP_DEPENDS+=argparse pyyaml ipaddress black build twine
|
||||
WIPE=dist $(VPPCFG).egg-info .pybuild debian/vppcfg debian/vppcfg.*.log
|
||||
WIPE+=debian/vppcfg.*.debhelper debian/.debhelper debian/files
|
||||
WIPE+=debian/vppcfg.substvars
|
||||
WHL_INSTALL=dist/$(VPPCFG)-$(VERSION)-py3-none-any.whl
|
||||
TESTS=tests.py
|
||||
|
||||
.PHONY: publish
|
||||
publish:
|
||||
$(PYTHON) -m twine upload dist/$(VPPCFG)*$(VERSION)*
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
$(PYTHON) -m build
|
||||
|
||||
.PHONY: install-deps
|
||||
install-deps:
|
||||
sudo $(PIP) install $(PIP_DEPENDS)
|
||||
$(PIP) install $(PIP_DEPENDS)
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
|
||||
33
README.md
33
README.md
@@ -5,30 +5,9 @@ reconciles a running [VPP](https://fd.io/) daemon with its configuration. It is
|
||||
re-entrant and stateless. The tool connects to the VPP API and creates/removes all of the
|
||||
configuration in a minimally intrusive way.
|
||||
|
||||
***NOTE*** This code is under development, and probably won't work well until this note is removed.
|
||||
If you're interested in helping, reach out to <pim at ipng dot ch> to discuss options.
|
||||
|
||||
## Building
|
||||
|
||||
This program expects Python3 and PIP to be installed. It's known to work on Debian Bullseye and
|
||||
Ubuntu Focal/Jammy.
|
||||
|
||||
You can simply install this package from [[PyPi](https://pypi.org/project/vppcfg/)]:
|
||||
```
|
||||
## Install python build dependencies
|
||||
$ make install-deps
|
||||
|
||||
## Ensure all unittests pass.
|
||||
$ make test
|
||||
|
||||
## Build vppcfg
|
||||
$ make build
|
||||
|
||||
## Install the tool with PIP
|
||||
$ make install
|
||||
|
||||
## To build & install debian packaging
|
||||
$ make pkg-deb
|
||||
$ ls -l ../vppcfg_*_amd64.deb
|
||||
$ pip install [--break-system-packages] vppcfg
|
||||
```
|
||||
|
||||
## Running
|
||||
@@ -55,18 +34,18 @@ Please see vppcfg <command> -h for per-command arguments
|
||||
## Documentation
|
||||
|
||||
Main user-focused documentation:
|
||||
* [YAML Configuration Guide](docs/config-guide.md)
|
||||
* [User Guide](docs/user-guide.md)
|
||||
* [YAML Configuration Guide](https://git.ipng.ch/ipng/vppcfg/src/branch/main/docs/config-guide.md)
|
||||
* [User Guide](https://git.ipng.ch/ipng/vppcfg/src/branch/main/docs/user-guide.md)
|
||||
|
||||
Developer deep-dives:
|
||||
* [Validation](https://ipng.ch/s/articles/2022/03/27/vppcfg-1.html)
|
||||
* [Path Planning](https://ipng.ch/s/articles/2022/04/02/vppcfg-2.html)
|
||||
* [Design - Reconciliation](docs/design.md)
|
||||
* [Design - Reconciliation](https://git.ipng.ch/ipng/vppcfg/src/branch/main/docs/design.md)
|
||||
|
||||
|
||||
## Licensing
|
||||
|
||||
The code in this project is release under Apache 2.0 license. A copy of the license
|
||||
The code in this project is released under Apache 2.0 license. A copy of the license
|
||||
is provided in this repo [here](LICENSE). All contributions are held against our
|
||||
[contributing](docs/contributing.md) guidelines. Notably, all code must be licensed
|
||||
Apache 2.0, and all contributions must come with a certificate of origin in the
|
||||
|
||||
45
debian/changelog
vendored
45
debian/changelog
vendored
@@ -1,45 +0,0 @@
|
||||
vppcfg (0.0.6) unstable; urgency=low
|
||||
|
||||
Feature release:
|
||||
* Re-order the interfaces: loopbacks before phy/sub-int
|
||||
|
||||
-- Pim van Pelt <pim@ipng.nl> Sun, 07 APr 2024 12:15:00 +0000
|
||||
vppcfg (0.0.5) unstable; urgency=low
|
||||
|
||||
Feature release:
|
||||
* Allow for 'unnumbered' interfaces.
|
||||
-- Pim van Pelt <pim@ipng.nl> Sun, 07 APr 2024 12:15:00 +0000
|
||||
vppcfg (0.0.4) unstable; urgency=low
|
||||
|
||||
Feature release:
|
||||
* Bugfix, ensuring IPv6 addresses are in their canonical form, to avoid
|
||||
spurious diffs when comparing to VPP dataplane addresses.
|
||||
* Document the 'device-type' field for interfaces
|
||||
-- Pim van Pelt <pim@ipng.nl> Sat, 03 Dec 2022 14:15:00 +0000
|
||||
vppcfg (0.0.3) unstable; urgency=low
|
||||
|
||||
Feature release:
|
||||
* Allow the ability to set --novpp flag on planners. See user-guide for
|
||||
details.
|
||||
* Add device-type to the schema, which allows the configuratoin to specify
|
||||
'dpdk' type interfaces. This paves the way for RDMA and AVF interfaces
|
||||
in a later release.
|
||||
* Some small bugfixes and refactors
|
||||
* Lint: address a few pylint errors
|
||||
|
||||
-- Pim van Pelt <pim@ipng.nl> Sat, 03 Dec 2022 14:15:00 +0000
|
||||
vppcfg (0.0.2) unstable; urgency=low
|
||||
|
||||
Bugfix release:
|
||||
* Fix python load / include paths, now vppcfg runs relative to $PATH
|
||||
* Partial fix for 'make test' (doesn't execute the YAMLTest suite yet,
|
||||
this has no impact to production)
|
||||
* Bugfix: correctly prune vxlan tunnels which have an address set
|
||||
* Lint: address a few pylint errors
|
||||
|
||||
-- Ray Kinsella <mdr@ashroe.eu> Thu, 30 Jun 2022 14:00:00 +0000
|
||||
vppcfg (0.0.1) unstable; urgency=low
|
||||
|
||||
* Initial public release
|
||||
|
||||
-- Ray Kinsella <mdr@ashroe.eu> Thu, 30 Jun 2022 14:00:00 +0000
|
||||
1
debian/compat
vendored
1
debian/compat
vendored
@@ -1 +0,0 @@
|
||||
9
|
||||
12
debian/control
vendored
12
debian/control
vendored
@@ -1,12 +0,0 @@
|
||||
Source: vppcfg
|
||||
Section: python
|
||||
Priority: extra
|
||||
Maintainer: Ray Kinsella <mdr@ashroe.eu>
|
||||
Build-Depends: debhelper (>= 9), python3-all, dh-python
|
||||
Standards-Version: 3.9.5
|
||||
|
||||
Package: vppcfg
|
||||
Architecture: any
|
||||
Pre-Depends: dpkg (>= 1.16.1), python3 (>=3.8), ${misc:Pre-Depends}
|
||||
Depends: python3-netaddr, python3-ipaddr, ${misc:Depends}
|
||||
Description: A configuration tool for FD.io VPP
|
||||
4
debian/rules
vendored
4
debian/rules
vendored
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
%:
|
||||
dh $@ --with python3 --buildsystem=pybuild --with systemd
|
||||
13
debian/vppcfg.service
vendored
13
debian/vppcfg.service
vendored
@@ -1,13 +0,0 @@
|
||||
[Unit]
|
||||
Description=vppcfg
|
||||
BindsTo=vpp.service
|
||||
After=vpp.service
|
||||
ConditionPathExists=/etc/vpp/config.yaml
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=vppcfg apply -c /etc/vpp/config.yaml
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -171,11 +171,23 @@ BondEthernets are required to be named `BondEthernetN` (note the camelcase) wher
|
||||
* ***load-balance***: A loadbalancing strategy to use, if the mode is either XOR or LACP.
|
||||
Can be one of 'l2', 'l23', or 'l34'. The default is l34, which hashes on the source and
|
||||
destination IPs and ports.
|
||||
* ***mac***: the MAC address assigned to the bondethernet device. At creation time, an ephemeral,
|
||||
address is chosen; then once the first interface is added, the MAC address from that phy
|
||||
overwrites the bondethernet address. To avoid that, you can hard-code it here.
|
||||
|
||||
Note that the configuration object here only specifies the link aggregation and its members.
|
||||
BondEthernets are expected to occur as well in the `interfaces` section, where their sub-interfaces
|
||||
and IP addresses and so on are specified.
|
||||
|
||||
There's a notable interaction with Linux Control Plane: When _LIP_ is created, VPP will copy
|
||||
the MAC address that is present at that time. This means that the order of operations matters:
|
||||
1. Create the BondEthernet -- an ephemeral MAC is chosen
|
||||
1. Add the first member interface -- the MAC for the BondEthernet is changed to that of the member
|
||||
1. Create the Linux Controlplane Pair -- the resulting Linux interface will now have the same MAC
|
||||
as the dataplane BondEthernet device.
|
||||
|
||||
When using Linux Control Plane, you **MUST** set the intended MAC address at creation time.
|
||||
|
||||
Examples:
|
||||
```
|
||||
bondethernets:
|
||||
@@ -184,6 +196,7 @@ bondethernets:
|
||||
interfaces: [ GigabitEthernet1/0/0, GigabitEthernet1/0/1 ]
|
||||
mode: lacp
|
||||
load-balance: l2
|
||||
mac: '64:9d:99:b1:31:db'
|
||||
BondEthernet1:
|
||||
description: "Core: RR LAG"
|
||||
interfaces: [ GigabitEthernet3/0/0, GigabitEthernet3/0/1 ]
|
||||
@@ -522,7 +535,7 @@ be changed.
|
||||
|
||||
The following configuration elements are provided for the plugin:
|
||||
|
||||
* **sample-rate**: Capture 1-in-N packets. Defaults to 10000. A good value is the interface
|
||||
* **sampling-rate**: Capture 1-in-N packets. Defaults to 10000. A good value is the interface
|
||||
bitrate divided by 1000, so for GigabitEthernet choose 1000, for TenGigabitEthernet choose
|
||||
10000 (the default).
|
||||
* **polling-interval**: Determines the period of interface byte and packet counter reads. This
|
||||
@@ -532,7 +545,7 @@ The following configuration elements are provided for the plugin:
|
||||
|
||||
```
|
||||
sflow:
|
||||
sample-rate: 10000
|
||||
sampling-rate: 10000
|
||||
polling-interval: 20
|
||||
header-bytes: 128
|
||||
```
|
||||
|
||||
@@ -5,6 +5,29 @@ safely to a running VPP dataplane. It contains a strict syntax and semantic vali
|
||||
and a path planner that brings the dataplane from any configuration state safely to any
|
||||
other configuration state, as defined by these YAML files.
|
||||
|
||||
## Building
|
||||
|
||||
This program expects Python3 and PIP to be installed. It's known to work on Debian Bullseye,
|
||||
Bookworm and Trixie, as well as on Ubuntu Focal, Jammy and Noble.
|
||||
|
||||
Optionally, edit `vppcfg/_version.py` and bump the version number. Then:
|
||||
```
|
||||
## Start a Virtual Environment
|
||||
$ python3 -m venv .venv
|
||||
$ source .venv/bin/activate
|
||||
|
||||
$ make install-deps
|
||||
$ make test
|
||||
$ make build
|
||||
$ deactivate
|
||||
|
||||
## Publish requires a valid ~/.pypirc with an upload token.
|
||||
$ make publish
|
||||
|
||||
## Install the tool with PIP
|
||||
$ make install
|
||||
```
|
||||
|
||||
## User Guide
|
||||
|
||||
```
|
||||
@@ -230,7 +253,7 @@ $ vppcfg plan -c example.yaml -o example.exec
|
||||
[INFO ] vppcfg.config.valid_config: Configuration validated successfully
|
||||
[INFO ] root.main: Configuration is valid
|
||||
[INFO ] vppcfg.vppapi.connect: VPP version is 22.06-rc0~320-g8f60318ac
|
||||
[INFO ] vppcfg.reconciler.write: Wrote 78 lines to example.exec
|
||||
[INFO ] vppcfg.planner.write: Wrote 78 lines to example.exec
|
||||
[INFO ] root.main: Planning succeeded
|
||||
|
||||
$ vppctl exec ~/src/vppcfg/example.exec
|
||||
@@ -240,7 +263,7 @@ $ vppcfg plan -c example.yaml
|
||||
[INFO ] vppcfg.config.valid_config: Configuration validated successfully
|
||||
[INFO ] root.main: Configuration is valid
|
||||
[INFO ] vppcfg.vppapi.connect: VPP version is 22.06-rc0~320-g8f60318ac
|
||||
[INFO ] vppcfg.reconciler.write: Wrote 0 lines to (stdout)
|
||||
[INFO ] vppcfg.planner.write: Wrote 0 lines to (stdout)
|
||||
[INFO ] root.main: Planning succeeded
|
||||
```
|
||||
|
||||
|
||||
36
pyproject.toml
Normal file
36
pyproject.toml
Normal file
@@ -0,0 +1,36 @@
|
||||
[project]
|
||||
name = "vppcfg"
|
||||
dynamic = ["version"]
|
||||
dependencies = [
|
||||
"requests",
|
||||
"importlib-metadata; python_version >= '3.8'",
|
||||
"yamale",
|
||||
"netaddr",
|
||||
"vpp_papi",
|
||||
]
|
||||
authors = [
|
||||
{ name="Pim van Pelt", email="pimg@ipng.ch" }
|
||||
]
|
||||
description = "A configuration tool for Vector Packet Processing"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE"]
|
||||
|
||||
[project.scripts]
|
||||
vppcfg = "vppcfg.vppcfg:main"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://git.ipng.ch/ipng/vppcfg"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "vppcfg._version.__version__"}
|
||||
22
setup.py
22
setup.py
@@ -1,23 +1,15 @@
|
||||
"""vppcfg setuptools setup.py for pip and deb pkg installations"""
|
||||
|
||||
import os
|
||||
from setuptools import setup
|
||||
|
||||
# Read version from _version.py
|
||||
version_file = os.path.join(os.path.dirname(__file__), "vppcfg", "_version.py")
|
||||
with open(version_file) as f:
|
||||
exec(f.read())
|
||||
|
||||
setup(
|
||||
name="vppcfg",
|
||||
version="0.0.6",
|
||||
install_requires=[
|
||||
"requests",
|
||||
'importlib-metadata; python_version >= "3.8"',
|
||||
"yamale",
|
||||
"netaddr",
|
||||
"vpp_papi",
|
||||
],
|
||||
version=__version__,
|
||||
packages=["vppcfg", "vppcfg/config", "vppcfg/vpp"],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"vppcfg = vppcfg.vppcfg:main",
|
||||
]
|
||||
},
|
||||
test_suite="vppcfg.config",
|
||||
package_data={"vppcfg": ["*.yaml"]},
|
||||
)
|
||||
|
||||
3
vppcfg/_version.py
Normal file
3
vppcfg/_version.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Version information for vppcfg."""
|
||||
|
||||
__version__ = "1.0.5"
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2022 Pim van Pelt
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -40,6 +38,7 @@ from .vxlan_tunnel import validate_vxlan_tunnels
|
||||
from .tap import validate_taps
|
||||
from .prefixlist import validate_prefixlists
|
||||
from .acl import validate_acls
|
||||
from .sflow import validate_sflow
|
||||
|
||||
|
||||
class IPInterfaceWithPrefixLength(validators.Validator):
|
||||
@@ -94,6 +93,7 @@ class Validator:
|
||||
validate_taps,
|
||||
validate_prefixlists,
|
||||
validate_acls,
|
||||
validate_sflow,
|
||||
]
|
||||
|
||||
def validate(self, yaml):
|
||||
|
||||
30
vppcfg/config/sflow.py
Normal file
30
vppcfg/config/sflow.py
Normal file
@@ -0,0 +1,30 @@
|
||||
#
|
||||
# Copyright (c) 2024 Pim van Pelt
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
"""A vppcfg configuration module that validates sflow config"""
|
||||
import logging
|
||||
|
||||
|
||||
def validate_sflow(yaml):
|
||||
"""Validate the semantics of all YAML 'sflow' config entries"""
|
||||
result = True
|
||||
msgs = []
|
||||
logger = logging.getLogger("vppcfg.config")
|
||||
logger.addHandler(logging.NullHandler())
|
||||
|
||||
if not "sflow" in yaml:
|
||||
return result, msgs
|
||||
|
||||
## NOTE(pim): Nothing to validate. sflow config values are all
|
||||
## integers and enforced by yamale.
|
||||
return result, msgs
|
||||
@@ -130,28 +130,28 @@ class TestACLMethods(unittest.TestCase):
|
||||
for s in ["192.0.2.1", "192.0.2.1/24", "2001:db8::1", "2001:db8::1/64"]:
|
||||
l = acl.get_network_list(self.cfg, s)
|
||||
self.assertIsInstance(l, list)
|
||||
self.assertEquals(1, len(l))
|
||||
self.assertEqual(1, len(l))
|
||||
n = l[0]
|
||||
|
||||
l = acl.get_network_list(self.cfg, "trusted")
|
||||
self.assertIsInstance(l, list)
|
||||
self.assertEquals(5, len(l))
|
||||
self.assertEqual(5, len(l))
|
||||
|
||||
l = acl.get_network_list(self.cfg, "trusted", want_ipv6=False)
|
||||
self.assertIsInstance(l, list)
|
||||
self.assertEquals(2, len(l))
|
||||
self.assertEqual(2, len(l))
|
||||
|
||||
l = acl.get_network_list(self.cfg, "trusted", want_ipv4=False)
|
||||
self.assertIsInstance(l, list)
|
||||
self.assertEquals(3, len(l))
|
||||
self.assertEqual(3, len(l))
|
||||
|
||||
l = acl.get_network_list(self.cfg, "trusted", want_ipv4=False, want_ipv6=False)
|
||||
self.assertIsInstance(l, list)
|
||||
self.assertEquals(0, len(l))
|
||||
self.assertEqual(0, len(l))
|
||||
|
||||
l = acl.get_network_list(self.cfg, "pl-notexist")
|
||||
self.assertIsInstance(l, list)
|
||||
self.assertEquals(0, len(l))
|
||||
self.assertEqual(0, len(l))
|
||||
|
||||
def test_network_list_has_family(self):
|
||||
l = acl.get_network_list(self.cfg, "trusted")
|
||||
|
||||
@@ -79,22 +79,22 @@ class TestACLMethods(unittest.TestCase):
|
||||
def test_get_network_list(self):
|
||||
l = prefixlist.get_network_list(self.cfg, "trusted")
|
||||
self.assertIsInstance(l, list)
|
||||
self.assertEquals(5, len(l))
|
||||
self.assertEqual(5, len(l))
|
||||
|
||||
l = prefixlist.get_network_list(self.cfg, "trusted", want_ipv6=False)
|
||||
self.assertIsInstance(l, list)
|
||||
self.assertEquals(2, len(l))
|
||||
self.assertEqual(2, len(l))
|
||||
|
||||
l = prefixlist.get_network_list(self.cfg, "trusted", want_ipv4=False)
|
||||
self.assertIsInstance(l, list)
|
||||
self.assertEquals(3, len(l))
|
||||
self.assertEqual(3, len(l))
|
||||
|
||||
l = prefixlist.get_network_list(
|
||||
self.cfg, "trusted", want_ipv4=False, want_ipv6=False
|
||||
)
|
||||
self.assertIsInstance(l, list)
|
||||
self.assertEquals(0, len(l))
|
||||
self.assertEqual(0, len(l))
|
||||
|
||||
l = prefixlist.get_network_list(self.cfg, "pl-notexist")
|
||||
self.assertIsInstance(l, list)
|
||||
self.assertEquals(0, len(l))
|
||||
self.assertEqual(0, len(l))
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""module to help locate unittest resources"""
|
||||
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2022 Ray Kinsella
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -10,10 +10,12 @@ interfaces:
|
||||
device-type: dpdk
|
||||
mtu: 9000
|
||||
description: "LAG #1"
|
||||
sflow: true
|
||||
GigabitEthernet3/0/1:
|
||||
device-type: dpdk
|
||||
mtu: 9000
|
||||
description: "LAG #2"
|
||||
sflow: false
|
||||
|
||||
HundredGigabitEthernet12/0/0:
|
||||
device-type: dpdk
|
||||
@@ -163,3 +165,8 @@ acls:
|
||||
icmp-code: any
|
||||
- description: "Deny any IPv4 or IPv6"
|
||||
action: deny
|
||||
|
||||
sflow:
|
||||
header-bytes: 128
|
||||
polling-interval: 30
|
||||
sampling-rate: 1000
|
||||
|
||||
@@ -4,8 +4,9 @@ loopbacks: map(include('loopback'),key=str(matches='loop[0-9]+'),required=False)
|
||||
bridgedomains: map(include('bridgedomain'),key=str(matches='bd[0-9]+'),required=False)
|
||||
vxlan_tunnels: map(include('vxlan'),key=str(matches='vxlan_tunnel[0-9]+'),required=False)
|
||||
taps: map(include('tap'),key=str(matches='tap[0-9]+'),required=False)
|
||||
prefixlists: map(include('prefixlist'),key=str(matches='[a-z][a-z0-9\-]+',min=1,max=64),required=False)
|
||||
acls: map(include('acl'),key=str(matches='[a-z][a-z0-9\-]+',min=1,max=56),required=False)
|
||||
prefixlists: map(include('prefixlist'),key=str(matches='[a-z][a-z0-9-]+',min=1,max=64),required=False)
|
||||
acls: map(include('acl'),key=str(matches='[a-z][a-z0-9-]+',min=1,max=56),required=False)
|
||||
sflow: include('sflow',required=False)
|
||||
---
|
||||
vxlan:
|
||||
description: str(exclude='\'"',len=64,required=False)
|
||||
@@ -41,7 +42,7 @@ loopback:
|
||||
bondethernet:
|
||||
description: str(exclude='\'"',len=64,required=False)
|
||||
mac: mac(required=False)
|
||||
interfaces: list(str(matches='.*GigabitEthernet[0-9a-z]+/[0-9]+/[0-9]+'),required=False)
|
||||
interfaces: list(str(max=63),required=False)
|
||||
mode: enum('round-robin','active-backup','broadcast','lacp','xor',required=False)
|
||||
load-balance: enum('l2','l23','l34',required=False)
|
||||
---
|
||||
@@ -56,7 +57,8 @@ interface:
|
||||
l2xc: str(required=False)
|
||||
state: enum('up', 'down', required=False)
|
||||
mpls: bool(required=False)
|
||||
device-type: enum('dpdk', required=False)
|
||||
device-type: enum('dpdk', 'af-packet', required=False)
|
||||
sflow: bool(required=False)
|
||||
---
|
||||
sub-interface:
|
||||
description: str(exclude='\'"',len=64,required=False)
|
||||
@@ -113,3 +115,8 @@ acl-term:
|
||||
acl:
|
||||
description: str(exclude='\'"',len=64,required=False)
|
||||
terms: list(include('acl-term'), min=1, max=100, required=True)
|
||||
---
|
||||
sflow:
|
||||
header-bytes: int(min=1,max=256,required=False)
|
||||
polling-interval: int(min=5,max=600,required=False)
|
||||
sampling-rate: int(min=100,max=1000000,required=False)
|
||||
|
||||
@@ -174,9 +174,9 @@ if __name__ == "__main__":
|
||||
.wasSuccessful()
|
||||
)
|
||||
|
||||
RETVAL = 0
|
||||
retval = 0
|
||||
if not yaml_ok:
|
||||
RETVAL -= 1
|
||||
retval -= 1
|
||||
if not unit_ok:
|
||||
RETVAL -= 2
|
||||
sys.exit(RETVAL)
|
||||
retval -= 2
|
||||
sys.exit(retval)
|
||||
|
||||
@@ -8,9 +8,6 @@ test:
|
||||
interfaces:
|
||||
GigabitEthernet1/0/0:
|
||||
mtu: 3000
|
||||
GigabitEthernet1/0/1:
|
||||
mtu: 3000
|
||||
|
||||
GigabitEthernet1/0/1:
|
||||
mtu: 3000
|
||||
l2xc: GigabitEthernet1/0/0
|
||||
|
||||
@@ -47,7 +47,7 @@ interfaces:
|
||||
mtu: 3000
|
||||
description: "Cannot be unnumbered off of a bond-member"
|
||||
unnumbered: GigabitEthernet3/0/0
|
||||
GigabitEthernet4/0/0:
|
||||
GigabitEthernet4/0/1:
|
||||
mtu: 2000
|
||||
description: "Cannot be unnumbered off of an l2xc"
|
||||
unnumbered: GigabitEthernet3/0/2
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2022 Ray Kinsella
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -17,6 +17,7 @@ The functions in this file interact with the VPP API to modify certain
|
||||
interface metadata.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from .vppapi import VPPApi
|
||||
|
||||
|
||||
@@ -29,134 +30,44 @@ class Applier(VPPApi):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cfg,
|
||||
planner_cli,
|
||||
vpp_api_socket="/run/vpp/api.sock",
|
||||
vpp_json_dir=None,
|
||||
clientname="vppcfg",
|
||||
):
|
||||
VPPApi.__init__(self, vpp_api_socket, vpp_json_dir, clientname)
|
||||
self.logger.info("VPP Applier: changing the dataplane is enabled")
|
||||
super().__init__(vpp_api_socket, vpp_json_dir, clientname)
|
||||
self.logger = logging.getLogger("vppcfg.applier")
|
||||
self.logger.addHandler(logging.NullHandler())
|
||||
self.cli = planner_cli
|
||||
|
||||
def set_interface_ip_address(self, ifname, address, is_set=True):
|
||||
"""Add (if_set=True) or remove (if_set=False) an IPv4 or IPv6 address including
|
||||
prefixlen (ie 192.0.2.0/24 or 2001:db8::1/64) to an interface given by name
|
||||
(ie GigabitEthernet3/0/0)"""
|
||||
pass
|
||||
def apply(self):
|
||||
"""Apply the commands from self.cli to the cli_inband API call. Will eventually be
|
||||
replaced with actual API calls."""
|
||||
|
||||
def delete_loopback(self, ifname):
|
||||
"""Delete a loopback identified by name (ie loop0)"""
|
||||
pass
|
||||
cli_calls = 0
|
||||
cli_success = 0
|
||||
for phase, cmds in self.cli.items():
|
||||
for cmd in cmds:
|
||||
cli_calls += 1
|
||||
|
||||
def delete_subinterface(self, ifname):
|
||||
"""Delete a sub-int identified by name (ie GigabitEthernet3/0/0.100)"""
|
||||
pass
|
||||
self.logger.debug(f"{phase}: {cmd}")
|
||||
ret = self.cli_inband(cmd=cmd)
|
||||
self.logger.debug(f"Retval: {ret}")
|
||||
|
||||
def set_interface_l2_tag_rewrite(
|
||||
self, ifname, vtr_op, vtr_push_dot1q, vtr_tag1, vtr_tag2
|
||||
):
|
||||
"""Set l2 tag rewrite on an interface identified by name (ie GigabitEthernet3/0/0.100)
|
||||
into a certain operational mode. TODO(pim) clarify the vtr_* arguments."""
|
||||
## somewhere in interface.api see vtr_* fields
|
||||
pass
|
||||
if ret is False:
|
||||
self.logger.error("VPP returned error")
|
||||
elif ret.retval == 0:
|
||||
cli_success += 1
|
||||
else:
|
||||
self.logger.warning(f"VPP {cmd} returned {ret}")
|
||||
|
||||
def set_interface_l3(self, ifname):
|
||||
"""Set an interface or sub-interface identified by name (ie GigabitEthernet3/0/0)
|
||||
to L3 mode, removing it from bridges and l2xcs"""
|
||||
pass
|
||||
if cli_calls == 0:
|
||||
self.logger.info("Nothing to do")
|
||||
return True
|
||||
|
||||
def delete_bridgedomain(self, bd_id):
|
||||
"""Delete a bridgedomain given by instance bd_id (ie 100). Cannot delete instance==0."""
|
||||
pass
|
||||
|
||||
def delete_tap(self, ifname):
|
||||
"""Delete a tap identified by name (ie tap100)"""
|
||||
pass
|
||||
|
||||
def bond_remove_member(self, bondname, membername):
|
||||
"""Remove a member interface given by name (ie GigabitEthernet3/0/0) from a bondethernet
|
||||
interface given by name (ie BondEthernet0)"""
|
||||
pass
|
||||
|
||||
def delete_bond(self, ifname):
|
||||
"""Delete a bondethernet identified by name (ie BondEthernet0)"""
|
||||
pass
|
||||
|
||||
def create_vxlan_tunnel(self, instance, config, is_create=True):
|
||||
"""'config' is the YAML configuration for the vxlan_tunnels: entry"""
|
||||
pass
|
||||
|
||||
def set_interface_link_mtu(self, ifname, link_mtu):
|
||||
"""Set the max frame size of an interface given by name to the link_mtu value (typically
|
||||
1500, 9000, 9216"""
|
||||
|
||||
pass
|
||||
|
||||
def lcp_delete(self, lcpname):
|
||||
"""Delete a linux control plane interface pair by name (ie 'xe0' or 'be10')"""
|
||||
pass
|
||||
|
||||
def set_interface_packet_mtu(self, ifname, packet_mtu):
|
||||
"""Set the L3 MTU of an interface given by name (ie GigabitEthernet3/0/0)"""
|
||||
pass
|
||||
|
||||
def set_interface_state(self, ifname, state):
|
||||
"""Set the admin link state (True is up, False is down) of an interface given
|
||||
by name (ie GigabitEthernet3/0/0)"""
|
||||
pass
|
||||
|
||||
def create_loopback_interface(self, instance, config):
|
||||
"""'config' is the YAML configuration for the loopbacks: entry"""
|
||||
pass
|
||||
|
||||
def create_bond(self, instance, config):
|
||||
"""'config' is the YAML configuration for the bondethernets: entry"""
|
||||
pass
|
||||
|
||||
def create_subinterface(self, parent_ifname, sub_id, config):
|
||||
"""'config' is the YAML configuration for the sub-interfaces: entry"""
|
||||
pass
|
||||
|
||||
def create_tap(self, instance, config):
|
||||
"""'config' is the YAML configuration for the taps: entry"""
|
||||
pass
|
||||
|
||||
def create_bridgedomain(self, bd_id, config):
|
||||
"""'config' is the YAML configuration for the bridgedomains: entry"""
|
||||
pass
|
||||
|
||||
def lcp_create(self, ifname, host_if_name):
|
||||
"""Create a linux control plane interface pair for an interface given by name
|
||||
(ie GigabitEthernet3/0/0) under a Linux TAP device name host_if_name (ie e3-0-0)
|
||||
"""
|
||||
pass
|
||||
|
||||
def set_interface_mac(self, ifname, mac):
|
||||
"""Set the MAC address of interface given by name (ie GigabitEthernet3/0/0), the
|
||||
MAC is of form aa:bb:cc:dd:ee:ff"""
|
||||
pass
|
||||
|
||||
def bond_add_member(self, bondname, membername):
|
||||
"""Add a member interface given by name (ie GigabitEthernet3/0/0) to a bondethernet
|
||||
given by name (ie BondEthernet0)"""
|
||||
pass
|
||||
|
||||
def sync_bridgedomain(self, bd_id, config):
|
||||
"""'config' is the YAML configuration for the bridgedomains: entry"""
|
||||
pass
|
||||
|
||||
def set_interface_l2_bridge_bvi(self, bd_id, ifname):
|
||||
"""Set a loopback / BVI interface given by name (ie 'loop100') as a BVI of a bridge
|
||||
domain identified by bd_id (ie 100)"""
|
||||
pass
|
||||
|
||||
def set_interface_l2_bridge(self, bd_id, ifname):
|
||||
"""Set an interface given by name (ie 'GigabitEthernet3/0/0') into a bridge
|
||||
domain identified by bd_id (ie 100)"""
|
||||
pass
|
||||
|
||||
def set_interface_l2xc(self, rx_ifname, tx_ifname):
|
||||
"""Cross connect the rx_ifname (ie GigabitEthernet3/0/0) to emit into the tx_ifname
|
||||
(ie GigabitEthernet3/0/1). Note that this operation typically happens twice, once
|
||||
for the a->b crossconnect, and again for the b->a crossconnect. Note that
|
||||
crossconnecting sub-interfaces requires as well L2 rewriting (pop N for the amount
|
||||
of tags on the source interface)"""
|
||||
pass
|
||||
self.logger.info(f"VPP API calls: {cli_calls}, success: {cli_success}")
|
||||
if cli_calls != cli_success:
|
||||
self.logger.warning("Not all VPP calls were successful!")
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -41,18 +41,14 @@ class Dumper(VPPApi):
|
||||
|
||||
def write(self, outfile):
|
||||
"""Emit the configuration to either stdout (outfile=='-') or a filename"""
|
||||
config = self.cache_to_config()
|
||||
if outfile and outfile == "-":
|
||||
file = sys.stdout
|
||||
outfile = "(stdout)"
|
||||
else:
|
||||
file = open(outfile, "w", encoding="utf-8")
|
||||
|
||||
config = self.cache_to_config()
|
||||
|
||||
print(yaml.dump(config), file=file)
|
||||
|
||||
if file is not sys.stdout:
|
||||
file.close()
|
||||
else:
|
||||
with open(outfile, "w", encoding="utf-8") as file:
|
||||
print(yaml.dump(config), file=file)
|
||||
self.logger.info(f"Wrote YAML config to {outfile}")
|
||||
|
||||
def cache_to_config(self):
|
||||
@@ -67,6 +63,7 @@ class Dumper(VPPApi):
|
||||
"taps": {},
|
||||
"prefixlists": {},
|
||||
"acls": {},
|
||||
"sflow": {},
|
||||
}
|
||||
for idx, bond_iface in self.cache["bondethernets"].items():
|
||||
bond = {"description": ""}
|
||||
@@ -122,6 +119,7 @@ class Dumper(VPPApi):
|
||||
"bond",
|
||||
"VXLAN",
|
||||
"dpdk",
|
||||
"af-packet",
|
||||
"virtio",
|
||||
"pg",
|
||||
]:
|
||||
@@ -141,6 +139,8 @@ class Dumper(VPPApi):
|
||||
i["addresses"] = self.cache["interface_addresses"][
|
||||
iface.sw_if_index
|
||||
]
|
||||
if iface.sw_if_index in self.cache["interface_mpls"]:
|
||||
i["mpls"] = self.cache["interface_mpls"][iface.sw_if_index]
|
||||
if iface.sw_if_index in self.cache["l2xcs"]:
|
||||
l2xc = self.cache["l2xcs"][iface.sw_if_index]
|
||||
i["l2xc"] = self.cache["interfaces"][
|
||||
@@ -152,7 +152,7 @@ class Dumper(VPPApi):
|
||||
i["state"] = "down"
|
||||
|
||||
if (
|
||||
iface.interface_dev_type == "dpdk"
|
||||
iface.interface_dev_type in ["dpdk", "af-packet"]
|
||||
and iface.sub_number_of_tags == 0
|
||||
):
|
||||
i["mac"] = str(iface.l2_address)
|
||||
@@ -190,6 +190,10 @@ class Dumper(VPPApi):
|
||||
config["interfaces"][sup_iface.interface_name]["sub-interfaces"][
|
||||
iface.sub_id
|
||||
] = i
|
||||
if iface.interface_dev_type in ["dpdk", "af-packet"]:
|
||||
config["interfaces"][iface.interface_name][
|
||||
"device-type"
|
||||
] = iface.interface_dev_type
|
||||
|
||||
for idx, iface in self.cache["vxlan_tunnels"].items():
|
||||
vpp_iface = self.cache["interfaces"][iface.sw_if_index]
|
||||
@@ -353,4 +357,9 @@ class Dumper(VPPApi):
|
||||
|
||||
config["acls"][aclname] = config_acl
|
||||
|
||||
config["sflow"] = self.cache["sflow"]
|
||||
for hw_if_index in self.cache["interface_sflow"]:
|
||||
vpp_iface = self.cache["interfaces"][hw_if_index]
|
||||
config["interfaces"][vpp_iface.interface_name]["sflow"] = True
|
||||
|
||||
return config
|
||||
|
||||
124
vppcfg/vpp/planner.py
Normal file
124
vppcfg/vpp/planner.py
Normal file
@@ -0,0 +1,124 @@
|
||||
# Copyright (c) 2025 Pim van Pelt
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
The functions in this file interact with the VPP API to retrieve certain
|
||||
metadata, and plan configuration changes towards a given YAML target configuration.
|
||||
"""
|
||||
import sys
|
||||
import logging
|
||||
from vppcfg.config import interface
|
||||
from vppcfg.config import lcp
|
||||
from .vppapi import VPPApi
|
||||
from .planner_prune import PlannerPruneOperations
|
||||
from .planner_create import PlannerCreateOperations
|
||||
from .planner_sync import PlannerSyncOperations
|
||||
|
||||
|
||||
class Planner(PlannerPruneOperations, PlannerCreateOperations, PlannerSyncOperations):
|
||||
"""The Planner class first reads the running configuration of a VPP Dataplane,
|
||||
and based on an intended target YAML configuration file, plans a path to make the
|
||||
dataplane safely reflect the target config. It first prunes (removes) objects that
|
||||
are not meant to be in the dataplane, or are in the dataplane but are not of the
|
||||
correct create-time attributes; then it creates objects that are in the configuration
|
||||
but not yet in the dataplane; and finally it syncs the configuration attributes of
|
||||
objects that can be changed at runtime."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cfg,
|
||||
vpp_api_socket="/run/vpp/api.sock",
|
||||
vpp_json_dir=None,
|
||||
):
|
||||
super().__init__()
|
||||
self.logger = logging.getLogger("vppcfg.planner")
|
||||
self.logger.addHandler(logging.NullHandler())
|
||||
|
||||
self.vpp = VPPApi(vpp_api_socket, vpp_json_dir)
|
||||
self.cfg = cfg
|
||||
|
||||
## List of CLI calls emitted during the prune, create and sync phases.
|
||||
self.cli = {"prune": [], "create": [], "sync": []}
|
||||
|
||||
def __del__(self):
|
||||
self.vpp.disconnect()
|
||||
|
||||
def lcps_exist_with_lcp_enabled(self):
|
||||
"""Returns False if there are LCPs defined in the configuration, but LinuxCP
|
||||
functionality is not enabled in VPP."""
|
||||
if not lcp.get_lcps(self.cfg):
|
||||
return True
|
||||
return self.vpp.lcp_enabled
|
||||
|
||||
def phys_exist_in_vpp(self):
|
||||
"""Return True if all PHYs in the config exist as physical interface names
|
||||
in VPP. Return False otherwise."""
|
||||
|
||||
ret = True
|
||||
for ifname in interface.get_phys(self.cfg):
|
||||
if not ifname in self.vpp.cache["interface_names"]:
|
||||
self.logger.warning(f"Interface {ifname} does not exist in VPP")
|
||||
ret = False
|
||||
return ret
|
||||
|
||||
def phys_exist_in_config(self):
|
||||
"""Return True if all interfaces in VPP exist as physical interface names
|
||||
in the config. Return False otherwise."""
|
||||
|
||||
ret = True
|
||||
for ifname in self.vpp.get_phys():
|
||||
if not ifname in interface.get_interfaces(self.cfg):
|
||||
self.logger.warning(f"Interface {ifname} does not exist in the config")
|
||||
ret = False
|
||||
return ret
|
||||
|
||||
def write(self, outfile, emit_ok=False):
|
||||
"""Emit the CLI contents to stdout (if outfile=='-') or a named file otherwise.
|
||||
If the 'emit_ok' flag is False, emit a warning at the top and bottom of the file.
|
||||
"""
|
||||
# Assemble the intended output into a list
|
||||
output = []
|
||||
if not emit_ok:
|
||||
output.append(
|
||||
"comment { vppcfg: Planning failed, be careful with this output! }"
|
||||
)
|
||||
|
||||
for phase in ["prune", "create", "sync"]:
|
||||
ncount = len(self.cli[phase])
|
||||
if ncount > 0:
|
||||
output.append(
|
||||
f"comment {{ vppcfg {phase}: {ncount} CLI statement(s) follow }}"
|
||||
)
|
||||
output.extend(self.cli[phase])
|
||||
|
||||
if not emit_ok:
|
||||
output.append(
|
||||
"comment { vppcfg: Planning failed, be careful with this output! }"
|
||||
)
|
||||
|
||||
# Emit the output list to stdout or a file
|
||||
if outfile and outfile == "-":
|
||||
file = sys.stdout
|
||||
outfile = "(stdout)"
|
||||
if len(output) > 0:
|
||||
print("\n".join(output), file=file)
|
||||
else:
|
||||
with open(outfile, "w", encoding="utf-8") as file:
|
||||
if len(output) > 0:
|
||||
print("\n".join(output), file=file)
|
||||
|
||||
self.logger.info(f"Wrote {len(output)} lines to {outfile}")
|
||||
|
||||
def get_commands(self):
|
||||
"""Returns the CLI commands as a dictionary."""
|
||||
return self.cli
|
||||
30
vppcfg/vpp/planner_base.py
Normal file
30
vppcfg/vpp/planner_base.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Copyright (c) 2025 Pim van Pelt
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Base class defining the interface expected by planner mixins.
|
||||
"""
|
||||
|
||||
from abc import ABC
|
||||
from typing import Dict, Any, List
|
||||
|
||||
|
||||
class PlannerBase(ABC): # pylint: disable=too-few-public-methods
|
||||
"""Abstract base class defining the interface expected by planner mixins."""
|
||||
|
||||
def __init__(self):
|
||||
# These attributes will be set by the concrete Planner class
|
||||
self.cfg: Dict[str, Any]
|
||||
self.vpp: Any # VPPApi instance
|
||||
self.logger: Any # Logger instance
|
||||
self.cli: Dict[str, List[str]] # CLI commands grouped by operation type
|
||||
215
vppcfg/vpp/planner_create.py
Normal file
215
vppcfg/vpp/planner_create.py
Normal file
@@ -0,0 +1,215 @@
|
||||
# Copyright (c) 2025 Pim van Pelt
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Planner create operations - handles creation of objects in VPP that exist in config but not in VPP.
|
||||
"""
|
||||
|
||||
from vppcfg.config import loopback
|
||||
from vppcfg.config import interface
|
||||
from vppcfg.config import bondethernet
|
||||
from vppcfg.config import bridgedomain
|
||||
from vppcfg.config import vxlan_tunnel
|
||||
from vppcfg.config import tap
|
||||
from .planner_base import PlannerBase
|
||||
|
||||
|
||||
class PlannerCreateOperations(PlannerBase): # pylint: disable=too-few-public-methods
|
||||
"""Mixin class providing create operations for the Planner."""
|
||||
|
||||
def create(self):
|
||||
"""Create all objects in VPP that occur in the config but not in VPP. For an indepth
|
||||
explanation of how and why this particular creation order is chosen, see README.md
|
||||
section on Planning."""
|
||||
ret = True
|
||||
if not self._create_loopbacks():
|
||||
self.logger.warning("Could not create Loopbacks in VPP")
|
||||
ret = False
|
||||
if not self._create_bondethernets():
|
||||
self.logger.warning("Could not create BondEthernets in VPP")
|
||||
ret = False
|
||||
if not self._create_vxlan_tunnels():
|
||||
self.logger.warning("Could not create VXLAN Tunnels in VPP")
|
||||
ret = False
|
||||
if not self._create_taps():
|
||||
self.logger.warning("Could not create TAPs in VPP")
|
||||
ret = False
|
||||
if not self._create_sub_interfaces():
|
||||
self.logger.warning("Could not create Sub Interfaces in VPP")
|
||||
ret = False
|
||||
if not self._create_bridgedomains():
|
||||
self.logger.warning("Could not create BridgeDomains in VPP")
|
||||
ret = False
|
||||
if not self._create_lcps():
|
||||
self.logger.warning("Could not create LCPs in VPP")
|
||||
ret = False
|
||||
return ret
|
||||
|
||||
def _create_loopbacks(self):
|
||||
"""Create all loopbacks that occur in the config but not in VPP"""
|
||||
for ifname in loopback.get_loopbacks(self.cfg):
|
||||
if ifname in self.vpp.cache["interface_names"]:
|
||||
continue
|
||||
instance = int(ifname[4:])
|
||||
cli = f"create loopback interface instance {int(instance)}"
|
||||
ifname, iface = loopback.get_by_name(self.cfg, ifname)
|
||||
if "mac" in iface:
|
||||
cli += f" mac {iface['mac']}"
|
||||
self.cli["create"].append(cli)
|
||||
return True
|
||||
|
||||
def _create_bondethernets(self):
|
||||
"""Create all bondethernets that occur in the config but not in VPP"""
|
||||
for ifname in bondethernet.get_bondethernets(self.cfg):
|
||||
if ifname in self.vpp.cache["interface_names"]:
|
||||
continue
|
||||
ifname, iface = bondethernet.get_by_name(self.cfg, ifname)
|
||||
instance = int(ifname[12:])
|
||||
mode = bondethernet.get_mode(self.cfg, ifname)
|
||||
cli = f"create bond id {int(instance)} mode {mode}"
|
||||
loadbalance = bondethernet.get_lb(self.cfg, ifname)
|
||||
if loadbalance:
|
||||
cli += f" load-balance {loadbalance}"
|
||||
if "mac" in iface:
|
||||
cli += f" hw-addr {iface['mac']}"
|
||||
self.cli["create"].append(cli)
|
||||
return True
|
||||
|
||||
def _create_vxlan_tunnels(self):
|
||||
"""Create all vxlan_tunnels that occur in the config but not in VPP"""
|
||||
for ifname in vxlan_tunnel.get_vxlan_tunnels(self.cfg):
|
||||
if ifname in self.vpp.cache["interface_names"]:
|
||||
continue
|
||||
ifname, iface = vxlan_tunnel.get_by_name(self.cfg, ifname)
|
||||
instance = int(ifname[12:])
|
||||
cli = (
|
||||
f"create vxlan tunnel src {iface['local']} dst {iface['remote']} "
|
||||
f"instance {instance} vni {iface['vni']} decap-next l2"
|
||||
)
|
||||
self.cli["create"].append(cli)
|
||||
return True
|
||||
|
||||
def _create_sub_interfaces(self):
|
||||
"""Create all sub-interfaces that occur in the config but not in VPP"""
|
||||
## First create 1-tag (Dot1Q/Dot1AD), and then create 2-tag (Qin*) sub-interfaces
|
||||
for do_qinx in [False, True]:
|
||||
for ifname in interface.get_sub_interfaces(self.cfg):
|
||||
if not do_qinx == interface.is_qinx(self.cfg, ifname):
|
||||
continue
|
||||
|
||||
ifname, _iface = interface.get_by_name(self.cfg, ifname)
|
||||
if ifname in self.vpp.cache["interface_names"]:
|
||||
continue
|
||||
|
||||
## Assemble the encapsulation string
|
||||
encap = interface.get_encapsulation(self.cfg, ifname)
|
||||
if encap["dot1ad"] > 0:
|
||||
encapstr = f"dot1ad {int(encap['dot1ad'])}"
|
||||
else:
|
||||
encapstr = f"dot1q {int(encap['dot1q'])}"
|
||||
if do_qinx:
|
||||
encapstr += f" inner-dot1q {int(encap['inner-dot1q'])}"
|
||||
if encap["exact-match"]:
|
||||
encapstr += " exact-match"
|
||||
parent, subid = ifname.split(".")
|
||||
cli = f"create sub {parent} {int(int(subid))} {encapstr}"
|
||||
self.cli["create"].append(cli)
|
||||
return True
|
||||
|
||||
def _create_taps(self):
|
||||
"""Create all taps that occur in the config but not in VPP"""
|
||||
for ifname in tap.get_taps(self.cfg):
|
||||
ifname, iface = tap.get_by_name(self.cfg, ifname)
|
||||
if ifname in self.vpp.cache["interface_names"]:
|
||||
continue
|
||||
instance = int(ifname[3:])
|
||||
cli = f"create tap id {int(instance)} host-if-name {iface['host']['name']}"
|
||||
if "mac" in iface["host"]:
|
||||
cli += f" host-mac-addr {iface['host']['mac']}"
|
||||
if "namespace" in iface["host"]:
|
||||
cli += f" host-ns {int(iface['host']['namespace'])}"
|
||||
if "bridge" in iface["host"]:
|
||||
cli += f" host-bridge {iface['host']['bridge']}"
|
||||
if "mtu" in iface["host"]:
|
||||
cli += f" host-mtu-size {int(iface['host']['mtu'])}"
|
||||
if "rx-ring-size" in iface:
|
||||
cli += f" rx-ring-size {int(iface['rx-ring-size'])}"
|
||||
if "tx-ring-size" in iface:
|
||||
cli += f" tx-ring-size {int(iface['tx-ring-size'])}"
|
||||
self.cli["create"].append(cli)
|
||||
|
||||
return True
|
||||
|
||||
def _create_bridgedomains(self):
|
||||
"""Create all bridgedomains that occur in the config but not in VPP"""
|
||||
for ifname in bridgedomain.get_bridgedomains(self.cfg):
|
||||
ifname, _iface = bridgedomain.get_by_name(self.cfg, ifname)
|
||||
instance = int(ifname[2:])
|
||||
settings = bridgedomain.get_settings(self.cfg, ifname)
|
||||
if instance in self.vpp.cache["bridgedomains"]:
|
||||
continue
|
||||
cli = f"create bridge-domain {instance}"
|
||||
if not settings["learn"]:
|
||||
cli += " learn 0"
|
||||
if not settings["unicast-flood"]:
|
||||
cli += " flood 0"
|
||||
if not settings["unknown-unicast-flood"]:
|
||||
cli += " uu-flood 0"
|
||||
if not settings["unicast-forward"]:
|
||||
cli += " forward 0"
|
||||
if settings["arp-termination"]:
|
||||
cli += " arp-term 1"
|
||||
if settings["arp-unicast-forward"]:
|
||||
cli += " arp-ufwd 1"
|
||||
if settings["mac-age-minutes"] > 0:
|
||||
cli += f" mac-age {int(settings['mac-age-minutes'])}"
|
||||
self.cli["create"].append(cli)
|
||||
return True
|
||||
|
||||
def _create_lcps(self):
|
||||
"""Create all LCPs that occur in the config but not in VPP"""
|
||||
lcpnames = [
|
||||
self.vpp.cache["lcps"][x].host_if_name for x in self.vpp.cache["lcps"]
|
||||
]
|
||||
|
||||
## First create untagged ...
|
||||
for ifname in loopback.get_loopbacks(self.cfg) + interface.get_interfaces(
|
||||
self.cfg
|
||||
):
|
||||
if interface.is_sub(self.cfg, ifname):
|
||||
continue
|
||||
|
||||
if ifname.startswith("loop"):
|
||||
ifname, iface = loopback.get_by_name(self.cfg, ifname)
|
||||
else:
|
||||
ifname, iface = interface.get_by_name(self.cfg, ifname)
|
||||
if not "lcp" in iface:
|
||||
continue
|
||||
if iface["lcp"] in lcpnames:
|
||||
continue
|
||||
cli = f"lcp create {ifname} host-if {iface['lcp']}"
|
||||
self.cli["create"].append(cli)
|
||||
|
||||
## ... then 1-tag (Dot1Q/Dot1AD), and then create 2-tag (Qin*) LCPs
|
||||
for do_qinx in [False, True]:
|
||||
for ifname in interface.get_sub_interfaces(self.cfg):
|
||||
if not do_qinx == interface.is_qinx(self.cfg, ifname):
|
||||
continue
|
||||
ifname, iface = interface.get_by_name(self.cfg, ifname)
|
||||
if not "lcp" in iface:
|
||||
continue
|
||||
if iface["lcp"] in lcpnames:
|
||||
continue
|
||||
cli = f"lcp create {ifname} host-if {iface['lcp']}"
|
||||
self.cli["create"].append(cli)
|
||||
return True
|
||||
693
vppcfg/vpp/planner_prune.py
Normal file
693
vppcfg/vpp/planner_prune.py
Normal file
@@ -0,0 +1,693 @@
|
||||
# Copyright (c) 2025 Pim van Pelt
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Planner prune operations - handles removal of objects from VPP that don't exist in config.
|
||||
"""
|
||||
|
||||
from vppcfg.config import loopback
|
||||
from vppcfg.config import interface
|
||||
from vppcfg.config import bondethernet
|
||||
from vppcfg.config import bridgedomain
|
||||
from vppcfg.config import vxlan_tunnel
|
||||
from vppcfg.config import tap
|
||||
from .planner_base import PlannerBase
|
||||
|
||||
|
||||
class PlannerPruneOperations(PlannerBase): # pylint: disable=too-few-public-methods
|
||||
"""Mixin class providing prune operations for the Planner."""
|
||||
|
||||
def prune(self):
|
||||
"""Remove all objects from VPP that do not occur in the config. For an indepth explanation
|
||||
of how and why this particular pruning order is chosen, see README.md section on
|
||||
Planning."""
|
||||
ret = True
|
||||
if not self._prune_admin_state():
|
||||
self.logger.warning("Could not set interfaces down in VPP")
|
||||
ret = False
|
||||
if not self._prune_lcps():
|
||||
self.logger.warning("Could not prune LCPs from VPP")
|
||||
ret = False
|
||||
if not self._prune_bridgedomains():
|
||||
self.logger.warning("Could not prune BridgeDomains from VPP")
|
||||
ret = False
|
||||
if not self._prune_loopbacks():
|
||||
self.logger.warning("Could not prune Loopbacks from VPP")
|
||||
ret = False
|
||||
if not self._prune_l2xcs():
|
||||
self.logger.warning("Could not prune L2 Cross Connects from VPP")
|
||||
ret = False
|
||||
if not self._prune_sub_interfaces():
|
||||
self.logger.warning("Could not prune Sub Interfaces from VPP")
|
||||
ret = False
|
||||
if not self._prune_taps():
|
||||
self.logger.warning("Could not prune TAPs from VPP")
|
||||
ret = False
|
||||
if not self._prune_vxlan_tunnels():
|
||||
self.logger.warning("Could not prune VXLAN Tunnels from VPP")
|
||||
ret = False
|
||||
if not self._prune_bondethernets():
|
||||
self.logger.warning("Could not prune BondEthernets from VPP")
|
||||
ret = False
|
||||
if not self._prune_phys():
|
||||
self.logger.warning("Could not prune PHYs from VPP")
|
||||
ret = False
|
||||
return ret
|
||||
|
||||
def _prune_unnumbered_usage(self, target_ifname):
|
||||
"""Remove the unnumbered use of all VPP interfaces that are using the given 'target_ifname'."""
|
||||
target_iface = self.vpp.get_interface_by_name(target_ifname)
|
||||
|
||||
for idx, target_idx in self.vpp.cache["interface_unnumbered"].items():
|
||||
if target_idx == target_iface.sw_if_index:
|
||||
unnumbered_ifname = self.vpp.cache["interfaces"][idx].interface_name
|
||||
cli = f"set interface unnumbered del {unnumbered_ifname}"
|
||||
self.cli["prune"].append(cli)
|
||||
return True
|
||||
|
||||
def _prune_addresses(self, ifname, address_list):
|
||||
"""Remove all addresses from interface ifname, except those in address_list,
|
||||
which may be an empty list, in which case all addresses are removed.
|
||||
"""
|
||||
_iface = self.vpp.get_interface_by_name(ifname)
|
||||
if not _iface:
|
||||
self.logger.error(
|
||||
f"Trying to prune addresses from non-existent interface {ifname}"
|
||||
)
|
||||
return
|
||||
|
||||
idx = _iface.sw_if_index
|
||||
removed_addresses = []
|
||||
for addr in self.vpp.cache["interface_addresses"][idx]:
|
||||
if not addr in address_list:
|
||||
cli = f"set interface ip address del {ifname} {addr}"
|
||||
self.cli["prune"].append(cli)
|
||||
removed_addresses.append(addr)
|
||||
else:
|
||||
self.logger.debug(f"Address OK: {ifname} {addr}")
|
||||
for addr in removed_addresses:
|
||||
self.vpp.cache["interface_addresses"][idx].remove(addr)
|
||||
|
||||
def _prune_loopbacks(self):
|
||||
"""Remove loopbacks from VPP, if they do not occur in the config."""
|
||||
removed_interfaces = []
|
||||
for numtags in [2, 1, 0]:
|
||||
for _idx, vpp_iface in self.vpp.cache["interfaces"].items():
|
||||
if vpp_iface.interface_dev_type != "Loopback":
|
||||
continue
|
||||
if vpp_iface.sub_number_of_tags != numtags:
|
||||
continue
|
||||
_config_ifname, config_iface = loopback.get_by_name(
|
||||
self.cfg, vpp_iface.interface_name
|
||||
)
|
||||
if not config_iface:
|
||||
self._prune_addresses(vpp_iface.interface_name, [])
|
||||
self._prune_unnumbered_usage(vpp_iface.interface_name)
|
||||
if numtags == 0:
|
||||
cli = f"delete loopback interface intfc {vpp_iface.interface_name}"
|
||||
self.cli["prune"].append(cli)
|
||||
removed_interfaces.append(vpp_iface.interface_name)
|
||||
else:
|
||||
cli = f"delete sub {vpp_iface.interface_name}"
|
||||
self.cli["prune"].append(cli)
|
||||
removed_interfaces.append(vpp_iface.interface_name)
|
||||
continue
|
||||
self.logger.debug(f"Loopback OK: {vpp_iface.interface_name}")
|
||||
addresses = []
|
||||
if "addresses" in config_iface:
|
||||
addresses = config_iface["addresses"]
|
||||
self._prune_addresses(vpp_iface.interface_name, addresses)
|
||||
|
||||
for ifname in removed_interfaces:
|
||||
self.vpp.cache_remove_interface(ifname)
|
||||
|
||||
return True
|
||||
|
||||
def _prune_bridgedomains(self):
|
||||
"""Remove bridge-domains from VPP, if they do not occur in the config. If any interfaces are
|
||||
found in to-be removed bridge-domains, they are returned to L3 mode, and tag-rewrites removed.
|
||||
"""
|
||||
for idx, bridge in self.vpp.cache["bridgedomains"].items():
|
||||
bridgename = f"bd{int(idx)}"
|
||||
_config_ifname, config_iface = bridgedomain.get_by_name(
|
||||
self.cfg, bridgename
|
||||
)
|
||||
if not config_iface:
|
||||
for member in bridge.sw_if_details:
|
||||
if member.sw_if_index == bridge.bvi_sw_if_index:
|
||||
continue
|
||||
member_iface = self.vpp.cache["interfaces"][member.sw_if_index]
|
||||
member_ifname = member_iface.interface_name
|
||||
if member_iface.sub_id > 0:
|
||||
cli = f"set interface l2 tag-rewrite {member_ifname} disable"
|
||||
self.cli["prune"].append(cli)
|
||||
cli = f"set interface l3 {member_ifname}"
|
||||
self.cli["prune"].append(cli)
|
||||
if bridge.bvi_sw_if_index in self.vpp.cache["interfaces"]:
|
||||
bviname = self.vpp.cache["interfaces"][
|
||||
bridge.bvi_sw_if_index
|
||||
].interface_name
|
||||
cli = f"set interface l3 {bviname}"
|
||||
self.cli["prune"].append(cli)
|
||||
cli = f"create bridge-domain {int(idx)} del"
|
||||
self.cli["prune"].append(cli)
|
||||
else:
|
||||
self.logger.debug(f"BridgeDomain OK: {bridgename}")
|
||||
for member in bridge.sw_if_details:
|
||||
member_ifname = self.vpp.cache["interfaces"][
|
||||
member.sw_if_index
|
||||
].interface_name
|
||||
if (
|
||||
"members" in config_iface
|
||||
and member_ifname in config_iface["members"]
|
||||
):
|
||||
if interface.is_sub(self.cfg, member_ifname):
|
||||
cli = (
|
||||
f"set interface l2 tag-rewrite {member_ifname} disable"
|
||||
)
|
||||
self.cli["prune"].append(cli)
|
||||
cli = f"set interface l3 {member_ifname}"
|
||||
self.cli["prune"].append(cli)
|
||||
if (
|
||||
"bvi" in config_iface
|
||||
and bridge.bvi_sw_if_index in self.vpp.cache["interfaces"]
|
||||
):
|
||||
bviname = self.vpp.cache["interfaces"][
|
||||
bridge.bvi_sw_if_index
|
||||
].interface_name
|
||||
if bviname != config_iface["bvi"]:
|
||||
cli = f"set interface l3 {bviname}"
|
||||
self.cli["prune"].append(cli)
|
||||
|
||||
return True
|
||||
|
||||
def _prune_l2xcs(self):
|
||||
"""Remove all L2XC source interfaces from VPP, if they do not occur in the config. If they occur,
|
||||
but are crossconnected to a different interface name, also remove them. Interfaces are put
|
||||
back into L3 mode, and their tag-rewrites removed."""
|
||||
removed_l2xcs = []
|
||||
for _idx, l2xc in self.vpp.cache["l2xcs"].items():
|
||||
vpp_rx_ifname = self.vpp.cache["interfaces"][
|
||||
l2xc.rx_sw_if_index
|
||||
].interface_name
|
||||
config_rx_ifname, config_rx_iface = interface.get_by_name(
|
||||
self.cfg, vpp_rx_ifname
|
||||
)
|
||||
if not config_rx_ifname:
|
||||
if self.vpp.cache["interfaces"][l2xc.rx_sw_if_index].sub_id > 0:
|
||||
cli = f"set interface l2 tag-rewrite {vpp_rx_ifname} disable"
|
||||
self.cli["prune"].append(cli)
|
||||
cli = f"set interface l3 {vpp_rx_ifname}"
|
||||
self.cli["prune"].append(cli)
|
||||
removed_l2xcs.append(vpp_rx_ifname)
|
||||
continue
|
||||
|
||||
if not interface.is_l2xc_interface(self.cfg, config_rx_ifname):
|
||||
if interface.is_sub(self.cfg, config_rx_ifname):
|
||||
cli = f"set interface l2 tag-rewrite {vpp_rx_ifname} disable"
|
||||
self.cli["prune"].append(cli)
|
||||
cli = f"set interface l3 {vpp_rx_ifname}"
|
||||
self.cli["prune"].append(cli)
|
||||
removed_l2xcs.append(vpp_rx_ifname)
|
||||
continue
|
||||
vpp_tx_ifname = self.vpp.cache["interfaces"][
|
||||
l2xc.tx_sw_if_index
|
||||
].interface_name
|
||||
if vpp_tx_ifname != config_rx_iface["l2xc"]:
|
||||
if interface.is_sub(self.cfg, config_rx_ifname):
|
||||
cli = f"set interface l2 tag-rewrite {vpp_rx_ifname} disable"
|
||||
self.cli["prune"].append(cli)
|
||||
cli = f"set interface l3 {vpp_rx_ifname}"
|
||||
self.cli["prune"].append(cli)
|
||||
removed_l2xcs.append(vpp_rx_ifname)
|
||||
continue
|
||||
self.logger.debug(f"L2XC OK: {vpp_rx_ifname} -> {vpp_tx_ifname}")
|
||||
for l2xc in removed_l2xcs:
|
||||
self.vpp.cache_remove_l2xc(l2xc)
|
||||
return True
|
||||
|
||||
def _vxlan_tunnel_has_diff(self, ifname):
|
||||
"""Returns True if the given ifname (vxlan_tunnel0) has different attributes between VPP
|
||||
and the given configuration, or if either does not exist.
|
||||
|
||||
Returns False if they are identical."""
|
||||
|
||||
vpp_iface = self.vpp.get_interface_by_name(ifname)
|
||||
if (
|
||||
not vpp_iface
|
||||
or vpp_iface.sw_if_index not in self.vpp.cache["vxlan_tunnels"]
|
||||
):
|
||||
return True
|
||||
vpp_vxlan = self.vpp.cache["vxlan_tunnels"][vpp_iface.sw_if_index]
|
||||
|
||||
_config_ifname, config_iface = vxlan_tunnel.get_by_name(self.cfg, ifname)
|
||||
if not config_iface:
|
||||
return True
|
||||
|
||||
if config_iface["local"] != str(vpp_vxlan.src_address):
|
||||
return True
|
||||
if config_iface["remote"] != str(vpp_vxlan.dst_address):
|
||||
return True
|
||||
if config_iface["vni"] != vpp_vxlan.vni:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _tap_has_diff(self, ifname): # pylint: disable=too-many-return-statements
|
||||
"""Returns True if the given ifname (tap0) has different attributes between VPP
|
||||
and the given configuration, or if either does not exist.
|
||||
|
||||
Returns False if the TAP is a Linux Control Plane LIP.
|
||||
Returns False if they are identical."""
|
||||
|
||||
vpp_iface = self.vpp.get_interface_by_name(ifname)
|
||||
vpp_tap = self.vpp.cache["taps"][vpp_iface.sw_if_index]
|
||||
if not vpp_iface:
|
||||
return True
|
||||
|
||||
_config_ifname, config_iface = tap.get_by_name(self.cfg, ifname)
|
||||
if not config_iface:
|
||||
return True
|
||||
|
||||
if self.vpp.tap_is_lcp(ifname):
|
||||
return False
|
||||
|
||||
if (
|
||||
"name" in config_iface["host"]
|
||||
and config_iface["host"]["name"] != vpp_tap.host_if_name
|
||||
):
|
||||
return True
|
||||
if (
|
||||
"mtu" in config_iface["host"]
|
||||
and config_iface["host"]["mtu"] != vpp_tap.host_mtu_size
|
||||
):
|
||||
return True
|
||||
if "mac" in config_iface["host"] and config_iface["host"]["mac"] != str(
|
||||
vpp_tap.host_mac_addr
|
||||
):
|
||||
return True
|
||||
if (
|
||||
"bridge" in config_iface["host"]
|
||||
and config_iface["host"]["bridge"] != vpp_tap.host_bridge
|
||||
):
|
||||
return True
|
||||
if (
|
||||
"namespace" in config_iface["host"]
|
||||
and config_iface["host"]["namespace"] != vpp_tap.host_namespace
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _bond_has_diff(self, ifname):
|
||||
"""Returns True if the given ifname (BondEthernet0) have different attributes,
|
||||
or if either does not exist.
|
||||
|
||||
Returns False if they are identical.
|
||||
"""
|
||||
|
||||
vpp_iface = self.vpp.get_interface_by_name(ifname)
|
||||
if (
|
||||
not vpp_iface
|
||||
or not vpp_iface.sw_if_index in self.vpp.cache["bondethernets"]
|
||||
):
|
||||
return True
|
||||
|
||||
config_ifname, config_iface = bondethernet.get_by_name(self.cfg, ifname)
|
||||
if not config_iface:
|
||||
return True
|
||||
|
||||
vpp_bond = self.vpp.cache["bondethernets"][vpp_iface.sw_if_index]
|
||||
mode = bondethernet.mode_to_int(bondethernet.get_mode(self.cfg, config_ifname))
|
||||
if mode not in (-1, vpp_bond.mode):
|
||||
return True
|
||||
loadbalance = bondethernet.lb_to_int(
|
||||
bondethernet.get_lb(self.cfg, config_ifname)
|
||||
)
|
||||
if loadbalance not in (-1, vpp_bond.lb):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _prune_taps(self):
|
||||
"""Remove all TAPs from VPP, if they are not in the config. As an exception,
|
||||
TAPs which are a part of Linux Control Plane, are left alone, to be handled
|
||||
by _prune_lcps() later."""
|
||||
removed_taps = []
|
||||
for _idx, vpp_tap in self.vpp.cache["taps"].items():
|
||||
vpp_iface = self.vpp.cache["interfaces"][vpp_tap.sw_if_index]
|
||||
vpp_ifname = vpp_iface.interface_name
|
||||
if self.vpp.tap_is_lcp(vpp_ifname):
|
||||
continue
|
||||
if self._tap_has_diff(vpp_ifname):
|
||||
removed_taps.append(vpp_ifname)
|
||||
continue
|
||||
|
||||
for ifname in removed_taps:
|
||||
cli = f"delete tap {ifname}"
|
||||
self.cli["prune"].append(cli)
|
||||
self.vpp.cache_remove_interface(ifname)
|
||||
return True
|
||||
|
||||
def _prune_bondethernets(self):
|
||||
"""Remove all BondEthernets from VPP, if they are not in the config. If the bond has members,
|
||||
remove those from the bond before removing the bond."""
|
||||
removed_interfaces = []
|
||||
removed_bondethernet_members = []
|
||||
for idx, bond in self.vpp.cache["bondethernets"].items():
|
||||
vpp_ifname = bond.interface_name
|
||||
_config_ifname, config_iface = bondethernet.get_by_name(
|
||||
self.cfg, vpp_ifname
|
||||
)
|
||||
|
||||
if self._bond_has_diff(vpp_ifname):
|
||||
self._prune_addresses(vpp_ifname, [])
|
||||
self._prune_unnumbered_usage(vpp_ifname)
|
||||
for member in self.vpp.cache["bondethernet_members"][idx]:
|
||||
member_ifname = self.vpp.cache["interfaces"][member].interface_name
|
||||
cli = f"bond del {member_ifname}"
|
||||
self.cli["prune"].append(cli)
|
||||
removed_bondethernet_members.append(member_ifname)
|
||||
cli = f"delete bond {vpp_ifname}"
|
||||
self.cli["prune"].append(cli)
|
||||
removed_interfaces.append(vpp_ifname)
|
||||
continue
|
||||
|
||||
for member in self.vpp.cache["bondethernet_members"][idx]:
|
||||
member_ifname = self.vpp.cache["interfaces"][member].interface_name
|
||||
if (
|
||||
"interfaces" in config_iface
|
||||
and not member_ifname in config_iface["interfaces"]
|
||||
):
|
||||
cli = f"bond del {member_ifname}"
|
||||
self.cli["prune"].append(cli)
|
||||
removed_bondethernet_members.append(member_ifname)
|
||||
addresses = []
|
||||
if "addresses" in config_iface:
|
||||
addresses = config_iface["addresses"]
|
||||
self._prune_addresses(vpp_ifname, addresses)
|
||||
self.logger.debug(f"BondEthernet OK: {vpp_ifname}")
|
||||
|
||||
for ifname in removed_bondethernet_members:
|
||||
self.vpp.cache_remove_bondethernet_member(ifname)
|
||||
|
||||
for ifname in removed_interfaces:
|
||||
self.vpp.cache_remove_interface(ifname)
|
||||
|
||||
return True
|
||||
|
||||
def _prune_vxlan_tunnels(self):
|
||||
"""Remove all VXLAN Tunnels from VPP, if they are not in the config. If they are in the config
|
||||
but with differing attributes, remove them also."""
|
||||
removed_interfaces = []
|
||||
for idx, vpp_vxlan in self.vpp.cache["vxlan_tunnels"].items():
|
||||
vpp_ifname = self.vpp.cache["interfaces"][idx].interface_name
|
||||
config_ifname, config_iface = vxlan_tunnel.get_by_name(self.cfg, vpp_ifname)
|
||||
if not config_iface or self._vxlan_tunnel_has_diff(config_ifname):
|
||||
self._prune_addresses(vpp_ifname, [])
|
||||
cli = (
|
||||
f"create vxlan tunnel instance {vpp_vxlan.instance} "
|
||||
f"src {vpp_vxlan.src_address} dst {vpp_vxlan.dst_address} vni {vpp_vxlan.vni} del"
|
||||
)
|
||||
self.cli["prune"].append(cli)
|
||||
removed_interfaces.append(vpp_ifname)
|
||||
continue
|
||||
config_ifname, config_iface = interface.get_by_name(self.cfg, vpp_ifname)
|
||||
if config_iface:
|
||||
addresses = []
|
||||
if "addresses" in config_iface:
|
||||
addresses = config_iface["addresses"]
|
||||
self._prune_addresses(vpp_ifname, addresses)
|
||||
self.logger.debug(f"VXLAN Tunnel OK: {vpp_ifname}")
|
||||
|
||||
for ifname in removed_interfaces:
|
||||
self.vpp.cache_remove_vxlan_tunnel(ifname)
|
||||
self.vpp.cache_remove_interface(ifname)
|
||||
|
||||
return True
|
||||
|
||||
def _prune_sub_interfaces(self):
|
||||
"""Remove interfaces from VPP if they are not in the config, if their encapsulation is different,
|
||||
or if the BondEthernet they reside on is different.
|
||||
Start with inner-most (QinQ/QinAD), then Dot1Q/Dot1AD."""
|
||||
removed_interfaces = []
|
||||
for numtags in [2, 1]:
|
||||
for vpp_ifname in self.vpp.get_sub_interfaces():
|
||||
vpp_iface = self.vpp.get_interface_by_name(vpp_ifname)
|
||||
if not vpp_iface or vpp_iface.sub_number_of_tags != numtags:
|
||||
continue
|
||||
|
||||
if self.vpp.tap_is_lcp(vpp_ifname):
|
||||
continue
|
||||
|
||||
prune = False
|
||||
_config_ifname, config_iface = interface.get_by_name(
|
||||
self.cfg, vpp_ifname
|
||||
)
|
||||
if not config_iface:
|
||||
prune = True
|
||||
elif (
|
||||
vpp_iface.interface_dev_type == "bond"
|
||||
and vpp_iface.sub_number_of_tags > 0
|
||||
):
|
||||
(
|
||||
config_parent_ifname,
|
||||
_config_parent_iface,
|
||||
) = interface.get_parent_by_name(self.cfg, vpp_ifname)
|
||||
if self._bond_has_diff(config_parent_ifname):
|
||||
prune = True
|
||||
|
||||
config_encap = interface.get_encapsulation(self.cfg, vpp_ifname)
|
||||
vpp_encap = self._get_encapsulation(vpp_iface)
|
||||
if config_encap != vpp_encap:
|
||||
prune = True
|
||||
|
||||
if prune:
|
||||
self._prune_addresses(vpp_ifname, [])
|
||||
self._prune_unnumbered_usage(vpp_ifname)
|
||||
cli = f"delete sub {vpp_ifname}"
|
||||
self.cli["prune"].append(cli)
|
||||
removed_interfaces.append(vpp_ifname)
|
||||
continue
|
||||
|
||||
addresses = []
|
||||
if "addresses" in config_iface:
|
||||
addresses = config_iface["addresses"]
|
||||
self._prune_addresses(vpp_ifname, addresses)
|
||||
self.logger.debug(f"Sub Interface OK: {vpp_ifname}")
|
||||
|
||||
for ifname in removed_interfaces:
|
||||
self.vpp.cache_remove_interface(ifname)
|
||||
|
||||
return True
|
||||
|
||||
def _prune_phys(self):
|
||||
"""Set default MTU and remove IPs for PHYs that are not in the config."""
|
||||
for vpp_ifname in self.vpp.get_phys():
|
||||
vpp_iface = self.vpp.get_interface_by_name(vpp_ifname)
|
||||
if not vpp_iface:
|
||||
continue
|
||||
|
||||
_config_ifname, config_iface = interface.get_by_name(self.cfg, vpp_ifname)
|
||||
if not config_iface:
|
||||
## Interfaces were sent DOWN in the _prune_admin_state() step previously
|
||||
self._prune_addresses(vpp_ifname, [])
|
||||
if vpp_iface.link_mtu != 9000:
|
||||
cli = f"set interface mtu 9000 {vpp_ifname}"
|
||||
self.cli["prune"].append(cli)
|
||||
continue
|
||||
addresses = []
|
||||
if "addresses" in config_iface:
|
||||
addresses = config_iface["addresses"]
|
||||
self._prune_addresses(vpp_ifname, addresses)
|
||||
self.logger.debug(f"Interface OK: {vpp_ifname}")
|
||||
return True
|
||||
|
||||
def _parent_iface_by_encap(self, sup_sw_if_index, outer, dot1ad=True):
|
||||
"""Returns the sw_if_index of an interface on a given super_sw_if_index with given dot1q/dot1ad outer and inner-dot1q=0,
|
||||
in other words the intermediary Dot1Q/Dot1AD belonging to a QinX interface. If the interface doesn't exist, None is
|
||||
returned."""
|
||||
for idx, iface in self.vpp.cache["interfaces"].items():
|
||||
if iface.sup_sw_if_index != sup_sw_if_index:
|
||||
continue
|
||||
if iface.sub_inner_vlan_id > 0:
|
||||
continue
|
||||
if dot1ad and (iface.sub_if_flags & 8) and iface.sub_outer_vlan_id == outer:
|
||||
self.logger.debug(f"match: {iface.interface_name} (dot1ad)")
|
||||
return idx
|
||||
if (
|
||||
not dot1ad
|
||||
and not (iface.sub_if_flags & 8)
|
||||
and iface.sub_outer_vlan_id == outer
|
||||
):
|
||||
self.logger.debug(f"match: {iface.interface_name} (dot1q)")
|
||||
return idx
|
||||
return None
|
||||
|
||||
def _get_encapsulation(self, iface):
|
||||
"""Return a dictionary-based encapsulation of the sub-interface, which helps comparing them to the same object
|
||||
returned by config.interface.get_encapsulation()."""
|
||||
if iface.sub_if_flags & 8:
|
||||
dot1ad = iface.sub_outer_vlan_id
|
||||
dot1q = 0
|
||||
else:
|
||||
dot1q = iface.sub_outer_vlan_id
|
||||
dot1ad = 0
|
||||
inner_dot1q = iface.sub_inner_vlan_id
|
||||
exact_match = iface.sub_if_flags & 16
|
||||
return {
|
||||
"dot1q": int(dot1q),
|
||||
"dot1ad": int(dot1ad),
|
||||
"inner-dot1q": int(inner_dot1q),
|
||||
"exact-match": bool(exact_match),
|
||||
}
|
||||
|
||||
def _prune_lcps(self):
|
||||
"""Remove LCPs which are not in the configuration, starting with QinQ/QinAD interfaces, then Dot1Q/Dot1AD,
|
||||
and finally PHYs/BondEthernets/Tunnels/Loopbacks. For QinX, special care is taken to ensure that
|
||||
their intermediary interface exists, and has the correct encalsulation. If the intermediary interface
|
||||
changed, the QinX LCP is removed. The same is true for Dot1Q/Dot1AD interfaces: if their encapsulation
|
||||
has changed, we will have to re-create the underlying sub-interface, so the LCP has to be removed.
|
||||
|
||||
Order is important: destroying an LCP of a PHY will invalidate its Dot1Q/Dot1AD as well as their
|
||||
downstream children in Linux.
|
||||
"""
|
||||
lcps = self.vpp.cache["lcps"]
|
||||
|
||||
removed_lcps = []
|
||||
for numtags in [2, 1, 0]:
|
||||
for _idx, lcp_iface in lcps.items():
|
||||
vpp_iface = self.vpp.cache["interfaces"][lcp_iface.phy_sw_if_index]
|
||||
if vpp_iface.sub_number_of_tags != numtags:
|
||||
continue
|
||||
if vpp_iface.interface_dev_type == "Loopback":
|
||||
config_ifname, config_iface = loopback.get_by_lcp_name(
|
||||
self.cfg, lcp_iface.host_if_name
|
||||
)
|
||||
else:
|
||||
config_ifname, config_iface = interface.get_by_lcp_name(
|
||||
self.cfg, lcp_iface.host_if_name
|
||||
)
|
||||
if not config_iface:
|
||||
## Interface doesn't exist in the config
|
||||
removed_lcps.append(lcp_iface)
|
||||
continue
|
||||
if not "lcp" in config_iface:
|
||||
## Interface doesn't have an LCP
|
||||
removed_lcps.append(lcp_iface)
|
||||
continue
|
||||
if vpp_iface.sub_number_of_tags == 2:
|
||||
vpp_parent_idx = self._parent_iface_by_encap(
|
||||
vpp_iface.sup_sw_if_index,
|
||||
vpp_iface.sub_outer_vlan_id,
|
||||
vpp_iface.sub_if_flags & 8,
|
||||
)
|
||||
vpp_parent_iface = self.vpp.cache["interfaces"][vpp_parent_idx]
|
||||
parent_lcp = lcps[vpp_parent_iface.sw_if_index]
|
||||
(
|
||||
config_parent_ifname,
|
||||
config_parent_iface,
|
||||
) = interface.get_by_lcp_name(self.cfg, parent_lcp.host_if_name)
|
||||
if not config_parent_iface:
|
||||
## QinX's parent doesn't exist in the config
|
||||
removed_lcps.append(lcp_iface)
|
||||
continue
|
||||
if not "lcp" in config_parent_iface:
|
||||
## QinX's parent doesn't have an LCP
|
||||
removed_lcps.append(lcp_iface)
|
||||
continue
|
||||
if parent_lcp.host_if_name != config_parent_iface["lcp"]:
|
||||
## QinX's parent LCP name mismatch
|
||||
removed_lcps.append(lcp_iface)
|
||||
continue
|
||||
config_parent_encap = interface.get_encapsulation(
|
||||
self.cfg, config_parent_ifname
|
||||
)
|
||||
vpp_parent_encap = self._get_encapsulation(vpp_parent_iface)
|
||||
if config_parent_encap != vpp_parent_encap:
|
||||
## QinX's parent encapsulation mismatch
|
||||
removed_lcps.append(lcp_iface)
|
||||
continue
|
||||
|
||||
if vpp_iface.sub_number_of_tags > 0:
|
||||
config_encap = interface.get_encapsulation(self.cfg, config_ifname)
|
||||
vpp_encap = self._get_encapsulation(vpp_iface)
|
||||
if config_encap != vpp_encap:
|
||||
## Encapsulation mismatch
|
||||
removed_lcps.append(lcp_iface)
|
||||
continue
|
||||
|
||||
if vpp_iface.interface_dev_type == "Loopback":
|
||||
## Loopbacks will not have a PHY to check.
|
||||
continue
|
||||
if vpp_iface.interface_dev_type == "bond":
|
||||
bond_iface = self.vpp.cache["interfaces"][vpp_iface.sup_sw_if_index]
|
||||
if self._bond_has_diff(bond_iface.interface_name):
|
||||
## If BondEthernet changed, it has to be re-created, so all LCPs must be removed.
|
||||
removed_lcps.append(lcp_iface)
|
||||
continue
|
||||
|
||||
phy_lcp = lcps[vpp_iface.sup_sw_if_index]
|
||||
_config_phy_ifname, config_phy_iface = interface.get_by_lcp_name(
|
||||
self.cfg, phy_lcp.host_if_name
|
||||
)
|
||||
if not config_phy_iface:
|
||||
## Phy doesn't exist in the config
|
||||
removed_lcps.append(lcp_iface)
|
||||
continue
|
||||
if not "lcp" in config_phy_iface:
|
||||
## Phy doesn't have an LCP
|
||||
removed_lcps.append(lcp_iface)
|
||||
continue
|
||||
if phy_lcp.host_if_name != config_phy_iface["lcp"]:
|
||||
## Phy LCP name mismatch
|
||||
removed_lcps.append(lcp_iface)
|
||||
continue
|
||||
|
||||
self.logger.debug(
|
||||
f"LCP OK: {lcp_iface.host_if_name} -> (vpp={vpp_iface.interface_name}, config={config_ifname})"
|
||||
)
|
||||
|
||||
for lcp_iface in removed_lcps:
|
||||
vpp_ifname = self.vpp.cache["interfaces"][
|
||||
lcp_iface.phy_sw_if_index
|
||||
].interface_name
|
||||
cli = f"lcp delete {vpp_ifname}"
|
||||
self.cli["prune"].append(cli)
|
||||
self.vpp.cache_remove_lcp(lcp_iface.host_if_name)
|
||||
return True
|
||||
|
||||
def _prune_admin_state(self):
|
||||
"""Set admin-state down for all interfaces that are not in the config."""
|
||||
for ifname in (
|
||||
self.vpp.get_qinx_interfaces()
|
||||
+ self.vpp.get_dot1x_interfaces()
|
||||
+ self.vpp.get_bondethernets()
|
||||
+ self.vpp.get_phys()
|
||||
+ self.vpp.get_vxlan_tunnels()
|
||||
+ self.vpp.get_loopbacks()
|
||||
):
|
||||
if not ifname in interface.get_interfaces(
|
||||
self.cfg
|
||||
) + loopback.get_loopbacks(self.cfg):
|
||||
vpp_iface = self.vpp.get_interface_by_name(ifname)
|
||||
if not vpp_iface:
|
||||
continue
|
||||
|
||||
if self.vpp.tap_is_lcp(ifname):
|
||||
continue
|
||||
|
||||
if vpp_iface.flags & 1: # IF_STATUS_API_FLAG_ADMIN_UP
|
||||
cli = f"set interface state {ifname} down"
|
||||
self.cli["prune"].append(cli)
|
||||
|
||||
return True
|
||||
591
vppcfg/vpp/planner_sync.py
Normal file
591
vppcfg/vpp/planner_sync.py
Normal file
@@ -0,0 +1,591 @@
|
||||
# Copyright (c) 2025 Pim van Pelt
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Planner sync operations - handles synchronization of VPP dataplane configuration.
|
||||
"""
|
||||
|
||||
from vppcfg.config import loopback
|
||||
from vppcfg.config import interface
|
||||
from vppcfg.config import bondethernet
|
||||
from vppcfg.config import bridgedomain
|
||||
from .planner_base import PlannerBase
|
||||
|
||||
|
||||
class PlannerSyncOperations(PlannerBase): # pylint: disable=too-few-public-methods
|
||||
"""Mixin class providing sync operations for the Planner."""
|
||||
|
||||
def sync(self):
|
||||
"""Synchronize the VPP Dataplane configuration for all objects in the config"""
|
||||
ret = True
|
||||
if not self._sync_loopbacks():
|
||||
self.logger.warning("Could not sync Loopbacks in VPP")
|
||||
ret = False
|
||||
if not self._sync_bondethernets():
|
||||
self.logger.warning("Could not sync bondethernets in VPP")
|
||||
ret = False
|
||||
if not self._sync_bridgedomains():
|
||||
self.logger.warning("Could not sync bridgedomains in VPP")
|
||||
ret = False
|
||||
if not self._sync_l2xcs():
|
||||
self.logger.warning("Could not sync L2 Cross Connects in VPP")
|
||||
ret = False
|
||||
if not self._sync_mtu():
|
||||
self.logger.warning("Could not sync interface MTU in VPP")
|
||||
ret = False
|
||||
if not self._sync_addresses():
|
||||
self.logger.warning("Could not sync interface addresses in VPP")
|
||||
ret = False
|
||||
if not self._sync_unnumbered():
|
||||
self.logger.warning("Could not sync unnumbered interfaces in VPP")
|
||||
ret = False
|
||||
if not self._sync_phys():
|
||||
self.logger.warning("Could not sync PHYs in VPP")
|
||||
ret = False
|
||||
if not self._sync_mpls_state():
|
||||
self.logger.warning("Could not sync interface MPLS state in VPP")
|
||||
ret = False
|
||||
if not self._sync_sflow_state():
|
||||
self.logger.warning("Could not sync interface sFlow state in VPP")
|
||||
ret = False
|
||||
if not self._sync_admin_state():
|
||||
self.logger.warning("Could not sync interface adminstate in VPP")
|
||||
ret = False
|
||||
return ret
|
||||
|
||||
def _sync_loopbacks(self):
|
||||
"""Synchronize the VPP Dataplane configuration for loopbacks"""
|
||||
for ifname in loopback.get_loopbacks(self.cfg):
|
||||
if not ifname in self.vpp.cache["interface_names"]:
|
||||
## New loopback
|
||||
continue
|
||||
vpp_iface = self.vpp.get_interface_by_name(ifname)
|
||||
if not vpp_iface:
|
||||
continue
|
||||
|
||||
config_ifname, config_iface = loopback.get_by_name(self.cfg, ifname)
|
||||
if "mac" in config_iface and config_iface["mac"] != str(
|
||||
vpp_iface.l2_address
|
||||
):
|
||||
cli = f"set interface mac address {config_ifname} {config_iface['mac']}"
|
||||
self.cli["sync"].append(cli)
|
||||
return True
|
||||
|
||||
def _sync_phys(self):
|
||||
"""Synchronize the VPP Dataplane configuration for PHYs"""
|
||||
for ifname in interface.get_phys(self.cfg):
|
||||
if not ifname in self.vpp.cache["interface_names"]:
|
||||
## New interface
|
||||
continue
|
||||
vpp_iface = self.vpp.get_interface_by_name(ifname)
|
||||
if not vpp_iface:
|
||||
continue
|
||||
|
||||
config_ifname, config_iface = interface.get_by_name(self.cfg, ifname)
|
||||
if "mac" in config_iface and config_iface["mac"] != str(
|
||||
vpp_iface.l2_address
|
||||
):
|
||||
cli = f"set interface mac address {config_ifname} {config_iface['mac']}"
|
||||
self.cli["sync"].append(cli)
|
||||
return True
|
||||
|
||||
def _sync_bondethernets(self):
|
||||
"""Synchronize the VPP Dataplane configuration for bondethernets"""
|
||||
for ifname in bondethernet.get_bondethernets(self.cfg):
|
||||
vpp_iface = self.vpp.get_interface_by_name(ifname)
|
||||
if vpp_iface:
|
||||
vpp_members = [
|
||||
self.vpp.cache["interfaces"][x].interface_name
|
||||
for x in self.vpp.cache["bondethernet_members"][
|
||||
vpp_iface.sw_if_index
|
||||
]
|
||||
]
|
||||
else:
|
||||
## New BondEthernet
|
||||
vpp_members = []
|
||||
|
||||
config_bond_ifname, config_bond_iface = bondethernet.get_by_name(
|
||||
self.cfg, ifname
|
||||
)
|
||||
if not "interfaces" in config_bond_iface:
|
||||
continue
|
||||
config_ifname, config_iface = interface.get_by_name(self.cfg, ifname)
|
||||
bondmac = None
|
||||
for member_ifname in sorted(config_bond_iface["interfaces"]):
|
||||
member_ifname, member_iface = interface.get_by_name(
|
||||
self.cfg, member_ifname
|
||||
)
|
||||
member_iface = self.vpp.get_interface_by_name(member_ifname)
|
||||
if not member_iface or member_ifname not in vpp_members:
|
||||
if (
|
||||
len(vpp_members) == 0
|
||||
and member_iface
|
||||
and member_iface.l2_address != "00:00:00:00:00:00"
|
||||
):
|
||||
bondmac = member_iface.l2_address
|
||||
cli = f"bond add {config_bond_ifname} {member_ifname}"
|
||||
self.cli["sync"].append(cli)
|
||||
if (
|
||||
vpp_iface
|
||||
and "mac" in config_iface
|
||||
and str(vpp_iface.l2_address) != config_iface["mac"]
|
||||
):
|
||||
cli = f"set interface mac address {config_ifname} {config_iface['mac']}"
|
||||
self.cli["sync"].append(cli)
|
||||
elif bondmac and "lcp" in config_iface:
|
||||
## TODO(pim) - Ensure LCP has the same MAC as the BondEthernet
|
||||
## VPP, when creating a BondEthernet, will give it an ephemeral MAC. Then, when the
|
||||
## first member is enslaved, the MAC address changes to that of the first member.
|
||||
## However, LinuxCP does not propagate this change to the Linux side (because there
|
||||
## is no API callback for MAC address changes). To ensure consistency, every time we
|
||||
## sync members, we ought to ensure the Linux device has the same MAC as its BondEthernet.
|
||||
cli = (
|
||||
f"comment {{ ip link set {config_iface['lcp']} address {bondmac} }}"
|
||||
)
|
||||
self.cli["sync"].append(cli)
|
||||
return True
|
||||
|
||||
def _sync_bridgedomains(self):
|
||||
"""Synchronize the VPP Dataplane configuration for bridgedomains"""
|
||||
for ifname in bridgedomain.get_bridgedomains(self.cfg):
|
||||
instance = int(ifname[2:])
|
||||
if instance in self.vpp.cache["bridgedomains"]:
|
||||
vpp_bridge = self.vpp.cache["bridgedomains"][instance]
|
||||
bvi_sw_if_index = vpp_bridge.bvi_sw_if_index
|
||||
bridge_sw_if_index_list = [
|
||||
x.sw_if_index for x in vpp_bridge.sw_if_details
|
||||
]
|
||||
bridge_members = [
|
||||
self.vpp.cache["interfaces"][x].interface_name
|
||||
for x in bridge_sw_if_index_list
|
||||
if x in self.vpp.cache["interfaces"]
|
||||
]
|
||||
else:
|
||||
## New BridgeDomain
|
||||
vpp_bridge = None
|
||||
bvi_sw_if_index = -1
|
||||
bridge_members = []
|
||||
|
||||
config_bridge_ifname, config_bridge_iface = bridgedomain.get_by_name(
|
||||
self.cfg, f"bd{int(instance)}"
|
||||
)
|
||||
if vpp_bridge:
|
||||
# Sync settings on existing bridge. _create_bridgedomain() will have set them for new bridges.
|
||||
settings = bridgedomain.get_settings(self.cfg, config_bridge_ifname)
|
||||
if settings["learn"] != vpp_bridge.learn:
|
||||
cli = f"set bridge-domain learn {int(instance)}"
|
||||
if not settings["learn"]:
|
||||
cli += " disable"
|
||||
self.cli["sync"].append(cli)
|
||||
if settings["unicast-forward"] != vpp_bridge.forward:
|
||||
cli = f"set bridge-domain forward {int(instance)}"
|
||||
if not settings["unicast-forward"]:
|
||||
cli += " disable"
|
||||
self.cli["sync"].append(cli)
|
||||
if settings["unicast-flood"] != vpp_bridge.flood:
|
||||
cli = f"set bridge-domain flood {int(instance)}"
|
||||
if not settings["unicast-flood"]:
|
||||
cli += " disable"
|
||||
self.cli["sync"].append(cli)
|
||||
if settings["unknown-unicast-flood"] != vpp_bridge.uu_flood:
|
||||
cli = f"set bridge-domain uu-flood {int(instance)}"
|
||||
if not settings["unknown-unicast-flood"]:
|
||||
cli += " disable"
|
||||
self.cli["sync"].append(cli)
|
||||
if settings["arp-termination"] != vpp_bridge.arp_term:
|
||||
cli = f"set bridge-domain arp term {int(instance)}"
|
||||
if not settings["arp-termination"]:
|
||||
cli += " disable"
|
||||
self.cli["sync"].append(cli)
|
||||
if settings["arp-unicast-forward"] != vpp_bridge.arp_ufwd:
|
||||
cli = f"set bridge-domain arp-ufwd {int(instance)}"
|
||||
if not settings["arp-unicast-forward"]:
|
||||
cli += " disable"
|
||||
self.cli["sync"].append(cli)
|
||||
if settings["mac-age-minutes"] != vpp_bridge.mac_age:
|
||||
cli = f"set bridge-domain mac-age {int(instance)} {int(settings['mac-age-minutes'])}"
|
||||
self.cli["sync"].append(cli)
|
||||
|
||||
if "bvi" in config_bridge_iface:
|
||||
bviname = config_bridge_iface["bvi"]
|
||||
bvi_iface = self.vpp.get_interface_by_name(bviname)
|
||||
if not bvi_iface or bvi_iface.sw_if_index != bvi_sw_if_index:
|
||||
cli = f"set interface l2 bridge {bviname} {int(instance)} bvi"
|
||||
self.cli["sync"].append(cli)
|
||||
|
||||
if "interfaces" in config_bridge_iface:
|
||||
for member_ifname in config_bridge_iface["interfaces"]:
|
||||
member_ifname, _member_iface = interface.get_by_name(
|
||||
self.cfg, member_ifname
|
||||
)
|
||||
if not member_ifname in bridge_members:
|
||||
cli = f"set interface l2 bridge {member_ifname} {int(instance)}"
|
||||
self.cli["sync"].append(cli)
|
||||
operation = "disable"
|
||||
if interface.is_qinx(self.cfg, member_ifname):
|
||||
operation = "pop 2"
|
||||
elif interface.is_sub(self.cfg, member_ifname):
|
||||
operation = "pop 1"
|
||||
cli = (
|
||||
f"set interface l2 tag-rewrite {member_ifname} {operation}"
|
||||
)
|
||||
self.cli["sync"].append(cli)
|
||||
return True
|
||||
|
||||
def _sync_l2xcs(self):
|
||||
"""Synchronize the VPP Dataplane configuration for L2 cross connects"""
|
||||
for ifname in interface.get_l2xc_interfaces(self.cfg):
|
||||
config_rx_ifname, config_rx_iface = interface.get_by_name(self.cfg, ifname)
|
||||
config_tx_ifname, _config_tx_iface = interface.get_by_name(
|
||||
self.cfg, config_rx_iface["l2xc"]
|
||||
)
|
||||
vpp_rx_iface = self.vpp.get_interface_by_name(config_rx_ifname)
|
||||
vpp_tx_iface = self.vpp.get_interface_by_name(config_tx_ifname)
|
||||
|
||||
l2xc_changed = False
|
||||
if not vpp_rx_iface or not vpp_tx_iface:
|
||||
l2xc_changed = True
|
||||
elif not vpp_rx_iface.sw_if_index in self.vpp.cache["l2xcs"]:
|
||||
l2xc_changed = True
|
||||
elif (
|
||||
not vpp_tx_iface.sw_if_index
|
||||
== self.vpp.cache["l2xcs"][vpp_rx_iface.sw_if_index].tx_sw_if_index
|
||||
):
|
||||
l2xc_changed = True
|
||||
|
||||
if l2xc_changed:
|
||||
cli = f"set interface l2 xconnect {config_rx_ifname} {config_tx_ifname}"
|
||||
self.cli["sync"].append(cli)
|
||||
|
||||
operation = "disable"
|
||||
if interface.is_qinx(self.cfg, config_rx_ifname):
|
||||
operation = "pop 2"
|
||||
elif interface.is_sub(self.cfg, config_rx_ifname):
|
||||
operation = "pop 1"
|
||||
cli = f"set interface l2 tag-rewrite {config_rx_ifname} {operation}"
|
||||
self.cli["sync"].append(cli)
|
||||
return True
|
||||
|
||||
def _sync_mtu_direction(self, shrink=True):
|
||||
"""Synchronize the VPP Dataplane packet MTU, where 'shrink' determines the
|
||||
direction (if shrink is True, go from inner-most (QinQ) to outer-most (untagged),
|
||||
and the other direction if shrink is False"""
|
||||
if shrink:
|
||||
tag_list = [2, 1, 0]
|
||||
else:
|
||||
tag_list = [0, 1, 2]
|
||||
|
||||
for numtags in tag_list:
|
||||
for ifname in loopback.get_loopbacks(self.cfg) + interface.get_interfaces(
|
||||
self.cfg
|
||||
):
|
||||
if numtags == 0 and interface.is_sub(self.cfg, ifname):
|
||||
continue
|
||||
if numtags == 1 and not interface.is_sub(self.cfg, ifname):
|
||||
continue
|
||||
if numtags == 1 and interface.is_qinx(self.cfg, ifname):
|
||||
continue
|
||||
if numtags == 2 and not interface.is_qinx(self.cfg, ifname):
|
||||
continue
|
||||
config_mtu = 1500
|
||||
vpp_mtu = 9000
|
||||
if ifname.startswith("loop"):
|
||||
_iface = self.vpp.get_interface_by_name(ifname)
|
||||
if _iface:
|
||||
vpp_mtu = _iface.mtu[0]
|
||||
vpp_ifname, config_iface = loopback.get_by_name(self.cfg, ifname)
|
||||
if "mtu" in config_iface:
|
||||
config_mtu = config_iface["mtu"]
|
||||
else:
|
||||
if numtags > 0:
|
||||
vpp_mtu = 0
|
||||
_iface = self.vpp.get_interface_by_name(ifname)
|
||||
if _iface:
|
||||
vpp_mtu = _iface.mtu[0]
|
||||
vpp_ifname, config_iface = interface.get_by_name(self.cfg, ifname)
|
||||
config_mtu = interface.get_mtu(self.cfg, ifname)
|
||||
|
||||
if shrink and config_mtu < vpp_mtu:
|
||||
cli = f"set interface mtu packet {int(config_mtu)} {vpp_ifname}"
|
||||
self.cli["sync"].append(cli)
|
||||
elif not shrink and config_mtu > vpp_mtu:
|
||||
cli = f"set interface mtu packet {int(config_mtu)} {vpp_ifname}"
|
||||
self.cli["sync"].append(cli)
|
||||
return True
|
||||
|
||||
def _sync_link_mtu_direction(self, shrink=True):
|
||||
"""Synchronize the VPP Dataplane max frame size (link MTU), where 'shrink' determines the
|
||||
direction (if shrink is True, go from inner-most (QinQ) to outer-most (untagged),
|
||||
and the other direction if shrink is False"""
|
||||
for _idx, vpp_iface in self.vpp.cache["interfaces"].items():
|
||||
if vpp_iface.sub_number_of_tags != 0:
|
||||
continue
|
||||
if vpp_iface.interface_dev_type in ["local", "Loopback", "VXLAN", "virtio"]:
|
||||
continue
|
||||
|
||||
_config_ifname, config_iface = interface.get_by_name(
|
||||
self.cfg, vpp_iface.interface_name
|
||||
)
|
||||
if not config_iface:
|
||||
self.logger.warning(
|
||||
f"Interface {vpp_iface.interface_name} exists in VPP but not in config, this is dangerous"
|
||||
)
|
||||
continue
|
||||
if not interface.is_phy(self.cfg, vpp_iface.interface_name):
|
||||
continue
|
||||
config_mtu = interface.get_mtu(self.cfg, vpp_iface.interface_name)
|
||||
|
||||
if (
|
||||
vpp_iface.interface_dev_type == "bond"
|
||||
and vpp_iface.link_mtu < config_mtu
|
||||
):
|
||||
self.logger.warning(
|
||||
f"{vpp_iface.interface_name} has a Max Frame Size ({vpp_iface.link_mtu}) "
|
||||
"lower than desired MTU ({config_mtu}), this is unsupported"
|
||||
)
|
||||
continue
|
||||
|
||||
if shrink and config_mtu < vpp_iface.link_mtu:
|
||||
## If the interface is up, temporarily down it in order to change the Max Frame Size
|
||||
if vpp_iface.flags & 1: # IF_STATUS_API_FLAG_ADMIN_UP
|
||||
cli = f"set interface state {vpp_iface.interface_name} down"
|
||||
self.cli["sync"].append(cli)
|
||||
|
||||
cli = f"set interface mtu {int(config_mtu)} {vpp_iface.interface_name}"
|
||||
self.cli["sync"].append(cli)
|
||||
|
||||
if vpp_iface.flags & 1: # IF_STATUS_API_FLAG_ADMIN_UP
|
||||
cli = f"set interface state {vpp_iface.interface_name} up"
|
||||
self.cli["sync"].append(cli)
|
||||
elif not shrink and config_mtu > vpp_iface.link_mtu:
|
||||
## If the interface is up, temporarily down it in order to change the Max Frame Size
|
||||
if vpp_iface.flags & 1: # IF_STATUS_API_FLAG_ADMIN_UP
|
||||
cli = f"set interface state {vpp_iface.interface_name} down"
|
||||
self.cli["sync"].append(cli)
|
||||
|
||||
cli = f"set interface mtu {int(config_mtu)} {vpp_iface.interface_name}"
|
||||
self.cli["sync"].append(cli)
|
||||
|
||||
if vpp_iface.flags & 1: # IF_STATUS_API_FLAG_ADMIN_UP
|
||||
cli = f"set interface state {vpp_iface.interface_name} up"
|
||||
self.cli["sync"].append(cli)
|
||||
return True
|
||||
|
||||
def _sync_mtu(self):
|
||||
"""Synchronize the VPP Dataplane configuration for interface MTU"""
|
||||
ret = True
|
||||
if not self._sync_link_mtu_direction(shrink=False):
|
||||
self.logger.warning(
|
||||
"Could not sync growing interface Max Frame Size in VPP"
|
||||
)
|
||||
ret = False
|
||||
if not self._sync_link_mtu_direction(shrink=True):
|
||||
self.logger.warning(
|
||||
"Could not sync shrinking interface Max Frame Size in VPP"
|
||||
)
|
||||
ret = False
|
||||
if not self._sync_mtu_direction(shrink=True):
|
||||
self.logger.warning("Could not sync shrinking interface MTU in VPP")
|
||||
ret = False
|
||||
if not self._sync_mtu_direction(shrink=False):
|
||||
self.logger.warning("Could not sync growing interface MTU in VPP")
|
||||
ret = False
|
||||
return ret
|
||||
|
||||
def _sync_sflow_state(self):
|
||||
"""Synchronize the VPP Dataplane configuration and phy sFlow state"""
|
||||
|
||||
if "sflow" in self.cfg and self.vpp.cache["sflow"]:
|
||||
if "header-bytes" in self.cfg["sflow"]:
|
||||
if (
|
||||
self.vpp.cache["sflow"]["header-bytes"]
|
||||
!= self.cfg["sflow"]["header-bytes"]
|
||||
):
|
||||
cli = f"sflow header-bytes {self.cfg['sflow']['header-bytes']}"
|
||||
self.cli["sync"].append(cli)
|
||||
if "polling-interval" in self.cfg["sflow"]:
|
||||
if (
|
||||
self.vpp.cache["sflow"]["polling-interval"]
|
||||
!= self.cfg["sflow"]["polling-interval"]
|
||||
):
|
||||
cli = f"sflow polling-interval {self.cfg['sflow']['polling-interval']}"
|
||||
self.cli["sync"].append(cli)
|
||||
if "sampling-rate" in self.cfg["sflow"]:
|
||||
if (
|
||||
self.vpp.cache["sflow"]["sampling-rate"]
|
||||
!= self.cfg["sflow"]["sampling-rate"]
|
||||
):
|
||||
cli = f"sflow sampling-rate {self.cfg['sflow']['sampling-rate']}"
|
||||
self.cli["sync"].append(cli)
|
||||
|
||||
for ifname in interface.get_interfaces(self.cfg):
|
||||
vpp_ifname, config_iface = interface.get_by_name(self.cfg, ifname)
|
||||
|
||||
try:
|
||||
config_sflow = config_iface["sflow"]
|
||||
except KeyError:
|
||||
config_sflow = False
|
||||
|
||||
vpp_sflow = False
|
||||
if vpp_ifname in self.vpp.cache["interface_names"]:
|
||||
hw_if_index = self.vpp.cache["interface_names"][vpp_ifname]
|
||||
try:
|
||||
vpp_sflow = self.vpp.cache["interface_sflow"][hw_if_index]
|
||||
except KeyError:
|
||||
pass
|
||||
if vpp_sflow != config_sflow:
|
||||
if config_sflow:
|
||||
cli = f"sflow enable {vpp_ifname}"
|
||||
else:
|
||||
cli = f"sflow enable-disable {vpp_ifname} disable"
|
||||
self.cli["sync"].append(cli)
|
||||
return True
|
||||
|
||||
def _sync_mpls_state(self):
|
||||
"""Synchronize the VPP Dataplane configuration for interface and loopback MPLS state"""
|
||||
for ifname in loopback.get_loopbacks(self.cfg) + interface.get_interfaces(
|
||||
self.cfg
|
||||
):
|
||||
if ifname.startswith("loop"):
|
||||
vpp_ifname, config_iface = loopback.get_by_name(self.cfg, ifname)
|
||||
else:
|
||||
vpp_ifname, config_iface = interface.get_by_name(self.cfg, ifname)
|
||||
|
||||
try:
|
||||
config_mpls = config_iface["mpls"]
|
||||
except KeyError:
|
||||
config_mpls = False
|
||||
|
||||
vpp_mpls = False
|
||||
if vpp_ifname in self.vpp.cache["interface_names"]:
|
||||
sw_if_index = self.vpp.cache["interface_names"][vpp_ifname]
|
||||
try:
|
||||
vpp_mpls = self.vpp.cache["interface_mpls"][sw_if_index]
|
||||
except KeyError:
|
||||
pass
|
||||
if vpp_mpls != config_mpls:
|
||||
state = "disable"
|
||||
if config_mpls:
|
||||
state = "enable"
|
||||
cli = f"set interface mpls {vpp_ifname} {state}"
|
||||
self.cli["sync"].append(cli)
|
||||
return True
|
||||
|
||||
def _sync_unnumbered(self):
|
||||
"""Synchronize the VPP Dataplane configuration for unnumbered interface"""
|
||||
for ifname in loopback.get_loopbacks(self.cfg) + interface.get_interfaces(
|
||||
self.cfg
|
||||
):
|
||||
if ifname.startswith("loop"):
|
||||
config_ifname, config_iface = loopback.get_by_name(self.cfg, ifname)
|
||||
else:
|
||||
config_ifname, config_iface = interface.get_by_name(self.cfg, ifname)
|
||||
|
||||
config_unnumbered_ifname = None
|
||||
if "unnumbered" in config_iface:
|
||||
config_unnumbered_ifname = config_iface["unnumbered"]
|
||||
self.logger.debug(
|
||||
f"unnumbered iface {config_ifname} use {config_unnumbered_ifname}"
|
||||
)
|
||||
|
||||
vpp_iface = self.vpp.get_interface_by_name(config_ifname)
|
||||
vpp_iface_unnumbered = self.vpp.get_interface_by_name(
|
||||
config_unnumbered_ifname
|
||||
)
|
||||
self.logger.debug(
|
||||
f"unnumbered iface {vpp_iface} use {vpp_iface_unnumbered}"
|
||||
)
|
||||
|
||||
if not config_unnumbered_ifname:
|
||||
if (
|
||||
vpp_iface
|
||||
and vpp_iface.sw_if_index in self.vpp.cache["interface_unnumbered"]
|
||||
):
|
||||
cli = f"set interface unnumbered del {config_ifname}"
|
||||
self.cli["sync"].append(cli)
|
||||
del self.vpp.cache["interface_unnumbered"][vpp_iface.sw_if_index]
|
||||
continue
|
||||
continue
|
||||
|
||||
if (
|
||||
vpp_iface_unnumbered
|
||||
and vpp_iface
|
||||
and vpp_iface.sw_if_index in self.vpp.cache["interface_unnumbered"]
|
||||
):
|
||||
if (
|
||||
self.vpp.cache["interface_unnumbered"][vpp_iface.sw_if_index]
|
||||
== vpp_iface_unnumbered.sw_if_index
|
||||
):
|
||||
continue
|
||||
|
||||
cli = f"set interface unnumbered {config_ifname} use {config_unnumbered_ifname}"
|
||||
self.cli["sync"].append(cli)
|
||||
|
||||
return True
|
||||
|
||||
def _sync_addresses(self):
|
||||
"""Synchronize the VPP Dataplane configuration for interface addresses"""
|
||||
for ifname in loopback.get_loopbacks(self.cfg) + interface.get_interfaces(
|
||||
self.cfg
|
||||
):
|
||||
config_addresses = []
|
||||
vpp_addresses = []
|
||||
if ifname.startswith("loop"):
|
||||
vpp_ifname, config_iface = loopback.get_by_name(self.cfg, ifname)
|
||||
if "addresses" in config_iface:
|
||||
config_addresses = config_iface["addresses"]
|
||||
else:
|
||||
vpp_ifname, config_iface = interface.get_by_name(self.cfg, ifname)
|
||||
if "addresses" in config_iface:
|
||||
config_addresses = config_iface["addresses"]
|
||||
if vpp_ifname in self.vpp.cache["interface_names"]:
|
||||
_iface = self.vpp.get_interface_by_name(vpp_ifname)
|
||||
if _iface.sw_if_index in self.vpp.cache["interface_addresses"]:
|
||||
vpp_addresses = [
|
||||
str(x)
|
||||
for x in self.vpp.cache["interface_addresses"][
|
||||
_iface.sw_if_index
|
||||
]
|
||||
]
|
||||
for addr in config_addresses:
|
||||
if addr in vpp_addresses:
|
||||
continue
|
||||
cli = f"set interface ip address {vpp_ifname} {addr}"
|
||||
self.cli["sync"].append(cli)
|
||||
return True
|
||||
|
||||
def _sync_admin_state(self):
|
||||
"""Synchronize the VPP Dataplane configuration for interface admin state"""
|
||||
for ifname in interface.get_interfaces(self.cfg) + loopback.get_loopbacks(
|
||||
self.cfg
|
||||
):
|
||||
if ifname.startswith("loop"):
|
||||
vpp_ifname, _config_iface = loopback.get_by_name(self.cfg, ifname)
|
||||
config_admin_state = 1
|
||||
else:
|
||||
vpp_ifname, _config_iface = interface.get_by_name(self.cfg, ifname)
|
||||
config_admin_state = interface.get_admin_state(self.cfg, ifname)
|
||||
|
||||
vpp_admin_state = 0
|
||||
_iface = self.vpp.get_interface_by_name(vpp_ifname)
|
||||
if _iface:
|
||||
vpp_admin_state = _iface.flags & 1 # IF_STATUS_API_FLAG_ADMIN_UP
|
||||
|
||||
if config_admin_state == vpp_admin_state:
|
||||
continue
|
||||
state = "up"
|
||||
if config_admin_state == 0:
|
||||
state = "down"
|
||||
cli = f"set interface state {vpp_ifname} {state}"
|
||||
self.cli["sync"].append(cli)
|
||||
return True
|
||||
File diff suppressed because it is too large
Load Diff
@@ -130,6 +130,8 @@ class VPPApi:
|
||||
"taps": {},
|
||||
"acls": {},
|
||||
"acl_tags": {},
|
||||
"interface_sflow": {},
|
||||
"sflow": {},
|
||||
}
|
||||
return True
|
||||
|
||||
@@ -415,6 +417,33 @@ class VPPApi:
|
||||
for tap in api_response:
|
||||
self.cache["taps"][tap.sw_if_index] = tap
|
||||
|
||||
try:
|
||||
self.logger.debug("Retrieving sFlow")
|
||||
|
||||
api_response = self.vpp.api.sflow_sampling_rate_get()
|
||||
if api_response:
|
||||
self.cache["sflow"]["sampling-rate"] = api_response.sampling_N
|
||||
api_response = self.vpp.api.sflow_polling_interval_get()
|
||||
if api_response:
|
||||
self.cache["sflow"]["polling-interval"] = api_response.polling_S
|
||||
api_response = self.vpp.api.sflow_header_bytes_get()
|
||||
if api_response:
|
||||
self.cache["sflow"]["header-bytes"] = api_response.header_B
|
||||
|
||||
api_response = self.vpp.api.sflow_interface_dump()
|
||||
for iface in api_response:
|
||||
self.cache["interface_sflow"][iface.hw_if_index] = True
|
||||
except AttributeError as err:
|
||||
self.logger.warning(f"sFlow API not found - missing plugin: {err}")
|
||||
|
||||
self.logger.debug("Retrieving interface Unnumbered state")
|
||||
api_response = self.vpp.api.ip_unnumbered_dump()
|
||||
for iface in api_response:
|
||||
self.cache["interface_unnumbered"][iface.sw_if_index] = iface.ip_sw_if_index
|
||||
|
||||
self.logger.debug("Retrieving bondethernets")
|
||||
api_response = self.vpp.api.sw_bond_interface_dump()
|
||||
|
||||
self.cache_read = True
|
||||
return self.cache_read
|
||||
|
||||
@@ -525,3 +554,14 @@ class VPPApi:
|
||||
if vpp_iface.sw_if_index == lcp.host_sw_if_index:
|
||||
return True
|
||||
return False
|
||||
|
||||
def cli_inband(self, cmd):
|
||||
"""Call the VPP inband CLI with the given command, and return any retun value or False if we
|
||||
could not connect."""
|
||||
|
||||
if not self.connected and not self.connect():
|
||||
self.logger.error("Could not connect to VPP")
|
||||
return False
|
||||
|
||||
ret = self.vpp.api.cli_inband(cmd=cmd)
|
||||
return ret
|
||||
|
||||
@@ -24,10 +24,13 @@ import yaml
|
||||
# Ensure the paths are correct when we execute from the source tree
|
||||
try:
|
||||
from vppcfg.config import Validator
|
||||
from vppcfg._version import __version__
|
||||
except ModuleNotFoundError:
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
from vppcfg.config import Validator
|
||||
from vppcfg.vpp.reconciler import Reconciler
|
||||
from vppcfg._version import __version__
|
||||
from vppcfg.vpp.applier import Applier
|
||||
from vppcfg.vpp.planner import Planner
|
||||
from vppcfg.vpp.dumper import Dumper
|
||||
|
||||
try:
|
||||
@@ -211,6 +214,7 @@ def main():
|
||||
logging.basicConfig(
|
||||
format="[%(levelname)-8s] %(name)s.%(funcName)s: %(message)s", level=level
|
||||
)
|
||||
logging.info(f"vppcfg version {__version__}")
|
||||
|
||||
opt_kwargs = {}
|
||||
if "vpp_json_dir" in args and args.vpp_json_dir is not None:
|
||||
@@ -243,44 +247,44 @@ def main():
|
||||
if args.command == "check":
|
||||
sys.exit(0)
|
||||
|
||||
reconciler = Reconciler(cfg, **opt_kwargs)
|
||||
planner = Planner(cfg, **opt_kwargs)
|
||||
if args.command == "plan" and args.novpp:
|
||||
if not reconciler.vpp.mockconfig(cfg):
|
||||
if not planner.vpp.mockconfig(cfg):
|
||||
sys.exit(-7)
|
||||
else:
|
||||
if not reconciler.vpp.readconfig():
|
||||
if not planner.vpp.readconfig():
|
||||
sys.exit(-3)
|
||||
|
||||
if not reconciler.phys_exist_in_vpp():
|
||||
if not planner.phys_exist_in_vpp():
|
||||
logging.error("Not all PHYs in the config exist in VPP")
|
||||
sys.exit(-4)
|
||||
|
||||
if not reconciler.phys_exist_in_config():
|
||||
if not planner.phys_exist_in_config():
|
||||
logging.error("Not all PHYs in VPP exist in the config")
|
||||
sys.exit(-5)
|
||||
|
||||
if not reconciler.lcps_exist_with_lcp_enabled():
|
||||
if not planner.lcps_exist_with_lcp_enabled():
|
||||
logging.error(
|
||||
"Linux Control Plane is needed, but linux-cp API is not available"
|
||||
)
|
||||
sys.exit(-6)
|
||||
|
||||
failed = False
|
||||
if not reconciler.prune():
|
||||
if not planner.prune():
|
||||
if not args.force:
|
||||
logging.error("Planning prune failure")
|
||||
sys.exit(-10)
|
||||
failed = True
|
||||
logging.warning("Planning prune failure, continuing due to --force")
|
||||
|
||||
if not reconciler.create():
|
||||
if not planner.create():
|
||||
if not args.force:
|
||||
logging.error("Planning create failure")
|
||||
sys.exit(-20)
|
||||
failed = True
|
||||
logging.warning("Planning create failure, continuing due to --force")
|
||||
|
||||
if not reconciler.sync():
|
||||
if not planner.sync():
|
||||
if not args.force:
|
||||
logging.error("Planning sync failure")
|
||||
sys.exit(-30)
|
||||
@@ -288,7 +292,7 @@ def main():
|
||||
logging.warning("Planning sync failure, continuing due to --force")
|
||||
|
||||
if args.command == "plan":
|
||||
reconciler.write(args.outfile, emit_ok=not failed)
|
||||
planner.write(args.outfile, emit_ok=not failed)
|
||||
|
||||
if failed:
|
||||
logging.error("Planning failed")
|
||||
@@ -298,6 +302,13 @@ def main():
|
||||
if args.command == "plan":
|
||||
sys.exit(0)
|
||||
|
||||
if args.command == "apply":
|
||||
applier = Applier(cfg, planner.get_commands())
|
||||
if not applier.apply():
|
||||
logging.error("Applying configuration failed")
|
||||
sys.exit(-50)
|
||||
logging.info("Apply succeeded")
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user