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/deploy.yml b/ansible/deploy.yml index 22905c8..eb71784 100644 --- a/ansible/deploy.yml +++ b/ansible/deploy.yml @@ -89,3 +89,27 @@ loop_control: loop_var: domain index_var: port_offset + +- hosts: localhost + tasks: + - 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.1 }}" + ironic_hypervisor: "{{ alloc.0 }}" + ironic_virtualenv_path: "{{ virtualenv_path }}" + ironic_python_upper_constraints_url: >- + {{ python_upper_constraints_url }} + loop: "{{ allocations.result.iteritems() | list }}" + loop_control: + loop_var: alloc diff --git a/ansible/filter_plugins/tenks.py b/ansible/filter_plugins/tenks.py index 018bf84..ce19c33 100644 --- a/ansible/filter_plugins/tenks.py +++ b/ansible/filter_plugins/tenks.py @@ -111,7 +111,7 @@ def ovs_link_name(context, node, physnet): 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'))] + base = source[:-len(_get_hostvar(context, 'veth_node_source_suffix'))] return base + _get_hostvar(context, 'veth_node_ovs_suffix') 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/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 }}'