
This patchset adds a support for Swift Global Cluster feature as described at: https://docs.openstack.org/swift/latest/overview_global_cluster.html It allows specifying affinity settings as parrt of the deployment. Moreover, the master - slave relation is introduced for the purpose of rings distribution across proxy nodes participating in the Swift Global Cluster. Change-Id: I406445493e2226aa5ae40a09c9053ac8633a46e9 Closes-Bug: 1815879 Depends-On: I11b6c7802e5bfbd61b06e4d11c65804a165781b6
296 lines
8.9 KiB
Python
Executable File
296 lines
8.9 KiB
Python
Executable File
# Copyright 2016 Canonical Ltd
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from __future__ import print_function
|
|
|
|
try:
|
|
import cPickle as pickle
|
|
except ModuleNotFoundError:
|
|
import _pickle as pickle
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
|
|
_usage = """This file is called from the swift_utils.py file to implement
|
|
various swift ring builder calls and functions. It is called with one
|
|
parameter which is a json encoded string that contains the 'arguments' string
|
|
with the following parameters:
|
|
|
|
{
|
|
'path': The function that needs ot be performed
|
|
'args': the non-keyword argument to supply to the swift manager call.
|
|
'kwargs': any keyword args to supply to the swift manager call.
|
|
}
|
|
|
|
The result of the call, or an error, is returned as a json encoded result that
|
|
is printed to the STDOUT, Any errors are printed to STDERR.
|
|
|
|
The format of the output has the same keys as, but in a compressed form:
|
|
|
|
{
|
|
'result': <whatever the result of the function call was>
|
|
'error': <if an error occured, the text of the error
|
|
}
|
|
|
|
This system is currently needed to decouple the majority of the charm from the
|
|
underlying package being used for keystone.
|
|
"""
|
|
|
|
JSON_ENCODE_OPTIONS = dict(
|
|
sort_keys=True,
|
|
allow_nan=False,
|
|
indent=None,
|
|
separators=(',', ':'),
|
|
)
|
|
|
|
|
|
# These are the main 'API' functions that are called in the manager.py file
|
|
|
|
def initialize_ring(path, part_power, replicas, min_hours):
|
|
"""Initialize a new swift ring with given parameters."""
|
|
from swift.common.ring import RingBuilder
|
|
ring = RingBuilder(part_power, replicas, min_hours)
|
|
_write_ring(ring, path)
|
|
|
|
|
|
def exists_in_ring(ring_path, node):
|
|
"""Return boolean True if the node exists in the ring defined by the
|
|
ring_path.
|
|
|
|
:param ring_path: the file representing the ring
|
|
:param node: a dictionary of the node (ip, region, port, zone, weight,
|
|
device)
|
|
:returns: boolean
|
|
"""
|
|
ring = _load_builder(ring_path).to_dict()
|
|
|
|
for dev in ring['devs']:
|
|
# Devices in the ring can be None if there are holes from previously
|
|
# removed devices so skip any that are None.
|
|
if not dev:
|
|
continue
|
|
d = [(i, dev[i]) for i in dev if i in node and i != 'zone']
|
|
n = [(i, node[i]) for i in node if i in dev and i != 'zone']
|
|
if sorted(d) == sorted(n):
|
|
return True
|
|
return False
|
|
|
|
|
|
def add_dev(ring_path, dev):
|
|
"""Add a new device to the ring_path
|
|
|
|
The dev is in the form of:
|
|
|
|
new_dev = {
|
|
'region': node['region'],
|
|
'zone': node['zone'],
|
|
'ip': node['ip'],
|
|
'replication_ip': node['ip_rep']
|
|
'port': port,
|
|
'replication_port': port_rep,
|
|
'device': node['device'],
|
|
'weight': 100,
|
|
'meta': '',
|
|
}
|
|
|
|
:param ring_path: a ring_path for _load_builder
|
|
:parm dev: the device in the above format
|
|
"""
|
|
ring = _load_builder(ring_path)
|
|
ring.add_dev(dev)
|
|
_write_ring(ring, ring_path)
|
|
|
|
|
|
def get_min_part_hours(ring_path):
|
|
"""Get the min_part_hours for a ring
|
|
|
|
:param ring_path: The path for the ring
|
|
:returns: integer that is the min_part_hours
|
|
"""
|
|
builder = _load_builder(ring_path)
|
|
return builder.min_part_hours
|
|
|
|
|
|
def get_current_replicas(ring_path):
|
|
""" Gets replicas from the ring (lp1815879)
|
|
|
|
:param ring_path: The path for the ring
|
|
:type ring_path: str
|
|
:returns: replicas
|
|
:rtype: int
|
|
"""
|
|
builder = _load_builder(ring_path)
|
|
return builder.min_part_hours
|
|
|
|
|
|
def get_zone(ring_path):
|
|
"""Determine the zone for the ring_path
|
|
|
|
If there is no zone in the ring's devices, then simple return 1 as the
|
|
first zone.
|
|
|
|
Otherwise, return the lowest numerically ordered unique zone being used
|
|
across the devices of the ring if the number of unique zones is less that
|
|
the number of replicas for that ring.
|
|
|
|
If the replicas >= to the number of unique zones, the if all the zones are
|
|
equal, start again at 1.
|
|
|
|
Otherwise, if the zones aren't equal, return the lowest zone number across
|
|
the devices
|
|
|
|
:param ring_path: The path to the ring to get the zone for.
|
|
:returns: <integer> zone id
|
|
"""
|
|
builder = _load_builder(ring_path)
|
|
replicas = builder.replicas
|
|
zones = [d['zone'] for d in builder.devs]
|
|
if not zones:
|
|
return 1
|
|
|
|
# zones is a per-device list, so we may have one
|
|
# node with 3 devices in zone 1. For balancing
|
|
# we need to track the unique zones being used
|
|
# not necessarily the number of devices
|
|
unique_zones = list(set(zones))
|
|
if len(unique_zones) < replicas:
|
|
return sorted(unique_zones).pop() + 1
|
|
|
|
zone_distrib = {}
|
|
for z in zones:
|
|
zone_distrib[z] = zone_distrib.get(z, 0) + 1
|
|
|
|
if len(set(zone_distrib.values())) == 1:
|
|
# all zones are equal, start assigning to zone 1 again.
|
|
return 1
|
|
|
|
return sorted(zone_distrib, key=zone_distrib.get).pop(0)
|
|
|
|
|
|
def has_minimum_zones(rings):
|
|
"""Determine if enough zones exist to satisfy minimum replicas
|
|
|
|
Returns a structure with:
|
|
|
|
{
|
|
"result": boolean,
|
|
"log": <Not present> | string to log to the debug_log
|
|
"level": <string>
|
|
}
|
|
|
|
:param rings: list of strings of the ring_path
|
|
:returns: structure with boolean and possible log
|
|
"""
|
|
for ring in rings:
|
|
if not os.path.isfile(ring):
|
|
return {
|
|
"result": False
|
|
}
|
|
builder = _load_builder(ring).to_dict()
|
|
if not builder['devs']:
|
|
return {
|
|
"result": False
|
|
}
|
|
replicas = builder['replicas']
|
|
regions = [dev['region'] for dev in builder['devs'] if dev]
|
|
zones = [dev['zone'] for dev in builder['devs'] if dev]
|
|
num_regions = len(set(regions))
|
|
num_zones = len(set(zones))
|
|
num_zones_in_regions = num_regions * num_zones
|
|
if num_zones_in_regions < replicas:
|
|
log = ("Not enough zones ({}) defined to satisfy minimum "
|
|
"replicas (need >= {})".format(num_zones, int(replicas)))
|
|
return {
|
|
"result": False,
|
|
"log": log,
|
|
"level": "INFO",
|
|
}
|
|
|
|
return {
|
|
"result": True
|
|
}
|
|
|
|
|
|
# These are utility functions that are for the 'API' functions above (i.e. they
|
|
# are not called from the main function)
|
|
|
|
def _load_builder(path):
|
|
# lifted straight from /usr/bin/swift-ring-builder
|
|
from swift.common.ring import RingBuilder
|
|
try:
|
|
builder = pickle.load(open(path, 'rb'))
|
|
if not hasattr(builder, 'devs'):
|
|
builder_dict = builder
|
|
builder = RingBuilder(1, 1, 1)
|
|
builder.copy_from(builder_dict)
|
|
except ImportError: # Happens with really old builder pickles
|
|
builder = RingBuilder(1, 1, 1)
|
|
builder.copy_from(pickle.load(open(path, 'rb')))
|
|
for dev in builder.devs:
|
|
if dev and 'meta' not in dev:
|
|
dev['meta'] = ''
|
|
|
|
return builder
|
|
|
|
|
|
def _write_ring(ring, ring_path):
|
|
with open(ring_path, "wb") as fd:
|
|
pickle.dump(ring.to_dict(), fd, protocol=2)
|
|
|
|
|
|
# The following code is just the glue to link the manager.py and swift_utils.py
|
|
# files together at a 'python' function level.
|
|
|
|
|
|
class ManagerException(Exception):
|
|
pass
|
|
|
|
|
|
if __name__ == '__main__':
|
|
# This script needs 1 argument which is the input json. See file header
|
|
# for details on how it is called. It returns a JSON encoded result, in
|
|
# the same file, which is overwritten
|
|
result = None
|
|
try:
|
|
if len(sys.argv) != 2:
|
|
raise ManagerException(
|
|
"{} called without 2 arguments: must pass the filename"
|
|
.format(__file__))
|
|
spec = json.loads(sys.argv[1])
|
|
_callable = sys.modules[__name__]
|
|
for attr in spec['path']:
|
|
_callable = getattr(_callable, attr)
|
|
# now make the call and return the arguments
|
|
result = {'result': _callable(*spec['args'], **spec['kwargs'])}
|
|
except ManagerException as e:
|
|
# deal with sending an error back.
|
|
print(str(e), file=sys.stderr)
|
|
import traceback
|
|
print(traceback.format_exc(), file=sys.stderr)
|
|
result = {'error': str(e)}
|
|
except Exception as e:
|
|
print("{}: something went wrong: {}".format(__file__, str(e)),
|
|
file=sys.stderr)
|
|
import traceback
|
|
print(traceback.format_exc(), file=sys.stderr)
|
|
result = {'error': str(e)}
|
|
finally:
|
|
if result is not None:
|
|
result_json = json.dumps(result, **JSON_ENCODE_OPTIONS)
|
|
print(result_json)
|
|
|
|
# normal exit
|
|
sys.exit(0)
|