aboutsummaryrefslogtreecommitdiffstats
path: root/charms/trusty/cassandra/hooks/relations.py
blob: f7870a17cc795a320ac67f7346c1d7391f5cb575 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# Copyright 2015 Canonical Ltd.
#
# This file is part of the Cassandra Charm for Juju.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3, as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE.  See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os.path

import yaml

from charmhelpers.core import hookenv, host
from charmhelpers.core.hookenv import log, WARNING
from charmhelpers.core.services.helpers import RelationContext

from coordinator import coordinator


class PeerRelation(RelationContext):
    interface = 'cassandra-cluster'
    name = 'cluster'

    def is_ready(self):
        # All units except the leader need to wait until the peer
        # relation is available.
        if coordinator.relid is not None or hookenv.is_leader():
            return True
        return False


# FOR CHARMHELPERS (if we can integrate Juju 1.24 storage too)
class StorageRelation(RelationContext):
    '''Wait for the block storage mount to become available.

    Charms using this should add a 'wait_for_storage_broker' boolean
    configuration option in their config.yaml file. This is necessary
    to avoid potential data loss race conditions, because otherwise a
    unit will be started up using local disk before it becomes aware
    that it should be using external storage.

    'relname' is the relation name.

    'mountpount' is the mountpoint. Use the default if you have a single
    block storage broker relation. The default is calculated to avoid
    configs using the unit name (/srv/${service}_${unitnumber}).
    '''
    interface = 'block-storage'
    mountpoint = None

    def __init__(self, name=None, mountpoint=None):
        if name is None:
            name = self._get_relation_name()
        super(StorageRelation, self).__init__(name)

        if mountpoint is None:
            mountpoint = os.path.join('/srv/',
                                      hookenv.local_unit().replace('/', '_'))
        self._requested_mountpoint = mountpoint

        if len(self.get('data', [])) == 0:
            self.mountpoint = None
        elif mountpoint == self['data'][0].get('mountpoint', None):
            self.mountpoint = mountpoint
        else:
            self.mountpoint = None

    def _get_relation_name(self):
        with open(os.path.join(hookenv.charm_dir(),
                               'metadata.yaml'), 'r') as mdf:
            md = yaml.safe_load(mdf)
        for section in ['requires', 'provides']:
            for relname in md.get(section, {}).keys():
                if md[section][relname]['interface'] == 'block-storage':
                    return relname
        raise LookupError('No block-storage relation defined')

    def is_ready(self):
        if hookenv.config('wait_for_storage_broker'):
            if self.mountpoint:
                log("External storage mounted at {}".format(self.mountpoint))
                return True
            else:
                log("Waiting for block storage broker to mount {}".format(
                    self._requested_mountpoint), WARNING)
                return False
        return True

    def provide_data(self, remote_service, service_ready):
        hookenv.log('Requesting mountpoint {} from {}'
                    .format(self._requested_mountpoint, remote_service))
        return dict(mountpoint=self._requested_mountpoint)

    def needs_remount(self):
        config = hookenv.config()
        return config.get('live_mountpoint') != self.mountpoint

    def migrate(self, src_dir, subdir):
        assert self.needs_remount()
        assert subdir, 'Can only migrate to a subdirectory on a mount'

        config = hookenv.config()
        config['live_mountpoint'] = self.mountpoint

        if self.mountpoint is None:
            hookenv.log('External storage AND DATA gone.'
                        'Reverting to original local storage', WARNING)
            return

        dst_dir = os.path.join(self.mountpoint, subdir)
        if os.path.exists(dst_dir):
            hookenv.log('{} already exists. Not migrating data.'.format(
                dst_dir))
            return

        # We are migrating the contents of src_dir, so we want a
        # trailing slash to ensure rsync's behavior.
        if not src_dir.endswith('/'):
            src_dir += '/'

        # We don't migrate data directly into the new destination,
        # which allows us to detect a failed migration and recover.
        tmp_dst_dir = dst_dir + '.migrating'
        hookenv.log('Migrating data from {} to {}'.format(
            src_dir, tmp_dst_dir))
        host.rsync(src_dir, tmp_dst_dir, flags='-av')

        hookenv.log('Moving {} to {}'.format(tmp_dst_dir, dst_dir))
        os.rename(tmp_dst_dir, dst_dir)

        assert not self.needs_remount()