def __init__(self, rms_conf):
     conf = rms_conf['ringmasterd']
     self.swiftdir = conf.get('swiftdir', '/etc/swift')
     self.builder_files = \
         {'account': conf.get('account_builder',
                              '/etc/swift/account.builder'),
          'container': conf.get('container_builder',
                                '/etc/swift/container.builder'),
          'object': conf.get('object_builder',
                             '/etc/swift/object.builder')}
     self.ring_files = \
         {'account': conf.get('account_ring',
                              '/etc/swift/account.ring.gz'),
          'container': conf.get('container_ring',
                                '/etc/swift/container.ring.gz'),
          'object': conf.get('object_ring',
                             '/etc/swift/object.ring.gz')}
     self.debug = conf.get('debug_mode', 'n') in TRUE_VALUES
     self.pause_file = conf.get('pause_file_path', '/tmp/.srm-pause')
     self.default_weight_shift = float(conf.get('default_weight_shift',
                                                '25.0'))
     self.backup_dir = conf.get('backup_dir', '/etc/swift/backups')
     self.recheck_interval = int(conf.get('interval', '120'))
     self.recheck_after_change_interval = int(conf.get('change_interval',
                                                       '3600'))
     self.mph_enabled = conf.get('min_part_hours_check', 'n') in TRUE_VALUES
     self.sec_since_modified = int(conf.get('min_seconds_since_change',
                                            '120'))
     self.balance_threshold = float(conf.get('balance_threshold', '2'))
     self.dispersion_cmd = conf.get('dispersion_cmd',
                                    '/usr/bin/swift-dispersion-report')
     self.dispersion_pct = {'container': float(conf.get('container_min_pct',
                                                        '99.75')),
                            'object': float(conf.get('object_min_pct',
                                                     '99.75'))}
     self.lock_timeout = int(conf.get('lock_timeout', '90'))
     window = conf.get('change_window', '0000,2400')
     self.change_window = [int(x) for x in window.split(',')]
     if self.debug:
         conf['log_level'] = 'DEBUG'
     self.logger = get_logger(conf, 'ringmasterd', self.debug)
     if not os.access(self.swiftdir, os.W_OK):
         self.logger.error('swift_dir is not writable. exiting!')
         sys.exit(1)
     if conf.get('email_notify', 'n') in TRUE_VALUES:
         self.email_notify = EmailNotify(conf, self.logger)
     else:
         self.email_notify = None
class RingMasterServer(object):

    def __init__(self, rms_conf):
        conf = rms_conf['ringmasterd']
        self.swiftdir = conf.get('swiftdir', '/etc/swift')
        self.builder_files = \
            {'account': conf.get('account_builder',
                                 '/etc/swift/account.builder'),
             'container': conf.get('container_builder',
                                   '/etc/swift/container.builder'),
             'object': conf.get('object_builder',
                                '/etc/swift/object.builder')}
        self.ring_files = \
            {'account': conf.get('account_ring',
                                 '/etc/swift/account.ring.gz'),
             'container': conf.get('container_ring',
                                   '/etc/swift/container.ring.gz'),
             'object': conf.get('object_ring',
                                '/etc/swift/object.ring.gz')}
        self.debug = conf.get('debug_mode', 'n') in TRUE_VALUES
        self.pause_file = conf.get('pause_file_path', '/tmp/.srm-pause')
        self.default_weight_shift = float(conf.get('default_weight_shift',
                                                   '25.0'))
        self.backup_dir = conf.get('backup_dir', '/etc/swift/backups')
        self.recheck_interval = int(conf.get('interval', '120'))
        self.recheck_after_change_interval = int(conf.get('change_interval',
                                                          '3600'))
        self.mph_enabled = conf.get('min_part_hours_check', 'n') in TRUE_VALUES
        self.sec_since_modified = int(conf.get('min_seconds_since_change',
                                               '120'))
        self.balance_threshold = float(conf.get('balance_threshold', '2'))
        self.dispersion_cmd = conf.get('dispersion_cmd',
                                       '/usr/bin/swift-dispersion-report')
        self.dispersion_pct = {'container': float(conf.get('container_min_pct',
                                                           '99.75')),
                               'object': float(conf.get('object_min_pct',
                                                        '99.75'))}
        self.lock_timeout = int(conf.get('lock_timeout', '90'))
        window = conf.get('change_window', '0000,2400')
        self.change_window = [int(x) for x in window.split(',')]
        if self.debug:
            conf['log_level'] = 'DEBUG'
        self.logger = get_logger(conf, 'ringmasterd', self.debug)
        if not os.access(self.swiftdir, os.W_OK):
            self.logger.error('swift_dir is not writable. exiting!')
            sys.exit(1)
        if conf.get('email_notify', 'n') in TRUE_VALUES:
            self.email_notify = EmailNotify(conf, self.logger)
        else:
            self.email_notify = None

    def _emit_notify(self, source, message):
        "Send out any configured notifications"
        if self.email_notify:
            self.email_notify.send_message(source, message)

    def pause_if_asked(self):
        """Check if pause file exists and sleep until its removed if it does"""
        if exists(self.pause_file):
            self.logger.notice('--> Pause file found. Pausing orchestration!')
            while exists(self.pause_file):
                sleep(1)
            self.logger.notice('--> Pause removed. Resuming orchestration!')

    def rebalance_ring(self, builder):
        """Rebalance a ring

        :param builder: builder to rebalance
        :returns: True on successful rebalance, False if it fails.
        """
        self.pause_if_asked()
        devs_changed = builder.devs_changed
        try:
            last_balance = builder.get_balance()
            parts, balance = builder.rebalance()
        except exceptions.RingBuilderError:
            self.logger.error("-> Rebalance failed!")
            self.logger.exception('RingBuilderError')
            return False
        if not parts:
            self.logger.notice("-> No partitions reassigned!")
            self.logger.notice("-> (%d/%.02f)" % (parts, balance))
            return False
        if not devs_changed and abs(last_balance - balance) < 1:
            self.logger.notice("-> Rebalance failed to change more than 1%!")
            return False
        self.logger.notice('--> Reassigned %d (%.02f%%) partitions. Balance '
                           'is %.02f.' % (parts, 100.0 * parts / builder.parts,
                                          balance))
        return True

    def adjust_ring(self, builder):
        """Adjust device weights in a ring

        :param builder: builder to adjust
        """
        self.pause_if_asked()
        for dev in builder.devs:
            if not dev:
                continue
            if 'target_weight' in dev:
                if 'weight_shift' in dev:
                    weight_shift = dev['weight_shift']
                else:
                    weight_shift = self.default_weight_shift
                if dev['weight'] == dev['target_weight']:
                    continue
                elif dev['weight'] < dev['target_weight']:
                    if dev['weight'] + weight_shift \
                            < dev['target_weight']:
                        builder.set_dev_weight(
                            dev['id'], dev['weight'] + weight_shift)
                    else:
                        builder.set_dev_weight(dev['id'], dev['target_weight'])
                    self.logger.debug(
                        "--> [%s/%s] ++ weight to %s" % (dev['ip'],
                                                         dev['device'],
                                                         dev['weight']))
                elif dev['weight'] > dev['target_weight']:
                    if dev['weight'] - weight_shift \
                            > dev['target_weight']:
                        builder.set_dev_weight(
                            dev['id'], dev['weight'] - weight_shift)
                    else:
                        builder.set_dev_weight(dev['id'], dev['target_weight'])
                    self.logger.debug(
                        "--> [%s/%s] -- weight to %s" % (dev['ip'],
                                                         dev['device'],
                                                         dev['weight']))

    def ring_requires_change(self, builder):
        """Check if a ring requires changes

        :param builder: builder who's devices to check
        :returns: True if ring requires change
        """
        self.pause_if_asked()
        if builder.devs_changed:
            return True
        if not self.ring_balance_ok(builder):
            return True
        for dev in builder.devs:
            if not dev:
                continue
            if 'target_weight' in dev:
                if dev['weight'] != dev['target_weight']:
                    self.logger.debug("--> [%s] weight %s | target %s"
                                      % (
                                      dev['ip'] + '/' +
                                      dev['device'], dev['weight'],
                                      dev['target_weight']))
                    return True
        return False

    def in_change_window(self):
        """Check if we are within the allowed time window for a change"""
        start = self.change_window[0]
        end = self.change_window[1]
        now = gmtime().tm_hour + gmtime().tm_min
        if start <= end:
            return start <= now <= end
        else:
            return start <= now or now <= end

    def dispersion_ok(self, swift_type):
        """Run a dispersion report and check whether its 'ok'

        :param swift_type: either 'container' or 'object'
        :returns: True if the dispersion report is 'ok'
        """
        self.pause_if_asked()
        if swift_type == 'account':
            return True
        self.logger.debug("--> Running %s dispersion report" % swift_type)
        dsp_cmd = [self.dispersion_cmd, '-j', '--%s-only' % swift_type]
        try:
            result = json.loads(subprocess.Popen(dsp_cmd,
                                stdout=subprocess.PIPE).communicate()[0])
        except Exception:
            self.logger.exception('Error running dispersion report')
            return False
        if not result[swift_type]:
            self.logger.notice("--> Dispersion report run returned nothing!")
            return False
        self.logger.debug("--> Dispersion info: %s" % result)
        #the dsp report json output has changed a bit so we have to check for all
        if not result[swift_type].get('missing_2', 0) == 0 and \
                result[swift_type].get('missing_3', 0) == 0 and \
                result[swift_type].get('missing_all', 0) == 0:
            return False
        if result[swift_type]['pct_found'] > self.dispersion_pct[swift_type]:
            return True
        else:
            return False

    def min_part_hours_ok(self, builder):
        """Check if min part hours has elapsed

        :param builder: builder to check
        :returns: True if min part hours have elapsed
        """
        self.pause_if_asked()
        elapsed_hours = int(time() - builder._last_part_moves_epoch) / 3600
        self.logger.debug('--> partitions last moved %d hours ago [%s]'
                          % (elapsed_hours, datetime.utcfromtimestamp(
                             builder._last_part_moves_epoch)))
        if elapsed_hours > builder.min_part_hours:
            return True
        else:
            return False

    def min_modify_time(self, btype):
        """Check if minimum modify time has passed

        :param btype: builder to check one of account|container|object
        :returns: True if min modify time has elapsed
        """
        self.pause_if_asked()
        since_modified = time() - stat(self.builder_files[btype]).st_mtime
        self.logger.debug(
            '--> Ring last modified %d seconds ago.' % since_modified)
        if since_modified > self.sec_since_modified:
            return True
        else:
            return False

    def ring_balance_ok(self, builder):
        """Check if ring balance is ok

        :param builder: builder to check
        :returns: True ring balance is ok
        """
        self.pause_if_asked()
        self.logger.debug(
            '--> Current balance: %.02f' % builder.get_balance())
        return builder.get_balance() <= self.balance_threshold

    def write_builder(self, btype, builder):
        """Write out new builder file

        :param btype: The builder type
        :param builder: The builder to dump
        :returns: new ring file md5
        """
        self.pause_if_asked()
        builder_file = self.builder_files[btype]
        try:
            fd, tmppath = mkstemp(dir=self.swiftdir, suffix='.tmp.builder')
            pickle.dump(builder.to_dict(), fdopen(fd, 'wb'), protocol=2)
            backup, backup_md5 = make_backup(builder_file, self.backup_dir)
            self.logger.notice('--> Backed up %s to %s (%s)' %
                              (builder_file, backup, backup_md5))
            chmod(tmppath, 0644)
            rename(tmppath, builder_file)
        except Exception as err:
            raise Exception('Error writing builder: %s' % err)
        finally:
            if fd:
                try:
                    close(fd)
                except OSError:
                    pass
            if tmppath:
                try:
                    unlink(tmppath)
                except OSError:
                    pass
        return get_md5sum(builder_file)

    def write_ring(self, btype, builder):
        """Write out new ring files

        :param btype: The builder type
        :param builder: The builder to dump
        :returns: new ring file md5
        """
        try:
            self.pause_if_asked()
            ring_file = self.ring_files[btype]
            fd, tmppath = mkstemp(dir=self.swiftdir, suffix='.tmp.ring.gz')
            builder.get_ring().save(tmppath)
            close(fd)
            if not is_valid_ring(tmppath):
                unlink(tmppath)
                raise Exception('Ring Validate Failed')
            backup, backup_md5 = make_backup(ring_file, self.backup_dir)
            self.logger.notice('--> Backed up %s to %s (%s)' %
                              (ring_file, backup, backup_md5))
            chmod(tmppath, 0644)
            rename(tmppath, ring_file)
        except Exception as err:
            raise Exception('Error writing builder: %s' % err)
        finally:
            if fd:
                try:
                    close(fd)
                except OSError:
                    pass
            if tmppath:
                try:
                    unlink(tmppath)
                except OSError:
                    pass
        return get_md5sum(ring_file)

    def orchestration_pass(self, btype):
        """Check the rings, make any needed adjustments, and deploy the ring

        :param btype: The builder type to work on.
        :return: True if the builder was modified , False if it was not
        """
        self.pause_if_asked()
        self.logger.debug("=" * 79)
        self.logger.notice("Checking on %s ring..." % btype)
        self.logger.debug("=" * 79)
        builder = RingBuilder.load(self.builder_files[btype])
        if self.ring_requires_change(builder):
            self.logger.notice("[%s] -> ring requires weight change." % btype)

            if self.mph_enabled:
                if not self.min_part_hours_ok(builder):
                    self.logger.notice(
                        "[%s] -> Ring min_part_hours: not ready!" % btype)
                    return False
                else:
                    self.logger.notice(
                        "[%s] -> Ring min_part_hours: ok" % btype)

            if not self.min_modify_time(btype):
                self.logger.notice(
                    "[%s] -> Ring last modify time: not ready!" % btype)
                return False
            else:
                self.logger.notice("[%s] -> Ring last modify time: ok" % btype)

            if not self.dispersion_ok(btype):
                self.logger.notice(
                    "[%s] -> Dispersion report: not ready!" % btype)
                return False
            else:
                self.logger.notice("[%s] -> Dispersion report: ok" % btype)

            if self.ring_balance_ok(builder):
                self.logger.notice("[%s] -> Current Ring balance: ok" % btype)
                self.logger.notice("[%s] -> Adjusting ring..." % btype)
                self.adjust_ring(builder)
                self.logger.notice("[%s] -> Rebalancing ring..." % btype)
                rebalanced = self.rebalance_ring(builder)
                if not rebalanced:
                    self.logger.notice("[%s] -> Rebalance: not ready!" % btype)
                    return True  # we should sleep a bit longer
                else:
                    self.logger.notice("[%s] -> Rebalance: ok" % btype)
            else:
                self.logger.notice(
                    "[%s] -> Current Ring balance: not ready!" % btype)
                self.logger.notice('[%s] -> Rebalancing ring with no '
                                   'modifications...' % btype)
                rebalanced = self.rebalance_ring(builder)
                if not rebalanced:
                    self.logger.notice(
                        "[%s] -> Rebalance: not ready!" % btype)
                    return True  # we should sleep a bit longer
                else:
                    self.logger.notice("[%s] -> Rebalance: ok" % btype)
            self.logger.notice("[%s] -> Writing builder..." % btype)
            try:
                builder_md5 = self.write_builder(btype, builder)
                self.logger.notice('[%s] --> Wrote new builder with md5: '
                                   '%s' % (btype, builder_md5))
                self.logger.notice("[%s] -> Writing ring..." % btype)
                ring_md5 = self.write_ring(btype, builder)
                self.logger.notice("[%s] --> Wrote new ring with md5: %s" %
                                   (btype, ring_md5))
                self._emit_notify('%s ring change' % btype,
                                  'Wrote new ring with md5: %s' % ring_md5)
                return True
            except Exception:
                self.logger.exception('Error dumping builder or ring')
        else:
            self.logger.notice("[%s] -> No ring change required" % btype)
            return False

    def start(self):
        """Start up the ring master"""
        self.logger.notice("Ring-Master starting up")
        self.logger.notice("-> Entering ring orchestration loop.")
        while True:
            try:
                self.pause_if_asked()
                if self.in_change_window():
                    for btype in sorted(self.builder_files.keys()):
                        with lock_parent_directory(self.builder_files[btype],
                                                   self.lock_timeout):
                            ring_changed = self.orchestration_pass(btype)
                        if ring_changed:
                            sleep(self.recheck_after_change_interval)
                        else:
                            sleep(self.recheck_interval)
                else:
                    self.logger.debug('Not in change window')
                    sleep(60)
            except exceptions.LockTimeout:
                self.logger.exception('Orchestration LockTimeout Encountered')
            except Exception:
                self.logger.exception('Orchestration Error')
                sleep(60)
            sleep(1)