15
###############################################################################
17
###############################################################################
18
default_haproxy_config_dir = "/etc/haproxy"
19
default_haproxy_config = "%s/haproxy.cfg" % default_haproxy_config_dir
20
default_haproxy_service_config_dir = "/var/run/haproxy"
21
hook_name = os.path.basename(sys.argv[0])
23
###############################################################################
24
# Supporting functions
25
###############################################################################
28
#------------------------------------------------------------------------------
29
# config_get: Returns a dictionary containing all of the config information
30
# Optional parameter: scope
31
# scope: limits the scope of the returned configuration to the
32
# desired config item.
33
#------------------------------------------------------------------------------
34
def config_get(scope=None):
36
config_cmd_line = ['config-get']
38
config_cmd_line.append(scope)
39
config_cmd_line.append('--format=json')
40
config_data = json.loads(subprocess.check_output(config_cmd_line))
47
#------------------------------------------------------------------------------
48
# relation_get: Returns a dictionary containing the relation information
49
# Optional parameters: scope, relation_id
50
# scope: limits the scope of the returned data to the
52
# unit_name: limits the data ( and optionally the scope )
53
# to the specified unit
54
#------------------------------------------------------------------------------
55
def relation_get(scope=None, unit_name=None):
57
relation_cmd_line = ['relation-get', '--format=json']
59
relation_cmd_line.append(scope)
61
relation_cmd_line.append('')
62
if unit_name is not None:
63
relation_cmd_line.append(unit_name)
64
relation_data = json.loads(subprocess.check_output(relation_cmd_line))
71
#------------------------------------------------------------------------------
72
# apt_get_install( package ): Installs a package
73
#------------------------------------------------------------------------------
74
def apt_get_install(packages=None):
77
cmd_line = ['apt-get', '-y', 'install', '-qq']
78
cmd_line.append(packages)
79
return(subprocess.call(cmd_line))
82
#------------------------------------------------------------------------------
83
# enable_haproxy: Enabled haproxy at boot time
84
#------------------------------------------------------------------------------
86
default_haproxy = "/etc/default/haproxy"
88
open(default_haproxy).read().replace('ENABLED=0', 'ENABLED=1')
89
with open(default_haproxy, 'w') as f:
90
f.write(enabled_haproxy)
93
#------------------------------------------------------------------------------
94
# create_haproxy_globals: Creates the global section of the haproxy config
95
#------------------------------------------------------------------------------
96
def create_haproxy_globals():
97
config_data = config_get()
98
global_log = config_data['global_log'].split(',')
100
haproxy_globals.append('global')
101
for global_log_item in global_log:
102
haproxy_globals.append(" log %s" % global_log_item.strip())
103
haproxy_globals.append(" maxconn %d" % config_data['global_maxconn'])
104
haproxy_globals.append(" user %s" % config_data['global_user'])
105
haproxy_globals.append(" group %s" % config_data['global_group'])
106
if config_data['global_debug'] is True:
107
haproxy_globals.append(" debug")
108
if config_data['global_quiet'] is True:
109
haproxy_globals.append(" quiet")
110
haproxy_globals.append(" spread-checks %d" % \
111
config_data['global_spread_checks'])
112
return('\n'.join(haproxy_globals))
115
#------------------------------------------------------------------------------
116
# create_haproxy_defaults: Creates the defaults section of the haproxy config
117
#------------------------------------------------------------------------------
118
def create_haproxy_defaults():
119
config_data = config_get()
120
default_options = config_data['default_options'].split(',')
121
default_timeouts = config_data['default_timeouts'].split(',')
122
haproxy_defaults = []
123
haproxy_defaults.append("defaults")
124
haproxy_defaults.append(" log %s" % config_data['default_log'])
125
haproxy_defaults.append(" mode %s" % config_data['default_mode'])
126
for option_item in default_options:
127
haproxy_defaults.append(" option %s" % option_item.strip())
128
haproxy_defaults.append(" retries %d" % config_data['default_retries'])
129
for timeout_item in default_timeouts:
130
haproxy_defaults.append(" timeout %s" % timeout_item.strip())
131
return('\n'.join(haproxy_defaults))
134
#------------------------------------------------------------------------------
135
# load_haproxy_config: Convenience function that loads (as a string) the
136
# current haproxy configuration file.
137
# Returns a string containing the haproxy config or
139
#------------------------------------------------------------------------------
140
def load_haproxy_config(haproxy_config_file="/etc/haproxy/haproxy.cfg"):
141
if os.path.isfile(haproxy_config_file):
142
return(open(haproxy_config_file).read())
147
#------------------------------------------------------------------------------
148
# get_monitoring_password: Gets the monitoring password from the
150
# This prevents the password from being constantly
151
# regenerated by the system.
152
#------------------------------------------------------------------------------
153
def get_monitoring_password(haproxy_config_file="/etc/haproxy/haproxy.cfg"):
154
haproxy_config = load_haproxy_config(haproxy_config_file)
155
if haproxy_config is None:
157
m = re.search("stats auth\s+(\w+):(\w+)", haproxy_config)
164
#------------------------------------------------------------------------------
165
# get_service_ports: Convenience function that scans the existing haproxy
166
# configuration file and returns a list of the existing
167
# ports being used. This is necessary to know which ports
168
# to open and close when exposing/unexposing a service
169
#------------------------------------------------------------------------------
170
def get_service_ports(haproxy_config_file="/etc/haproxy/haproxy.cfg"):
171
haproxy_config = load_haproxy_config(haproxy_config_file)
172
if haproxy_config is None:
174
return(re.findall("listen.*:(.*)", haproxy_config))
177
#------------------------------------------------------------------------------
178
# open_port: Convenience function to open a port in juju to
180
#------------------------------------------------------------------------------
181
def open_port(port=None, protocol="TCP"):
184
return(subprocess.call(['/usr/bin/open-port', "%d/%s" % \
185
(int(port), protocol)]))
188
#------------------------------------------------------------------------------
189
# close_port: Convenience function to close a port in juju to
191
#------------------------------------------------------------------------------
192
def close_port(port=None, protocol="TCP"):
195
return(subprocess.call(['/usr/bin/close-port', "%d/%s" % \
196
(int(port), protocol)]))
199
#------------------------------------------------------------------------------
200
# update_service_ports: Convenience function that evaluate the old and new
201
# service ports to decide which ports need to be
202
# opened and which to close
203
#------------------------------------------------------------------------------
204
def update_service_ports(old_service_ports=None, new_service_ports=None):
205
if old_service_ports is None or new_service_ports is None:
207
for port in old_service_ports:
208
if port not in new_service_ports:
210
for port in new_service_ports:
211
if port not in old_service_ports:
215
#------------------------------------------------------------------------------
216
# pwgen: Generates a random password
217
# pwd_length: Defines the length of the password to generate
219
#------------------------------------------------------------------------------
220
def pwgen(pwd_length=20):
221
alphanumeric_chars = [l for l in (string.letters + string.digits) \
222
if l not in 'Iil0oO1']
223
random_chars = [random.choice(alphanumeric_chars) \
224
for i in range(pwd_length)]
225
return(''.join(random_chars))
228
#------------------------------------------------------------------------------
229
# create_listen_stanza: Function to create a generic listen section in the
231
# service_name: Arbitrary service name
232
# service_ip: IP address to listen for connections
233
# service_port: Port to listen for connections
234
# service_options: Comma separated list of options
235
# server_entries: List of tuples
240
#------------------------------------------------------------------------------
241
def create_listen_stanza(service_name=None, service_ip=None,
242
service_port=None, service_options=None,
243
server_entries=None):
244
if service_name is None or service_ip is None or service_port is None:
247
service_config.append("listen %s %s:%s" % \
248
(service_name, service_ip, service_port))
249
if service_options is not None:
250
for service_option in service_options:
251
service_config.append(" %s" % service_option.strip())
252
if server_entries is not None and type(server_entries) == type([]):
253
for (server_name, server_ip, server_port, server_options) \
255
server_line = " server %s %s:%s" % \
256
(server_name, server_ip, server_port)
257
if server_options is not None:
258
server_line += " %s" % server_options
259
service_config.append(server_line)
260
return('\n'.join(service_config))
263
#------------------------------------------------------------------------------
264
# create_monitoring_stanza: Function to create the haproxy monitoring section
265
# service_name: Arbitrary name
266
#------------------------------------------------------------------------------
267
def create_monitoring_stanza(service_name="haproxy_monitoring"):
268
config_data = config_get()
269
if config_data['enable_monitoring'] is False:
271
monitoring_password = get_monitoring_password()
272
if config_data['monitoring_password'] != "changeme":
273
monitoring_password = config_data['monitoring_password']
274
elif monitoring_password is None and \
275
config_data['monitoring_password'] == "changeme":
276
monitoring_password = pwgen()
277
monitoring_config = []
278
monitoring_config.append("mode http")
279
monitoring_config.append("acl allowed_cidr src %s" % \
280
config_data['monitoring_allowed_cidr'])
281
monitoring_config.append("block unless allowed_cidr")
282
monitoring_config.append("stats enable")
283
monitoring_config.append("stats uri /")
284
monitoring_config.append("stats realm Haproxy\ Statistics")
285
monitoring_config.append("stats auth %s:%s" % \
286
(config_data['monitoring_username'], monitoring_password))
287
monitoring_config.append("stats refresh %d" % \
288
config_data['monitoring_stats_refresh'])
289
return(create_listen_stanza(service_name, \
291
config_data['monitoring_port'], \
295
#------------------------------------------------------------------------------
296
# get_config_services: Convenience function that returns a list
297
# of dictionary entries containing all of the services
299
#------------------------------------------------------------------------------
300
def get_config_services():
301
config_data = config_get()
302
services_list = yaml.load(config_data['services'])
303
return(services_list)
306
#------------------------------------------------------------------------------
307
# create_services: Function that will create the services configuration
308
# from the config data and/or relation information
309
#------------------------------------------------------------------------------
310
def create_services():
311
services_list = get_config_services()
313
for service_item in services_list:
314
service_name = service_item['service_name']
315
service_host = service_item['service_host']
316
service_port = service_item['service_port']
317
service_options = service_item['service_options']
318
server_options = service_item['server_options']
319
services_dict[service_name] = {'service_name': service_name,
320
'service_host': service_host,
321
'service_port': service_port,
322
'service_options': service_options,
323
'server_options': server_options}
326
for unit in json.loads(\
327
subprocess.check_output(['relation-list', '--format=json'])):
328
relation_info = relation_get(None, unit)
329
if type(relation_info) != type({}):
331
# Mandatory switches ( hostname, port )
332
server_name = "%s__%s" % \
333
(relation_info['hostname'].replace('.', '_'), \
334
relation_info['port'])
335
server_ip = relation_info['hostname']
336
server_port = relation_info['port']
337
# Optional switches ( service_name )
338
if 'service_name' in relation_info:
339
if relation_info['service_name'] in services_dict:
340
service_name = relation_info['service_name']
343
'juju-log', 'service %s does not exists. ' % \
344
relation_info['service_name']])
347
service_name = services_list[0]['service_name']
348
if os.path.exists("%s/%s.is.proxy" % \
349
(default_haproxy_service_config_dir, service_name)):
350
if 'option forwardfor' not in service_options:
351
service_options.append("option forwardfor")
352
# Add the server entries
353
if not 'servers' in services_dict[service_name]:
354
services_dict[service_name]['servers'] = \
355
[(server_name, server_ip, server_port, \
356
services_dict[service_name]['server_options'])]
358
services_dict[service_name]['servers'].append((\
359
server_name, server_ip, server_port, \
360
services_dict[service_name]['server_options']))
363
# Construct the new haproxy.cfg file
364
for service in services_dict:
365
print "Service: ", service
366
server_entries = None
367
if 'servers' in services_dict[service]:
368
server_entries = services_dict[service]['servers']
369
with open("%s/%s.service" % (\
370
default_haproxy_service_config_dir, \
371
services_dict[service]['service_name']), 'w') as service_config:
372
service_config.write(\
373
create_listen_stanza(services_dict[service]['service_name'],\
374
services_dict[service]['service_host'],
375
services_dict[service]['service_port'],
376
services_dict[service]['service_options'],
380
#------------------------------------------------------------------------------
381
# load_services: Convenience function that load the service snippet
382
# configuration from the filesystem.
383
#------------------------------------------------------------------------------
384
def load_services(service_name=None):
386
if service_name is not None:
387
if os.path.exists("%s/%s.service" % \
388
(default_haproxy_service_config_dir, service_name)):
389
services = open("%s/%s.service" % \
390
(default_haproxy_service_config_dir, service_name)).read()
394
for service in glob.glob("%s/*.service" % \
395
default_haproxy_service_config_dir):
396
services += open(service).read()
401
#------------------------------------------------------------------------------
402
# remove_services: Convenience function that removes the configuration
403
# snippets from the filesystem. This is necessary
404
# To ensure sync between the config/relation-data
405
# and the existing haproxy services.
406
#------------------------------------------------------------------------------
407
def remove_services(service_name=None):
408
if service_name is not None:
409
if os.path.exists("%s/%s.service" % \
410
(default_haproxy_service_config_dir, service_name)):
412
os.remove("%s/%s.service" % \
413
(default_haproxy_service_config_dir, service_name))
418
for service in glob.glob("%s/*.service" % \
419
default_haproxy_service_config_dir):
427
#------------------------------------------------------------------------------
428
# construct_haproxy_config: Convenience function to write haproxy.cfg
429
# haproxy_globals, haproxy_defaults,
430
# haproxy_monitoring, haproxy_services
431
# are all strings that will be written without
433
# haproxy_monitoring and haproxy_services are
435
#------------------------------------------------------------------------------
436
def construct_haproxy_config(haproxy_globals=None,
437
haproxy_defaults=None,
438
haproxy_monitoring=None,
439
haproxy_services=None):
440
if haproxy_globals is None or \
441
haproxy_defaults is None:
443
with open(default_haproxy_config, 'w') as haproxy_config:
444
haproxy_config.write(haproxy_globals)
445
haproxy_config.write("\n")
446
haproxy_config.write("\n")
447
haproxy_config.write(haproxy_defaults)
448
haproxy_config.write("\n")
449
haproxy_config.write("\n")
450
if haproxy_monitoring is not None:
451
haproxy_config.write(haproxy_monitoring)
452
haproxy_config.write("\n")
453
haproxy_config.write("\n")
454
if haproxy_services is not None:
455
haproxy_config.write(haproxy_services)
456
haproxy_config.write("\n")
457
haproxy_config.write("\n")
460
#------------------------------------------------------------------------------
461
# service_haproxy: Convenience function to start/stop/restart/reload
462
# the haproxy service
463
#------------------------------------------------------------------------------
464
def service_haproxy(action=None, haproxy_config=default_haproxy_config):
465
if action is None or haproxy_config is None:
467
elif action == "check":
468
retVal = subprocess.call(\
469
['/usr/sbin/haproxy', '-f', haproxy_config, '-c'])
477
retVal = subprocess.call(['service', 'haproxy', action])
484
###############################################################################
486
###############################################################################
488
if not os.path.exists(default_haproxy_service_config_dir):
489
os.mkdir(default_haproxy_service_config_dir, 0600)
490
return (apt_get_install("haproxy") == enable_haproxy() == True)
493
def config_changed():
494
config_data = config_get()
495
current_service_ports = get_service_ports()
496
haproxy_globals = create_haproxy_globals()
497
haproxy_defaults = create_haproxy_defaults()
498
if config_data['enable_monitoring'] is True:
499
haproxy_monitoring = create_monitoring_stanza()
501
haproxy_monitoring = None
504
haproxy_services = load_services()
505
construct_haproxy_config(haproxy_globals, \
507
haproxy_monitoring, \
510
if service_haproxy("check"):
511
updated_service_ports = get_service_ports()
512
update_service_ports(current_service_ports, updated_service_ports)
513
service_haproxy("reload")
517
if service_haproxy("status"):
518
return(service_haproxy("restart"))
520
return(service_haproxy("start"))
524
if service_haproxy("status"):
525
return(service_haproxy("stop"))
528
def reverseproxy_interface(hook_name=None):
529
if hook_name is None:
531
if hook_name == "changed":
535
def website_interface(hook_name=None):
536
if hook_name is None:
538
my_fqdn = socket.getfqdn(socket.gethostname())
540
relation_data = relation_get()
541
if hook_name == "joined":
542
subprocess.call(['relation-set', 'port=%d' % \
543
default_port, 'hostname=%s' % my_fqdn])
544
elif hook_name == "changed":
545
if 'is-proxy' in relation_data:
546
service_name = "%s__%d" % \
547
(relation_data['hostname'], relation_data['port'])
548
open("%s/%s.is.proxy" % \
549
(default_haproxy_service_config_dir, service_name), 'a').close()
552
###############################################################################
554
###############################################################################
555
if hook_name == "install":
557
elif hook_name == "config-changed":
559
elif hook_name == "start":
561
elif hook_name == "stop":
563
elif hook_name == "reverseproxy-relation-broken":
565
elif hook_name == "reverseproxy-relation-changed":
566
reverseproxy_interface("changed")
567
elif hook_name == "website-relation-joined":
568
website_interface("joined")
569
elif hook_name == "website-relation-changed":
570
website_interface("changed")