1
# Copyright 2014-2015 Canonical Limited.
3
# This file is part of charm-helpers.
5
# charm-helpers is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License version 3 as
7
# published by the Free Software Foundation.
9
# charm-helpers is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU Lesser General Public License for more details.
14
# You should have received a copy of the GNU Lesser General Public License
15
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
18
Functions for managing volumes in juju units. One volume is supported per unit.
19
Subordinates may have their own storage, provided it is on its own partition.
21
Configuration stanzas::
27
If false, a volume is mounted as sepecified in "volume-map"
28
If true, ephemeral storage will be used, meaning that log data
29
will only exist as long as the machine. YOU HAVE BEEN WARNED.
34
YAML map of units to device names, e.g:
35
"{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }"
36
Service units will raise a configure-error if volume-ephemeral
37
is 'true' and no volume-map value is set. Use 'juju set' to set a
38
value and 'juju resolved' to complete configuration.
42
from charmsupport.volumes import configure_volume, VolumeConfigurationError
43
from charmsupport.hookenv import log, ERROR
44
def post_mount_hook():
45
stop_service('myservice')
46
def post_mount_hook():
47
start_service('myservice')
49
if __name__ == '__main__':
51
configure_volume(before_change=pre_mount_hook,
52
after_change=post_mount_hook)
53
except VolumeConfigurationError:
54
log('Storage could not be configured', ERROR)
58
# XXX: Known limitations
59
# - fstab is neither consulted nor updated
62
from charmhelpers.core import hookenv
63
from charmhelpers.core import host
67
MOUNT_BASE = '/srv/juju/volumes'
70
class VolumeConfigurationError(Exception):
71
'''Volume configuration data is missing or invalid'''
76
'''Gather and sanity-check volume configuration data'''
78
config = hookenv.config()
82
if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
83
volume_config['ephemeral'] = True
85
volume_config['ephemeral'] = False
88
volume_map = yaml.safe_load(config.get('volume-map', '{}'))
89
except yaml.YAMLError as e:
90
hookenv.log("Error parsing YAML volume-map: {}".format(e),
93
if volume_map is None:
94
# probably an empty string
96
elif not isinstance(volume_map, dict):
97
hookenv.log("Volume-map should be a dictionary, not {}".format(
101
volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME'])
102
if volume_config['device'] and volume_config['ephemeral']:
103
# asked for ephemeral storage but also defined a volume ID
104
hookenv.log('A volume is defined for this unit, but ephemeral '
105
'storage was requested', hookenv.ERROR)
107
elif not volume_config['device'] and not volume_config['ephemeral']:
108
# asked for permanent storage but did not define volume ID
109
hookenv.log('Ephemeral storage was requested, but there is no volume '
110
'defined for this unit.', hookenv.ERROR)
113
unit_mount_name = hookenv.local_unit().replace('/', '-')
114
volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
121
def mount_volume(config):
122
if os.path.exists(config['mountpoint']):
123
if not os.path.isdir(config['mountpoint']):
124
hookenv.log('Not a directory: {}'.format(config['mountpoint']))
125
raise VolumeConfigurationError()
127
host.mkdir(config['mountpoint'])
128
if os.path.ismount(config['mountpoint']):
129
unmount_volume(config)
130
if not host.mount(config['device'], config['mountpoint'], persist=True):
131
raise VolumeConfigurationError()
134
def unmount_volume(config):
135
if os.path.ismount(config['mountpoint']):
136
if not host.umount(config['mountpoint'], persist=True):
137
raise VolumeConfigurationError()
140
def managed_mounts():
141
'''List of all mounted managed volumes'''
142
return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
145
def configure_volume(before_change=lambda: None, after_change=lambda: None):
146
'''Set up storage (or don't) according to the charm's volume configuration.
147
Returns the mount point or "ephemeral". before_change and after_change
148
are optional functions to be called if the volume configuration changes.
151
config = get_config()
153
hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
154
raise VolumeConfigurationError()
156
if config['ephemeral']:
157
if os.path.ismount(config['mountpoint']):
159
unmount_volume(config)
164
if os.path.ismount(config['mountpoint']):
165
mounts = dict(managed_mounts())
166
if mounts.get(config['mountpoint']) != config['device']:
168
unmount_volume(config)
175
return config['mountpoint']