Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cloudinit/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
# What u get if no config is provided
CFG_BUILTIN = {
"datasource_list": [
"QemuFwCfg",
"NoCloud",
"ConfigDrive",
"LXD",
Expand Down
146 changes: 146 additions & 0 deletions cloudinit/sources/DataSourceQemuFwCfg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# This file is part of cloud-init. See LICENSE file for license information.

"""DataSource for QEMU fw_cfg sysfs interface.

QEMU exposes arbitrary blobs to the guest via the fw_cfg mechanism.
The Linux ``qemu_fw_cfg`` kernel driver surfaces these blobs under
``/sys/firmware/qemu_fw_cfg/by_name/<entry-name>/raw``.

This datasource reads under the ``opt/io.cloud-init/cloud-init/``
namespace one entry for each of the cloud-init data, in the order
that cloud-init processes them:

``meta-data``
YAML dict with instance metadata (``instance-id``, ``local-hostname``,
etc.)
``network-config``
Network configuration in v1 or v2 YAML format.
``user-data``
User-data payload (cloud-config YAML, shell script, etc.)
``vendor-data``
Vendor-data payload.

Both ``meta-data`` and ``user-data`` must be present for the datasource to
claim the instance.
"""

import logging
import os
from typing import Any, Dict, List, Optional, Set

from cloudinit import sources, util

LOG = logging.getLogger(__name__)

# Base sysfs path exposed by the qemu_fw_cfg kernel driver.
FWCFG_SYSFS = "/sys/firmware/qemu_fw_cfg/by_name"
# fw_cfg entry namespace agreed upon by the cloud-init project.
FWCFG_PREFIX = "opt/io.cloud-init/cloud-init"
FWCFG_PATH = os.path.join(FWCFG_SYSFS, FWCFG_PREFIX)

DEFAULT_IID = "iid-qemufwcfg"
DEFAULT_METADATA = {"instance-id": DEFAULT_IID}

# Slots in the order they are read and applied by cloud-init.
_SLOT_FILES = ("meta-data", "network-config", "user-data", "vendor-data")
_REQUIRED_SLOTS = frozenset({"meta-data", "user-data"})


def _read_fwcfg_slot(name: str) -> Optional[bytes]:
"""Return raw bytes from a fw_cfg slot, or None if the slot is absent."""
raw_path = os.path.join(FWCFG_PATH, name, "raw")
try:
return util.load_binary_file(raw_path)
except FileNotFoundError:
return None
except OSError as exc:
LOG.warning("Failed to read fw_cfg slot %s: %s", name, exc)
return None


class DataSourceQemuFwCfg(sources.DataSource):
"""Read instance configuration from QEMU fw_cfg sysfs entries."""

dsname = "QemuFwCfg"

def __init__(self, sys_cfg, distro, paths):
super().__init__(sys_cfg, distro, paths)
self._network_config = None

def _unpickle(self, ci_pkl_version: int) -> None:
super()._unpickle(ci_pkl_version)
if not hasattr(self, "_network_config"):
self._network_config = None

def ds_detect(self) -> bool:
"""Return True when the fw_cfg namespace directory exists in sysfs."""
return os.path.isdir(FWCFG_PATH)

def _get_data(self) -> bool:
mydata: Dict[str, Any] = {
"meta-data": {},
"network-config": None,
"user-data": "",
"vendor-data": "",
}
found: Set[str] = set()

for slot in _SLOT_FILES:
raw = _read_fwcfg_slot(slot)
if raw is None:
continue
found.add(slot)
text = raw.decode("utf-8", errors="replace")

if slot == "meta-data":
md = util.load_yaml(text)
if isinstance(md, dict):
mydata["meta-data"] = md
else:
LOG.warning(
"meta-data did not parse as a YAML dict: ignoring"
)
elif slot == "network-config":
nc = util.load_yaml(text)
if nc:
mydata["network-config"] = nc
else:
mydata[slot] = text

missing = _REQUIRED_SLOTS - found
if missing:
LOG.debug(
"QemuFwCfg: required slot(s) missing: %s",
", ".join(sorted(missing)),
)
return False

# DEFAULT_METADATA to fill gap if user did not specify
mydata["meta-data"] = util.mergemanydict(
[mydata["meta-data"], DEFAULT_METADATA]
)
self.metadata = mydata["meta-data"]
self._network_config = mydata["network-config"]
self.userdata_raw = mydata["user-data"]
self.vendordata_raw = mydata["vendor-data"]
return True

def _get_subplatform(self) -> str:
return "fw_cfg (%s)" % FWCFG_PATH

def _get_cloud_name(self) -> str:
return sources.METADATA_UNKNOWN

@property
def network_config(self) -> Optional[Dict]:
return self._network_config


# QemuFwCfg only needs the filesystem (sysfs is available in the local stage).
datasources = [
(DataSourceQemuFwCfg, (sources.DEP_FILESYSTEM,)),
]


def get_datasource_list(depends: List[str]) -> List[sources.DataSource]:
return sources.list_from_depends(depends, datasources)
1 change: 1 addition & 0 deletions doc/rtd/reference/datasources.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ The following is a page for each supported datasource:
datasources/openstack.rst
datasources/oracle.rst
datasources/ovf.rst
datasources/qemufwcfg.rst
datasources/rbxcloud.rst
datasources/scaleway.rst
datasources/smartos.rst
Expand Down
117 changes: 117 additions & 0 deletions doc/rtd/reference/datasources/qemufwcfg.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
.. _datasource_qemufwcfg:

QEMU fw_cfg
***********

The ``QemuFwCfg`` datasource reads cloud-init configuration from the QEMU
firmware configuration device (``fw_cfg``). This allows the hypervisor to
inject ``user-data``, ``meta-data``, ``vendor-data``, and ``network-config``
directly into a guest VM without requiring virtual disks, kernel command line
injection, SMBIOS strings, or a network metadata service.

The Linux ``qemu_fw_cfg`` kernel driver (``CONFIG_FW_CFG_SYSFS``) exposes
fw_cfg entries to the guest as sysfs files under
:file:`/sys/firmware/qemu_fw_cfg/by_name/`.
See the `QEMU fw_cfg specification <https://www.qemu.org/docs/master/specs/fw_cfg.html>`_
and the `Linux fw_cfg sysfs driver <https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-firmware-qemu_fw_cfg>`_
for more details.
The datasource reads from the ``opt/io.cloud-init/cloud-init/`` namespace:

- ``opt/io.cloud-init/cloud-init/meta-data``
- ``opt/io.cloud-init/cloud-init/network-config``
- ``opt/io.cloud-init/cloud-init/user-data``
- ``opt/io.cloud-init/cloud-init/vendor-data``

Each entry is exposed in sysfs as a directory; its content is available via
the ``raw`` file inside that directory.

Both ``meta-data`` and ``user-data`` must be present for the datasource to
claim the instance. ``network-config`` and ``vendor-data`` are optional.

Detection
=========

The datasource is detected by ``ds-identify`` during the
:ref:`detect stage<boot-Detect>` when the sysfs directory
:file:`/sys/firmware/qemu_fw_cfg/by_name/opt/io.cloud-init/cloud-init`
exists in the guest.

Injecting configuration
=======================

QEMU command line
-----------------

Use the ``-fw_cfg`` option to inject each slot as a named entry:

.. code-block:: shell-session

$ qemu-system-x86_64 \
-fw_cfg name=opt/io.cloud-init/cloud-init/meta-data,file=meta-data \
-fw_cfg name=opt/io.cloud-init/cloud-init/user-data,file=user-data \
[other options...]

libvirt domain XML
------------------

When using libvirt, pass the entries via ``<sysinfo type='fwcfg'>`` in the
domain XML:

.. code-block:: xml

<sysinfo type='fwcfg'>
<entry name='opt/io.cloud-init/cloud-init/meta-data'>instance-id: vm-001</entry>
<entry name='opt/io.cloud-init/cloud-init/user-data'>#cloud-config</entry>
</sysinfo>

Example
=======

The following minimal example creates a VM with a guest user:

**meta-data** (saved as :file:`meta-data`):

.. code-block:: yaml

instance-id: my-vm-001
local-hostname: my-vm

**user-data** (saved as :file:`user-data`):

.. code-block:: yaml

#cloud-config
users:
- name: guest
lock_passwd: false
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL

chpasswd:
users:
- name: guest
password: guest
type: text
expire: false

Launch the VM:

.. code-block:: shell-session

$ qemu-system-x86_64 \
-fw_cfg name=opt/io.cloud-init/cloud-init/meta-data,file=meta-data \
-fw_cfg name=opt/io.cloud-init/cloud-init/user-data,file=user-data \
[other options...]

Once the guest has booted, log in as ``guest``.
To debug issues, check the blob contents:

.. code-block:: shell-session

$ cat /sys/firmware/qemu_fw_cfg/by_name/opt/io.cloud-init/cloud-init/meta-data/raw
$ cat /sys/firmware/qemu_fw_cfg/by_name/opt/io.cloud-init/cloud-init/user-data/raw

See :ref:`user-data <user_data_formats>` and
:ref:`vendor-data <vendor-data>` for the supported data formats, and
:ref:`network_config_v1` or :ref:`network_config_v2` for network
configuration syntax.
Loading