Browse Source

backup: libvirt: implement backup create/prune

keep-around/c550d9a040735042a60d6f144a387ec494ddb13a
Loïc Dachary 3 months ago
parent
commit
1b988cdaa7
Signed by: dachary GPG Key ID: 992D23B392F9E4F2
  1. 4
      enough/common/__init__.py
  2. 42
      enough/common/libvirt.py
  3. 55
      enough/common/openstack.py
  4. 6
      inventory/group_vars/all/libvirt.yml
  5. 8
      tests/enough/common/test_init.py
  6. 21
      tests/enough/common/test_libvirt.py
  7. 30
      tests/enough/common/test_openstack.py

4
enough/common/__init__.py

@ -306,8 +306,12 @@ class Enough(object):
def backup_create(self):
if self.args.get('driver') == 'openstack':
self.openstack.backup_create(self.args['volumes'])
elif self.args.get('driver') == 'libvirt':
self.libvirt.backup_create(OpenStack(self.config_dir, **self.args))
def backup_prune(self):
if self.args.get('driver') == 'openstack':
self.openstack.backup_prune(self.args['days'])
self.openstack.volume_prune(self.args['days'])
elif self.args.get('driver') == 'libvirt':
self.libvirt.backup_prune(OpenStack(self.config_dir, **self.args))

42
enough/common/libvirt.py

@ -30,13 +30,18 @@ class Libvirt(object):
self.args = kwargs
self.config_dir = config_dir
self.share_dir = share_dir
self.lv = libvirt.open('qemu:///system')
self.cnx = None
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}'
self.network_definitions = None
def lv(self):
if self.cnx is None:
self.cnx = libvirt.open('qemu:///system')
return self.cnx
def host_image_name(self, name):
return f'{self.images_dir}/{name}.qcow2'
@ -71,7 +76,7 @@ class Libvirt(object):
def get(self, name):
try:
return self.lv.lookupByName(name)
return self.lv().lookupByName(name)
except libvirt.libvirtError:
return None
@ -121,7 +126,7 @@ class Libvirt(object):
@retry.retry((libvirt.libvirtError, AssertionError), 8)
def get_ipv4(self, name):
dom = self.lv.lookupByName(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():
@ -251,7 +256,7 @@ class Libvirt(object):
return f"<host mac='{mac}' name='{host}' ip='{ip}'/>"
def network_host_set(self, name, host, mac, ip):
network = self.lv.networkLookupByName(name)
network = self.lv().networkLookupByName(name)
xml = self.network_host_definition(host, mac, ip)
if xml in network.XMLDesc():
return False
@ -263,7 +268,7 @@ class Libvirt(object):
return True
def network_host_unset(self, name, host, mac, ip):
network = self.lv.networkLookupByName(name)
network = self.lv().networkLookupByName(name)
xml = self.network_host_definition(host, mac, ip)
if xml not in network.XMLDesc():
return False
@ -275,7 +280,7 @@ class Libvirt(object):
return True
def network_create(self, name, prefix):
if name not in self.lv.listNetworks():
if name not in self.lv().listNetworks():
network = textwrap.dedent(f"""
<network>
<name>{name}</name>
@ -288,16 +293,16 @@ class Libvirt(object):
</ip>
</network>
""")
network = self.lv.networkDefineXML(network)
network = self.lv().networkDefineXML(network)
network.create()
network.autostart()
else:
network = self.lv.networkLookupByName(name)
network = self.lv().networkLookupByName(name)
return network
def network_destroy(self, name):
if name in self.lv.listNetworks():
network = self.lv.networkLookupByName(name)
if name in self.lv().listNetworks():
network = self.lv().networkLookupByName(name)
network.destroy()
network.undefine()
return True
@ -313,9 +318,22 @@ class Libvirt(object):
return domain is not None
def destroy_everything(self, prefix):
for network in self.lv.listNetworks():
for network in self.lv().listNetworks():
if prefix in network:
self.network_destroy(network)
for domain in self.lv.listAllDomains():
for domain in self.lv().listAllDomains():
if prefix in domain.name():
self.delete(domain.name())
def pets_get(self):
return self.ansible.get_global_variable('libvirt_pets')
def backup_create(self, openstack):
log.info(f"pets {self.pets_get()}")
for pet in self.pets_get():
pathname = self.host_image_name(pet)
log.info(f"backup upload {pathname} to {pet}")
openstack.image_backup_upload(pet, pathname)
def backup_prune(self, openstack):
openstack.image_backup_prune(self.pets_get(), self.args['days'])

55
enough/common/openstack.py

@ -340,8 +340,7 @@ class OpenStack(OpenStackBase):
self.o.stack.delete('--yes', '--wait', name)
def delete_images():
for image in self.o.image.list('--private', '--format=value', '-c', 'Name', _iter=True):
image = image.strip()
for image in self.image_list():
if prefix is None or prefix in image:
leftovers.append(f'image({image})')
self.o.image.delete(image)
@ -452,6 +451,12 @@ class OpenStack(OpenStackBase):
def backup_date(self):
return datetime.datetime.today().strftime('%Y-%m-%d')
def backup_name_create(self, name):
return f'{self.backup_date()}-{name}'
def backup_extract_name(self, backup):
return backup[11:]
def backup_create(self, volumes):
if len(volumes) == 0:
volumes = [x.strip() for x in self.o.volume.list('--format=value', '-c', 'Name')]
@ -493,21 +498,26 @@ class OpenStack(OpenStackBase):
raise OpenStackBackupCreate(progress)
def backup_prune(self, days):
before = (datetime.datetime.today() - datetime.timedelta(days)).strftime('%Y-%m-%d')
count = 0
for name, status in self._backup_list():
if name[:10] > before:
if not self.backup_name_is_old(name, days):
continue
self.o.volume.snapshot.delete(name)
count += 1
return count
def backup_name_is_old(self, name, days):
before = (datetime.datetime.today() - datetime.timedelta(days)).strftime('%Y-%m-%d')
if re.match(r'^\d\d\d\d\-\d\d-\d\d-', name):
return name[:10] <= before
else:
return None
def volume_prune(self, days):
r = self.o.volume.snapshot.list(
'--format=json', '--long', '-c', 'Volume', '--limit', '5000')
volumes_with_snapshots = set([x["Volume"] for x in json.loads(r.stdout)])
r = self.o.volume.list('--format=json', '-c', 'Name', '-c', 'ID', '--limit', '5000')
before = (datetime.datetime.today() - datetime.timedelta(days)).strftime('%Y-%m-%d')
info = {
'no_date_prefix': [],
'has_snapshots': [],
@ -515,10 +525,11 @@ class OpenStack(OpenStackBase):
'recent': [],
}
for volume, volume_id in set([(x["Name"], x["ID"]) for x in json.loads(r.stdout)]):
if re.match(r'^\d\d\d\d\-\d\d-\d\d-', volume):
volume_is_old = self.backup_name_is_old(volume, days)
if volume_is_old is not None:
if volume_id in volumes_with_snapshots:
info['has_snapshots'].append(volume)
elif volume[:10] < before:
elif volume_is_old:
self.o.volume.delete(volume)
info['deleted'].append(volume)
else:
@ -550,8 +561,8 @@ class OpenStack(OpenStackBase):
# using current_volume_id here is not possible unfortunately
self.o.volume.delete(current_volume)
else:
date = self.backup_date()
self.o.volume.set('--name', f'{date}-{current_volume}', current_volume_id)
backup_name = self.backup_name_create(current_volume)
self.o.volume.set('--name', backup_name, current_volume_id)
self.o.volume.set('--name', current_volume, volume_id)
self.o.server.add.volume(host, volume_id)
self.o.server.start(host)
@ -584,8 +595,24 @@ class OpenStack(OpenStackBase):
self.o.server.start(host)
return True
def region_empty(self):
volumes = self.o.volume.list()
servers = self.o.server.list()
images = self.o.image.list('--private')
return volumes.strip() == '' and servers.strip() == '' and images.strip() == ''
def image_list(self):
return [
i.strip() for i in self.o.image.list(
'--private', '--format=value', '-c', 'Name', _iter=True)
]
def image_backup_upload(self, name, pathname):
date = self.backup_date()
image_name = f'{date}-{name}'
if image_name in self.image_list():
return False
self.o.image.create('--private', '--disk-format=qcow2', '--file', pathname, image_name)
return True
def image_backup_prune(self, names, days):
to_delete = []
for image in self.image_list():
if self.backup_name_is_old(image, days) and self.backup_extract_name(image) in names:
to_delete.append(image)
if to_delete:
self.o.image.delete(*to_delete)

6
inventory/group_vars/all/libvirt.yml

@ -20,6 +20,12 @@ libvirt_cpus: 1
#
#######################################
#
# List of hosts that need backups
#
libvirt_pets: []
#
#######################################
#
libvirt_network_external_name: enough-ext
libvirt_network_external_prefix: 10.23.10
libvirt_network_internal_name: enough-int

8
tests/enough/common/test_init.py

@ -146,7 +146,7 @@ def test_create_volume_from_snapshot(request, tmpdir):
e.set_args(name='sample', playbook='enough-playbook.yml')
e.service.create_or_update()
e.openstack.backup_create(['sample-volume'])
snapshot = f'{e.openstack.backup_date()}-sample-volume'
snapshot = f'{e.openstack.backup_name_create("sample-volume")}'
assert snapshot in e.openstack.o.volume.snapshot.list()
volume = 'test-volume'
@ -173,7 +173,7 @@ def test_clone_volume_from_snapshot(request, tmpdir):
sh.ssh('-oStrictHostKeyChecking=no',
'-i', original.dotenough.private_key(), f'debian@{ip}', 'sync')
original.openstack.backup_create(['sample-volume'])
snapshot = f'{original.openstack.backup_date()}-sample-volume'
snapshot = f'{original.openstack.backup_name_create("sample-volume")}'
assert snapshot in original.openstack.o.volume.snapshot.list()
clone_domain = 'clone.com'
@ -233,7 +233,7 @@ def test_restore_remote(request, tmpdir):
'-i', original.dotenough.private_key(), f'-p{port}',
f'debian@{ip}', 'touch', '/srv/STONE')
original.openstack.backup_create(['sample-volume'])
snapshot = f'{original.openstack.backup_date()}-sample-volume'
snapshot = f'{original.openstack.backup_name_create("sample-volume")}'
assert snapshot in original.openstack.o.volume.snapshot.list()
# original host isn't needed anymore
@ -279,7 +279,7 @@ def test_restore_local(request, tmpdir):
e.openstack.backup_create(['sample-volume'])
sh.ssh('-oStrictHostKeyChecking=no', '-i', e.dotenough.private_key(),
f'-p{port}', f'debian@{ip}', 'rm', '/srv/STONE')
snapshot = f'{e.openstack.backup_date()}-sample-volume'
snapshot = f'{e.openstack.backup_name_create("sample-volume")}'
assert snapshot in e.openstack.o.volume.snapshot.list()
assert e == e.restore_local(snapshot)

21
tests/enough/common/test_libvirt.py

@ -1,6 +1,8 @@
import pytest
import sh
from enough.common import libvirt
from enough.common.openstack import OpenStack
@pytest.mark.libvirt_integration
@ -50,3 +52,22 @@ def test_libvirt_image_builder(dotenough_libvirt_fixture):
lv = libvirt.Libvirt(dotenough_libvirt_fixture.config_dir, '.',
domain=dotenough_libvirt_fixture.domain)
assert lv.image_builder() is False
@pytest.mark.openstack_integration
@pytest.mark.libvirt_integration
def test_libvirt_backup(mocker, dotenough_libvirt_fixture, openstack_name, dot_openstack):
lv = libvirt.Libvirt(dotenough_libvirt_fixture.config_dir, '.',
domain=dotenough_libvirt_fixture.domain)
host = 'fake-host'
mocker.patch('enough.common.libvirt.Libvirt.pets_get', return_value=[host])
sh.qemu_img('create', '-f', 'qcow2', lv.host_image_name(host), '1M')
o = OpenStack(dot_openstack.config_dir)
lv.backup_create(o)
backup = o.backup_name_create(host)
assert [backup] == o.image_list()
lv.args['days'] = 0
lv.backup_prune(o)
assert [] == o.image_list()

30
tests/enough/common/test_openstack.py

@ -1,5 +1,6 @@
import datetime
import os
import sh
import pytest
import requests_mock as requests_mock_module
@ -129,7 +130,7 @@ def test_backup_create_with_name(openstack_name, dot_openstack, caplog):
o.o.volume.create('--size=1', openstack_name)
assert o.backup_create([openstack_name]) == 1
assert o.backup_create([openstack_name]) == 0
available_snapshot = f'AVAILABLE {o.backup_date()}-{openstack_name}'
available_snapshot = f'AVAILABLE {o.backup_name_create(openstack_name)}'
assert available_snapshot in caplog.text
assert o.backup_prune(0) == 1
assert o.backup_prune(0) == 0
@ -171,7 +172,7 @@ def test_backup_create_no_names(openstack_name, dot_openstack, caplog):
o = OpenStack(settings.CONFIG_DIR)
o.o.volume.create('--size=1', openstack_name)
o.backup_create([])
available_snapshot = f'AVAILABLE {o.backup_date()}-{openstack_name}'
available_snapshot = f'AVAILABLE {o.backup_name_create(openstack_name)}'
assert available_snapshot in caplog.text
@ -195,8 +196,7 @@ def test_openstack_replace_volume(openstack_name, dot_openstack,
assert openstack_name in o.o.volume.list('--name', openstack_name)
other_volume = f'{openstack_name}_other'
date = o.backup_date()
backup_volume = f'{date}-{openstack_name}'
backup_volume = o.backup_name_create(openstack_name)
o.o.volume.create('--size=1', other_volume)
o.replace_volume(openstack_name, other_volume, delete_volume=True)
@ -282,3 +282,25 @@ def test_destroy_volumes_with_same_name(openstack_name, dot_openstack):
assert 2 == o.o.volume.list().count(volume_name)
o.destroy_everything(openstack_name)
assert 0 == o.o.volume.list().count(volume_name)
@pytest.mark.openstack_integration
def test_image_backup_prune(tmpdir, mocker, openstack_name, dot_openstack):
pathname = f'{tmpdir}/{openstack_name}.qcow2'
sh.qemu_img('create', '-f', 'qcow2', pathname, '1M')
o = OpenStack(dot_openstack.config_dir)
today = o.backup_date()
old = '2010-01-01'
mocker.patch('enough.common.openstack.OpenStack.backup_date', return_value=old)
assert o.image_backup_upload(openstack_name, pathname) is True
mocker.patch('enough.common.openstack.OpenStack.backup_date', return_value=today)
assert o.image_backup_upload(openstack_name, pathname) is True
assert o.image_backup_upload(openstack_name, pathname) is False
days = 10
o.image_backup_prune([openstack_name], days)
assert o.image_list() == [o.backup_name_create(openstack_name)]
Loading…
Cancel
Save