~james-page/charms/trusty/swift-proxy/trunk

« back to all changes in this revision

Viewing changes to lib/swift_utils.py

  • Committer: Ryan Beisner
  • Date: 2016-01-19 12:47:49 UTC
  • mto: This revision was merged to the branch mainline in revision 135.
  • Revision ID: ryan.beisner@canonical.com-20160119124749-5uj102427wonbfmx
Fix typo in mitaka amulet test definition

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
import copy
2
 
import glob
3
 
import hashlib
4
 
import os
5
 
import pwd
6
 
import shutil
7
 
import subprocess
8
 
import tempfile
9
 
import threading
10
 
import uuid
11
 
 
12
 
from collections import OrderedDict
13
 
from swift_context import (
14
 
    get_swift_hash,
15
 
    SwiftHashContext,
16
 
    SwiftIdentityContext,
17
 
    HAProxyContext,
18
 
    SwiftRingContext,
19
 
    ApacheSSLContext,
20
 
    MemcachedContext,
21
 
)
22
 
 
23
 
import charmhelpers.contrib.openstack.context as context
24
 
import charmhelpers.contrib.openstack.templating as templating
25
 
from charmhelpers.contrib.openstack.utils import (
26
 
    os_release,
27
 
    get_os_codename_package,
28
 
    get_os_codename_install_source,
29
 
    configure_installation_source,
30
 
    set_os_workload_status,
31
 
)
32
 
from charmhelpers.contrib.hahelpers.cluster import (
33
 
    is_elected_leader,
34
 
    peer_units,
35
 
)
36
 
from charmhelpers.core.hookenv import (
37
 
    log,
38
 
    DEBUG,
39
 
    INFO,
40
 
    WARNING,
41
 
    config,
42
 
    relation_get,
43
 
    unit_get,
44
 
    relation_set,
45
 
    relation_ids,
46
 
    related_units,
47
 
    status_get,
48
 
    status_set,
49
 
)
50
 
from charmhelpers.fetch import (
51
 
    apt_update,
52
 
    apt_upgrade,
53
 
    apt_install,
54
 
    add_source
55
 
)
56
 
from charmhelpers.core.host import (
57
 
    lsb_release,
58
 
    restart_on_change,
59
 
)
60
 
from charmhelpers.contrib.network.ip import (
61
 
    format_ipv6_addr,
62
 
    get_ipv6_addr,
63
 
    is_ipv6,
64
 
)
65
 
from charmhelpers.core.decorators import (
66
 
    retry_on_exception,
67
 
)
68
 
from charmhelpers.core.unitdata import (
69
 
    HookData,
70
 
    kv,
71
 
)
72
 
 
73
 
 
74
 
# Various config files that are managed via templating.
75
 
SWIFT_CONF_DIR = '/etc/swift'
76
 
SWIFT_RING_EXT = 'ring.gz'
77
 
SWIFT_CONF = os.path.join(SWIFT_CONF_DIR, 'swift.conf')
78
 
SWIFT_PROXY_CONF = os.path.join(SWIFT_CONF_DIR, 'proxy-server.conf')
79
 
SWIFT_CONF_DIR = os.path.dirname(SWIFT_CONF)
80
 
MEMCACHED_CONF = '/etc/memcached.conf'
81
 
SWIFT_RINGS_CONF = '/etc/apache2/conf.d/swift-rings'
82
 
SWIFT_RINGS_24_CONF = '/etc/apache2/conf-available/swift-rings.conf'
83
 
HAPROXY_CONF = '/etc/haproxy/haproxy.cfg'
84
 
APACHE_SITES_AVAILABLE = '/etc/apache2/sites-available'
85
 
APACHE_SITE_CONF = os.path.join(APACHE_SITES_AVAILABLE,
86
 
                                'openstack_https_frontend')
87
 
APACHE_SITE_24_CONF = os.path.join(APACHE_SITES_AVAILABLE,
88
 
                                   'openstack_https_frontend.conf')
89
 
 
90
 
WWW_DIR = '/var/www/swift-rings'
91
 
ALTERNATE_WWW_DIR = '/var/www/html/swift-rings'
92
 
 
93
 
RING_SYNC_SEMAPHORE = threading.Semaphore()
94
 
 
95
 
 
96
 
def get_www_dir():
97
 
    if os.path.isdir(os.path.dirname(ALTERNATE_WWW_DIR)):
98
 
        return ALTERNATE_WWW_DIR
99
 
    else:
100
 
        return WWW_DIR
101
 
 
102
 
 
103
 
SWIFT_RINGS = {
104
 
    'account': os.path.join(SWIFT_CONF_DIR, 'account.builder'),
105
 
    'container': os.path.join(SWIFT_CONF_DIR, 'container.builder'),
106
 
    'object': os.path.join(SWIFT_CONF_DIR, 'object.builder')
107
 
}
108
 
 
109
 
SSL_CERT = os.path.join(SWIFT_CONF_DIR, 'cert.crt')
110
 
SSL_KEY = os.path.join(SWIFT_CONF_DIR, 'cert.key')
111
 
 
112
 
# Essex packages
113
 
BASE_PACKAGES = [
114
 
    'swift',
115
 
    'swift-proxy',
116
 
    'memcached',
117
 
    'apache2',
118
 
    'python-keystone',
119
 
]
120
 
# > Folsom specific packages
121
 
FOLSOM_PACKAGES = BASE_PACKAGES + ['swift-plugin-s3']
122
 
 
123
 
SWIFT_HA_RES = 'grp_swift_vips'
124
 
TEMPLATES = 'templates/'
125
 
 
126
 
# Map config files to hook contexts and services that will be associated
127
 
# with file in restart_on_changes()'s service map.
128
 
CONFIG_FILES = OrderedDict([
129
 
    (SWIFT_CONF, {
130
 
        'hook_contexts': [SwiftHashContext()],
131
 
        'services': ['swift-proxy'],
132
 
    }),
133
 
    (SWIFT_PROXY_CONF, {
134
 
        'hook_contexts': [SwiftIdentityContext(),
135
 
                          context.BindHostContext()],
136
 
        'services': ['swift-proxy'],
137
 
    }),
138
 
    (HAPROXY_CONF, {
139
 
        'hook_contexts': [context.HAProxyContext(singlenode_mode=True),
140
 
                          HAProxyContext()],
141
 
        'services': ['haproxy'],
142
 
    }),
143
 
    (SWIFT_RINGS_CONF, {
144
 
        'hook_contexts': [SwiftRingContext()],
145
 
        'services': ['apache2'],
146
 
    }),
147
 
    (SWIFT_RINGS_24_CONF, {
148
 
        'hook_contexts': [SwiftRingContext()],
149
 
        'services': ['apache2'],
150
 
    }),
151
 
    (APACHE_SITE_CONF, {
152
 
        'hook_contexts': [ApacheSSLContext()],
153
 
        'services': ['apache2'],
154
 
    }),
155
 
    (APACHE_SITE_24_CONF, {
156
 
        'hook_contexts': [ApacheSSLContext()],
157
 
        'services': ['apache2'],
158
 
    }),
159
 
    (MEMCACHED_CONF, {
160
 
        'hook_contexts': [MemcachedContext()],
161
 
        'services': ['memcached'],
162
 
    }),
163
 
])
164
 
 
165
 
 
166
 
class SwiftProxyCharmException(Exception):
167
 
    pass
168
 
 
169
 
 
170
 
class SwiftProxyClusterRPC(object):
171
 
    """Provides cluster relation rpc dicts.
172
 
 
173
 
    Crucially, this ensures that any settings we don't use in any given call
174
 
    are set to None, therefore removing them from the relation so they don't
175
 
    get accidentally interpreted by the receiver as part of the request.
176
 
 
177
 
    NOTE: these are only intended to be used from cluster peer relations.
178
 
    """
179
 
 
180
 
    KEY_STOP_PROXY_SVC = 'stop-proxy-service'
181
 
    KEY_STOP_PROXY_SVC_ACK = 'stop-proxy-service-ack'
182
 
    KEY_NOTIFY_LEADER_CHANGED = 'leader-changed-notification'
183
 
 
184
 
    def __init__(self, version=1):
185
 
        self._version = version
186
 
 
187
 
    def template(self):
188
 
        # Everything must be None by default so it gets dropped from the
189
 
        # relation unless we want it to be set.
190
 
        templates = {1: {'trigger': None,
191
 
                         'broker-token': None,
192
 
                         'builder-broker': None,
193
 
                         self.KEY_STOP_PROXY_SVC: None,
194
 
                         self.KEY_STOP_PROXY_SVC_ACK: None,
195
 
                         self.KEY_NOTIFY_LEADER_CHANGED: None,
196
 
                         'peers-only': None,
197
 
                         'sync-only-builders': None}}
198
 
        return copy.deepcopy(templates[self._version])
199
 
 
200
 
    def stop_proxy_request(self, peers_only=False):
201
 
        """Request to stop peer proxy service.
202
 
 
203
 
        NOTE: leader action
204
 
        """
205
 
        rq = self.template()
206
 
        rq['trigger'] = str(uuid.uuid4())
207
 
        rq[self.KEY_STOP_PROXY_SVC] = rq['trigger']
208
 
        if peers_only:
209
 
            rq['peers-only'] = 1
210
 
 
211
 
        return rq
212
 
 
213
 
    def stop_proxy_ack(self, echo_token, echo_peers_only):
214
 
        """Ack that peer proxy service is stopped.
215
 
 
216
 
        NOTE: non-leader action
217
 
        """
218
 
        rq = self.template()
219
 
        rq['trigger'] = str(uuid.uuid4())
220
 
        # These echo values should match those received in the request
221
 
        rq[self.KEY_STOP_PROXY_SVC_ACK] = echo_token
222
 
        rq['peers-only'] = echo_peers_only
223
 
        return rq
224
 
 
225
 
    def sync_rings_request(self, broker_host, broker_token,
226
 
                           builders_only=False):
227
 
        """Request for peer to sync rings.
228
 
 
229
 
        NOTE: leader action
230
 
        """
231
 
        rq = self.template()
232
 
        rq['trigger'] = str(uuid.uuid4())
233
 
 
234
 
        if builders_only:
235
 
            rq['sync-only-builders'] = 1
236
 
 
237
 
        rq['broker-token'] = broker_token
238
 
        rq['builder-broker'] = broker_host
239
 
        return rq
240
 
 
241
 
    def notify_leader_changed(self):
242
 
        """Notify peers that leader has changed.
243
 
 
244
 
        NOTE: leader action
245
 
        """
246
 
        rq = self.template()
247
 
        rq['trigger'] = str(uuid.uuid4())
248
 
        rq[self.KEY_NOTIFY_LEADER_CHANGED] = rq['trigger']
249
 
        return rq
250
 
 
251
 
 
252
 
def get_first_available_value(responses, key, default=None):
253
 
    for r in responses:
254
 
        if key in r:
255
 
            return r[key]
256
 
 
257
 
    return default
258
 
 
259
 
 
260
 
def all_responses_equal(responses, key, must_exist=True):
261
 
    """If key exists in responses, all values for it must be equal.
262
 
 
263
 
    If all equal return True. If key does not exist and must_exist is True
264
 
    return False otherwise True.
265
 
    """
266
 
    sentinel = object()
267
 
    val = None
268
 
    all_equal = True
269
 
    for r in responses:
270
 
        _val = r.get(key, sentinel)
271
 
        if val is not None and val != _val:
272
 
            all_equal = False
273
 
            break
274
 
        elif _val != sentinel:
275
 
            val = _val
276
 
 
277
 
    if must_exist and val is None:
278
 
        all_equal = False
279
 
 
280
 
    if all_equal:
281
 
        return True
282
 
 
283
 
    log("Responses not all equal for key '%s'" % (key), level=DEBUG)
284
 
    return False
285
 
 
286
 
 
287
 
def register_configs():
288
 
    """Register config files with their respective contexts.
289
 
 
290
 
    Registration of some configs may not be required depending on
291
 
    existing of certain relations.
292
 
    """
293
 
    # if called without anything installed (eg during install hook)
294
 
    # just default to earliest supported release. configs dont get touched
295
 
    # till post-install, anyway.
296
 
    release = get_os_codename_package('swift-proxy', fatal=False) \
297
 
        or 'essex'
298
 
    configs = templating.OSConfigRenderer(templates_dir=TEMPLATES,
299
 
                                          openstack_release=release)
300
 
 
301
 
    confs = [SWIFT_CONF,
302
 
             SWIFT_PROXY_CONF,
303
 
             HAPROXY_CONF,
304
 
             MEMCACHED_CONF]
305
 
 
306
 
    for conf in confs:
307
 
        configs.register(conf, CONFIG_FILES[conf]['hook_contexts'])
308
 
 
309
 
    if os.path.exists('/etc/apache2/conf-available'):
310
 
        configs.register(SWIFT_RINGS_24_CONF,
311
 
                         CONFIG_FILES[SWIFT_RINGS_24_CONF]['hook_contexts'])
312
 
        configs.register(APACHE_SITE_24_CONF,
313
 
                         CONFIG_FILES[APACHE_SITE_24_CONF]['hook_contexts'])
314
 
    else:
315
 
        configs.register(SWIFT_RINGS_CONF,
316
 
                         CONFIG_FILES[SWIFT_RINGS_CONF]['hook_contexts'])
317
 
        configs.register(APACHE_SITE_CONF,
318
 
                         CONFIG_FILES[APACHE_SITE_CONF]['hook_contexts'])
319
 
    return configs
320
 
 
321
 
 
322
 
def restart_map():
323
 
    """Determine the correct resource map to be passed to
324
 
    charmhelpers.core.restart_on_change() based on the services configured.
325
 
 
326
 
    :returns dict: A dictionary mapping config file to lists of services
327
 
                    that should be restarted when file changes.
328
 
    """
329
 
    _map = []
330
 
    for f, ctxt in CONFIG_FILES.iteritems():
331
 
        svcs = []
332
 
        for svc in ctxt['services']:
333
 
            svcs.append(svc)
334
 
        if svcs:
335
 
            _map.append((f, svcs))
336
 
 
337
 
    return OrderedDict(_map)
338
 
 
339
 
 
340
 
def services():
341
 
    ''' Returns a list of services associate with this charm '''
342
 
    _services = []
343
 
    for v in restart_map().values():
344
 
        _services = _services + v
345
 
    return list(set(_services))
346
 
 
347
 
 
348
 
def swift_user(username='swift'):
349
 
    user = pwd.getpwnam(username)
350
 
    return (user.pw_uid, user.pw_gid)
351
 
 
352
 
 
353
 
def ensure_swift_dir(conf_dir=os.path.dirname(SWIFT_CONF)):
354
 
    if not os.path.isdir(conf_dir):
355
 
        os.mkdir(conf_dir, 0o750)
356
 
 
357
 
    uid, gid = swift_user()
358
 
    os.chown(conf_dir, uid, gid)
359
 
 
360
 
 
361
 
def determine_packages(release):
362
 
    """Determine what packages are needed for a given OpenStack release."""
363
 
    if release == 'essex':
364
 
        return BASE_PACKAGES
365
 
    elif release == 'folsom':
366
 
        return FOLSOM_PACKAGES
367
 
    elif release == 'grizzly':
368
 
        return FOLSOM_PACKAGES
369
 
    else:
370
 
        return FOLSOM_PACKAGES
371
 
 
372
 
 
373
 
def _load_builder(path):
374
 
    # lifted straight from /usr/bin/swift-ring-builder
375
 
    from swift.common.ring import RingBuilder
376
 
    import cPickle as pickle
377
 
    try:
378
 
        builder = pickle.load(open(path, 'rb'))
379
 
        if not hasattr(builder, 'devs'):
380
 
            builder_dict = builder
381
 
            builder = RingBuilder(1, 1, 1)
382
 
            builder.copy_from(builder_dict)
383
 
    except ImportError:  # Happens with really old builder pickles
384
 
        builder = RingBuilder(1, 1, 1)
385
 
        builder.copy_from(pickle.load(open(path, 'rb')))
386
 
    for dev in builder.devs:
387
 
        if dev and 'meta' not in dev:
388
 
            dev['meta'] = ''
389
 
 
390
 
    return builder
391
 
 
392
 
 
393
 
def _write_ring(ring, ring_path):
394
 
    import cPickle as pickle
395
 
    with open(ring_path, "wb") as fd:
396
 
        pickle.dump(ring.to_dict(), fd, protocol=2)
397
 
 
398
 
 
399
 
def ring_port(ring_path, node):
400
 
    """Determine correct port from relation settings for a given ring file."""
401
 
    for name in ['account', 'object', 'container']:
402
 
        if name in ring_path:
403
 
            return node[('%s_port' % name)]
404
 
 
405
 
 
406
 
def initialize_ring(path, part_power, replicas, min_hours):
407
 
    """Initialize a new swift ring with given parameters."""
408
 
    from swift.common.ring import RingBuilder
409
 
    ring = RingBuilder(part_power, replicas, min_hours)
410
 
    _write_ring(ring, path)
411
 
 
412
 
 
413
 
def exists_in_ring(ring_path, node):
414
 
    ring = _load_builder(ring_path).to_dict()
415
 
    node['port'] = ring_port(ring_path, node)
416
 
 
417
 
    for dev in ring['devs']:
418
 
        d = [(i, dev[i]) for i in dev if i in node and i != 'zone']
419
 
        n = [(i, node[i]) for i in node if i in dev and i != 'zone']
420
 
        if sorted(d) == sorted(n):
421
 
 
422
 
            log('Node already exists in ring (%s).' % ring_path, level=INFO)
423
 
            return True
424
 
 
425
 
    return False
426
 
 
427
 
 
428
 
def add_to_ring(ring_path, node):
429
 
    ring = _load_builder(ring_path)
430
 
    port = ring_port(ring_path, node)
431
 
 
432
 
    devs = ring.to_dict()['devs']
433
 
    next_id = 0
434
 
    if devs:
435
 
        next_id = len([d['id'] for d in devs])
436
 
 
437
 
    new_dev = {
438
 
        'id': next_id,
439
 
        'zone': node['zone'],
440
 
        'ip': node['ip'],
441
 
        'port': port,
442
 
        'device': node['device'],
443
 
        'weight': 100,
444
 
        'meta': '',
445
 
    }
446
 
    ring.add_dev(new_dev)
447
 
    _write_ring(ring, ring_path)
448
 
    msg = 'Added new device to ring %s: %s' % (ring_path, new_dev)
449
 
    log(msg, level=INFO)
450
 
 
451
 
 
452
 
def _get_zone(ring_builder):
453
 
    replicas = ring_builder.replicas
454
 
    zones = [d['zone'] for d in ring_builder.devs]
455
 
    if not zones:
456
 
        return 1
457
 
 
458
 
    # zones is a per-device list, so we may have one
459
 
    # node with 3 devices in zone 1.  For balancing
460
 
    # we need to track the unique zones being used
461
 
    # not necessarily the number of devices
462
 
    unique_zones = list(set(zones))
463
 
    if len(unique_zones) < replicas:
464
 
        return sorted(unique_zones).pop() + 1
465
 
 
466
 
    zone_distrib = {}
467
 
    for z in zones:
468
 
        zone_distrib[z] = zone_distrib.get(z, 0) + 1
469
 
 
470
 
    if len(set([total for total in zone_distrib.itervalues()])) == 1:
471
 
        # all zones are equal, start assigning to zone 1 again.
472
 
        return 1
473
 
 
474
 
    return sorted(zone_distrib, key=zone_distrib.get).pop(0)
475
 
 
476
 
 
477
 
def get_min_part_hours(ring):
478
 
    builder = _load_builder(ring)
479
 
    return builder.min_part_hours
480
 
 
481
 
 
482
 
def set_min_part_hours(path, value):
483
 
    cmd = ['swift-ring-builder', path, 'set_min_part_hours', str(value)]
484
 
    p = subprocess.Popen(cmd)
485
 
    p.communicate()
486
 
    rc = p.returncode
487
 
    if rc != 0:
488
 
        msg = ("Failed to set min_part_hours=%s on %s" % (value, path))
489
 
        raise SwiftProxyCharmException(msg)
490
 
 
491
 
 
492
 
def get_zone(assignment_policy):
493
 
    """Determine appropriate zone based on configured assignment policy.
494
 
 
495
 
    Manual assignment relies on each storage zone being deployed as a
496
 
    separate service unit with its desired zone set as a configuration
497
 
    option.
498
 
 
499
 
    Auto assignment distributes swift-storage machine units across a number
500
 
    of zones equal to the configured minimum replicas.  This allows for a
501
 
    single swift-storage service unit, with each 'add-unit'd machine unit
502
 
    being assigned to a different zone.
503
 
    """
504
 
    if assignment_policy == 'manual':
505
 
        return relation_get('zone')
506
 
    elif assignment_policy == 'auto':
507
 
        potential_zones = []
508
 
        for ring in SWIFT_RINGS.itervalues():
509
 
            builder = _load_builder(ring)
510
 
            potential_zones.append(_get_zone(builder))
511
 
        return set(potential_zones).pop()
512
 
    else:
513
 
        msg = ('Invalid zone assignment policy: %s' % assignment_policy)
514
 
        raise SwiftProxyCharmException(msg)
515
 
 
516
 
 
517
 
def balance_ring(ring_path):
518
 
    """Balance a ring.
519
 
 
520
 
    Returns True if it needs redistribution.
521
 
    """
522
 
    # shell out to swift-ring-builder instead, since the balancing code there
523
 
    # does a bunch of un-importable validation.'''
524
 
    cmd = ['swift-ring-builder', ring_path, 'rebalance']
525
 
    p = subprocess.Popen(cmd)
526
 
    p.communicate()
527
 
    rc = p.returncode
528
 
    if rc == 0:
529
 
        return True
530
 
 
531
 
    if rc == 1:
532
 
        # Ring builder exit-code=1 is supposed to indicate warning but I have
533
 
        # noticed that it can also return 1 with the following sort of message:
534
 
        #
535
 
        #   NOTE: Balance of 166.67 indicates you should push this ring, wait
536
 
        #         at least 0 hours, and rebalance/repush.
537
 
        #
538
 
        # This indicates that a balance has occurred and a resync would be
539
 
        # required so not sure why 1 is returned in this case.
540
 
        return False
541
 
 
542
 
    msg = ('balance_ring: %s returned %s' % (cmd, rc))
543
 
    raise SwiftProxyCharmException(msg)
544
 
 
545
 
 
546
 
def should_balance(rings):
547
 
    """Determine whether or not a re-balance is required and allowed.
548
 
 
549
 
    Ring balance can be disabled/postponed using the disable-ring-balance
550
 
    config option.
551
 
 
552
 
    Otherwise, using zones vs min. replicas, determine whether or not the rings
553
 
    should be balanced.
554
 
    """
555
 
    if config('disable-ring-balance'):
556
 
        return False
557
 
 
558
 
    return has_minimum_zones(rings)
559
 
 
560
 
 
561
 
def do_openstack_upgrade(configs):
562
 
    new_src = config('openstack-origin')
563
 
    new_os_rel = get_os_codename_install_source(new_src)
564
 
 
565
 
    log('Performing OpenStack upgrade to %s.' % (new_os_rel), level=DEBUG)
566
 
    configure_installation_source(new_src)
567
 
    dpkg_opts = [
568
 
        '--option', 'Dpkg::Options::=--force-confnew',
569
 
        '--option', 'Dpkg::Options::=--force-confdef',
570
 
    ]
571
 
    apt_update()
572
 
    apt_upgrade(options=dpkg_opts, fatal=True, dist=True)
573
 
    configs.set_release(openstack_release=new_os_rel)
574
 
    configs.write_all()
575
 
 
576
 
 
577
 
def setup_ipv6():
578
 
    """Validate that we can support IPv6 mode.
579
 
 
580
 
    This should be called if prefer-ipv6 is True to ensure that we are running
581
 
    in an environment that supports ipv6.
582
 
    """
583
 
    ubuntu_rel = lsb_release()['DISTRIB_CODENAME'].lower()
584
 
    if ubuntu_rel < "trusty":
585
 
        msg = ("IPv6 is not supported in the charms for Ubuntu versions less "
586
 
               "than Trusty 14.04")
587
 
        raise SwiftProxyCharmException(msg)
588
 
 
589
 
    # Need haproxy >= 1.5.3 for ipv6 so for Trusty if we are <= Kilo we need to
590
 
    # use trusty-backports otherwise we can use the UCA.
591
 
    if ubuntu_rel == 'trusty' and os_release('swift-proxy') < 'liberty':
592
 
        add_source('deb http://archive.ubuntu.com/ubuntu trusty-backports '
593
 
                   'main')
594
 
        apt_update()
595
 
        apt_install('haproxy/trusty-backports', fatal=True)
596
 
 
597
 
 
598
 
@retry_on_exception(3, base_delay=2, exc_type=subprocess.CalledProcessError)
599
 
def sync_proxy_rings(broker_url, builders=True, rings=True):
600
 
    """The leader proxy is responsible for intialising, updating and
601
 
    rebalancing the ring. Once the leader is ready the rings must then be
602
 
    synced into each other proxy unit.
603
 
 
604
 
    Note that we sync the ring builder and .gz files since the builder itself
605
 
    is linked to the underlying .gz ring.
606
 
    """
607
 
    log('Fetching swift rings & builders from proxy @ %s.' % broker_url,
608
 
        level=DEBUG)
609
 
    target = SWIFT_CONF_DIR
610
 
    synced = []
611
 
    tmpdir = tempfile.mkdtemp(prefix='swiftrings')
612
 
    try:
613
 
        for server in ['account', 'object', 'container']:
614
 
            if builders:
615
 
                url = '%s/%s.builder' % (broker_url, server)
616
 
                log('Fetching %s.' % url, level=DEBUG)
617
 
                builder = "%s.builder" % (server)
618
 
                cmd = ['wget', url, '--retry-connrefused', '-t', '10', '-O',
619
 
                       os.path.join(tmpdir, builder)]
620
 
                subprocess.check_call(cmd)
621
 
                synced.append(builder)
622
 
 
623
 
            if rings:
624
 
                url = '%s/%s.%s' % (broker_url, server, SWIFT_RING_EXT)
625
 
                log('Fetching %s.' % url, level=DEBUG)
626
 
                ring = '%s.%s' % (server, SWIFT_RING_EXT)
627
 
                cmd = ['wget', url, '--retry-connrefused', '-t', '10', '-O',
628
 
                       os.path.join(tmpdir, ring)]
629
 
                subprocess.check_call(cmd)
630
 
                synced.append(ring)
631
 
 
632
 
        # Once all have been successfully downloaded, move them to actual
633
 
        # location.
634
 
        for f in synced:
635
 
            os.rename(os.path.join(tmpdir, f), os.path.join(target, f))
636
 
    finally:
637
 
        shutil.rmtree(tmpdir)
638
 
 
639
 
 
640
 
def ensure_www_dir_permissions(www_dir):
641
 
    if not os.path.isdir(www_dir):
642
 
        os.mkdir(www_dir, 0o755)
643
 
    else:
644
 
        os.chmod(www_dir, 0o755)
645
 
 
646
 
    uid, gid = swift_user()
647
 
    os.chown(www_dir, uid, gid)
648
 
 
649
 
 
650
 
def update_www_rings(rings=True, builders=True):
651
 
    """Copy rings to apache www dir.
652
 
 
653
 
    Try to do this as atomically as possible to avoid races with storage nodes
654
 
    syncing rings.
655
 
    """
656
 
    if not (rings or builders):
657
 
        return
658
 
 
659
 
    tmp_dir = tempfile.mkdtemp(prefix='swift-rings-www-tmp')
660
 
    for ring, builder_path in SWIFT_RINGS.iteritems():
661
 
        if rings:
662
 
            ringfile = '%s.%s' % (ring, SWIFT_RING_EXT)
663
 
            src = os.path.join(SWIFT_CONF_DIR, ringfile)
664
 
            dst = os.path.join(tmp_dir, ringfile)
665
 
            shutil.copyfile(src, dst)
666
 
 
667
 
        if builders:
668
 
            src = builder_path
669
 
            dst = os.path.join(tmp_dir, os.path.basename(builder_path))
670
 
            shutil.copyfile(src, dst)
671
 
 
672
 
    www_dir = get_www_dir()
673
 
    deleted = "%s.deleted" % (www_dir)
674
 
    ensure_www_dir_permissions(tmp_dir)
675
 
    os.rename(www_dir, deleted)
676
 
    os.rename(tmp_dir, www_dir)
677
 
    shutil.rmtree(deleted)
678
 
 
679
 
 
680
 
def get_rings_checksum():
681
 
    """Returns sha256 checksum for rings in /etc/swift."""
682
 
    sha = hashlib.sha256()
683
 
    for ring in SWIFT_RINGS.iterkeys():
684
 
        path = os.path.join(SWIFT_CONF_DIR, '%s.%s' % (ring, SWIFT_RING_EXT))
685
 
        if not os.path.isfile(path):
686
 
            continue
687
 
 
688
 
        with open(path, 'rb') as fd:
689
 
            sha.update(fd.read())
690
 
 
691
 
    return sha.hexdigest()
692
 
 
693
 
 
694
 
def get_builders_checksum():
695
 
    """Returns sha256 checksum for builders in /etc/swift."""
696
 
    sha = hashlib.sha256()
697
 
    for builder in SWIFT_RINGS.itervalues():
698
 
        if not os.path.exists(builder):
699
 
            continue
700
 
 
701
 
        with open(builder, 'rb') as fd:
702
 
            sha.update(fd.read())
703
 
 
704
 
    return sha.hexdigest()
705
 
 
706
 
 
707
 
def get_broker_token():
708
 
    """Get ack token from peers to be used as broker token.
709
 
 
710
 
    Must be equal across all peers.
711
 
 
712
 
    Returns token or None if not found.
713
 
    """
714
 
    responses = []
715
 
    ack_key = SwiftProxyClusterRPC.KEY_STOP_PROXY_SVC_ACK
716
 
    for rid in relation_ids('cluster'):
717
 
        for unit in related_units(rid):
718
 
            responses.append(relation_get(attribute=ack_key, rid=rid,
719
 
                                          unit=unit))
720
 
 
721
 
    # If no acks exist we have probably never done a sync so make up a token
722
 
    if len(responses) == 0:
723
 
        return str(uuid.uuid4())
724
 
 
725
 
    if not all(responses) or len(set(responses)) != 1:
726
 
        log("Not all ack tokens equal - %s" % (responses), level=DEBUG)
727
 
        return None
728
 
 
729
 
    return responses[0]
730
 
 
731
 
 
732
 
def sync_builders_and_rings_if_changed(f):
733
 
    """Only trigger a ring or builder sync if they have changed as a result of
734
 
    the decorated operation.
735
 
    """
736
 
    def _inner_sync_builders_and_rings_if_changed(*args, **kwargs):
737
 
        if not is_elected_leader(SWIFT_HA_RES):
738
 
            log("Sync rings called by non-leader - skipping", level=WARNING)
739
 
            return
740
 
 
741
 
        try:
742
 
            # Ensure we don't do a double sync if we are nested.
743
 
            do_sync = False
744
 
            if RING_SYNC_SEMAPHORE.acquire(blocking=0):
745
 
                do_sync = True
746
 
                rings_before = get_rings_checksum()
747
 
                builders_before = get_builders_checksum()
748
 
 
749
 
            ret = f(*args, **kwargs)
750
 
 
751
 
            if not do_sync:
752
 
                return ret
753
 
 
754
 
            rings_after = get_rings_checksum()
755
 
            builders_after = get_builders_checksum()
756
 
 
757
 
            rings_path = os.path.join(SWIFT_CONF_DIR, '*.%s' %
758
 
                                      (SWIFT_RING_EXT))
759
 
            rings_ready = len(glob.glob(rings_path)) == len(SWIFT_RINGS)
760
 
            rings_changed = rings_after != rings_before
761
 
            builders_changed = builders_after != builders_before
762
 
            if rings_changed or builders_changed:
763
 
                # Copy builders and rings (if available) to the server dir.
764
 
                update_www_rings(rings=rings_ready)
765
 
                if rings_changed and rings_ready:
766
 
                    # Trigger sync
767
 
                    cluster_sync_rings(peers_only=not rings_changed)
768
 
                else:
769
 
                    cluster_sync_rings(peers_only=True, builders_only=True)
770
 
                    log("Rings not ready for sync - syncing builders",
771
 
                        level=DEBUG)
772
 
            else:
773
 
                log("Rings/builders unchanged - skipping sync", level=DEBUG)
774
 
 
775
 
            return ret
776
 
        finally:
777
 
            RING_SYNC_SEMAPHORE.release()
778
 
 
779
 
    return _inner_sync_builders_and_rings_if_changed
780
 
 
781
 
 
782
 
@sync_builders_and_rings_if_changed
783
 
def update_rings(nodes=[], min_part_hours=None):
784
 
    """Update builder with node settings and balance rings if necessary.
785
 
 
786
 
    Also update min_part_hours if provided.
787
 
    """
788
 
    if not is_elected_leader(SWIFT_HA_RES):
789
 
        log("Update rings called by non-leader - skipping", level=WARNING)
790
 
        return
791
 
 
792
 
    balance_required = False
793
 
 
794
 
    if min_part_hours:
795
 
        # NOTE: no need to stop the proxy since we are not changing the rings,
796
 
        # only the builder.
797
 
 
798
 
        # Only update if all exist
799
 
        if all([os.path.exists(p) for p in SWIFT_RINGS.itervalues()]):
800
 
            for ring, path in SWIFT_RINGS.iteritems():
801
 
                current_min_part_hours = get_min_part_hours(path)
802
 
                if min_part_hours != current_min_part_hours:
803
 
                    log("Setting ring %s min_part_hours to %s" %
804
 
                        (ring, min_part_hours), level=INFO)
805
 
                    try:
806
 
                        set_min_part_hours(path, min_part_hours)
807
 
                    except SwiftProxyCharmException as exc:
808
 
                        # TODO: ignore for now since this should not be
809
 
                        # critical but in the future we should support a
810
 
                        # rollback.
811
 
                        log(str(exc), level=WARNING)
812
 
                    else:
813
 
                        balance_required = True
814
 
 
815
 
    for node in nodes:
816
 
        for ring in SWIFT_RINGS.itervalues():
817
 
            if not exists_in_ring(ring, node):
818
 
                add_to_ring(ring, node)
819
 
                balance_required = True
820
 
 
821
 
    if balance_required:
822
 
        balance_rings()
823
 
 
824
 
 
825
 
@sync_builders_and_rings_if_changed
826
 
def balance_rings():
827
 
    """Rebalance each ring and notify peers that new rings are available."""
828
 
    if not is_elected_leader(SWIFT_HA_RES):
829
 
        log("Balance rings called by non-leader - skipping", level=WARNING)
830
 
        return
831
 
 
832
 
    if not should_balance([r for r in SWIFT_RINGS.itervalues()]):
833
 
        log("Not yet ready to balance rings - insufficient replicas?",
834
 
            level=INFO)
835
 
        return
836
 
 
837
 
    rebalanced = False
838
 
    for path in SWIFT_RINGS.itervalues():
839
 
        if balance_ring(path):
840
 
            log('Balanced ring %s' % path, level=DEBUG)
841
 
            rebalanced = True
842
 
        else:
843
 
            log('Ring %s not rebalanced' % path, level=DEBUG)
844
 
 
845
 
    if not rebalanced:
846
 
        log("Rings unchanged by rebalance", level=DEBUG)
847
 
        # NOTE: checksum will tell for sure
848
 
 
849
 
 
850
 
def mark_www_rings_deleted():
851
 
    """Mark any rings from the apache server directory as deleted so that
852
 
    storage units won't see them.
853
 
    """
854
 
    www_dir = get_www_dir()
855
 
    for ring, _ in SWIFT_RINGS.iteritems():
856
 
        path = os.path.join(www_dir, '%s.ring.gz' % ring)
857
 
        if os.path.exists(path):
858
 
            os.rename(path, "%s.deleted" % (path))
859
 
 
860
 
 
861
 
def notify_peers_builders_available(broker_token, builders_only=False):
862
 
    """Notify peer swift-proxy units that they should synchronise ring and
863
 
    builder files.
864
 
 
865
 
    Note that this should only be called from the leader unit.
866
 
    """
867
 
    if not is_elected_leader(SWIFT_HA_RES):
868
 
        log("Ring availability peer broadcast requested by non-leader - "
869
 
            "skipping", level=WARNING)
870
 
        return
871
 
 
872
 
    hostname = get_hostaddr()
873
 
    hostname = format_ipv6_addr(hostname) or hostname
874
 
    # Notify peers that builders are available
875
 
    log("Notifying peer(s) that rings are ready for sync.", level=INFO)
876
 
    rq = SwiftProxyClusterRPC().sync_rings_request(hostname,
877
 
                                                   broker_token,
878
 
                                                   builders_only=builders_only)
879
 
    for rid in relation_ids('cluster'):
880
 
        log("Notifying rid=%s (%s)" % (rid, rq), level=DEBUG)
881
 
        relation_set(relation_id=rid, relation_settings=rq)
882
 
 
883
 
 
884
 
def broadcast_rings_available(broker_token, peers=True, storage=True,
885
 
                              builders_only=False):
886
 
    """Notify storage relations and cluster (peer) relations that rings and
887
 
    builders are availble for sync.
888
 
 
889
 
    We can opt to only notify peer or storage relations if needs be.
890
 
    """
891
 
    if storage:
892
 
        # TODO: get ack from storage units that they are synced before
893
 
        # syncing proxies.
894
 
        notify_storage_rings_available()
895
 
    else:
896
 
        log("Skipping notify storage relations", level=DEBUG)
897
 
 
898
 
    if peers:
899
 
        notify_peers_builders_available(broker_token,
900
 
                                        builders_only=builders_only)
901
 
    else:
902
 
        log("Skipping notify peer relations", level=DEBUG)
903
 
 
904
 
 
905
 
def cluster_sync_rings(peers_only=False, builders_only=False):
906
 
    """Notify peer relations that they should stop their proxy services.
907
 
 
908
 
    Peer units will then be expected to do a relation_set with
909
 
    stop-proxy-service-ack set rq value. Once all peers have responded, the
910
 
    leader will send out notification to all relations that rings are available
911
 
    for sync.
912
 
 
913
 
    If peers_only is True, only peer units will be synced. This is typically
914
 
    used when only builder files have been changed.
915
 
 
916
 
    This should only be called by the leader unit.
917
 
    """
918
 
    if not is_elected_leader(SWIFT_HA_RES):
919
 
        # Only the leader can do this.
920
 
        return
921
 
 
922
 
    if not peer_units():
923
 
        # If we have no peer units just go ahead and broadcast to storage
924
 
        # relations. If we have been instructed to only broadcast to peers this
925
 
        # should do nothing.
926
 
        broker_token = get_broker_token()
927
 
        broadcast_rings_available(broker_token, peers=False,
928
 
                                  storage=not peers_only)
929
 
        return
930
 
    elif builders_only:
931
 
        # No need to stop proxies if only syncing builders between peers.
932
 
        broker_token = get_broker_token()
933
 
        broadcast_rings_available(broker_token, storage=False,
934
 
                                  builders_only=builders_only)
935
 
        return
936
 
 
937
 
    rel_ids = relation_ids('cluster')
938
 
    trigger = str(uuid.uuid4())
939
 
 
940
 
    log("Sending request to stop proxy service to all peers (%s)" % (trigger),
941
 
        level=INFO)
942
 
    rq = SwiftProxyClusterRPC().stop_proxy_request(peers_only)
943
 
    for rid in rel_ids:
944
 
        relation_set(relation_id=rid, relation_settings=rq)
945
 
 
946
 
 
947
 
def notify_storage_rings_available():
948
 
    """Notify peer swift-storage relations that they should synchronise ring
949
 
    and builder files.
950
 
 
951
 
    Note that this should only be called from the leader unit.
952
 
    """
953
 
    if not is_elected_leader(SWIFT_HA_RES):
954
 
        log("Ring availability storage-relation broadcast requested by "
955
 
            "non-leader - skipping", level=WARNING)
956
 
        return
957
 
 
958
 
    hostname = get_hostaddr()
959
 
    hostname = format_ipv6_addr(hostname) or hostname
960
 
    path = os.path.basename(get_www_dir())
961
 
    rings_url = 'http://%s/%s' % (hostname, path)
962
 
    trigger = uuid.uuid4()
963
 
    # Notify storage nodes that there is a new ring to fetch.
964
 
    log("Notifying storage nodes that new ring is ready for sync.", level=INFO)
965
 
    for relid in relation_ids('swift-storage'):
966
 
        relation_set(relation_id=relid, swift_hash=get_swift_hash(),
967
 
                     rings_url=rings_url, trigger=trigger)
968
 
 
969
 
 
970
 
def fully_synced():
971
 
    """Check that we have all the rings and builders synced from the leader.
972
 
 
973
 
    Returns True if we have all rings and builders.
974
 
    """
975
 
    not_synced = []
976
 
    for ring, builder in SWIFT_RINGS.iteritems():
977
 
        if not os.path.exists(builder):
978
 
            not_synced.append(builder)
979
 
 
980
 
        ringfile = os.path.join(SWIFT_CONF_DIR,
981
 
                                '%s.%s' % (ring, SWIFT_RING_EXT))
982
 
        if not os.path.exists(ringfile):
983
 
            not_synced.append(ringfile)
984
 
 
985
 
    if not_synced:
986
 
        log("Not yet synced: %s" % ', '.join(not_synced), level=INFO)
987
 
        return False
988
 
 
989
 
    return True
990
 
 
991
 
 
992
 
def get_hostaddr():
993
 
    if config('prefer-ipv6'):
994
 
        return get_ipv6_addr(exc_list=[config('vip')])[0]
995
 
 
996
 
    return unit_get('private-address')
997
 
 
998
 
 
999
 
def is_paused(status_get=status_get):
1000
 
    """Is the unit paused?"""
1001
 
    with HookData()():
1002
 
        if kv().get('unit-paused'):
1003
 
            return True
1004
 
        else:
1005
 
            return False
1006
 
 
1007
 
 
1008
 
def pause_aware_restart_on_change(restart_map):
1009
 
    """Avoids restarting services if config changes when unit is paused."""
1010
 
    def wrapper(f):
1011
 
        if is_paused():
1012
 
            return f
1013
 
        else:
1014
 
            return restart_on_change(restart_map)(f)
1015
 
    return wrapper
1016
 
 
1017
 
 
1018
 
def has_minimum_zones(rings):
1019
 
    """Determine if enough zones exist to satisfy minimum replicas"""
1020
 
    for ring in rings:
1021
 
        if not os.path.isfile(ring):
1022
 
            return False
1023
 
        builder = _load_builder(ring).to_dict()
1024
 
        replicas = builder['replicas']
1025
 
        zones = [dev['zone'] for dev in builder['devs']]
1026
 
        num_zones = len(set(zones))
1027
 
        if num_zones < replicas:
1028
 
            log("Not enough zones (%d) defined to satisfy minimum replicas "
1029
 
                "(need >= %d)" % (num_zones, replicas), level=INFO)
1030
 
            return False
1031
 
 
1032
 
    return True
1033
 
 
1034
 
 
1035
 
def assess_status(configs):
1036
 
    """Assess status of current unit"""
1037
 
    required_interfaces = {}
1038
 
 
1039
 
    if is_paused():
1040
 
        status_set("maintenance",
1041
 
                   "Paused. Use 'resume' action to resume normal service.")
1042
 
        return
1043
 
 
1044
 
    # Check for required swift-storage relation
1045
 
    if len(relation_ids('swift-storage')) < 1:
1046
 
        status_set('blocked', 'Missing relation: storage')
1047
 
        return
1048
 
 
1049
 
    # Verify allowed_hosts is populated with enough unit IP addresses
1050
 
    ctxt = SwiftRingContext()()
1051
 
    if len(ctxt['allowed_hosts']) < config('replicas'):
1052
 
        status_set('blocked', 'Not enough related storage nodes')
1053
 
        return
1054
 
 
1055
 
    # Verify there are enough storage zones to satisfy minimum replicas
1056
 
    rings = [r for r in SWIFT_RINGS.itervalues()]
1057
 
    if not has_minimum_zones(rings):
1058
 
        status_set('blocked', 'Not enough storage zones for minimum replicas')
1059
 
        return
1060
 
 
1061
 
    if config('prefer-ipv6'):
1062
 
        for rid in relation_ids('swift-storage'):
1063
 
            for unit in related_units(rid):
1064
 
                addr = relation_get(attribute='private-address', unit=unit,
1065
 
                                    rid=rid)
1066
 
                if not is_ipv6(addr):
1067
 
                    status_set('blocked', 'Did not get IPv6 address from '
1068
 
                               'storage relation (got=%s)' % (addr))
1069
 
                    return
1070
 
 
1071
 
    if relation_ids('identity-service'):
1072
 
        required_interfaces['identity'] = ['identity-service']
1073
 
 
1074
 
    if required_interfaces:
1075
 
        set_os_workload_status(configs, required_interfaces)
1076
 
    else:
1077
 
        status_set('active', 'Unit is ready')