Browse Source

enough: implement libvirt driver create_or_update

keep-around/68a3c3746a6b9a97ade4ac05826340644ed27ed2
Loïc Dachary 10 months ago
committed by some
parent
commit
d1120e920a
Signed by: dachary GPG Key ID: 992D23B392F9E4F2
  1. 2
      .gitignore
  2. 216
      enough/common/libvirt.py
  3. 14
      tests/conftest.py
  4. 69
      tests/enough/common/test_libvirt.py
  5. 5
      tests/enough/common/test_libvirt/networks_create/inventory/group_vars/all/libvirt.yml
  6. 27
      tests/run-tests.sh

2
.gitignore

@ -1,3 +1,5 @@
**/*.qcow2
**/*.img
*~
*.retry
__pycache__

216
enough/common/libvirt.py

@ -1,74 +1,148 @@
import json
import libvirt
import logging
import os
import sh
import textwrap
from enough import settings
from enough.common.ssh import SSH
from enough.common import ansible_utils
from enough.common.dotenough import Hosts
from enough.common import retry
log = logging.getLogger(__name__)
class Libvirt(object):
EXTERNAL_NETWORK = 'enough-external'
EXTERNAL_NETWORK_PREFIX = '10.23.10'
INTERNAL_NETWORK = 'enough-internal'
INTERNAL_NETWORK_PREFIX = '10.23.90'
NETWORK = {
'external': {
'prefix': '10.23.10',
'name': 'enough-external',
},
'internal': {
'prefix': '10.23.90',
'name': 'enough-internal',
},
}
def __init__(self, config_dir, **kwargs):
def __init__(self, config_dir, share_dir, **kwargs):
self.args = kwargs
self.config_dir = config_dir
self.share_dir = share_dir
self.lv = libvirt.open('qemu:///system')
self.ansible = ansible_utils.Ansible(self.config_dir, self.share_dir,
self.args.get('inventory'))
self.domain = kwargs.get('domain', 'enough.community')
self.images_dir = f'/var/lib/libvirt/images/enough/{self.domain}'
def get_stack_definitions(self, share_dir=settings.SHARE_DIR):
args = ['-i', f'{share_dir}/inventory',
'-i', f'{self.config_dir}/inventory']
if self.args.get('inventory'):
args.extend([f'--inventory={i}' for i in self.args['inventory']])
args.extend(['--vars', '--list'])
password_file = f'{self.config_dir}.pass'
if os.path.exists(password_file):
args.extend(['--vault-password-file', password_file])
r = sh.ansible_inventory(*args)
inventory = json.loads(r.stdout)
return inventory['_meta']['hostvars']
def get_stack_definition(self, host):
h = self.get_stack_definitions()[host]
if h.get('network_internal_only', False):
network_interface_unconfigured = h.get('network_primary_interface', 'eth0')
network_interface_routed = h.get('network_secondary_interface', 'eth1')
network_interface_not_routed = 'noname'
else:
network_interface_unconfigured = 'noname'
network_interface_routed = h.get('network_primary_interface', 'eth0')
network_interface_not_routed = h.get('network_secondary_interface', 'eth1')
definition = {
'name': host,
'port': h.get('ansible_port', '22'),
'network': h.get('libvirt_network', Libvirt.EXTERNAL_NETWORK),
'network_prefix': h.get('libvirt_network_prefix',
Libvirt.EXTERNAL_NETWORK_PREFIX),
'network_internal_only': h.get('network_internal_only', False),
'network_interface_unconfigured': network_interface_unconfigured,
'network_interface_routed': network_interface_routed,
'network_interface_not_routed': network_interface_not_routed,
'internal_network': h.get('libvirt_network_internal', Libvirt.INTERNAL_NETWORK),
'internal_network_prefix': h.get('libvirt_network_internal_prefix',
Libvirt.INTERNAL_NETWORK_PREFIX),
def host_image_name(self, name):
return f'{self.images_dir}/{name}.qcow2'
def public_key(self):
return f'{self.config_dir}/infrastructure_key.pub'
def sysprep(self, name, definition):
sh.virt_sysprep('-a', self.host_image_name(name),
'--enable', 'customize',
'--no-network',
'--ssh-inject', f'debian:file:{self.public_key()}')
def get(self, name):
try:
return self.lv.lookupByName(name)
except libvirt.libvirtError:
return None
def _create_or_update(self, definition):
name = definition['name']
if self.get(name) is not None:
return None
log.info(f"{name}: building image")
self.image_builder()
sh.cp('-a', '--sparse=always', self.image_name(), self.host_image_name(name))
log.info(f"{name}: preparing image")
self.sysprep(name, definition)
log.info(f"{name}: creating host")
sh.env('HOME=/tmp',
'virt-install',
'--connect', 'qemu:///system',
'--network', f"network={definition['network-external']}",
'--network', f"network={definition['network-internal']}",
'--boot', 'hd',
'--name', name,
'--memory', definition['ram'],
'--vcpus', '1',
'--cpu', 'host',
'--disk', f'path={self.host_image_name(name)},bus=virtio,format=qcow2',
'--os-type=linux',
'--os-variant=debian10',
'--graphics', 'none',
'--noautoconsole')
log.info(f"{name}: waiting for ipv4 to be allocated")
info = {
'ipv4': self.get_ipv4(name),
'port': definition['port'],
}
if 'openstack_volumes' in h:
definition['volumes'] = h['openstack_volumes']
return definition
log.info(f"{name}: waiting for {info['ipv4']}:{info['port']} to come up")
SSH.wait_for_ssh(info['ipv4'], info['port'])
Hosts(self.config_dir).create_or_update(
definition['name'], info['ipv4'], info['port'])
log.info(f"{name}: host is ready")
return info
@retry.retry((libvirt.libvirtError, AssertionError), 8)
def get_ipv4(self, name):
dom = self.lv.lookupByName(name)
ifaces = dom.interfaceAddresses(libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE)
assert ifaces, f'interfaceAddresses returned {ifaces}'
for (name, val) in ifaces.items():
addrs = val['addrs']
assert len(addrs) == 1, f"{addrs} expected len is 1"
addr = val['addrs'][0]
assert addr['type'] == libvirt.VIR_IP_ADDR_TYPE_IPV4
return addr['addr']
def create_or_update(self, names):
self.networks_create()
definitions = self.get_definitions()
r = {}
for name in names:
r[name] = self._create_or_update(
self.get_definition(name, definitions[name]))
return r
def get_definitions(self):
return self.ansible.get_hostvars(variable=None)
def virt_builder(self):
image = f'{self.config_dir}/debian-10.qcow2'
if self.path.exists(image):
def get_definition(self, name, definition):
r = {}
for network in ('external', 'internal'):
r[f'network-{network}'] = definition.get(
f'libvirt_network_{network}_name',
Libvirt.NETWORK[network]['name'],
)
r.update({
'name': name,
'port': definition.get('ansible_port', '22'),
'ram': definition.get('libvirt_ram', '2048'),
# 'disk': definition.get('libvirt_disk', '20G'),
})
return r
@staticmethod
def _image_name():
return 'debian-10.qcow2'
def image_name(self):
return f'{self.images_dir}/{self._image_name()}'
@staticmethod
def _image_builder(image):
if os.path.exists(image):
return False
sh.virt_builder(
'debian-10'
'debian-10',
'--no-cache',
'--output', image,
'--format', 'qcow2',
'--size', '20G',
@ -77,8 +151,35 @@ class Libvirt(object):
'--run-command', ('useradd -s /bin/bash -m debian || true ; '
'echo "debian ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/90-debian'),
'--edit', '/etc/network/interfaces: s/ens2/enp1s0/')
sh.chmod('0660', image)
sh.chgrp('libvirt', image)
return True
def image_builder(self):
if not os.path.exists(self.images_dir):
os.makedirs(self.images_dir)
sh.chmod('0771', self.images_dir)
sh.chgrp('libvirt', self.images_dir)
return self._image_builder(self.image_name())
def networks_create(self):
variables = ('libvirt_network_external_prefix',
'libvirt_network_internal_prefix',
'libvirt_network_external_name',
'libvirt_network_internal_name')
variables = "{%s}" % ', '.join(f'"{x}": {x}' for x in variables)
network_vars = self.ansible.get_global_variable(variables)
r = []
for network in ('external', 'internal'):
vars = {}
for var in ('prefix', 'name'):
vars[var] = network_vars.get(
f'libvirt_network_{network}_{var}',
Libvirt.NETWORK[network][var],
)
r.append(self.network_create(vars['name'], vars['prefix']))
return r
def network_host_definition(self, host, mac, ip):
return f"<host mac='{mac}' name='{host}' ip='{ip}'/>"
@ -129,12 +230,25 @@ class Libvirt(object):
def network_destroy(self, name):
if name in self.lv.listNetworks():
self.lv.networkLookupByName(name).destroy()
network = self.lv.networkLookupByName(name)
network.destroy()
network.undefine()
return True
else:
return False
def delete(self, name):
domain = self.get(name)
if domain:
domain.destroy()
domain.undefine()
Hosts(self.config_dir).delete(name)
return domain is not None
def destroy_everything(self, prefix):
for network in self.lv.listNetworks():
if prefix in network:
self.network_destroy(network)
for domain in self.lv.listAllDomains():
if prefix in domain.name():
self.delete(domain.name())

14
tests/conftest.py

@ -2,6 +2,8 @@ import logging
import os
import pytest
import sh
import shutil
import tempfile
import time
from enough import settings
@ -92,9 +94,9 @@ def openstack_name():
@pytest.fixture
def libvirt_name():
prefix = 'et' + str(int(time.time()))[3:]
prefix = 'et' + str(int(time.time()))[5:]
yield prefix
lv = libvirt.Libvirt('.')
lv = libvirt.Libvirt('.', '.')
lv.destroy_everything(prefix)
@ -108,6 +110,14 @@ def tmp_config_dir(monkeypatch, tmpdir):
monkeypatch.setattr(settings, 'CONFIG_DIR', config_dir)
@pytest.fixture
def libvirt_cachedimagesdir():
d = os.path.abspath(
tempfile.mkdtemp(dir='/var/lib/libvirt/images/enough'))
yield d
shutil.rmtree(d)
# Some tests mock os.path.exists using the mocker fixture, use trylast in
# order to avoid any interference
@pytest.hookimpl(trylast=True)

69
tests/enough/common/test_libvirt.py

@ -1,8 +1,28 @@
import os
import shutil
from enough.common import libvirt
from enough.common.dotenough import DotEnoughLibvirt
from tests import make_config_dir
def libvirt_prepare_config_dir(domain, enough_dot_dir):
config_dir = make_config_dir(domain, enough_dot_dir)
dotenough = DotEnoughLibvirt(config_dir, domain)
dotenough.ensure()
return config_dir
def libvirt_image_cache(tmpimagesdir):
cachedimagesdir = os.path.dirname(tmpimagesdir)
filename = libvirt.Libvirt._image_name()
cachedimage = f'{cachedimagesdir}/{filename}'
libvirt.Libvirt._image_builder(cachedimage)
os.symlink(cachedimage, f'{tmpimagesdir}/{filename}')
def test_libvirt_network(libvirt_name):
lv = libvirt.Libvirt('.')
lv = libvirt.Libvirt('.', '.')
prefix = '10.2.3'
network = lv.network_create(libvirt_name, prefix)
assert network.name() == libvirt_name
@ -17,3 +37,50 @@ def test_libvirt_network(libvirt_name):
assert lv.network_host_unset(libvirt_name, host, mac, ip) is False
assert lv.network_destroy(libvirt_name) is True
assert lv.network_destroy(libvirt_name) is False
def test_libvirt_create_or_udpate(tmpdir, mocker, libvirt_cachedimagesdir, libvirt_name):
port = '22'
global_variables = {
'libvirt_network_external_name': f'{libvirt_name}e',
'libvirt_network_internal_name': f'{libvirt_name}i',
}
mocker.patch('enough.common.ansible_utils.Ansible.get_global_variable',
return_value=global_variables)
definitions = {
libvirt_name: {
'name': libvirt_name,
'ansible_port': port,
'libvirt_ram': '1024',
},
}
definitions[libvirt_name].update(global_variables)
mocker.patch('enough.common.libvirt.Libvirt.get_definitions',
return_value=definitions)
enough_domain = 'enough.com'
config_dir = libvirt_prepare_config_dir(enough_domain, tmpdir)
libvirt_image_cache(libvirt_cachedimagesdir)
lv = libvirt.Libvirt(config_dir, '.')
lv.images_dir = libvirt_cachedimagesdir
info = lv.create_or_update([libvirt_name])
assert info[libvirt_name]['port'] == port
def test_libvirt_networks_create(mocker, tmpdir, libvirt_name):
enough_domain = 'enough.com'
shutil.copytree(f'tests/enough/common/test_libvirt/networks_create',
f'{tmpdir}/{enough_domain}')
config_dir = libvirt_prepare_config_dir(enough_domain, tmpdir)
lv = libvirt.Libvirt(config_dir, '.')
mocker.patch('enough.common.libvirt.Libvirt.network_create', side_effect=lambda *args: args)
assert lv.networks_create() == [('test-external', '1.2.3'), ('test-internal', '10.20.30')]
def test_libvirt_image_builder(tmpdir, libvirt_cachedimagesdir, libvirt_name):
enough_domain = 'enough.com'
config_dir = libvirt_prepare_config_dir(enough_domain, tmpdir)
libvirt_image_cache(libvirt_cachedimagesdir)
lv = libvirt.Libvirt(config_dir, '.')
lv.images_dir = libvirt_cachedimagesdir
assert lv.image_builder() is False

5
tests/enough/common/test_libvirt/networks_create/inventory/group_vars/all/libvirt.yml

@ -0,0 +1,5 @@
---
libvirt_network_external_name: test-external
libvirt_network_external_prefix: 1.2.3
libvirt_network_internal_name: test-internal
libvirt_network_internal_prefix: 10.20.30

27
tests/run-tests.sh

@ -15,6 +15,12 @@ function prepare_environment() {
if test -d ~/.ansible && test "$(find ~/.ansible ! -user $(id -u) -print -quit)" ; then
$SUDO chown -R $(id -u) ~/.ansible
fi
local d=/var/lib/libvirt/images/enough
if ! test -d $d ; then
$SUDO mkdir -p $d
$SUDO chgrp libvirt $d
$SUDO chmod 771 $d
fi
}
function prepare_repository() {
@ -69,7 +75,13 @@ function build_image() {
if test "$GITLAB_CI" ; then
cat tests/copy-to-opt.dockerfile
fi
) | timeout $max_execution_time docker build --build-arg=USER_ID="$(id -u)" --build-arg=DOCKER_GID="$(getent group docker | cut -d: -f3)" --build-arg=LIBVIRT_GID="$(getent group libvirt | cut -d: -f3)" --build-arg=KVM_GID="$(getent group kvm | cut -d: -f3)" --build-arg=USER_NAME="${USER:-root}" --tag $name -f - .
) | timeout $max_execution_time docker build \
--build-arg=USER_ID="$(id -u)" \
--build-arg=DOCKER_GID="$(getent group docker | cut -d: -f3)" \
--build-arg=LIBVIRT_GID="$(getent group libvirt | cut -d: -f3)" \
--build-arg=KVM_GID="$(getent group kvm | cut -d: -f3)" \
--build-arg=USER_NAME="${USER:-root}" \
--tag $name -f - .
}
function run_tests() {
@ -98,7 +110,18 @@ function run_tests() {
local interactive=-ti
fi
container_name="$name-$(date +%s)"
docker run $interactive --rm --device /dev/kvm --device=/dev/net/tun --name $container_name --user "${USER:-root}" -e PYTEST_ADDOPTS="${PYTEST_ADDOPTS-}" -e ENOUGH_API_TOKEN=$ENOUGH_API_TOKEN ${PASS_GITLAB_CI} --cap-add=NET_ADMIN $args --volume /run/libvirt/libvirt-sock:/run/libvirt/libvirt-sock --volume /var/run/docker.sock:/var/run/docker.sock $name "${@:-tox}"
docker run $interactive --rm \
--device /dev/kvm --device=/dev/net/tun \
--name $container_name \
--user "${USER:-root}" \
-e PYTEST_ADDOPTS="${PYTEST_ADDOPTS-}" \
-e ENOUGH_API_TOKEN=$ENOUGH_API_TOKEN ${PASS_GITLAB_CI} \
--cap-add=NET_ADMIN \
$args \
--volume /run/libvirt/libvirt-sock:/run/libvirt/libvirt-sock \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume /var/lib/libvirt/images/enough:/var/lib/libvirt/images/enough \
$name "${@:-tox}"
}
function main() {

Loading…
Cancel
Save