Merge lp:~mthaddon/charms/precise/haproxy/mini-sprint-sf into lp:charms/haproxy
- Precise Pangolin (12.04)
- mini-sprint-sf
- Merge into trunk
Proposed by
Tom Haddon
on 2012-07-13
| Status: | Merged |
|---|---|
| Approved by: | Clint Byrum on 2012-07-23 |
| Approved revision: | 54 |
| Merge reported by: | Juan L. Negron |
| Merged at revision: | not available |
| Proposed branch: | lp:~mthaddon/charms/precise/haproxy/mini-sprint-sf |
| Merge into: | lp:charms/haproxy |
| Diff against target: |
889 lines (+683/-141) 10 files modified
config.yaml (+108/-0) hooks/hooks.py (+573/-0) hooks/install (+0/-11) hooks/reverseproxy-relation-changed (+0/-114) hooks/start (+0/-1) hooks/stop (+0/-3) hooks/website-relation-changed (+0/-8) hooks/website-relation-joined (+0/-2) metadata.yaml (+1/-1) revision (+1/-1) |
| To merge this branch: | bzr merge lp:~mthaddon/charms/precise/haproxy/mini-sprint-sf |
| Related bugs: |
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| Clint Byrum (community) | 2012-07-13 | Approve on 2012-07-23 | |
|
Review via email:
|
|||
Commit Message
Description of the Change
This is the outcome of a mini-sprint with myself and Juan Negron. We've added the ability to customise the haproxy config file's global and default sections as needed, allowed for an optional monitoring interface that's restricted to localhost by default, and also created the ability to specify multiple listen stanzas each of which can be customised as needed.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
| 1 | === added file 'config.yaml' |
| 2 | --- config.yaml 1970-01-01 00:00:00 +0000 |
| 3 | +++ config.yaml 2012-07-13 21:44:18 +0000 |
| 4 | @@ -0,0 +1,108 @@ |
| 5 | +options: |
| 6 | + global_log: |
| 7 | + default: "127.0.0.1 local0, 127.0.0.1 local1 notice" |
| 8 | + type: string |
| 9 | + description: Global log line ( multiples ... comma separated list ) |
| 10 | + global_maxconn: |
| 11 | + default: 4096 |
| 12 | + type: int |
| 13 | + description: | |
| 14 | + Sets the maximum per-process number of concurrent connections to |
| 15 | + <number>. |
| 16 | + global_user: |
| 17 | + default: "haproxy" |
| 18 | + type: string |
| 19 | + description: User |
| 20 | + global_group: |
| 21 | + default: "haproxy" |
| 22 | + type: string |
| 23 | + description: Group |
| 24 | + global_debug: |
| 25 | + default: False |
| 26 | + type: boolean |
| 27 | + description: Debug or not |
| 28 | + global_quiet: |
| 29 | + default: False |
| 30 | + type: boolean |
| 31 | + description: Quiet |
| 32 | + global_spread_checks: |
| 33 | + default: 0 |
| 34 | + type: int |
| 35 | + descriptions: | |
| 36 | + Sometimes it is desirable to avoid sending health checks to servers at |
| 37 | + exact intervals, for instance when many logical servers are located on |
| 38 | + the same physical server. With the help of this parameter, it becomes |
| 39 | + possible to add some randomness in the check interval between 0 and |
| 40 | + +/- 50%. A value between 2 and 5 seems to show good results. |
| 41 | + default_log: |
| 42 | + default: "global" |
| 43 | + type: string |
| 44 | + description: Default log |
| 45 | + default_mode: |
| 46 | + default: "http" |
| 47 | + type: string |
| 48 | + description: Default mode |
| 49 | + default_options: |
| 50 | + default: "httplog, dontlognull" |
| 51 | + type: string |
| 52 | + description: Default options |
| 53 | + default_retries: |
| 54 | + default: 3 |
| 55 | + type: int |
| 56 | + description: | |
| 57 | + Set the number of retries to perform on a server after a connection |
| 58 | + failure. It is important to understand that this value applies to the |
| 59 | + number of connection attempts, not full requests. When a connection |
| 60 | + has effectively been established to a server, there will be no more |
| 61 | + retry. |
| 62 | + In order to avoid immediate reconnections to a server which is |
| 63 | + restarting, a turn-around timer of 1 second is applied before a retry |
| 64 | + occurs. |
| 65 | + default_timeouts: |
| 66 | + default: "queue 1000, connect 1000, client 1000, server 1000" |
| 67 | + type: string |
| 68 | + description: Default timeouts |
| 69 | + enable_monitoring: |
| 70 | + default: False |
| 71 | + type: boolean |
| 72 | + description: Enable monitoring |
| 73 | + monitoring_port: |
| 74 | + default: 10000 |
| 75 | + type: int |
| 76 | + description: Default monitoring port |
| 77 | + monitoring_allowed_cidr: |
| 78 | + default: "127.0.0.1/32" |
| 79 | + type: string |
| 80 | + description: | |
| 81 | + CIDR allowed ( multiple CIDRs separated by space ) access to the |
| 82 | + monitoring interface. |
| 83 | + monitoring_username: |
| 84 | + default: "haproxy" |
| 85 | + type: string |
| 86 | + description: Monitoring username |
| 87 | + monitoring_password: |
| 88 | + default: "changeme" |
| 89 | + type: string |
| 90 | + description: | |
| 91 | + Password to the monitoring interface ( if "changeme", a new password |
| 92 | + will be generated and displayed in juju-log ) |
| 93 | + monitoring_stats_refresh: |
| 94 | + default: 3 |
| 95 | + type: int |
| 96 | + description: Monitoring interface refresh interval (in seconds) |
| 97 | + services: |
| 98 | + default: | |
| 99 | + - service_name: haproxy_service |
| 100 | + service_host: "0.0.0.0" |
| 101 | + service_port: 80 |
| 102 | + service_options: [balance leastconn] |
| 103 | + server_options: maxconn 100 |
| 104 | + type: string |
| 105 | + description: | |
| 106 | + Services definition(s). Although the variable type is a string, this is |
| 107 | + interpreted in the charm as yaml. To use multiple services within the |
| 108 | + same haproxy instance, specify all of the variables (service_name, |
| 109 | + service_host, service_port, service_options, server_options) with a "-" |
| 110 | + before the first variable, service_name, as above. Service options is a |
| 111 | + comma separated list, server options will be appended as a string to |
| 112 | + the individual server lines for a given listen stanza. |
| 113 | |
| 114 | === added symlink 'hooks/config-changed' |
| 115 | === target is u'./hooks.py' |
| 116 | === added file 'hooks/hooks.py' |
| 117 | --- hooks/hooks.py 1970-01-01 00:00:00 +0000 |
| 118 | +++ hooks/hooks.py 2012-07-13 21:44:18 +0000 |
| 119 | @@ -0,0 +1,573 @@ |
| 120 | +#!/usr/bin/env python |
| 121 | + |
| 122 | +import json |
| 123 | +import glob |
| 124 | +import os |
| 125 | +import random |
| 126 | +import re |
| 127 | +import socket |
| 128 | +import string |
| 129 | +import subprocess |
| 130 | +import sys |
| 131 | +import yaml |
| 132 | + |
| 133 | + |
| 134 | +############################################################################### |
| 135 | +# Global variables |
| 136 | +############################################################################### |
| 137 | +default_haproxy_config_dir = "/etc/haproxy" |
| 138 | +default_haproxy_config = "%s/haproxy.cfg" % default_haproxy_config_dir |
| 139 | +default_haproxy_service_config_dir = "/var/run/haproxy" |
| 140 | +hook_name = os.path.basename(sys.argv[0]) |
| 141 | + |
| 142 | +############################################################################### |
| 143 | +# Supporting functions |
| 144 | +############################################################################### |
| 145 | + |
| 146 | + |
| 147 | +#------------------------------------------------------------------------------ |
| 148 | +# config_get: Returns a dictionary containing all of the config information |
| 149 | +# Optional parameter: scope |
| 150 | +# scope: limits the scope of the returned configuration to the |
| 151 | +# desired config item. |
| 152 | +#------------------------------------------------------------------------------ |
| 153 | +def config_get(scope=None): |
| 154 | + try: |
| 155 | + config_cmd_line = ['config-get'] |
| 156 | + if scope is not None: |
| 157 | + config_cmd_line.append(scope) |
| 158 | + config_cmd_line.append('--format=json') |
| 159 | + config_data = json.loads(subprocess.check_output(config_cmd_line)) |
| 160 | + except: |
| 161 | + config_data = None |
| 162 | + finally: |
| 163 | + return(config_data) |
| 164 | + |
| 165 | + |
| 166 | +#------------------------------------------------------------------------------ |
| 167 | +# relation_get: Returns a dictionary containing the relation information |
| 168 | +# Optional parameters: scope, relation_id |
| 169 | +# scope: limits the scope of the returned data to the |
| 170 | +# desired item. |
| 171 | +# unit_name: limits the data ( and optionally the scope ) |
| 172 | +# to the specified unit |
| 173 | +#------------------------------------------------------------------------------ |
| 174 | +def relation_get(scope=None, unit_name=None): |
| 175 | + try: |
| 176 | + relation_cmd_line = ['relation-get', '--format=json'] |
| 177 | + if scope is not None: |
| 178 | + relation_cmd_line.append(scope) |
| 179 | + else: |
| 180 | + relation_cmd_line.append('') |
| 181 | + if unit_name is not None: |
| 182 | + relation_cmd_line.append(unit_name) |
| 183 | + relation_data = json.loads(subprocess.check_output(relation_cmd_line)) |
| 184 | + except: |
| 185 | + relation_data = None |
| 186 | + finally: |
| 187 | + return(relation_data) |
| 188 | + |
| 189 | + |
| 190 | +#------------------------------------------------------------------------------ |
| 191 | +# apt_get_install( package ): Installs a package |
| 192 | +#------------------------------------------------------------------------------ |
| 193 | +def apt_get_install(packages=None): |
| 194 | + if packages is None: |
| 195 | + return(False) |
| 196 | + cmd_line = ['apt-get', '-y', 'install', '-qq'] |
| 197 | + cmd_line.append(packages) |
| 198 | + return(subprocess.call(cmd_line)) |
| 199 | + |
| 200 | + |
| 201 | +#------------------------------------------------------------------------------ |
| 202 | +# enable_haproxy: Enabled haproxy at boot time |
| 203 | +#------------------------------------------------------------------------------ |
| 204 | +def enable_haproxy(): |
| 205 | + default_haproxy = "/etc/default/haproxy" |
| 206 | + enabled_haproxy = \ |
| 207 | + open(default_haproxy).read().replace('ENABLED=0', 'ENABLED=1') |
| 208 | + with open(default_haproxy, 'w') as f: |
| 209 | + f.write(enabled_haproxy) |
| 210 | + |
| 211 | + |
| 212 | +#------------------------------------------------------------------------------ |
| 213 | +# create_haproxy_globals: Creates the global section of the haproxy config |
| 214 | +#------------------------------------------------------------------------------ |
| 215 | +def create_haproxy_globals(): |
| 216 | + config_data = config_get() |
| 217 | + global_log = config_data['global_log'].split(',') |
| 218 | + haproxy_globals = [] |
| 219 | + haproxy_globals.append('global') |
| 220 | + for global_log_item in global_log: |
| 221 | + haproxy_globals.append(" log %s" % global_log_item.strip()) |
| 222 | + haproxy_globals.append(" maxconn %d" % config_data['global_maxconn']) |
| 223 | + haproxy_globals.append(" user %s" % config_data['global_user']) |
| 224 | + haproxy_globals.append(" group %s" % config_data['global_group']) |
| 225 | + if config_data['global_debug'] is True: |
| 226 | + haproxy_globals.append(" debug") |
| 227 | + if config_data['global_quiet'] is True: |
| 228 | + haproxy_globals.append(" quiet") |
| 229 | + haproxy_globals.append(" spread-checks %d" % \ |
| 230 | + config_data['global_spread_checks']) |
| 231 | + return('\n'.join(haproxy_globals)) |
| 232 | + |
| 233 | + |
| 234 | +#------------------------------------------------------------------------------ |
| 235 | +# create_haproxy_defaults: Creates the defaults section of the haproxy config |
| 236 | +#------------------------------------------------------------------------------ |
| 237 | +def create_haproxy_defaults(): |
| 238 | + config_data = config_get() |
| 239 | + default_options = config_data['default_options'].split(',') |
| 240 | + default_timeouts = config_data['default_timeouts'].split(',') |
| 241 | + haproxy_defaults = [] |
| 242 | + haproxy_defaults.append("defaults") |
| 243 | + haproxy_defaults.append(" log %s" % config_data['default_log']) |
| 244 | + haproxy_defaults.append(" mode %s" % config_data['default_mode']) |
| 245 | + for option_item in default_options: |
| 246 | + haproxy_defaults.append(" option %s" % option_item.strip()) |
| 247 | + haproxy_defaults.append(" retries %d" % config_data['default_retries']) |
| 248 | + for timeout_item in default_timeouts: |
| 249 | + haproxy_defaults.append(" timeout %s" % timeout_item.strip()) |
| 250 | + return('\n'.join(haproxy_defaults)) |
| 251 | + |
| 252 | + |
| 253 | +#------------------------------------------------------------------------------ |
| 254 | +# load_haproxy_config: Convenience function that loads (as a string) the |
| 255 | +# current haproxy configuration file. |
| 256 | +# Returns a string containing the haproxy config or |
| 257 | +# None |
| 258 | +#------------------------------------------------------------------------------ |
| 259 | +def load_haproxy_config(haproxy_config_file="/etc/haproxy/haproxy.cfg"): |
| 260 | + if os.path.isfile(haproxy_config_file): |
| 261 | + return(open(haproxy_config_file).read()) |
| 262 | + else: |
| 263 | + return(None) |
| 264 | + |
| 265 | + |
| 266 | +#------------------------------------------------------------------------------ |
| 267 | +# get_monitoring_password: Gets the monitoring password from the |
| 268 | +# haproxy config. |
| 269 | +# This prevents the password from being constantly |
| 270 | +# regenerated by the system. |
| 271 | +#------------------------------------------------------------------------------ |
| 272 | +def get_monitoring_password(haproxy_config_file="/etc/haproxy/haproxy.cfg"): |
| 273 | + haproxy_config = load_haproxy_config(haproxy_config_file) |
| 274 | + if haproxy_config is None: |
| 275 | + return(None) |
| 276 | + m = re.search("stats auth\s+(\w+):(\w+)", haproxy_config) |
| 277 | + if m is not None: |
| 278 | + return(m.group(2)) |
| 279 | + else: |
| 280 | + return(None) |
| 281 | + |
| 282 | + |
| 283 | +#------------------------------------------------------------------------------ |
| 284 | +# get_service_ports: Convenience function that scans the existing haproxy |
| 285 | +# configuration file and returns a list of the existing |
| 286 | +# ports being used. This is necessary to know which ports |
| 287 | +# to open and close when exposing/unexposing a service |
| 288 | +#------------------------------------------------------------------------------ |
| 289 | +def get_service_ports(haproxy_config_file="/etc/haproxy/haproxy.cfg"): |
| 290 | + haproxy_config = load_haproxy_config(haproxy_config_file) |
| 291 | + if haproxy_config is None: |
| 292 | + return(None) |
| 293 | + return(re.findall("listen.*:(.*)", haproxy_config)) |
| 294 | + |
| 295 | + |
| 296 | +#------------------------------------------------------------------------------ |
| 297 | +# open_port: Convenience function to open a port in juju to |
| 298 | +# expose a service |
| 299 | +#------------------------------------------------------------------------------ |
| 300 | +def open_port(port=None, protocol="TCP"): |
| 301 | + if port is None: |
| 302 | + return(None) |
| 303 | + return(subprocess.call(['/usr/bin/open-port', "%d/%s" % \ |
| 304 | + (int(port), protocol)])) |
| 305 | + |
| 306 | + |
| 307 | +#------------------------------------------------------------------------------ |
| 308 | +# close_port: Convenience function to close a port in juju to |
| 309 | +# unexpose a service |
| 310 | +#------------------------------------------------------------------------------ |
| 311 | +def close_port(port=None, protocol="TCP"): |
| 312 | + if port is None: |
| 313 | + return(None) |
| 314 | + return(subprocess.call(['/usr/bin/close-port', "%d/%s" % \ |
| 315 | + (int(port), protocol)])) |
| 316 | + |
| 317 | + |
| 318 | +#------------------------------------------------------------------------------ |
| 319 | +# update_service_ports: Convenience function that evaluate the old and new |
| 320 | +# service ports to decide which ports need to be |
| 321 | +# opened and which to close |
| 322 | +#------------------------------------------------------------------------------ |
| 323 | +def update_service_ports(old_service_ports=None, new_service_ports=None): |
| 324 | + if old_service_ports is None or new_service_ports is None: |
| 325 | + return(None) |
| 326 | + for port in old_service_ports: |
| 327 | + if port not in new_service_ports: |
| 328 | + close_port(port) |
| 329 | + for port in new_service_ports: |
| 330 | + if port not in old_service_ports: |
| 331 | + open_port(port) |
| 332 | + |
| 333 | + |
| 334 | +#------------------------------------------------------------------------------ |
| 335 | +# pwgen: Generates a random password |
| 336 | +# pwd_length: Defines the length of the password to generate |
| 337 | +# default: 20 |
| 338 | +#------------------------------------------------------------------------------ |
| 339 | +def pwgen(pwd_length=20): |
| 340 | + alphanumeric_chars = [l for l in (string.letters + string.digits) \ |
| 341 | + if l not in 'Iil0oO1'] |
| 342 | + random_chars = [random.choice(alphanumeric_chars) \ |
| 343 | + for i in range(pwd_length)] |
| 344 | + return(''.join(random_chars)) |
| 345 | + |
| 346 | + |
| 347 | +#------------------------------------------------------------------------------ |
| 348 | +# create_listen_stanza: Function to create a generic listen section in the |
| 349 | +# haproxy config |
| 350 | +# service_name: Arbitrary service name |
| 351 | +# service_ip: IP address to listen for connections |
| 352 | +# service_port: Port to listen for connections |
| 353 | +# service_options: Comma separated list of options |
| 354 | +# server_entries: List of tuples |
| 355 | +# server_name |
| 356 | +# server_ip |
| 357 | +# server_port |
| 358 | +# server_options |
| 359 | +#------------------------------------------------------------------------------ |
| 360 | +def create_listen_stanza(service_name=None, service_ip=None, |
| 361 | + service_port=None, service_options=None, |
| 362 | + server_entries=None): |
| 363 | + if service_name is None or service_ip is None or service_port is None: |
| 364 | + return(None) |
| 365 | + service_config = [] |
| 366 | + service_config.append("listen %s %s:%s" % \ |
| 367 | + (service_name, service_ip, service_port)) |
| 368 | + if service_options is not None: |
| 369 | + for service_option in service_options: |
| 370 | + service_config.append(" %s" % service_option.strip()) |
| 371 | + if server_entries is not None and type(server_entries) == type([]): |
| 372 | + for (server_name, server_ip, server_port, server_options) \ |
| 373 | + in server_entries: |
| 374 | + server_line = " server %s %s:%s" % \ |
| 375 | + (server_name, server_ip, server_port) |
| 376 | + if server_options is not None: |
| 377 | + server_line += " %s" % server_options |
| 378 | + service_config.append(server_line) |
| 379 | + return('\n'.join(service_config)) |
| 380 | + |
| 381 | + |
| 382 | +#------------------------------------------------------------------------------ |
| 383 | +# create_monitoring_stanza: Function to create the haproxy monitoring section |
| 384 | +# service_name: Arbitrary name |
| 385 | +#------------------------------------------------------------------------------ |
| 386 | +def create_monitoring_stanza(service_name="haproxy_monitoring"): |
| 387 | + config_data = config_get() |
| 388 | + if config_data['enable_monitoring'] is False: |
| 389 | + return(None) |
| 390 | + monitoring_password = get_monitoring_password() |
| 391 | + if config_data['monitoring_password'] != "changeme": |
| 392 | + monitoring_password = config_data['monitoring_password'] |
| 393 | + elif monitoring_password is None and \ |
| 394 | + config_data['monitoring_password'] == "changeme": |
| 395 | + monitoring_password = pwgen() |
| 396 | + monitoring_config = [] |
| 397 | + monitoring_config.append("mode http") |
| 398 | + monitoring_config.append("acl allowed_cidr src %s" % \ |
| 399 | + config_data['monitoring_allowed_cidr']) |
| 400 | + monitoring_config.append("block unless allowed_cidr") |
| 401 | + monitoring_config.append("stats enable") |
| 402 | + monitoring_config.append("stats uri /") |
| 403 | + monitoring_config.append("stats realm Haproxy\ Statistics") |
| 404 | + monitoring_config.append("stats auth %s:%s" % \ |
| 405 | + (config_data['monitoring_username'], monitoring_password)) |
| 406 | + monitoring_config.append("stats refresh %d" % \ |
| 407 | + config_data['monitoring_stats_refresh']) |
| 408 | + return(create_listen_stanza(service_name, \ |
| 409 | + "0.0.0.0", \ |
| 410 | + config_data['monitoring_port'], \ |
| 411 | + monitoring_config)) |
| 412 | + |
| 413 | + |
| 414 | +#------------------------------------------------------------------------------ |
| 415 | +# get_config_services: Convenience function that returns a list |
| 416 | +# of dictionary entries containing all of the services |
| 417 | +# configuration |
| 418 | +#------------------------------------------------------------------------------ |
| 419 | +def get_config_services(): |
| 420 | + config_data = config_get() |
| 421 | + services_list = yaml.load(config_data['services']) |
| 422 | + return(services_list) |
| 423 | + |
| 424 | + |
| 425 | +#------------------------------------------------------------------------------ |
| 426 | +# create_services: Function that will create the services configuration |
| 427 | +# from the config data and/or relation information |
| 428 | +#------------------------------------------------------------------------------ |
| 429 | +def create_services(): |
| 430 | + services_list = get_config_services() |
| 431 | + services_dict = {} |
| 432 | + for service_item in services_list: |
| 433 | + service_name = service_item['service_name'] |
| 434 | + service_host = service_item['service_host'] |
| 435 | + service_port = service_item['service_port'] |
| 436 | + service_options = service_item['service_options'] |
| 437 | + server_options = service_item['server_options'] |
| 438 | + services_dict[service_name] = {'service_name': service_name, |
| 439 | + 'service_host': service_host, |
| 440 | + 'service_port': service_port, |
| 441 | + 'service_options': service_options, |
| 442 | + 'server_options': server_options} |
| 443 | + |
| 444 | + try: |
| 445 | + for unit in json.loads(\ |
| 446 | + subprocess.check_output(['relation-list', '--format=json'])): |
| 447 | + relation_info = relation_get(None, unit) |
| 448 | + if type(relation_info) != type({}): |
| 449 | + sys.exit(0) |
| 450 | + # Mandatory switches ( hostname, port ) |
| 451 | + server_name = "%s__%s" % \ |
| 452 | + (relation_info['hostname'].replace('.', '_'), \ |
| 453 | + relation_info['port']) |
| 454 | + server_ip = relation_info['hostname'] |
| 455 | + server_port = relation_info['port'] |
| 456 | + # Optional switches ( service_name ) |
| 457 | + if 'service_name' in relation_info: |
| 458 | + if relation_info['service_name'] in services_dict: |
| 459 | + service_name = relation_info['service_name'] |
| 460 | + else: |
| 461 | + subprocess.call([\ |
| 462 | + 'juju-log', 'service %s does not exists. ' % \ |
| 463 | + relation_info['service_name']]) |
| 464 | + sys.exit(1) |
| 465 | + else: |
| 466 | + service_name = services_list[0]['service_name'] |
| 467 | + if os.path.exists("%s/%s.is.proxy" % \ |
| 468 | + (default_haproxy_service_config_dir, service_name)): |
| 469 | + if 'option forwardfor' not in service_options: |
| 470 | + service_options.append("option forwardfor") |
| 471 | + # Add the server entries |
| 472 | + if not 'servers' in services_dict[service_name]: |
| 473 | + services_dict[service_name]['servers'] = \ |
| 474 | + [(server_name, server_ip, server_port, \ |
| 475 | + services_dict[service_name]['server_options'])] |
| 476 | + else: |
| 477 | + services_dict[service_name]['servers'].append((\ |
| 478 | + server_name, server_ip, server_port, \ |
| 479 | + services_dict[service_name]['server_options'])) |
| 480 | + except: |
| 481 | + pass |
| 482 | + # Construct the new haproxy.cfg file |
| 483 | + for service in services_dict: |
| 484 | + print "Service: ", service |
| 485 | + server_entries = None |
| 486 | + if 'servers' in services_dict[service]: |
| 487 | + server_entries = services_dict[service]['servers'] |
| 488 | + with open("%s/%s.service" % (\ |
| 489 | + default_haproxy_service_config_dir, \ |
| 490 | + services_dict[service]['service_name']), 'w') as service_config: |
| 491 | + service_config.write(\ |
| 492 | + create_listen_stanza(services_dict[service]['service_name'],\ |
| 493 | + services_dict[service]['service_host'], |
| 494 | + services_dict[service]['service_port'], |
| 495 | + services_dict[service]['service_options'], |
| 496 | + server_entries)) |
| 497 | + |
| 498 | + |
| 499 | +#------------------------------------------------------------------------------ |
| 500 | +# load_services: Convenience function that load the service snippet |
| 501 | +# configuration from the filesystem. |
| 502 | +#------------------------------------------------------------------------------ |
| 503 | +def load_services(service_name=None): |
| 504 | + services = '' |
| 505 | + if service_name is not None: |
| 506 | + if os.path.exists("%s/%s.service" % \ |
| 507 | + (default_haproxy_service_config_dir, service_name)): |
| 508 | + services = open("%s/%s.service" % \ |
| 509 | + (default_haproxy_service_config_dir, service_name)).read() |
| 510 | + else: |
| 511 | + services = None |
| 512 | + else: |
| 513 | + for service in glob.glob("%s/*.service" % \ |
| 514 | + default_haproxy_service_config_dir): |
| 515 | + services += open(service).read() |
| 516 | + services += "\n\n" |
| 517 | + return(services) |
| 518 | + |
| 519 | + |
| 520 | +#------------------------------------------------------------------------------ |
| 521 | +# remove_services: Convenience function that removes the configuration |
| 522 | +# snippets from the filesystem. This is necessary |
| 523 | +# To ensure sync between the config/relation-data |
| 524 | +# and the existing haproxy services. |
| 525 | +#------------------------------------------------------------------------------ |
| 526 | +def remove_services(service_name=None): |
| 527 | + if service_name is not None: |
| 528 | + if os.path.exists("%s/%s.service" % \ |
| 529 | + (default_haproxy_service_config_dir, service_name)): |
| 530 | + try: |
| 531 | + os.remove("%s/%s.service" % \ |
| 532 | + (default_haproxy_service_config_dir, service_name)) |
| 533 | + return(True) |
| 534 | + except: |
| 535 | + return(False) |
| 536 | + else: |
| 537 | + for service in glob.glob("%s/*.service" % \ |
| 538 | + default_haproxy_service_config_dir): |
| 539 | + try: |
| 540 | + os.remove(service) |
| 541 | + except: |
| 542 | + pass |
| 543 | + return(True) |
| 544 | + |
| 545 | + |
| 546 | +#------------------------------------------------------------------------------ |
| 547 | +# construct_haproxy_config: Convenience function to write haproxy.cfg |
| 548 | +# haproxy_globals, haproxy_defaults, |
| 549 | +# haproxy_monitoring, haproxy_services |
| 550 | +# are all strings that will be written without |
| 551 | +# any checks. |
| 552 | +# haproxy_monitoring and haproxy_services are |
| 553 | +# optional arguments |
| 554 | +#------------------------------------------------------------------------------ |
| 555 | +def construct_haproxy_config(haproxy_globals=None, |
| 556 | + haproxy_defaults=None, |
| 557 | + haproxy_monitoring=None, |
| 558 | + haproxy_services=None): |
| 559 | + if haproxy_globals is None or \ |
| 560 | + haproxy_defaults is None: |
| 561 | + return(None) |
| 562 | + with open(default_haproxy_config, 'w') as haproxy_config: |
| 563 | + haproxy_config.write(haproxy_globals) |
| 564 | + haproxy_config.write("\n") |
| 565 | + haproxy_config.write("\n") |
| 566 | + haproxy_config.write(haproxy_defaults) |
| 567 | + haproxy_config.write("\n") |
| 568 | + haproxy_config.write("\n") |
| 569 | + if haproxy_monitoring is not None: |
| 570 | + haproxy_config.write(haproxy_monitoring) |
| 571 | + haproxy_config.write("\n") |
| 572 | + haproxy_config.write("\n") |
| 573 | + if haproxy_services is not None: |
| 574 | + haproxy_config.write(haproxy_services) |
| 575 | + haproxy_config.write("\n") |
| 576 | + haproxy_config.write("\n") |
| 577 | + |
| 578 | + |
| 579 | +#------------------------------------------------------------------------------ |
| 580 | +# service_haproxy: Convenience function to start/stop/restart/reload |
| 581 | +# the haproxy service |
| 582 | +#------------------------------------------------------------------------------ |
| 583 | +def service_haproxy(action=None, haproxy_config=default_haproxy_config): |
| 584 | + if action is None or haproxy_config is None: |
| 585 | + return(None) |
| 586 | + elif action == "check": |
| 587 | + retVal = subprocess.call(\ |
| 588 | + ['/usr/sbin/haproxy', '-f', haproxy_config, '-c']) |
| 589 | + if retVal == 1: |
| 590 | + return(False) |
| 591 | + elif retVal == 0: |
| 592 | + return(True) |
| 593 | + else: |
| 594 | + return(False) |
| 595 | + else: |
| 596 | + retVal = subprocess.call(['service', 'haproxy', action]) |
| 597 | + if retVal == 0: |
| 598 | + return(True) |
| 599 | + else: |
| 600 | + return(False) |
| 601 | + |
| 602 | + |
| 603 | +############################################################################### |
| 604 | +# Hook functions |
| 605 | +############################################################################### |
| 606 | +def install_hook(): |
| 607 | + if not os.path.exists(default_haproxy_service_config_dir): |
| 608 | + os.mkdir(default_haproxy_service_config_dir, 0600) |
| 609 | + return (apt_get_install("haproxy") == enable_haproxy() == True) |
| 610 | + |
| 611 | + |
| 612 | +def config_changed(): |
| 613 | + config_data = config_get() |
| 614 | + current_service_ports = get_service_ports() |
| 615 | + haproxy_globals = create_haproxy_globals() |
| 616 | + haproxy_defaults = create_haproxy_defaults() |
| 617 | + if config_data['enable_monitoring'] is True: |
| 618 | + haproxy_monitoring = create_monitoring_stanza() |
| 619 | + else: |
| 620 | + haproxy_monitoring = None |
| 621 | + remove_services() |
| 622 | + create_services() |
| 623 | + haproxy_services = load_services() |
| 624 | + construct_haproxy_config(haproxy_globals, \ |
| 625 | + haproxy_defaults, \ |
| 626 | + haproxy_monitoring, \ |
| 627 | + haproxy_services) |
| 628 | + |
| 629 | + if service_haproxy("check"): |
| 630 | + updated_service_ports = get_service_ports() |
| 631 | + update_service_ports(current_service_ports, updated_service_ports) |
| 632 | + service_haproxy("reload") |
| 633 | + |
| 634 | + |
| 635 | +def start_hook(): |
| 636 | + if service_haproxy("status"): |
| 637 | + return(service_haproxy("restart")) |
| 638 | + else: |
| 639 | + return(service_haproxy("start")) |
| 640 | + |
| 641 | + |
| 642 | +def stop_hook(): |
| 643 | + if service_haproxy("status"): |
| 644 | + return(service_haproxy("stop")) |
| 645 | + |
| 646 | + |
| 647 | +def reverseproxy_interface(hook_name=None): |
| 648 | + if hook_name is None: |
| 649 | + return(None) |
| 650 | + if hook_name == "changed": |
| 651 | + config_changed() |
| 652 | + |
| 653 | + |
| 654 | +def website_interface(hook_name=None): |
| 655 | + if hook_name is None: |
| 656 | + return(None) |
| 657 | + my_fqdn = socket.getfqdn(socket.gethostname()) |
| 658 | + default_port = 80 |
| 659 | + relation_data = relation_get() |
| 660 | + if hook_name == "joined": |
| 661 | + subprocess.call(['relation-set', 'port=%d' % \ |
| 662 | + default_port, 'hostname=%s' % my_fqdn]) |
| 663 | + elif hook_name == "changed": |
| 664 | + if 'is-proxy' in relation_data: |
| 665 | + service_name = "%s__%d" % \ |
| 666 | + (relation_data['hostname'], relation_data['port']) |
| 667 | + open("%s/%s.is.proxy" % \ |
| 668 | + (default_haproxy_service_config_dir, service_name), 'a').close() |
| 669 | + |
| 670 | + |
| 671 | +############################################################################### |
| 672 | +# Main section |
| 673 | +############################################################################### |
| 674 | +if hook_name == "install": |
| 675 | + install_hook() |
| 676 | +elif hook_name == "config-changed": |
| 677 | + config_changed() |
| 678 | +elif hook_name == "start": |
| 679 | + start_hook() |
| 680 | +elif hook_name == "stop": |
| 681 | + stop_hook() |
| 682 | +elif hook_name == "reverseproxy-relation-broken": |
| 683 | + config_changed() |
| 684 | +elif hook_name == "reverseproxy-relation-changed": |
| 685 | + reverseproxy_interface("changed") |
| 686 | +elif hook_name == "website-relation-joined": |
| 687 | + website_interface("joined") |
| 688 | +elif hook_name == "website-relation-changed": |
| 689 | + website_interface("changed") |
| 690 | +else: |
| 691 | + print "Unknown hook" |
| 692 | + sys.exit(1) |
| 693 | |
| 694 | === modified file 'hooks/install' |
| 695 | --- hooks/install 2011-02-16 01:17:47 +0000 |
| 696 | +++ hooks/install 1970-01-01 00:00:00 +0000 |
| 697 | @@ -1,11 +0,0 @@ |
| 698 | -#!/bin/bash |
| 699 | - |
| 700 | -set -e |
| 701 | - |
| 702 | -DEBIAN_FRONTEND=noninteractive apt-get -y install -qq haproxy |
| 703 | - |
| 704 | -cat > /etc/default/haproxy <<EOF |
| 705 | -ENABLED=1 |
| 706 | -EOF |
| 707 | -# We actually want it stopped until at least one thing has joined |
| 708 | -service haproxy stop |
| 709 | |
| 710 | === target is u'./hooks.py' |
| 711 | === added symlink 'hooks/reverseproxy-relation-broken' |
| 712 | === target is u'./hooks.py' |
| 713 | === modified file 'hooks/reverseproxy-relation-changed' |
| 714 | --- hooks/reverseproxy-relation-changed 2011-09-30 20:13:29 +0000 |
| 715 | +++ hooks/reverseproxy-relation-changed 1970-01-01 00:00:00 +0000 |
| 716 | @@ -1,114 +0,0 @@ |
| 717 | -#!/usr/bin/env python |
| 718 | -# |
| 719 | -# reverseproxy-relation-changed - hook for when reverse proxy relation changes |
| 720 | -# |
| 721 | -# Copyright (C) 2011 Canonical Ltd. |
| 722 | -# Author: Clint Byrum <clint.byrum@canonical.com> |
| 723 | -# |
| 724 | -# This program is free software: you can redistribute it and/or modify |
| 725 | -# it under the terms of the GNU General Public License as published by |
| 726 | -# the Free Software Foundation, either version 3 of the License, or |
| 727 | -# (at your option) any later version. |
| 728 | -# |
| 729 | -# This program is distributed in the hope that it will be useful, |
| 730 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 731 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 732 | -# GNU General Public License for more details. |
| 733 | -# |
| 734 | -# You should have received a copy of the GNU General Public License |
| 735 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 736 | -# |
| 737 | - |
| 738 | -import sys |
| 739 | -import os |
| 740 | -import subprocess |
| 741 | -import json |
| 742 | -import tempfile |
| 743 | -import glob |
| 744 | - |
| 745 | -from socket import getaddrinfo |
| 746 | - |
| 747 | -remote_unit = os.environ.get("JUJU_REMOTE_UNIT") |
| 748 | - |
| 749 | -service_name, _ = remote_unit.split("/") |
| 750 | - |
| 751 | -# TODO: maybe load this from disk for easier customization |
| 752 | -template = """ |
| 753 | -# Generated by juju |
| 754 | -# this config needs haproxy-1.1.28 or haproxy-1.2.1 |
| 755 | - |
| 756 | -global |
| 757 | - log 127.0.0.1 local0 |
| 758 | - log 127.0.0.1 local1 notice |
| 759 | - #log loghost local0 info |
| 760 | - maxconn 4096 |
| 761 | - #chroot /usr/share/haproxy |
| 762 | - user haproxy |
| 763 | - group haproxy |
| 764 | - daemon |
| 765 | - #debug |
| 766 | - #quiet |
| 767 | - |
| 768 | -defaults |
| 769 | - log global |
| 770 | - mode http |
| 771 | - option httplog |
| 772 | - option dontlognull |
| 773 | - retries 3 |
| 774 | - option redispatch |
| 775 | - maxconn 2000 |
| 776 | - contimeout 5000 |
| 777 | - clitimeout 50000 |
| 778 | - srvtimeout 50000 |
| 779 | - |
| 780 | -listen %s 0.0.0.0:80 |
| 781 | - option httpchk * |
| 782 | - balance roundrobin |
| 783 | -""" |
| 784 | - |
| 785 | -units = [] |
| 786 | -p = subprocess.Popen("relation-list", stdout=subprocess.PIPE) |
| 787 | -for unit in p.stdout: |
| 788 | - units.append(unit.strip()) |
| 789 | - |
| 790 | -print units |
| 791 | - |
| 792 | -# Right now we don't know how to connect fronts to backs, but we're going to say |
| 793 | -# if there are any upstream proxies we will turn off forwardedfor |
| 794 | -options = '' |
| 795 | -proxy_services = glob.glob("/etc/haproxy/*.is.proxy") |
| 796 | -if len(proxy_services) == 0: |
| 797 | - options += " option forwardfor\n" |
| 798 | - |
| 799 | -servers = '' |
| 800 | -for unit in units: |
| 801 | - p = subprocess.Popen(["relation-get", "--format", "json", "-", unit], |
| 802 | - stdout=subprocess.PIPE, close_fds=True) |
| 803 | - settings = json.loads(p.stdout.read().strip()) |
| 804 | - p.wait() |
| 805 | - # Add all configured units: |
| 806 | - if 'hostname' in settings and 'port' in settings: |
| 807 | - servers += (" server %(hostname)s %(hostname)s:%(port)s check\n" % settings) |
| 808 | - |
| 809 | -print servers |
| 810 | - |
| 811 | -with tempfile.NamedTemporaryFile(dir="/etc/haproxy",prefix="haproxy.cfg", delete=False) as conf: |
| 812 | - conf.write((template % service_name) + options + servers) |
| 813 | - try: |
| 814 | - os.unlink("/etc/haproxy/haproxy.cfg.old") |
| 815 | - except: |
| 816 | - pass |
| 817 | - try: |
| 818 | - os.rename("/etc/haproxy/haproxy.cfg","/etc/haproxy/haproxy.old") |
| 819 | - except: |
| 820 | - pass |
| 821 | - try: |
| 822 | - os.rename(conf.name, "/etc/haproxy/haproxy.cfg") |
| 823 | - except: |
| 824 | - os.unlink(conf.name) |
| 825 | - |
| 826 | -# Just in case haproxy wouldn't start because of empty/bad configs before, start it now |
| 827 | -subprocess.call(["service", "haproxy", "start"]) |
| 828 | -subprocess.call(["service", "haproxy", "reload"]) |
| 829 | - |
| 830 | -subprocess.call(["open-port", "80"]) |
| 831 | |
| 832 | === target is u'./hooks.py' |
| 833 | === modified file 'hooks/start' |
| 834 | --- hooks/start 2011-02-16 01:17:47 +0000 |
| 835 | +++ hooks/start 1970-01-01 00:00:00 +0000 |
| 836 | @@ -1,1 +0,0 @@ |
| 837 | -#!/bin/bash |
| 838 | \ No newline at end of file |
| 839 | |
| 840 | === target is u'./hooks.py' |
| 841 | === modified file 'hooks/stop' |
| 842 | --- hooks/stop 2012-06-27 16:48:38 +0000 |
| 843 | +++ hooks/stop 1970-01-01 00:00:00 +0000 |
| 844 | @@ -1,3 +0,0 @@ |
| 845 | -#!/bin/bash |
| 846 | - |
| 847 | -/etc/init.d/haproxy stop |
| 848 | |
| 849 | === target is u'./hooks.py' |
| 850 | === modified file 'hooks/website-relation-changed' |
| 851 | --- hooks/website-relation-changed 2011-09-30 20:13:29 +0000 |
| 852 | +++ hooks/website-relation-changed 1970-01-01 00:00:00 +0000 |
| 853 | @@ -1,8 +0,0 @@ |
| 854 | -#!/bin/sh |
| 855 | - |
| 856 | -service=`echo $JUJU_REMOTE_UNIT | cut -d/ -f1` |
| 857 | -isproxy=`relation-get is-proxy` |
| 858 | - |
| 859 | -if [ -n "$isproxy" ] ; then |
| 860 | - touch /etc/haproxy/$service.is.proxy |
| 861 | -fi |
| 862 | |
| 863 | === target is u'./hooks.py' |
| 864 | === modified file 'hooks/website-relation-joined' |
| 865 | --- hooks/website-relation-joined 2011-06-15 23:55:56 +0000 |
| 866 | +++ hooks/website-relation-joined 1970-01-01 00:00:00 +0000 |
| 867 | @@ -1,2 +0,0 @@ |
| 868 | -#!/bin/sh |
| 869 | -relation-set port=80 hostname=`hostname -f` |
| 870 | |
| 871 | === target is u'./hooks.py' |
| 872 | === modified file 'metadata.yaml' |
| 873 | --- metadata.yaml 2012-05-22 22:29:14 +0000 |
| 874 | +++ metadata.yaml 2012-07-13 21:44:18 +0000 |
| 875 | @@ -1,6 +1,6 @@ |
| 876 | name: haproxy |
| 877 | summary: "fast and reliable load balancing reverse proxy" |
| 878 | -maintainer: Clint Byrum <clint@ubuntu.com> |
| 879 | +maintainer: Juan Negron <juan@ubuntu.com>, Tom Haddon <tom.haddon@canonical.com> |
| 880 | description: |
| 881 | HAProxy is a TCP/HTTP reverse proxy which is particularly suited for high |
| 882 | availability environments. It features connection persistence through HTTP |
| 883 | |
| 884 | === modified file 'revision' |
| 885 | --- revision 2011-10-11 19:17:16 +0000 |
| 886 | +++ revision 2012-07-13 21:44:18 +0000 |
| 887 | @@ -1,1 +1,1 @@ |
| 888 | -15 |
| 889 | +22 |

Alright, the discussions on IRC were very helpful and I wanted to record the outcome here.
* First, this includes what is basically a workaround for not having "relation configs". Putting matching pairs of configuration data in the config options of each charm does not feel natural and will lead to confusion. A bug has been filed in juju-core to address this:
https:/ /bugs.launchpad .net/juju- core/+bug/ 1026422
* Second, the abstractions in here are really beautiful. I think that many of them belong in charm-helper so that other python charms can make use of them and simplify their code.
* Third, nicely done, please merge!