diff --git a/ansible/baremetal-compute-serial-console.yml b/ansible/baremetal-compute-serial-console.yml new file mode 100644 index 000000000..b4783b728 --- /dev/null +++ b/ansible/baremetal-compute-serial-console.yml @@ -0,0 +1,130 @@ +--- +# This playbook will enable a serial console on all ironic nodes. This +# will allow you to access the serial console from within Horizon. +# See: https://docs.openstack.org/ironic/latest/admin/console.html + +- name: Setup OpenStack Environment + hosts: controllers[0] + gather_facts: False + vars: + venv: "{{ virtualenv_path }}/openstack-cli" + pre_tasks: + - name: Set up openstack cli virtualenv + pip: + virtualenv: "{{ venv }}" + name: + - python-openstackclient + - python-ironicclient + + - block: + - name: Fail if allocation pool start not defined + fail: + msg: > + The variable, ironic_serial_console_tcp_pool_start is not defined. + This variable is required to run this playbook. + when: not ironic_serial_console_tcp_pool_start + + - name: Fail if allocation pool end not defined + fail: + msg: > + The variable, ironic_serial_console_tcp_pool_end is not defined. + This variable is required to run this playbook. + when: + - not ironic_serial_console_tcp_pool_end + + - name: Get list of nodes that we should configure serial consoles on + set_fact: + baremetal_nodes: >- + {{ query('inventory_hostnames', console_compute_node_limit | + default('baremetal-compute') ) | unique }} + + - name: Reserve TCP ports for ironic serial consoles + include_role: + name: console-allocation + vars: + console_allocation_pool_start: "{{ ironic_serial_console_tcp_pool_start }}" + console_allocation_pool_end: "{{ ironic_serial_console_tcp_pool_end }}" + console_allocation_ironic_nodes: "{{ baremetal_nodes }}" + console_allocation_filename: "{{ kayobe_config_path }}/console-allocation.yml" + when: cmd == "enable" + +- name: Enable serial console + hosts: "{{ console_compute_node_limit | default('baremetal-compute') }}" + gather_facts: False + vars: + venv: "{{ virtualenv_path }}/openstack-cli" + controller_host: "{{ groups['controllers'][0] }}" + tasks: + - name: Get list of nodes + command: > + {{ venv }}/bin/openstack baremetal node list -f json --long + register: nodes + delegate_to: "{{ controller_host }}" + environment: "{{ openstack_auth_env }}" + run_once: true + changed_when: false + vars: + # NOTE: Without this, the controller's ansible_host variable will not + # be respected when using delegate_to. + ansible_host: "{{ hostvars[controller_host].ansible_host | default(controller_host) }}" + + - block: + - name: Fail if console interface is not ipmitool-socat + fail: + msg: >- + In order to use the serial console you must set the console_interface to ipmitool-socat. + when: node["Console Interface"] != "ipmitool-socat" + + - name: Set IPMI serial console terminal port + vars: + name: "{{ node['Name'] }}" + port: "{{ hostvars[controller_host].console_allocation_result.ports[name] }}" + # NOTE: Without this, the controller's ansible_host variable will not + # be respected when using delegate_to. + ansible_host: "{{ hostvars[controller_host].ansible_host | default(controller_host) }}" + command: > + {{ venv }}/bin/openstack baremetal node set {{ name }} --driver-info ipmi_terminal_port={{ port }} + delegate_to: "{{ controller_host }}" + environment: "{{ openstack_auth_env }}" + when: >- + node['Driver Info'].ipmi_terminal_port is not defined or + node['Driver Info'].ipmi_terminal_port | int != port | int + + - name: Enable the IPMI socat serial console + vars: + # NOTE: Without this, the controller's ansible_host variable will not + # be respected when using delegate_to. + ansible_host: "{{ hostvars[controller_host].ansible_host | default(controller_host) }}" + command: > + {{ venv }}/bin/openstack baremetal node console enable {{ node['Name'] }} + delegate_to: "{{ controller_host }}" + environment: "{{ openstack_auth_env }}" + when: not node['Console Enabled'] + vars: + matching_nodes: >- + {{ (nodes.stdout | from_json) | selectattr('Name', 'defined') | + selectattr('Name', 'equalto', inventory_hostname ) | list }} + node: "{{ matching_nodes | first }}" + when: + - cmd == "enable" + - matching_nodes | length > 0 + + - block: + - name: Disable the IPMI socat serial console + vars: + # NOTE: Without this, the controller's ansible_host variable will not + # be respected when using delegate_to. + ansible_host: "{{ hostvars[controller_host].ansible_host | default(controller_host) }}" + command: > + {{ venv }}/bin/openstack baremetal node console disable {{ node['Name'] }} + delegate_to: "{{ controller_host }}" + environment: "{{ openstack_auth_env }}" + when: node['Console Enabled'] + vars: + matching_nodes: >- + {{ (nodes.stdout | from_json) | selectattr('Name', 'defined') | + selectattr('Name', 'equalto', inventory_hostname ) | list }} + node: "{{ matching_nodes | first }}" + when: + - cmd == "disable" + - matching_nodes | length > 0 diff --git a/ansible/group_vars/all/ironic b/ansible/group_vars/all/ironic index f950b9c6e..f70d24ccd 100644 --- a/ansible/group_vars/all/ironic +++ b/ansible/group_vars/all/ironic @@ -127,3 +127,16 @@ kolla_ironic_pxe_append_params_extra: [] kolla_ironic_pxe_append_params: > {{ kolla_ironic_pxe_append_params_default + kolla_ironic_pxe_append_params_extra }} + +############################################################################### +# Ironic Node Configuration + +# This defines the start of the range of TCP ports to used for the IPMI socat +# serial consoles +ironic_serial_console_tcp_pool_start: 30000 + +# This defines the end of the range of TCP ports to used for the IPMI socat +# serial consoles +ironic_serial_console_tcp_pool_end: 31000 + +############################################################################### diff --git a/ansible/roles/console-allocation/defaults/main.yml b/ansible/roles/console-allocation/defaults/main.yml new file mode 100644 index 000000000..d3743fb33 --- /dev/null +++ b/ansible/roles/console-allocation/defaults/main.yml @@ -0,0 +1,13 @@ +--- +# Path to file in which to store console allocations. +console_allocation_filename: + +# List of Names or UUIDs corresponding to Ironic nodes that you want to allocate +# serial consoles for +console_allocation_ironic_nodes: [] + +# allocation_pool_start: First TCP port in the allocation pool +console_allocation_pool_start: + +# allocation_pool_end: Last TCP port in the allocation pool +console_allocation_pool_end: diff --git a/ansible/roles/console-allocation/library/console_allocation.py b/ansible/roles/console-allocation/library/console_allocation.py new file mode 100644 index 000000000..bb4f7bc45 --- /dev/null +++ b/ansible/roles/console-allocation/library/console_allocation.py @@ -0,0 +1,192 @@ +#!/usr/bin/python + +# Copyright (c) 2017 StackHPC Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# TODO(wszumski): If we have multiple conductors and they are on different machines +# we could make a pool per machine. + +DOCUMENTATION = """ +module: console_allocation +short_description: Allocate a serial console TCP port for an Ironic node from a pool +author: Mark Goddard (mark@stackhpc.com) and Will Szumski (will@stackhpc.com) +options: + - option-name: nodes + description: List of Names or UUIDs corresponding to Ironic Nodes + required: True + type: list + - option-name: allocation_pool_start + description: First address of the pool from which to allocate + required: True + type: int + - option-name: allocation_pool_end + description: Last address of the pool from which to allocate + required: True + type: int + - option-name: allocation_file + description: > + Path to a file in which to store the allocations. Will be created if it + does not exist. + required: True + type: string +requirements: + - PyYAML +""" + +EXAMPLES = """ +- name: Ensure Ironic node has a TCP port assigned for it's serial console + console_allocation: + nodes: ['node-1', 'node-2'] + allocation_pool_start: 30000 + allocation_pool_end: 31000 + allocation_file: /path/to/allocation/file.yml +""" + +RETURN = """ +ports: + description: > + A dictionary mapping the node name to the allocated serial console TCP port + returned: success + type: dict + sample: { 'node1' : 30000, 'node2':300001 } +""" + +from ansible.module_utils.basic import * +import sys + +# Store a list of import errors to report to the user. +IMPORT_ERRORS=[] +try: + import yaml +except Exception as e: + IMPORT_ERRORS.append(e) + + +def read_allocations(module): + """Read TCP port allocations from the allocation file.""" + filename = module.params['allocation_file'] + try: + with open(filename, 'r') as f: + content = yaml.load(f) + except IOError as e: + if e.errno == errno.ENOENT: + # Ignore ENOENT - we will create the file. + return {} + module.fail_json(msg="Failed to open allocation file %s for reading" % filename) + except yaml.YAMLError as e: + module.fail_json(msg="Failed to parse allocation file %s as YAML" % filename) + if content is None: + # If the file is empty, yaml.load() will return None. + content = {} + return content + + +def write_allocations(module, allocations): + """Write TCP port allocations to the allocation file.""" + filename = module.params['allocation_file'] + try: + with open(filename, 'w') as f: + yaml.dump(allocations, f, default_flow_style=False) + except IOError as e: + module.fail_json(msg="Failed to open allocation file %s for writing" % filename) + except yaml.YAMLError as e: + module.fail_json(msg="Failed to dump allocation file %s as YAML" % filename) + +def is_valid_port(port): + try: + int(port) + except ValueError: + return False + if port < 0: + return False + if port > 65535: + return False + return True + + +def update_allocation(module, allocations): + """Allocate a TCP port of an Ironic serial console. + + :param module: AnsibleModule instance + :param allocations: Existing IP address allocations + """ + nodes = module.params['nodes'] + + allocation_pool_start = module.params['allocation_pool_start'] + allocation_pool_end = module.params['allocation_pool_end'] + result = { + 'changed': False, + 'ports': {} + } + object_name = "serial_console_allocations" + console_allocations = allocations.setdefault(object_name, {}) + invalid_allocations = {node: port for node, port in console_allocations.items() + if not is_valid_port(port)} + if invalid_allocations: + module.fail_json(msg="Found invalid existing allocations in %s: %s" % + (object_name, + ", ".join("%s: %s" % (node, port) + for node, port in invalid_allocations.items()))) + + allocated_consoles = { int(x) for x in console_allocations.values() } + allocation_pool = { x for x in range(allocation_pool_start, allocation_pool_end + 1) } + free_ports = list(allocation_pool - allocated_consoles) + free_ports.sort(reverse=True) + + for node in nodes: + if node not in console_allocations: + if len(free_ports) < 1: + module.fail_json(msg="No unallocated TCP ports for %s in %s" % (node, object_name)) + result['changed'] = True + free_port = free_ports.pop() + console_allocations[node] = free_port + result['ports'][node] = console_allocations[node] + return result + + +def allocate(module): + """Allocate a TCP port an ironic serial console, updating the allocation file.""" + allocations = read_allocations(module) + result = update_allocation(module, allocations) + if result['changed'] and not module.check_mode: + write_allocations(module, allocations) + return result + + +def main(): + module = AnsibleModule( + argument_spec=dict( + nodes=dict(required=True, type='list'), + allocation_pool_start=dict(required=True, type='int'), + allocation_pool_end=dict(required=True, type='int'), + allocation_file=dict(required=True, type='str'), + ), + supports_check_mode=True, + ) + + # Fail if there were any exceptions when importing modules. + if IMPORT_ERRORS: + module.fail_json(msg="Import errors: %s" % + ", ".join([repr(e) for e in IMPORT_ERRORS])) + + try: + results = allocate(module) + except Exception as e: + module.fail_json(msg="Failed to allocate TCP port: %s" % repr(e)) + else: + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/ansible/roles/console-allocation/tasks/main.yml b/ansible/roles/console-allocation/tasks/main.yml new file mode 100644 index 000000000..2d3da89c3 --- /dev/null +++ b/ansible/roles/console-allocation/tasks/main.yml @@ -0,0 +1,73 @@ +--- +# Facts may not be available for the Ansible control host, so read the OS +# release manually. +- name: Check the OS release + local_action: + module: shell . /etc/os-release && echo $ID + changed_when: False + register: console_allocation_os_release + +- name: Include RedHat family-specific variables + include_vars: "RedHat.yml" + when: console_allocation_os_release.stdout in ['centos', 'fedora', 'rhel'] + +- name: Include Debian family-specific variables + include_vars: "Debian.yml" + when: console_allocation_os_release.stdout in ['debian', 'ubuntu'] + +# Note: Currently we install these using the system package manager rather than +# pip to a virtualenv. This is because Yum is required elsewhere and cannot +# easily be installed in a virtualenv. +- name: Ensure package dependencies are installed + local_action: + module: package + name: "{{ item }}" + state: installed + use: "{{ console_allocation_package_manager }}" + become: True + with_items: "{{ console_allocation_package_dependencies }}" + run_once: True + +- name: Validate allocation pool start + vars: + port: "{{ console_allocation_pool_start | int(default=-1) }}" + fail: + msg: >- + You must must define an console_allocation_pool_start. This should + be a valid TCP port. + when: >- + console_allocation_pool_end is none or + port | int < 0 or port | int > 65535 + +- name: Validate allocation pool end + vars: + port: "{{ console_allocation_pool_end | int(default=-1) }}" + fail: + msg: >- + You must must define an console_allocation_pool_end. This should + be a valid TCP port. + when: >- + console_allocation_pool_end is none or + port | int < 0 or port | int > 65535 + +- name: Validate that allocation start is less than allocation end + fail: + msg: >- + console_allocation_start and console_allocation_end define a range + of TCP ports. You have defined a range with a start that is less than + the end + when: + - (console_allocation_pool_start | int) > (console_allocation_pool_end | int) + +- name: Ensure Ironic serial console ports are allocated + local_action: + module: console_allocation + allocation_file: "{{ console_allocation_filename }}" + nodes: "{{ console_allocation_ironic_nodes }}" + allocation_pool_start: "{{ console_allocation_pool_start }}" + allocation_pool_end: "{{ console_allocation_pool_end }}" + register: result + +- name: Register a fact containing the console allocation result + set_fact: + console_allocation_result: "{{ result }}" diff --git a/ansible/roles/console-allocation/vars/Debian.yml b/ansible/roles/console-allocation/vars/Debian.yml new file mode 100644 index 000000000..de0361a96 --- /dev/null +++ b/ansible/roles/console-allocation/vars/Debian.yml @@ -0,0 +1,7 @@ +--- +# Package manager to use. +console_allocation_package_manager: apt + +# List of packages to install. +console_allocation_package_dependencies: + - python-yaml diff --git a/ansible/roles/console-allocation/vars/RedHat.yml b/ansible/roles/console-allocation/vars/RedHat.yml new file mode 100644 index 000000000..fe9989bb9 --- /dev/null +++ b/ansible/roles/console-allocation/vars/RedHat.yml @@ -0,0 +1,7 @@ +--- +# Package manager to use. +console_allocation_package_manager: yum + +# List of packages to install. +console_allocation_package_dependencies: + - PyYAML diff --git a/doc/source/administration.rst b/doc/source/administration.rst index dc9887ce3..47a3f3d04 100644 --- a/doc/source/administration.rst +++ b/doc/source/administration.rst @@ -203,6 +203,53 @@ according to their inventory host names, you can run the following command:: This command will use the ``ipmi_address`` host variable from the inventory to map the inventory host name to the correct node. + +Ironic Serial Console +--------------------- + +To access the baremetal nodes from within Horizon you need to enable the serial +console. For this to work the you must set ``kolla_enable_nova_serialconsole_proxy`` +to ``true`` in ``etc/kayobe/kolla.yml``:: + + kolla_enable_nova_serialconsole_proxy: true + +The console interface on the Ironic nodes is expected to be ``ipmitool-socat``, you +can check this with:: + + openstack baremetal node show --fields console_interface + +where should be the UUID or name of the Ironic node you want to check. + +If you have set ``kolla_ironic_enabled_console_interfaces`` in ``etc/kayobe/ironic.yml``, +it should include ``ipmitool-socat`` in the list of enabled interfaces. + +The playbook to enable the serial console currently only works if the Ironic node +name matches the inventory hostname. + +Once these requirements have been satisfied, you can run:: + + (kayobe) $ kayobe baremetal compute serial console enable + +This will reserve a TCP port for each node to use for the serial console interface. +The allocations are stored in ``${KAYOBE_CONFIG_PATH}/console-allocation.yml``. The +current implementation uses a global pool, which is specified by +``ironic_serial_console_tcp_pool_start`` and ``ironic_serial_console_tcp_pool_end``; +these variables can set in ``etc/kayobe/ironic.yml``. + +To disable the serial console you can use:: + + (kayobe) $ kayobe baremetal compute serial console disable + +The port allocated for each node is retained and must be manually removed from +``${KAYOBE_CONFIG_PATH}/console-allocation.yml`` if you want it to be reused by another +Ironic node with a different name. + +You can optionally limit the nodes targeted by setting ``baremetal-compute-limit``:: + + (kayobe) $ kayobe baremetal compute serial console enable --baremetal-compute-limit sand-6-1 + +which should take the form of an `ansible host pattern `_. + .. _update_deployment_image: Update Deployment Image diff --git a/etc/kayobe/ironic.yml b/etc/kayobe/ironic.yml index 948cd3480..4c0a0e429 100644 --- a/etc/kayobe/ironic.yml +++ b/etc/kayobe/ironic.yml @@ -103,6 +103,17 @@ # List of kernel parameters to append for baremetal PXE boot. #kolla_ironic_pxe_append_params: +############################################################################### +# Ironic Node Configuration + +# This defines the start of the range of TCP ports to used for the IPMI socat +# serial consoles +#ironic_serial_console_tcp_pool_start: + +# This defines the end of the range of TCP ports to used for the IPMI socat +# serial consoles +#ironic_serial_console_tcp_pool_end: + ############################################################################### # Dummy variable to allow Ansible to accept this file. workaround_ansible_issue_8743: yes diff --git a/kayobe/cli/commands.py b/kayobe/cli/commands.py index c44a0d19e..9278f28da 100644 --- a/kayobe/cli/commands.py +++ b/kayobe/cli/commands.py @@ -1247,6 +1247,57 @@ class BaremetalComputeRename(KayobeAnsibleMixin, VaultMixin, Command): self.run_kayobe_playbooks(parsed_args, playbooks) +class BaremetalComputeSerialConsoleBase(KayobeAnsibleMixin, VaultMixin, + Command): + + """Base class for the baremetal serial console commands""" + + @staticmethod + def process_limit(parsed_args, extra_vars): + if parsed_args.baremetal_compute_limit: + extra_vars["console_compute_node_limit"] = ( + parsed_args.baremetal_compute_limit + ) + + def get_parser(self, prog_name): + parser = super(BaremetalComputeSerialConsoleBase, self).get_parser( + prog_name) + group = parser.add_argument_group("Baremetal Serial Consoles") + group.add_argument("--baremetal-compute-limit", + help="Limit the change to the hosts specified in " + "this limit" + ) + return parser + + +class BaremetalComputeSerialConsoleEnable(BaremetalComputeSerialConsoleBase): + """Enable Serial Console for Baremetal Compute Nodes""" + + def take_action(self, parsed_args): + self.app.LOG.debug("Enabling serial console for ironic nodes") + extra_vars = {} + BaremetalComputeSerialConsoleBase.process_limit(parsed_args, + extra_vars) + extra_vars["cmd"] = "enable" + playbooks = _build_playbook_list("baremetal-compute-serial-console") + self.run_kayobe_playbooks(parsed_args, playbooks, + extra_vars=extra_vars) + + +class BaremetalComputeSerialConsoleDisable(BaremetalComputeSerialConsoleBase): + """Disable Serial Console for Baremetal Compute Nodes""" + + def take_action(self, parsed_args): + self.app.LOG.debug("Disable serial console for ironic nodes") + extra_vars = {} + BaremetalComputeSerialConsoleBase.process_limit(parsed_args, + extra_vars) + extra_vars["cmd"] = "disable" + playbooks = _build_playbook_list("baremetal-compute-serial-console") + self.run_kayobe_playbooks(parsed_args, playbooks, + extra_vars=extra_vars) + + class BaremetalComputeUpdateDeploymentImage(KayobeAnsibleMixin, VaultMixin, Command): """Update the Ironic nodes to use the new kernel and ramdisk images.""" diff --git a/kayobe/tests/unit/cli/test_commands.py b/kayobe/tests/unit/cli/test_commands.py index 07748a7e9..28cd7cbcc 100644 --- a/kayobe/tests/unit/cli/test_commands.py +++ b/kayobe/tests/unit/cli/test_commands.py @@ -1150,6 +1150,100 @@ class TestCase(unittest.TestCase): ] self.assertEqual(expected_calls, mock_run.call_args_list) + @mock.patch.object(commands.KayobeAnsibleMixin, + "run_kayobe_playbooks") + def test_baremetal_compute_serial_console_enable(self, mock_run): + command = commands.BaremetalComputeSerialConsoleEnable(TestApp(), []) + parser = command.get_parser("test") + parsed_args = parser.parse_args([]) + result = command.run(parsed_args) + self.assertEqual(0, result) + expected_calls = [ + mock.call( + mock.ANY, + [ + "ansible/baremetal-compute-serial-console.yml", + + ], + extra_vars={ + "cmd": "enable", + } + ), + ] + self.assertEqual(expected_calls, mock_run.call_args_list) + + @mock.patch.object(commands.KayobeAnsibleMixin, + "run_kayobe_playbooks") + def test_baremetal_compute_serial_console_enable_with_limit(self, + mock_run): + command = commands.BaremetalComputeSerialConsoleEnable(TestApp(), []) + parser = command.get_parser("test") + parsed_args = parser.parse_args(["--baremetal-compute-limit", + "sand-6-1"]) + result = command.run(parsed_args) + self.assertEqual(0, result) + expected_calls = [ + mock.call( + mock.ANY, + [ + "ansible/baremetal-compute-serial-console.yml", + + ], + extra_vars={ + "cmd": "enable", + "console_compute_node_limit": "sand-6-1", + } + ), + ] + self.assertEqual(expected_calls, mock_run.call_args_list) + + @mock.patch.object(commands.KayobeAnsibleMixin, + "run_kayobe_playbooks") + def test_baremetal_compute_serial_console_disable(self, mock_run): + command = commands.BaremetalComputeSerialConsoleDisable(TestApp(), []) + parser = command.get_parser("test") + parsed_args = parser.parse_args([]) + result = command.run(parsed_args) + self.assertEqual(0, result) + expected_calls = [ + mock.call( + mock.ANY, + [ + "ansible/baremetal-compute-serial-console.yml", + + ], + extra_vars={ + "cmd": "disable", + } + ), + ] + self.assertEqual(expected_calls, mock_run.call_args_list) + + @mock.patch.object(commands.KayobeAnsibleMixin, + "run_kayobe_playbooks") + def test_baremetal_compute_serial_console_disable_with_limit(self, + mock_run): + command = commands.BaremetalComputeSerialConsoleDisable(TestApp(), []) + parser = command.get_parser("test") + parsed_args = parser.parse_args(["--baremetal-compute-limit", + "sand-6-1"]) + result = command.run(parsed_args) + self.assertEqual(0, result) + expected_calls = [ + mock.call( + mock.ANY, + [ + "ansible/baremetal-compute-serial-console.yml", + + ], + extra_vars={ + "cmd": "disable", + "console_compute_node_limit": "sand-6-1", + } + ), + ] + self.assertEqual(expected_calls, mock_run.call_args_list) + @mock.patch.object(commands.KayobeAnsibleMixin, "run_kayobe_playbooks") def test_baremetal_compute_update_deployment_image(self, mock_run): diff --git a/releasenotes/notes/add-ironic-node-serial-console-commands-75f1255d62e05c87.yaml b/releasenotes/notes/add-ironic-node-serial-console-commands-75f1255d62e05c87.yaml new file mode 100644 index 000000000..cddb07d1c --- /dev/null +++ b/releasenotes/notes/add-ironic-node-serial-console-commands-75f1255d62e05c87.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added commands to enable and disable the Ironic serial console. + This allows you to use the serial console from within Horizon. diff --git a/setup.cfg b/setup.cfg index d9a4b670e..a0b4773cb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,6 +33,8 @@ kayobe.cli= baremetal_compute_provide = kayobe.cli.commands:BaremetalComputeProvide baremetal_compute_rename = kayobe.cli.commands:BaremetalComputeRename baremetal_compute_update_deployment_image = kayobe.cli.commands:BaremetalComputeUpdateDeploymentImage + baremetal_compute_serial_console_enable = kayobe.cli.commands:BaremetalComputeSerialConsoleEnable + baremetal_compute_serial_console_disable = kayobe.cli.commands:BaremetalComputeSerialConsoleDisable control_host_bootstrap = kayobe.cli.commands:ControlHostBootstrap control_host_upgrade = kayobe.cli.commands:ControlHostUpgrade configuration_dump = kayobe.cli.commands:ConfigurationDump