Merge "Add support for block device encryption"

This commit is contained in:
Zuul 2018-05-15 09:56:23 +00:00 committed by Gerrit Code Review
commit b03a8bc5bd
21 changed files with 314 additions and 52 deletions

2
.gitignore vendored
View File

@ -8,3 +8,5 @@ tags
*.pyc *.pyc
tests/cirros-* tests/cirros-*
func-results.json func-results.json
.settings
**/__pycache__

View File

@ -6,6 +6,7 @@ global
group haproxy group haproxy
spread-checks 0 spread-checks 0
stats socket /var/run/haproxy/admin.sock mode 600 level admin stats socket /var/run/haproxy/admin.sock mode 600 level admin
stats socket /var/run/haproxy/operator.sock mode 600 level operator
stats timeout 2m stats timeout 2m
defaults defaults

View File

@ -40,6 +40,17 @@ options:
description: | description: |
If true, charm will attempt to unmount and overwrite existing and in-use If true, charm will attempt to unmount and overwrite existing and in-use
block-devices (WARNING). block-devices (WARNING).
ephemeral-unmount:
type: string
default:
description: |
Cloud instances provide ephermeral storage which is normally mounted
on /mnt.
.
Setting this option to the path of the ephemeral mountpoint will force
an unmount of the corresponding device so that it can be used as a swift
storage device. This is useful for testing purposes (cloud deployment
is not a typical use case).
zone: zone:
default: 1 default: 1
type: int type: int
@ -189,3 +200,9 @@ options:
be loaded, the charm will fail to install. be loaded, the charm will fail to install.
type: boolean type: boolean
default: False default: False
encrypt:
default: false
type: boolean
description: |
Encrypt block devices used by swift using dm-crypt, making use of
vault for encryption key management; requires a relation to vault.

View File

@ -0,0 +1 @@
storage.bootstrap

View File

@ -0,0 +1 @@
storage.bootstrap

View File

@ -0,0 +1 @@
swift_storage_hooks.py

View File

@ -0,0 +1 @@
swift_storage_hooks.py

View File

@ -0,0 +1 @@
swift_storage_hooks.py

View File

@ -0,0 +1 @@
swift_storage_hooks.py

8
hooks/storage.bootstrap Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
if ! dpkg -s swift > /dev/null 2>&1; then
juju-log "Swift not yet installed."
exit 0
fi
./hooks/storage.real

1
hooks/storage.real Symbolic link
View File

@ -0,0 +1 @@
swift_storage_hooks.py

View File

@ -14,16 +14,20 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import base64
import copy
import json
import os import os
import shutil import shutil
import sys import sys
import socket
import subprocess
import tempfile import tempfile
from lib.swift_storage_utils import ( from lib.swift_storage_utils import (
PACKAGES, PACKAGES,
RESTART_MAP, RESTART_MAP,
SWIFT_SVCS, SWIFT_SVCS,
determine_block_devices,
do_openstack_upgrade, do_openstack_upgrade,
ensure_swift_directories, ensure_swift_directories,
fetch_swift_rings, fetch_swift_rings,
@ -53,16 +57,20 @@ from charmhelpers.core.hookenv import (
relations_of_type, relations_of_type,
status_set, status_set,
ingress_address, ingress_address,
DEBUG,
) )
from charmhelpers.fetch import ( from charmhelpers.fetch import (
apt_install, apt_install,
apt_update, apt_update,
add_source,
filter_installed_packages filter_installed_packages
) )
from charmhelpers.core.host import ( from charmhelpers.core.host import (
add_to_updatedb_prunepath, add_to_updatedb_prunepath,
rsync, rsync,
write_file,
umount,
) )
from charmhelpers.core.sysctl import create as create_sysctl from charmhelpers.core.sysctl import create as create_sysctl
@ -81,9 +89,12 @@ from charmhelpers.contrib.network.ip import (
from charmhelpers.contrib.network import ufw from charmhelpers.contrib.network import ufw
from charmhelpers.contrib.charmsupport import nrpe from charmhelpers.contrib.charmsupport import nrpe
from charmhelpers.contrib.hardening.harden import harden from charmhelpers.contrib.hardening.harden import harden
from charmhelpers.core.unitdata import kv
from distutils.dir_util import mkpath from distutils.dir_util import mkpath
import charmhelpers.contrib.openstack.vaultlocker as vaultlocker
hooks = Hooks() hooks = Hooks()
CONFIGS = register_configs() CONFIGS = register_configs()
NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins' NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
@ -173,8 +184,6 @@ def install():
apt_update() apt_update()
apt_install(PACKAGES, fatal=True) apt_install(PACKAGES, fatal=True)
initialize_ufw() initialize_ufw()
status_set('maintenance', 'Setting up storage')
setup_storage()
ensure_swift_directories() ensure_swift_directories()
@ -186,6 +195,10 @@ def config_changed():
initialize_ufw() initialize_ufw()
else: else:
ufw.disable() ufw.disable()
if config('ephemeral-unmount'):
umount(config('ephemeral-unmount'), persist=True)
if config('prefer-ipv6'): if config('prefer-ipv6'):
status_set('maintenance', 'Configuring ipv6') status_set('maintenance', 'Configuring ipv6')
assert_charm_supports_ipv6() assert_charm_supports_ipv6()
@ -198,10 +211,9 @@ def config_changed():
status_set('maintenance', 'Running openstack upgrade') status_set('maintenance', 'Running openstack upgrade')
do_openstack_upgrade(configs=CONFIGS) do_openstack_upgrade(configs=CONFIGS)
setup_storage() install_vaultlocker()
for rid in relation_ids('swift-storage'): configure_storage()
swift_storage_relation_joined(rid=rid)
CONFIGS.write_all() CONFIGS.write_all()
@ -216,6 +228,17 @@ def config_changed():
add_to_updatedb_prunepath(STORAGE_MOUNT_PATH) add_to_updatedb_prunepath(STORAGE_MOUNT_PATH)
def install_vaultlocker():
"""Determine whether vaultlocker is required and install"""
if config('encrypt'):
pkgs = ['vaultlocker', 'python-hvac']
installed = len(filter_installed_packages(pkgs)) == 0
if not installed:
add_source('ppa:openstack-charmers/vaultlocker')
apt_update(fatal=True)
apt_install(pkgs, fatal=True)
@hooks.hook('upgrade-charm') @hooks.hook('upgrade-charm')
@harden() @harden()
def upgrade_charm(): def upgrade_charm():
@ -227,6 +250,10 @@ def upgrade_charm():
@hooks.hook() @hooks.hook()
def swift_storage_relation_joined(rid=None): def swift_storage_relation_joined(rid=None):
if config('encrypt') and not vaultlocker.vault_relation_complete():
log('Encryption configured and vault not ready, deferring',
level=DEBUG)
return
rel_settings = { rel_settings = {
'zone': config('zone'), 'zone': config('zone'),
'object_port': config('object-server-port'), 'object_port': config('object-server-port'),
@ -234,7 +261,8 @@ def swift_storage_relation_joined(rid=None):
'account_port': config('account-server-port'), 'account_port': config('account-server-port'),
} }
devs = determine_block_devices() or [] db = kv()
devs = db.get('prepared-devices', [])
devs = [os.path.basename(d) for d in devs] devs = [os.path.basename(d) for d in devs]
rel_settings['device'] = ':'.join(devs) rel_settings['device'] = ':'.join(devs)
# Keep a reference of devices we are adding to the ring # Keep a reference of devices we are adding to the ring
@ -272,6 +300,34 @@ def swift_storage_relation_departed():
revoke_access(removed_client, port) revoke_access(removed_client, port)
@hooks.hook('secrets-storage-relation-joined')
def secrets_storage_joined(relation_id=None):
relation_set(relation_id=relation_id,
secret_backend='charm-vaultlocker',
isolated=True,
access_address=get_relation_ip('secrets-storage'),
hostname=socket.gethostname())
@hooks.hook('secrets-storage-relation-changed')
def secrets_storage_changed():
vault_ca = relation_get('vault_ca')
if vault_ca:
vault_ca = base64.decodestring(json.loads(vault_ca).encode())
write_file('/usr/local/share/ca-certificates/vault-ca.crt',
vault_ca, perms=0o644)
subprocess.check_call(['update-ca-certificates', '--fresh'])
configure_storage()
@hooks.hook('storage.real')
def configure_storage():
setup_storage(config('encrypt'))
for rid in relation_ids('swift-storage'):
swift_storage_relation_joined(rid=rid)
@hooks.hook('nrpe-external-master-relation-joined') @hooks.hook('nrpe-external-master-relation-joined')
@hooks.hook('nrpe-external-master-relation-changed') @hooks.hook('nrpe-external-master-relation-changed')
def update_nrpe_config(): def update_nrpe_config():
@ -318,7 +374,10 @@ def main():
hooks.execute(sys.argv) hooks.execute(sys.argv)
except UnregisteredHookError as e: except UnregisteredHookError as e:
log('Unknown hook {} - skipping.'.format(e)) log('Unknown hook {} - skipping.'.format(e))
set_os_workload_status(CONFIGS, REQUIRED_INTERFACES, required_interfaces = copy.deepcopy(REQUIRED_INTERFACES)
if config('encrypt'):
required_interfaces['vault'] = ['secrets-storage']
set_os_workload_status(CONFIGS, required_interfaces,
charm_func=assess_status) charm_func=assess_status)
os_application_version_set(VERSION_PACKAGE) os_application_version_set(VERSION_PACKAGE)

View File

@ -4,6 +4,7 @@ import re
import subprocess import subprocess
import shutil import shutil
import tempfile import tempfile
import uuid
from subprocess import check_call, call, CalledProcessError, check_output from subprocess import check_call, call, CalledProcessError, check_output
@ -54,6 +55,8 @@ from charmhelpers.core.hookenv import (
relation_ids, relation_ids,
iter_units_for_relation_name, iter_units_for_relation_name,
ingress_address, ingress_address,
storage_list,
storage_get,
) )
from charmhelpers.contrib.network import ufw from charmhelpers.contrib.network import ufw
@ -62,6 +65,7 @@ from charmhelpers.contrib.network.ip import get_host_ip
from charmhelpers.contrib.storage.linux.utils import ( from charmhelpers.contrib.storage.linux.utils import (
is_block_device, is_block_device,
is_device_mounted, is_device_mounted,
mkfs_xfs,
) )
from charmhelpers.contrib.storage.linux.loopback import ( from charmhelpers.contrib.storage.linux.loopback import (
@ -84,6 +88,10 @@ from charmhelpers.core.decorators import (
retry_on_exception, retry_on_exception,
) )
import charmhelpers.contrib.openstack.vaultlocker as vaultlocker
from charmhelpers.core.unitdata import kv
PACKAGES = [ PACKAGES = [
'swift', 'swift-account', 'swift-container', 'swift-object', 'swift', 'swift-account', 'swift-container', 'swift-object',
'xfsprogs', 'gdisk', 'lvm2', 'python-jinja2', 'python-psutil', 'xfsprogs', 'gdisk', 'lvm2', 'python-jinja2', 'python-psutil',
@ -162,11 +170,14 @@ def register_configs():
[SwiftStorageContext()]) [SwiftStorageContext()])
configs.register('/etc/rsync-juju.d/050-swift-storage.conf', configs.register('/etc/rsync-juju.d/050-swift-storage.conf',
[RsyncContext(), SwiftStorageServerContext()]) [RsyncContext(), SwiftStorageServerContext()])
# NOTE: add VaultKVContext so interface status can be assessed
for server in ['account', 'object', 'container']: for server in ['account', 'object', 'container']:
configs.register('/etc/swift/%s-server.conf' % server, configs.register('/etc/swift/%s-server.conf' % server,
[SwiftStorageServerContext(), [SwiftStorageServerContext(),
context.BindHostContext(), context.BindHostContext(),
context.WorkerConfigContext()]), context.WorkerConfigContext(),
vaultlocker.VaultKVContext(
vaultlocker.VAULTLOCKER_BACKEND)]),
return configs return configs
@ -269,6 +280,12 @@ def determine_block_devices():
else: else:
bdevs = block_device.split(' ') bdevs = block_device.split(' ')
# List storage instances for the 'block-devices'
# store declared for this charm too, and add
# their block device paths to the list.
storage_ids = storage_list('block-devices')
bdevs.extend((storage_get('location', s) for s in storage_ids))
bdevs = list(set(bdevs)) bdevs = list(set(bdevs))
# attempt to ensure block devices, but filter out missing devs # attempt to ensure block devices, but filter out missing devs
_none = ['None', 'none'] _none = ['None', 'none']
@ -279,19 +296,6 @@ def determine_block_devices():
return valid_bdevs return valid_bdevs
def mkfs_xfs(bdev, force=False):
"""Format device with XFS filesystem.
By default this should fail if the device already has a filesystem on it.
"""
cmd = ['mkfs.xfs']
if force:
cmd.append("-f")
cmd += ['-i', 'size=1024', bdev]
check_call(cmd)
def devstore_safe_load(devstore): def devstore_safe_load(devstore):
"""Attempt to decode json data and return None if an error occurs while """Attempt to decode json data and return None if an error occurs while
also printing a log. also printing a log.
@ -446,21 +450,73 @@ def ensure_devs_tracked():
is_device_in_ring(dev, skip_rel_check=True) is_device_in_ring(dev, skip_rel_check=True)
def setup_storage(): def setup_storage(encrypt=False):
# Preflight check vault relation if encryption is enabled
vault_kv = vaultlocker.VaultKVContext(vaultlocker.VAULTLOCKER_BACKEND)
context = vault_kv()
if encrypt and not vault_kv.complete:
log("Encryption requested but vault relation not complete",
level=DEBUG)
return
elif encrypt and vault_kv.complete:
# NOTE: only write vaultlocker configuration once relation is complete
# otherwise we run the chance of an empty configuration file
# being installed on a machine with other vaultlocker based
# services
vaultlocker.write_vaultlocker_conf(context, priority=90)
# Ensure /srv/node exists just in case no disks # Ensure /srv/node exists just in case no disks
# are detected and used. # are detected and used.
mkdir(os.path.join('/srv', 'node'), mkdir(os.path.join('/srv', 'node'),
owner='swift', group='swift', owner='swift', group='swift',
perms=0o755) perms=0o755)
reformat = str(config('overwrite')).lower() == "true" reformat = str(config('overwrite')).lower() == "true"
db = kv()
prepared_devices = db.get('prepared-devices', [])
for dev in determine_block_devices(): for dev in determine_block_devices():
if dev in prepared_devices:
log('Device {} already processed by charm,'
' skipping'.format(dev))
continue
if is_device_in_ring(os.path.basename(dev)): if is_device_in_ring(os.path.basename(dev)):
log("Device '%s' already in the ring - ignoring" % (dev)) log("Device '%s' already in the ring - ignoring" % (dev))
# NOTE: record existing use of device dealing with
# upgrades from older versions of charms without
# this feature
prepared_devices.append(dev)
db.set('prepared-devices', prepared_devices)
db.flush()
continue
# NOTE: this deals with a dm-crypt'ed block device already in
# use
if is_device_mounted(dev):
log("Device '{}' is already mounted, ignoring".format(dev))
continue continue
if reformat: if reformat:
clean_storage(dev) clean_storage(dev)
loopback_device = is_mapped_loopback_device(dev)
options = None
if encrypt and not loopback_device:
dev_uuid = str(uuid.uuid4())
check_call(['vaultlocker', 'encrypt',
'--uuid', dev_uuid,
dev])
dev = '/dev/mapper/crypt-{}'.format(dev_uuid)
options = ','.join([
"defaults",
"nofail",
("x-systemd.requires="
"vaultlocker-decrypt@{uuid}.service".format(uuid=dev_uuid)),
"comment=vaultlocker",
])
try: try:
# If not cleaned and in use, mkfs should fail. # If not cleaned and in use, mkfs should fail.
mkfs_xfs(dev, force=reformat) mkfs_xfs(dev, force=reformat)
@ -475,8 +531,6 @@ def setup_storage():
_mp = os.path.join('/srv', 'node', basename) _mp = os.path.join('/srv', 'node', basename)
mkdir(_mp, owner='swift', group='swift') mkdir(_mp, owner='swift', group='swift')
options = None
loopback_device = is_mapped_loopback_device(dev)
mountpoint = '/srv/node/%s' % basename mountpoint = '/srv/node/%s' % basename
if loopback_device: if loopback_device:
# If an exiting fstab entry exists using the image file as the # If an exiting fstab entry exists using the image file as the
@ -497,6 +551,12 @@ def setup_storage():
check_call(['chown', '-R', 'swift:swift', mountpoint]) check_call(['chown', '-R', 'swift:swift', mountpoint])
check_call(['chmod', '-R', '0755', mountpoint]) check_call(['chmod', '-R', '0755', mountpoint])
# NOTE: record preparation of device - this will be used when
# providing block device configuration for ring builders.
prepared_devices.append(dev)
db.set('prepared-devices', prepared_devices)
db.flush()
@retry_on_exception(3, base_delay=2, exc_type=CalledProcessError) @retry_on_exception(3, base_delay=2, exc_type=CalledProcessError)
def fetch_swift_rings(rings_url): def fetch_swift_rings(rings_url):

View File

@ -28,3 +28,12 @@ provides:
scope: container scope: container
swift-storage: swift-storage:
interface: swift interface: swift
requires:
secrets-storage:
interface: vault-kv
storage:
block-devices:
type: block
multiple:
range: 0-
minimum-size: 1G

View File

@ -0,0 +1,6 @@
# vaultlocker configuration from swift-proxy charm
[vault]
url = {{ vault_url }}
approle = {{ role_id }}
backend = {{ secret_backend }}
secret_id = {{ secret_id }}

View File

@ -5,12 +5,12 @@ coverage>=3.6
mock>=1.2 mock>=1.2
flake8>=2.2.4,<=2.4.1 flake8>=2.2.4,<=2.4.1
os-testr>=0.4.1 os-testr>=0.4.1
charm-tools>=2.0.0 charm-tools>=2.0.0;python_version=='2.7' # cheetah templates aren't availble in Python 3+
requests==2.6.0 requests==2.6.0
# BEGIN: Amulet OpenStack Charm Helper Requirements # BEGIN: Amulet OpenStack Charm Helper Requirements
# Liberty client lower constraints # Liberty client lower constraints
amulet>=1.14.3,<2.0 amulet>=1.14.3,<2.0
bundletester>=0.6.1,<1.0 bundletester>=0.6.1,<1.0;python_version=='2.7' # cheetah templates aren't availble in Python 3+
python-ceilometerclient>=1.5.0 python-ceilometerclient>=1.5.0
python-cinderclient>=1.4.0 python-cinderclient>=1.4.0
python-glanceclient>=1.1.0 python-glanceclient>=1.1.0

View File

@ -96,6 +96,7 @@ class SwiftStorageBasicDeployment(OpenStackAmuletDeployment):
'zone': '1', 'zone': '1',
'block-device': 'vdb', 'block-device': 'vdb',
'overwrite': 'true', 'overwrite': 'true',
'ephemeral-unmount': '/mnt',
} }
pxc_config = { pxc_config = {
'innodb-buffer-pool-size': '256M', 'innodb-buffer-pool-size': '256M',

View File

@ -26,6 +26,11 @@ basepython = python3.5
deps = -r{toxinidir}/requirements.txt deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt -r{toxinidir}/test-requirements.txt
[testenv:py36]
basepython = python3.6
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
[testenv:pep8] [testenv:pep8]
basepython = python2.7 basepython = python2.7
deps = -r{toxinidir}/requirements.txt deps = -r{toxinidir}/requirements.txt

View File

@ -18,7 +18,7 @@ import json
import uuid import uuid
import tempfile import tempfile
from test_utils import CharmTestCase, patch_open from test_utils import CharmTestCase, TestKV, patch_open
with patch('hooks.charmhelpers.contrib.hardening.harden.harden') as mock_dec: with patch('hooks.charmhelpers.contrib.hardening.harden.harden') as mock_dec:
mock_dec.side_effect = (lambda *dargs, **dkwargs: lambda f: mock_dec.side_effect = (lambda *dargs, **dkwargs: lambda f:
@ -47,7 +47,6 @@ TO_PATCH = [
'configure_installation_source', 'configure_installation_source',
'openstack_upgrade_available', 'openstack_upgrade_available',
# swift_storage_utils # swift_storage_utils
'determine_block_devices',
'do_openstack_upgrade', 'do_openstack_upgrade',
'ensure_swift_directories', 'ensure_swift_directories',
'execd_preinstall', 'execd_preinstall',
@ -66,6 +65,7 @@ TO_PATCH = [
'ufw', 'ufw',
'setup_ufw', 'setup_ufw',
'revoke_access', 'revoke_access',
'kv',
] ]
@ -93,6 +93,8 @@ class SwiftStorageRelationsTests(CharmTestCase):
self.config.side_effect = self.test_config.get self.config.side_effect = self.test_config.get
self.relation_get.side_effect = self.test_relation.get self.relation_get.side_effect = self.test_relation.get
self.get_relation_ip.return_value = '10.10.10.2' self.get_relation_ip.return_value = '10.10.10.2'
self.test_kv = TestKV()
self.kv.return_value = self.test_kv
@patch.object(hooks, 'add_ufw_gre_rule', lambda *args: None) @patch.object(hooks, 'add_ufw_gre_rule', lambda *args: None)
def test_prunepath(self): def test_prunepath(self):
@ -108,8 +110,6 @@ class SwiftStorageRelationsTests(CharmTestCase):
) )
self.assertTrue(self.apt_update.called) self.assertTrue(self.apt_update.called)
self.apt_install.assert_called_with(PACKAGES, fatal=True) self.apt_install.assert_called_with(PACKAGES, fatal=True)
self.assertTrue(self.setup_storage.called)
self.assertTrue(self.execd_preinstall.called) self.assertTrue(self.execd_preinstall.called)
@patch.object(hooks, 'add_ufw_gre_rule', lambda *args: None) @patch.object(hooks, 'add_ufw_gre_rule', lambda *args: None)
@ -197,7 +197,7 @@ class SwiftStorageRelationsTests(CharmTestCase):
kvstore = mock_kvstore.return_value kvstore = mock_kvstore.return_value
kvstore.__enter__.return_value = kvstore kvstore.__enter__.return_value = kvstore
kvstore.get.return_value = None kvstore.get.return_value = None
self.determine_block_devices.return_value = ['/dev/vdb'] self.test_kv.set('prepared-devices', ['/dev/vdb'])
hooks.swift_storage_relation_joined() hooks.swift_storage_relation_joined()
@ -254,8 +254,8 @@ class SwiftStorageRelationsTests(CharmTestCase):
test_uuid = uuid.uuid4() test_uuid = uuid.uuid4()
test_environ = {'JUJU_ENV_UUID': test_uuid} test_environ = {'JUJU_ENV_UUID': test_uuid}
mock_environ.get.side_effect = test_environ.get mock_environ.get.side_effect = test_environ.get
self.determine_block_devices.return_value = ['/dev/vdb', '/dev/vdc', self.test_kv.set('prepared-devices', ['/dev/vdb', '/dev/vdc',
'/dev/vdd'] '/dev/vdd'])
mock_local_unit.return_value = 'test/0' mock_local_unit.return_value = 'test/0'
kvstore = mock_kvstore.return_value kvstore = mock_kvstore.return_value
kvstore.__enter__.return_value = kvstore kvstore.__enter__.return_value = kvstore
@ -298,8 +298,8 @@ class SwiftStorageRelationsTests(CharmTestCase):
test_uuid = uuid.uuid4() test_uuid = uuid.uuid4()
test_environ = {'JUJU_ENV_UUID': test_uuid} test_environ = {'JUJU_ENV_UUID': test_uuid}
mock_environ.get.side_effect = test_environ.get mock_environ.get.side_effect = test_environ.get
self.determine_block_devices.return_value = ['/dev/vdb', '/dev/vdc', self.test_kv.set('prepared-devices', ['/dev/vdb', '/dev/vdc',
'/dev/vdd'] '/dev/vdd'])
mock_local_unit.return_value = 'test/0' mock_local_unit.return_value = 'test/0'
kvstore = mock_kvstore.return_value kvstore = mock_kvstore.return_value
kvstore.__enter__.return_value = kvstore kvstore.__enter__.return_value = kvstore

View File

@ -17,7 +17,7 @@ import tempfile
from collections import namedtuple from collections import namedtuple
from mock import call, patch, MagicMock from mock import call, patch, MagicMock
from test_utils import CharmTestCase, patch_open from test_utils import CharmTestCase, TestKV, patch_open
import lib.swift_storage_utils as swift_utils import lib.swift_storage_utils as swift_utils
@ -50,6 +50,8 @@ TO_PATCH = [
'iter_units_for_relation_name', 'iter_units_for_relation_name',
'ingress_address', 'ingress_address',
'relation_ids', 'relation_ids',
'vaultlocker',
'kv',
] ]
@ -104,11 +106,14 @@ TARGET SOURCE FSTYPE OPTIONS
""" """
class SwiftStorageUtilsTests(CharmTestCase): class SwiftStorageUtilsTests(CharmTestCase):
def setUp(self): def setUp(self):
super(SwiftStorageUtilsTests, self).setUp(swift_utils, TO_PATCH) super(SwiftStorageUtilsTests, self).setUp(swift_utils, TO_PATCH)
self.config.side_effect = self.test_config.get self.config.side_effect = self.test_config.get
self.test_kv = TestKV()
self.kv.return_value = self.test_kv
def test_ensure_swift_directories(self): def test_ensure_swift_directories(self):
with patch('os.path.isdir') as isdir: with patch('os.path.isdir') as isdir:
@ -229,18 +234,6 @@ class SwiftStorageUtilsTests(CharmTestCase):
self.assertTrue(_find.called) self.assertTrue(_find.called)
self.assertEqual(result, []) self.assertEqual(result, [])
def test_mkfs_xfs(self):
swift_utils.mkfs_xfs('/dev/sdb')
self.check_call.assert_called_with(
['mkfs.xfs', '-i', 'size=1024', '/dev/sdb']
)
def test_mkfs_xfs_force(self):
swift_utils.mkfs_xfs('/dev/sdb', force=True)
self.check_call.assert_called_with(
['mkfs.xfs', '-f', '-i', 'size=1024', '/dev/sdb']
)
@patch.object(swift_utils.charmhelpers.core.fstab, "Fstab") @patch.object(swift_utils.charmhelpers.core.fstab, "Fstab")
@patch.object(swift_utils, 'is_device_in_ring') @patch.object(swift_utils, 'is_device_in_ring')
@patch.object(swift_utils, 'clean_storage') @patch.object(swift_utils, 'clean_storage')
@ -249,6 +242,7 @@ class SwiftStorageUtilsTests(CharmTestCase):
def test_setup_storage_no_overwrite(self, determine, mkfs, clean, def test_setup_storage_no_overwrite(self, determine, mkfs, clean,
mock_is_device_in_ring, mock_Fstab): mock_is_device_in_ring, mock_Fstab):
mock_is_device_in_ring.return_value = False mock_is_device_in_ring.return_value = False
self.is_device_mounted.return_value = False
determine.return_value = ['/dev/vdb'] determine.return_value = ['/dev/vdb']
swift_utils.setup_storage() swift_utils.setup_storage()
self.assertFalse(clean.called) self.assertFalse(clean.called)
@ -260,6 +254,8 @@ class SwiftStorageUtilsTests(CharmTestCase):
perms=0o755), perms=0o755),
call('/srv/node/vdb', group='swift', owner='swift') call('/srv/node/vdb', group='swift', owner='swift')
]) ])
self.assertEqual(self.test_kv.get('prepared-devices'),
['/dev/vdb'])
@patch.object(swift_utils, 'is_device_in_ring') @patch.object(swift_utils, 'is_device_in_ring')
@patch.object(swift_utils, 'clean_storage') @patch.object(swift_utils, 'clean_storage')
@ -270,6 +266,7 @@ class SwiftStorageUtilsTests(CharmTestCase):
self.test_config.set('overwrite', True) self.test_config.set('overwrite', True)
mock_is_device_in_ring.return_value = False mock_is_device_in_ring.return_value = False
self.is_mapped_loopback_device.return_value = None self.is_mapped_loopback_device.return_value = None
self.is_device_mounted.return_value = False
determine.return_value = ['/dev/vdb'] determine.return_value = ['/dev/vdb']
swift_utils.setup_storage() swift_utils.setup_storage()
clean.assert_called_with('/dev/vdb') clean.assert_called_with('/dev/vdb')
@ -288,6 +285,8 @@ class SwiftStorageUtilsTests(CharmTestCase):
perms=0o755), perms=0o755),
call('/srv/node/vdb', group='swift', owner='swift') call('/srv/node/vdb', group='swift', owner='swift')
]) ])
self.assertEqual(self.test_kv.get('prepared-devices'),
['/dev/vdb'])
@patch.object(swift_utils, 'is_device_in_ring') @patch.object(swift_utils, 'is_device_in_ring')
@patch.object(swift_utils, 'determine_block_devices') @patch.object(swift_utils, 'determine_block_devices')
@ -304,6 +303,71 @@ class SwiftStorageUtilsTests(CharmTestCase):
swift_utils.setup_storage() swift_utils.setup_storage()
self.assertEqual(self.check_call.call_count, 0) self.assertEqual(self.check_call.call_count, 0)
@patch.object(swift_utils, "uuid")
@patch.object(swift_utils, "vaultlocker")
@patch.object(swift_utils.charmhelpers.core.fstab, "Fstab")
@patch.object(swift_utils, 'is_device_in_ring')
@patch.object(swift_utils, 'clean_storage')
@patch.object(swift_utils, 'mkfs_xfs')
@patch.object(swift_utils, 'determine_block_devices')
def test_setup_storage_encrypt(self, determine, mkfs, clean,
mock_is_device_in_ring, mock_Fstab,
mock_vaultlocker, mock_uuid):
mock_context = MagicMock()
mock_context.complete = True
mock_context.return_value = 'test_context'
mock_vaultlocker.VaultKVContext.return_value = mock_context
mock_uuid.uuid4.return_value = '7c3ff7c8-fd20-4dca-9be6-6f44f213d3fe'
mock_is_device_in_ring.return_value = False
self.is_device_mounted.return_value = False
self.is_mapped_loopback_device.return_value = None
determine.return_value = ['/dev/vdb']
swift_utils.setup_storage(encrypt=True)
self.assertFalse(clean.called)
calls = [
call(['vaultlocker', 'encrypt',
'--uuid', '7c3ff7c8-fd20-4dca-9be6-6f44f213d3fe',
'/dev/vdb']),
call(['chown', '-R', 'swift:swift',
'/srv/node/crypt-7c3ff7c8-fd20-4dca-9be6-6f44f213d3fe']),
call(['chmod', '-R', '0755',
'/srv/node/crypt-7c3ff7c8-fd20-4dca-9be6-6f44f213d3fe'])
]
self.check_call.assert_has_calls(calls)
self.mkdir.assert_has_calls([
call('/srv/node', owner='swift', group='swift',
perms=0o755),
call('/srv/node/crypt-7c3ff7c8-fd20-4dca-9be6-6f44f213d3fe',
group='swift', owner='swift')
])
self.assertEqual(self.test_kv.get('prepared-devices'),
['/dev/mapper/crypt-7c3ff7c8-fd20-4dca-9be6-6f44f213d3fe'])
mock_vaultlocker.write_vaultlocker_conf.assert_called_with(
'test_context',
priority=90
)
@patch.object(swift_utils, "uuid")
@patch.object(swift_utils, "vaultlocker")
@patch.object(swift_utils.charmhelpers.core.fstab, "Fstab")
@patch.object(swift_utils, 'is_device_in_ring')
@patch.object(swift_utils, 'clean_storage')
@patch.object(swift_utils, 'mkfs_xfs')
@patch.object(swift_utils, 'determine_block_devices')
def test_setup_storage_encrypt_noready(self, determine, mkfs, clean,
mock_is_device_in_ring, mock_Fstab,
mock_vaultlocker, mock_uuid):
mock_context = MagicMock()
mock_context.complete = False
mock_context.return_value = {}
mock_vaultlocker.VaultKVContext.return_value = mock_context
swift_utils.setup_storage(encrypt=True)
mock_vaultlocker.write_vaultlocker_conf.assert_not_called()
clean.assert_not_called()
self.check_call.assert_not_called()
self.mkdir.assert_not_called()
self.assertEqual(self.test_kv.get('prepared-devices'), None)
def _fake_is_device_mounted(self, device): def _fake_is_device_mounted(self, device):
if device in ["/dev/sda", "/dev/vda", "/dev/cciss/c0d0"]: if device in ["/dev/sda", "/dev/vda", "/dev/cciss/c0d0"]:
return True return True
@ -373,6 +437,7 @@ class SwiftStorageUtilsTests(CharmTestCase):
server.return_value = 'swift_server_context' server.return_value = 'swift_server_context'
bind_context.return_value = 'bind_host_context' bind_context.return_value = 'bind_host_context'
worker_context.return_value = 'worker_context' worker_context.return_value = 'worker_context'
self.vaultlocker.VaultKVContext.return_value = 'vl_context'
self.get_os_codename_package.return_value = 'grizzly' self.get_os_codename_package.return_value = 'grizzly'
configs = MagicMock() configs = MagicMock()
configs.register = MagicMock() configs.register = MagicMock()
@ -386,13 +451,16 @@ class SwiftStorageUtilsTests(CharmTestCase):
['rsync_context', 'swift_context']), ['rsync_context', 'swift_context']),
call('/etc/swift/account-server.conf', ['swift_context', call('/etc/swift/account-server.conf', ['swift_context',
'bind_host_context', 'bind_host_context',
'worker_context']), 'worker_context',
'vl_context']),
call('/etc/swift/object-server.conf', ['swift_context', call('/etc/swift/object-server.conf', ['swift_context',
'bind_host_context', 'bind_host_context',
'worker_context']), 'worker_context',
'vl_context']),
call('/etc/swift/container-server.conf', ['swift_context', call('/etc/swift/container-server.conf', ['swift_context',
'bind_host_context', 'bind_host_context',
'worker_context']) 'worker_context',
'vl_context'])
] ]
self.assertEqual(ex, configs.register.call_args_list) self.assertEqual(ex, configs.register.call_args_list)
@ -434,6 +502,7 @@ class SwiftStorageUtilsTests(CharmTestCase):
mock_is_device_in_ring.return_value = False mock_is_device_in_ring.return_value = False
determine.return_value = ["/dev/loop0", ] determine.return_value = ["/dev/loop0", ]
self.is_mapped_loopback_device.return_value = "/srv/test.img" self.is_mapped_loopback_device.return_value = "/srv/test.img"
self.is_device_mounted.return_value = False
swift_utils.setup_storage() swift_utils.setup_storage()
self.mount.assert_called_with( self.mount.assert_called_with(
"/dev/loop0", "/dev/loop0",
@ -476,6 +545,7 @@ class SwiftStorageUtilsTests(CharmTestCase):
mock_is_device_in_ring.return_value = False mock_is_device_in_ring.return_value = False
determine.return_value = ["/dev/loop0", ] determine.return_value = ["/dev/loop0", ]
self.is_mapped_loopback_device.return_value = "/srv/test.img" self.is_mapped_loopback_device.return_value = "/srv/test.img"
self.is_device_mounted.return_value = False
swift_utils.setup_storage() swift_utils.setup_storage()
self.mount.assert_called_with( self.mount.assert_called_with(
"/srv/test.img", "/srv/test.img",

View File

@ -117,6 +117,23 @@ class TestRelation(object):
return None return None
class TestKV(dict):
def __init__(self):
super(TestKV, self).__init__()
self.flushed = False
self.data = {}
def get(self, attribute, default=None):
return self.data.get(attribute, default)
def set(self, attribute, value):
self.data[attribute] = value
def flush(self):
self.flushed = True
@contextmanager @contextmanager
def patch_open(): def patch_open():
'''Patch open() to allow mocking both open() itself and the file that is '''Patch open() to allow mocking both open() itself and the file that is