diff --git a/ansible/inventory/group_vars/all/inspector b/ansible/inventory/group_vars/all/inspector index f74f39a09..b65faaf80 100644 --- a/ansible/inventory/group_vars/all/inspector +++ b/ansible/inventory/group_vars/all/inspector @@ -119,22 +119,24 @@ inspector_rule_ipmi_credentials: description: "Set IPMI driver_info if no credentials" conditions: - args: - value: "{node.driver_info.ipmi_username}" - regex: '\\{node\\.driver_info\\.ipmi_username\\}' + # If value matches itself as a regex, this is becaused interpolation + # failed which means the ipmi_username key was not set. + value: "{node.driver_info[ipmi_username]}" + regex: "{node\\.driver_info\\[ipmi_username\\]}" op: "matches" - args: - value: "{node.driver_info.ipmi_password}" - regex: '\\{node\\.driver_info\\.ipmi_password\\}' + value: "{node.driver_info[ipmi_password]}" + regex: "{node\\.driver_info\\[ipmi_password\\]}" op: "matches" sensitive: "true" actions: - op: "set-attribute" args: - path: "driver_info/ipmi_username" + path: "/driver_info/ipmi_username" value: "{{ inspector_rule_var_ipmi_username }}" - op: "set-attribute" args: - path: "driver_info/ipmi_password" + path: "/driver_info/ipmi_password" value: "{{ inspector_rule_var_ipmi_password }}" # Deployment kernel referenced by inspector rule. @@ -145,13 +147,13 @@ inspector_rule_deploy_kernel: description: "Set deploy kernel" conditions: - args: - value: "{node.driver_info.deploy_kernel}" - regex: '\\{node\\.driver_info\\.deploy_kernel\\}' + value: "{node.driver_info[deploy_kernel]}" + regex: "{node\\.driver_info\\[deploy_kernel\\]}" op: "matches" actions: - op: "set-attribute" args: - path: "driver_info/deploy_kernel" + path: "/driver_info/deploy_kernel" value: "{{ inspector_rule_var_deploy_kernel }}" # Deployment ramdisk referenced by inspector rule. @@ -162,13 +164,13 @@ inspector_rule_deploy_ramdisk: description: "Set deploy ramdisk" conditions: - args: - value: "{node.driver_info.deploy_ramdisk}" - regex: '\\{node\\.driver_info\\.deploy_ramdisk\\}' + value: "{node.driver_info[deploy_ramdisk]}" + regex: "{node\\.driver_info\\[deploy_ramdisk\\]}" op: "matches" actions: - op: "set-attribute" args: - path: "driver_info/deploy_ramdisk" + path: "/driver_info/deploy_ramdisk" value: "{{ inspector_rule_var_deploy_ramdisk }}" # Ironic inspector rule to set serial root device hint. @@ -177,14 +179,16 @@ inspector_rule_root_hint_serial: conditions: - args: value: "{node.properties[root_device]}" - regex: "\\{node\\.properties\\[root_device\\]\\}" + regex: "{node\\.properties\\[root_device\\]}" op: "matches" + - args: + value: "{plugin_data[root_disk][serial]}" + op: "!is-empty" actions: - op: "set-attribute" args: - path: "properties/root_device/name" - value: "{plugin_data[root_disk][by_path]}" - + path: "/properties/root_device/serial" + value: "{plugin_data[root_disk][serial]}" # Ironic inspector rule to set the interface on which the node PXE booted. inspector_rule_set_pxe_interface_mac: @@ -192,12 +196,12 @@ inspector_rule_set_pxe_interface_mac: conditions: - args: value: "{plugin_data[boot_interface]}" - regex: "'\\{plugin_data\\[boot_interface\\]\\}'" + regex: "{plugin_data\\[boot_interface\\]}" op: "!matches" actions: - op: "set-attribute" args: - path: "extra/pxe_interface_mac" + path: "/extra/pxe_interface_mac" value: "{plugin_data[boot_interface]}" # Name of network interface to use for LLDP referenced by switch port @@ -255,7 +259,7 @@ inspector_rule_lldp_switch_port_desc_to_name: actions: - op: "set-attribute" args: - path: "name" + path: "/name" value: "{{ _inspector_rule_switch_port_description_path }}" # Ironic inspector rule to save system vendor manufacturer data in the node's @@ -264,17 +268,17 @@ inspector_rule_save_system_vendor_manufacturer: description: "Save system vendor manufacturer data in Ironic node metadata" conditions: - args: - value: "{inventory.system_vendor}" - regex: "\\{inventory\\.system_vendor\\}" + value: "{inventory[system_vendor]}" + regex: "{inventory\\[system_vendor\\]}" op: "!matches" - args: - value: "{inventory.system_vendor.manufacturer}" - regex: "\\{inventory\\.system_vendor\\.manufacturer\\}" + value: "{inventory[system_vendor][manufacturer]}" + regex: "{inventory\\[system_vendor\\]\\[manufacturer\\]}" op: "!matches" actions: - op: "set-attribute" args: - path: "extra/system_vendor/manufacturer" + path: "/extra/system_vendor/manufacturer" value: "{inventory[system_vendor][manufacturer]}" # Ironic inspector rule to save system vendor serial number in the node's @@ -283,17 +287,17 @@ inspector_rule_save_system_vendor_serial_number: description: "Save system vendor serial number in Ironic node metadata" conditions: - args: - value: "{inventory.system_vendor}" - regex: "\\{inventory\\.system_vendor\\}" + value: "{inventory[system_vendor]}" + regex: "{inventory\\[system_vendor\\]}" op: "!matches" - args: - value: "{inventory.system_vendor.serial_number}" - regex: "\\{inventory\\.system_vendor\\.serial_number\\}" + value: "{inventory[system_vendor][serial_number]}" + regex: "{inventory\\[system_vendor\\]\\[serial_number\\]}" op: "!matches" actions: - op: "set-attribute" args: - path: "extra/system_vendor/serial_number" + path: "/extra/system_vendor/serial_number" value: "{inventory[system_vendor][serial_number]}" # Ironic inspector rule to save system vendor product name in the node's @@ -302,17 +306,17 @@ inspector_rule_save_system_vendor_product_name: description: "Save system vendor product name in Ironic node metadata" conditions: - args: - value: "{inventory.system_vendor}" - regex: "\\{inventory\\.system_vendor\\}" + value: "{inventory[system_vendor]}" + regex: "{inventory\\[system_vendor\\]}" op: "!matches" - args: - value: "{inventory.system_vendor.product_name}" - regex: "\\{inventory\\.system_vendor\\.product_name\\}" + value: "{inventory[system_vendor][product_name]}" + regex: "{inventory\\[system_vendor\\]\\[product_name\\]}" op: "!matches" actions: - op: "set-attribute" args: - path: "extra/system_vendor/product_name" + path: "/extra/system_vendor/product_name" value: "{inventory[system_vendor][product_name]}" # Ironic inspector rule to save introspection data to the node. @@ -322,11 +326,11 @@ inspector_rule_save_data: actions: - op: "set-attribute" args: - path: "extra/introspection_data/inventory" + path: "/extra/introspection_data/inventory" value: "{inventory}" - op: "set-attribute" args: - path: "extra/introspection_data/plugin_data" + path: "/extra/introspection_data/plugin_data" value: "{plugin_data}" # Redfish rules @@ -335,22 +339,22 @@ inspector_rule_redfish_credentials: description: "Set Redfish driver_info if no credentials" conditions: - args: - value: "{node.driver_info.redfish_username}" - regex: "\\{node\\.driver_info\\.redfish_username\\}" + value: "{node.driver_info[redfish_username]}" + regex: "{node\\.driver_info\\[redfish_username\\]}" op: "matches" - args: - value: "{node.driver_info.redfish_password}" - regex: "\\{node\\.driver_info\\.redfish_password\\}" + value: "{node.driver_info[redfish_password]}" + regex: "{node\\.driver_info\\[redfish_password\\]}" op: "matches" sensitive: true actions: - op: "set-attribute" args: - path: "driver_info/redfish_username" + path: "/driver_info/redfish_username" value: "{{ inspector_rule_var_redfish_username }}" - op: "set-attribute" args: - path: "driver_info/redfish_password" + path: "/driver_info/redfish_password" value: "{{ inspector_rule_var_redfish_password }}" # Ironic inspector rule to set Redfish address. @@ -358,13 +362,13 @@ inspector_rule_redfish_address: description: "Set Redfish address" conditions: - args: - value: "{node.driver_info.redfish_address}" - regex: "\\{node\\.driver_info\\.redfish_address\\}" + value: "{node.driver_info[redfish_address]}" + regex: "{node\\.driver_info\\[redfish_address\\]}" op: "matches" actions: - op: "set-attribute" args: - path: "driver_info/redfish_address" + path: "/driver_info/redfish_address" value: "{inventory[bmc_address]}" # Ironic inspector rule to set Redfish certificate authority. @@ -372,13 +376,13 @@ inspector_rule_redfish_verify_ca: description: "Set Redfish Verify CA" conditions: - args: - value: "{node.driver_info.redfish_verify_ca}" - regex: "\\{node\\.driver_info\\.redfish_verify_ca\\}" + value: "{node.driver_info[redfish_verify_ca]}" + regex: "{node\\.driver_info\\[redfish_verify_ca\\]}" op: "matches" actions: - op: "set-attribute" args: - path: "driver_info/redfish_verify_ca" + path: "/driver_info/redfish_verify_ca" value: "{{ inspector_rule_var_redfish_verify_ca }}" # List of default ironic inspector rules. @@ -421,4 +425,3 @@ inspector_rules: "{{ inspector_rules_default + inspector_rules_extra + (inspecto # Ansible group containing switch hosts to which the workaround should be # applied. inspector_dell_switch_lldp_workaround_group: - diff --git a/ansible/kayobe-target-venv.yml b/ansible/kayobe-target-venv.yml index 9e782bdee..11d7a8938 100644 --- a/ansible/kayobe-target-venv.yml +++ b/ansible/kayobe-target-venv.yml @@ -119,7 +119,7 @@ become: True when: kayobe_virtualenv is not defined - - name: Ensure kolla-ansible virtualenv has docker SDK for python installed + - name: Ensure kayobe virtualenv has docker SDK for python installed pip: name: docker state: latest diff --git a/ansible/network-connectivity.yml b/ansible/network-connectivity.yml index 2d8d2ccdd..3e0238d8c 100644 --- a/ansible/network-connectivity.yml +++ b/ansible/network-connectivity.yml @@ -69,7 +69,7 @@ ping {{ remote_ip }} -c1 -M do {% if mtu %} -s {{ mtu | int - icmp_overhead_bytes }}{% endif %} with_items: "{{ network_interfaces }}" loop_control: - label: "{{ remote_host }} on {{ item }}" + label: "{{ remote_host | default('none', true) }} on {{ item }}" when: - remote_hosts | length > 0 - remote_ip | length > 0 @@ -90,6 +90,6 @@ # when check: remote_ip | length > 0, would pass, but remote_ip was '' # in the command. Assumption was that this was being evaluated once # for the when clause and then again for the command. Bug? - remote_host: "{{ remote_hosts | random(seed=ansible_facts.date_time.iso8601) }}" + remote_host: "{{ remote_hosts | random(seed=ansible_facts.date_time.iso8601) if remote_hosts | length > 0 else '' }}" remote_ip: "{{ lookup('cached', 'vars', item ~ '_ips', default={})[remote_host] | default('', true) }}" mtu: "{{ item | net_mtu }}" diff --git a/dev/functions b/dev/functions index 907151f10..000370eb6 100644 --- a/dev/functions +++ b/dev/functions @@ -618,6 +618,47 @@ function overcloud_test_bounce_interface { run_kayobe overcloud host configure -t network } +function overcloud_inspection_rule_dump { + echo "Listing inspection rules ..." + openstack baremetal inspection rule list + echo "Dumping inspection rules ..." + openstack baremetal inspection rule list -c UUID -f value | xargs -L 1 openstack baremetal inspection rule show +} + +function overcloud_test_inspect { + set -eu + node=$1 + + environment_setup + + source "${KOLLA_CONFIG_PATH:-/etc/kolla}/admin-openrc-system.sh" + + overcloud_inspection_rule_dump + + echo "Baremetal node: $node before inspection" + openstack baremetal node show "$node" + + # NOTE(wszumski): Switch to using kayobe playbooks to manage and inspect + # when we switch to node registration + + if [ "$(openstack baremetal node show -c provision_state -f value $node)" != "manageable" ]; then + openstack baremetal node manage "$node" --wait + fi + + # Run inspection + openstack baremetal node inspect "$node" --wait + + echo "Baremetal node: $node after inspection" + openstack baremetal node show $node + openstack baremetal node inventory save $node + + # Use Kayobe to save introspection data + run_kayobe baremetal compute introspection data save --limit "baremetal-compute,controllers[0]" --output-dir /tmp/baremetal-compute-inspection-data + + # Move back to available + openstack baremetal node provide "$node" --wait +} + function overcloud_test { set -eu diff --git a/dev/overcloud-test-inspect.sh b/dev/overcloud-test-inspect.sh new file mode 100755 index 000000000..ea6aec667 --- /dev/null +++ b/dev/overcloud-test-inspect.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -eu +set -o pipefail + +PARENT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +source "${PARENT}/functions" + + +function main { + config_init + overcloud_test_inspect tk0 +} + +main diff --git a/doc/source/contributor/releases.rst b/doc/source/contributor/releases.rst index b0d17aba6..a95a93599 100644 --- a/doc/source/contributor/releases.rst +++ b/doc/source/contributor/releases.rst @@ -242,3 +242,21 @@ Stable Releases Stable branch releases should be made periodically for each supported stable branch, no less than once every 45 days. + +Transitioning to Unmaintained +============================= + +When an OpenStack release transitions to `Unmaintained +`__, +all references to ``stable/`` need to be changed to +``unmaintained/``. This change needs to be made on the new +unmaintained branch. For example, see +https://review.opendev.org/c/openstack/kayobe/+/968298. + +Transitioning to End of Life (EOL) +================================== + +When an OpenStack release transitions to `End of Life (EOL) +`__, +upgrade jobs in later releases need to be removed. For example, see +https://review.opendev.org/c/openstack/kayobe/+/968296. diff --git a/doc/source/usage.rst b/doc/source/usage.rst index a311490dc..70581eb9e 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -48,6 +48,16 @@ Environment variable: ``ANSIBLE_VAULT_PASSWORD_FILE`` password from a (plain text) file, with the path to that file being read from the environment. +Whilst the kolla passwords file ``kolla/passwords.yml`` should remain encrypted +at all times it can be useful to view the contents of this file to acquire a +password for a given service. +This can be done with ``ansible-vault view`` however if an absolute path is not +provided it will cause the command to fail. +Therefore, to make reading the contents of this file easier for administrators +it is possible to use ``kayobe overcloud passwords view`` which will +temporarily decrypt and display the contents of ``kolla/passwords.yml`` for the +active kayobe environment. + Limiting Hosts -------------- diff --git a/kayobe/cli/commands.py b/kayobe/cli/commands.py index be233af7f..2c9cb97a0 100644 --- a/kayobe/cli/commands.py +++ b/kayobe/cli/commands.py @@ -1579,6 +1579,14 @@ def take_action(self, parsed_args): self.run_kolla_ansible_overcloud(parsed_args, "prechecks") +class OvercloudServicePasswordsView(KayobeAnsibleMixin, VaultMixin, Command): + """View Passwords.""" + + def take_action(self, parsed_args): + self.app.LOG.debug("Displaying Passwords") + vault.view_passwords(parsed_args) + + class OvercloudServiceReconfigure(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin, Command): """Reconfigure the overcloud services. diff --git a/kayobe/vault.py b/kayobe/vault.py index 82bc0f208..3a4d32b9b 100644 --- a/kayobe/vault.py +++ b/kayobe/vault.py @@ -172,3 +172,22 @@ def update_environment(parsed_args, env): if vault_password is not None: env[VAULT_PASSWORD_ENV] = vault_password + + +def view_passwords(parsed_args): + """View passwords stored in the Ansible Vault. + + :param parsed_args: Parsed command line arguments. + """ + env_path = utils.get_kayobe_environment_path( + parsed_args.config_path, parsed_args.environment) + path = env_path if env_path else parsed_args.config_path + passwords_path = os.path.join(path, 'kolla', 'passwords.yml') + cmd = ["ansible-vault", "view", passwords_path] + cmd += ["--vault-password-file", _get_vault_password_helper()] + try: + utils.run_command(cmd) + except subprocess.CalledProcessError as e: + LOG.error("Failed to view passwords via ansible-vault " + "returncode %d", e.returncode) + sys.exit(e.returncode) diff --git a/playbooks/kayobe-overcloud-base/baremetal.j2 b/playbooks/kayobe-overcloud-base/baremetal.j2 index 013468258..47920424d 100644 --- a/playbooks/kayobe-overcloud-base/baremetal.j2 +++ b/playbooks/kayobe-overcloud-base/baremetal.j2 @@ -1,2 +1,4 @@ [baremetal-compute] hv100 +tk0 +tk1 diff --git a/playbooks/kayobe-overcloud-base/run.yml b/playbooks/kayobe-overcloud-base/run.yml index 76ecd8af6..e5c617bc8 100644 --- a/playbooks/kayobe-overcloud-base/run.yml +++ b/playbooks/kayobe-overcloud-base/run.yml @@ -36,6 +36,14 @@ chdir: "{{ kayobe_src_dir }}" executable: /bin/bash + - name: Test inspection of the baremetal machines + shell: + cmd: dev/overcloud-test-inspect.sh &> {{ logs_dir }}/ansible/overcloud-test-inspect + chdir: "{{ kayobe_src_dir }}" + executable: /bin/bash + # TODO(priteau): Fix baremetal inspect issues with UEFI + when: ironic_boot_mode == 'bios' + - name: Perform testing of the baremetal machines shell: cmd: dev/overcloud-test-baremetal.sh &> {{ logs_dir }}/ansible/overcloud-test-baremetal diff --git a/releasenotes/notes/add-passwords-view-command-2f55d83dca037e3d.yaml b/releasenotes/notes/add-passwords-view-command-2f55d83dca037e3d.yaml new file mode 100644 index 000000000..b908fb105 --- /dev/null +++ b/releasenotes/notes/add-passwords-view-command-2f55d83dca037e3d.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for easily viewing the content of ``kolla/passwords.yml`` with + the new command ``kayobe overcloud passwords view``. diff --git a/releasenotes/notes/bump-ansible-12-536bc4a3ff55dc3b.yaml b/releasenotes/notes/bump-ansible-12-536bc4a3ff55dc3b.yaml index 5901b6a26..90ee0665e 100644 --- a/releasenotes/notes/bump-ansible-12-536bc4a3ff55dc3b.yaml +++ b/releasenotes/notes/bump-ansible-12-536bc4a3ff55dc3b.yaml @@ -1,6 +1,6 @@ --- upgrade: - | - Updates the maximum supported version of Ansible from 12 (ansible-core - 2.18) to 13 (ansible-core 2.19). The minimum supported version is updated + Updates the maximum supported version of Ansible from 11 (ansible-core + 2.18) to 12 (ansible-core 2.19). The minimum supported version is updated from 10.x to 11.x. This is true for both Kayobe and Kolla Ansible. diff --git a/releasenotes/notes/drop-python310-and-311-b284d9a4d8d91324.yaml b/releasenotes/notes/drop-python310-and-311-b284d9a4d8d91324.yaml new file mode 100644 index 000000000..e72c8148a --- /dev/null +++ b/releasenotes/notes/drop-python310-and-311-b284d9a4d8d91324.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Python 3.10 and 3.11 are no longer supported on the control host. Use + Python 3.12 as a minimum version for the Kayobe virtualenv. diff --git a/releasenotes/notes/fixes-cumulus-5.13-74e0d08675404f46.yaml b/releasenotes/notes/fixes-cumulus-5.13-74e0d08675404f46.yaml new file mode 100644 index 000000000..34401e5cf --- /dev/null +++ b/releasenotes/notes/fixes-cumulus-5.13-74e0d08675404f46.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Bumps version of ``nvidia.nvue`` Ansible collection from ``1.2.6`` to + ``1.2.9``. This fixes an issue where switch configuration could not be + applied to switches running Cumulus Linux 5.13. See `LP#2131677 + `__ for more details. diff --git a/releasenotes/source/2024.1.rst b/releasenotes/source/2024.1.rst index 4977a4f1a..6896656be 100644 --- a/releasenotes/source/2024.1.rst +++ b/releasenotes/source/2024.1.rst @@ -3,4 +3,4 @@ =========================== .. release-notes:: - :branch: stable/2024.1 + :branch: unmaintained/2024.1 diff --git a/releasenotes/source/2025.2.rst b/releasenotes/source/2025.2.rst new file mode 100644 index 000000000..4dae18d86 --- /dev/null +++ b/releasenotes/source/2025.2.rst @@ -0,0 +1,6 @@ +=========================== +2025.2 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2025.2 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 4a413860a..6cd206129 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ Kayobe Release Notes :maxdepth: 1 unreleased + 2025.2 2025.1 2024.2 2024.1 diff --git a/requirements.yml b/requirements.yml index 5b08a56bc..300f6ea5a 100644 --- a/requirements.yml +++ b/requirements.yml @@ -14,7 +14,7 @@ collections: - name: dellemc.os10 version: 1.2.7 - name: nvidia.nvue - version: 1.2.6 + version: 1.2.9 - name: openstack.cloud version: '<3' - name: stackhpc.linux diff --git a/roles/kayobe-diagnostics/files/get_logs.sh b/roles/kayobe-diagnostics/files/get_logs.sh index 2b2b54964..cc880b9d6 100644 --- a/roles/kayobe-diagnostics/files/get_logs.sh +++ b/roles/kayobe-diagnostics/files/get_logs.sh @@ -151,6 +151,11 @@ copy_logs() { cp /opt/kayobe/images/deployment_image/deployment_image.stderr /opt/kayobe/images/deployment_image/deployment_image.stdout ${LOG_DIR}/kayobe/ fi + # Baremetal inspection data + if [ -d "/tmp/baremetal-compute-inspection-data" ]; then + cp -rf /tmp/baremetal-compute-inspection-data ${LOG_DIR} + fi + # Rename files to .txt; this is so that when displayed via # logs.openstack.org clicking results in the browser shows the # files, rather than trying to send it to another app or make you diff --git a/setup.cfg b/setup.cfg index 46ea41577..82526ed5b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,7 @@ description_file = author = OpenStack author_email = openstack-discuss@lists.openstack.org home_page = https://docs.openstack.org/kayobe/latest/ -python_requires = >=3.10 +python_requires = >=3.12 license = Apache License, Version 2.0 classifier = Environment :: OpenStack @@ -18,8 +18,6 @@ classifier = Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 [files] @@ -77,6 +75,7 @@ kayobe.cli= overcloud_service_deploy = kayobe.cli.commands:OvercloudServiceDeploy overcloud_service_deploy_containers = kayobe.cli.commands:OvercloudServiceDeployContainers overcloud_service_destroy = kayobe.cli.commands:OvercloudServiceDestroy + overcloud_service_passwords_view = kayobe.cli.commands:OvercloudServicePasswordsView overcloud_service_prechecks = kayobe.cli.commands:OvercloudServicePrechecks overcloud_service_reconfigure = kayobe.cli.commands:OvercloudServiceReconfigure overcloud_service_stop = kayobe.cli.commands:OvercloudServiceStop @@ -195,6 +194,8 @@ kayobe.cli.overcloud_service_upgrade = hooks = kayobe.cli.commands:HookDispatcher kayobe.cli.overcloud_swift_rings_generate = hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_passwords_view = + hooks = kayobe.cli.commands:HookDispatcher kayobe.cli.physical_network_configure = hooks = kayobe.cli.commands:HookDispatcher kayobe.cli.playbook_run =