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. Maintainer checklists for reviewing pull requests and doing releases.
Pull Requests Pull Requests
------------- -------------
- If applicable, is there a readable ChangeLog entry? - If applicable, is there a readable ChangeLog entry?
- If any LICENSE file has been updated, has the `.hash` file been updated? - 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? - 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 I.e., is there a runlevel and/or condition defined to prevent them
from running outside of their intended runlevel? 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 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 u-boot/buildroot, has it been tested with `<booloader>_defconfig`
- If any change to logging, have the resulting logs been audited? - If any change to logging, have the resulting logs been audited?
- Check for duplicate entries, misspellings - Check for duplicate entries, misspellings
- Check for sneaky severity changes, e.g., error vs note, error vs warning - 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 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 mDNS, has it been tested with netbrowse?
- If change to `_defconfig`, verify `local.mk` and sync with other archs - 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 - 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 - Build from distclean, or use artifacts built by build servers, for manual tests
Releases Releases
-------- --------
Recommended checkpoints, use at your own discretion: Recommended checkpoints, use at your own discretion:
- Make at least one -betaN release to verify the GitHub workflow well in time release day - Make at least one -betaN release to verify the GitHub workflow well in time release day
- Stuff happens, remember kernelkit/infix#735 - Stuff happens, remember kernelkit/infix#735
- Make at least one -rcN to flush out any issues in customer repos - 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 - Easy to forget adaptations/hacks in customer repos -- may need Infix change/support
- Verify release artifacts (checksums, completeness, no corrupted files) - Verify release artifacts (checksums, completeness, no corrupted files)
- Test on actual hardware for at least one architecture - Test on actual hardware for at least one architecture
- Review ChangeLog for completeness - Review ChangeLog for completeness
- Check for release-blocking issues - Check for release-blocking issues
- Verify generated GNS3 appliance, no marketplace update on -rc builds - Verify generated GNS3 appliance, no marketplace update on -rc builds
- Ensure the markdown link for the release diff is updated - Ensure the markdown link for the release diff is updated
- Ensure subrepos are tagged (can be automated, see kernelkit/infix#393) - Ensure subrepos are tagged (can be automated, see kernelkit/infix#393)
- Sync tags for all repo. sync activities - 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: If you are unsure how to start implementing an idea or fix:
- :bug: open an issue, there are human friendly templates for _bugs_ - :bug: open an issue, there are human friendly templates for _bugs_
and _feature requests_ at <https://github.com/kernelkit/infix/issues> and _feature requests_ at <https://github.com/kernelkit/infix/issues>
- :speech_balloon: use the [Q&A Forum][discuss] - :speech_balloon: use the [Q&A Forum][discuss]
- :technologist: The [Developer's Guide][devguide] is also a useful start - :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 > started before submitting a pull request. We have found it always
> saves time, yours and ours._ > saves time, yours and ours.
:sparkles: General Guidelines :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 *why* -- from your perspective, why is it a bug, why does the code need
changing in this way. Start with why. changing in this way. Start with why.
- :bug: Bug reports need metadata like Infix version or commit hash - :bug: Bug reports need metadata like Infix version or commit hash
- :adhesive_bandage: Bug fixes also need version, and (preferably) a - :adhesive_bandage: Bug fixes also need version, and (preferably) a
corresponding issue number for the ChangeLog corresponding issue number for the ChangeLog
- :new: New features, you need to get approval of the YANG model first! - :new: New features, you need to get approval of the YANG model first!
:speech_balloon: Please use the [Forum][discuss], e.g., category: :speech_balloon: Please use the [Forum][discuss], e.g., category:
*Ideas*, or open a :pray: feature request issue *Ideas*, or open a :pray: feature request issue
- :white_check_mark: New features also need new regression tests, this - :white_check_mark: New features also need new regression tests, this
can be basic tests or more complex use-case tests comprising multiple can be basic tests or more complex use-case tests comprising multiple
subsystems, see [Testing Changes](#test_tube-testing-changes), below subsystems, see [Testing Changes](#test_tube-testing-changes), below
Please take care to ensure you follow the project coding style and the Please take care to ensure you follow the project coding style and the
commit message format. If you follow these recommendations you help commit message format. If you follow these recommendations you help
the maintainers and make it easier for them to include your code. the maintainers and make it easier for them to include your code.
:woman_technologist: Coding Style :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 release notes (ChangeLog) and at least a configuration example in the
manual for new features. manual for new features.
> **Tip:** consider ["Readme driven development"][RDD] for new features. > [!TIP]
> It is amazing how many flaws in your own bright ideas come to bare > Consider ["Readme driven development"][RDD] for new features. It is
> when you suddenly have to explain them to someone else! > 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: We expect code contributions for:
- C code in [Linux Coding Style][Linux] - C code in [Linux Coding Style][Linux]
- Python code should follow [PEP-8][] - Python code should follow [PEP-8][]
> [!IMPORTANT]
> **However,** always submit code that follows the style of surrounding > **However,** always submit code that follows the style of surrounding
> code! Legacy takes precedence, and remember, we read code a lot more > code! Legacy takes precedence, and remember, we read code a lot more
> than write it, so legibility is important. > than write it, so legibility is important.
The ChangeLog deserves a separate mention: The ChangeLog deserves a separate mention:
- Releases are listed in reverse chronological order order, so the - Releases are listed in reverse chronological order order, so the
latest/next release is at the beginning of the file latest/next release is at the beginning of the file
- Only *user-facing bugs and features* are detailed, so code refactor, - Only *user-facing bugs and features* are detailed, so code refactor,
new tests, etc. are not listed. new tests, etc. are not listed.
- Add your changes/features to the Changes section - Add your changes/features to the Changes section
- Add your Fix line in the Fixes section, in numeric order - Add your Fix line in the Fixes section, in numeric order
- Changes and fixes without an issue number are listed after all - Changes and fixes without an issue number are listed after all
numbered ones numbered ones
- YANG model changes are documented in their respective model, for - YANG model changes are documented in their respective model, for
standard models, e.g., for `ietf-interfaces.yang`, the corresponding standard models, e.g., for `ietf-interfaces.yang`, the corresponding
`infix-interfaces.yang` detail augments/deviations as revisions. `infix-interfaces.yang` detail augments/deviations as revisions.
A final note, lines of code are allowed to be longer than 72 characters 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 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 maximum, but the team usually keep it around 100 characters for both C
and Python. and Python.
:test_tube: Testing Changes :test_tube: Testing Changes
--------------------------- ---------------------------
@@ -97,9 +97,8 @@ the same pull request.
For help getting started with testing, see the following resources: For help getting started with testing, see the following resources:
- [Developer's Guide][devguide] - [Developer's Guide][devguide]
- [Regression Testing][testing] - [Regression Testing][testing]
:memo: Commit Messages :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"> <img src="../doc/jack.png" width=70 align="right">
$ git config --global user.name "Jacky Linker" ```bash
$ git config --global user.email jacky.linker@example.com $ 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 Example commit message from one of many [online guides][cbeams]. Use
`git commit -s` to automatically add a `Signed-off-by` for proof of `git commit -s` to automatically add a `Signed-off-by` for proof of
origin, see [DCO][] for more info. origin, see [DCO][] for more info.
subsystem: brief, but clear and concise summary of changes ```text
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
If you use an issue tracker, put references to them at the bottom, More detailed explanatory text, if necessary. Wrap it to about 72
like this: 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 Further paragraphs should be separated with empty lines.
See also: #456, #789
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 This is an example of how to [automatically close][closing] an issue
when the commit is merged to mainline. Several keywords are available. 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 :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 A pull request should preferably address a single issue or change. This
may of course include multiple related changes, but what is important to 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 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. lost when the changes are merged to the mainline branch.
:balance_scale: Code of Conduct :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/ [PEP-8]: https://peps.python.org/pep-0008/
[RDD]: https://tom.preston-werner.com/2010/08/23/readme-driven-development [RDD]: https://tom.preston-werner.com/2010/08/23/readme-driven-development
[cbeams]: https://cbea.ms/git-commit/#seven-rules [cbeams]: https://cbea.ms/git-commit/#seven-rules
[conduct]: CODE-OF-CONDUCT.md [conduct]: CODE-OF-CONDUCT.md
[DCO]: https://developercertificate.org/ [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 [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": "hostname"},
{"id": "dns-server"}, {"id": "dns-server"},
{"id": "router"}, {"id": "router"},
{"id": "netmask"} {"id": "netmask"},
{"id": "vendor-class", "value": "Banana Pi BPI-R3"}
] ]
} }
} }
@@ -68,7 +68,8 @@
{"id": "hostname"}, {"id": "hostname"},
{"id": "dns-server"}, {"id": "dns-server"},
{"id": "router"}, {"id": "router"},
{"id": "netmask"} {"id": "netmask"},
{"id": "vendor-class", "value": "NanoPi R2S"}
] ]
} }
} }
@@ -47,7 +47,8 @@
{"id": "domain"}, {"id": "domain"},
{"id": "hostname"}, {"id": "hostname"},
{"id": "dns-server"}, {"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] flat_devices = [device for sublist in self.infix_devices("vpds") for device in sublist]
return [self.into_vpd(device) for device in flat_devices] 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: class QEMUSystem:
BASE = "/sys/firmware/qemu_fw_cfg" BASE = "/sys/firmware/qemu_fw_cfg"
@@ -355,6 +436,9 @@ def probe_qemusystem(out):
if not out[attr]: if not out[attr]:
out[attr] = default out[attr] = default
if os.path.exists(DMISystem.BASE):
DMISystem().populate(out)
if not out["factory-password-hash"] and \ if not out["factory-password-hash"] and \
not out["vpd"]["product"]["available"]: not out["vpd"]["product"]["available"]:
# Virtual instance without VPD emulation, fallback to # 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] 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): def probe_dtsystem(out):
"""Probe DTS based system, expects a VPD in ONIE PROM format.""" """Probe DTS based system, expects a VPD in ONIE PROM format."""
dtsys = DTSystem() dtsys = DTSystem()
@@ -413,12 +512,24 @@ def probe_dtsystem(out):
out["compatible"] = dtsys.base.str_array("compatible") 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") staticpw = dtsys.infix.str("factory-password-hash")
if not out["factory-password-hash"]: if not out["factory-password-hash"]:
out["factory-password-hash"] = staticpw out["factory-password-hash"] = staticpw
vpd_inject(out, vpds) 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) # Fallback to interface MAC if VPD doesn't provide one (e.g., SBCs)
if not out["mac-address"]: if not out["mac-address"]:
out["mac-address"] = fallback_base_mac() out["mac-address"] = fallback_base_mac()
@@ -430,6 +541,7 @@ def main():
out = { out = {
"vendor": None, "vendor": None,
"product-name": None, "product-name": None,
"product-version": None,
"part-number": None, "part-number": None,
"serial-number": None, "serial-number": None,
"mac-address": None, "mac-address": None,
@@ -440,6 +552,8 @@ def main():
if os.path.exists(QEMUSystem.REV): if os.path.exists(QEMUSystem.REV):
err = probe_qemusystem(out) err = probe_qemusystem(out)
elif os.path.exists(DMISystem.BASE):
err = probe_dmisystem(out)
elif os.path.exists(DTSystem.BASE): elif os.path.exists(DTSystem.BASE):
err = probe_dtsystem(out) err = probe_dtsystem(out)
else: 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!" 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 # RFC3442: If the DHCP server returns both a Classless
# Static Routes option and a Router option, the DHCP # Static Routes option and a Router option, the DHCP
# client MUST ignore the Router option. # client MUST ignore the Router option.
@@ -57,17 +77,25 @@ set_dhcp_routes()
{ {
echo "! Generated by udhcpc" > "$NEXT" echo "! Generated by udhcpc" > "$NEXT"
if [ -n "$staticroutes" ]; then if [ -n "$staticroutes" ]; then
# format: dest1/mask gw1 ... destn/mask gwn if was_option_requested 121; then
set -- $staticroutes # format: dest1/mask gw1 ... destn/mask gwn
while [ -n "$1" -a -n "$2" ]; do set -- $staticroutes
dbg "adding route $1 via $2 metric $metric tag 100" while [ -n "$1" -a -n "$2" ]; do
echo "ip route $1 $2 $metric tag 100" >> "$NEXT" dbg "adding route $1 via $2 metric $metric tag 100"
shift 2 echo "ip route $1 $2 $metric tag 100" >> "$NEXT"
done shift 2
done
else
log "ignoring unrequested staticroutes (option 121)"
fi
elif [ -n "$router" ] ; then elif [ -n "$router" ] ; then
for i in $router ; do if was_option_requested 3; then
echo "ip route 0.0.0.0/0 $i $metric tag 100" >> "$NEXT" for i in $router ; do
done echo "ip route 0.0.0.0/0 $i $metric tag 100" >> "$NEXT"
done
else
log "ignoring unrequested router (option 3)"
fi
fi fi
# Reduce changes needed by comparing with previous route(s) # Reduce changes needed by comparing with previous route(s)
@@ -108,6 +136,11 @@ case "$ACTION" in
# drop info from this interface # drop info from this interface
rm -f "$RESOLV_CONF" rm -f "$RESOLV_CONF"
rm -f "$NTPFILE" 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 if [ -x /usr/sbin/avahi-autoipd ]; then
/usr/sbin/avahi-autoipd -c $interface && /usr/sbin/avahi-autoipd -k $interface /usr/sbin/avahi-autoipd -c $interface && /usr/sbin/avahi-autoipd -k $interface
fi fi
@@ -133,21 +166,35 @@ case "$ACTION" in
set_dhcp_routes set_dhcp_routes
# set hostname if given # set hostname if given and requested
if [ -n "$hostname" ]; then if [ -n "$hostname" ]; then
log "setting new hostname: $hostname" if was_option_requested 12; then
hostname "$hostname" log "received DHCP hostname: $hostname"
sed -i -E "s/^(127\.0\.1\.1\s+).*/\1$hostname/" /etc/hosts 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 fi
# drop info from this interface # drop info from this interface
truncate -s 0 "$RESOLV_CONF" truncate -s 0 "$RESOLV_CONF"
# prefer rfc3397 domain search list (option 119) if available # prefer rfc3397 domain search list (option 119) if available
search_list=""
if [ -n "$search" ]; then 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 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 fi
if [ -n "$search_list" ]; then if [ -n "$search_list" ]; then
@@ -155,19 +202,29 @@ case "$ACTION" in
echo "search $search_list # $interface" >> $RESOLV_CONF echo "search $search_list # $interface" >> $RESOLV_CONF
fi fi
for i in $dns ; do if [ -n "$dns" ]; then
dbg "adding dns $i" if was_option_requested 6; then
echo "nameserver $i # $interface" >> $RESOLV_CONF for i in $dns ; do
resolvconf -u dbg "adding dns $i"
done echo "nameserver $i # $interface" >> $RESOLV_CONF
resolvconf -u
done
else
log "ignoring unrequested dns (option 6): $dns"
fi
fi
if [ -n "$ntpsrv" ]; then if [ -n "$ntpsrv" ]; then
truncate -s 0 "$NTPFILE" if was_option_requested 42; then
for srv in $ntpsrv; do truncate -s 0 "$NTPFILE"
dbg "got NTP server $srv" for srv in $ntpsrv; do
echo "server $srv iburst" >> "$NTPFILE" dbg "got NTP server $srv"
done echo "server $srv iburst" >> "$NTPFILE"
chronyc reload sources >/dev/null done
chronyc reload sources >/dev/null
else
log "ignoring unrequested ntpsrv (option 42): $ntpsrv"
fi
fi fi
esac esac
+4 -2
View File
@@ -52,11 +52,13 @@ All notable changes to the project are documented in this file.
### Fixes ### Fixes
- Fix #855: User admin sometimes fails to be added to `wheel` group - 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 - 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 stub or NSSA. The backbone must always be a normal area per RFC 2328. Any
existing invalid configurations are automatically corrected during upgrade existing invalid configurations are automatically corrected during upgrade
- Fix serious regression in boot time, introduced in v25.10, delays the - Fix #1255: serious regression in boot time, introduced in v25.10, delays the
boot step "Mounting filesystems ..." with up to 30 seconds! boot step "Mounting filesystems ...", from 30 seconds up to five minutes!
- Fix broken intra-document links in container and tunnel documentation - Fix broken intra-document links in container and tunnel documentation
[lastest-boot]: https://github.com/kernelkit/infix/releases/latest-boot [lastest-boot]: https://github.com/kernelkit/infix/releases/latest-boot
+22 -73
View File
@@ -206,11 +206,6 @@ done:
return rc; return rc;
} }
static int sys_reload_services(void)
{
return systemf("initctl -nbq touch sysklogd");
}
#define TIMEZONE_CONF "/etc/timezone" #define TIMEZONE_CONF "/etc/timezone"
#define TIMEZONE_PREV TIMEZONE_CONF "-" #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) 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 hostnm[65], domain[65];
char buf[256], *fmt; int rc = SR_ERR_OK;
FILE *nfp, *fp; char *fmt;
int err, fd; FILE *fp;
if (event != SR_EV_DONE || !lydx_get_xpathf(diff, XPATH_HOSTNAME_)) if (event != SR_EV_DONE || !lydx_get_xpathf(diff, XPATH_HOSTNAME_))
return SR_ERR_OK; 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); fmt = strdup(nm);
if (hostnamefmt(confd, fmt, hostnm, sizeof(hostnm), domain, sizeof(domain))) { if (hostnamefmt(confd, fmt, hostnm, sizeof(hostnm), domain, sizeof(domain))) {
err = SR_ERR_SYS; rc = SR_ERR_SYS;
goto err; goto failed;
} }
err = sethostname(hostnm, strlen(hostnm)); /* Use hostname.d for deterministic hostname management */
if (err) { systemf("mkdir -p /etc/hostname.d");
ERROR("failed setting hostname");
err = SR_ERR_SYS;
goto err;
}
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))) { if (domain[0] && setdomainname(domain, strlen(domain))) {
ERROR("failed setting domain name"); ERROR("failed setting domain name");
/* Not cause for failing this function */ /* Not cause for failing this function */
} }
fp = fopen(_PATH_HOSTNAME, "w"); if (systemf("/usr/libexec/infix/hostname")) {
if (!fp) { failed:
err = SR_ERR_INTERNAL; ERROR("failed setting hostname");
goto err; 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) if (fmt)
free(fmt); free(fmt);
if (rc)
if (err) { return rc;
ERROR("Failed activating changes.");
return err;
}
if (sys_reload_services())
return SR_ERR_SYS;
return SR_ERR_OK; 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 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; int rc = SR_ERR_OK;
if ((rc = change_auth(session, config, diff, event, confd))) if ((rc = change_auth(session, config, diff, event, confd)))
return rc; return rc;
if ((rc = change_ntp(session, config, diff, event, confd))) 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; 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) 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 *metric = lydx_get_cattr(cfg, "route-preference");
const char *client_id = lydx_get_cattr(cfg, "client-id"); const char *client_id = lydx_get_cattr(cfg, "client-id");
char vendor[128] = { 0 }, do_arp[20] = { 0 };
char *cid = NULL, *options = NULL; char *cid = NULL, *options = NULL;
const char *action = "disable"; const char *action = "disable";
const char *vendor_class;
char vendor[128] = { 0 };
char do_arp[20] = { 0 };
bool arping; bool arping;
FILE *fp; FILE *fp;
@@ -112,7 +128,12 @@ static void add(const char *ifname, struct lyd_node *cfg)
options = dhcp_options(ifname, 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); fp = fopenf("w", "/etc/finit.d/available/dhcp-client-%s.conf", ifname);
if (!fp) { if (!fp) {
+12 -27
View File
@@ -97,33 +97,6 @@ char *dhcp_fqdn(const char *val, char *str, size_t len)
return str; 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, 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, const char *val, const char *hex, char *option, size_t len,
char *(*ip_cache_cb)(const char *, char *, size_t)) 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; return NULL;
} }
/* Skip option 60 (vendor-class) - handled separately via -V flag */
if (num == 60)
return NULL;
if (val || hex) { if (val || hex) {
switch (num) { switch (num) {
case 81: /* fqdn */ 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) 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[] = { const char *opt[] = {
"netmask", "netmask",
"broadcast", "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++) for (i = 0; i < NELEMS(opt); i++)
srx_set_item(session, NULL, 0, "%s/option[id='%s']", xpath, 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); int dhcp_option_lookup(const struct lyd_node *id);
char *dhcp_hostname(struct lyd_node *cfg, char *str, size_t len); 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_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, 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, const char *val, const char *hex, char *option, size_t len,
char *(*ip_cache_cb)(const char *, char *, size_t)); 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."; description "Shared types between DHCP server and client.";
revision 2025-11-09 { revision 2025-11-09 {
description "Add DHCPv6 options typedef."; description "Add DHCPv6 options typedef and DHCPv4 vendor-class option.";
reference "internal"; reference "internal";
} }
revision 2025-01-29 { revision 2025-01-29 {
@@ -79,6 +79,10 @@ module infix-dhcp-common {
value 50; value 50;
description "Request (previously cached) address"; description "Request (previously cached) address";
} }
enum vendor-class {
value 60;
description "Vendor class, e.g., product name, type/model";
}
enum client-id { enum client-id {
value 61; value 61;
description "Client ID (default MAC, and option 12)"; 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 - Interface enable/disable state management
- Virtual Ethernet (VETH) pair lifecycle management - Virtual Ethernet (VETH) pair lifecycle management
- GRE tunneling configuration and data plane operation - GRE tunneling configuration and data plane operation
- Tunnel TTL (Time-To-Live) and hop-limit verification
include::vlan_ping/Readme.adoc[] include::vlan_ping/Readme.adoc[]
@@ -125,3 +126,7 @@ include::tunnel_basic/Readme.adoc[]
<<< <<<
include::tunnel_bridged/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[] include::firewall_basic/Readme.adoc[]
<<< <<<
+22 -7
View File
@@ -1,14 +1,17 @@
:testgroup: :testgroup:
== infix-dhcp == 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 - Basic DHCPv4 client lease acquisition and configuration
- DHCP client with default gateway assignment - DHCPv4 client with default gateway assignment
- DHCP client with static route configuration - DHCPv4 client with static route configuration
- Basic DHCP server operation and lease assignment - DHCPv4 client hostname management and priority
- DHCP server with host-specific IP reservations - Basic DHCPv6 client operation with address assignment
- DHCP server with multiple subnet configurations - 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[] 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[] 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 - name: DHCP option 121 vs option 3
case: client_routes/test.py case: client_routes/test.py
- name: DHCP Hostname Priority
case: client_hostname/test.py
- name: DHCPv6 Basic - name: DHCPv6 Basic
case: client6_basic/test.py case: client6_basic/test.py
+1
View File
@@ -228,6 +228,7 @@ if [ "$containerize" ]; then
--cap-add=NET_ADMIN \ --cap-add=NET_ADMIN \
--device=/dev/net/tun \ --device=/dev/net/tun \
--env PYTHONHASHSEED=${PYTHONHASHSEED:-$(shuf -i 0-$(((1 << 32) - 1)) -n 1)} \ --env PYTHONHASHSEED=${PYTHONHASHSEED:-$(shuf -i 0-$(((1 << 32) - 1)) -n 1)} \
--env PYTHONPYCACHEDIR=/tmp/__pycache__ \
--env VIRTUAL_ENV_DISABLE_PROMPT=yes \ --env VIRTUAL_ENV_DISABLE_PROMPT=yes \
--env INFAMY_ARGS="$INFAMY_ARGS" \ --env INFAMY_ARGS="$INFAMY_ARGS" \
--env INFAMY_EXTRA_ARGS="$INFAMY_EXTRA_ARGS" \ --env INFAMY_EXTRA_ARGS="$INFAMY_EXTRA_ARGS" \
+7 -3
View File
@@ -8,11 +8,13 @@ class Server:
config_file = '/tmp/udhcpd.conf' config_file = '/tmp/udhcpd.conf'
leases_file = '/tmp/udhcpd.leases' 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.process = None
self.netns = netns self.netns = netns
self.iface = iface 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): def __del__(self):
#print(self.config_file) #print(self.config_file)
@@ -26,7 +28,7 @@ class Server:
def __exit__(self, _, __, ___): def __exit__(self, _, __, ___):
self.stop() 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 = open(self.leases_file, "w")
f.close() f.close()
@@ -47,6 +49,8 @@ option lease 864000
f.write(f"option router {router}\n") f.write(f"option router {router}\n")
if prefix and router: if prefix and router:
f.write(f"option staticroutes {prefix} {router}\n") f.write(f"option staticroutes {prefix} {router}\n")
if hostname:
f.write(f"option hostname {hostname}\n")
def get_pid(self): def get_pid(self):
return self.process.pid 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 NOTE: For the simple case when only one interface needs to be
mapped, see IsolatedMacVlan below. 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: Example:
netns = IsolatedMacVlans({ "eth2": "a", "eth3": "b" }) netns = IsolatedMacVlans({ "eth2": "a", "eth3": "b" })
@@ -46,9 +57,10 @@ class IsolatedMacVlans:
for ns in list(IsolatedMacVlans.Instances): for ns in list(IsolatedMacVlans.Instances):
ns.stop() 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.sleeper = None
self.ifmap, self.lo, self.set_up = ifmap, lo, set_up self.ifmap, self.lo, self.set_up = ifmap, lo, set_up
self.mode = mode
self.ping_timeout = env.ENV.attr("ping_timeout", 5) self.ping_timeout = env.ENV.attr("ping_timeout", 5)
def start(self): def start(self):
@@ -64,7 +76,8 @@ class IsolatedMacVlans:
"link", parent, "link", parent,
"address", self._stable_mac(parent), "address", self._stable_mac(parent),
"netns", str(self.sleeper.pid), "netns", str(self.sleeper.pid),
"type", "macvlan", "mode", "passthru"], check=True) "type", "macvlan", "mode", self.mode],
check=True)
self.runsh(f""" self.runsh(f"""
while ! ip link show dev {ifname}; do while ! ip link show dev {ifname}; do
sleep 0.1 sleep 0.1
@@ -287,6 +300,17 @@ class IsolatedMacVlan(IsolatedMacVlans):
moves that interface to a separate namespace, isolating it from moves that interface to a separate namespace, isolating it from
all other interfaces. 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: Example:
netns = IsolatedMacVlan("eth3") netns = IsolatedMacVlan("eth3")
@@ -298,13 +322,19 @@ class IsolatedMacVlan(IsolatedMacVlans):
| |
eth0 eth1 eth2 eth3 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 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"): 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): def must_receive(self, expr, timeout=None, ifname=None, must=True):
ifname = ifname if ifname else self._ifname 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 This is useful to verify the correctness of fail-over behavior in
various protocols. See ospf_bfd for a usage example. 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): def __init__(self, a, b, mode="passthru"):
super().__init__(ifmap={ a: "a", b: "b" }, lo=False) super().__init__(ifmap={ a: "a", b: "b" }, lo=False, mode=mode)
def start(self, forward=True): def start(self, forward=True):
ret = super().start() ret = super().start()