Browse Source

enough: implement Enough.clone_volume_from_snapshot

keep-around/7f2bc0d22ec09ff17e66fcb005b9263daa8dd796
Loïc Dachary 1 year ago
committed by Loic Dachary
parent
commit
5d4987434a
Signed by: dachary GPG Key ID: 992D23B392F9E4F2
  1. 75
      enough/common/__init__.py
  2. 13
      enough/common/ansible_utils.py
  3. 6
      enough/common/dotenough.py
  4. 86
      enough/common/openstack.py
  5. 5
      inventory/02-all.yml
  6. 3
      inventory/all.yml
  7. 5
      molecule/infrastructure/roles/encrypted_device/tasks/main.yml
  8. 42
      tests/enough/common/test_init.py
  9. 6
      tests/enough/common/test_init/backup/encrypted-volume-playbook.yml

75
enough/common/__init__.py

@ -1,13 +1,17 @@
import logging
import os
import re
import sh
import shutil
from enough.common.dotenough import DotEnough
from enough.common.dotenough import DotEnough, Hosts
from enough.common.host import host_factory
from enough.common.service import service_factory
from enough.common.openstack import OpenStack
from enough.common import ansible_utils
log = logging.getLogger(__name__)
class Enough(object):
@ -24,6 +28,7 @@ class Enough(object):
if 'clouds' not in self.args:
self.args['clouds'] = f'{self.config_dir}/inventory/group_vars/all/clouds.yml'
self.dotenough = DotEnough(self.config_dir, self.args['domain'])
self.hosts = Hosts(self.config_dir)
self.host = host_factory(self.config_dir, self.share_dir, **self.args)
self.service = service_factory(self.config_dir, self.share_dir, **self.args)
if self.args.get('driver', 'openstack'):
@ -31,7 +36,7 @@ class Enough(object):
self.playbook = ansible_utils.Playbook(self.config_dir, self.share_dir)
self.ansible = ansible_utils.Ansible(self.config_dir, self.share_dir)
def clone(self, target_domain, target_clouds):
def clone(self, target_domain, target_clouds, assert_region_is_empty):
config_base = os.path.dirname(self.config_dir)
config_dir = f'{config_base}/{target_domain}'
assert not os.path.exists(config_dir)
@ -52,22 +57,80 @@ class Enough(object):
'driver': self.args['driver'],
}
clone = Enough(config_dir, self.share_dir, **kwargs)
assert clone.openstack.region_empty()
if assert_region_is_empty:
assert clone.openstack.region_empty()
return clone
def create_copy_host(self, original_volume, copy_volume):
self.set_args(name='copy-host')
def create_copy_host(self, name, original_volume, copy_volume):
self.set_args(name=name)
h = self.host.create_or_update()
if self.openstack.o.volume.list(
'-c', 'Status', '--format=value', '--name', copy_volume).strip() == 'available':
self.openstack.o.server.add.volume('copy-host', copy_volume)
self.openstack.o.server.add.volume(name, copy_volume)
self.playbook.run([
f'--private-key={self.dotenough.private_key()}',
'--extra-vars', f'encrypted_volume_name={original_volume}',
'--limit', f'localhost,{name}',
'copy-playbook.yml',
])
return h['ipv4']
def delete_copy_host(self, name):
self.set_args(name=[name])
self.host.delete()
def _rsync_copy_host(self, from_ip, to_ip):
sh.ssh('-i', self.dotenough.private_key(), f'root@{from_ip}',
'rsync', '-avHS', '--numeric-ids', '--delete',
'/srv/', f'root@{to_ip}:/srv/',
_tee=True,
_out=lambda x: log.info(x.strip()),
_err=lambda x: log.info(x.strip()),
_truncate_exc=False)
def clone_volume_from_snapshot(self, clone, snapshot):
(from_ip, to_ip, copy_from_volume) = self._clone_volume_from_snapshot_body(clone, snapshot)
self.rsync_copy_host(from_ip, to_ip)
self._clone_volume_from_snapshot_cleanup(clone, copy_from_volume)
def _clone_volume_from_snapshot_body(self, clone, snapshot):
#
# in the current region
# Step 1: create volume from snapshot
#
volume_name = re.sub('^\d\d\d\d-\d\d-\d\d-', '', snapshot)
copy_from_volume = f'copy-from-{volume_name}'
if not self.openstack.o.volume.list(
'-c', 'Name', '--format=value',
'--name', copy_from_volume).strip() == copy_from_volume:
self.openstack.o.volume.create('--snapshot', snapshot, copy_from_volume)
size = self.openstack.o.volume.show(
'-c', 'size', '--format=value', copy_from_volume).strip()
#
# in the current region
# Step 2: attach the volume to a host dedicated to copying the content of the volume
#
from_ip = self.create_copy_host('copy-from-host', volume_name, copy_from_volume)
#
# in the clone region
# Step 3: create an empty volume of the same size and attach it to a host dedicated
# to receive the copy
#
copy_to_volume = f'copy-to-{volume_name}'
if not clone.openstack.o.volume.list(
'-c', 'Name', '--format=value',
'--name', copy_to_volume).strip() == copy_to_volume:
clone.openstack.o.volume.create('--size', size, copy_to_volume)
to_ip = clone.create_copy_host('copy-to-host', volume_name, copy_to_volume)
return (from_ip, to_ip, copy_from_volume)
def _clone_volume_from_snapshot_cleanup(self, clone, copy_from_volume):
self.delete_copy_host('copy-from-host')
self.openstack.o.volume.delete(copy_from_volume)
clone.delete_copy_host('copy-to-host')
def destroy(self):
self.openstack.destroy_everything(None)
sh.rm('-r', self.config_dir)

13
enough/common/ansible_utils.py

@ -17,7 +17,11 @@ class Ansible(object):
self.share_dir = share_dir
def bake_ansible_playbook(self):
args = ['-i', 'inventory', '-i', 'development-inventory']
args = [
'-i', 'inventory',
'-i', 'development-inventory',
'--extra-vars', f'enough_domain_config_directory={self.config_dir}',
]
if self.config_dir != '.':
args.extend(['-i', f'{self.config_dir}/inventory'])
args.extend(['-i', f'{self.config_dir}/development-inventory'])
@ -32,7 +36,11 @@ class Ansible(object):
)
def ansible_inventory(self):
args = ['--list', '-i', 'inventory', '-i', 'development-inventory']
args = [
'--list',
'-i', 'inventory',
'-i', 'development-inventory',
]
if self.config_dir != '.':
args.extend(['-i', f'{self.config_dir}/inventory'])
args.extend(['-i', f'{self.config_dir}/development-inventory'])
@ -144,6 +152,7 @@ class Playbook(Ansible):
def bake(self):
args = [
'--extra-vars', f'enough_domain_config_directory={self.config_dir}',
'-i', f'{self.share_dir}/inventory',
]
if self.vault_password_option():

6
enough/common/dotenough.py

@ -18,6 +18,10 @@ class Hosts(object):
self.hosts = yaml.load(open(self.f).read())['all']['hosts']
else:
self.hosts = {}
return self
def get_ip(self, host):
return self.hosts.get(host, {}).get('ansible_host')
def save(self):
if not os.path.exists(self.d):
@ -35,7 +39,7 @@ class Hosts(object):
return [name for name in names if name not in self.hosts]
def create_or_update(self, name, ipv4):
if self.hosts.get(name, {}).get('ansible_host') != ipv4:
if self.get_ip(name) != ipv4:
self.hosts[name] = {'ansible_host': ipv4}
self.save()
return True

86
enough/common/openstack.py

@ -261,38 +261,60 @@ class OpenStack(OpenStackBase):
def destroy_everything(self, prefix):
leftovers = []
for snapshot in self.o.volume.snapshot.list('--format=value', '-c', 'Name', _iter=True):
snapshot = snapshot.strip()
if prefix is None or prefix in snapshot:
leftovers.append(f'snapshot({snapshot})')
self.o.volume.snapshot.delete(snapshot)
r = self.o.stack.list('--format=json', '-c', 'Stack Name', '-c', 'Stack Status')
for name, status in [(x["Stack Name"], x["Stack Status"]) for x in json.loads(r.stdout)]:
if prefix is None or prefix in name:
leftovers.append(f'stack({name})')
if not status.startswith('DELETE'):
self.o.stack.delete('--yes', '--wait', name)
for image in self.o.image.list('--private', '--format=value', '-c', 'Name', _iter=True):
image = image.strip()
if prefix is None or prefix in image:
leftovers.append(f'image({image})')
self.o.image.delete(image)
for volume in self.o.volume.list('--format=value', '-c', 'Name', _iter=True):
volume = volume.strip()
if prefix is None or prefix in volume:
leftovers.append(f'volume({volume})')
self.o.volume.delete(volume)
for network in self.o.network.list('--format=value', '-c', 'Name', _iter=True):
network = network.strip()
if network == 'Ext-Net':
continue
if prefix is None or prefix in network:
leftovers.append(f'network({network})')
self.o.network.delete(network)
def delete_snapshots():
for snapshot in self.o.volume.snapshot.list('--format=value', '-c', 'Name', _iter=True):
snapshot = snapshot.strip()
if prefix is None or prefix in snapshot:
leftovers.append(f'snapshot({snapshot})')
self.o.volume.snapshot.delete(snapshot)
def delete_stacks():
r = self.o.stack.list('--format=json', '-c', 'Stack Name', '-c', 'Stack Status')
for name, status in [(x["Stack Name"], x["Stack Status"])
for x in json.loads(r.stdout)]:
if prefix is None or prefix in name:
leftovers.append(f'stack({name})')
if status == 'DELETE_FAILED' or not status.startswith('DELETE'):
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()
if prefix is None or prefix in image:
leftovers.append(f'image({image})')
self.o.image.delete(image)
def delete_volumes():
for volume in self.o.volume.list('--format=value', '-c', 'Name', _iter=True):
volume = volume.strip()
if prefix is None or prefix in volume:
leftovers.append(f'volume({volume})')
self.o.volume.delete(volume)
def delete_networks():
for network in self.o.network.list('--format=value', '-c', 'Name', _iter=True):
network = network.strip()
if network == 'Ext-Net':
continue
if prefix is None or prefix in network:
leftovers.append(f'network({network})')
self.o.network.delete(network)
#
# There may be complex interdependencies between resources and
# no easy way to figure them out. For instance, say there
# exists a volume created from a snapshot of a volume created
# by a stack. The stack cannot be deleted befor the volume created
# from the snapshot is deleted. Because the snapshot cannot be deleted
# before all volumes created from it are deleted. And the volumes from
# which the snapshot are created cannot be deleted before all their
# snapshots are deleted.
#
for f in (delete_snapshots, delete_stacks, delete_images, delete_volumes, delete_networks):
try:
f()
except sh.ErrorReturnCode_1:
pass
if leftovers:
raise OpenStackLeftovers('scheduled removal of ' + ' '.join(leftovers))

5
inventory/02-all.yml

@ -24,3 +24,8 @@ enough-user-group:
children:
backup-service-group:
api-service-group:
copy-group:
hosts:
copy-from-host:
copy-to-host:

3
inventory/all.yml

@ -14,4 +14,5 @@ all-hosts:
wazuh-host:
weblate-host:
website-host:
copy-host:
copy-from-host:
copy-to-host:

5
molecule/infrastructure/roles/encrypted_device/tasks/main.yml

@ -35,6 +35,11 @@
register: result
changed_when: '"Changed" in result.stdout'
- name: "mkdir volume-keys"
file:
path: "{{ enough_domain_config_directory }}/volume-keys"
state: directory
- name: "save keyfile"
fetch:
src: /etc/cryptsetup/keyfile

42
tests/enough/common/test_init.py

@ -112,3 +112,45 @@ def test_create_copy_host(tmpdir):
o = OpenStack(settings.CONFIG_DIR, clouds)
# comment out the following line to re-use the content of the regions and save time
o.destroy_everything(None)
@pytest.mark.skipif('SKIP_OPENSTACK_INTEGRATION_TESTS' in os.environ,
reason='skip integration test')
def test_clone_volume_from_snapshot(tmpdir, mocker):
clone_clouds = os.path.expanduser("~/.enough/dev/clone-clouds.yml")
test_clouds = 'inventory/group_vars/all/clouds.yml'
try:
original = create_enough(tmpdir, test_clouds, 'backup')
original.set_args(
name='sample',
playbook=os.path.abspath(f'{original.config_dir}/encrypted-volume-playbook.yml'))
original.service.create_or_update()
ip = original.hosts.load().get_ip('sample-host')
sh.ssh('-i', original.dotenough.private_key(), f'debian@{ip}', 'touch', '/srv/STONE')
sh.ssh('-i', original.dotenough.private_key(), f'debian@{ip}', 'sync')
original.openstack.backup_create(['sample-volume'])
snapshot = f'{original.openstack.backup_date()}-sample-volume'
assert snapshot in original.openstack.o.volume.snapshot.list()
clone_domain = 'clone.com'
clone = original.clone(clone_domain, clone_clouds, assert_region_is_empty=False)
(from_ip, to_ip, copy_from_volume) = original._clone_volume_from_snapshot_body(
clone, snapshot)
assert sh.ssh('-i', original.dotenough.private_key(), f'root@{from_ip}',
'test', '-e', '/srv/STONE').exit_code == 0
assert sh.ssh('-i', clone.dotenough.private_key(), f'root@{to_ip}',
'test', '!', '-e', '/srv/STONE').exit_code == 0
original._rsync_copy_host(from_ip, to_ip)
assert sh.ssh('-i', clone.dotenough.private_key(), f'root@{to_ip}',
'test', '-e', '/srv/STONE').exit_code == 0
original._clone_volume_from_snapshot_cleanup(clone, copy_from_volume)
finally:
for clouds in (test_clouds, clone_clouds):
o = OpenStack(settings.CONFIG_DIR, clouds)
# comment out the following line to re-use the content of the regions and save time
o.destroy_everything(None)

6
tests/enough/common/test_init/backup/encrypted-volume-playbook.yml

@ -22,3 +22,9 @@
roles:
- role: encrypted_device
encrypted_device_mount_point: /srv
tasks:
- name: chown debian /srv
file:
path: /srv
owner: debian
Loading…
Cancel
Save