Skip to content

Commit 03012d3

Browse files
authored
DPE-3115 Fix MAAS deployment (#444)
* refactor hostname resolution with maas fix * call on init * proper version definition and more specific offending address filtering
1 parent 0894bc7 commit 03012d3

File tree

7 files changed

+122
-120
lines changed

7 files changed

+122
-120
lines changed

poetry.lock

Lines changed: 12 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ boto3 = "^1.28.23"
1717
pyopenssl = "^24.0.0"
1818
typing_extensions = "^4.7.1"
1919
jinja2 = "^3.1.2"
20+
python_hosts = "^1.0.6"
2021

2122
[tool.poetry.group.charm-libs.dependencies]
2223
# data_platform_libs/v0/data_interfaces.py

src/charm.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,9 @@ def workload_initialise(self) -> None:
620620
self._mysql.reset_root_password_and_start_mysqld()
621621
self._mysql.configure_mysql_users()
622622

623+
# ensure hostname can be resolved
624+
self.hostname_resolution.update_etc_hosts(None)
625+
623626
current_mysqld_pid = self._mysql.get_pid_of_port_3306()
624627
self._mysql.configure_instance()
625628

src/hostname_resolution.py

Lines changed: 40 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33

44
"""Library containing logic pertaining to hostname resolutions in the VM charm."""
55

6-
import io
76
import json
87
import logging
98
import socket
109
import typing
1110

12-
from ops.charm import RelationDepartedEvent
1311
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
1514

1615
from constants import HOSTNAME_DETAILS, PEER
1716
from ip_address_observer import IPAddressChangeCharmEvents, IPAddressObserver
@@ -22,11 +21,15 @@
2221
if typing.TYPE_CHECKING:
2322
from charm import MySQLOperatorCharm
2423

24+
COMMENT = "Managed by mysql charm"
25+
2526

2627
class 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")

tests/unit/test_backups.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ def test_can_unit_perform_backup(
327327
@patch_network_get(private_address="1.1.1.1")
328328
@patch("mysql_vm_helpers.MySQL.offline_mode_and_hidden_instance_exists", return_value=False)
329329
@patch("mysql_vm_helpers.MySQL.get_member_state")
330-
@patch("hostname_resolution.MySQLMachineHostnameResolution._remove_host_from_etc_hosts")
330+
@patch("python_hosts.Hosts.write")
331331
def test_can_unit_perform_backup_failure(
332332
self,
333333
_,
@@ -379,7 +379,7 @@ def test_can_unit_perform_backup_failure(
379379
@patch_network_get(private_address="1.1.1.1")
380380
@patch("mysql_vm_helpers.MySQL.set_instance_option")
381381
@patch("mysql_vm_helpers.MySQL.set_instance_offline_mode")
382-
@patch("hostname_resolution.MySQLMachineHostnameResolution._remove_host_from_etc_hosts")
382+
@patch("python_hosts.Hosts.write")
383383
def test_pre_backup(
384384
self,
385385
_,
@@ -549,7 +549,7 @@ def test_pre_restore_checks(
549549
@patch_network_get(private_address="1.1.1.1")
550550
@patch("mysql_vm_helpers.MySQL.is_server_connectable", return_value=True)
551551
@patch("charm.MySQLOperatorCharm.is_unit_busy", return_value=False)
552-
@patch("hostname_resolution.MySQLMachineHostnameResolution._remove_host_from_etc_hosts")
552+
@patch("python_hosts.Hosts.write")
553553
def test_pre_restore_checks_failure(
554554
self,
555555
_,

tests/unit/test_charm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ def test_on_start_exceptions(
290290
@patch("charm.is_volume_mounted", return_value=True)
291291
@patch("mysql_vm_helpers.MySQL.reboot_from_complete_outage")
292292
@patch("charm.snap_service_operation")
293-
@patch("hostname_resolution.MySQLMachineHostnameResolution._remove_host_from_etc_hosts")
293+
@patch("python_hosts.Hosts.write")
294294
def test_on_update(
295295
self,
296296
_,

0 commit comments

Comments
 (0)