# 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)