33Instance domain support.
44aka HAProxy Dynamic Configuration Updater
55
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.
88
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.
1111
1212
13- For the HAP protocol and commands used refer to
13+ For the control protocol and commands used, refer to
1414https://www.haproxy.com/documentation/haproxy-configuration-manual/2-8r1/management/
1515
16- FIXME A known bug is that at HAProxy startup, the map file is loaded but the backend are
17- not set.
1816"""
1917
2018import dataclasses
@@ -210,21 +208,42 @@ def get_current_backends(socket_path, backend_name):
210208 return servers
211209
212210
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 ):
214235 """Update HAProxy backend servers config based on the map file.
215236
216237 Sync the running config with the content of the map file.
217238
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.
219240
220241 It reads domain-to-IP mappings from a map file and uses HAProxy's
221242 socket commands to dynamically add/update backend servers allowing update without requiring a reload
222243 HAProxy.
223244 """
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 )
228247
229248 # Get current backend servers
230249 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):
234253 processed_servers = set ()
235254
236255 # 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"
239263 processed_servers .add (server_name )
240264
241- # Check if server already exists in mapping
265+ # Check if the server already exists
242266 if server_name in current_servers :
243- # FIXME : In the future, don't update the address if it hasn't changed'
244267 # Update existing server
245- addr , port = target .split (":" )
246268 command = f"set server { backend_name } /{ server_name } addr { addr } port { port } "
247269 logger .info (f"Updating server: { command } " )
248270 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
250272 logger .warning (f"Server not found: { server_name } , trying to add it" )
251273 # 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"
253275 logger .info (f"Adding server: { command } " )
254276 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 } " )
260279
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 } " )
267283 response = send_socket_command (socket_path , command )
268284 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 = } " )
270300
271301 # Remove servers that are not in the map file
272302 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)
306336 instances = [i for i in instances if i ["item_hash" ] in local_vms ]
307337 # This should match the config in haproxy.cfg
308338 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 )
310340
311341
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 )
314344 if force_update :
315345 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 )
317347 elif updated :
318348 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 )
320350
321351 else :
322352 logger .debug ("Map file content no modification" )
323353
324354
325- def update_mapfile (instances : list , map_file_path : str , port ) -> bool :
355+ def update_mapfile (instances : list , map_file_path : str ) -> bool :
326356 mapfile = Path (map_file_path )
327357 previous_mapfile = ""
328358 if mapfile .exists ():
329359 content = mapfile .read_text ()
330360 previous_mapfile = content
331361 current_content = ""
362+
332363 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 "
339366 updated = current_content != previous_mapfile
340367 if updated :
341368 mapfile .write_text (current_content )
0 commit comments