# 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': 'error': = 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: 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": | string to log to the debug_log "level": } :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() replicas = builder['replicas'] zones = [dev['zone'] for dev in builder['devs'] if dev] num_zones = len(set(zones)) if num_zones < replicas: log = ("Not enough zones ({:d}) defined to satisfy minimum " "replicas (need >= {:d})".format(num_zones, 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)