commit
5707fd3309
@ -51,6 +51,8 @@ class ActionModule(ActionBase):
|
|||||||
for typ, cnt in six.iteritems(task_vars['specs']):
|
for typ, cnt in six.iteritems(task_vars['specs']):
|
||||||
for _ in six.moves.range(cnt):
|
for _ in six.moves.range(cnt):
|
||||||
node = deepcopy(task_vars['node_types'][typ])
|
node = deepcopy(task_vars['node_types'][typ])
|
||||||
|
# Set the type, for future reference.
|
||||||
|
node['type'] = typ
|
||||||
# Sequentially number the node and volume names.
|
# Sequentially number the node and volume names.
|
||||||
node['name'] = "%s%d" % (task_vars['node_name_prefix'], idx)
|
node['name'] = "%s%d" % (task_vars['node_name_prefix'], idx)
|
||||||
for vol_idx, vol in enumerate(node['volumes']):
|
for vol_idx, vol in enumerate(node['volumes']):
|
||||||
|
38
ansible/bmc.yml
Normal file
38
ansible/bmc.yml
Normal file
@ -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
|
24
ansible/create_nodes.yml
Normal file
24
ansible/create_nodes.yml
Normal file
@ -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 }}
|
@ -1,86 +1,9 @@
|
|||||||
---
|
---
|
||||||
- hosts: hypervisors
|
- name: Perform deployment host configuration
|
||||||
tasks:
|
import_playbook: deploy_hosts.yml
|
||||||
- include_tasks: host_setup.yml
|
|
||||||
|
|
||||||
- hosts: libvirt
|
- name: Perform deployment node configuration
|
||||||
tasks:
|
import_playbook: deploy_nodes.yml
|
||||||
- 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
|
- name: Enrol nodes in Ironic
|
||||||
include_role:
|
import_playbook: enrol_nodes.yml
|
||||||
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
|
|
||||||
|
27
ansible/deploy_hosts.yml
Normal file
27
ansible/deploy_hosts.yml
Normal file
@ -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 }}"
|
12
ansible/deploy_nodes.yml
Normal file
12
ansible/deploy_nodes.yml
Normal file
@ -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
|
28
ansible/enrol_nodes.yml
Normal file
28
ansible/enrol_nodes.yml
Normal file
@ -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
|
@ -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
|
|
188
ansible/filter_plugins/tenks.py
Normal file
188
ansible/filter_plugins/tenks.py
Normal file
@ -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]
|
@ -36,3 +36,13 @@ veth_node_source_suffix: '-tap'
|
|||||||
|
|
||||||
# Directory in which to store Tenks logs.
|
# Directory in which to store Tenks logs.
|
||||||
log_directory: /var/log/tenks/
|
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
|
||||||
|
@ -11,7 +11,3 @@ libvirt_pool_group: "{{ ansible_user_id }}"
|
|||||||
# By default, allow QEMU without hardware virtualisation since this is a
|
# By default, allow QEMU without hardware virtualisation since this is a
|
||||||
# development tool.
|
# development tool.
|
||||||
libvirt_require_vt: false
|
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
|
|
||||||
|
@ -32,3 +32,8 @@ node_types: {}
|
|||||||
# # 'type0'.
|
# # 'type0'.
|
||||||
# type0: 4
|
# type0: 4
|
||||||
specs: {}
|
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:
|
||||||
|
@ -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 }}
|
|
24
ansible/node_networking.yml
Normal file
24
ansible/node_networking.yml
Normal file
@ -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') }}
|
@ -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 }}
|
|
30
ansible/roles/ironic-enrolment/README.md
Normal file
30
ansible/roles/ironic-enrolment/README.md
Normal file
@ -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.
|
14
ansible/roles/ironic-enrolment/defaults/main.yml
Normal file
14
ansible/roles/ironic-enrolment/defaults/main.yml
Normal file
@ -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:
|
6
ansible/roles/ironic-enrolment/files/requirements.txt
Normal file
6
ansible/roles/ironic-enrolment/files/requirements.txt
Normal file
@ -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
|
18
ansible/roles/ironic-enrolment/tasks/main.yml
Normal file
18
ansible/roles/ironic-enrolment/tasks/main.yml
Normal file
@ -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
|
90
ansible/roles/ironic-enrolment/tasks/node.yml
Normal file
90
ansible/roles/ironic-enrolment/tasks/node.yml
Normal file
@ -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):
|
||||||
|
# <name> <type> <source interface> <model> <MAC>
|
||||||
|
# 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 }}'
|
29
ansible/roles/ironic-enrolment/tasks/port.yml
Normal file
29
ansible/roles/ironic-enrolment/tasks/port.yml
Normal file
@ -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 }}'
|
@ -15,13 +15,10 @@ Role Variables
|
|||||||
- `vbmc_domain`: The name of the Libvirt domain to be added to Virtual BMC.
|
- `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
|
- `vbmc_virtualenv_path`: The path to the virtualenv in which Virtual BMC is
|
||||||
installed.
|
installed.
|
||||||
- `vbmc_ipmi_listen_address`: The address on which Virtual BMC will listen for
|
- `vbmc_ipmi_address`: The address on which Virtual BMC will listen for IPMI
|
||||||
IPMI traffic. Default is 0.0.0.0.
|
traffic.
|
||||||
- `vbmc_ipmi_port`: The port 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.
|
||||||
- `vbmc_ipmi_username`: The IPMI username that Virtual BMC will use. Default is
|
- `vbmc_ipmi_password`: The IPMI password that Virtual BMC will use.
|
||||||
'username'.
|
|
||||||
- `vbmc_ipmi_password`: The IPMI password that Virtual BMC will use. Default is
|
|
||||||
'password'.
|
|
||||||
- `vbmc_log_directory`: The directory in which to store Virtual BMC logs. If
|
- `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.
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
---
|
---
|
||||||
# The address on which VBMC will listen for IPMI traffic for this domain.
|
# 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.
|
# 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.
|
# The IPMI username that VBMC will use.
|
||||||
vbmc_ipmi_username: username
|
vbmc_ipmi_username:
|
||||||
# The IPMI password that VBMC will use.
|
# 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.
|
# The name of the Libvirt domain to be added to Virtual BMC.
|
||||||
vbmc_domain:
|
vbmc_domain:
|
||||||
# The directory in which to store VBMC logs.
|
# The directory in which to store VBMC logs.
|
||||||
|
@ -30,19 +30,24 @@
|
|||||||
# Check here to be safe.
|
# Check here to be safe.
|
||||||
- name: Wait to ensure socket is closed
|
- name: Wait to ensure socket is closed
|
||||||
wait_for:
|
wait_for:
|
||||||
host: "{{ vbmc_ipmi_listen_address }}"
|
host: "{{ vbmc_ipmi_address }}"
|
||||||
port: "{{ vbmc_ipmi_port }}"
|
port: "{{ vbmc_ipmi_port }}"
|
||||||
state: stopped
|
state: stopped
|
||||||
timeout: 15
|
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
|
- name: Ensure domain is added to VBMC
|
||||||
command: >-
|
command: >-
|
||||||
{{ cmd }} add '{{ domain }}'
|
{{ cmd }} add '{{ domain }}'
|
||||||
--port {{ vbmc_ipmi_port }}
|
--port {{ vbmc_ipmi_port }}
|
||||||
--username '{{ vbmc_ipmi_username }}'
|
--username '{{ vbmc_ipmi_username }}'
|
||||||
--password '{{ vbmc_ipmi_password }}'
|
--password '{{ vbmc_ipmi_password }}'
|
||||||
--address {{ vbmc_ipmi_listen_address }}
|
--address {{ vbmc_ipmi_address }}
|
||||||
become: true
|
become: true
|
||||||
|
tags:
|
||||||
|
- skip_ansible_lint
|
||||||
|
|
||||||
- name: Ensure domain is started in VBMC
|
- name: Ensure domain is started in VBMC
|
||||||
command: >
|
command: >
|
||||||
@ -52,3 +57,5 @@
|
|||||||
# few commands.
|
# few commands.
|
||||||
until: res is succeeded
|
until: res is succeeded
|
||||||
become: true
|
become: true
|
||||||
|
tags:
|
||||||
|
- skip_ansible_lint
|
||||||
|
@ -1,28 +1,34 @@
|
|||||||
---
|
---
|
||||||
- name: Check that all specified node types exist
|
# Ensure we have facts about all hypervisors before scheduling begins.
|
||||||
fail:
|
- hosts: hypervisors
|
||||||
msg: The non-existent node type {{ item }} was specified in 'specs'.
|
gather_facts: true
|
||||||
when: item not in node_types
|
|
||||||
loop: "{{ specs.keys() }}"
|
|
||||||
|
|
||||||
# Creates a dict mapping each hypervisor's hostname to its hostvars, to be used
|
- hosts: localhost
|
||||||
# during scheduling.
|
tasks:
|
||||||
- name: Collect hypervisor hostvars
|
- name: Check that all specified node types exist
|
||||||
set_fact:
|
fail:
|
||||||
hypervisor_vars: >-
|
msg: The non-existent node type {{ item }} was specified in 'specs'.
|
||||||
{{ hypervisor_vars | default({}) | combine({item: hostvars[item]}) }}
|
when: item not in node_types
|
||||||
loop: "{{ groups['hypervisors'] }}"
|
loop: "{{ specs.keys() }}"
|
||||||
|
|
||||||
- name: Schedule nodes to hypervisors
|
# Creates a dict mapping each hypervisor's hostname to its hostvars, to be
|
||||||
tenks_schedule:
|
# used during scheduling.
|
||||||
hypervisor_vars: "{{ hypervisor_vars }}"
|
- name: Collect hypervisor hostvars
|
||||||
node_types: "{{ node_types }}"
|
set_fact:
|
||||||
specs: "{{ specs }}"
|
hypervisor_vars: >-
|
||||||
register: allocations
|
{{ hypervisor_vars | default({}) | combine({item: hostvars[item]}) }}
|
||||||
|
loop: "{{ groups['hypervisors'] }}"
|
||||||
|
|
||||||
- name: Write node allocations to file
|
- name: Schedule nodes to hypervisors
|
||||||
copy:
|
tenks_schedule:
|
||||||
# tenks_schedule lookup plugin outputs a dict. Pretty-print this to persist
|
hypervisor_vars: "{{ hypervisor_vars }}"
|
||||||
# it in a YAML file.
|
node_types: "{{ node_types }}"
|
||||||
content: "{{ allocations.result | to_nice_yaml }}"
|
specs: "{{ specs }}"
|
||||||
dest: "{{ allocations_file_path }}"
|
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 }}"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user