33
44"""Library containing logic pertaining to hostname resolutions in the VM charm."""
55
6- import io
76import json
87import logging
98import socket
109import typing
1110
12- from ops .charm import RelationDepartedEvent
1311from ops .framework import Object
14- from ops .model import BlockedStatus , Unit
12+ from ops .model import Unit
13+ from python_hosts import Hosts , HostsEntry
1514
1615from constants import HOSTNAME_DETAILS , PEER
1716from ip_address_observer import IPAddressChangeCharmEvents , IPAddressObserver
2221if typing .TYPE_CHECKING :
2322 from charm import MySQLOperatorCharm
2423
24+ COMMENT = "Managed by mysql charm"
25+
2526
2627class MySQLMachineHostnameResolution (Object ):
2728 """Encapsulation of the the machine hostname resolution."""
2829
29- on = IPAddressChangeCharmEvents ()
30+ on = ( # pyright: ignore [reportIncompatibleMethodOverride, reportAssignmentType
31+ IPAddressChangeCharmEvents ()
32+ )
3033
3134 def __init__ (self , charm : "MySQLOperatorCharm" ):
3235 super ().__init__ (charm , "hostname-resolution" )
@@ -38,12 +41,8 @@ def __init__(self, charm: "MySQLOperatorCharm"):
3841 self .framework .observe (self .charm .on .config_changed , self ._update_host_details_in_databag )
3942 self .framework .observe (self .on .ip_address_change , self ._update_host_details_in_databag )
4043
41- self .framework .observe (
42- self .charm .on [PEER ].relation_changed , self ._potentially_update_etc_hosts
43- )
44- self .framework .observe (
45- self .charm .on [PEER ].relation_departed , self ._remove_host_from_etc_hosts
46- )
44+ self .framework .observe (self .charm .on [PEER ].relation_changed , self .update_etc_hosts )
45+ self .framework .observe (self .charm .on [PEER ].relation_departed , self .update_etc_hosts )
4746
4847 self .ip_address_observer .start_observer ()
4948
@@ -60,111 +59,58 @@ def _update_host_details_in_databag(self, _) -> None:
6059 logger .exception ("Unable to get local IP address" )
6160 ip = "127.0.0.1"
6261
63- host_details = {
64- "hostname" : hostname ,
65- "fqdn" : fqdn ,
66- "ip" : ip ,
67- }
62+ host_details = {"names" : [hostname , fqdn ], "address" : ip }
6863
6964 self .charm .unit_peer_data [HOSTNAME_DETAILS ] = json .dumps (host_details )
7065
71- def _get_host_details (self ) -> dict [str , str ]:
72- host_details = {}
66+ def _get_host_details (self ) -> list [HostsEntry ]:
67+ host_details = list ()
68+
69+ if not self .charm .peers :
70+ return []
7371
7472 for key , data in self .charm .peers .data .items ():
7573 if isinstance (key , Unit ) and data .get (HOSTNAME_DETAILS ):
7674 unit_details = json .loads (data [HOSTNAME_DETAILS ])
77- unit_details ["unit" ] = key .name
78- host_details [unit_details ["hostname" ]] = unit_details
79-
80- return host_details
81-
82- def _does_etc_hosts_need_update (self , host_details : dict [str , str ]) -> bool :
83- outdated_hosts = host_details .copy ()
8475
85- with open ("/etc/hosts" , "r" ) as hosts_file :
86- for line in hosts_file :
87- if "# unit=" not in line :
88- continue
76+ if unit_details .get ("address" ):
77+ entry = HostsEntry (comment = COMMENT , entry_type = "ipv4" , ** unit_details )
78+ else :
79+ # case when migrating from old format
80+ entry = HostsEntry (
81+ address = unit_details ["ip" ],
82+ names = [unit_details ["hostname" ], unit_details ["fqdn" ]],
83+ comment = COMMENT ,
84+ entry_type = "ipv4" ,
85+ )
8986
90- ip , fqdn , hostname = line .split ("#" )[0 ].strip ().split ()
91- if outdated_hosts .get (hostname ).get ("ip" ) == ip :
92- outdated_hosts .pop (hostname )
87+ host_details .append (entry )
9388
94- return bool ( outdated_hosts )
89+ return host_details
9590
96- def _potentially_update_etc_hosts (self , _ ) -> None :
91+ def update_etc_hosts (self , _ ) -> None :
9792 """Potentially update the /etc/hosts file with new hostname to IP for units."""
9893 if not self .charm ._is_peer_data_set :
9994 return
10095
101- host_details = self ._get_host_details ()
102- if not host_details :
96+ host_entries = self ._get_host_details ()
97+ if not host_entries :
10398 logger .debug ("No hostnames in the peer databag. Skipping update of /etc/hosts" )
10499 return
105100
106- if not self ._does_etc_hosts_need_update (host_details ):
107- logger .debug ("No hostnames in /etc/hosts changed. Skipping update to /etc/hosts" )
108- return
109-
110- hosts_in_file = []
111-
112- with io .StringIO () as updated_hosts_file :
113- with open ("/etc/hosts" , "r" ) as hosts_file :
114- for line in hosts_file :
115- if "# unit=" not in line :
116- updated_hosts_file .write (line )
117- continue
118-
119- for hostname , details in host_details .items ():
120- if hostname == line .split ()[2 ]:
121- hosts_in_file .append (hostname )
122-
123- fqdn , ip , unit = details ["fqdn" ], details ["ip" ], details ["unit" ]
124-
125- logger .debug (
126- f"Overwriting { hostname } ({ unit = } ) with { ip = } , { fqdn = } in /etc/hosts"
127- )
128- updated_hosts_file .write (f"{ ip } { fqdn } { hostname } # unit={ unit } \n " )
129- break
130-
131- for hostname , details in host_details .items ():
132- if hostname not in hosts_in_file :
133- fqdn , ip , unit = details ["fqdn" ], details ["ip" ], details ["unit" ]
134-
135- logger .debug (f"Adding { hostname } ({ unit = } with { ip = } , { fqdn = } in /etc/hosts" )
136- updated_hosts_file .write (f"{ ip } { fqdn } { hostname } # unit={ unit } \n " )
137-
138- with open ("/etc/hosts" , "w" ) as hosts_file :
139- hosts_file .write (updated_hosts_file .getvalue ())
140-
141- try :
142- self .charm ._mysql .flush_host_cache ()
143- except MySQLFlushHostCacheError :
144- self .charm .unit .status = BlockedStatus ("Unable to flush MySQL host cache" )
145-
146- def _remove_host_from_etc_hosts (self , event : RelationDepartedEvent ) -> None :
147- departing_unit_name = event .unit .name
148-
149- logger .debug (f"Checking if an entry for { departing_unit_name } is in /etc/hosts" )
150- with open ("/etc/hosts" , "r" ) as hosts_file :
151- for line in hosts_file :
152- if f"# unit={ departing_unit_name } " in line :
153- break
154- else :
155- return
101+ logger .debug ("Updating /etc/hosts with new hostname to IP mappings" )
102+ hosts = Hosts ()
156103
157- logger .debug (f"Removing entry for { departing_unit_name } from /etc/hosts" )
158- with io .StringIO () as updated_hosts_file :
159- with open ("/etc/hosts" , "r" ) as hosts_file :
160- for line in hosts_file :
161- if f"# unit={ departing_unit_name } " not in line :
162- updated_hosts_file .write (line )
104+ if hosts .exists (address = "127.0.1.1" , names = [socket .getfqdn ()]):
105+ # remove MAAS injected entry
106+ logger .debug ("Removing MAAS injected entry from /etc/hosts" )
107+ hosts .remove_all_matching (address = "127.0.1.1" )
163108
164- with open ("/etc/hosts" , "w" ) as hosts_file :
165- hosts_file .write (updated_hosts_file .getvalue ())
109+ hosts .remove_all_matching (comment = COMMENT )
110+ hosts .add (host_entries )
111+ hosts .write ()
166112
167113 try :
168114 self .charm ._mysql .flush_host_cache ()
169115 except MySQLFlushHostCacheError :
170- self . charm . unit . status = BlockedStatus ("Unable to flush MySQL host cache" )
116+ logger . warning ("Unable to flush MySQL host cache" )
0 commit comments