12
from collections import OrderedDict
13
from swift_context import (
23
import charmhelpers.contrib.openstack.context as context
24
import charmhelpers.contrib.openstack.templating as templating
25
from charmhelpers.contrib.openstack.utils import (
27
get_os_codename_package,
28
get_os_codename_install_source,
29
configure_installation_source,
30
set_os_workload_status,
32
from charmhelpers.contrib.hahelpers.cluster import (
36
from charmhelpers.core.hookenv import (
50
from charmhelpers.fetch import (
56
from charmhelpers.core.host import (
60
from charmhelpers.contrib.network.ip import (
65
from charmhelpers.core.decorators import (
68
from charmhelpers.core.unitdata import (
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')
90
WWW_DIR = '/var/www/swift-rings'
91
ALTERNATE_WWW_DIR = '/var/www/html/swift-rings'
93
RING_SYNC_SEMAPHORE = threading.Semaphore()
97
if os.path.isdir(os.path.dirname(ALTERNATE_WWW_DIR)):
98
return ALTERNATE_WWW_DIR
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')
109
SSL_CERT = os.path.join(SWIFT_CONF_DIR, 'cert.crt')
110
SSL_KEY = os.path.join(SWIFT_CONF_DIR, 'cert.key')
120
# > Folsom specific packages
121
FOLSOM_PACKAGES = BASE_PACKAGES + ['swift-plugin-s3']
123
SWIFT_HA_RES = 'grp_swift_vips'
124
TEMPLATES = 'templates/'
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([
130
'hook_contexts': [SwiftHashContext()],
131
'services': ['swift-proxy'],
134
'hook_contexts': [SwiftIdentityContext(),
135
context.BindHostContext()],
136
'services': ['swift-proxy'],
139
'hook_contexts': [context.HAProxyContext(singlenode_mode=True),
141
'services': ['haproxy'],
144
'hook_contexts': [SwiftRingContext()],
145
'services': ['apache2'],
147
(SWIFT_RINGS_24_CONF, {
148
'hook_contexts': [SwiftRingContext()],
149
'services': ['apache2'],
152
'hook_contexts': [ApacheSSLContext()],
153
'services': ['apache2'],
155
(APACHE_SITE_24_CONF, {
156
'hook_contexts': [ApacheSSLContext()],
157
'services': ['apache2'],
160
'hook_contexts': [MemcachedContext()],
161
'services': ['memcached'],
166
class SwiftProxyCharmException(Exception):
170
class SwiftProxyClusterRPC(object):
171
"""Provides cluster relation rpc dicts.
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.
177
NOTE: these are only intended to be used from cluster peer relations.
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'
184
def __init__(self, version=1):
185
self._version = version
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,
197
'sync-only-builders': None}}
198
return copy.deepcopy(templates[self._version])
200
def stop_proxy_request(self, peers_only=False):
201
"""Request to stop peer proxy service.
206
rq['trigger'] = str(uuid.uuid4())
207
rq[self.KEY_STOP_PROXY_SVC] = rq['trigger']
213
def stop_proxy_ack(self, echo_token, echo_peers_only):
214
"""Ack that peer proxy service is stopped.
216
NOTE: non-leader action
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
225
def sync_rings_request(self, broker_host, broker_token,
226
builders_only=False):
227
"""Request for peer to sync rings.
232
rq['trigger'] = str(uuid.uuid4())
235
rq['sync-only-builders'] = 1
237
rq['broker-token'] = broker_token
238
rq['builder-broker'] = broker_host
241
def notify_leader_changed(self):
242
"""Notify peers that leader has changed.
247
rq['trigger'] = str(uuid.uuid4())
248
rq[self.KEY_NOTIFY_LEADER_CHANGED] = rq['trigger']
252
def get_first_available_value(responses, key, default=None):
260
def all_responses_equal(responses, key, must_exist=True):
261
"""If key exists in responses, all values for it must be equal.
263
If all equal return True. If key does not exist and must_exist is True
264
return False otherwise True.
270
_val = r.get(key, sentinel)
271
if val is not None and val != _val:
274
elif _val != sentinel:
277
if must_exist and val is None:
283
log("Responses not all equal for key '%s'" % (key), level=DEBUG)
287
def register_configs():
288
"""Register config files with their respective contexts.
290
Registration of some configs may not be required depending on
291
existing of certain relations.
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) \
298
configs = templating.OSConfigRenderer(templates_dir=TEMPLATES,
299
openstack_release=release)
307
configs.register(conf, CONFIG_FILES[conf]['hook_contexts'])
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'])
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'])
323
"""Determine the correct resource map to be passed to
324
charmhelpers.core.restart_on_change() based on the services configured.
326
:returns dict: A dictionary mapping config file to lists of services
327
that should be restarted when file changes.
330
for f, ctxt in CONFIG_FILES.iteritems():
332
for svc in ctxt['services']:
335
_map.append((f, svcs))
337
return OrderedDict(_map)
341
''' Returns a list of services associate with this charm '''
343
for v in restart_map().values():
344
_services = _services + v
345
return list(set(_services))
348
def swift_user(username='swift'):
349
user = pwd.getpwnam(username)
350
return (user.pw_uid, user.pw_gid)
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)
357
uid, gid = swift_user()
358
os.chown(conf_dir, uid, gid)
361
def determine_packages(release):
362
"""Determine what packages are needed for a given OpenStack release."""
363
if release == 'essex':
365
elif release == 'folsom':
366
return FOLSOM_PACKAGES
367
elif release == 'grizzly':
368
return FOLSOM_PACKAGES
370
return FOLSOM_PACKAGES
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
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:
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)
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)]
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)
413
def exists_in_ring(ring_path, node):
414
ring = _load_builder(ring_path).to_dict()
415
node['port'] = ring_port(ring_path, node)
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):
422
log('Node already exists in ring (%s).' % ring_path, level=INFO)
428
def add_to_ring(ring_path, node):
429
ring = _load_builder(ring_path)
430
port = ring_port(ring_path, node)
432
devs = ring.to_dict()['devs']
435
next_id = len([d['id'] for d in devs])
439
'zone': node['zone'],
442
'device': node['device'],
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)
452
def _get_zone(ring_builder):
453
replicas = ring_builder.replicas
454
zones = [d['zone'] for d in ring_builder.devs]
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
468
zone_distrib[z] = zone_distrib.get(z, 0) + 1
470
if len(set([total for total in zone_distrib.itervalues()])) == 1:
471
# all zones are equal, start assigning to zone 1 again.
474
return sorted(zone_distrib, key=zone_distrib.get).pop(0)
477
def get_min_part_hours(ring):
478
builder = _load_builder(ring)
479
return builder.min_part_hours
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)
488
msg = ("Failed to set min_part_hours=%s on %s" % (value, path))
489
raise SwiftProxyCharmException(msg)
492
def get_zone(assignment_policy):
493
"""Determine appropriate zone based on configured assignment policy.
495
Manual assignment relies on each storage zone being deployed as a
496
separate service unit with its desired zone set as a configuration
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.
504
if assignment_policy == 'manual':
505
return relation_get('zone')
506
elif assignment_policy == 'auto':
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()
513
msg = ('Invalid zone assignment policy: %s' % assignment_policy)
514
raise SwiftProxyCharmException(msg)
517
def balance_ring(ring_path):
520
Returns True if it needs redistribution.
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)
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:
535
# NOTE: Balance of 166.67 indicates you should push this ring, wait
536
# at least 0 hours, and rebalance/repush.
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.
542
msg = ('balance_ring: %s returned %s' % (cmd, rc))
543
raise SwiftProxyCharmException(msg)
546
def should_balance(rings):
547
"""Determine whether or not a re-balance is required and allowed.
549
Ring balance can be disabled/postponed using the disable-ring-balance
552
Otherwise, using zones vs min. replicas, determine whether or not the rings
555
if config('disable-ring-balance'):
558
return has_minimum_zones(rings)
561
def do_openstack_upgrade(configs):
562
new_src = config('openstack-origin')
563
new_os_rel = get_os_codename_install_source(new_src)
565
log('Performing OpenStack upgrade to %s.' % (new_os_rel), level=DEBUG)
566
configure_installation_source(new_src)
568
'--option', 'Dpkg::Options::=--force-confnew',
569
'--option', 'Dpkg::Options::=--force-confdef',
572
apt_upgrade(options=dpkg_opts, fatal=True, dist=True)
573
configs.set_release(openstack_release=new_os_rel)
578
"""Validate that we can support IPv6 mode.
580
This should be called if prefer-ipv6 is True to ensure that we are running
581
in an environment that supports ipv6.
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 "
587
raise SwiftProxyCharmException(msg)
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 '
595
apt_install('haproxy/trusty-backports', fatal=True)
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.
604
Note that we sync the ring builder and .gz files since the builder itself
605
is linked to the underlying .gz ring.
607
log('Fetching swift rings & builders from proxy @ %s.' % broker_url,
609
target = SWIFT_CONF_DIR
611
tmpdir = tempfile.mkdtemp(prefix='swiftrings')
613
for server in ['account', 'object', 'container']:
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)
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)
632
# Once all have been successfully downloaded, move them to actual
635
os.rename(os.path.join(tmpdir, f), os.path.join(target, f))
637
shutil.rmtree(tmpdir)
640
def ensure_www_dir_permissions(www_dir):
641
if not os.path.isdir(www_dir):
642
os.mkdir(www_dir, 0o755)
644
os.chmod(www_dir, 0o755)
646
uid, gid = swift_user()
647
os.chown(www_dir, uid, gid)
650
def update_www_rings(rings=True, builders=True):
651
"""Copy rings to apache www dir.
653
Try to do this as atomically as possible to avoid races with storage nodes
656
if not (rings or builders):
659
tmp_dir = tempfile.mkdtemp(prefix='swift-rings-www-tmp')
660
for ring, builder_path in SWIFT_RINGS.iteritems():
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)
669
dst = os.path.join(tmp_dir, os.path.basename(builder_path))
670
shutil.copyfile(src, dst)
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)
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):
688
with open(path, 'rb') as fd:
689
sha.update(fd.read())
691
return sha.hexdigest()
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):
701
with open(builder, 'rb') as fd:
702
sha.update(fd.read())
704
return sha.hexdigest()
707
def get_broker_token():
708
"""Get ack token from peers to be used as broker token.
710
Must be equal across all peers.
712
Returns token or None if not found.
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,
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())
725
if not all(responses) or len(set(responses)) != 1:
726
log("Not all ack tokens equal - %s" % (responses), level=DEBUG)
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.
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)
742
# Ensure we don't do a double sync if we are nested.
744
if RING_SYNC_SEMAPHORE.acquire(blocking=0):
746
rings_before = get_rings_checksum()
747
builders_before = get_builders_checksum()
749
ret = f(*args, **kwargs)
754
rings_after = get_rings_checksum()
755
builders_after = get_builders_checksum()
757
rings_path = os.path.join(SWIFT_CONF_DIR, '*.%s' %
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:
767
cluster_sync_rings(peers_only=not rings_changed)
769
cluster_sync_rings(peers_only=True, builders_only=True)
770
log("Rings not ready for sync - syncing builders",
773
log("Rings/builders unchanged - skipping sync", level=DEBUG)
777
RING_SYNC_SEMAPHORE.release()
779
return _inner_sync_builders_and_rings_if_changed
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.
786
Also update min_part_hours if provided.
788
if not is_elected_leader(SWIFT_HA_RES):
789
log("Update rings called by non-leader - skipping", level=WARNING)
792
balance_required = False
795
# NOTE: no need to stop the proxy since we are not changing the rings,
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)
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
811
log(str(exc), level=WARNING)
813
balance_required = True
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
825
@sync_builders_and_rings_if_changed
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)
832
if not should_balance([r for r in SWIFT_RINGS.itervalues()]):
833
log("Not yet ready to balance rings - insufficient replicas?",
838
for path in SWIFT_RINGS.itervalues():
839
if balance_ring(path):
840
log('Balanced ring %s' % path, level=DEBUG)
843
log('Ring %s not rebalanced' % path, level=DEBUG)
846
log("Rings unchanged by rebalance", level=DEBUG)
847
# NOTE: checksum will tell for sure
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.
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))
861
def notify_peers_builders_available(broker_token, builders_only=False):
862
"""Notify peer swift-proxy units that they should synchronise ring and
865
Note that this should only be called from the leader unit.
867
if not is_elected_leader(SWIFT_HA_RES):
868
log("Ring availability peer broadcast requested by non-leader - "
869
"skipping", level=WARNING)
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,
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)
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.
889
We can opt to only notify peer or storage relations if needs be.
892
# TODO: get ack from storage units that they are synced before
894
notify_storage_rings_available()
896
log("Skipping notify storage relations", level=DEBUG)
899
notify_peers_builders_available(broker_token,
900
builders_only=builders_only)
902
log("Skipping notify peer relations", level=DEBUG)
905
def cluster_sync_rings(peers_only=False, builders_only=False):
906
"""Notify peer relations that they should stop their proxy services.
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
913
If peers_only is True, only peer units will be synced. This is typically
914
used when only builder files have been changed.
916
This should only be called by the leader unit.
918
if not is_elected_leader(SWIFT_HA_RES):
919
# Only the leader can do this.
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
926
broker_token = get_broker_token()
927
broadcast_rings_available(broker_token, peers=False,
928
storage=not peers_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)
937
rel_ids = relation_ids('cluster')
938
trigger = str(uuid.uuid4())
940
log("Sending request to stop proxy service to all peers (%s)" % (trigger),
942
rq = SwiftProxyClusterRPC().stop_proxy_request(peers_only)
944
relation_set(relation_id=rid, relation_settings=rq)
947
def notify_storage_rings_available():
948
"""Notify peer swift-storage relations that they should synchronise ring
951
Note that this should only be called from the leader unit.
953
if not is_elected_leader(SWIFT_HA_RES):
954
log("Ring availability storage-relation broadcast requested by "
955
"non-leader - skipping", level=WARNING)
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)
971
"""Check that we have all the rings and builders synced from the leader.
973
Returns True if we have all rings and builders.
976
for ring, builder in SWIFT_RINGS.iteritems():
977
if not os.path.exists(builder):
978
not_synced.append(builder)
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)
986
log("Not yet synced: %s" % ', '.join(not_synced), level=INFO)
993
if config('prefer-ipv6'):
994
return get_ipv6_addr(exc_list=[config('vip')])[0]
996
return unit_get('private-address')
999
def is_paused(status_get=status_get):
1000
"""Is the unit paused?"""
1002
if kv().get('unit-paused'):
1008
def pause_aware_restart_on_change(restart_map):
1009
"""Avoids restarting services if config changes when unit is paused."""
1014
return restart_on_change(restart_map)(f)
1018
def has_minimum_zones(rings):
1019
"""Determine if enough zones exist to satisfy minimum replicas"""
1021
if not os.path.isfile(ring):
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)
1035
def assess_status(configs):
1036
"""Assess status of current unit"""
1037
required_interfaces = {}
1040
status_set("maintenance",
1041
"Paused. Use 'resume' action to resume normal service.")
1044
# Check for required swift-storage relation
1045
if len(relation_ids('swift-storage')) < 1:
1046
status_set('blocked', 'Missing relation: storage')
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')
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')
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,
1066
if not is_ipv6(addr):
1067
status_set('blocked', 'Did not get IPv6 address from '
1068
'storage relation (got=%s)' % (addr))
1071
if relation_ids('identity-service'):
1072
required_interfaces['identity'] = ['identity-service']
1074
if required_interfaces:
1075
set_os_workload_status(configs, required_interfaces)
1077
status_set('active', 'Unit is ready')