|
| 1 | +#!/usr/bin/env python2 |
| 2 | +from yaml import load, dump |
| 3 | +try: |
| 4 | + from yaml import CLoader as Loader, CDumper as Dumper |
| 5 | +except ImportError: |
| 6 | + from yaml import Loader, Dumper |
| 7 | +from optparse import OptionParser |
| 8 | +from os.path import dirname, exists |
| 9 | +from os import remove |
| 10 | + |
| 11 | +########### |
| 12 | +# CLASSES # |
| 13 | +########### |
| 14 | + |
| 15 | +class Config: |
| 16 | + """A class encapsulating the config.yaml config file""" |
| 17 | + def __init__(self, filename=dirname(__file__) + '/config.yaml'): |
| 18 | + self.filename = filename |
| 19 | + self.config_raw = None |
| 20 | + self.config = None |
| 21 | + self.load() |
| 22 | + |
| 23 | + def load(self): |
| 24 | + if self.config_raw is None: |
| 25 | + self.load_raw() |
| 26 | + if self.config is None: |
| 27 | + self.process() |
| 28 | + def load_raw(self): |
| 29 | + config_file = open(self.filename) |
| 30 | + self.config_raw = load(config_file, Loader=Loader) |
| 31 | + config_file.close() |
| 32 | + def process(self): |
| 33 | + config_raw = self.config_raw |
| 34 | + |
| 35 | + config = dict([(cluster_key, dict([(server_key, config_raw.get('_default', {}).copy()) |
| 36 | + for server_key in config_raw[cluster_key].keys() |
| 37 | + if server_key[0] != '_'])) |
| 38 | + for cluster_key in config_raw.keys() |
| 39 | + if cluster_key[0] != '_']) |
| 40 | + for cluster_key in config.keys(): |
| 41 | + for server_key in config[cluster_key].keys(): |
| 42 | + config[cluster_key][server_key].update(config_raw[cluster_key].get('_default', {})) |
| 43 | + config[cluster_key][server_key].update(config_raw[cluster_key][server_key]) |
| 44 | + |
| 45 | + config[cluster_key]['_ip_hash'] = bool(config_raw[cluster_key].get('_ip_hash', False)) |
| 46 | + config[cluster_key]['_file'] = dirname(__file__) + '/' + config_raw[cluster_key].get('_file', cluster_key + '.conf') |
| 47 | + self.config = config |
| 48 | + def save(self): |
| 49 | + config_file = open(self.filename, 'w') |
| 50 | + config_file.write(dump(self.config_raw, default_flow_style=False, Dumper=Dumper)) |
| 51 | + config_file.close() |
| 52 | + |
| 53 | + def cluster(self, cluster_name): |
| 54 | + return Cluster(cluster_name, self.config[cluster_name]) |
| 55 | + |
| 56 | + def _set_prop(self, cluster, server, prop, value): |
| 57 | + cluster_name = cluster.name |
| 58 | + self.config[cluster_name][server][prop] = value |
| 59 | + |
| 60 | + cluster_default = self.config_raw[cluster_name].get('_default', {}).get(prop, None) |
| 61 | + global_default = self.config_raw.get('_default', {}).get(prop, None) |
| 62 | + if cluster_default == value or (cluster_default is None and global_default == value): |
| 63 | + if prop in self.config_raw[cluster_name][server]: |
| 64 | + del self.config_raw[cluster_name][server][prop] |
| 65 | + else: |
| 66 | + self.config_raw[cluster_name][server][prop] = value |
| 67 | + |
| 68 | + def enable(self, cluster, server): |
| 69 | + self._set_prop(cluster, server, 'enabled', 1) |
| 70 | + def disable(self, cluster, server): |
| 71 | + self._set_prop(cluster, server, 'enabled', 0) |
| 72 | + |
| 73 | + def backup(self, cluster, server): |
| 74 | + self._set_prop(cluster, server, 'backup', 1) |
| 75 | + def nonbackup(self, cluster, server): |
| 76 | + self._set_prop(cluster, server, 'backup', 0) |
| 77 | + |
| 78 | + def down(self, cluster, server): |
| 79 | + self._set_prop(cluster, server, 'down', 1) |
| 80 | + def up(self, cluster, server): |
| 81 | + self._set_prop(cluster, server, 'down', 0) |
| 82 | + |
| 83 | + def weight(self, cluster, server, new): |
| 84 | + if new is None: |
| 85 | + new = 1 |
| 86 | + self._set_prop(cluster, server, 'weight', new) |
| 87 | + def max_fails(self, cluster, server, new): |
| 88 | + if new is None: |
| 89 | + new = 1 |
| 90 | + self._set_prop(cluster, server, 'max_fails', new) |
| 91 | + def fail_timeout(self, cluster, server, new): |
| 92 | + if new is None: |
| 93 | + new = '10s' |
| 94 | + self._set_prop(cluster, server, 'fail_timeout', new) |
| 95 | + |
| 96 | +class Cluster: |
| 97 | + """A class encapsulating a specific upstream/cluster""" |
| 98 | + def __init__(self, name, config): |
| 99 | + self.name = name |
| 100 | + self.ip_hash = config.get('_ip_hash') |
| 101 | + self.filename = config.get('_file') |
| 102 | + self.servers = [Server(name, config[name]) for name in config.keys() if name[0] != '_'] |
| 103 | + def save(self, **kwargs): |
| 104 | + upstream_def = 'upstream %s {\n%%s}\n' % self.name |
| 105 | + if self.ip_hash: |
| 106 | + upstream_def = upstream_def % ' ip_hash;\n%s' |
| 107 | + count = 1 |
| 108 | + for server in self.servers: |
| 109 | + if kwargs.get('rotate', False) == count: |
| 110 | + server.rotate = True |
| 111 | + upstream_def = upstream_def % server.comment_line(' ', '\n%s') |
| 112 | + upstream_def = upstream_def % server.upstream_line(' ', '\n%s') |
| 113 | + if server.active(): |
| 114 | + count = count + 1 |
| 115 | + upstream_def = upstream_def % '' |
| 116 | + |
| 117 | + config_file = open(self.filename, 'w') |
| 118 | + config_file.write(upstream_def) |
| 119 | + config_file.close() |
| 120 | + |
| 121 | +class Server: |
| 122 | + """A class encapsulating a specific server within a cluster""" |
| 123 | + def __init__(self, name, config): |
| 124 | + self.name = name |
| 125 | + if config.get('host', False): |
| 126 | + self.host = config['host'] |
| 127 | + self.port = config['port'] |
| 128 | + self.upstream = "%s:%s" % (self.host, + self.port) |
| 129 | + elif config.get('upstream', False): |
| 130 | + self.host = self.port = None |
| 131 | + self.upstream = config['upstream'] |
| 132 | + else: |
| 133 | + raise Exception('Please provide either host/port or upstream for server %s' % self.name) |
| 134 | + for prop in ['weight', 'max_fails', 'fail_timeout', 'down', 'backup', 'enabled']: |
| 135 | + setattr(self, prop, config.get(prop, None)) |
| 136 | + self.rotate = False |
| 137 | + def active(self): |
| 138 | + return self.enabled and not self.down |
| 139 | + def comment_line(self, prefix='', postfix=''): |
| 140 | + ret = '# %s' % self.name |
| 141 | + if not self.enabled: |
| 142 | + ret = ret + ' (disabled)' |
| 143 | + if self.down: |
| 144 | + ret = ret + ' (down)' |
| 145 | + if self.rotate: |
| 146 | + ret = ret + ' (rotated out)' |
| 147 | + return prefix + ret + postfix |
| 148 | + def upstream_line(self, prefix='', postfix=''): |
| 149 | + properties = [] |
| 150 | + if self.rotate: |
| 151 | + properties.append('#') |
| 152 | + elif not self.enabled: |
| 153 | + properties.append('##') |
| 154 | + |
| 155 | + properties.extend(['server', self.upstream]) |
| 156 | + for prop in ['weight', 'max_fails', 'fail_timeout']: |
| 157 | + if getattr(self, prop) is not None: |
| 158 | + properties.append("%s=%s" % (prop, getattr(self,prop))) |
| 159 | + for prop in ['down', 'backup']: |
| 160 | + if getattr(self, prop): |
| 161 | + properties.append(prop) |
| 162 | + return prefix + " ".join(properties) + ';' + postfix |
| 163 | + |
| 164 | +########### |
| 165 | +# ACTIONS # |
| 166 | +########### |
| 167 | + |
| 168 | +def rotate_action(config, cluster, *other_args): |
| 169 | + statefile = dirname(__file__) + '/.rotate-' + cluster.name |
| 170 | + if exists(statefile): |
| 171 | + fh = open(statefile, 'r') |
| 172 | + state = int(fh.read().strip()) + 1 |
| 173 | + fh.close() |
| 174 | + else: |
| 175 | + state = 1 |
| 176 | + valid = [server.host for server in cluster.servers if server.active()] |
| 177 | + cluster.save(rotate=state) |
| 178 | + if state > len(valid): |
| 179 | + print "Done" |
| 180 | + remove(statefile) |
| 181 | + else: |
| 182 | + print valid[state-1] |
| 183 | + fh = open(statefile, 'w') |
| 184 | + fh.write(str(state)) |
| 185 | + fh.close() |
| 186 | + |
| 187 | +def generate_action(config, cluster, *other_args): |
| 188 | + cluster.save() |
| 189 | + print "Saved" |
| 190 | + |
| 191 | +def disable_action(config, cluster, args, options, parser): |
| 192 | + if len(args) < 1: |
| 193 | + parser.error('Please provide a server to disable.') |
| 194 | + to_disable = args[0] |
| 195 | + if cluster.ip_hash: |
| 196 | + config.down(cluster, to_disable) |
| 197 | + else: |
| 198 | + config.disable(cluster, to_disable) |
| 199 | + config.save() |
| 200 | + config.cluster(cluster.name).save() |
| 201 | + print "Disabled " + to_disable |
| 202 | + |
| 203 | +def enable_action(config, cluster, args, options, parser): |
| 204 | + if len(args) < 1: |
| 205 | + parser.error('Please provide a server to enable.') |
| 206 | + to_enable = args[0] |
| 207 | + if cluster.ip_hash: |
| 208 | + config.enable(cluster, to_enable) |
| 209 | + config.up(cluster, to_enable) |
| 210 | + else: |
| 211 | + config.enable(cluster, to_enable) |
| 212 | + config.save() |
| 213 | + config.cluster(cluster.name).save() |
| 214 | + print "Enabled " + to_enable |
| 215 | + |
| 216 | +def weight_action(config, cluster, args, options, parser): |
| 217 | + if len(args) < 1: |
| 218 | + parser.error('Please provide a server.') |
| 219 | + if len(args) < 2: |
| 220 | + parser.error('Please provide a new value.') |
| 221 | + server = args[0] |
| 222 | + value = args[1] |
| 223 | + config.weight(cluster, server, value) |
| 224 | + config.save() |
| 225 | + config.cluster(cluster.name).save() |
| 226 | + print "Changed %s weight to %s" % (server, value) |
| 227 | + |
| 228 | +actions = {'generate': generate_action, |
| 229 | + 'rotate': rotate_action, |
| 230 | + 'disable': disable_action, |
| 231 | + 'enable': enable_action, |
| 232 | + 'weight': weight_action} |
| 233 | + |
| 234 | +# Run the script! |
| 235 | +if __name__ == '__main__': |
| 236 | + usage = """Usage: %%prog [options] cluster action [server ...] |
| 237 | + 'cluster' should be a named cluster in your config file |
| 238 | + 'action' should be one of: %s |
| 239 | + 'server' and past is required for some action types""" % ", ".join(actions.keys()) |
| 240 | + parser = OptionParser(usage=usage) |
| 241 | + (options, args) = parser.parse_args() |
| 242 | + |
| 243 | + if len(args) < 2: |
| 244 | + parser.error('Please provide both a cluster and an action!') |
| 245 | + |
| 246 | + config = Config() |
| 247 | + if args[0] not in config.config.keys(): |
| 248 | + parser.error('Unknown cluster. Available clusters are: ' + ", ".join([k for k in config.keys() if k[0] != '_'])) |
| 249 | + |
| 250 | + cluster = config.cluster(args[0]) |
| 251 | + action = args[1] |
| 252 | + |
| 253 | + if action not in actions.keys(): |
| 254 | + parser.error('Unknown action. Available actions are: ' + ", ".join(actions.keys())) |
| 255 | + |
| 256 | + actions[action](config, cluster, args[2:], options, parser) |
0 commit comments