3
3
4
4
"""Library containing logic pertaining to hostname resolutions in the VM charm."""
5
5
6
- import io
7
6
import json
8
7
import logging
9
8
import socket
10
9
import typing
11
10
12
- from ops .charm import RelationDepartedEvent
13
11
from ops .framework import Object
14
- from ops .model import BlockedStatus , Unit
12
+ from ops .model import Unit
13
+ from python_hosts import Hosts , HostsEntry
15
14
16
15
from constants import HOSTNAME_DETAILS , PEER
17
16
from ip_address_observer import IPAddressChangeCharmEvents , IPAddressObserver
22
21
if typing .TYPE_CHECKING :
23
22
from charm import MySQLOperatorCharm
24
23
24
+ COMMENT = "Managed by mysql charm"
25
+
25
26
26
27
class MySQLMachineHostnameResolution (Object ):
27
28
"""Encapsulation of the the machine hostname resolution."""
28
29
29
- on = IPAddressChangeCharmEvents ()
30
+ on = ( # pyright: ignore [reportIncompatibleMethodOverride, reportAssignmentType
31
+ IPAddressChangeCharmEvents ()
32
+ )
30
33
31
34
def __init__ (self , charm : "MySQLOperatorCharm" ):
32
35
super ().__init__ (charm , "hostname-resolution" )
@@ -38,12 +41,8 @@ def __init__(self, charm: "MySQLOperatorCharm"):
38
41
self .framework .observe (self .charm .on .config_changed , self ._update_host_details_in_databag )
39
42
self .framework .observe (self .on .ip_address_change , self ._update_host_details_in_databag )
40
43
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 )
47
46
48
47
self .ip_address_observer .start_observer ()
49
48
@@ -60,111 +59,58 @@ def _update_host_details_in_databag(self, _) -> None:
60
59
logger .exception ("Unable to get local IP address" )
61
60
ip = "127.0.0.1"
62
61
63
- host_details = {
64
- "hostname" : hostname ,
65
- "fqdn" : fqdn ,
66
- "ip" : ip ,
67
- }
62
+ host_details = {"names" : [hostname , fqdn ], "address" : ip }
68
63
69
64
self .charm .unit_peer_data [HOSTNAME_DETAILS ] = json .dumps (host_details )
70
65
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 []
73
71
74
72
for key , data in self .charm .peers .data .items ():
75
73
if isinstance (key , Unit ) and data .get (HOSTNAME_DETAILS ):
76
74
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 ()
84
75
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
+ )
89
86
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 )
93
88
94
- return bool ( outdated_hosts )
89
+ return host_details
95
90
96
- def _potentially_update_etc_hosts (self , _ ) -> None :
91
+ def update_etc_hosts (self , _ ) -> None :
97
92
"""Potentially update the /etc/hosts file with new hostname to IP for units."""
98
93
if not self .charm ._is_peer_data_set :
99
94
return
100
95
101
- host_details = self ._get_host_details ()
102
- if not host_details :
96
+ host_entries = self ._get_host_details ()
97
+ if not host_entries :
103
98
logger .debug ("No hostnames in the peer databag. Skipping update of /etc/hosts" )
104
99
return
105
100
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 ()
156
103
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" )
163
108
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 ()
166
112
167
113
try :
168
114
self .charm ._mysql .flush_host_cache ()
169
115
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