Merge pull request #1 from stackhpc/proof-of-concept
[WIP] Proof of concept
This commit is contained in:
commit
bbb727dda8
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,3 +1,12 @@
|
||||
# All dot-files
|
||||
.*
|
||||
# Ansible retry files
|
||||
*.retry
|
||||
# Tenks allocations file
|
||||
allocations.yml
|
||||
# Tenks Galaxy roles
|
||||
ansible/roles/stackhpc.*
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
18
README.md
Normal file
18
README.md
Normal file
@ -0,0 +1,18 @@
|
||||
Tenks
|
||||
=====
|
||||
|
||||
Tenks is a utility that manages virtual bare metal clusters for development and
|
||||
testing purposes.
|
||||
|
||||
Getting Started
|
||||
---------------
|
||||
|
||||
Tenks has dependencies on Ansible roles that are hosted by Ansible Galaxy.
|
||||
Given that your virtualenv of choice is active and Ansible (>=2.6) is
|
||||
installed inside it, Tenks' role dependencies can be installed by
|
||||
`ansible-galaxy install --role-file=requirements.yml
|
||||
--roles-path=ansible/roles/`.
|
||||
|
||||
Currently, Tenks does not have a CLI or wrapper. A virtual cluster can be
|
||||
deployed by configuring the variables defined in `group_vars/*` as necessary,
|
||||
then calling `ansible-playbook --inventory ansible/inventory deploy.yml`.
|
90
ansible/action_plugins/tenks_schedule.py
Normal file
90
ansible/action_plugins/tenks_schedule.py
Normal file
@ -0,0 +1,90 @@
|
||||
# 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.
|
||||
|
||||
# Avoid shadowing of system copy module by copy action plugin.
|
||||
from __future__ import absolute_import
|
||||
from copy import deepcopy
|
||||
|
||||
from ansible.errors import AnsibleActionFail
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.plugins.action import ActionBase
|
||||
import six
|
||||
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
def run(self, tmp=None, task_vars=None):
|
||||
"""
|
||||
Schedule specifications of VMs by type onto hypervisors.
|
||||
|
||||
The following task vars are accepted:
|
||||
:hypervisor_vars: A dict of hostvars for each hypervisor, keyed
|
||||
by hypervisor hostname. Required.
|
||||
:specs: A dict mapping VM type names to the number of VMs required
|
||||
of that type. Required.
|
||||
:vm_types: A dict mapping VM type names to a dict of properties
|
||||
of that type.
|
||||
:vm_name_prefix: A string with which to prefix all sequential VM
|
||||
names.
|
||||
:vol_name_prefix: A string with which to prefix all sequential
|
||||
volume names.
|
||||
:returns: A dict containing lists of VM details, keyed by the
|
||||
hostname of the hypervisor to which they are scheduled.
|
||||
"""
|
||||
result = super(ActionModule, self).run(tmp, task_vars)
|
||||
del tmp # tmp no longer has any effect
|
||||
self._validate_vars(task_vars)
|
||||
|
||||
vms = []
|
||||
idx = 0
|
||||
for typ, cnt in six.iteritems(task_vars['specs']):
|
||||
for _ in six.range(cnt):
|
||||
vm = deepcopy(task_vars['vm_types'][typ])
|
||||
# Sequentially number the VM and volume names.
|
||||
vm['name'] = "%s%d" % (task_vars['vm_name_prefix'], idx)
|
||||
for vol_idx, vol in enumerate(vm['volumes']):
|
||||
vol['name'] = "%s%d" % (task_vars['vol_name_prefix'],
|
||||
vol_idx)
|
||||
vms.append(vm)
|
||||
idx += 1
|
||||
|
||||
# TODO(w-miller): currently we just arbitrarily schedule all VMs to the
|
||||
# first hypervisor. Improve this algorithm to make it more
|
||||
# sophisticated.
|
||||
result['result'] = {task_vars['hypervisor_vars'].keys()[0]: vms}
|
||||
return result
|
||||
|
||||
def _validate_vars(self, task_vars):
|
||||
if task_vars is None:
|
||||
task_vars = {}
|
||||
|
||||
REQUIRED_TASK_VARS = {'hypervisor_vars', 'specs', 'vm_types'}
|
||||
# Var names and their defaults.
|
||||
OPTIONAL_TASK_VARS = {
|
||||
('vm_name_prefix', 'vm'),
|
||||
('vol_name_prefix', 'vol'),
|
||||
}
|
||||
for var in REQUIRED_TASK_VARS:
|
||||
if var not in task_vars:
|
||||
e = "The parameter '%s' must be specified." % var
|
||||
raise AnsibleActionFail(to_text(e))
|
||||
|
||||
for var in OPTIONAL_TASK_VARS:
|
||||
if var[0] not in task_vars:
|
||||
task_vars[var[0]] = var[1]
|
||||
|
||||
if not task_vars['hypervisor_vars']:
|
||||
e = ("There are no hosts in the 'hypervisors' group to which we "
|
||||
"can schedule.")
|
||||
raise AnsibleActionFail(to_text(e))
|
53
ansible/deploy.yml
Normal file
53
ansible/deploy.yml
Normal file
@ -0,0 +1,53 @@
|
||||
---
|
||||
- hosts: hypervisors
|
||||
tasks:
|
||||
- include_tasks: host_setup.yml
|
||||
|
||||
- hosts: libvirt
|
||||
tasks:
|
||||
- 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 }}"
|
||||
|
||||
# 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: vm_physical_network.yml
|
||||
vars:
|
||||
vm_name: "{{ item.0.name }}"
|
||||
physnet: "{{ item.1 }}"
|
||||
# Loop over each physical network for each VM allocated to this host.
|
||||
# Allocations are stored in localhost's vars.
|
||||
loop: >-
|
||||
{{ hostvars['localhost'].allocations.result[inventory_hostname]
|
||||
| default([]) | subelements('physical_networks') }}
|
||||
|
||||
- hosts: libvirt
|
||||
tasks:
|
||||
- include_tasks: libvirt_create_vms.yml
|
||||
vars:
|
||||
# Allocations are stored in the localhost's vars.
|
||||
vms: >-
|
||||
{{ hostvars['localhost'].allocations.result[inventory_hostname]
|
||||
| default([]) }}
|
70
ansible/filter_plugins/libvirt_vm_config.py
Normal file
70
ansible/filter_plugins/libvirt_vm_config.py
Normal file
@ -0,0 +1,70 @@
|
||||
# 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, vm):
|
||||
"""Set interfaces for a VM's specified physical networks.
|
||||
"""
|
||||
physnet_mappings = _get_hostvar(context, 'physnet_mappings')
|
||||
prefix = _get_hostvar(context, 'veth_prefix')
|
||||
suffix = _get_hostvar(context, 'veth_vm_source_suffix')
|
||||
|
||||
vm['interfaces'] = []
|
||||
# Libvirt doesn't need to know about physical networks, so pop them here.
|
||||
for physnet in vm.pop('physical_networks', []):
|
||||
# Get the ID of this physical network on the hypervisor.
|
||||
idx = sorted(physnet_mappings).index(physnet)
|
||||
vm['interfaces'].append(
|
||||
{'type': 'direct',
|
||||
# FIXME(w-miller): Don't duplicate the logic of this naming scheme
|
||||
# from vm_physical_network.yml
|
||||
'source': {'dev': prefix + vm['name'] + '-' + str(idx) + suffix}}
|
||||
)
|
||||
return vm
|
||||
|
||||
|
||||
@contextfilter
|
||||
def set_libvirt_volume_pool(context, vm):
|
||||
"""Set the Libvirt volume pool for each volume.
|
||||
"""
|
||||
pool = _get_hostvar(context, 'libvirt_pool_name')
|
||||
for vol in vm.get('volumes', []):
|
||||
vol['pool'] = pool
|
||||
return vm
|
37
ansible/group_vars/hypervisors
Normal file
37
ansible/group_vars/hypervisors
Normal file
@ -0,0 +1,37 @@
|
||||
---
|
||||
# Map physical network names to their source device. This can be either an
|
||||
# existing interface or an existing bridge.
|
||||
physnet_mappings: {}
|
||||
|
||||
system_requirements:
|
||||
- python-virtualenv
|
||||
|
||||
# Path to virtualenv used to install Python requirements. If a virtualenv does
|
||||
# not exist at this location, one will be created.
|
||||
virtualenv_path: "{{ '/'.join([ansible_env['HOME'], 'tenks-venv']) }}"
|
||||
|
||||
# Naming scheme for bridges created by tenks for physical networks is
|
||||
# {{ bridge_prefix + i }}, where `i` is the index of the physical network in
|
||||
# physnet_mappings (sorted alphabetically by key).
|
||||
bridge_prefix: brtenks
|
||||
|
||||
# Prefix for all veth links.
|
||||
veth_prefix: 'p-'
|
||||
|
||||
# Suffix for veth links attached to a Tenks OVS bridge.
|
||||
veth_bridge_ovs_suffix: '-ovs'
|
||||
# Suffix for veth links attached to a source Linux bridge.
|
||||
veth_bridge_source_suffix: '-phy'
|
||||
|
||||
# Suffix for veth links attached to a Tenks OVS bridge.
|
||||
veth_vm_ovs_suffix: '-ovs'
|
||||
# Suffix for veth links attached to a VM. VMs aren't physical so '-phy' doesn't
|
||||
# seem right.
|
||||
veth_vm_source_suffix: '-tap'
|
||||
|
||||
console_log_directory: /var/log/tenks/console_logs/
|
||||
|
||||
# The URL of the upper constraints file to pass to pip when installing Python
|
||||
# packages.
|
||||
python_upper_contraints_url: >-
|
||||
https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt
|
13
ansible/group_vars/libvirt
Normal file
13
ansible/group_vars/libvirt
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
libvirt_pool_name: tenks
|
||||
libvirt_pool_path: /var/lib/libvirt/tenks_pool/
|
||||
libvirt_pool_type: dir
|
||||
# Capacity is irrelevant for directory-based pools.
|
||||
libvirt_pool_capacity:
|
||||
libvirt_pool_mode: 755
|
||||
libvirt_pool_owner: "{{ ansible_user_id }}"
|
||||
libvirt_pool_group: "{{ ansible_user_id }}"
|
||||
|
||||
# By default, allow QEMU without hardware virtualisation since this is a
|
||||
# development tool.
|
||||
libvirt_require_vt: false
|
53
ansible/host_setup.yml
Normal file
53
ansible/host_setup.yml
Normal file
@ -0,0 +1,53 @@
|
||||
---
|
||||
- name: Ensure general system requirements are installed
|
||||
yum:
|
||||
name: "{{ system_requirements }}"
|
||||
become: true
|
||||
|
||||
- name: Ensure console log directory exists
|
||||
file:
|
||||
path: "{{ console_log_directory }}"
|
||||
state: directory
|
||||
become: true
|
||||
|
||||
- name: Check if ovs-vsctl command is present
|
||||
command: ovs-vsctl --version
|
||||
register: ovs_vsctl_check
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- block:
|
||||
- name: Ensure Open vSwitch package is installed
|
||||
yum:
|
||||
name: openvswitch
|
||||
become: true
|
||||
|
||||
- name: Ensure Open vSwitch is started and enabled
|
||||
service:
|
||||
name: openvswitch
|
||||
state: running
|
||||
enabled: true
|
||||
become: true
|
||||
# Return code 127 means the command does not exist. Do this check to avoid
|
||||
# installing Open vSwitch system-wide if the command already exists as a link
|
||||
# to a containerised version of OVS.
|
||||
when: ovs_vsctl_check.rc == 127
|
||||
|
||||
- name: Configure physical networks
|
||||
include_tasks: physical_network.yml
|
||||
vars:
|
||||
network_name: "{{ item.0 }}"
|
||||
tenks_bridge: "{{ bridge_prefix ~ idx }}"
|
||||
source_interface: "{{ item.1 }}"
|
||||
# Sort to ensure we always enumerate in the same order.
|
||||
loop: "{{ physnet_mappings | dictsort }}"
|
||||
loop_control:
|
||||
index_var: idx
|
||||
|
||||
- name: Ensure Python requirements are installed
|
||||
pip:
|
||||
requirements: >-
|
||||
{{ '/'.join([(playbook_dir | dirname), 'venv-requirements.txt']) }}
|
||||
extra_args: >-
|
||||
-c {{ python_upper_contraints_url }}
|
||||
virtualenv: "{{ virtualenv_path }}"
|
33
ansible/host_vars/localhost
Normal file
33
ansible/host_vars/localhost
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
allocations_file_path: >-
|
||||
{{ '/'.join([(playbook_dir | dirname), 'allocations.yml']) }}
|
||||
|
||||
# vm_types is a dict that defines different sets of VM specifications, keyed by
|
||||
# a 'VM type name' to associate with each set of specifications. An example of
|
||||
# the format of this variable is below:
|
||||
#
|
||||
# vm_types:
|
||||
# # The type name.
|
||||
# type0:
|
||||
# # The amount of RAM, in mebibytes.
|
||||
# memory_mb: 1024
|
||||
# # The number of virtual CPUs.
|
||||
# vcpus: 2
|
||||
# # A list of volumes, each with a capacity.
|
||||
# volumes:
|
||||
# - capacity: 2GB
|
||||
# # A list of physical network names to connect to. These physical network
|
||||
# # names should be keyed in `physnet_mappings` in each hypervisor's host
|
||||
# # vars.
|
||||
# physical_networks:
|
||||
# - physnet1
|
||||
vm_types: {}
|
||||
|
||||
# specs is a dict that maps different VM type names (define in `vm_types`
|
||||
# above) to the number of VMs of that type that are to be created. Only VM
|
||||
# types which you want to create VMs from need be keyed here. For example:
|
||||
#
|
||||
# specs:
|
||||
# # Create four VMs with the specifications defined in `vm_types` by 'type0'.
|
||||
# type0: 4
|
||||
specs: {}
|
5
ansible/inventory/groups
Normal file
5
ansible/inventory/groups
Normal file
@ -0,0 +1,5 @@
|
||||
[hypervisors:children]
|
||||
libvirt
|
||||
|
||||
[libvirt]
|
||||
# Empty group to provide declaration of libvirt group.
|
2
ansible/inventory/hosts
Normal file
2
ansible/inventory/hosts
Normal file
@ -0,0 +1,2 @@
|
||||
[libvirt]
|
||||
localhost ansible_connection=local
|
11
ansible/libvirt_create_vms.yml
Normal file
11
ansible/libvirt_create_vms.yml
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
- name: Create VM
|
||||
include_role:
|
||||
name: stackhpc.libvirt-vm
|
||||
vars:
|
||||
libvirt_vm_default_console_log_dir: "{{ console_log_directory }}"
|
||||
# Configure VM definitions for the Libvirt provider.
|
||||
libvirt_vms: >-
|
||||
{{ vms | map('set_libvirt_interfaces')
|
||||
| map('set_libvirt_volume_pool')
|
||||
| list }}
|
86
ansible/physical_network.yml
Normal file
86
ansible/physical_network.yml
Normal file
@ -0,0 +1,86 @@
|
||||
---
|
||||
- name: Fail if source interface does not exist
|
||||
fail:
|
||||
msg: >
|
||||
The interface {{ source_interface }} specified for the physical network
|
||||
{{ network_name }} does not exist.
|
||||
when: source_interface not in ansible_interfaces
|
||||
|
||||
### Firstly, some fact gathering.
|
||||
# Start off by assuming the source interface is direct, unless proven
|
||||
# otherwise.
|
||||
- set_fact:
|
||||
source_type: direct
|
||||
|
||||
- name: Get source interface details
|
||||
command: ip -details link show {{ source_interface }}
|
||||
register: if_details
|
||||
changed_when: false
|
||||
|
||||
- name: Register source interface as a Linux bridge
|
||||
set_fact:
|
||||
source_type: linux_bridge
|
||||
when: if_details.stdout_lines[-1].split()[0] == 'bridge'
|
||||
|
||||
- block:
|
||||
- name: Get list of OVS bridges
|
||||
command: ovs-vsctl list-br
|
||||
register: ovs_bridges
|
||||
changed_when: false
|
||||
|
||||
- name: Register source interface as an Open vSwitch bridge
|
||||
set_fact:
|
||||
source_type: ovs_bridge
|
||||
when: source_interface in ovs_bridges.stdout_lines
|
||||
|
||||
when: if_details.stdout_lines[-1].split()[0] == 'openvswitch'
|
||||
|
||||
|
||||
### Actual configuration starts here.
|
||||
- name: Ensure Open vSwitch bridge exists
|
||||
openvswitch_bridge:
|
||||
bridge: "{{ tenks_bridge }}"
|
||||
|
||||
- name: Connect to existing Linux bridge
|
||||
when: source_type == 'linux_bridge'
|
||||
include_role:
|
||||
name: veth-pair
|
||||
vars:
|
||||
veth_pair_ovs_bridge: "{{ tenks_bridge }}"
|
||||
veth_pair_ovs_link_name: >-
|
||||
{{ veth_prefix + tenks_bridge + veth_bridge_ovs_suffix }}
|
||||
veth_pair_source_bridge: "{{ source_interface }}"
|
||||
veth_pair_source_link_name: >-
|
||||
{{ veth_prefix + tenks_bridge + veth_bridge_source_suffix }}
|
||||
plug_into_source: true
|
||||
|
||||
- name: Connect to existing Open vSwitch bridge
|
||||
when: source_type == 'ovs_bridge'
|
||||
block:
|
||||
- name: Create patch port on Tenks bridge
|
||||
openvswitch_port:
|
||||
bridge: "{{ tenks_bridge }}"
|
||||
port: "{{ veth_prefix + tenks_bridge + veth_bridge_ovs_suffix }}"
|
||||
# Despite the module documentation, `set` will happily take multiple
|
||||
# properties.
|
||||
set: >-
|
||||
Interface {{ veth_prefix + tenks_bridge + veth_bridge_ovs_suffix }}
|
||||
type=patch
|
||||
options:peer={{ veth_prefix + tenks_bridge +
|
||||
veth_bridge_source_suffix }}
|
||||
|
||||
- name: Create patch port on source bridge
|
||||
openvswitch_port:
|
||||
bridge: "{{ source_interface }}"
|
||||
port: "{{ veth_prefix + tenks_bridge + veth_bridge_source_suffix }}"
|
||||
set: >-
|
||||
Interface {{ veth_prefix + tenks_bridge + veth_bridge_source_suffix }}
|
||||
type=patch
|
||||
options:peer={{ veth_prefix + tenks_bridge +
|
||||
veth_bridge_ovs_suffix }}
|
||||
|
||||
- name: Plug source interface into Tenks bridge
|
||||
when: source_type == 'direct'
|
||||
openvswitch_port:
|
||||
bridge: "{{ tenks_bridge }}"
|
||||
port: "{{ source_interface }}"
|
26
ansible/roles/veth-pair/README.md
Normal file
26
ansible/roles/veth-pair/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
Veth Pair
|
||||
=========
|
||||
|
||||
This role creates a veth pair. It will plug one end into the specified OVS
|
||||
bridge and, optionally, can plug the other end into a source Linux bridge.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
The host should have the `ip` and `ovs-vsctl` commands accessible. If
|
||||
`veth_pair_plug_into_source` is enabled, the command `brctl` must also be
|
||||
accessible.
|
||||
|
||||
Role Variables
|
||||
--------------
|
||||
|
||||
- `veth_pair_ovs_link_name`: The name to give the veth link that plugs into the
|
||||
OVS bridge.
|
||||
- `veth_pair_ovs_bridge`: The name of the OVS bridge to plug into.
|
||||
- `veth_pair_source_link_name`: The name to give the veth link that plugs into
|
||||
the source device.
|
||||
- `veth_pair_source_bridge`: The name of the source Linux bridge to plug into. Must be
|
||||
specified if and only if `veth_pair_plug_into_source` is enabled.
|
||||
- `veth_pair_plug_into_source`: Whether or not to plug the source end of the
|
||||
veth pair into a Linux bridge. If enabled, `veth_pair_source_bridge` must
|
||||
also be specified. Default is `false`.
|
3
ansible/roles/veth-pair/defaults/main.yml
Normal file
3
ansible/roles/veth-pair/defaults/main.yml
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
# Whether or not to plug the source end of the veth pair into a Linux bridge.
|
||||
veth_pair_plug_into_source: false
|
29
ansible/roles/veth-pair/tasks/main.yml
Normal file
29
ansible/roles/veth-pair/tasks/main.yml
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
- name: Create veth pair
|
||||
command: >-
|
||||
ip link add dev {{ veth_pair_ovs_link_name }}
|
||||
type veth
|
||||
peer name {{ veth_pair_source_link_name }}
|
||||
register: res
|
||||
changed_when: res.rc == 0
|
||||
# Return code 2 means the veth pair already exists
|
||||
failed_when: res.rc not in [0, 2]
|
||||
become: true
|
||||
|
||||
- name: Plug veth into OVS bridge
|
||||
openvswitch_port:
|
||||
bridge: "{{ veth_pair_ovs_bridge }}"
|
||||
port: "{{ veth_pair_ovs_link_name }}"
|
||||
become: true
|
||||
|
||||
- name: Plug veth into source bridge
|
||||
command: >-
|
||||
brctl addif {{ veth_pair_source_bridge }}
|
||||
{{ veth_pair_source_link_name }}
|
||||
register: res
|
||||
failed_when:
|
||||
- res.rc != 0
|
||||
- "'already a member of a bridge' not in res.stderr"
|
||||
changed_when: "'already a member of a bridge' not in res.stderr"
|
||||
when: veth_pair_plug_into_source | bool
|
||||
become: true
|
22
ansible/schedule.yml
Normal file
22
ansible/schedule.yml
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
# 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: Schedule VMs to hypervisors
|
||||
tenks_schedule:
|
||||
hypervisor_vars: "{{ hypervisor_vars }}"
|
||||
vm_types: "{{ vm_types }}"
|
||||
specs: "{{ specs }}"
|
||||
register: allocations
|
||||
|
||||
- name: Write VM 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 }}"
|
28
ansible/vm_physical_network.yml
Normal file
28
ansible/vm_physical_network.yml
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
- name: Gather details for VM 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 VM veth base name
|
||||
set_fact:
|
||||
# Veth pairs are unique for any VM-physnet combination. However, device
|
||||
# names cannot be longer than 15 characters, so use physical networks'
|
||||
# indices instead.
|
||||
veth_base_name: >-
|
||||
{{ veth_prefix + vm_name + '-' + idx }}
|
||||
|
||||
- name: Set up veth pairs for the VM
|
||||
include_role:
|
||||
name: veth-pair
|
||||
vars:
|
||||
veth_pair_ovs_bridge: >-
|
||||
{{ bridge_prefix ~ idx }}
|
||||
veth_pair_ovs_link_name: "{{ veth_base_name + veth_vm_ovs_suffix }}"
|
||||
veth_pair_source_link_name: >-
|
||||
{{ veth_base_name + veth_vm_source_suffix }}
|
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@ -0,0 +1,6 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
pbr>=2.0 # Apache-2.0
|
||||
ansible>=2.6.0 # GPLv3
|
3
requirements.yml
Normal file
3
requirements.yml
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
- src: stackhpc.libvirt-host
|
||||
- src: stackhpc.libvirt-vm
|
20
setup.cfg
Normal file
20
setup.cfg
Normal file
@ -0,0 +1,20 @@
|
||||
[metadata]
|
||||
name = tenks
|
||||
summary = Deployment of virtual bare metal clusters with Tenks
|
||||
description-file =
|
||||
README.md
|
||||
author = Will Miller
|
||||
author-email = willm@stackhpc.com
|
||||
home-page = https://stackhpc.com
|
||||
classifier =
|
||||
Environment :: OpenStack
|
||||
Intended Audience :: Information Technology
|
||||
Intended Audience :: System Administrators
|
||||
Operating System :: POSIX :: Linux
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 2
|
||||
Programming Language :: Python :: 2.7
|
||||
|
||||
[files]
|
||||
packages =
|
||||
tenks
|
29
setup.py
Normal file
29
setup.py
Normal file
@ -0,0 +1,29 @@
|
||||
# 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.
|
||||
|
||||
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
|
||||
import setuptools
|
||||
|
||||
# In python < 2.7.4, a lazy loading of package `pbr` will break
|
||||
# setuptools if some other modules registered functions in `atexit`.
|
||||
# solution from: http://bugs.python.org/issue15881#msg170215
|
||||
try:
|
||||
import multiprocessing # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['pbr'],
|
||||
pbr=True)
|
6
test-requirements.txt
Normal file
6
test-requirements.txt
Normal file
@ -0,0 +1,6 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
ansible-lint>=3.0.0 # MIT
|
||||
flake8>=3.5.0 # MIT
|
46
tox.ini
Normal file
46
tox.ini
Normal file
@ -0,0 +1,46 @@
|
||||
[tox]
|
||||
minversion = 2.0
|
||||
envlist = py35,py27,pep8,alint
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
install_command = pip install {opts} {packages}
|
||||
passenv =
|
||||
HOME
|
||||
whitelist_externals =
|
||||
bash
|
||||
rm
|
||||
setenv =
|
||||
VIRTUAL_ENV={envdir}
|
||||
PYTHONWARNINGS=default::DeprecationWarning
|
||||
OS_STDOUT_CAPTURE=1
|
||||
OS_STDERR_CAPTURE=1
|
||||
OS_TEST_TIMEOUT=60
|
||||
deps =
|
||||
-c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt?h=stable/rocky}
|
||||
-r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
|
||||
[testenv:pep8]
|
||||
basepython = python3
|
||||
commands =
|
||||
flake8 {posargs}
|
||||
|
||||
[testenv:alint]
|
||||
basepython = python3
|
||||
# ansible-lint doesn't support custom modules, so add ours to the Ansible path.
|
||||
setenv = ANSIBLE_LIBRARY = {toxinidir}/ansible/action_plugins/
|
||||
# Exclude roles downloaded from Galaxy (in the form 'author.role') from
|
||||
# linting.
|
||||
commands = bash -c "ansible-lint \
|
||||
$(find {toxinidir}/ansible -path '*.yml' \
|
||||
-not -path '{toxinidir}/ansible/roles/*.*/*' -print)"
|
||||
|
||||
[flake8]
|
||||
# E123, E125 skipped as they are invalid PEP-8.
|
||||
|
||||
show-source = True
|
||||
ignore = E123,E125
|
||||
builtins = _
|
||||
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build
|
4
venv-requirements.txt
Normal file
4
venv-requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
# This file contains the Python packages that are needed in the Tenks virtual
|
||||
# env.
|
||||
|
||||
virtualbmc>=1.4.0 # Apache
|
Loading…
x
Reference in New Issue
Block a user