canonical / canonical/cloud-init

extract_physdevs: unhandled TypeError on a v1 physical entry with no name. DataSourceHetzner emits one when a private NIC has not hotplugged yet, and first boot fails

Open
#6,982 2 comments 0 reactions 0 assignees View on GitHub
bug new
Dominant language
Python
Stars
3.8k
Forks
1.1k
Avg merge
2d 23h
Merged PRs (30d)
18

Description

## Bug report
TypeError in extract_physdevs when a network-config v1 physical entry has a mac_address and name is None; the Hetzner datasource emits such entries and first boot fails

A first boot on Hetzner Cloud can fail before cloud-init applies any user data. The instance boots, and SSH is reachable through the image defaults, but the instance stays unconfigured.

The cause has two parts.

`_version_1` in `cloudinit.net.extract_physdevs` skips entries that have no `mac_address`. It does not skip entries that have no `name`. An entry with a MAC and `name: None` reaches `device_driver(None)`. `sys_dev_path` then adds the name into a sysfs path string and raises:

TypeError: can only concatenate str (not "NoneType") to str

https://github.com/canonical/cloud-init/blob/8bf3567532b07e2cc15aa4c76c36ebed65ccfaec/cloudinit/net/__init__.py#L595-L611

https://github.com/canonical/cloud-init/blob/8bf3567532b07e2cc15aa4c76c36ebed65ccfaec/cloudinit/net/__init__.py#L61-L62

`DataSourceHetzner` emits entries of this shape. It creates one v1 physical entry for each attached private network. It sets `name` from a lookup of the MAC in the host's interfaces. The helper returns `Optional[str]`:

https://github.com/canonical/cloud-init/blob/8bf3567532b07e2cc15aa4c76c36ebed65ccfaec/cloudinit/sources/DataSourceHetzner.py#L215-L231

https://github.com/canonical/cloud-init/blob/8bf3567532b07e2cc15aa4c76c36ebed65ccfaec/cloudinit/sources/helpers/hetzner.py#L41

Hetzner attaches private NICs by hotplug. The hotplug can arrive after `init-local` builds the network config. The lookup then returns None, and the datasource emits the entry with `name: null`. The entry
also fails the v1 schema validation, but validation only produces a warning. The exception then exits `apply_network_config`, and the `init-local` and `init` stages fail. The invalid config stays in place for that boot, so every later stage fails in the same way.

The two code locations are identical on the current main branch (a6d999ae517d, checked 2026-08-07).

The scope is wider than one datasource. The parser crash is generic: any config source can supply a v1 physical entry with a MAC and no name, including user network-config. The silent `None` is specific to the Hetzner datasource. Other datasources with the same lookup raise a clear RuntimeError when the MAC is not found (for example, the DigitalOcean and UpCloud helpers).

Possible corrections, alone or combined:

- `extract_physdevs` skips physical entries that have no name, as it
already skips entries that have no MAC.
- `DataSourceHetzner` does not emit `name: None`. When the NIC is not
present, it omits the entry, or it omits `name` and lets cloud-init
match the device by MAC later.
- `wait_for_physdevs` identifies these entries as expected but not
present, and waits for the udev events. That wait is the purpose of
the function.

## Steps to reproduce the problem

Minimal reproducer, no cloud required:

from cloudinit.net import extract_physdevs

netcfg = {
"version": 1,
"config": [
{
"type": "physical",
"mac_address": "86:00:00:3c:5c:43",
"name": None,
"subnets": [{"type": "dhcp", "ipv4": True}],
},
],
}
extract_physdevs(netcfg)
# TypeError: can only concatenate str (not "NoneType") to str

We verified this against cloud-init 26.1. The same crash occurs when the `name` key is absent.

On the live platform:

1. Create a Hetzner Cloud server with a private network attached at server creation.
2. The first boot fails when the NIC hotplug arrives after `init-local` reads the metadata. The result depends on the timing.

## Environment details
- Cloud-init version: 26.1-0ubuntu2 (the affected code is identical
on main at a6d999ae517d)
- Operating System Distribution: Ubuntu 26.04 (resolute)
- Cloud provider, platform or installer type: Hetzner Cloud
(DataSourceHetzner), with a private network attached at server
creation

## cloud-init logs

Excerpt of /var/log/cloud-init.log from a failed first boot:

2026-08-07 04:36:55,343 - main.py[ERROR]: failed stage init-local
Traceback (most recent call last):
File ".../cloudinit/cmd/main.py", line 967, in status_wrapper
ret = functor(name, args)
File ".../cloudinit/cmd/main.py", line 597, in main_init
init.apply_network_config(bring_up=bring_up_interfaces)
File ".../cloudinit/stages.py", line 1105, in apply_network_config
self.distro.networking.wait_for_physdevs(netcfg)
File ".../cloudinit/distros/networking.py", line 133, in wait_for_physdevs
physdevs = self.extract_physdevs(netcfg)
File ".../cloudinit/distros/networking.py", line 43, in extract_physdevs
return net.extract_physdevs(netcfg)
File ".../cloudinit/net/__init__.py", line 635, in extract_physdevs
return _version_1(netcfg)
File ".../cloudinit/net/__init__.py", line 607, in _version_1
driver = device_driver(name)
File ".../cloudinit/net/__init__.py", line 360, in device_driver
driver_path = sys_dev_path(devname, "device/driver")
File ".../cloudinit/net/__init__.py", line 62, in sys_dev_path
return get_sys_class_path() + devname + "/" + path
TypeError: can only concatenate str (not "NoneType") to str

`cloud-init status --long` after the boot shows `status: error`, five `can only concatenate str (not "NoneType") to str` errors, the failed stages `init-local` and `init`, and repeated `network-config-v1 failed schema validation` warnings (the null name).

The instance was ephemeral, and a replacement removed it. A `cloud-init collect-logs` tarball is not available. The excerpts above are verbatim. We can re-create the setup and collect full logs on request.

## Related issues

- #2969 (closed): invalid network config dictionaries are not handled
well. The same parser, the same robustness theme.
- #6977 (open): RuntimeError from `get_interfaces_by_mac_on_linux`
with GRE tunnels. A different function in the same MAC-to-interface
lookup family.
- PR #6935 (open): DataSourceHetzner `EXTRA_HOTPLUG_UDEV_RULES` MAC
matching. Adjacent work on the same Hetzner private-NIC hotplug
path. It does not correct this crash.

Contributor guide

Open the contributing guide

Research direction

Start in cloudinit/net/__init__.py at extract_physdevs and its _version_1 path, reproducing the failure with the minimal network-config shown. Then inspect cloudinit/sources/DataSourceHetzner.py, cloudinit/sources/helpers/hetzner.py, and cloudinit/distros/networking.py around wait_for_physdevs. Done means missing physical-device names no longer crash parsing or first boot, with regression coverage for the reproduced configuration and preserved handling of valid entries.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
infrastructure, networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.