~dpb/charms/precise/haproxy/fixes

« back to all changes in this revision

Viewing changes to hooks/hooks.py

  • Committer: Juan L. Negron
  • Date: 2012-07-23 17:30:16 UTC
  • mfrom: (51.1.3 mini-sprint-sf)
  • Revision ID: juan.negron@canonical.com-20120723173016-b4s47xw207am5jda
Merging MP:114951 - modifications by mthaddon and negronjl

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
 
 
3
import json
 
4
import glob
 
5
import os
 
6
import random
 
7
import re
 
8
import socket
 
9
import string
 
10
import subprocess
 
11
import sys
 
12
import yaml
 
13
 
 
14
 
 
15
###############################################################################
 
16
# Global variables
 
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])
 
22
 
 
23
###############################################################################
 
24
# Supporting functions
 
25
###############################################################################
 
26
 
 
27
 
 
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):
 
35
    try:
 
36
        config_cmd_line = ['config-get']
 
37
        if scope is not None:
 
38
            config_cmd_line.append(scope)
 
39
        config_cmd_line.append('--format=json')
 
40
        config_data = json.loads(subprocess.check_output(config_cmd_line))
 
41
    except:
 
42
        config_data = None
 
43
    finally:
 
44
        return(config_data)
 
45
 
 
46
 
 
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
 
51
#                              desired item.
 
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):
 
56
    try:
 
57
        relation_cmd_line = ['relation-get', '--format=json']
 
58
        if scope is not None:
 
59
            relation_cmd_line.append(scope)
 
60
        else:
 
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))
 
65
    except:
 
66
        relation_data = None
 
67
    finally:
 
68
        return(relation_data)
 
69
 
 
70
 
 
71
#------------------------------------------------------------------------------
 
72
# apt_get_install( package ):  Installs a package
 
73
#------------------------------------------------------------------------------
 
74
def apt_get_install(packages=None):
 
75
    if packages is None:
 
76
        return(False)
 
77
    cmd_line = ['apt-get', '-y', 'install', '-qq']
 
78
    cmd_line.append(packages)
 
79
    return(subprocess.call(cmd_line))
 
80
 
 
81
 
 
82
#------------------------------------------------------------------------------
 
83
# enable_haproxy:  Enabled haproxy at boot time
 
84
#------------------------------------------------------------------------------
 
85
def enable_haproxy():
 
86
    default_haproxy = "/etc/default/haproxy"
 
87
    enabled_haproxy = \
 
88
    open(default_haproxy).read().replace('ENABLED=0', 'ENABLED=1')
 
89
    with open(default_haproxy, 'w') as f:
 
90
        f.write(enabled_haproxy)
 
91
 
 
92
 
 
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(',')
 
99
    haproxy_globals = []
 
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))
 
113
 
 
114
 
 
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))
 
132
 
 
133
 
 
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
 
138
#                       None
 
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())
 
143
    else:
 
144
        return(None)
 
145
 
 
146
 
 
147
#------------------------------------------------------------------------------
 
148
# get_monitoring_password:  Gets the monitoring password from the
 
149
#                           haproxy config.
 
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:
 
156
        return(None)
 
157
    m = re.search("stats auth\s+(\w+):(\w+)", haproxy_config)
 
158
    if m is not None:
 
159
        return(m.group(2))
 
160
    else:
 
161
        return(None)
 
162
 
 
163
 
 
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:
 
173
        return(None)
 
174
    return(re.findall("listen.*:(.*)", haproxy_config))
 
175
 
 
176
 
 
177
#------------------------------------------------------------------------------
 
178
# open_port:  Convenience function to open a port in juju to
 
179
#             expose a service
 
180
#------------------------------------------------------------------------------
 
181
def open_port(port=None, protocol="TCP"):
 
182
    if port is None:
 
183
        return(None)
 
184
    return(subprocess.call(['/usr/bin/open-port', "%d/%s" % \
 
185
    (int(port), protocol)]))
 
186
 
 
187
 
 
188
#------------------------------------------------------------------------------
 
189
# close_port:  Convenience function to close a port in juju to
 
190
#              unexpose a service
 
191
#------------------------------------------------------------------------------
 
192
def close_port(port=None, protocol="TCP"):
 
193
    if port is None:
 
194
        return(None)
 
195
    return(subprocess.call(['/usr/bin/close-port', "%d/%s" % \
 
196
    (int(port), protocol)]))
 
197
 
 
198
 
 
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:
 
206
        return(None)
 
207
    for port in old_service_ports:
 
208
        if port not in new_service_ports:
 
209
            close_port(port)
 
210
    for port in new_service_ports:
 
211
        if port not in old_service_ports:
 
212
            open_port(port)
 
213
 
 
214
 
 
215
#------------------------------------------------------------------------------
 
216
# pwgen:  Generates a random password
 
217
#         pwd_length:  Defines the length of the password to generate
 
218
#                      default: 20
 
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))
 
226
 
 
227
 
 
228
#------------------------------------------------------------------------------
 
229
# create_listen_stanza: Function to create a generic listen section in the
 
230
#                       haproxy config
 
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
 
236
#                                         server_name
 
237
#                                         server_ip
 
238
#                                         server_port
 
239
#                                         server_options
 
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:
 
245
        return(None)
 
246
    service_config = []
 
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) \
 
254
        in server_entries:
 
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))
 
261
 
 
262
 
 
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:
 
270
        return(None)
 
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, \
 
290
                                "0.0.0.0", \
 
291
                                config_data['monitoring_port'], \
 
292
                                monitoring_config))
 
293
 
 
294
 
 
295
#------------------------------------------------------------------------------
 
296
# get_config_services:  Convenience function that returns a list
 
297
#                       of dictionary entries containing all of the services
 
298
#                       configuration
 
299
#------------------------------------------------------------------------------
 
300
def get_config_services():
 
301
    config_data = config_get()
 
302
    services_list = yaml.load(config_data['services'])
 
303
    return(services_list)
 
304
 
 
305
 
 
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()
 
312
    services_dict = {}
 
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}
 
324
 
 
325
    try:
 
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({}):
 
330
                sys.exit(0)
 
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']
 
341
                else:
 
342
                    subprocess.call([\
 
343
                    'juju-log', 'service %s does not exists. ' % \
 
344
                    relation_info['service_name']])
 
345
                    sys.exit(1)
 
346
            else:
 
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'])]
 
357
            else:
 
358
                services_dict[service_name]['servers'].append((\
 
359
                server_name, server_ip, server_port, \
 
360
                services_dict[service_name]['server_options']))
 
361
    except:
 
362
        pass
 
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'],
 
377
                                     server_entries))
 
378
 
 
379
 
 
380
#------------------------------------------------------------------------------
 
381
# load_services: Convenience function that load the service snippet
 
382
#                configuration from the filesystem.
 
383
#------------------------------------------------------------------------------
 
384
def load_services(service_name=None):
 
385
    services = ''
 
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()
 
391
        else:
 
392
            services = None
 
393
    else:
 
394
        for service in glob.glob("%s/*.service" % \
 
395
            default_haproxy_service_config_dir):
 
396
            services += open(service).read()
 
397
            services += "\n\n"
 
398
    return(services)
 
399
 
 
400
 
 
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)):
 
411
            try:
 
412
                os.remove("%s/%s.service" % \
 
413
                (default_haproxy_service_config_dir, service_name))
 
414
                return(True)
 
415
            except:
 
416
                return(False)
 
417
    else:
 
418
        for service in glob.glob("%s/*.service" % \
 
419
        default_haproxy_service_config_dir):
 
420
            try:
 
421
                os.remove(service)
 
422
            except:
 
423
                pass
 
424
        return(True)
 
425
 
 
426
 
 
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
 
432
#                            any checks.
 
433
#                            haproxy_monitoring and haproxy_services are
 
434
#                            optional arguments
 
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:
 
442
        return(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")
 
458
 
 
459
 
 
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:
 
466
        return(None)
 
467
    elif action == "check":
 
468
        retVal = subprocess.call(\
 
469
        ['/usr/sbin/haproxy', '-f', haproxy_config, '-c'])
 
470
        if retVal == 1:
 
471
            return(False)
 
472
        elif retVal == 0:
 
473
            return(True)
 
474
        else:
 
475
            return(False)
 
476
    else:
 
477
        retVal = subprocess.call(['service', 'haproxy', action])
 
478
        if retVal == 0:
 
479
            return(True)
 
480
        else:
 
481
            return(False)
 
482
 
 
483
 
 
484
###############################################################################
 
485
# Hook functions
 
486
###############################################################################
 
487
def install_hook():
 
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)
 
491
 
 
492
 
 
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()
 
500
    else:
 
501
        haproxy_monitoring = None
 
502
    remove_services()
 
503
    create_services()
 
504
    haproxy_services = load_services()
 
505
    construct_haproxy_config(haproxy_globals, \
 
506
                             haproxy_defaults, \
 
507
                             haproxy_monitoring, \
 
508
                             haproxy_services)
 
509
 
 
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")
 
514
 
 
515
 
 
516
def start_hook():
 
517
    if service_haproxy("status"):
 
518
        return(service_haproxy("restart"))
 
519
    else:
 
520
        return(service_haproxy("start"))
 
521
 
 
522
 
 
523
def stop_hook():
 
524
    if service_haproxy("status"):
 
525
        return(service_haproxy("stop"))
 
526
 
 
527
 
 
528
def reverseproxy_interface(hook_name=None):
 
529
    if hook_name is None:
 
530
        return(None)
 
531
    if hook_name == "changed":
 
532
        config_changed()
 
533
 
 
534
 
 
535
def website_interface(hook_name=None):
 
536
    if hook_name is None:
 
537
        return(None)
 
538
    my_fqdn = socket.getfqdn(socket.gethostname())
 
539
    default_port = 80
 
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()
 
550
 
 
551
 
 
552
###############################################################################
 
553
# Main section
 
554
###############################################################################
 
555
if hook_name == "install":
 
556
    install_hook()
 
557
elif hook_name == "config-changed":
 
558
    config_changed()
 
559
elif hook_name == "start":
 
560
    start_hook()
 
561
elif hook_name == "stop":
 
562
    stop_hook()
 
563
elif hook_name == "reverseproxy-relation-broken":
 
564
    config_changed()
 
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")
 
571
else:
 
572
    print "Unknown hook"
 
573
    sys.exit(1)