171 lines
6.9 KiB
Python
171 lines
6.9 KiB
Python
import json
|
|
from pathlib import Path
|
|
from subprocess import check_call, check_output, CalledProcessError
|
|
from uuid import uuid4
|
|
|
|
from charms.reactive import set_flag
|
|
from charmhelpers.core import hookenv
|
|
from charmhelpers.core import host
|
|
from charmhelpers.core import unitdata
|
|
from charmhelpers.contrib.openstack.vaultlocker import ( # noqa
|
|
retrieve_secret_id,
|
|
write_vaultlocker_conf,
|
|
)
|
|
from charmhelpers.contrib.storage.linux.utils import (
|
|
is_block_device,
|
|
is_device_mounted,
|
|
mkfs_xfs,
|
|
)
|
|
|
|
|
|
LOOP_ENVS = Path('/etc/vaultlocker/loop-envs')
|
|
|
|
|
|
class VaultLockerError(Exception):
|
|
"""
|
|
Wrapper for exceptions raised when configuring VaultLocker.
|
|
"""
|
|
def __init__(self, msg, *args, **kwargs):
|
|
super().__init__(msg.format(*args, **kwargs))
|
|
|
|
|
|
def encrypt_storage(storage_name, mountbase=None):
|
|
"""
|
|
Set up encryption for the given Juju storage entry, and optionally create
|
|
and mount XFS filesystems on the encrypted storage entry location(s).
|
|
|
|
Note that the storage entry **must** be defined with ``type: block``.
|
|
|
|
If ``mountbase`` is not given, the location(s) will not be formatted or
|
|
mounted. When interacting with or mounting the location(s) manually, the
|
|
name returned by :func:`decrypted_device` called on the storage entry's
|
|
location should be used in place of the raw location.
|
|
|
|
If the storage is defined as ``multiple``, the individual locations
|
|
will be mounted at ``{mountbase}/{storage_name}/{num}`` where ``{num}``
|
|
is based on the storage ID. Otherwise, the storage will mounted at
|
|
``{mountbase}/{storage_name}``.
|
|
"""
|
|
metadata = hookenv.metadata()
|
|
storage_metadata = metadata['storage'][storage_name]
|
|
if storage_metadata['type'] != 'block':
|
|
raise VaultLockerError('Cannot encrypt non-block storage: {}',
|
|
storage_name)
|
|
multiple = 'multiple' in storage_metadata
|
|
for storage_id in hookenv.storage_list():
|
|
if not storage_id.startswith(storage_name + '/'):
|
|
continue
|
|
storage_location = hookenv.storage_get('location', storage_id)
|
|
if mountbase and multiple:
|
|
mountpoint = Path(mountbase) / storage_id
|
|
elif mountbase:
|
|
mountpoint = Path(mountbase) / storage_name
|
|
else:
|
|
mountpoint = None
|
|
encrypt_device(storage_location, mountpoint)
|
|
set_flag('layer.vaultlocker.{}.ready'.format(storage_id))
|
|
set_flag('layer.vaultlocker.{}.ready'.format(storage_name))
|
|
|
|
|
|
def encrypt_device(device, mountpoint=None, uuid=None):
|
|
"""
|
|
Set up encryption for the given block device, and optionally create and
|
|
mount an XFS filesystem on the encrypted device.
|
|
|
|
If ``mountpoint`` is not given, the device will not be formatted or
|
|
mounted. When interacting with or mounting the device manually, the
|
|
name returned by :func:`decrypted_device` called on the device name
|
|
should be used in place of the raw device name.
|
|
"""
|
|
if not is_block_device(device):
|
|
raise VaultLockerError('Cannot encrypt non-block device: {}', device)
|
|
if is_device_mounted(device):
|
|
raise VaultLockerError('Cannot encrypt mounted device: {}', device)
|
|
hookenv.log('Encrypting device: {}'.format(device))
|
|
if uuid is None:
|
|
uuid = str(uuid4())
|
|
try:
|
|
check_call(['vaultlocker', 'encrypt', '--uuid', uuid, device])
|
|
unitdata.kv().set('layer.vaultlocker.uuids.{}'.format(device), uuid)
|
|
if mountpoint:
|
|
mapped_device = decrypted_device(device)
|
|
hookenv.log('Creating filesystem on {} ({})'.format(mapped_device,
|
|
device))
|
|
# If this fails, it's probalby due to the size of the loopback
|
|
# backing file that is defined by the `dd`.
|
|
mkfs_xfs(mapped_device)
|
|
Path(mountpoint).mkdir(mode=0o755, parents=True, exist_ok=True)
|
|
hookenv.log('Mounting filesystem for {} ({}) at {}'
|
|
''.format(mapped_device, device, mountpoint))
|
|
host.mount(mapped_device, mountpoint, filesystem='xfs')
|
|
host.fstab_add(mapped_device, mountpoint, 'xfs', ','.join([
|
|
"defaults",
|
|
"nofail",
|
|
"x-systemd.requires=vaultlocker-decrypt@{uuid}.service".format(
|
|
uuid=uuid,
|
|
),
|
|
"comment=vaultlocker",
|
|
]))
|
|
except (CalledProcessError, OSError) as e:
|
|
raise VaultLockerError('Error configuring VaultLocker') from e
|
|
|
|
|
|
def decrypted_device(device):
|
|
"""
|
|
Returns the mapped device name for the decrypted version of the encrypted
|
|
device.
|
|
|
|
This mapped device name is what should be used for mounting the device.
|
|
"""
|
|
uuid = unitdata.kv().get('layer.vaultlocker.uuids.{}'.format(device))
|
|
if not uuid:
|
|
return None
|
|
return '/dev/mapper/crypt-{uuid}'.format(uuid=uuid)
|
|
|
|
|
|
def create_encrypted_loop_mount(mount_path, block_size='1M', block_count=20,
|
|
backing_file=None):
|
|
"""
|
|
Creates a persistent loop device, encrypts it, formats it as XFS, and
|
|
mounts it at the given `mount_path`.
|
|
|
|
A backing file will be created under `/var/lib/vaultlocker/backing_files`,
|
|
in a UUID named file, according to `block_size` and `block_count`
|
|
parameters, which map to `bs` and `count` of the `dd` command. Note that
|
|
the backing file must be a bit over 16M to allow for the XFS file system
|
|
plus some additional metadata needed for the encryption. It is not
|
|
recommended to go below the default of 20M (20 blocks, 1M each).
|
|
|
|
The `backing_file` parameter can be used to change the location where the
|
|
backing file is created.
|
|
"""
|
|
uuid = str(uuid4())
|
|
if backing_file is None:
|
|
backing_file = Path('/var/lib/vaultlocker/backing_files') / uuid
|
|
backing_file.parent.mkdir(parents=True, exist_ok=True)
|
|
else:
|
|
backing_file = Path(backing_file)
|
|
if backing_file.exists():
|
|
raise VaultLockerError('Backing file already exists: {}',
|
|
backing_file)
|
|
|
|
try:
|
|
# ensure loop devices are enabled
|
|
check_call(['modprobe', 'loop'])
|
|
# create the backing file filled with random data
|
|
check_call(['dd', 'if=/dev/urandom', 'of={}'.format(backing_file),
|
|
'bs=8M', 'count=4'])
|
|
# claim an unused loop device
|
|
output = check_output(['losetup', '--show', '-f', str(backing_file)])
|
|
device_name = output.decode('utf8').strip()
|
|
# encrypt the new loop device
|
|
encrypt_device(device_name, str(mount_path), uuid)
|
|
# setup the service to ensure loop device is restored after reboot
|
|
(LOOP_ENVS / uuid).write_text(''.join([
|
|
'BACK_FILE={}\n'.format(backing_file),
|
|
]))
|
|
check_call(['systemctl', 'enable',
|
|
'vaultlocker-loop@{}.service'.format(uuid)])
|
|
except (CalledProcessError, OSError) as e:
|
|
raise VaultLockerError('Error configuring VaultLocker') from e
|