mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-25 02:03:02 +02:00
Merge pull request #1258 from kernelkit/hostname-stability
Hostname stability Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
+27
-29
@@ -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
@@ -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
|
||||
|
||||
+2
-1
@@ -109,7 +109,8 @@
|
||||
{"id": "hostname"},
|
||||
{"id": "dns-server"},
|
||||
{"id": "router"},
|
||||
{"id": "netmask"}
|
||||
{"id": "netmask"},
|
||||
{"id": "vendor-class", "value": "Banana Pi BPI-R3"}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -68,7 +68,8 @@
|
||||
{"id": "hostname"},
|
||||
{"id": "dns-server"},
|
||||
{"id": "router"},
|
||||
{"id": "netmask"}
|
||||
{"id": "netmask"},
|
||||
{"id": "vendor-class", "value": "NanoPi R2S"}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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)))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)";
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -41,6 +41,10 @@ include::volume/Readme.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
include::upgrade/Readme.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
include::firewall_basic/Readme.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
@@ -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[]
|
||||
|
||||
<<<
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Executable
+74
@@ -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--client -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt--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 |
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user