Skip to content

Commit 7eae404

Browse files
committed
Add actual code.
1 parent c375cad commit 7eae404

4 files changed

+283
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
config.yaml
2+
*.conf
3+
*~

config.yaml.sample

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
_default:
2+
enabled: 1
3+
fail_timeout: 10s
4+
max_fails: 10
5+
port: 80
6+
fancycluster:
7+
snap:
8+
host: 192.168.0.2
9+
weight: 18
10+
crackle:
11+
host: 192.168.0.3
12+
weight: 22
13+
pop:
14+
enabled: 0
15+
host: 192.168.0.4
16+
weight: 19

fancycluster.conf.sample

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
upstream fancycluster {
2+
# snap
3+
server 192.168.0.2:80 weight=18 max_fails=10 fail_timeout=10s;
4+
# crackle
5+
server 192.168.0.3:80 weight=22 max_fails=10 fail_timeout=10s;
6+
# pop (disabled)
7+
## server 192.168.0.4:80 weight=19 max_fails=10 fail_timeout=10s;
8+
}

upstream_manager.py

+256
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
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

Comments
 (0)