Merge pull request #1258 from kernelkit/hostname-stability

Hostname stability

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2025-11-20 11:59:25 +01:00
committed by GitHub
27 changed files with 787 additions and 246 deletions
+27 -29
View File
@@ -3,41 +3,39 @@ Checklists for Pull Requests and Releases
Maintainer checklists for reviewing pull requests and doing releases.
Pull Requests
-------------
- If applicable, is there a readable ChangeLog entry?
- If any LICENSE file has been updated, has the `.hash` file been updated?
- If any change to a Finit `.svc` file, does any run/task linger?
I.e., is there a runlevel and/or condition defined to prevent them
from running outside of their intended runlevel?
- If any change to grub or qemu/qeneth setup, has it been tested in GNS3?
- If any change to u-boot/buildroot, has it been tested with `<booloader>_defconfig`
- If any change to logging, have the resulting logs been audited?
- Check for duplicate entries, misspellings
- Check for sneaky severity changes, e.g., error vs note, error vs warning
- If new subsystem, or major changes to a subsystem, have the docs been updated?
- If change to mDNS, has it been tested with netbrowse?
- If change to `_defconfig`, verify `local.mk` and sync with other archs
- Test manually as well, e.g., CLI changes do not have ha regression tests
- Build from distclean, or use artifacts built by build servers, for manual tests
- If applicable, is there a readable ChangeLog entry?
- If any LICENSE file has been updated, has the `.hash` file been updated?
- If any change to a Finit `.svc` file, does any run/task linger?
I.e., is there a runlevel and/or condition defined to prevent them
from running outside of their intended runlevel?
- If any change to grub or qemu/qeneth setup, has it been tested in GNS3?
- If any change to u-boot/buildroot, has it been tested with `<booloader>_defconfig`
- If any change to logging, have the resulting logs been audited?
- Check for duplicate entries, misspellings
- Check for sneaky severity changes, e.g., error vs note, error vs warning
- If new subsystem, or major changes to a subsystem, have the docs been updated?
- If change to mDNS, has it been tested with netbrowse?
- If change to `_defconfig`, verify `local.mk` and sync with other archs
- Test manually as well, e.g., CLI changes do not have ha regression tests
- Build from distclean, or use artifacts built by build servers, for manual tests
Releases
--------
Recommended checkpoints, use at your own discretion:
- Make at least one -betaN release to verify the GitHub workflow well in time release day
- Stuff happens, remember kernelkit/infix#735
- Make at least one -rcN to flush out any issues in customer repos
- Easy to forget adaptations/hacks in customer repos -- may need Infix change/support
- Verify release artifacts (checksums, completeness, no corrupted files)
- Test on actual hardware for at least one architecture
- Review ChangeLog for completeness
- Check for release-blocking issues
- Verify generated GNS3 appliance, no marketplace update on -rc builds
- Ensure the markdown link for the release diff is updated
- Ensure subrepos are tagged (can be automated, see kernelkit/infix#393)
- Sync tags for all repo. sync activities
- Make at least one -betaN release to verify the GitHub workflow well in time release day
- Stuff happens, remember kernelkit/infix#735
- Make at least one -rcN to flush out any issues in customer repos
- Easy to forget adaptations/hacks in customer repos -- may need Infix change/support
- Verify release artifacts (checksums, completeness, no corrupted files)
- Test on actual hardware for at least one architecture
- Review ChangeLog for completeness
- Check for release-blocking issues
- Verify generated GNS3 appliance, no marketplace update on -rc builds
- Ensure the markdown link for the release diff is updated
- Ensure subrepos are tagged (can be automated, see kernelkit/infix#393)
- Sync tags for all repo. sync activities
+154 -64
View File
@@ -9,15 +9,15 @@ forms of collaboration as well. [Let's talk!][support] :handshake:
If you are unsure how to start implementing an idea or fix:
- :bug: open an issue, there are human friendly templates for _bugs_
and _feature requests_ at <https://github.com/kernelkit/infix/issues>
- :speech_balloon: use the [Q&A Forum][discuss]
- :technologist: The [Developer's Guide][devguide] is also a useful start
- :bug: open an issue, there are human friendly templates for _bugs_
and _feature requests_ at <https://github.com/kernelkit/infix/issues>
- :speech_balloon: use the [Q&A Forum][discuss]
- :technologist: The [Developer's Guide][devguide] is also a useful start
> _Talking about code and problems first is often the best way to get
> [!IMPORTANT]
> Talking about code and problems first is often the best way to get
> started before submitting a pull request. We have found it always
> saves time, yours and ours._
> saves time, yours and ours.
:sparkles: General Guidelines
-----------------------------
@@ -27,21 +27,20 @@ version the change is made against, what it does, and, more importantly
*why* -- from your perspective, why is it a bug, why does the code need
changing in this way. Start with why.
- :bug: Bug reports need metadata like Infix version or commit hash
- :adhesive_bandage: Bug fixes also need version, and (preferably) a
corresponding issue number for the ChangeLog
- :new: New features, you need to get approval of the YANG model first!
:speech_balloon: Please use the [Forum][discuss], e.g., category:
*Ideas*, or open a :pray: feature request issue
- :white_check_mark: New features also need new regression tests, this
can be basic tests or more complex use-case tests comprising multiple
subsystems, see [Testing Changes](#test_tube-testing-changes), below
- :bug: Bug reports need metadata like Infix version or commit hash
- :adhesive_bandage: Bug fixes also need version, and (preferably) a
corresponding issue number for the ChangeLog
- :new: New features, you need to get approval of the YANG model first!
:speech_balloon: Please use the [Forum][discuss], e.g., category:
*Ideas*, or open a :pray: feature request issue
- :white_check_mark: New features also need new regression tests, this
can be basic tests or more complex use-case tests comprising multiple
subsystems, see [Testing Changes](#test_tube-testing-changes), below
Please take care to ensure you follow the project coding style and the
commit message format. If you follow these recommendations you help
the maintainers and make it easier for them to include your code.
:woman_technologist: Coding Style
---------------------------------
@@ -51,39 +50,40 @@ and it is expected that you provide a human-readable summary for the
release notes (ChangeLog) and at least a configuration example in the
manual for new features.
> **Tip:** consider ["Readme driven development"][RDD] for new features.
> It is amazing how many flaws in your own bright ideas come to bare
> when you suddenly have to explain them to someone else!
> [!TIP]
> Consider ["Readme driven development"][RDD] for new features. It is
> amazing how many flaws in your own bright ideas come to bare when you
> suddenly have to explain them to someone else!
We expect code contributions for:
- C code in [Linux Coding Style][Linux]
- Python code should follow [PEP-8][]
- C code in [Linux Coding Style][Linux]
- Python code should follow [PEP-8][]
> [!IMPORTANT]
> **However,** always submit code that follows the style of surrounding
> code! Legacy takes precedence, and remember, we read code a lot more
> than write it, so legibility is important.
The ChangeLog deserves a separate mention:
- Releases are listed in reverse chronological order order, so the
latest/next release is at the beginning of the file
- Only *user-facing bugs and features* are detailed, so code refactor,
new tests, etc. are not listed.
- Add your changes/features to the Changes section
- Add your Fix line in the Fixes section, in numeric order
- Changes and fixes without an issue number are listed after all
numbered ones
- YANG model changes are documented in their respective model, for
standard models, e.g., for `ietf-interfaces.yang`, the corresponding
`infix-interfaces.yang` detail augments/deviations as revisions.
- Releases are listed in reverse chronological order order, so the
latest/next release is at the beginning of the file
- Only *user-facing bugs and features* are detailed, so code refactor,
new tests, etc. are not listed.
- Add your changes/features to the Changes section
- Add your Fix line in the Fixes section, in numeric order
- Changes and fixes without an issue number are listed after all
numbered ones
- YANG model changes are documented in their respective model, for
standard models, e.g., for `ietf-interfaces.yang`, the corresponding
`infix-interfaces.yang` detail augments/deviations as revisions.
A final note, lines of code are allowed to be longer than 72 characters
these days, unless you live by PEP-8 (see above). There is no enforced
maximum, but the team usually keep it around 100 characters for both C
and Python.
:test_tube: Testing Changes
---------------------------
@@ -97,9 +97,8 @@ the same pull request.
For help getting started with testing, see the following resources:
- [Developer's Guide][devguide]
- [Regression Testing][testing]
- [Developer's Guide][devguide]
- [Regression Testing][testing]
:memo: Commit Messages
----------------------
@@ -110,45 +109,135 @@ proud of your work and set up a proper GIT identity for your commits:
<img src="../doc/jack.png" width=70 align="right">
$ git config --global user.name "Jacky Linker"
$ git config --global user.email jacky.linker@example.com
```bash
$ git config --global user.name "Jacky Linker"
$ git config --global user.email jacky.linker@example.com
```
Example commit message from one of many [online guides][cbeams]. Use
`git commit -s` to automatically add a `Signed-off-by` for proof of
origin, see [DCO][] for more info.
subsystem: brief, but clear and concise summary of changes
More detailed explanatory text, if necessary. Wrap it to about 72
characters or so. In some contexts, the first line is treated as
the subject of an email and the rest of the text as the body. The
empty line separating summary from body is critical. Tools like
rebase can get confused if the empty line is missing.
Further paragraphs should be separated with empty lines.
- Bullet points are okay, too
- Typically a hyphen or asterisk is used for the bullet, preceded
by a single space, with blank lines in between, but conventions
vary here
```text
subsystem: brief, but clear and concise summary of changes
If you use an issue tracker, put references to them at the bottom,
like this:
More detailed explanatory text, if necessary. Wrap it to about 72
characters or so. In some contexts, the first line is treated as
the subject of an email and the rest of the text as the body. The
empty line separating summary from body is critical. Tools like
rebase can get confused if the empty line is missing.
Resolves: #123
See also: #456, #789
Further paragraphs should be separated with empty lines.
Signed-off-by: Jacky Linker <jacky.linker@example.com>
- Bullet points are okay, too
- Typically a hyphen or asterisk is used for the bullet, preceded
by a single space, with blank lines in between, but conventions
vary here
If you use an issue tracker, put references to them at the bottom,
like this:
Resolves: #123
See also: #456, #789
Signed-off-by: Jacky Linker <jacky.linker@example.com>
```
This is an example of how to [automatically close][closing] an issue
when the commit is merged to mainline. Several keywords are available.
:lock_with_ink_pen: Signing Commits with GPG
---------------------------------------------
To ensure the authenticity and integrity of your contributions, we
**require** all commits to be signed with GPG. This cryptographically
verifies that commits come from a trusted source.
### Generating a GPG Key
If you don't already have a GPG key, generate one:
```bash
$ gpg --full-generate-key
```
When prompted, choose:
- Key type: `RSA and RSA` (default)
- Key size: `4096` bits (recommended for security)
- Expiration: `0` (key does not expire)
- Real name and email: Use the same email as your Git configuration
> [!NOTE]
> We recommend keys that do not expire for signing commits. Expiration
> creates a "usability time bomb" without providing meaningful security
> benefits for code signing. See [this article][pgpfan] for details.
### Configuring Git to Sign Commits
First, find your GPG key ID:
```bash
$ gpg --list-secret-keys --keyid-format=long
```
Look for the line starting with `sec`, the key ID is the part after the `/`.
For example, in `sec rsa4096/ABCD1234EFGH5678`, the key ID is `ABCD1234EFGH5678`.
Configure Git to use your GPG key:
```bash
$ git config --global user.signingkey ABCD1234EFGH5678
$ git config --global commit.gpgsign true
```
The second command enables automatic signing for all commits. Alternatively,
you can sign individual commits with `git commit -S`.
### Publishing Your Public Key
To allow others to verify your signatures, publish your public key to a
keyserver:
```bash
$ gpg --keyserver hkps://keys.openpgp.org --send-keys ABCD1234EFGH5678
```
> [!IMPORTANT]
> The keyserver will send a verification email to the address associated
> with your key. You **must** click the link in that email to confirm
> ownership before your key becomes searchable by email address.
Alternative keyservers you can use:
- `hkps://keyserver.ubuntu.com`
- `hkps://pgp.mit.edu`
### Adding Your GPG Key to GitHub
For GitHub to show your commits as "Verified", you need to add your public
key to your account:
1. Export your public key:
```bash
$ gpg --armor --export ABCD1234EFGH5678
```
2. Copy the entire output, including the `-----BEGIN PGP PUBLIC KEY BLOCK-----`
and `-----END PGP PUBLIC KEY BLOCK-----` lines.
3. Go to [GitHub Settings → SSH and GPG keys](https://github.com/settings/keys)
4. Click **New GPG key** and paste your public key.
Now your signed commits will display a "Verified" badge on GitHub! :white_check_mark:
For more details, see GitHub's [official documentation on commit signature verification][gpg-verify].
:twisted_rightwards_arrows: Pull Requests
-----------------------------------------
> _The git repository is the canonical location for information._
> [!NOTE]
> _The git repository is the canonical location for all information._
A pull request should preferably address a single issue or change. This
may of course include multiple related changes, but what is important to
@@ -167,7 +256,6 @@ Buildroot, consider the pull request message body similar to the cover
letter for a series of patches -- it's a summary of changes, and it is
lost when the changes are merged to the mainline branch.
:balance_scale: Code of Conduct
-------------------------------
@@ -184,6 +272,8 @@ other contributions that are not aligned to this Code of Conduct."*
[PEP-8]: https://peps.python.org/pep-0008/
[RDD]: https://tom.preston-werner.com/2010/08/23/readme-driven-development
[cbeams]: https://cbea.ms/git-commit/#seven-rules
[conduct]: CODE-OF-CONDUCT.md
[DCO]: https://developercertificate.org/
[closing]: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests
[conduct]: CODE-OF-CONDUCT.md
[DCO]: https://developercertificate.org/
[closing]: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests
[gpg-verify]: https://docs.github.com/en/authentication/managing-commit-signature-verification
[pgpfan]: https://articles.59.ca/doku.php?id=pgpfan:expire
@@ -109,7 +109,8 @@
{"id": "hostname"},
{"id": "dns-server"},
{"id": "router"},
{"id": "netmask"}
{"id": "netmask"},
{"id": "vendor-class", "value": "Banana Pi BPI-R3"}
]
}
}
@@ -68,7 +68,8 @@
{"id": "hostname"},
{"id": "dns-server"},
{"id": "router"},
{"id": "netmask"}
{"id": "netmask"},
{"id": "vendor-class", "value": "NanoPi R2S"}
]
}
}
@@ -47,7 +47,8 @@
{"id": "domain"},
{"id": "hostname"},
{"id": "dns-server"},
{"id": "ntp-server"}
{"id": "ntp-server"},
{"id": "vendor-class", "value": "Raspberry Pi 4 Model B"}
]
}
}
+74
View File
@@ -0,0 +1,74 @@
#!/bin/sh
# Deterministically set system hostname from /etc/hostname.d/
#
# Highest numbered file wins (lexicographic sort, 90-dhcp > 50-configured > 10-default)
#
# Priority scheme:
# 10-default - Bootstrap/factory default (%h-%m format)
# 50-configured - From confd /system/hostname
# 90-dhcp-<iface> - From DHCP clietn on interface (highest priority)
HOSTNAME_D="/etc/hostname.d"
# Ensure directory exists
mkdir -p "$HOSTNAME_D"
# Find the highest priority file (reverse sort, take first)
hostname_file=$(ls -1 "$HOSTNAME_D" 2>/dev/null | sort -r | head -1)
if [ -z "$hostname_file" ]; then
logger -it confd "No hostname sources found in $HOSTNAME_D"
exit 1
fi
# Read hostname from the file (first line only, strip whitespace)
new_hostname=$(cat "$HOSTNAME_D/$hostname_file" | head -1 | tr -d '\n\r\t ')
if [ -z "$new_hostname" ]; then
logger -it confd "Empty hostname in $hostname_file"
exit 1
fi
if [ ${#new_hostname} -gt 64 ]; then
logger -it confd "Hostname too long (${#new_hostname} > 64) in $hostname_file"
exit 1
fi
# Check if hostname has actually changed
current_hostname=$(hostname)
if [ "$new_hostname" = "$current_hostname" ]; then
# No change needed, exit silently
exit 0
fi
# Set the hostname
logger -it confd "Setting hostname to '$new_hostname' from $hostname_file"
hostname "$new_hostname"
# Update /etc/hostname (for persistence across reboots)
echo "$new_hostname" > /etc/hostname
# Update /etc/hosts (127.0.1.1 entry for proper name resolution)
if grep -q "^127\.0\.1\.1" /etc/hosts; then
sed -i -E "s/^(127\.0\.1\.1\s+).*/\1$new_hostname/" /etc/hosts
else
# Add entry if it doesn't exist
echo "127.0.1.1 $new_hostname" >> /etc/hosts
fi
# Notify services of hostname change, skip while in bootstrap
initctl -nbq touch sysklogd
if ! runlevel >/dev/null 2>&1; then
exit 0
fi
initctl -bq status lldpd && lldpcli configure system hostname "$new_hostname" 2>/dev/null
initctl -bq status mdns && avahi-set-host-name "$new_hostname" 2>/dev/null
initctl -bq touch netbrowse 2>/dev/null
# If called from dhcp script we need to reload to activate new name in syslogd
# Otherwise we're called from confd, which does the reload when all is done.
if [ -n "$1" ]; then
initctl -b reload
fi
exit 0
@@ -146,6 +146,87 @@ class DTSystem:
flat_devices = [device for sublist in self.infix_devices("vpds") for device in sublist]
return [self.into_vpd(device) for device in flat_devices]
def vendor_name(self):
"""Extract vendor name from devicetree compatible property"""
compatible = self.base.str_array("compatible")
if not compatible or len(compatible) == 0:
return None
# Map of common devicetree vendor prefixes to proper names
vendor_map = {
"raspberrypi": "Raspberry Pi Foundation",
"brcm": "Broadcom Inc.",
"marvell": "Marvell Technology, Inc.",
"fsl": "NXP Semiconductors N.V.",
"nxp": "NXP Semiconductors N.V.",
"ti": "Texas Instruments Inc.",
"qcom": "Qualcomm Inc.",
"rockchip": "Rockchip Electronics Co., Ltd.",
"amlogic": "Amlogic Inc.",
"allwinner": "Allwinner Technology Co., Ltd.",
"mediatek": "MediaTek Inc.",
"st": "STMicroelectronics N.V.",
"xlnx": "Xilinx, Inc.",
"intel": "Intel Corporation",
"amd": "Advanced Micro Devices, Inc.",
"nvidia": "NVIDIA Corporation",
"bananapi": "SinoVoip Co., Ltd.",
"sinovoip": "SinoVoip Co., Ltd.",
"friendlyarm": "FriendlyElec",
"friendlyelec": "FriendlyElec",
"microchip": "Microchip Technology Inc.",
"atmel": "Microchip Technology Inc.",
}
# Get the first (most specific) compatible string
compat = compatible[0]
# Extract vendor prefix (part before comma)
if ',' in compat:
prefix = compat.split(',')[0].lower()
# Return mapped name or capitalized prefix
return vendor_map.get(prefix, prefix.capitalize())
return None
class DMISystem:
BASE = "/sys/class/dmi/id"
def read_dmi(self, attr):
"""Read DMI attribute from /sys/class/dmi/id/"""
path = os.path.join(DMISystem.BASE, attr)
if not os.path.exists(path):
return None
try:
with open(path, 'r', encoding='utf-8') as f:
value = f.read().strip()
return value if value else None
except:
return None
def populate(self, out):
"""Read DMI/SMBIOS data and populate output dictionary"""
vendor = self.read_dmi("sys_vendor")
if vendor:
out["vendor"] = vendor
product_name = self.read_dmi("product_name")
if product_name:
out["product-name"] = product_name
serial = self.read_dmi("product_serial")
if serial:
out["serial-number"] = serial
version = self.read_dmi("product_version")
if version:
out["product-version"] = version
def vpds(self):
"""DMI systems don't have VPD in the traditional sense"""
return []
class QEMUSystem:
BASE = "/sys/firmware/qemu_fw_cfg"
@@ -355,6 +436,9 @@ def probe_qemusystem(out):
if not out[attr]:
out[attr] = default
if os.path.exists(DMISystem.BASE):
DMISystem().populate(out)
if not out["factory-password-hash"] and \
not out["vpd"]["product"]["available"]:
# Virtual instance without VPD emulation, fallback to
@@ -397,6 +481,21 @@ def generic_usb_ports(out):
out["usb-ports"] = [{"name": f"USB{p['num']}", "path": p["path"]} for p in ports]
def probe_dmisystem(out):
"""Probe DMI/SMBIOS based system (x86/AMD64)"""
dmisys = DMISystem()
dmisys.populate(out)
generic_usb_ports(out)
if not out["mac-address"]:
out["mac-address"] = fallback_base_mac()
vpd_inject(out, dmisys.vpds())
return 0
def probe_dtsystem(out):
"""Probe DTS based system, expects a VPD in ONIE PROM format."""
dtsys = DTSystem()
@@ -413,12 +512,24 @@ def probe_dtsystem(out):
out["compatible"] = dtsys.base.str_array("compatible")
# Extract vendor from compatible string if not already set
if not out["vendor"]:
vendor = dtsys.vendor_name()
if vendor:
out["vendor"] = vendor
staticpw = dtsys.infix.str("factory-password-hash")
if not out["factory-password-hash"]:
out["factory-password-hash"] = staticpw
vpd_inject(out, vpds)
# Fallback to devicetree serial-number if VPD doesn't provide one
if not out["serial-number"]:
serial = dtsys.base.str("serial-number")
if serial:
out["serial-number"] = serial
# Fallback to interface MAC if VPD doesn't provide one (e.g., SBCs)
if not out["mac-address"]:
out["mac-address"] = fallback_base_mac()
@@ -430,6 +541,7 @@ def main():
out = {
"vendor": None,
"product-name": None,
"product-version": None,
"part-number": None,
"serial-number": None,
"mac-address": None,
@@ -440,6 +552,8 @@ def main():
if os.path.exists(QEMUSystem.REV):
err = probe_qemusystem(out)
elif os.path.exists(DMISystem.BASE):
err = probe_dmisystem(out)
elif os.path.exists(DTSystem.BASE):
err = probe_dtsystem(out)
else:
+18
View File
@@ -0,0 +1,18 @@
#!/bin/sh
# Initialize default hostname for hostname.d pattern
# This runs very early in boot to set up the default hostname entry
HOSTNAME_D="/etc/hostname.d"
# Ensure directory exists
mkdir -p "$HOSTNAME_D"
# If no default exists yet, create it from /etc/hostname (from squashfs)
if [ ! -f "$HOSTNAME_D/10-default" ] && [ -f /etc/hostname ]; then
cp /etc/hostname "$HOSTNAME_D/10-default"
fi
# Apply hostname using the deterministic helper
if [ -x /usr/libexec/infix/hostname ]; then
/usr/libexec/infix/hostname
fi
@@ -50,6 +50,26 @@ wait_for_ipv6_default_route()
err "Timed out waiting for IPv6 default route!"
}
# Check if a DHCP option was requested in the parameter request list
# Returns: 0 if requested, 1 if not requested or config unavailable
was_option_requested()
{
local opt_num="$1"
local config="/etc/finit.d/available/dhcp-client-${interface}.conf"
if [ ! -f "$config" ]; then
dbg "config file not found: $config"
return 1
fi
# Extract udhcpc command line and check for -O <option_num>
if grep -q -- "-O ${opt_num}\b" "$config"; then
return 0
fi
return 1
}
# RFC3442: If the DHCP server returns both a Classless
# Static Routes option and a Router option, the DHCP
# client MUST ignore the Router option.
@@ -57,17 +77,25 @@ set_dhcp_routes()
{
echo "! Generated by udhcpc" > "$NEXT"
if [ -n "$staticroutes" ]; then
# format: dest1/mask gw1 ... destn/mask gwn
set -- $staticroutes
while [ -n "$1" -a -n "$2" ]; do
dbg "adding route $1 via $2 metric $metric tag 100"
echo "ip route $1 $2 $metric tag 100" >> "$NEXT"
shift 2
done
if was_option_requested 121; then
# format: dest1/mask gw1 ... destn/mask gwn
set -- $staticroutes
while [ -n "$1" -a -n "$2" ]; do
dbg "adding route $1 via $2 metric $metric tag 100"
echo "ip route $1 $2 $metric tag 100" >> "$NEXT"
shift 2
done
else
log "ignoring unrequested staticroutes (option 121)"
fi
elif [ -n "$router" ] ; then
for i in $router ; do
echo "ip route 0.0.0.0/0 $i $metric tag 100" >> "$NEXT"
done
if was_option_requested 3; then
for i in $router ; do
echo "ip route 0.0.0.0/0 $i $metric tag 100" >> "$NEXT"
done
else
log "ignoring unrequested router (option 3)"
fi
fi
# Reduce changes needed by comparing with previous route(s)
@@ -108,6 +136,11 @@ case "$ACTION" in
# drop info from this interface
rm -f "$RESOLV_CONF"
rm -f "$NTPFILE"
if [ -f "/etc/hostname.d/90-dhcp-${interface}" ]; then
log "removing /etc/hostname.d/90-dhcp-${interface}"
rm -f "/etc/hostname.d/90-dhcp-${interface}"
/usr/libexec/infix/hostname dhcp
fi
if [ -x /usr/sbin/avahi-autoipd ]; then
/usr/sbin/avahi-autoipd -c $interface && /usr/sbin/avahi-autoipd -k $interface
fi
@@ -133,21 +166,35 @@ case "$ACTION" in
set_dhcp_routes
# set hostname if given
# set hostname if given and requested
if [ -n "$hostname" ]; then
log "setting new hostname: $hostname"
hostname "$hostname"
sed -i -E "s/^(127\.0\.1\.1\s+).*/\1$hostname/" /etc/hosts
if was_option_requested 12; then
log "received DHCP hostname: $hostname"
mkdir -p /etc/hostname.d
echo "$hostname" > "/etc/hostname.d/90-dhcp-${interface}"
/usr/libexec/infix/hostname dhcp
else
log "ignoring unrequested hostname (option 12): $hostname"
fi
fi
# drop info from this interface
truncate -s 0 "$RESOLV_CONF"
# prefer rfc3397 domain search list (option 119) if available
search_list=""
if [ -n "$search" ]; then
search_list=$search
if was_option_requested 119; then
search_list=$search
else
log "ignoring unrequested search (option 119): $search"
fi
elif [ -n "$domain" ]; then
search_list=$domain
if was_option_requested 15; then
search_list=$domain
else
log "ignoring unrequested domain (option 15): $domain"
fi
fi
if [ -n "$search_list" ]; then
@@ -155,19 +202,29 @@ case "$ACTION" in
echo "search $search_list # $interface" >> $RESOLV_CONF
fi
for i in $dns ; do
dbg "adding dns $i"
echo "nameserver $i # $interface" >> $RESOLV_CONF
resolvconf -u
done
if [ -n "$dns" ]; then
if was_option_requested 6; then
for i in $dns ; do
dbg "adding dns $i"
echo "nameserver $i # $interface" >> $RESOLV_CONF
resolvconf -u
done
else
log "ignoring unrequested dns (option 6): $dns"
fi
fi
if [ -n "$ntpsrv" ]; then
truncate -s 0 "$NTPFILE"
for srv in $ntpsrv; do
dbg "got NTP server $srv"
echo "server $srv iburst" >> "$NTPFILE"
done
chronyc reload sources >/dev/null
if was_option_requested 42; then
truncate -s 0 "$NTPFILE"
for srv in $ntpsrv; do
dbg "got NTP server $srv"
echo "server $srv iburst" >> "$NTPFILE"
done
chronyc reload sources >/dev/null
else
log "ignoring unrequested ntpsrv (option 42): $ntpsrv"
fi
fi
esac
+4 -2
View File
@@ -52,11 +52,13 @@ All notable changes to the project are documented in this file.
### Fixes
- Fix #855: User admin sometimes fails to be added to `wheel` group
- Fix #1112: setting hostname via DHCP client sometimes gets overridden by the
configured system hostname
- Fix #1247: Prevent invalid configuration of OSPF backbone area (0.0.0.0) as
stub or NSSA. The backbone must always be a normal area per RFC 2328. Any
existing invalid configurations are automatically corrected during upgrade
- Fix serious regression in boot time, introduced in v25.10, delays the
boot step "Mounting filesystems ..." with up to 30 seconds!
- Fix #1255: serious regression in boot time, introduced in v25.10, delays the
boot step "Mounting filesystems ...", from 30 seconds up to five minutes!
- Fix broken intra-document links in container and tunnel documentation
[lastest-boot]: https://github.com/kernelkit/infix/releases/latest-boot
+22 -73
View File
@@ -206,11 +206,6 @@ done:
return rc;
}
static int sys_reload_services(void)
{
return systemf("initctl -nbq touch sysklogd");
}
#define TIMEZONE_CONF "/etc/timezone"
#define TIMEZONE_PREV TIMEZONE_CONF "-"
@@ -1558,11 +1553,10 @@ int hostnamefmt(struct confd *confd, const char *fmt, char *hostnm, size_t hostl
static int change_hostname(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd)
{
const char *hostip = "127.0.1.1";
char hostnm[65], domain[65];
char buf[256], *fmt;
FILE *nfp, *fp;
int err, fd;
int rc = SR_ERR_OK;
char *fmt;
FILE *fp;
if (event != SR_EV_DONE || !lydx_get_xpathf(diff, XPATH_HOSTNAME_))
return SR_ERR_OK;
@@ -1572,82 +1566,36 @@ static int change_hostname(sr_session_ctx_t *session, struct lyd_node *config, s
fmt = strdup(nm);
if (hostnamefmt(confd, fmt, hostnm, sizeof(hostnm), domain, sizeof(domain))) {
err = SR_ERR_SYS;
goto err;
rc = SR_ERR_SYS;
goto failed;
}
err = sethostname(hostnm, strlen(hostnm));
if (err) {
ERROR("failed setting hostname");
err = SR_ERR_SYS;
goto err;
}
/* Use hostname.d for deterministic hostname management */
systemf("mkdir -p /etc/hostname.d");
fp = fopen("/etc/hostname.d/50-configured", "w");
if (!fp)
goto failed;
fprintf(fp, "%s\n", hostnm);
fclose(fp);
/* Handle domain name if present */
if (domain[0] && setdomainname(domain, strlen(domain))) {
ERROR("failed setting domain name");
/* Not cause for failing this function */
}
fp = fopen(_PATH_HOSTNAME, "w");
if (!fp) {
err = SR_ERR_INTERNAL;
goto err;
if (systemf("/usr/libexec/infix/hostname")) {
failed:
ERROR("failed setting hostname");
rc = SR_ERR_SYS;
}
fprintf(fp, "%s\n", hostnm);
fclose(fp);
nfp = fopen(_PATH_HOSTS "+", "w");
if (!nfp) {
err = SR_ERR_INTERNAL;
goto err;
}
fd = fileno(nfp);
if (fd == -1 || fchown(fd, 0, 0) || fchmod(fd, 0644)) {
fclose(nfp);
goto err;
}
fp = fopen(_PATH_HOSTS, "r");
if (!fp) {
err = SR_ERR_INTERNAL;
fclose(nfp);
goto err;
}
while (fgets(buf, sizeof(buf), fp)) {
if (!strncmp(buf, hostip, strlen(hostip))) {
if (domain[0])
snprintf(buf, sizeof(buf), "%s\t%s.%s %s\n", hostip, hostnm, domain, hostnm);
else
snprintf(buf, sizeof(buf), "%s\t%s\n", hostip, hostnm);
}
fputs(buf, nfp);
}
fclose(fp);
fclose(nfp);
if (rename(_PATH_HOSTS "+", _PATH_HOSTS))
ERRNO("Failed activating changes to "_PATH_HOSTS);
/* skip in bootstrap, lldpd and avahi have not started yet */
if (systemf("runlevel >/dev/null 2>&1"))
goto err;
/* Inform any running lldpd and avahi of the change ... */
systemf("initctl -bq status lldpd && lldpcli configure system hostname %s", hostnm);
systemf("initctl -bq status mdns && avahi-set-host-name %s", hostnm);
systemf("initctl -bq touch netbrowse");
err:
if (fmt)
free(fmt);
if (err) {
ERROR("Failed activating changes.");
return err;
}
if (sys_reload_services())
return SR_ERR_SYS;
if (rc)
return rc;
return SR_ERR_OK;
}
@@ -1656,6 +1604,7 @@ err:
int ietf_system_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd)
{
int rc = SR_ERR_OK;
if ((rc = change_auth(session, config, diff, event, confd)))
return rc;
if ((rc = change_ntp(session, config, diff, event, confd)))
+23 -2
View File
@@ -50,6 +50,20 @@ static char *ip_cache(const char *ifname, char *str, size_t len)
return str;
}
static const char *get_vendor_class(struct lyd_node *cfg)
{
struct lyd_node *option;
LYX_LIST_FOR_EACH(lyd_child(cfg), option, "option") {
struct lyd_node *id = lydx_get_child(option, "id");
const char *name = lyd_get_value(id);
if (strcmp(name, "vendor-class") == 0 || strcmp(name, "60") == 0)
return lydx_get_cattr(option, "value");
}
return NULL;
}
static char *fallback_options(const char *ifname)
{
@@ -84,9 +98,11 @@ static void add(const char *ifname, struct lyd_node *cfg)
{
const char *metric = lydx_get_cattr(cfg, "route-preference");
const char *client_id = lydx_get_cattr(cfg, "client-id");
char vendor[128] = { 0 }, do_arp[20] = { 0 };
char *cid = NULL, *options = NULL;
const char *action = "disable";
const char *vendor_class;
char vendor[128] = { 0 };
char do_arp[20] = { 0 };
bool arping;
FILE *fp;
@@ -112,7 +128,12 @@ static void add(const char *ifname, struct lyd_node *cfg)
options = dhcp_options(ifname, cfg);
dhcp_os_name_version(vendor, sizeof(vendor));
/* Check for vendor-class option (option 60) */
vendor_class = get_vendor_class(cfg);
if (vendor_class)
snprintf(vendor, sizeof(vendor), "-V \"%s\"", vendor_class);
else
snprintf(vendor, sizeof(vendor), "-V \"\"");
fp = fopenf("w", "/etc/finit.d/available/dhcp-client-%s.conf", ifname);
if (!fp) {
+12 -27
View File
@@ -97,33 +97,6 @@ char *dhcp_fqdn(const char *val, char *str, size_t len)
return str;
}
char *dhcp_os_name_version(char *str, size_t len)
{
char *val;
if (!str || !len)
return NULL;
str[0] = 0;
val = fgetkey("/etc/os-release", "NAME");
if (val)
snprintf(str, len, "-V \"%.32s ", val);
val = fgetkey("/etc/os-release", "VERSION");
if (val) {
strlcat(str, val, len);
strlcat(str, "\"", len);
}
if (strlen(str) > 0 && str[strlen(str) - 1] != '"') {
str[0] = 0;
return NULL;
}
return str;
}
char *dhcp_compose_option(struct lyd_node *cfg, const char *ifname, struct lyd_node *id,
const char *val, const char *hex, char *option, size_t len,
char *(*ip_cache_cb)(const char *, char *, size_t))
@@ -136,6 +109,10 @@ char *dhcp_compose_option(struct lyd_node *cfg, const char *ifname, struct lyd_n
return NULL;
}
/* Skip option 60 (vendor-class) - handled separately via -V flag */
if (num == 60)
return NULL;
if (val || hex) {
switch (num) {
case 81: /* fqdn */
@@ -210,6 +187,8 @@ char *dhcp_compose_options(struct lyd_node *cfg, const char *ifname, char **opti
*/
static void infer_options_v4(sr_session_ctx_t *session, const char *xpath)
{
sr_val_t val = { .type = SR_STRING_T };
struct json_t *product_name;
const char *opt[] = {
"netmask",
"broadcast",
@@ -223,6 +202,12 @@ static void infer_options_v4(sr_session_ctx_t *session, const char *xpath)
for (i = 0; i < NELEMS(opt); i++)
srx_set_item(session, NULL, 0, "%s/option[id='%s']", xpath, opt[i]);
product_name = json_object_get(confd.root, "product-name");
if (product_name) {
val.data.string_val = (char *)json_string_value(product_name);
srx_set_item(session, &val, 0, "%s/option[id='vendor-class']/value", xpath);
}
}
/*
-1
View File
@@ -9,7 +9,6 @@
int dhcp_option_lookup(const struct lyd_node *id);
char *dhcp_hostname(struct lyd_node *cfg, char *str, size_t len);
char *dhcp_fqdn(const char *val, char *str, size_t len);
char *dhcp_os_name_version(char *str, size_t len);
char *dhcp_compose_option(struct lyd_node *cfg, const char *ifname, struct lyd_node *id,
const char *val, const char *hex, char *option, size_t len,
char *(*ip_cache_cb)(const char *, char *, size_t));
+5 -1
View File
@@ -8,7 +8,7 @@ module infix-dhcp-common {
description "Shared types between DHCP server and client.";
revision 2025-11-09 {
description "Add DHCPv6 options typedef.";
description "Add DHCPv6 options typedef and DHCPv4 vendor-class option.";
reference "internal";
}
revision 2025-01-29 {
@@ -79,6 +79,10 @@ module infix-dhcp-common {
value 50;
description "Request (previously cached) address";
}
enum vendor-class {
value 60;
description "Vendor class, e.g., product name, type/model";
}
enum client-id {
value 61;
description "Client ID (default MAC, and option 12)";
+5
View File
@@ -15,6 +15,7 @@ Tests verifying IETF standard interface configuration and management:
- Interface enable/disable state management
- Virtual Ethernet (VETH) pair lifecycle management
- GRE tunneling configuration and data plane operation
- Tunnel TTL (Time-To-Live) and hop-limit verification
include::vlan_ping/Readme.adoc[]
@@ -125,3 +126,7 @@ include::tunnel_basic/Readme.adoc[]
<<<
include::tunnel_bridged/Readme.adoc[]
<<<
include::tunnel_ttl/Readme.adoc[]
+4
View File
@@ -41,6 +41,10 @@ include::volume/Readme.adoc[]
<<<
include::upgrade/Readme.adoc[]
<<<
include::firewall_basic/Readme.adoc[]
<<<
+22 -7
View File
@@ -1,14 +1,17 @@
:testgroup:
== infix-dhcp
Tests verifying DHCP client and server functionality in Infix:
Tests verifying DHCPv4/DHCPv6 client and server functionality in Infix:
- Basic DHCP client lease acquisition and configuration
- DHCP client with default gateway assignment
- DHCP client with static route configuration
- Basic DHCP server operation and lease assignment
- DHCP server with host-specific IP reservations
- DHCP server with multiple subnet configurations
- Basic DHCPv4 client lease acquisition and configuration
- DHCPv4 client with default gateway assignment
- DHCPv4 client with static route configuration
- DHCPv4 client hostname management and priority
- Basic DHCPv6 client operation with address assignment
- DHCPv6 client with prefix delegation (IA_PD)
- Basic DHCPv4 server operation and lease assignment
- DHCPv4 server with host-specific IP reservations
- DHCPv4 server with multiple subnet configurations
include::client_basic/Readme.adoc[]
@@ -22,6 +25,18 @@ include::client_routes/Readme.adoc[]
<<<
include::client_hostname/Readme.adoc[]
<<<
include::client6_basic/Readme.adoc[]
<<<
include::client6_prefix_delegation/Readme.adoc[]
<<<
include::server_basic/Readme.adoc[]
<<<
+1
View File
@@ -0,0 +1 @@
test.adoc
@@ -0,0 +1,26 @@
=== DHCP Hostname Priority
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_dhcp/client_hostname]
==== Description
Verify deterministic hostname management: a DHCP acquired hostname takes
precedence over a configured hostname. When a DHCP lease ends, or the
hostname option is removed, the system should revert to the configured
hostname.
==== Topology
image::topology.svg[DHCP Hostname Priority topology, align=center, scaledwidth=75%]
==== Sequence
. Set up topology and attach to target DUT
. Configure static system hostname
. Verify configured hostname is set
. Enable DHCP client requesting hostname option
. Verify DHCP hostname takes precedence
. Drop hostname option from client request
. Verify hostname reverts to configured value
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""DHCP Hostname Priority
Verify deterministic hostname management: a DHCP acquired hostname takes
precedence over a configured hostname. When a DHCP lease ends, or the
hostname option is removed, the system should revert to the configured
hostname.
"""
import infamy, infamy.dhcp
from infamy.util import until
def verify_hostname(node, expected):
"""Verify operational hostname matches expected value"""
data = node.get_data("/ietf-system:system")
return data["system"]["hostname"] == expected
with infamy.Test() as test:
DHCP_HOSTNAME = "dhcp-assigned"
CONF_HOSTNAME = "configured-host"
with test.step("Set up topology and attach to target DUT"):
env = infamy.Env()
client = env.attach("client", "mgmt")
_, host = env.ltop.xlate("host", "mgmt")
_, port = env.ltop.xlate("client", "mgmt")
with test.step("Configure static system hostname"):
client.put_config_dict("ietf-system", {
"system": {
"hostname": CONF_HOSTNAME
}
})
with test.step("Verify configured hostname is set"):
until(lambda: verify_hostname(client, CONF_HOSTNAME))
with infamy.IsolatedMacVlan(host, mode="private") as netns:
netns.addip("10.0.0.1")
with infamy.dhcp.Server(netns, ip="10.0.0.42", hostname=DHCP_HOSTNAME):
with test.step("Enable DHCP client requesting hostname option"):
client.put_config_dict("ietf-interfaces", {
"interfaces": {
"interface": [{
"name": port,
"ipv4": {
"infix-dhcp-client:dhcp": {
"option": [
{"id": "vendor-class", "value": "infamy"},
{"id": "hostname"},
{"id": "netmask"},
{"id": "router"}
]
}
}
}]
}
})
with test.step("Verify DHCP hostname takes precedence"):
until(lambda: verify_hostname(client, DHCP_HOSTNAME))
with test.step("Drop hostname option from client request"):
path = f"/ietf-interfaces:interfaces/interface[name='{port}']" \
+ "/ietf-ip:ipv4/infix-dhcp-client:dhcp/option[id='hostname']"
client.delete_xpath(path)
with test.step("Verify hostname reverts to configured value"):
until(lambda: verify_hostname(client, CONF_HOSTNAME))
test.succeed()
@@ -0,0 +1,22 @@
graph "1x1" {
layout="neato";
overlap="false";
esep="+100";
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="host | { <mgmt> mgmt }",
pos="0,20!",
requires="controller",
];
client [
label="{ <mgmt> mgmt } | client",
pos="200,20!",
requires="infix",
];
host:mgmt -- client:mgmt [requires="mgmt", color=lightgrey]
}
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: 1x1 Pages: 1 -->
<svg width="474pt" height="45pt"
viewBox="0.00 0.00 474.04 45.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 41)">
<title>1x1</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-41 470.04,-41 470.04,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-0.5 0,-36.5 100,-36.5 100,-0.5 0,-0.5"/>
<text text-anchor="middle" x="25" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-0.5 50,-36.5 "/>
<text text-anchor="middle" x="75" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
</g>
<!-- client -->
<g id="node2" class="node">
<title>client</title>
<polygon fill="none" stroke="black" points="350.04,-0.5 350.04,-36.5 466.04,-36.5 466.04,-0.5 350.04,-0.5"/>
<text text-anchor="middle" x="375.04" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="400.04,-0.5 400.04,-36.5 "/>
<text text-anchor="middle" x="433.04" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">client</text>
</g>
<!-- host&#45;&#45;client -->
<g id="edge1" class="edge">
<title>host:mgmt&#45;&#45;client:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M100,-18.5C100,-18.5 350.04,-18.5 350.04,-18.5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+3
View File
@@ -11,6 +11,9 @@
- name: DHCP option 121 vs option 3
case: client_routes/test.py
- name: DHCP Hostname Priority
case: client_hostname/test.py
- name: DHCPv6 Basic
case: client6_basic/test.py
+1
View File
@@ -228,6 +228,7 @@ if [ "$containerize" ]; then
--cap-add=NET_ADMIN \
--device=/dev/net/tun \
--env PYTHONHASHSEED=${PYTHONHASHSEED:-$(shuf -i 0-$(((1 << 32) - 1)) -n 1)} \
--env PYTHONPYCACHEDIR=/tmp/__pycache__ \
--env VIRTUAL_ENV_DISABLE_PROMPT=yes \
--env INFAMY_ARGS="$INFAMY_ARGS" \
--env INFAMY_EXTRA_ARGS="$INFAMY_EXTRA_ARGS" \
+7 -3
View File
@@ -8,11 +8,13 @@ class Server:
config_file = '/tmp/udhcpd.conf'
leases_file = '/tmp/udhcpd.leases'
def __init__(self, netns, start='192.168.0.100', end='192.168.0.110', netmask='255.255.255.0', ip=None, router=None, prefix=None, iface="iface"):
def __init__(self, netns, start='192.168.0.100', end='192.168.0.110',
netmask='255.255.255.0', ip=None, router=None, prefix=None,
hostname=None, iface="iface"):
self.process = None
self.netns = netns
self.iface = iface
self._create_files(start, end, netmask, ip, router, prefix)
self._create_files(start, end, netmask, ip, router, prefix, hostname)
def __del__(self):
#print(self.config_file)
@@ -26,7 +28,7 @@ class Server:
def __exit__(self, _, __, ___):
self.stop()
def _create_files(self, start, end, netmask, ip, router, prefix):
def _create_files(self, start, end, netmask, ip, router, prefix, hostname):
f = open(self.leases_file, "w")
f.close()
@@ -47,6 +49,8 @@ option lease 864000
f.write(f"option router {router}\n")
if prefix and router:
f.write(f"option staticroutes {prefix} {router}\n")
if hostname:
f.write(f"option hostname {hostname}\n")
def get_pid(self):
return self.process.pid
+46 -7
View File
@@ -28,6 +28,17 @@ class IsolatedMacVlans:
NOTE: For the simple case when only one interface needs to be
mapped, see IsolatedMacVlan below.
Args:
ifmap: Dictionary mapping parent interface names to MACVLAN names
lo: Enable loopback interface in the namespace (default: True)
set_up: Automatically bring up the interfaces (default: True)
mode: MACVLAN mode to use (default: "passthru")
- "passthru": Exclusive access, single MACVLAN per parent.
Parent interface becomes promiscuous.
- "bridge": Shared access, allows multiple MACVLANs to
communicate. Required for layer-2 tests that
need full control of all frames.
Example:
netns = IsolatedMacVlans({ "eth2": "a", "eth3": "b" })
@@ -46,9 +57,10 @@ class IsolatedMacVlans:
for ns in list(IsolatedMacVlans.Instances):
ns.stop()
def __init__(self, ifmap, lo=True, set_up=True):
def __init__(self, ifmap, lo=True, set_up=True, mode="passthru"):
self.sleeper = None
self.ifmap, self.lo, self.set_up = ifmap, lo, set_up
self.mode = mode
self.ping_timeout = env.ENV.attr("ping_timeout", 5)
def start(self):
@@ -64,7 +76,8 @@ class IsolatedMacVlans:
"link", parent,
"address", self._stable_mac(parent),
"netns", str(self.sleeper.pid),
"type", "macvlan", "mode", "passthru"], check=True)
"type", "macvlan", "mode", self.mode],
check=True)
self.runsh(f"""
while ! ip link show dev {ifname}; do
sleep 0.1
@@ -287,6 +300,17 @@ class IsolatedMacVlan(IsolatedMacVlans):
moves that interface to a separate namespace, isolating it from
all other interfaces.
Args:
parent: Name of the parent interface on the controller
ifname: Name of the MACVLAN interface in the namespace (default: "iface")
lo: Enable loopback interface in the namespace (default: True)
set_up: Automatically bring up the interface (default: True)
mode: MACVLAN mode to use (default: "passthru")
- "passthru": Exclusive access, single MACVLAN per parent.
- "bridge": Shared access, required for layer-2 tests that
need to communicate with other MACVLANs on the
same parent or control all frames.
Example:
netns = IsolatedMacVlan("eth3")
@@ -298,13 +322,19 @@ class IsolatedMacVlan(IsolatedMacVlans):
|
eth0 eth1 eth2 eth3
Example with bridge mode:
netns = IsolatedMacVlan("eth3", mode="bridge")
"""
def __init__(self, parent, ifname="iface", lo=True, set_up=True):
def __init__(self, parent, ifname="iface", lo=True, set_up=True, mode="passthru"):
self._ifname = ifname
return super().__init__(ifmap={ parent: ifname }, lo=lo, set_up=set_up)
return super().__init__(ifmap={parent: ifname}, lo=lo, set_up=set_up,
mode=mode)
def addip(self, addr, prefix_length=24, proto="ipv4"):
return super().addip(ifname=self._ifname, addr=addr, prefix_length=prefix_length, proto=proto)
return super().addip(ifname=self._ifname, addr=addr,
prefix_length=prefix_length, proto=proto)
def must_receive(self, expr, timeout=None, ifname=None, must=True):
ifname = ifname if ifname else self._ifname
@@ -387,10 +417,19 @@ class TPMR(IsolatedMacVlans):
This is useful to verify the correctness of fail-over behavior in
various protocols. See ospf_bfd for a usage example.
Args:
a: Name of the first parent interface on the controller
b: Name of the second parent interface on the controller
mode: MACVLAN mode to use (default: "passthru")
- "passthru": Exclusive access (default)
- "bridge": Shared access, allows communication between MACVLANs
and full control of all frames. May be required for
proper layer-2 relay functionality in some tests.
"""
def __init__(self, a, b):
super().__init__(ifmap={ a: "a", b: "b" }, lo=False)
def __init__(self, a, b, mode="passthru"):
super().__init__(ifmap={ a: "a", b: "b" }, lo=False, mode=mode)
def start(self, forward=True):
ret = super().start()