diff --git a/bindep.txt b/bindep.txt
index d99bfd217..ab5f0867b 100644
--- a/bindep.txt
+++ b/bindep.txt
@@ -2,5 +2,9 @@
libpcre2-dev [platform:dpkg test]
pcre2-devel [platform:rpm test]
+# Sphinx graphviz extension requires the dot binary.
+graphviz [platform:dpkg test doc]
+graphviz [platform:rpm test doc]
+
# PDF Docs package dependencies
tex-gyre [platform:dpkg doc]
diff --git a/doc/source/_static/network-topology-nwdiag.svg b/doc/source/_static/network-topology-nwdiag.svg
new file mode 100644
index 000000000..ac839d555
--- /dev/null
+++ b/doc/source/_static/network-topology-nwdiag.svg
@@ -0,0 +1,189 @@
+
+
+
diff --git a/doc/source/administration/general.rst b/doc/source/administration/general.rst
index e33e4bc75..2c8e2fa4e 100644
--- a/doc/source/administration/general.rst
+++ b/doc/source/administration/general.rst
@@ -58,6 +58,20 @@ We can use the ``--var-name`` argument to inspect a particular variable or the
``--host`` or ``--hosts`` arguments to view a variable or variables for a
specific host or set of hosts.
+Viewing the Kayobe inventory
+============================
+
+In some cases you may want to examine the full inventory that Kayobe will
+pass to Ansible. The ``kayobe inventory`` command wraps
+``ansible-inventory`` and automatically assembles the list of inventory
+paths (shared config plus any environment-specific directories). It can
+be used exactly like ``ansible-inventory``; for example::
+
+ (kayobe) $ kayobe inventory --graph
+
+Any other flags supported by ``ansible-inventory`` are also accepted.
+
+
Checking Network Connectivity
=============================
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 3c150fec6..19cca2bb6 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -56,6 +56,7 @@
'openstackdocstheme',
#'sphinx.ext.autodoc',
#'sphinx.ext.intersphinx',
+ 'sphinx.ext.graphviz',
'sphinxcontrib.rsvgconverter',
]
diff --git a/doc/source/configuration/examples/index.rst b/doc/source/configuration/examples/index.rst
new file mode 100644
index 000000000..641faeb9b
--- /dev/null
+++ b/doc/source/configuration/examples/index.rst
@@ -0,0 +1,10 @@
+======================
+Configuration Examples
+======================
+
+This section provides real-world configuration examples.
+
+.. toctree::
+ :maxdepth: 2
+
+ real-world-bridges-and-bonds-configuration-a-virtualised-case-study
diff --git a/doc/source/configuration/examples/real-world-bridges-and-bonds-configuration-a-virtualised-case-study.rst b/doc/source/configuration/examples/real-world-bridges-and-bonds-configuration-a-virtualised-case-study.rst
new file mode 100644
index 000000000..3efbbff23
--- /dev/null
+++ b/doc/source/configuration/examples/real-world-bridges-and-bonds-configuration-a-virtualised-case-study.rst
@@ -0,0 +1,963 @@
+===========================================================================
+Real-World Bridges and Bonds Configuration: A virtualised case study
+===========================================================================
+
+This section details a real-world configuration to illustrate how to configure
+bonds and bridges using Kayobe. The deployment runs
+virtualised compute workloads with no bare metal resources managed through
+Ironic.
+
+This scenario walks through configuring host networking for each host group,
+starting from a known network topology.
+
+Network Topology
+================
+
+The following diagram shows how networks are mapped to each of the hosts groups.
+
+.. figure:: ../../_static/network-topology-nwdiag.svg
+ :alt: Network assignment to host groups
+
+ Network assignment to host groups
+
+..
+ Original nwdiag source used to generate the SVG above:
+
+ nwdiag {
+ // Node labels keep names concise while preserving readable host group
+ // names in the rendered diagram.
+ seed_hv [label = "Seed Hypervisor"];
+ seed [label = "Seed"];
+ controllers [label = "Controllers"];
+ computes [label = "Computes"];
+ storage [label = "Storage"];
+
+ network external_admin_net {
+ address = "203.0.113.0/24";
+ color = "#93c5fd";
+ seed_hv;
+ seed;
+ }
+
+ network power_mgmt_net {
+ address = "10.0.1.0/24";
+ color = "#fca5a5";
+ seed_hv;
+ seed;
+ }
+
+ network provision_ctl_net {
+ address = "10.0.2.0/24";
+ color = "#86efac";
+ seed_hv;
+ seed;
+ controllers;
+ computes;
+ storage;
+ }
+
+ network internal_net {
+ address = "172.20.10.0/26";
+ color = "#fcd34d";
+ controllers;
+ computes;
+ storage;
+ }
+
+ network storage_net {
+ address = "172.20.20.0/25";
+ color = "#c4b5fd";
+ controllers;
+ computes;
+ storage;
+ }
+
+ network storage_mgmt_net {
+ address = "172.20.10.64/26";
+ color = "#fdba74";
+ storage;
+ }
+
+ network tenant_tunnel_net {
+ address = "10.0.4.0/25";
+ color = "#67e8f9";
+ controllers;
+ computes;
+ }
+
+ network external_user_net {
+ address = "172.31.50.0/24";
+ color = "#f9a8d4";
+ controllers;
+ }
+ }
+
+.. |color_sky_blue| raw:: html
+
+ sky blue
+
+.. |color_rose| raw:: html
+
+ rose
+
+.. |color_green| raw:: html
+
+ green
+
+.. |color_amber| raw:: html
+
+ amber
+
+.. |color_violet| raw:: html
+
+ violet
+
+.. |color_orange| raw:: html
+
+ orange
+
+.. |color_cyan| raw:: html
+
+ cyan
+
+.. |color_pink| raw:: html
+
+ pink
+
+.. note::
+
+ It is important to use non-routable (RFC 1918 private) address ranges for the
+ power management and provisioning/control networks. This prevents accidental
+ exposure of the infrastructure management and provisioning interfaces to
+ external networks, limiting access to authorized internal networks only.
+
+
+.. list-table:: VLAN key and colour groups
+ :header-rows: 1
+
+ * - Network
+ - VLAN
+ - Group
+ * - external_admin_net
+ - 2270
+ - Admin/management (|color_sky_blue|)
+ * - power_mgmt_net
+ - 2802
+ - Power management (|color_rose|)
+ * - provision_ctl_net
+ - 3
+ - Provision/control (|color_green|)
+ * - internal_net
+ - 4
+ - Internal API (|color_amber|)
+ * - storage_net
+ - 5
+ - Storage (|color_violet|)
+ * - storage_mgmt_net
+ - 6
+ - Storage management (|color_orange|)
+ * - tenant_tunnel_net
+ - 7
+ - Tenant tunnel (|color_cyan|)
+ * - external_user_net
+ - 2280
+ - Public API (|color_pink|)
+
+Define Networks in networks.yml
+===============================
+
+Kayobe uses well-known logical network role variables (such as
+``provision_oc_net_name`` and ``tunnel_net_name``). These can optionally be
+customised to give more descriptive names. The mappings are defined in
+``etc/kayobe/networks.yml``.
+
+.. code-block:: yaml
+ :caption: ``etc/kayobe/networks.yml``
+
+ ---
+ # Kayobe network configuration.
+
+ ###############################################################################
+ # Network role to network name mappings.
+
+ # Network used by the seed to manage bare metal overcloud hosts via their
+ # out-of-band management controllers.
+ oob_oc_net_name: "power_mgmt_net"
+
+ # Network used by the seed to provision bare metal overcloud hosts.
+ provision_oc_net_name: "provision_ctl_net"
+
+ # Network used to expose the internal OpenStack API endpoints.
+ internal_net_name: "internal_net"
+
+ # Network used to expose the public OpenStack API endpoints.
+ public_net_name: "external_user_net"
+
+ # Network used by Neutron to carry tenant overlay network traffic.
+ tunnel_net_name: "tenant_tunnel_net"
+
+With the names established, populate the CIDRs, VLAN IDs, MTU values, and
+allocation pools for each named network in ``etc/kayobe/environments/production/networks.yml``:
+
+.. code-block:: yaml
+ :caption: ``etc/kayobe/environments/production/networks.yml``
+
+ ---
+ # Kayobe network configuration.
+
+ ###############################################################################
+ # Network definitions.
+
+ hs_bond_mtu: 9150
+
+ # Power/Management network IP information.
+ power_mgmt_net_cidr: "10.0.1.0/24"
+ power_mgmt_net_allocation_pool_start: "10.0.1.10"
+ power_mgmt_net_allocation_pool_end: "10.0.1.250"
+ power_mgmt_net_vlan: 2802
+ power_mgmt_net_mtu: 1500
+
+ # Provisioning/Control network IP information.
+ provision_ctl_net_cidr: "10.0.2.0/24"
+ provision_ctl_net_allocation_pool_start: "10.0.2.10"
+ provision_ctl_net_allocation_pool_end: "10.0.2.127"
+ provision_ctl_net_inspection_allocation_pool_start: "10.0.2.128"
+ provision_ctl_net_inspection_allocation_pool_end: "10.0.2.250"
+ provision_ctl_net_switch_vlan: 3
+ provision_ctl_net_mtu: 1500
+
+ # Internal API network IP information.
+ internal_net_vip_address: "172.20.10.2"
+ internal_net_cidr: "172.20.10.0/26"
+ internal_net_allocation_pool_start: "172.20.10.7"
+ internal_net_allocation_pool_end: "172.20.10.62"
+ internal_net_vlan: 4
+ internal_net_mtu: 9000
+
+ # Storage network IP information.
+ storage_net_cidr: "172.20.20.0/25"
+ storage_net_allocation_pool_start: "172.20.20.10"
+ storage_net_allocation_pool_end: "172.20.20.126"
+ storage_net_vlan: 5
+ storage_net_mtu: "{{ hs_bond_mtu }}"
+
+ # Storage Management network IP information.
+ storage_mgmt_net_cidr: "172.20.10.64/26"
+ storage_mgmt_net_allocation_pool_start: "172.20.10.74"
+ storage_mgmt_net_allocation_pool_end: "172.20.10.124"
+ storage_mgmt_net_vlan: 6
+ storage_mgmt_net_mtu: 9000
+
+ # Tenant/Tunnel network IP information.
+ tenant_tunnel_net_cidr: "10.0.4.0/25"
+ tenant_tunnel_net_allocation_pool_start: "10.0.4.10"
+ tenant_tunnel_net_allocation_pool_end: "10.0.4.127"
+ tenant_tunnel_net_vlan: 7
+ tenant_tunnel_net_mtu: "{{ hs_bond_mtu }}"
+
+ # External/Admin network IP information.
+ # This network is used for management access.
+ external_admin_net_cidr: "203.0.113.0/24"
+ external_admin_net_allocation_pool_start: "203.0.113.5"
+ external_admin_net_allocation_pool_end: "203.0.113.9"
+ external_admin_net_vlan: 2270
+ external_admin_net_mtu: 1500
+ external_admin_net_gateway: "203.0.113.250"
+
+ # External User network
+ external_user_net_cidr: "172.31.50.0/24"
+ external_user_net_vip_address: "172.31.50.4"
+ external_user_net_gateway: "172.31.50.250"
+ external_user_net_allocation_pool_start: "172.31.50.10"
+ external_user_net_allocation_pool_end: "172.31.50.249"
+ external_user_net_vlan: 2280
+ external_user_net_mtu: 1500
+
+ # FQDN address for public API endpoints, registered with the external DNS
+ # server to resolve to the public VIP address.
+ external_user_net_fqdn: "api.example.org"
+
+ ###############################################################################
+ # Dummy variable to allow Ansible to accept this file.
+ workaround_ansible_issue_8743: yes
+
+Common Configuration
+====================
+
+Shared overcloud interface mappings should be defined in
+``inventory/group_vars/overcloud/network-interfaces``.
+This common configuration is inherited by
+all hosts in the overcloud group, including controllers, storage, and compute.
+
+The aim is to avoid repetition of common configuration. You can still override
+values for specific host groups where required.
+
+.. code-block:: yaml
+ :caption: ``inventory/group_vars/overcloud/network-interfaces``
+
+ ---
+ # High speed bond config
+ hs_bond_interface: "bond0"
+ hs_bond_bond_mode: "802.3ad"
+ hs_bond_bond_xmit_hash_policy: "layer3+4"
+ hs_bond_physical_network: "physnet2"
+
+ # 25G Control bond config
+ control_25g_interface: "bond1"
+ control_25g_bond_mode: "802.3ad"
+ control_25g_bond_xmit_hash_policy: "layer3+4"
+ control_25g_physical_network: "physnet3"
+
+ # Provision / Control maps to a bridge on the 25G bond port
+ provision_ctl_net_interface: "br{{ control_25g_interface }}"
+ provision_ctl_net_bridge_ports:
+ - "{{ control_25g_interface }}"
+ provision_ctl_net_physical_network: "physnet3"
+
+ # List of names of networks used to provide external network access via Neutron.
+ # This value is overridden for controllers to add the external_10g network.
+ external_net_names:
+ - "hs_bond"
+ - "provision_ctl_net"
+
+Seed Hypervisor
+===============
+
+.. warning::
+
+ Network reconfiguration can potentially break networking and render hosts
+ inaccessible. This is particularly risky when moving the admin interface
+ onto a bridge or bond. It is recommended that you:
+
+ - Add a password to a local user account that can be accessed via the BMC
+ (either web UI or serial console) to recover should configuration fail.
+ This password should be removed after successful initial deployment.
+
+ - Configure one host per group initially before applying the configuration
+ across the entire group. If a host becomes misconfigured, it can simply be
+ reprovisioned.
+
+.. graphviz::
+ :caption: Seed hypervisor interface layering
+
+ digraph seed_hypervisor_interfaces {
+ rankdir=LR;
+ nodesep=0.35;
+ ranksep=0.6;
+ fontname="Helvetica";
+ node [shape=box, style="rounded,filled", color="#334155", fillcolor="#f8fafc", fontname="Helvetica", fontsize=10];
+ edge [color="#475569", arrowsize=0.7, penwidth=1.1];
+
+ sh_m1 [label="enp65s0f0np0", fillcolor="#e2e8f0"];
+ sh_m2 [label="enp65s0f1np1", fillcolor="#e2e8f0"];
+ sh_m3 [label="enp2s0f0np0", fillcolor="#e2e8f0"];
+ sh_m4 [label="enp2s0f1np1", fillcolor="#e2e8f0"];
+
+ sh_b0 [label="bond0", fillcolor="#dbeafe", color="#1d4ed8"];
+ sh_b1 [label="bond1", fillcolor="#dbeafe", color="#1d4ed8"];
+
+ sh_v_admin [label="bond0.2270", fillcolor="#fef3c7", color="#b45309"];
+ sh_v_power [label="bond0.2802", fillcolor="#fef3c7", color="#b45309"];
+
+ sh_br_admin [label="brbond0_admin", fillcolor="#dcfce7", color="#15803d"];
+ sh_br_power [label="brbond0_power", fillcolor="#dcfce7", color="#15803d"];
+ sh_br_ctl [label="brbond1", fillcolor="#dcfce7", color="#15803d"];
+
+ sh_n_admin [label="external_admin_net", fillcolor="#fee2e2", color="#b91c1c"];
+ sh_n_power [label="power_mgmt_net", fillcolor="#fee2e2", color="#b91c1c"];
+ sh_n_ctl [label="provision_ctl_net", fillcolor="#fee2e2", color="#b91c1c"];
+
+ sh_n_admin -> sh_br_admin -> sh_v_admin -> sh_b0;
+ sh_n_power -> sh_br_power -> sh_v_power -> sh_b0;
+ sh_n_ctl -> sh_br_ctl -> sh_b1;
+
+ sh_b0 -> sh_m1;
+ sh_b0 -> sh_m2;
+ sh_b1 -> sh_m3;
+ sh_b1 -> sh_m4;
+ }
+
+.. note::
+
+ Any network interface that the seed VM needs to be connected to must be
+ backed by a Linux bridge on the hypervisor. Kayobe provisions the seed using
+ libvirt, which attaches virtual machine interfaces to host networks via
+ bridges — a plain bond or VLAN sub-interface cannot be used directly.
+
+In ``etc/kayobe/seed-hypervisor.yml`` define extra networks that are not
+mapped to this host by default:
+
+.. code-block:: yaml
+
+ seed_hypervisor_extra_network_interfaces:
+ - mgmt_bond_ext
+ - mgmt_vlan_admin
+ - mgmt_vlan_power
+ - mgmt_bond_int
+ - external_admin_net
+ - power_mgmt_net
+
+In ``inventory/group_vars/seed-hypervisor/network-interfaces`` define the
+network interface mappings and properties.
+
+To see which interface variables require definitions, run
+``kayobe configuration dump --var-name network_interfaces --limit seed-hypervisor``.
+Before creating this file, also run ``ip a`` on the seed
+hypervisor and use the reported physical interface names in
+``*_bond_slaves`` variables.
+
+.. code-block:: yaml
+
+ mgmt_bond_ext_interface: bond0
+ mgmt_bond_ext_bond_mode: 802.3ad
+ mgmt_bond_ext_bond_xmit_hash_policy: layer3+4
+ mgmt_bond_ext_bond_slaves:
+ - enp65s0f0np0
+ - enp65s0f1np1
+
+ mgmt_vlan_admin_interface: "{{ mgmt_bond_ext_interface }}.{{ external_admin_net_vlan }}"
+ external_admin_net_interface: "br{{ mgmt_bond_ext_interface }}_admin"
+ external_admin_net_bridge_ports:
+ - "{{ mgmt_vlan_admin_interface }}"
+
+ mgmt_vlan_power_interface: "{{ mgmt_bond_ext_interface }}.{{ power_mgmt_net_vlan }}"
+ power_mgmt_net_interface: "br{{ mgmt_bond_ext_interface }}_power"
+ power_mgmt_net_bridge_ports:
+ - "{{ mgmt_vlan_power_interface }}"
+
+ mgmt_bond_int_interface: bond1
+ mgmt_bond_int_bond_mode: 802.3ad
+ mgmt_bond_int_bond_xmit_hash_policy: layer3+4
+ mgmt_bond_int_bond_slaves:
+ - enp2s0f0np0
+ - enp2s0f1np1
+
+ provision_ctl_net_interface: "br{{ mgmt_bond_int_interface }}"
+ provision_ctl_net_bridge_ports:
+ - "{{ mgmt_bond_int_interface }}"
+
+Seed
+====
+
+.. warning::
+
+ Network reconfiguration can potentially break networking and render hosts
+ inaccessible. This is particularly risky when moving the admin interface
+ onto a bridge or bond. It is recommended that you:
+
+ - Add a password to a local user account that can be accessed via the BMC
+ (either web UI or serial console) to recover should configuration fail.
+ This password should be removed after successful initial deployment.
+
+ - Configure one host per group initially before applying the configuration
+ across the entire group. If a host becomes misconfigured, it can simply be
+ reprovisioned.
+
+.. note::
+
+ The seed's network interfaces must be defined before running
+ ``kayobe seed host provision``. Provisioning creates the seed VM and
+ attaches it to the bridges defined on the hypervisor, so the interface
+ configuration must be in place for Kayobe to know which networks to connect.
+
+ The interface order on the seed VM is derived from
+ ``seed_hypervisor_libvirt_networks``. To inspect the resolved value, run:
+
+ .. code-block:: console
+
+ kayobe configuration dump --var-name seed_hypervisor_libvirt_networks --limit seed-hypervisor
+
+The seed is configured to use a separate network card for each logical network.
+
+.. graphviz::
+ :caption: Seed host interface mapping
+
+ digraph seed_host_interfaces {
+ rankdir=LR;
+ nodesep=0.6;
+ ranksep=0.7;
+ fontname="Helvetica";
+ node [shape=box, style="rounded,filled", color="#334155", fillcolor="#f8fafc", fontname="Helvetica", fontsize=10];
+ edge [color="#475569", arrowsize=0.7, penwidth=1.1];
+
+ s_if2 [label="ens2", fillcolor="#e2e8f0"];
+ s_if3 [label="ens3", fillcolor="#e2e8f0"];
+ s_if4 [label="ens4", fillcolor="#e2e8f0"];
+
+ s_n_admin [label="external_admin_net", fillcolor="#fee2e2", color="#b91c1c"];
+ s_n_ctl [label="provision_ctl_net", fillcolor="#fee2e2", color="#b91c1c"];
+ s_n_power [label="power_mgmt_net", fillcolor="#fee2e2", color="#b91c1c"];
+
+ s_n_admin -> s_if2;
+ s_n_ctl -> s_if3;
+ s_n_power -> s_if4;
+ }
+
+In ``etc/kayobe/seed.yml`` define extra networks that are not mapped to this
+host by default:
+
+.. code-block:: yaml
+
+ seed_extra_network_interfaces:
+ - external_admin_net
+
+In ``inventory/group_vars/seed/network-interfaces`` map networks directly to
+host interfaces.
+
+To see which interface variables require definitions, run
+``kayobe configuration dump --var-name network_interfaces --limit seed``
+
+.. code-block:: yaml
+
+ external_admin_net_interface: ens2
+ provision_ctl_net_interface: ens3
+ power_mgmt_net_interface: ens4
+
+Controllers
+===========
+
+.. warning::
+
+ Network reconfiguration can potentially break networking and render hosts
+ inaccessible. This is particularly risky when moving the admin interface
+ onto a bridge or bond. It is recommended that you:
+
+ - Add a password to a local user account that can be accessed via the BMC
+ (either web UI or serial console) to recover should configuration fail.
+ This password should be removed after successful initial deployment.
+
+ - Configure one host per group initially before applying the configuration
+ across the entire group. If a host becomes misconfigured, it can simply be
+ reprovisioned.
+
+The diagram below shows the network interface configuration for the
+controllers:
+
+Controllers use a combination of bond aggregation and Linux bridges.
+
+.. graphviz::
+ :caption: Controller interface layering
+
+ digraph controller_interfaces {
+ rankdir=LR;
+ nodesep=0.35;
+ ranksep=0.6;
+ fontname="Helvetica";
+ node [shape=box, style="rounded,filled", color="#334155", fillcolor="#f8fafc", fontname="Helvetica", fontsize=10];
+ edge [color="#475569", arrowsize=0.7, penwidth=1.1];
+
+ c_m1 [label="eno33np0", fillcolor="#e2e8f0"];
+ c_m2 [label="eno34np1", fillcolor="#e2e8f0"];
+ c_m3 [label="ens3f0np0", fillcolor="#e2e8f0"];
+ c_m4 [label="ens3f1np1", fillcolor="#e2e8f0"];
+ c_m5 [label="eno35np2", fillcolor="#e2e8f0"];
+ c_m6 [label="eno36np3", fillcolor="#e2e8f0"];
+
+ c_b1 [label="bond1", fillcolor="#dbeafe", color="#1d4ed8"];
+ c_b0 [label="bond0", fillcolor="#dbeafe", color="#1d4ed8"];
+ c_b2 [label="bond2", fillcolor="#dbeafe", color="#1d4ed8"];
+
+ c_v_int [label="bond0.4", fillcolor="#fef3c7", color="#b45309"];
+ c_v_sto [label="bond0.5", fillcolor="#fef3c7", color="#b45309"];
+ c_v_tun [label="bond0.7", fillcolor="#fef3c7", color="#b45309"];
+ c_v_ext [label="brbond2.2280", fillcolor="#fef3c7", color="#b45309"];
+
+ c_br_ctl [label="brbond1", fillcolor="#dcfce7", color="#15803d"];
+ c_br_ext [label="brbond2", fillcolor="#dcfce7", color="#15803d"];
+
+ c_n_ctl [label="provision_ctl_net", fillcolor="#fee2e2", color="#b91c1c"];
+ c_n_int [label="internal_net", fillcolor="#fee2e2", color="#b91c1c"];
+ c_n_sto [label="storage_net", fillcolor="#fee2e2", color="#b91c1c"];
+ c_n_tun [label="tenant_tunnel_net", fillcolor="#fee2e2", color="#b91c1c"];
+ c_n_ext [label="external_user_net", fillcolor="#fee2e2", color="#b91c1c"];
+
+ c_n_ctl -> c_br_ctl -> c_b1;
+ c_n_int -> c_v_int -> c_b0;
+ c_n_sto -> c_v_sto -> c_b0;
+ c_n_tun -> c_v_tun -> c_b0;
+ c_n_ext -> c_v_ext -> c_br_ext -> c_b2;
+
+ c_b1 -> c_m1;
+ c_b1 -> c_m2;
+ c_b0 -> c_m3;
+ c_b0 -> c_m4;
+ c_b2 -> c_m5;
+ c_b2 -> c_m6;
+ }
+
+Controllers integrate the physical bond interfaces with OpenStack Neutron to
+provide tenant network connectivity.
+
+.. graphviz::
+ :caption: Wiring networks into Neutron (controllers)
+
+ digraph neutron_wiring_controllers {
+ rankdir=LR;
+ nodesep=0.35;
+ ranksep=0.7;
+ fontname="Helvetica";
+ node [shape=box, style="rounded,filled", color="#334155", fillcolor="#f8fafc", fontname="Helvetica", fontsize=10];
+ edge [color="#475569", arrowsize=0.7, penwidth=1.1];
+
+ { rank=same; c_net_ext; c_net_hs; c_net_ctl; }
+
+ c_net_ext [label="external_bridge\nphysnet1\n(network maps to Linux bridge)", fillcolor="#fee2e2", color="#b91c1c"];
+ c_net_hs [label="hs_bond\nphysnet2\n(network maps directly)", fillcolor="#fee2e2", color="#b91c1c"];
+ c_net_ctl [label="provision_ctl_net\nphysnet3\n(network maps to Linux bridge)", fillcolor="#fee2e2", color="#b91c1c"];
+
+ c_lx_ext [label="Linux bridge\nbrbond2", fillcolor="#dcfce7", color="#15803d"];
+ c_lx_ctl [label="Linux bridge\nbrbond1", fillcolor="#dcfce7", color="#15803d"];
+ c_bond0 [label="bond0", fillcolor="#fef3c7", color="#b45309"];
+
+ c_ovs_ext [label="OVS bridge\nbrbond2-ovs", fillcolor="#dbeafe", color="#1d4ed8"];
+ c_ovs_hs [label="OVS bridge\nbond0-ovs", fillcolor="#dbeafe", color="#1d4ed8"];
+ c_ovs_ctl [label="OVS bridge\nbrbond1-ovs", fillcolor="#dbeafe", color="#1d4ed8"];
+ c_neutron [label="Neutron\nprovider networks", fillcolor="#e0f2fe", color="#0284c7"];
+
+ subgraph cluster_c_pp_brbond2 {
+ label="patch link";
+ style="dashed";
+ color="#6d28d9";
+ fillcolor="#faf5ff";
+ c_pp_brbond2_phy [label="p-brbond2-phy", fillcolor="#ede9fe", color="#6d28d9"];
+ c_pp_brbond2_ovs [label="p-brbond2-ovs", fillcolor="#ede9fe", color="#6d28d9"];
+ }
+
+ subgraph cluster_c_pp_brbond1 {
+ label="patch link";
+ style="dashed";
+ color="#6d28d9";
+ fillcolor="#faf5ff";
+ c_pp_brbond1_phy [label="p-brbond1-phy", fillcolor="#ede9fe", color="#6d28d9"];
+ c_pp_brbond1_ovs [label="p-brbond1-ovs", fillcolor="#ede9fe", color="#6d28d9"];
+ }
+
+ c_net_ext -> c_lx_ext;
+ c_lx_ext -> c_pp_brbond2_phy;
+ c_pp_brbond2_ovs -> c_ovs_ext;
+ c_net_ctl -> c_lx_ctl;
+ c_lx_ctl -> c_pp_brbond1_phy;
+ c_pp_brbond1_ovs -> c_ovs_ctl;
+ c_net_hs -> c_bond0;
+ c_bond0 -> c_ovs_hs [label="direct interface"];
+
+ c_ovs_ext -> c_neutron;
+ c_ovs_hs -> c_neutron;
+ c_ovs_ctl -> c_neutron;
+ }
+
+This diagram shows how host networks are wired into Neutron through Open
+vSwitch bridges. When a network maps to a Linux bridge, that bridge connects to
+OVS via a patch link. When a network does not map to a Linux bridge, it is
+connected directly into the OVS bridge. Where possible, operators should
+prefer wiring a bond directly into OVS, since this can enable hardware
+offloading. Linux bridges are required when a service running on the host must
+attach directly to that network.
+
+Controllers usually combine bridge-on-bond for external/API traffic with VLAN
+subinterfaces for API and tunnel networks.
+
+In ``etc/kayobe/controllers.yml`` ensure the host network list includes the
+expected external and control networks:
+
+.. code-block:: yaml
+
+ controller_network_interfaces: "{{ storage_network_interfaces + controller_extra_network_interfaces }}"
+
+ controller_extra_network_interfaces:
+ - external_10g
+ - external_bridge
+ - control_25g
+ - external_user_net
+ - tenant_tunnel_net
+
+In ``inventory/group_vars/controllers/network-interfaces`` define the bond and
+bridge pattern.
+
+To see which interface variables require definitions, run
+``kayobe configuration dump --var-name network_interfaces --limit controllers[0]``. Before creating this file, also run ``ip a`` on
+controller hosts and capture the physical NIC names used in bond slave lists.
+
+.. code-block:: yaml
+
+ external_net_names:
+ - external_bridge
+ - hs_bond
+ - provision_ctl_net
+
+ external_10g_interface: bond2
+ external_10g_bond_mode: 802.3ad
+ external_10g_bond_xmit_hash_policy: layer3+4
+ external_10g_bond_slaves:
+ - eno35np2
+ - eno36np3
+
+ control_25g_bond_slaves:
+ - eno33np0
+ - eno34np1
+
+ hs_bond_bond_slaves:
+ - ens3f0np0
+ - ens3f1np1
+
+ external_bridge_interface: "br{{ external_10g_interface }}"
+ external_bridge_bridge_ports:
+ - "{{ external_10g_interface }}"
+
+ external_user_net_interface: "{{ external_bridge_interface }}.{{ external_user_net_vlan }}"
+ tenant_tunnel_net_interface: "{{ hs_bond_interface }}.{{ tenant_tunnel_net_vlan }}"
+
+Compute
+=======
+
+.. warning::
+
+ Network reconfiguration can potentially break networking and render hosts
+ inaccessible. This is particularly risky when moving the admin interface
+ onto a bridge or bond. It is recommended that you:
+
+ - Add a password to a local user account that can be accessed via the BMC
+ (either web UI or serial console) to recover should configuration fail.
+ This password should be removed after successful initial deployment.
+
+ - Configure one host per group initially before applying the configuration
+ across the entire group. If a host becomes misconfigured, it can simply be
+ reprovisioned.
+
+The diagram below shows the network interface configuration for the compute
+nodes:
+
+Compute hosts use a high-speed bond for internal OpenStack networks and a
+separate bond for management connectivity.
+
+.. graphviz::
+ :caption: Compute interface layering
+
+ digraph compute_interfaces {
+ rankdir=LR;
+ nodesep=0.35;
+ ranksep=0.6;
+ fontname="Helvetica";
+ node [shape=box, style="rounded,filled", color="#334155", fillcolor="#f8fafc", fontname="Helvetica", fontsize=10];
+ edge [color="#475569", arrowsize=0.7, penwidth=1.1];
+
+ k_m1 [label="eno12399np0", fillcolor="#e2e8f0"];
+ k_m2 [label="eno12409np1", fillcolor="#e2e8f0"];
+ k_m3 [label="ens3f0np0", fillcolor="#e2e8f0"];
+ k_m4 [label="ens3f1np1", fillcolor="#e2e8f0"];
+
+ k_b1 [label="bond1", fillcolor="#dbeafe", color="#1d4ed8"];
+ k_b0 [label="bond0", fillcolor="#dbeafe", color="#1d4ed8"];
+
+ k_v_int [label="bond0.4", fillcolor="#fef3c7", color="#b45309"];
+ k_v_sto [label="bond0.5", fillcolor="#fef3c7", color="#b45309"];
+ k_v_tun [label="bond0.7", fillcolor="#fef3c7", color="#b45309"];
+
+ k_br_ctl [label="brbond1", fillcolor="#dcfce7", color="#15803d"];
+
+ k_n_ctl [label="provision_ctl_net", fillcolor="#fee2e2", color="#b91c1c"];
+ k_n_int [label="internal_net", fillcolor="#fee2e2", color="#b91c1c"];
+ k_n_sto [label="storage_net", fillcolor="#fee2e2", color="#b91c1c"];
+ k_n_tun [label="tenant_tunnel_net", fillcolor="#fee2e2", color="#b91c1c"];
+
+ k_n_ctl -> k_br_ctl -> k_b1;
+ k_n_int -> k_v_int -> k_b0;
+ k_n_sto -> k_v_sto -> k_b0;
+ k_n_tun -> k_v_tun -> k_b0;
+
+ k_b1 -> k_m1;
+ k_b1 -> k_m2;
+ k_b0 -> k_m3;
+ k_b0 -> k_m4;
+ }
+
+Compute hosts also integrate with Neutron through Open vSwitch bridges, using
+a similar bridge-on-bond model for control network traffic.
+
+.. graphviz::
+ :caption: Wiring networks into Neutron (compute)
+
+ digraph neutron_wiring_compute {
+ rankdir=LR;
+ nodesep=0.35;
+ ranksep=0.7;
+ fontname="Helvetica";
+ node [shape=box, style="rounded,filled", color="#334155", fillcolor="#f8fafc", fontname="Helvetica", fontsize=10];
+ edge [color="#475569", arrowsize=0.7, penwidth=1.1];
+
+ { rank=same; k_net_hs; k_net_ctl; }
+
+ k_net_hs [label="hs_bond\nphysnet2\n(network maps directly)", fillcolor="#fee2e2", color="#b91c1c"];
+ k_net_ctl [label="provision_ctl_net\nphysnet3\n(network maps to Linux bridge)", fillcolor="#fee2e2", color="#b91c1c"];
+
+ k_lx_ctl [label="Linux bridge\nbrbond1", fillcolor="#dcfce7", color="#15803d"];
+ k_bond0 [label="bond0", fillcolor="#fef3c7", color="#b45309"];
+ k_ovs_hs [label="OVS bridge\nbond0-ovs", fillcolor="#dbeafe", color="#1d4ed8"];
+ k_ovs_ctl [label="OVS bridge\nbrbond1-ovs", fillcolor="#dbeafe", color="#1d4ed8"];
+ k_neutron [label="Neutron\nprovider networks", fillcolor="#e0f2fe", color="#0284c7"];
+
+ subgraph cluster_k_pp_brbond1 {
+ label="patch link";
+ style="dashed";
+ color="#6d28d9";
+ fillcolor="#faf5ff";
+ k_pp_brbond1_phy [label="p-brbond1-phy", fillcolor="#ede9fe", color="#6d28d9"];
+ k_pp_brbond1_ovs [label="p-brbond1-ovs", fillcolor="#ede9fe", color="#6d28d9"];
+ }
+
+ k_net_ctl -> k_lx_ctl;
+ k_lx_ctl -> k_pp_brbond1_phy;
+ k_pp_brbond1_ovs -> k_ovs_ctl;
+ k_net_hs -> k_bond0;
+ k_bond0 -> k_ovs_hs [label="direct interface"];
+
+ k_ovs_hs -> k_neutron;
+ k_ovs_ctl -> k_neutron;
+ }
+
+.. note::
+
+ Operators should prefer directly connecting interfaces into Open vSwitch
+ bridges to allow for hardware offloading.
+
+Compute hosts in this configuration use a high-speed bond and a separate
+control bond, with internal, storage, and tunnel VLANs carried on the
+high-speed bond.
+
+In ``etc/kayobe/compute.yml`` include the required network list:
+
+.. code-block:: yaml
+
+ compute_extra_network_interfaces:
+ - provision_ctl_net
+ - control_25g
+ - hs_bond
+
+In ``inventory/group_vars/compute/network-interfaces`` define bond members and
+VLAN subinterfaces.
+
+To see which interface variables require definitions, run
+``kayobe configuration dump --var-name network_interfaces --limit compute[0]``.
+Before creating this file, also run ``ip a`` on compute hosts
+and use the detected physical interface names for bond members.
+
+.. code-block:: yaml
+
+ control_25g_bond_slaves:
+ - eno12399np0
+ - eno12409np1
+
+ hs_bond_bond_slaves:
+ - ens3f0np0
+ - ens3f1np1
+
+ internal_net_interface: "{{ hs_bond_interface }}.{{ internal_net_vlan }}"
+ storage_net_interface: "{{ hs_bond_interface }}.{{ storage_net_vlan }}"
+ tenant_tunnel_net_interface: "{{ hs_bond_interface }}.{{ tenant_tunnel_net_vlan }}"
+
+Storage
+=======
+
+.. warning::
+
+ Network reconfiguration can potentially break networking and render hosts
+ inaccessible. This is particularly risky when moving the admin interface
+ onto a bridge or bond. It is recommended that you:
+
+ - Add a password to a local user account that can be accessed via the BMC
+ (either web UI or serial console) to recover should configuration fail.
+ This password should be removed after successful initial deployment.
+
+ - Configure one host per group initially before applying the configuration
+ across the entire group. If a host becomes misconfigured, it can simply be
+ reprovisioned.
+
+Storage hosts follow the same control-bond plus high-speed-bond model as
+compute hosts, with additional networks for storage and storage management.
+
+.. graphviz::
+ :caption: Storage interface layering
+
+ digraph storage_interfaces {
+ rankdir=LR;
+ nodesep=0.35;
+ ranksep=0.6;
+ fontname="Helvetica";
+ node [shape=box, style="rounded,filled", color="#334155", fillcolor="#f8fafc", fontname="Helvetica", fontsize=10];
+ edge [color="#475569", arrowsize=0.7, penwidth=1.1];
+
+ s_m1 [label="eno33np0", fillcolor="#e2e8f0"];
+ s_m2 [label="eno34np1", fillcolor="#e2e8f0"];
+ s_m3 [label="ens2f0np0", fillcolor="#e2e8f0"];
+ s_m4 [label="ens2f1np1", fillcolor="#e2e8f0"];
+
+ s_b1 [label="bond1", fillcolor="#dbeafe", color="#1d4ed8"];
+ s_b0 [label="bond0", fillcolor="#dbeafe", color="#1d4ed8"];
+
+ s_v_int [label="bond0.4", fillcolor="#fef3c7", color="#b45309"];
+ s_v_sto [label="bond0.5", fillcolor="#fef3c7", color="#b45309"];
+ s_v_sto_mgmt [label="bond0.6", fillcolor="#fef3c7", color="#b45309"];
+
+ s_br_ctl [label="brbond1", fillcolor="#dcfce7", color="#15803d"];
+
+ s_n_ctl [label="provision_ctl_net", fillcolor="#fee2e2", color="#b91c1c"];
+ s_n_int [label="internal_net", fillcolor="#fee2e2", color="#b91c1c"];
+ s_n_sto [label="storage_net", fillcolor="#fee2e2", color="#b91c1c"];
+ s_n_sto_mgmt [label="storage_mgmt_net", fillcolor="#fee2e2", color="#b91c1c"];
+
+ s_n_ctl -> s_br_ctl -> s_b1;
+ s_n_int -> s_v_int -> s_b0;
+ s_n_sto -> s_v_sto -> s_b0;
+ s_n_sto_mgmt -> s_v_sto_mgmt -> s_b0;
+
+ s_b1 -> s_m1;
+ s_b1 -> s_m2;
+ s_b0 -> s_m3;
+ s_b0 -> s_m4;
+ }
+
+Storage hosts in this configuration follow the same control-bond plus
+high-speed-bond model, with storage and storage management networks carried as
+VLAN subinterfaces on the high-speed bond.
+
+In ``etc/kayobe/storage.yml`` include the required network list:
+
+.. code-block:: yaml
+
+ storage_extra_network_interfaces:
+ - provision_ctl_net
+ - control_25g
+ - hs_bond
+ - storage_mgmt_net
+
+In ``inventory/group_vars/storage/network-interfaces`` define bond members and
+network VLAN mappings.
+
+To see which interface variables require definitions, run
+``kayobe configuration dump --var-name network_interfaces --limit storage[0]`` against the
+storage group. Before creating this file, also run ``ip a`` on storage hosts
+and use the detected physical interface names for bond members.
+
+.. code-block:: yaml
+
+ control_25g_bond_slaves:
+ - eno33np0
+ - eno34np1
+
+ hs_bond_bond_slaves:
+ - ens2f0np0
+ - ens2f1np1
+
+ internal_net_interface: "{{ hs_bond_interface }}.{{ internal_net_vlan }}"
+ storage_net_interface: "{{ hs_bond_interface }}.{{ storage_net_vlan }}"
+ storage_mgmt_net_interface: "{{ hs_bond_interface }}.{{ storage_mgmt_net_vlan }}"
+
+Validation
+==========
+
+After applying the configuration, verify network connectivity across all host
+groups:
+
+.. code-block:: console
+
+ kayobe network connectivity check
diff --git a/doc/source/configuration/index.rst b/doc/source/configuration/index.rst
index 3dc8c7a43..f567656d0 100644
--- a/doc/source/configuration/index.rst
+++ b/doc/source/configuration/index.rst
@@ -11,4 +11,5 @@ Kayobe's configuration options.
:maxdepth: 2
scenarios/index
+ examples/index
reference/index
diff --git a/doc/source/configuration/reference/network.rst b/doc/source/configuration/reference/network.rst
index 97f0e493a..00dac7e54 100644
--- a/doc/source/configuration/reference/network.rst
+++ b/doc/source/configuration/reference/network.rst
@@ -10,6 +10,13 @@ that define the network's attributes. For example, to configure the ``cidr``
attribute of a network named ``arpanet``, we would use a variable named
``arpanet_cidr``.
+Real World Examples
+===================
+
+* :doc:`../examples/real-world-bridges-and-bonds-configuration-a-virtualised-case-study`:
+ End-to-end example covering bridges, bonds, and seed hypervisor network
+ layout.
+
.. _configuration-network-global:
Global Network Configuration
@@ -1262,6 +1269,11 @@ are attached may be defined in a host or group variables file. See
Complete Example
================
+.. note::
+
+ For a more comprehensive real-world example, see
+ :doc:`../examples/real-world-bridges-and-bonds-configuration-a-virtualised-case-study`.
+
The following example combines the complete network configuration into a single
system configuration. In our example cloud we have three networks:
``management``, ``cloud`` and ``external``:
diff --git a/kayobe/ansible.py b/kayobe/ansible.py
index 10cb30726..406076a47 100644
--- a/kayobe/ansible.py
+++ b/kayobe/ansible.py
@@ -83,6 +83,169 @@ def add_args(parser):
"specific playbooks. \"all\" skips all playbooks")
+def add_inventory_args(parser):
+ """Add arguments required for running Ansible playbooks to a parser."""
+
+ # Kayobe configuration options
+ default_config_path = os.getenv(CONFIG_PATH_ENV, DEFAULT_CONFIG_PATH)
+ default_environment = os.getenv(ENVIRONMENT_ENV)
+ parser.add_argument(
+ "--config-path",
+ default=default_config_path,
+ help="path to Kayobe configuration. (default=$%s or %s)"
+ % (CONFIG_PATH_ENV, DEFAULT_CONFIG_PATH),
+ )
+ parser.add_argument(
+ "--environment",
+ default=default_environment,
+ help=("specify environment name "
+ "(default=$%s or None)" % ENVIRONMENT_ENV),
+ )
+ parser.add_argument(
+ "-i",
+ "--inventory",
+ metavar="INVENTORY",
+ action="append",
+ help=(
+ "specify inventory host path "
+ "(default=$%s/inventory or %s/inventory) "
+ )
+ % (CONFIG_PATH_ENV, DEFAULT_CONFIG_PATH),
+ )
+ # Positional argument
+ parser.add_argument(
+ "group",
+ nargs="?",
+ help=(
+ "The name of a group in the inventory, relevant when using --graph"
+ ),
+ )
+
+ # Standard options
+ parser.add_argument(
+ "-v",
+ "--verbose",
+ action="count",
+ help=(
+ "Causes Ansible to print more debug messages. Adding multiple -v"
+ " will increase the verbosity, the builtin plugins currently"
+ " evaluate up to -vvvvvv. A reasonable level to start is -vvv,"
+ " connection debugging might require -vvvv."
+ ),
+ )
+ parser.add_argument(
+ "-l",
+ "--limit",
+ metavar="SUBSET",
+ help="further limit selected hosts to an additional pattern",
+ )
+ parser.add_argument(
+ "--flush-cache",
+ action="store_true",
+ help="clear the fact cache for every host in inventory",
+ )
+ parser.add_argument(
+ "--vault-id",
+ metavar="VAULT_IDS",
+ action="append",
+ help=(
+ "the vault identity to use. This argument may be specified "
+ "multiple times."
+ ),
+ )
+ parser.add_argument(
+ "--playbook-dir",
+ metavar="BASEDIR",
+ help=(
+ "Since this tool does not use playbooks, use this as a substitute"
+ "playbook directory. This sets the relative path for many "
+ "features including roles/ group_vars/ etc."
+ ),
+ )
+ parser.add_argument(
+ "-e",
+ "--extra-vars",
+ metavar="EXTRA_VARS",
+ action="append",
+ dest="extra_vars",
+ help=(
+ "set additional variables as key=value or YAML/JSON, if filename"
+ " prepend with @. This argument may be specified multiple times."
+ ),
+ )
+
+ # Format options
+ parser.add_argument(
+ "-y",
+ "--yaml",
+ action="store_true",
+ help="Use YAML format instead of default JSON, ignored for --graph",
+ )
+ parser.add_argument(
+ "--toml",
+ action="store_true",
+ help="Use TOML format instead of default JSON, ignored for --graph",
+ )
+
+ # Other options
+ parser.add_argument(
+ "--export",
+ action="store_true",
+ help=(
+ "When doing --list, represent in a way that is optimized for"
+ " export,not as an accurate representation of how Ansible has"
+ " processed it"
+ ),
+ )
+ parser.add_argument(
+ "--output",
+ metavar="OUTPUT_FILE",
+ help=(
+ "When doing --list, send the inventory to a file instead of to the"
+ " screen"
+ ),
+ )
+ parser.add_argument(
+ "--vars",
+ action="store_true",
+ help="Add vars to graph display, ignored unless used with --graph",
+ )
+ parser.add_argument(
+ "--version",
+ action="version",
+ version="%(prog)s 2.15.0 (example version)",
+ help=(
+ "show program's version number, config file location, configured"
+ "module search path, module location, executable location "
+ "and exit"
+ ),
+ )
+
+ # Actions: mutually exclusive
+ action_group = parser.add_mutually_exclusive_group(required=True)
+ action_group.add_argument(
+ "--list",
+ action="store_true",
+ help="Output all hosts info, works as inventory script",
+ )
+ action_group.add_argument(
+ "--host",
+ metavar="HOST",
+ help=(
+ "Output specific host info, works as inventory script. It will"
+ " ignore limit"
+ ),
+ )
+ action_group.add_argument(
+ "--graph",
+ action="store_true",
+ help=(
+ "create inventory graph, if supplying pattern it must be a valid"
+ " group name. It will ignore limit"
+ ),
+ )
+
+
def _get_inventories_paths(parsed_args, env_paths):
"""Return the paths to the Kayobe inventories."""
default_inventory = utils.get_data_files_path("ansible", "inventory")
@@ -351,6 +514,70 @@ def run_playbooks(parsed_args, playbooks,
sys.exit(e.returncode)
+def build_long_opts(arg_dict):
+ """Convert dict to flat ['--flag', 'value', ...] list."""
+ cmd = []
+ for key, value in arg_dict.items():
+ if value is None or value is False:
+ continue
+ flag = f'--{key.replace("_", "-")}'
+ if isinstance(value, bool):
+ if value:
+ cmd.append(flag)
+ elif isinstance(value, list):
+ for v in value:
+ cmd.extend([flag, str(v)])
+ else:
+ cmd.extend([flag, str(value)])
+
+ return cmd
+
+
+def run_ansible_inventory(parsed_args):
+ """Run the Ansible inventory command."""
+
+ cmd = ["ansible-inventory"]
+ environment_finder = utils.EnvironmentFinder(
+ parsed_args.config_path, parsed_args.environment
+ )
+ env_paths = environment_finder.ordered_paths()
+ inventories = _get_inventories_paths(parsed_args, env_paths)
+ for inventory in inventories:
+ cmd += ["--inventory", inventory]
+ vars_paths = [parsed_args.config_path]
+ for env_path in env_paths:
+ vars_paths.append(env_path)
+ vars_files = _get_vars_files(vars_paths)
+ for vars_file in vars_files:
+ cmd += ["-e", "@%s" % vars_file]
+ cmd += vault.build_args(parsed_args, "--vault-password-file")
+
+ env = _get_environment(parsed_args, external_playbook=False)
+
+ passthrough_args = vars(parsed_args)
+
+ # Remove arguments that are not passthrough args.
+ passthrough_args.pop("inventory", None)
+ passthrough_args.pop("environment", None)
+ passthrough_args.pop("config_path", None)
+
+ # Remove positional argument 'group' from passthrough args
+ # prior to building inventory args, as we don't pass it as a long option.
+ # We do want to use it as a positional argument at the end of the
+ # command.
+ group = passthrough_args.pop("group", None)
+
+ cmd += build_long_opts(passthrough_args)
+
+ # Add positional args at the end, as required by ansible-inventory.
+ if group:
+ cmd += [group]
+
+ LOG.debug("Running: %s" % " ".join(cmd))
+
+ os.execvpe(cmd[0], cmd, env)
+
+
def run_playbook(parsed_args, playbook, *args, **kwargs):
"""Run a Kayobe Ansible playbook."""
return run_playbooks(parsed_args, [playbook], *args, **kwargs)
diff --git a/kayobe/cli/commands.py b/kayobe/cli/commands.py
index a0213be70..ec9275e1a 100644
--- a/kayobe/cli/commands.py
+++ b/kayobe/cli/commands.py
@@ -1172,6 +1172,20 @@ def take_action(self, parsed_args):
extra_vars=extra_vars)
+class Inventory(VaultMixin, Command):
+ """Wrapper for ansible-inventory showing Kayobe inventory."""
+
+ def get_parser(self, prog_name):
+ parser = super(Inventory, self).get_parser(prog_name)
+ group = parser.add_argument_group("Inventory")
+ ansible.add_inventory_args(group)
+ return parser
+
+ def take_action(self, parsed_args):
+ self.app.LOG.debug("Running ansible-inventory")
+ ansible.run_ansible_inventory(parsed_args)
+
+
class InfraVMHostUpgrade(KayobeAnsibleMixin, VaultMixin, Command):
"""Upgrade the infra VM host services.
diff --git a/kayobe/tests/unit/test_ansible.py b/kayobe/tests/unit/test_ansible.py
index eb4cee23a..9e7500b86 100644
--- a/kayobe/tests/unit/test_ansible.py
+++ b/kayobe/tests/unit/test_ansible.py
@@ -192,6 +192,92 @@ def test_reserved_environment_negative(
parsed_args = parser.parse_args(args)
ansible.run_playbooks(parsed_args, ["playbook1.yml", "playbook2.yml"])
+ def test_build_long_opts(self):
+ # Ensure build_long_opts handles different types correctly.
+ opts = ansible.build_long_opts({
+ "foo": "bar",
+ "baz": True,
+ "quux": False,
+ "items": ["a", "b"],
+ "none": None,
+ })
+ self.assertIn("--foo", opts)
+ self.assertIn("bar", opts)
+ self.assertIn("--baz", opts)
+ self.assertNotIn("--quux", opts)
+ # ensure list items appear correctly as repeated flags
+ # exact expected output including list entries and bool flag.
+ expected = ["--foo", "bar", "--baz", "--items", "a",
+ "--items", "b"]
+ self.assertEqual(expected, opts)
+ self.assertNotIn("--none", opts)
+
+ @mock.patch.object(os, "execvpe")
+ @mock.patch.object(ansible, "_get_inventories_paths")
+ @mock.patch.object(ansible, "_get_vars_files")
+ @mock.patch.object(vault, "build_args")
+ @mock.patch.object(ansible, "_get_environment")
+ def test_run_ansible_inventory_basic(self, mock_env, mock_vault_args,
+ mock_vars_files, mock_inventories,
+ mock_exec):
+ parser = argparse.ArgumentParser()
+ ansible.add_inventory_args(parser)
+ args = [
+ "--config-path", "/etc/kayobe",
+ "--environment", "foo",
+ "--inventory", "/inv1",
+ "--inventory", "/inv2",
+ "--list",
+ ]
+ parsed_args = parser.parse_args(args)
+ mock_inventories.return_value = ["/inv1", "/inv2"]
+ mock_vars_files.return_value = ["/vars1.yml"]
+ mock_vault_args.return_value = ["--vault-password-file", "/pw"]
+ mock_env.return_value = {"FOO": "bar"}
+ ansible.run_ansible_inventory(parsed_args)
+ expected_cmd = [
+ "ansible-inventory",
+ "--inventory", "/inv1",
+ "--inventory", "/inv2",
+ "-e", "@/vars1.yml",
+ "--vault-password-file", "/pw",
+ "--list",
+ ]
+ mock_exec.assert_called_once_with(
+ expected_cmd[0], expected_cmd, {"FOO": "bar"}
+ )
+
+ @mock.patch.object(os, "execvpe")
+ @mock.patch.object(ansible, "_get_inventories_paths")
+ @mock.patch.object(ansible, "_get_vars_files")
+ @mock.patch.object(vault, "build_args")
+ @mock.patch.object(ansible, "_get_environment")
+ def test_run_ansible_inventory_with_group(self, mock_env, mock_vault_args,
+ mock_vars_files,
+ mock_inventories, mock_exec):
+ parser = argparse.ArgumentParser()
+ ansible.add_inventory_args(parser)
+ args = [
+ "--inventory", "/inv",
+ "--graph",
+ "web",
+ ]
+ parsed_args = parser.parse_args(args)
+ mock_inventories.return_value = ["/inv"]
+ mock_vars_files.return_value = []
+ mock_vault_args.return_value = []
+ mock_env.return_value = {}
+ ansible.run_ansible_inventory(parsed_args)
+ expected_cmd = [
+ "ansible-inventory",
+ "--inventory", "/inv",
+ "--graph",
+ "web",
+ ]
+ mock_exec.assert_called_once_with(
+ expected_cmd[0], expected_cmd, {}
+ )
+
@mock.patch.object(utils, "run_command")
@mock.patch.object(ansible, "_get_vars_files")
@mock.patch.object(ansible, "_validate_args")
diff --git a/playbooks/kayobe-overcloud-base/globals.yml.j2 b/playbooks/kayobe-overcloud-base/globals.yml.j2
index 58c4c7ec9..75a08f71a 100644
--- a/playbooks/kayobe-overcloud-base/globals.yml.j2
+++ b/playbooks/kayobe-overcloud-base/globals.yml.j2
@@ -24,3 +24,6 @@ kolla_admin_openrc_cacert: "/etc/pki/tls/certs/ca-bundle.crt"
libvirt_tls: "yes"
certificates_libvirt_output_dir: "{% raw %}{{ kayobe_env_config_path }}{% endraw %}/certificates/libvirt"
{% endif %}
+
+# FIXME(wszumski): The time check is unreliable in CI
+prechecks_enable_host_ntp_checks: false
diff --git a/playbooks/kayobe-overcloud-upgrade-base/globals.yml.j2 b/playbooks/kayobe-overcloud-upgrade-base/globals.yml.j2
index 6dc842c47..561e1e9d5 100644
--- a/playbooks/kayobe-overcloud-upgrade-base/globals.yml.j2
+++ b/playbooks/kayobe-overcloud-upgrade-base/globals.yml.j2
@@ -14,3 +14,6 @@ openstack_service_rpc_workers: "1"
# Reduce size of libvirt logs when OpenStack debug logging is enabled
nova_libvirt_logging_debug: False
+
+# FIXME(wszumski): The time check is unreliable in CI
+prechecks_enable_host_ntp_checks: false
diff --git a/playbooks/kayobe-seed-base/globals.yml.j2 b/playbooks/kayobe-seed-base/globals.yml.j2
index 82a51b601..153934272 100644
--- a/playbooks/kayobe-seed-base/globals.yml.j2
+++ b/playbooks/kayobe-seed-base/globals.yml.j2
@@ -1,3 +1,6 @@
---
# Use HTTPS opendev quay.io registry proxy.
docker_registry_insecure: no
+
+# FIXME(wszumski): The time check is unreliable in CI
+prechecks_enable_host_ntp_checks: false
diff --git a/playbooks/kayobe-seed-upgrade-base/globals.yml.j2 b/playbooks/kayobe-seed-upgrade-base/globals.yml.j2
index 82a51b601..153934272 100644
--- a/playbooks/kayobe-seed-upgrade-base/globals.yml.j2
+++ b/playbooks/kayobe-seed-upgrade-base/globals.yml.j2
@@ -1,3 +1,6 @@
---
# Use HTTPS opendev quay.io registry proxy.
docker_registry_insecure: no
+
+# FIXME(wszumski): The time check is unreliable in CI
+prechecks_enable_host_ntp_checks: false
diff --git a/releasenotes/notes/adds-ansible-inventory-wrapper-3c3b88c0c0b33d16.yaml b/releasenotes/notes/adds-ansible-inventory-wrapper-3c3b88c0c0b33d16.yaml
new file mode 100644
index 000000000..b15b68e0b
--- /dev/null
+++ b/releasenotes/notes/adds-ansible-inventory-wrapper-3c3b88c0c0b33d16.yaml
@@ -0,0 +1,5 @@
+---
+feature:
+ - |
+ Adds a `kayobe inventory` command which wraps `ansible-inventory` to
+ display the Kayobe Ansible inventory.
diff --git a/roles/kayobe-ci-prep/tasks/main.yml b/roles/kayobe-ci-prep/tasks/main.yml
index c44508217..b3b9c25ad 100644
--- a/roles/kayobe-ci-prep/tasks/main.yml
+++ b/roles/kayobe-ci-prep/tasks/main.yml
@@ -1,4 +1,14 @@
---
+- name: Configure secondary storage drive
+ #NOTE(wszumski) Some instances provide a smaller 40G root disk with a
+ #secondary storage drive. This kolla-ansible provided role sets up the
+ #secondary drive for docker/podman storage.
+ # see: https://docs.opendev.org/opendev/infra-manual/latest/testing.html#unprivileged-single-use-vms
+ include_role:
+ name: configure-ephemeral
+ vars:
+ configure_ephemeral_mountpoint: "{{ '/var/lib/containers' if container_engine | default('docker') == 'podman' else '/var/lib/docker' }}"
+
- name: Set Rocky Linux mirror to download.rockylinux.org
become: true
ansible.builtin.shell:
diff --git a/roles/kayobe-diagnostics/files/get_logs.sh b/roles/kayobe-diagnostics/files/get_logs.sh
index cc880b9d6..267563523 100644
--- a/roles/kayobe-diagnostics/files/get_logs.sh
+++ b/roles/kayobe-diagnostics/files/get_logs.sh
@@ -114,6 +114,8 @@ copy_logs() {
iptables-save > ${LOG_DIR}/system_logs/iptables.txt
+ lsblk | tee ${LOG_DIR}/system_logs/lsblk.txt
+
if [ `command -v dpkg` ]; then
dpkg -l > ${LOG_DIR}/system_logs/dpkg-l.txt
fi
diff --git a/setup.cfg b/setup.cfg
index 81bc91a1d..61c698637 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -53,6 +53,7 @@ kayobe.cli=
control_host_upgrade = kayobe.cli.commands:ControlHostUpgrade
configuration_dump = kayobe.cli.commands:ConfigurationDump
environment_create = kayobe.cli.commands:EnvironmentCreate
+ inventory= kayobe.cli.commands:Inventory
kolla_ansible_run = kayobe.cli.commands:KollaAnsibleRun
network_connectivity_check = kayobe.cli.commands:NetworkConnectivityCheck
overcloud_bios_raid_configure = kayobe.cli.commands:OvercloudBIOSRAIDConfigure
diff --git a/zuul.d/jobs.yaml b/zuul.d/jobs.yaml
index ee817b185..ecd50c249 100644
--- a/zuul.d/jobs.yaml
+++ b/zuul.d/jobs.yaml
@@ -111,6 +111,8 @@
container_engine: 'docker'
ci_network_engine: default
ironic_boot_mode: "bios"
+ roles:
+ - zuul: openstack/kolla
- job:
name: kayobe-overcloud-base