diff --git a/ansible/action_plugins/tenks_schedule.py b/ansible/action_plugins/tenks_schedule.py index f800b02..275f8ba 100644 --- a/ansible/action_plugins/tenks_schedule.py +++ b/ansible/action_plugins/tenks_schedule.py @@ -51,6 +51,8 @@ class ActionModule(ActionBase): for typ, cnt in six.iteritems(task_vars['specs']): for _ in six.moves.range(cnt): node = deepcopy(task_vars['node_types'][typ]) + # Set the type, for future reference. + node['type'] = typ # Sequentially number the node and volume names. node['name'] = "%s%d" % (task_vars['node_name_prefix'], idx) for vol_idx, vol in enumerate(node['volumes']): diff --git a/ansible/bmc.yml b/ansible/bmc.yml new file mode 100644 index 0000000..9ce3563 --- /dev/null +++ b/ansible/bmc.yml @@ -0,0 +1,38 @@ +--- +- hosts: localhost + tasks: + - name: Load allocations from file + include_vars: + file: "{{ allocations_file_path }}" + name: allocations + +- hosts: libvirt + vars: + nodes: >- + {{ hostvars.localhost.allocations[inventory_hostname] + | default([]) }} + tasks: + - name: Check that enough ports are available for Virtual BMC + fail: + msg: > + {{ nodes | count }} nodes were specified to be added to Virtual BMC, + but only {{ ipmi_port_range_end - ipmi_port_range_start }} ports are + available for use by Virtual BMC. + when: >- + (nodes | count) > (ipmi_port_range_end - ipmi_port_range_start) + + - name: Register domains with Virtual BMC + include_role: + name: virtualbmc-domain + vars: + vbmc_domain: "{{ domain }}" + vbmc_ipmi_address: "{{ ipmi_address }}" + vbmc_ipmi_username: "{{ ipmi_username }}" + vbmc_ipmi_password: "{{ ipmi_password }}" + vbmc_ipmi_port: "{{ ipmi_port_range_start + port_offset }}" + vbmc_virtualenv_path: "{{ virtualenv_path }}" + vbmc_log_directory: "{{ log_directory }}" + loop: "{{ nodes | map(attribute='name') | sort | list }}" + loop_control: + loop_var: domain + index_var: port_offset diff --git a/ansible/create_nodes.yml b/ansible/create_nodes.yml new file mode 100644 index 0000000..8cd22c0 --- /dev/null +++ b/ansible/create_nodes.yml @@ -0,0 +1,24 @@ +--- +- hosts: localhost + tasks: + - name: Load allocations from file + include_vars: + file: "{{ allocations_file_path }}" + name: allocations + +- hosts: libvirt + vars: + nodes: >- + {{ hostvars.localhost.allocations[inventory_hostname] + | default([]) }} + tasks: + - name: Create VM + include_role: + name: stackhpc.libvirt-vm + vars: + libvirt_vm_default_console_log_dir: "{{ log_directory }}" + # Configure VM definitions for the Libvirt provider. + libvirt_vms: >- + {{ nodes | map('set_libvirt_interfaces') + | map('set_libvirt_volume_pool') + | list }} diff --git a/ansible/deploy.yml b/ansible/deploy.yml index eade89b..0197bc3 100644 --- a/ansible/deploy.yml +++ b/ansible/deploy.yml @@ -1,86 +1,9 @@ --- -- hosts: hypervisors - tasks: - - include_tasks: host_setup.yml +- name: Perform deployment host configuration + import_playbook: deploy_hosts.yml -- hosts: libvirt - tasks: - - name: Configure host for Libvirt - include_role: - name: stackhpc.libvirt-host - vars: - libvirt_host_pools: - - name: "{{ libvirt_pool_name }}" - type: "{{ libvirt_pool_type }}" - capacity: "{{ libvirt_pool_capacity }}" - path: "{{ libvirt_pool_path }}" - mode: "{{ libvirt_pool_mode }}" - owner: "{{ libvirt_pool_owner }}" - group: "{{ libvirt_pool_group }}" - libvirt_host_require_vt: "{{ libvirt_require_vt }}" +- name: Perform deployment node configuration + import_playbook: deploy_nodes.yml - - name: Set up Virtual BMC daemon - include_role: - name: virtualbmc-daemon - vars: - vbmcd_virtualenv_path: "{{ virtualenv_path }}" - vbmcd_python_upper_contraints_url: "{{ python_upper_constraints_url }}" - -# Ensure we have facts about all hypervisors before scheduling begins. -- hosts: hypervisors - gather_facts: true - -- hosts: localhost - tasks: - - include_tasks: schedule.yml - - - name: Load allocations from file - include_vars: - file: "{{ allocations_file_path }}" - name: allocations - -- hosts: hypervisors - tasks: - - include_tasks: node_physical_network.yml - vars: - node_name: "{{ item.0.name }}" - physnet: "{{ item.1 }}" - # Loop over each physical network for each node allocated to this host. - # Allocations are stored in localhost's vars. - loop: >- - {{ hostvars['localhost'].allocations.result[inventory_hostname] - | default([]) | subelements('physical_networks') }} - -- hosts: libvirt - vars: - # Allocations are stored in the localhost's vars. - nodes: >- - {{ hostvars['localhost'].allocations.result[inventory_hostname] - | default([]) }} - tasks: - - name: Check that enough ports are available for Virtual BMC - fail: - msg: > - {{ nodes | count }} nodes were specified to be added to Virtual BMC, - but only {{ ipmi_port_range_end - ipmi_port_range_start }} ports are - available for use by Virtual BMC. - when: >- - (nodes | count) > (ipmi_port_range_end - ipmi_port_range_start) - - - name: Create Libvirt VMs - include_tasks: libvirt_create_vms.yml - vars: - libvirt_nodes: "{{ nodes }}" - - - name: Register domains with Virtual BMC - include_role: - name: virtualbmc-domain - vars: - vbmc_domain: "{{ domain }}" - vbmc_ipmi_port: "{{ ipmi_port_range_start + port_offset }}" - vbmc_virtualenv_path: "{{ virtualenv_path }}" - vbmc_log_directory: "{{ log_directory }}" - loop: "{{ nodes | map(attribute='name') | list }}" - loop_control: - loop_var: domain - index_var: port_offset +- name: Enrol nodes in Ironic + import_playbook: enrol_nodes.yml diff --git a/ansible/deploy_hosts.yml b/ansible/deploy_hosts.yml new file mode 100644 index 0000000..a8cbbdf --- /dev/null +++ b/ansible/deploy_hosts.yml @@ -0,0 +1,27 @@ +--- +- hosts: hypervisors + tasks: + - include_tasks: host_setup.yml + +- hosts: libvirt + tasks: + - name: Configure host for Libvirt + include_role: + name: stackhpc.libvirt-host + vars: + libvirt_host_pools: + - name: "{{ libvirt_pool_name }}" + type: "{{ libvirt_pool_type }}" + capacity: "{{ libvirt_pool_capacity }}" + path: "{{ libvirt_pool_path }}" + mode: "{{ libvirt_pool_mode }}" + owner: "{{ libvirt_pool_owner }}" + group: "{{ libvirt_pool_group }}" + libvirt_host_require_vt: "{{ libvirt_require_vt }}" + + - name: Set up Virtual BMC daemon + include_role: + name: virtualbmc-daemon + vars: + vbmcd_virtualenv_path: "{{ virtualenv_path }}" + vbmcd_python_upper_contraints_url: "{{ python_upper_constraints_url }}" diff --git a/ansible/deploy_nodes.yml b/ansible/deploy_nodes.yml new file mode 100644 index 0000000..594e126 --- /dev/null +++ b/ansible/deploy_nodes.yml @@ -0,0 +1,12 @@ +--- +- name: Schedule nodes + import_playbook: schedule.yml + +- name: Set up node networking + import_playbook: node_networking.yml + +- name: Create nodes + import_playbook: create_nodes.yml + +- name: Set up virtual node BMCs + import_playbook: bmc.yml diff --git a/ansible/enrol_nodes.yml b/ansible/enrol_nodes.yml new file mode 100644 index 0000000..8efc896 --- /dev/null +++ b/ansible/enrol_nodes.yml @@ -0,0 +1,28 @@ +- hosts: localhost + tasks: + - name: Load allocations from file + include_vars: + file: "{{ allocations_file_path }}" + name: allocations + + - name: Check that OpenStack credentials exist in the environment + fail: + msg: > + $OS_USERNAME was not found in the environment. Ensure the OpenStack + credentials exist in your environment, perhaps by sourcing your RC file. + when: not lookup('env', 'OS_USERNAME') + + - name: Perform Ironic enrolment for each hypervisor's nodes + include_role: + name: ironic-enrolment + vars: + ironic_deploy_kernel_uuid: "{{ deploy_kernel_uuid }}" + ironic_deploy_ramdisk_uuid: "{{ deploy_ramdisk_uuid }}" + ironic_nodes: "{{ alloc.value }}" + ironic_hypervisor: "{{ alloc.key }}" + ironic_virtualenv_path: "{{ virtualenv_path }}" + ironic_python_upper_constraints_url: >- + {{ python_upper_constraints_url }} + loop: "{{ query('dict', allocations) }}" + loop_control: + loop_var: alloc diff --git a/ansible/filter_plugins/libvirt_vm_config.py b/ansible/filter_plugins/libvirt_vm_config.py deleted file mode 100644 index 1ff90ea..0000000 --- a/ansible/filter_plugins/libvirt_vm_config.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (c) 2018 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. - -from ansible.errors import AnsibleFilterError -from jinja2 import contextfilter - - -class FilterModule(object): - '''Libvirt configuration filters''' - - def filters(self): - return { - 'set_libvirt_interfaces': set_libvirt_interfaces, - 'set_libvirt_volume_pool': set_libvirt_volume_pool, - } - - -# Lifted from kayobe:ansible/filter_plugins/networks.py -def _get_hostvar(context, var_name, inventory_hostname=None): - if inventory_hostname is None: - namespace = context - else: - if inventory_hostname not in context['hostvars']: - raise AnsibleFilterError( - "Inventory hostname '%s' not in hostvars" % inventory_hostname) - namespace = context["hostvars"][inventory_hostname] - return namespace.get(var_name) - - -@contextfilter -def set_libvirt_interfaces(context, node): - """Set interfaces for a node's specified physical networks. - """ - physnet_mappings = _get_hostvar(context, 'physnet_mappings') - prefix = _get_hostvar(context, 'veth_prefix') - suffix = _get_hostvar(context, 'veth_node_source_suffix') - - node['interfaces'] = [] - # Libvirt doesn't need to know about physical networks, so pop them here. - for physnet in node.pop('physical_networks', []): - # Get the ID of this physical network on the hypervisor. - idx = sorted(physnet_mappings).index(physnet) - node['interfaces'].append( - {'type': 'direct', - # FIXME(w-miller): Don't duplicate the logic of this naming scheme - # from node_physical_network.yml - 'source': {'dev': prefix + node['name'] + '-' + str(idx) + - suffix}} - ) - return node - - -@contextfilter -def set_libvirt_volume_pool(context, node): - """Set the Libvirt volume pool for each volume. - """ - pool = _get_hostvar(context, 'libvirt_pool_name') - for vol in node.get('volumes', []): - vol['pool'] = pool - return node diff --git a/ansible/filter_plugins/tenks.py b/ansible/filter_plugins/tenks.py new file mode 100644 index 0000000..e284f22 --- /dev/null +++ b/ansible/filter_plugins/tenks.py @@ -0,0 +1,188 @@ +# Copyright (c) 2018 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. +import math +import re + +from ansible.errors import AnsibleFilterError +from ansible.module_utils._text import to_text +from jinja2 import contextfilter + + +class FilterModule(object): + '''Tenks filters + + NOTE(w-miller): The Libvirt filters need to use some of the network name + filters. Due to Ansible issue #27748, filter plugins cannot import any + custom Python modules, so we can't have a Libvirt filters file that imports + a network filters file; for the same reason, we can't have a shared utils + file either. This is why all Tenks filters are lumped together in this + file. + ''' + + def filters(self): + return { + # Network name filters. + 'bridge_name': bridge_name, + 'ovs_link_name': ovs_link_name, + 'source_link_name': source_link_name, + 'source_to_ovs_link_name': source_to_ovs_link_name, + 'source_link_to_physnet_name': source_link_to_physnet_name, + + # Libvirt filters. + 'set_libvirt_interfaces': set_libvirt_interfaces, + 'set_libvirt_volume_pool': set_libvirt_volume_pool, + + # Miscellaneous filters. + 'size_string_to_gb': size_string_to_gb, + } + + +# Lifted from kayobe:ansible/filter_plugins/networks.py +def _get_hostvar(context, var_name, inventory_hostname=None): + if inventory_hostname is None: + namespace = context + else: + if inventory_hostname not in context['hostvars']: + raise AnsibleFilterError( + "Inventory hostname '%s' not in hostvars" % inventory_hostname) + namespace = context['hostvars'][inventory_hostname] + return namespace.get(var_name) + + +@contextfilter +def set_libvirt_interfaces(context, node): + """Set interfaces for a node's specified physical networks. + """ + node['interfaces'] = [] + for physnet in node.get('physical_networks', []): + node['interfaces'].append( + {'type': 'direct', + 'source': {'dev': source_link_name(context, node, physnet)}} + ) + return node + + +@contextfilter +def set_libvirt_volume_pool(context, node): + """Set the Libvirt volume pool for each volume. + """ + pool = _get_hostvar(context, 'libvirt_pool_name') + for vol in node.get('volumes', []): + vol['pool'] = pool + return node + + +@contextfilter +def bridge_name(context, physnet): + """Get the Tenks OVS bridge name from a physical network name. + """ + return (_get_hostvar(context, 'bridge_prefix') + + str(_physnet_name_to_index(context, physnet))) + + +@contextfilter +def source_link_name(context, node, physnet): + """Get the source veth link name for a node/physnet combination. + """ + return (_link_name(context, node, physnet) + + _get_hostvar(context, 'veth_node_source_suffix')) + + +@contextfilter +def ovs_link_name(context, node, physnet): + """Get the OVS veth link name for a node/physnet combination. + """ + return (_link_name(context, node, physnet) + + _get_hostvar(context, 'veth_node_ovs_suffix')) + + +@contextfilter +def source_to_ovs_link_name(context, source): + """Get the corresponding OVS link name for a source link name. + """ + base = source[:-len(_get_hostvar(context, 'veth_node_source_suffix'))] + return base + _get_hostvar(context, 'veth_node_ovs_suffix') + + +@contextfilter +def source_link_to_physnet_name(context, source): + """ Get the physical network name that a source veth link is connected to. + """ + prefix = _get_hostvar(context, 'veth_prefix') + suffix = _get_hostvar(context, 'veth_node_source_suffix') + match = re.compile(r"%s.*-(\d+)%s" + % (re.escape(prefix), re.escape(suffix))).match(source) + idx = match.group(1) + return _physnet_index_to_name(context, int(idx)) + + +def size_string_to_gb(size): + """ + Parse a size string, and convert to the integer number of GB it represents. + """ + return int(math.ceil(_parse_size_string(size) / 10**9)) + + +def _parse_size_string(size): + """ + Parse a capacity string. + + Takes a string representing a capacity and returns the size in bytes, as an + integer. Accepts strings such as "5", "5B", "5g", "5GB", " 5 GiB ", etc. + Case insensitive. See `man virsh` for more details. + + :param size: The size string to parse. + :returns: The number of bytes represented by `size`, as an integer. + """ + # Base values for units. + BIN = 1024 + DEC = 1000 + POWERS = {"": 0, "k": 1, "m": 2, "g": 3, "t": 4} + # If an integer is passed, treat it as a string without units. + size = str(size).lower() + regex = r"\s*(\d+)\s*([%s])?(i?b)?\s*$" % "".join(POWERS.keys()) + match = re.compile(regex).match(size) + if not match: + msg = "The size string '%s' is not of a valid format." % size + raise AnsibleFilterError(to_text(msg)) + number = match.group(1) + power = match.group(2) + unit = match.group(3) + if not power: + power = "" + if unit == "b": + base = DEC + else: + base = BIN + return int(number) * (base ** POWERS[power]) + + +def _link_name(context, node, physnet): + prefix = _get_hostvar(context, 'veth_prefix') + return prefix + node['name'] + '-' + str(_physnet_name_to_index(context, + physnet)) + + +def _physnet_name_to_index(context, physnet): + """Get the ID of this physical network on the hypervisor. + """ + physnet_mappings = _get_hostvar(context, 'physnet_mappings') + return sorted(physnet_mappings).index(physnet) + + +def _physnet_index_to_name(context, idx): + """Get the name of this physical network on the hypervisor. + """ + physnet_mappings = _get_hostvar(context, 'physnet_mappings') + return sorted(physnet_mappings)[idx] diff --git a/ansible/group_vars/hypervisors b/ansible/group_vars/hypervisors index 5cc26a7..4b7eb96 100644 --- a/ansible/group_vars/hypervisors +++ b/ansible/group_vars/hypervisors @@ -36,3 +36,13 @@ veth_node_source_suffix: '-tap' # Directory in which to store Tenks logs. log_directory: /var/log/tenks/ + +# The address on which VBMC will listen for node IPMI communication. +ipmi_address: 0.0.0.0 +# The range of ports available for use for node IPMI communication. +ipmi_port_range_start: 6230 +ipmi_port_range_end: 6240 +# The username to use for node IPMI communication. +ipmi_username: username +# The password to use for node IPMI communication. +ipmi_password: password diff --git a/ansible/group_vars/libvirt b/ansible/group_vars/libvirt index 0e797d2..694a712 100644 --- a/ansible/group_vars/libvirt +++ b/ansible/group_vars/libvirt @@ -11,7 +11,3 @@ libvirt_pool_group: "{{ ansible_user_id }}" # By default, allow QEMU without hardware virtualisation since this is a # development tool. libvirt_require_vt: false - -# The range of ports available for use for node IPMI traffic. -ipmi_port_range_start: 6230 -ipmi_port_range_end: 6240 diff --git a/ansible/host_vars/localhost b/ansible/host_vars/localhost index 1345b90..a590603 100644 --- a/ansible/host_vars/localhost +++ b/ansible/host_vars/localhost @@ -32,3 +32,8 @@ node_types: {} # # 'type0'. # type0: 4 specs: {} + +# The Glance UUID of the image to use for the deployment kernel. +deploy_kernel_uuid: +# The Glance UUID of the image to use for the deployment ramdisk. +deploy_ramdisk_uuid: diff --git a/ansible/libvirt_create_vms.yml b/ansible/libvirt_create_vms.yml deleted file mode 100644 index 95cf164..0000000 --- a/ansible/libvirt_create_vms.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -- name: Create VM - include_role: - name: stackhpc.libvirt-vm - vars: - libvirt_vm_default_console_log_dir: "{{ log_directory }}" - # Configure VM definitions for the Libvirt provider. - libvirt_vms: >- - {{ libvirt_nodes | map('set_libvirt_interfaces') - | map('set_libvirt_volume_pool') - | list }} diff --git a/ansible/node_networking.yml b/ansible/node_networking.yml new file mode 100644 index 0000000..683ca5a --- /dev/null +++ b/ansible/node_networking.yml @@ -0,0 +1,24 @@ +- hosts: localhost + tasks: + - name: Load allocations from file + include_vars: + file: "{{ allocations_file_path }}" + name: allocations + +- hosts: hypervisors + vars: + nodes: >- + {{ hostvars.localhost.allocations[inventory_hostname] + | default([]) }} + tasks: + - name: Set up veth pairs for each node + include_role: + name: veth-pair + vars: + veth_pair_ovs_bridge: "{{ item.1 | bridge_name }}" + veth_pair_ovs_link_name: "{{ item.0 | ovs_link_name(item.1) }}" + veth_pair_source_link_name: "{{ item.0 | source_link_name(item.1) }}" + # Loop over each physical network for each node allocated to this host. + # Allocations are stored in localhost's vars. + loop: >- + {{ nodes | subelements('physical_networks') }} diff --git a/ansible/node_physical_network.yml b/ansible/node_physical_network.yml deleted file mode 100644 index 267ac6d..0000000 --- a/ansible/node_physical_network.yml +++ /dev/null @@ -1,28 +0,0 @@ ---- -- name: Gather details for node physical network connection - block: - - name: Get the physical network index - set_fact: - # The index of the physical network within this hypervisor's physical - # networks. - idx: >- - {{ (physnet_mappings | dictsort | list).index( - (physnet, physnet_mappings[physnet])) }} - - - name: Set node veth base name - set_fact: - # Veth pairs are unique for any node-physnet combination. However, - # device names cannot be longer than 15 characters, so use physical - # networks' indices instead. - veth_base_name: >- - {{ veth_prefix + node_name + '-' + idx }} - -- name: Set up veth pairs for the node - include_role: - name: veth-pair - vars: - veth_pair_ovs_bridge: >- - {{ bridge_prefix ~ idx }} - veth_pair_ovs_link_name: "{{ veth_base_name + veth_node_ovs_suffix }}" - veth_pair_source_link_name: >- - {{ veth_base_name + veth_node_source_suffix }} diff --git a/ansible/roles/ironic-enrolment/README.md b/ansible/roles/ironic-enrolment/README.md new file mode 100644 index 0000000..dacf458 --- /dev/null +++ b/ansible/roles/ironic-enrolment/README.md @@ -0,0 +1,30 @@ +Ironic Enrolment +================ + +This role enrols nodes with OpenStack Ironic, creates Ironic ports for each of +the nodes' NICs, and sets relevant attributes on created resources. + +Requirements +------------ + +- *OS_\** environment variables for the OpenStack cloud in question present in + the shell environment. These can be sourced from an OpenStack RC file, for + example. + +- The `virsh` command-line tool present at `/bin/virsh`. + +Role Variables +-------------- + +- `ironic_nodes`: A list of dicts of details for nodes that are to be enroled + in Ironic. +- `ironic_hypervisor`: The hostname of the hypervisor on which `ironic_nodes` + exist. +- `ironic_deploy_kernel_uuid`: The Glance UUID of the image to use for the + deployment kernel. +- `ironic_deploy_ramdisk_uuid`: The Glance UUID of the image to use for the + deployment ramdisk. +- `ironic_virtualenv_path`: The path to the virtualenv in which to install the + OpenStack clients. +- `ironic_python_upper_constraints_url`: The URL of the upper constraints file + to pass to pip when installing Python packages. diff --git a/ansible/roles/ironic-enrolment/defaults/main.yml b/ansible/roles/ironic-enrolment/defaults/main.yml new file mode 100644 index 0000000..813f185 --- /dev/null +++ b/ansible/roles/ironic-enrolment/defaults/main.yml @@ -0,0 +1,14 @@ +--- +# A list of dicts of details for nodes that are to be enroled in Ironic. +ironic_nodes: [] +# The hostname of the hypervisor where these nodes exist. +ironic_hypervisor: +# The Glance UUID of the image to use for the deployment kernel. +ironic_deploy_kernel_uuid: +# The Glance UUID of the image to use for the deployment ramdisk. +ironic_deploy_ramdisk_uuid: +# The path to the virtualenv in which to install the OpenStack clients. +ironic_virtualenv_path: +# The URL of the upper constraints file to pass to pip when installing Python +# packages. +ironic_python_upper_constraints_url: diff --git a/ansible/roles/ironic-enrolment/files/requirements.txt b/ansible/roles/ironic-enrolment/files/requirements.txt new file mode 100644 index 0000000..a95f44c --- /dev/null +++ b/ansible/roles/ironic-enrolment/files/requirements.txt @@ -0,0 +1,6 @@ +# This file contains the Python packages that are needed in the Tenks virtual +# env. + +openstacksdk>=0.17.2 # Apache +python-ironicclient>=2.5.0 # Apache +python-openstackclient>=3.16.0 # Apache diff --git a/ansible/roles/ironic-enrolment/tasks/main.yml b/ansible/roles/ironic-enrolment/tasks/main.yml new file mode 100644 index 0000000..8196d24 --- /dev/null +++ b/ansible/roles/ironic-enrolment/tasks/main.yml @@ -0,0 +1,18 @@ +--- +- name: Ensure Python requirements are installed + pip: + requirements: "{{ '/'.join([role_path, 'files', 'requirements.txt']) }}" + extra_args: >- + -c {{ ironic_python_upper_constraints_url }} + virtualenv: "{{ ironic_virtualenv_path }}" + +- name: Enrol the Ironic nodes + include_tasks: node.yml + vars: + node: "{{ ironic_node }}" + ipmi_port: >- + {{ hostvars[ironic_hypervisor].ipmi_port_range_start + port_offset }} + loop: "{{ ironic_nodes | sort(attribute='name') }}" + loop_control: + loop_var: ironic_node + index_var: port_offset diff --git a/ansible/roles/ironic-enrolment/tasks/node.yml b/ansible/roles/ironic-enrolment/tasks/node.yml new file mode 100644 index 0000000..77a6ce1 --- /dev/null +++ b/ansible/roles/ironic-enrolment/tasks/node.yml @@ -0,0 +1,90 @@ +--- +- name: Get vNIC MAC addresses + # The output format of this command gives two lines of header, followed by + # (for each vNIC): + # + # The VMs will have been created with the virt module, using become: true. + # This targets /bin/virsh rather than /usr/bin/virsh. + command: /bin/virsh domiflist '{{ node.name }}' + register: iflist_res + changed_when: false + become: true + delegate_to: "{{ ironic_hypervisor }}" + run_once: true + +# We need to do this for each run to ensure other nodes' NICs don't carry over +# to this run. +- name: Reset list of NICs + set_fact: + nics: [] + +- name: Collect MAC addresses into NIC list + set_fact: + nics: "{{ nics | union([{'mac': item.split()[4]}]) }}" + loop: "{{ iflist_res.stdout_lines[2:] }}" + +- name: Create node in Ironic + os_ironic: + auth_type: password + driver: ipmi + driver_info: + power: + ipmi_address: "{{ hostvars[ironic_hypervisor].ipmi_address }}" + # This is passed in from main.yml. + ipmi_port: "{{ ipmi_port }}" + ipmi_username: "{{ hostvars[ironic_hypervisor].ipmi_username }}" + ipmi_password: "{{ hostvars[ironic_hypervisor].ipmi_password }}" + deploy: + deploy_kernel: "{{ ironic_deploy_kernel_uuid | default(omit, true) }}" + deploy_ramdisk: "{{ ironic_deploy_ramdisk_uuid | default(omit, true) }}" + name: "{{ node.name }}" + nics: "{{ nics }}" + properties: + ram: "{{ node.memory_mb }}" + # FIXME(w-miller): Instead of assuming the first volume is the primary + # volume, make this configurable? + disk_size: >- + {{ (node.volumes.0.capacity | default('1')) | size_string_to_gb }} + cpus: "{{ node.vcpus }}" + vars: + # This module requires the openstacksdk package, which is installed within + # our virtualenv. + ansible_python_interpreter: >- + {{ '/'.join([ironic_virtualenv_path, 'bin', 'python']) }} + register: created_node + +# The os_ironic module automatically brings the node from 'enrol' to +# 'available' state, but we still need to set more port and node attributes. +# Use maintenance mode to do this. +- name: Put Ironic node into maintenance mode + command: >- + '{{ ironic_virtualenv_path }}/bin/openstack' baremetal node maintenance set + '{{ created_node.uuid }}' + +# FIXME(w-miller): Make interfaces/driver configurable, for example to allow +# use of Redfish instead of IPMI. +- name: Set Ironic node resource class + command: >- + '{{ ironic_virtualenv_path }}/bin/openstack' baremetal node set + '{{ created_node.uuid }}' + --resource-class {{ node.type }} +# --boot-interface pxe +# --deploy-interface iscsi +# --management-interface ipmitool +# --network-interface neutron +# --power-interface ipmitool + +- name: Set additional Ironic port attributes + include_tasks: port.yml + vars: + source_interface: "{{ vnic.split()[2] }}" + mac: "{{ vnic.split()[4] }}" + # Loop over each NIC. + loop: "{{ iflist_res.stdout_lines[2:] }}" + loop_control: + loop_var: vnic + +- name: Bring Ironic node out of maintenance mode + command: >- + '{{ ironic_virtualenv_path }}/bin/openstack' baremetal node maintenance + unset '{{ created_node.uuid }}' diff --git a/ansible/roles/ironic-enrolment/tasks/port.yml b/ansible/roles/ironic-enrolment/tasks/port.yml new file mode 100644 index 0000000..b6d036d --- /dev/null +++ b/ansible/roles/ironic-enrolment/tasks/port.yml @@ -0,0 +1,29 @@ +--- +- name: Get Ironic port UUID + command: >- + '{{ ironic_virtualenv_path }}/bin/openstack' baremetal port list + --format value + --column UUID + --address {{ mac }} + register: uuid + changed_when: false + +- name: Get physical network name + set_fact: + physnet: "{{ source_interface | source_link_to_physnet_name }}" + +- name: Get bridge name + set_fact: + bridge: "{{ physnet | bridge_name }}" + +- name: Set Ironic port attributes + command: >- + '{{ ironic_virtualenv_path }}/bin/openstack' baremetal port set + {{ uuid.stdout }} + --physical-network '{{ physnet }}' + --local-link-connection switch_id='{{ hostvars[ironic_hypervisor][ + 'ansible_' + bridge + ].macaddress }}' + --local-link-connection switch_info='{{ bridge }}' + --local-link-connection port_id='{{ source_interface + | source_to_ovs_link_name }}' diff --git a/ansible/roles/virtualbmc-domain/README.md b/ansible/roles/virtualbmc-domain/README.md index 9ce0236..06f4d2f 100644 --- a/ansible/roles/virtualbmc-domain/README.md +++ b/ansible/roles/virtualbmc-domain/README.md @@ -15,13 +15,10 @@ Role Variables - `vbmc_domain`: The name of the Libvirt domain to be added to Virtual BMC. - `vbmc_virtualenv_path`: The path to the virtualenv in which Virtual BMC is installed. -- `vbmc_ipmi_listen_address`: The address on which Virtual BMC will listen for - IPMI traffic. Default is 0.0.0.0. +- `vbmc_ipmi_address`: The address on which Virtual BMC will listen for IPMI + traffic. - `vbmc_ipmi_port`: The port on which Virtual BMC will listen for IPMI traffic. - Default is 6230. -- `vbmc_ipmi_username`: The IPMI username that Virtual BMC will use. Default is - 'username'. -- `vbmc_ipmi_password`: The IPMI password that Virtual BMC will use. Default is - 'password'. +- `vbmc_ipmi_username`: The IPMI username that Virtual BMC will use. +- `vbmc_ipmi_password`: The IPMI password that Virtual BMC will use. - `vbmc_log_directory`: The directory in which to store Virtual BMC logs. If - `None`, output will not be logged to a file. Default is `None`. + not overridden from `None`, output will not be logged to a file. diff --git a/ansible/roles/virtualbmc-domain/defaults/main.yml b/ansible/roles/virtualbmc-domain/defaults/main.yml index 25f4416..c847c2b 100644 --- a/ansible/roles/virtualbmc-domain/defaults/main.yml +++ b/ansible/roles/virtualbmc-domain/defaults/main.yml @@ -1,13 +1,12 @@ --- # The address on which VBMC will listen for IPMI traffic for this domain. -vbmc_ipmi_listen_address: 0.0.0.0 +vbmc_ipmi_address: # The port on which VBMC will listen for IPMI traffic for this domain. -vbmc_ipmi_port: 6230 +vbmc_ipmi_port: # The IPMI username that VBMC will use. -vbmc_ipmi_username: username +vbmc_ipmi_username: # The IPMI password that VBMC will use. -vbmc_ipmi_password: password - +vbmc_ipmi_password: # The name of the Libvirt domain to be added to Virtual BMC. vbmc_domain: # The directory in which to store VBMC logs. diff --git a/ansible/roles/virtualbmc-domain/tasks/main.yml b/ansible/roles/virtualbmc-domain/tasks/main.yml index 4873ea7..f60d8b1 100644 --- a/ansible/roles/virtualbmc-domain/tasks/main.yml +++ b/ansible/roles/virtualbmc-domain/tasks/main.yml @@ -30,19 +30,24 @@ # Check here to be safe. - name: Wait to ensure socket is closed wait_for: - host: "{{ vbmc_ipmi_listen_address }}" + host: "{{ vbmc_ipmi_address }}" port: "{{ vbmc_ipmi_port }}" state: stopped timeout: 15 +# These tasks will trigger ansible lint rule ANSIBLE0012 because they are not +# idempotent (we always delete and recreate the domain). Use a tag to suppress +# the checks. - name: Ensure domain is added to VBMC command: >- {{ cmd }} add '{{ domain }}' --port {{ vbmc_ipmi_port }} --username '{{ vbmc_ipmi_username }}' --password '{{ vbmc_ipmi_password }}' - --address {{ vbmc_ipmi_listen_address }} + --address {{ vbmc_ipmi_address }} become: true + tags: + - skip_ansible_lint - name: Ensure domain is started in VBMC command: > @@ -52,3 +57,5 @@ # few commands. until: res is succeeded become: true + tags: + - skip_ansible_lint diff --git a/ansible/schedule.yml b/ansible/schedule.yml index 7097474..a80647f 100644 --- a/ansible/schedule.yml +++ b/ansible/schedule.yml @@ -1,28 +1,34 @@ --- -- name: Check that all specified node types exist - fail: - msg: The non-existent node type {{ item }} was specified in 'specs'. - when: item not in node_types - loop: "{{ specs.keys() }}" +# Ensure we have facts about all hypervisors before scheduling begins. +- hosts: hypervisors + gather_facts: true -# Creates a dict mapping each hypervisor's hostname to its hostvars, to be used -# during scheduling. -- name: Collect hypervisor hostvars - set_fact: - hypervisor_vars: >- - {{ hypervisor_vars | default({}) | combine({item: hostvars[item]}) }} - loop: "{{ groups['hypervisors'] }}" +- hosts: localhost + tasks: + - name: Check that all specified node types exist + fail: + msg: The non-existent node type {{ item }} was specified in 'specs'. + when: item not in node_types + loop: "{{ specs.keys() }}" -- name: Schedule nodes to hypervisors - tenks_schedule: - hypervisor_vars: "{{ hypervisor_vars }}" - node_types: "{{ node_types }}" - specs: "{{ specs }}" - register: allocations + # Creates a dict mapping each hypervisor's hostname to its hostvars, to be + # used during scheduling. + - name: Collect hypervisor hostvars + set_fact: + hypervisor_vars: >- + {{ hypervisor_vars | default({}) | combine({item: hostvars[item]}) }} + loop: "{{ groups['hypervisors'] }}" -- name: Write node allocations to file - copy: - # tenks_schedule lookup plugin outputs a dict. Pretty-print this to persist - # it in a YAML file. - content: "{{ allocations.result | to_nice_yaml }}" - dest: "{{ allocations_file_path }}" + - name: Schedule nodes to hypervisors + tenks_schedule: + hypervisor_vars: "{{ hypervisor_vars }}" + node_types: "{{ node_types }}" + specs: "{{ specs }}" + register: scheduling + + - name: Write node allocations to file + copy: + # tenks_schedule lookup plugin outputs a dict. Pretty-print this to + # persist it in a YAML file. + content: "{{ scheduling.result | to_nice_yaml }}" + dest: "{{ allocations_file_path }}"