~canonical-ci-engineering/charms/trusty/core-image-publisher/trunk

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/contrib/charmsupport/volumes.py

  • Committer: Celso Providelo
  • Date: 2015-03-25 04:13:43 UTC
  • Revision ID: celso.providelo@canonical.com-20150325041343-jw05jaz6jscs3c8f
fork of core-image-watcher

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2014-2015 Canonical Limited.
 
2
#
 
3
# This file is part of charm-helpers.
 
4
#
 
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.
 
8
#
 
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.
 
13
#
 
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/>.
 
16
 
 
17
'''
 
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.
 
20
 
 
21
Configuration stanzas::
 
22
 
 
23
  volume-ephemeral:
 
24
    type: boolean
 
25
    default: true
 
26
    description: >
 
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.
 
30
  volume-map:
 
31
    type: string
 
32
    default: {}
 
33
    description: >
 
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.
 
39
 
 
40
Usage::
 
41
 
 
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')
 
48
 
 
49
    if __name__ == '__main__':
 
50
        try:
 
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)
 
55
 
 
56
'''
 
57
 
 
58
# XXX: Known limitations
 
59
# - fstab is neither consulted nor updated
 
60
 
 
61
import os
 
62
from charmhelpers.core import hookenv
 
63
from charmhelpers.core import host
 
64
import yaml
 
65
 
 
66
 
 
67
MOUNT_BASE = '/srv/juju/volumes'
 
68
 
 
69
 
 
70
class VolumeConfigurationError(Exception):
 
71
    '''Volume configuration data is missing or invalid'''
 
72
    pass
 
73
 
 
74
 
 
75
def get_config():
 
76
    '''Gather and sanity-check volume configuration data'''
 
77
    volume_config = {}
 
78
    config = hookenv.config()
 
79
 
 
80
    errors = False
 
81
 
 
82
    if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
 
83
        volume_config['ephemeral'] = True
 
84
    else:
 
85
        volume_config['ephemeral'] = False
 
86
 
 
87
    try:
 
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),
 
91
                    hookenv.ERROR)
 
92
        errors = True
 
93
    if volume_map is None:
 
94
        # probably an empty string
 
95
        volume_map = {}
 
96
    elif not isinstance(volume_map, dict):
 
97
        hookenv.log("Volume-map should be a dictionary, not {}".format(
 
98
            type(volume_map)))
 
99
        errors = True
 
100
 
 
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)
 
106
        errors = True
 
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)
 
111
        errors = True
 
112
 
 
113
    unit_mount_name = hookenv.local_unit().replace('/', '-')
 
114
    volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
 
115
 
 
116
    if errors:
 
117
        return None
 
118
    return volume_config
 
119
 
 
120
 
 
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()
 
126
    else:
 
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()
 
132
 
 
133
 
 
134
def unmount_volume(config):
 
135
    if os.path.ismount(config['mountpoint']):
 
136
        if not host.umount(config['mountpoint'], persist=True):
 
137
            raise VolumeConfigurationError()
 
138
 
 
139
 
 
140
def managed_mounts():
 
141
    '''List of all mounted managed volumes'''
 
142
    return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
 
143
 
 
144
 
 
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.
 
149
    '''
 
150
 
 
151
    config = get_config()
 
152
    if not config:
 
153
        hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
 
154
        raise VolumeConfigurationError()
 
155
 
 
156
    if config['ephemeral']:
 
157
        if os.path.ismount(config['mountpoint']):
 
158
            before_change()
 
159
            unmount_volume(config)
 
160
            after_change()
 
161
        return 'ephemeral'
 
162
    else:
 
163
        # persistent storage
 
164
        if os.path.ismount(config['mountpoint']):
 
165
            mounts = dict(managed_mounts())
 
166
            if mounts.get(config['mountpoint']) != config['device']:
 
167
                before_change()
 
168
                unmount_volume(config)
 
169
                mount_volume(config)
 
170
                after_change()
 
171
        else:
 
172
            before_change()
 
173
            mount_volume(config)
 
174
            after_change()
 
175
        return config['mountpoint']