3
3
Instance domain support.
4
4
aka HAProxy Dynamic Configuration Updater
5
5
6
- HAProxy is a proxy server that is used to redirect the HTTP, HTTPS and SSL trafic
7
- to the instance, if they have it configured.
6
+ HAProxy is a proxy server used to redirect the HTTP, HTTPS and SSL traffic
7
+ to the instances if they have it configured.
8
8
9
- This module get the instance domain ip mapping and update the HAProxy config
10
- both live via it's unix socket and via the map file.
9
+ This module gets the instance domain ip mapping and updates the HAProxy config
10
+ both live via its unix control socket and via the map file.
11
11
12
12
13
- For the HAP protocol and commands used refer to
13
+ For the control protocol and commands used, refer to
14
14
https://www.haproxy.com/documentation/haproxy-configuration-manual/2-8r1/management/
15
15
16
- FIXME A known bug is that at HAProxy startup, the map file is loaded but the backend are
17
- not set.
18
16
"""
19
17
20
18
import dataclasses
@@ -210,21 +208,42 @@ def get_current_backends(socket_path, backend_name):
210
208
return servers
211
209
212
210
213
- def update_haproxy_backends (socket_path , backend_name , map_file_path , weight = 1 ):
211
+ def get_current_mappings (socket_path , map_file ) -> dict [str , str ]:
212
+ """Get a list of current mapping from HaProxy"""
213
+ # show map /etc/haproxy/http_domains.map
214
+ command = f"show map { map_file } "
215
+ response = send_socket_command (socket_path , command )
216
+
217
+ if not response :
218
+ return {}
219
+ try :
220
+ mappings = {}
221
+ lines = response .splitlines ()
222
+ for line in lines :
223
+ if not line :
224
+ continue
225
+ mapping_id , mapping_name , mapping_target = line .split ()
226
+ mappings [mapping_name ] = mapping_target
227
+
228
+ except Exception as e :
229
+ msg = f"Error retrieving current mapping: { e !s} "
230
+ raise Exception (msg ) from e
231
+ return mappings
232
+
233
+
234
+ def update_haproxy_backend (socket_path , backend_name , instances , map_file_path , port , weight = 1 ):
214
235
"""Update HAProxy backend servers config based on the map file.
215
236
216
237
Sync the running config with the content of the map file.
217
238
218
- This allow us to update the config without needing to reload or restart HAProxy.
239
+ This allows us to update the config without needing to reload or restart HAProxy.
219
240
220
241
It reads domain-to-IP mappings from a map file and uses HAProxy's
221
242
socket commands to dynamically add/update backend servers allowing update without requiring a reload
222
243
HAProxy.
223
244
"""
224
- mappings = parse_map_file (map_file_path )
225
- if not mappings :
226
- logger .error ("No valid mappings found in the map file." )
227
- return False
245
+
246
+ mappings = get_current_mappings (socket_path , map_file_path )
228
247
229
248
# Get current backend servers
230
249
current_servers = get_current_backends (socket_path , backend_name )
@@ -234,39 +253,50 @@ def update_haproxy_backends(socket_path, backend_name, map_file_path, weight=1):
234
253
processed_servers = set ()
235
254
236
255
# Process each mapping
237
- for domain , target in mappings :
238
- server_name = domain
256
+ for instance in instances :
257
+ server_name = instance ["name" ]
258
+ local_ip = instance ["ipv4" ]["local" ]
259
+ # custom domain name doesn't return the ip addr but the network range
260
+ addr = local_ip .split ("/" )[0 ]
261
+ if addr .endswith (".1" ):
262
+ addr = addr .rstrip (".1" ) + ".2"
239
263
processed_servers .add (server_name )
240
264
241
- # Check if server already exists in mapping
265
+ # Check if the server already exists
242
266
if server_name in current_servers :
243
- # FIXME : In the future, don't update the address if it hasn't changed'
244
267
# Update existing server
245
- addr , port = target .split (":" )
246
268
command = f"set server { backend_name } /{ server_name } addr { addr } port { port } "
247
269
logger .info (f"Updating server: { command } " )
248
270
response = send_socket_command (socket_path , command )
249
- if response and "not found" in response :
271
+ if response and "not found" in response : # Fall back
250
272
logger .warning (f"Server not found: { server_name } , trying to add it" )
251
273
# If server doesn't exist, add it
252
- command = f"add server { backend_name } /{ server_name } { target } weight { weight } maxconn 30"
274
+ command = f"add server { backend_name } /{ server_name } { addr } : { port } weight { weight } maxconn 30"
253
275
logger .info (f"Adding server: { command } " )
254
276
response = send_socket_command (socket_path , command )
255
- else :
256
- # Add new server
257
- command = f"add server { backend_name } /{ server_name } { target } weight { weight } maxconn 30"
258
- logger .info (f"Adding server: { command } " )
259
- response = send_socket_command (socket_path , command )
277
+ if response .strip () != "" :
278
+ logger .info (f"Error adding server { response } " )
260
279
261
- # Check response
262
- if response and "not found" in response :
263
- logger .error (f"Error processing server { server_name } : { response } " )
264
- else :
265
- command = f"enable server { backend_name } /{ server_name } "
266
- logger .info (f"Enable server: { command } " )
280
+ else : # Add the new server
281
+ command = f"add server { backend_name } /{ server_name } { addr } :{ port } weight { weight } maxconn 30"
282
+ logger .info (f"Adding server: { command } " )
267
283
response = send_socket_command (socket_path , command )
268
284
if response .strip () != "" :
269
- logger .info ("Error enabling server Response" )
285
+ logger .info (f"Error adding server { response } " )
286
+ # Enable the server
287
+ command = f"enable server { backend_name } /{ server_name } "
288
+ logger .info (f"Enable server: { command } " )
289
+ response = send_socket_command (socket_path , command )
290
+ if response .strip () != "" :
291
+ logger .info (f"Error enabling server { response } " )
292
+
293
+ # Add/set mapping
294
+ if server_name not in mappings :
295
+ response = send_socket_command (socket_path , f"add map { map_file_path } { server_name } { server_name } " )
296
+ logger .info (f"Added mapping: { server_name } { response = } " )
297
+ elif mappings [server_name ] != server_name :
298
+ response = send_socket_command (socket_path , f"set map { map_file_path } { server_name } { server_name } " )
299
+ logger .info (f"updated mapping: { server_name } { response = } " )
270
300
271
301
# Remove servers that are not in the map file
272
302
servers_to_remove = set (current_servers ) - processed_servers
@@ -306,36 +336,33 @@ async def fetch_list_and_update(socket_path, local_vms: list[str], force_update)
306
336
instances = [i for i in instances if i ["item_hash" ] in local_vms ]
307
337
# This should match the config in haproxy.cfg
308
338
for backend in HAPROXY_BACKENDS :
309
- update_backend (backend ["name" ], backend ["map_file" ], backend ["port" ], socket_path , instances , force_update )
339
+ update_backends (backend ["name" ], backend ["map_file" ], backend ["port" ], socket_path , instances , force_update )
310
340
311
341
312
- def update_backend (backend_name , map_file_path , port , socket_path , instances , force_update = False ):
313
- updated = update_mapfile (instances , map_file_path , port )
342
+ def update_backends (backend_name , map_file_path , port , socket_path , instances , force_update = False ):
343
+ updated = update_mapfile (instances , map_file_path )
314
344
if force_update :
315
345
logger .info ("Updating backends" )
316
- update_haproxy_backends (socket_path , backend_name , map_file_path , weight = 1 )
346
+ update_haproxy_backend (socket_path , backend_name , instances , map_file_path , port , weight = 1 )
317
347
elif updated :
318
348
logger .info ("Map file content changed, updating backends" )
319
- update_haproxy_backends (socket_path , backend_name , map_file_path , weight = 1 )
349
+ update_haproxy_backend (socket_path , backend_name , instances , map_file_path , port , weight = 1 )
320
350
321
351
else :
322
352
logger .debug ("Map file content no modification" )
323
353
324
354
325
- def update_mapfile (instances : list , map_file_path : str , port ) -> bool :
355
+ def update_mapfile (instances : list , map_file_path : str ) -> bool :
326
356
mapfile = Path (map_file_path )
327
357
previous_mapfile = ""
328
358
if mapfile .exists ():
329
359
content = mapfile .read_text ()
330
360
previous_mapfile = content
331
361
current_content = ""
362
+
332
363
for instance in instances :
333
- local_ip = instance ["ipv4" ]["local" ]
334
- if local_ip :
335
- local_ip = local_ip .split ("/" )[0 ]
336
- if local_ip .endswith (".1" ):
337
- local_ip = local_ip .rstrip (".1" ) + ".2"
338
- current_content += f"{ instance ['name' ]} { local_ip } :{ port } \n "
364
+ if instance ["ipv4" ]["local" ]:
365
+ current_content += f"{ instance ['name' ]} { instance ['name' ]} \n "
339
366
updated = current_content != previous_mapfile
340
367
if updated :
341
368
mapfile .write_text (current_content )
0 commit comments