Merge ~barryprice/ubuntu-mirror-charm/+git/ubuntu-mirror-charm:master into ubuntu-mirror-charm:master

Proposed by Barry Price
Status: Merged
Approved by: Barry Price
Approved revision: 54910da854edbaf42b53be330e82f1114bc819a8
Merged at revision: 70d7abcd0bae4f2c2d8603576e0d6cfcb045b5e0
Proposed branch: ~barryprice/ubuntu-mirror-charm/+git/ubuntu-mirror-charm:master
Merge into: ubuntu-mirror-charm:master
Diff against target: 253 lines (+176/-1)
3 files modified
hooks/hooks.py (+108/-0)
tests/unit/requirements.txt (+1/-0)
tests/unit/test_charm.py (+67/-1)
Reviewer Review Type Date Requested Status
Haw Loeung Approve
Paul Collins lgtm Approve
Barry Price Needs Resubmitting
Canonical IS Reviewers Pending
Review via email: mp+390394@code.launchpad.net

Commit message

Handle LP:1891566 by bind-mounting any paths outside /srv/ftp.root back into it, so that our chrooted vsftp can serve all the same files as apache2/rsync.

To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
Joel Sing (jsing) :
Revision history for this message
Haw Loeung (hloeung) :
Revision history for this message
Barry Price (barryprice) :
review: Needs Resubmitting
Revision history for this message
Paul Collins (pjdc) :
review: Approve (lgtm)
Revision history for this message
Haw Loeung (hloeung) wrote :

LGTM

review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision 70d7abcd0bae4f2c2d8603576e0d6cfcb045b5e0

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/hooks/hooks.py b/hooks/hooks.py
2index 0b80434..2757b35 100755
3--- a/hooks/hooks.py
4+++ b/hooks/hooks.py
5@@ -4,6 +4,7 @@
6 # Author: Chris Stratford <chris.stratford@canonical.com>
7
8 import base64
9+import datetime
10 import json
11 import os
12 import os.path
13@@ -85,6 +86,8 @@ scripts_to_copy = [
14 'check-updates.sh',
15 ]
16
17+bind_mount_comment = "# Bind mount managed by the ubuntu-mirror charm"
18+
19
20 def juju_header():
21 header = ("#-------------------------------------------------#\n"
22@@ -144,6 +147,110 @@ def check_output(cmd):
23 return None
24
25
26+def get_managed_mounts():
27+ managed_mounts = []
28+ with open("/etc/fstab", "r") as f:
29+ for line in f:
30+ if line.strip().endswith(bind_mount_comment):
31+ managed_mounts.append(line.replace(bind_mount_comment, '').strip())
32+ return managed_mounts
33+
34+
35+def add_bind_mount(fstab_line):
36+ target = fstab_line.split()[1]
37+ line_to_write = "{} {}\n".format(fstab_line, bind_mount_comment)
38+ fstab_path = "/etc/fstab"
39+
40+ suffix = "_{:%Y-%m-%d_%H:%M:%S}".format(datetime.datetime.now())
41+ tempfile_suffix = "_temp" + suffix
42+ backup_suffix = "_backup" + suffix
43+
44+ tempfile_path = fstab_path + tempfile_suffix
45+ backup_path = fstab_path + backup_suffix
46+
47+ shutil.copyfile(fstab_path, tempfile_path)
48+ shutil.copyfile(fstab_path, backup_path)
49+
50+ with open(tempfile_path, "a") as f:
51+ f.write(line_to_write)
52+ os.rename(tempfile_path, fstab_path)
53+ if not os.path.isdir(target):
54+ mkdir(target)
55+ check_call("mount {}".format(target).split())
56+
57+
58+def remove_bind_mount(fstab_line):
59+ target = fstab_line.split()[1]
60+ removed = False
61+ fstab_path = "/etc/fstab"
62+
63+ suffix = "_{:%Y-%m-%d_%H:%M:%S}".format(datetime.datetime.now())
64+ tempfile_suffix = "_temp" + suffix
65+ backup_suffix = "_backup" + suffix
66+
67+ tempfile_path = fstab_path + tempfile_suffix
68+ backup_path = fstab_path + backup_suffix
69+
70+ shutil.copyfile(fstab_path, backup_path)
71+
72+ with open(fstab_path, "r") as f:
73+ lines = f.readlines()
74+ with open(tempfile_path, "w") as f:
75+ for line in lines:
76+ if line.strip().endswith(bind_mount_comment):
77+ if line.replace(bind_mount_comment, '').strip() == fstab_line:
78+ removed = True
79+ continue
80+ f.write(line)
81+ os.rename(tempfile_path, fstab_path)
82+ if removed:
83+ check_call("umount {}".format(target).split())
84+ check_call("rmdir {}".format(target).split())
85+
86+
87+def update_bind_mounts(required_mounts):
88+ managed_mounts = get_managed_mounts()
89+
90+ for required_mount in required_mounts:
91+ if required_mount not in managed_mounts:
92+ add_bind_mount(required_mount)
93+
94+ for managed_mount in managed_mounts:
95+ if managed_mount not in required_mounts:
96+ remove_bind_mount(managed_mount)
97+
98+
99+def configure_bind_mounts(conf, hostname):
100+ roles = conf.roles()
101+ paths = []
102+ required_mounts = []
103+ ftp_root = "/srv/ftp.root" # configure_vsftp() uses this hard-coded path, so we will too
104+ if roles.get(hostname):
105+ for role in roles.get(hostname):
106+ try:
107+ mirror = conf.mirror_map(role)
108+ except ValueError:
109+ log("CHARM: Missing details - skipping {}".format(role))
110+ continue
111+
112+ paths.append(mirror['path'])
113+ else:
114+ print("DEBUG: configure_bind_mounts found no defined roles for host {}".format(hostname))
115+
116+ if not paths:
117+ print("DEBUG: configure_bind_mounts found no configured paths for host {}".format(hostname))
118+
119+ for path in paths:
120+ if not path.startswith(ftp_root):
121+ dir_name = os.path.basename(os.path.normpath(path))
122+ target = os.path.join("/srv/ftp.root", dir_name)
123+ print("DEBUG: configure_bind_mounts needs to bind-mount {} to {}".format(path, target))
124+ fstab_line = "{} {} none defaults,bind 0 0".format(path, target)
125+ required_mounts.append(fstab_line)
126+
127+ update_bind_mounts(required_mounts)
128+
129+
130 def configure_scripts(conf, hostname):
131 mkdir(conf.script_dir())
132 for script in scripts_to_copy:
133@@ -978,6 +1085,7 @@ def config_changed():
134 configure_rsync_client(conf, hostname)
135 configure_rsync_server(conf, hostname)
136 configure_apache(conf, hostname)
137+ configure_bind_mounts(conf, hostname)
138 configure_vsftp(conf, hostname)
139 configure_triggers(conf, hostname)
140 configure_nrpe(conf, hostname)
141diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt
142index 449073d..b83c91e 100644
143--- a/tests/unit/requirements.txt
144+++ b/tests/unit/requirements.txt
145@@ -4,4 +4,5 @@ pytest
146 pytest-cov
147 # charm requirements
148 Cheetah
149+freezegun
150 pyyaml
151diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
152index a8a2fb8..9be689a 100644
153--- a/tests/unit/test_charm.py
154+++ b/tests/unit/test_charm.py
155@@ -1,3 +1,4 @@
156+import freezegun
157 import json
158 import os
159 import pytest
160@@ -5,20 +6,23 @@ import unittest
161
162 from base64 import b64encode
163 from copy import deepcopy
164-from mock import patch
165+from mock import mock_open, patch
166
167 import Config # for mockery
168 import hooks # noqa: F401; for mockery
169
170 from hooks import (
171 SSH_TRIGGER_TEMPLATE,
172+ add_bind_mount,
173 collect_mountpoints,
174 configure_triggers,
175 extract_ssh_public_key,
176 generate_disk_check,
177+ get_managed_mounts,
178 make_triggers,
179 merge_shared_key_triggers,
180 mountpoint_of,
181+ remove_bind_mount,
182 )
183
184 from utils import (
185@@ -141,6 +145,68 @@ class TestUbuntuMirrorCharm(unittest.TestCase):
186 """Given the tests for mountpoint_of, we can test collect_mountpoints like this."""
187 self.assertEqual(collect_mountpoints(['/', '/bin', '/proc/self/fd']), {'/', '/proc'})
188
189+ def test_get_managed_mounts_empty(self):
190+ """On a fresh install, there will be none."""
191+ with patch(
192+ "__builtin__.open",
193+ mock_open(
194+ read_data="LABEL=cloudimg-rootfs / ext4 defaults 0 0\n"
195+ )
196+ ) as m:
197+ self.assertEqual(get_managed_mounts(), [])
198+ m.assert_called_once_with("/etc/fstab", "r")
199+
200+ def test_get_managed_mounts_not_empty(self):
201+ """On a unit with a managed mount, we should identify it."""
202+ with patch(
203+ "__builtin__.open",
204+ mock_open(
205+ read_data="LABEL=cloudimg-rootfs / ext4 defaults 0 0\n"
206+ "/srv2/ftp.root/ubuntu-ports /srv/ftp.root/ubuntu-ports none defaults,bind 0 0"
207+ " # Bind mount managed by the ubuntu-mirror charm\n"
208+ )
209+ ) as m:
210+ self.assertEqual(
211+ get_managed_mounts(),
212+ ["/srv2/ftp.root/ubuntu-ports /srv/ftp.root/ubuntu-ports none defaults,bind 0 0", ]
213+ )
214+ m.assert_called_once_with("/etc/fstab", "r")
215+
216+ @patch("hooks.check_call")
217+ @patch("hooks.mkdir")
218+ @patch("os.rename")
219+ @patch("shutil.copyfile")
220+ @freezegun.freeze_time("2020-09-09 12:00:01")
221+ def test_add_bind_mount(self, _copyfile, _rename, _mkdir, _check_call):
222+ with patch("__builtin__.open", mock_open()) as m:
223+ add_bind_mount("/srv2/ftp.root/ubuntu-ports /srv/ftp.root/ubuntu-ports none defaults,bind 0 0")
224+ m.assert_called_with("/etc/fstab_temp_2020-09-09_12:00:01", "a")
225+ _check_call.assert_called_once_with(["mount", "/srv/ftp.root/ubuntu-ports"])
226+ _mkdir.assert_called_once_with("/srv/ftp.root/ubuntu-ports")
227+ _rename.assert_called_once_with("/etc/fstab_temp_2020-09-09_12:00:01", "/etc/fstab")
228+ _copyfile.assert_called_with("/etc/fstab", "/etc/fstab_backup_2020-09-09_12:00:01")
229+
230+ @patch("hooks.check_call")
231+ @patch("os.rename")
232+ @patch("shutil.copyfile")
233+ @freezegun.freeze_time("2020-09-09 12:00:01")
234+ def test_remove_bind_mount(self, _copyfile, _rename, _check_call):
235+ with patch(
236+ "__builtin__.open",
237+ mock_open(
238+ read_data="LABEL=cloudimg-rootfs / ext4 defaults 0 0\n"
239+ "/srv2/ftp.root/ubuntu-ports /srv/ftp.root/ubuntu-ports none defaults,bind 0 0"
240+ " # Bind mount managed by the ubuntu-mirror charm\n"
241+ )
242+ ) as m:
243+ remove_bind_mount("/srv2/ftp.root/ubuntu-ports /srv/ftp.root/ubuntu-ports none defaults,bind 0 0")
244+ m.assert_any_call("/etc/fstab", "r")
245+ m.assert_called_with("/etc/fstab_temp_2020-09-09_12:00:01", "w")
246+ m().write.assert_called_once_with("LABEL=cloudimg-rootfs / ext4 defaults 0 0\n")
247+ _copyfile.assert_called_with("/etc/fstab", "/etc/fstab_backup_2020-09-09_12:00:01")
248+ _check_call.assert_any_call(["umount", "/srv/ftp.root/ubuntu-ports"])
249+ _check_call.assert_any_call(["rmdir", "/srv/ftp.root/ubuntu-ports"])
250+
251 # Cheetah warns about using the Python version of NameMapper, so
252 # we ignore it here. Matching more closely by the message doesn't
253 # seem to work, probably because it begins with a newline and

Subscribers

People subscribed via source and target branches