Merge ~silverdrake11/landscape-charm:install-and-bootstrap-bug_lndeng-466 into landscape-charm:main

Proposed by Kevin Nasto
Status: Merged
Merged at revision: c2fecfd36a8f683e3a12b9327c56b012f883f9ca
Proposed branch: ~silverdrake11/landscape-charm:install-and-bootstrap-bug_lndeng-466
Merge into: landscape-charm:main
Diff against target: 185 lines (+38/-40)
4 files modified
src/charm.py (+8/-19)
src/settings_files.py (+9/-0)
tests/test_charm.py (+16/-18)
tests/test_settings_files.py (+5/-3)
Reviewer Review Type Date Requested Status
Mitch Burton Approve
Review via email: mp+441128@code.launchpad.net

Commit message

Fixed install/retry/config bug. Fixed bootstrap account on start. Fixed broken tests

Description of the change

Manual testing instructions:

Install race:

Use the following code to trigger a failure in the install and make sure it first errors then resolves itself

import random
num = random.randint(1,3)
if num == 3:
    logger.error("SUCCESS!!!!!")
    check_call(["add-apt-repository", "-y", landscape_ppa])
else:
    logger.error("SOMETHING WENT WRONG {}".format(num))

Bootstrap bug:
Make sure that filling out these fields before deploying in the bundle, creates the admin

admin_email:
admin_name:
admin_password:

To post a comment you must log in.
Revision history for this message
Kevin Nasto (silverdrake11) wrote (last edit ):

I learned that Juju's exponential retry behavior can be triggered by raising an exception. Some things are useful for this such as this install bug/ race condition. The install initially fails but will resolve itself when it's ready. Other things are not useful for this, like erroring out on a mistake in the config (retrying doesn't change the config value). So because of this, raising an exception can be useful for other charm functions in the future or when needed.

Revision history for this message
Kevin Nasto (silverdrake11) wrote (last edit ):

See diff comment

Revision history for this message
Mitch Burton (mitchburton) wrote :

+1 LGTM. Test based on RNG is not the world's best testing experience but it only took me three tries.

review: Approve
Revision history for this message
Kevin Nasto (silverdrake11) wrote :

> +1 LGTM. Test based on RNG is not the world's best testing experience but it
> only took me three tries.

Third time's the charm

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/charm.py b/src/charm.py
2index d883217..313208b 100755
3--- a/src/charm.py
4+++ b/src/charm.py
5@@ -204,8 +204,6 @@ class LandscapeServerCharm(CharmBase):
6 "package-upload": {"root-url": root_url},
7 })
8
9- self._bootstrap_account()
10-
11 config_host = self.model.config.get("db_host")
12 schema_password = self.model.config.get("db_schema_password")
13 landscape_password = self.model.config.get("db_landscape_password")
14@@ -230,6 +228,8 @@ class LandscapeServerCharm(CharmBase):
15 else:
16 return
17
18+ self._bootstrap_account()
19+
20 if isinstance(prev_status, BlockedStatus):
21 self.unit.status = prev_status
22
23@@ -245,22 +245,9 @@ class LandscapeServerCharm(CharmBase):
24 # Add the Landscape Server PPA and install via apt.
25 check_call(["add-apt-repository", "-y", landscape_ppa])
26 apt.add_package(["landscape-server", "landscape-hashids"])
27- except PackageNotFoundError:
28- logger.error("Landscape package not found in package cache "
29- "or on system")
30- self.unit.status = BlockedStatus("Failed to install packages")
31- return
32- except PackageError as e:
33- logger.error(
34- "Could not install landscape-server package. Reason: %s",
35- e.message)
36- self.unit.status = BlockedStatus("Failed to install packages")
37- return
38- except CalledProcessError as e:
39- logger.error("Package install failed with return code %d",
40- e.returncode)
41- self.unit.status = BlockedStatus("Failed to install packages")
42- return
43+ except (PackageNotFoundError, PackageError, CalledProcessError) as exc:
44+ logger.error("Failed to install packages")
45+ raise exc # This will trigger juju's exponential retry
46
47 # Write the config-provided SSL certificate, if it exists.
48 config_ssl_cert = self.model.config["ssl_cert"]
49@@ -417,10 +404,12 @@ class LandscapeServerCharm(CharmBase):
50 def _migrate_schema_bootstrap(self):
51 """
52 Migrates schema along with the bootstrap command which ensures that the
53- databases along with the landscape user exists. Returns True on success
54+ databases along with the landscape user exists. In addition creates
55+ admin if configured. Returns True on success
56 """
57 try:
58 check_call([SCHEMA_SCRIPT, "--bootstrap"])
59+ self._bootstrap_account()
60 return True
61 except CalledProcessError as e:
62 logger.error(
63diff --git a/src/settings_files.py b/src/settings_files.py
64index 6eb7231..ba8211d 100644
65--- a/src/settings_files.py
66+++ b/src/settings_files.py
67@@ -37,6 +37,10 @@ class SSLCertReadException(Exception):
68 pass
69
70
71+class ServiceConfMissing(Exception):
72+ pass
73+
74+
75 def configure_for_deployment_mode(mode: str) -> None:
76 """
77 Places files where Landscape expects to find them for different deployment
78@@ -111,6 +115,11 @@ def update_service_conf(updates: dict) -> None:
79 `updates` is a mapping of {section => {key => value}}, to be applied
80 to the config file.
81 """
82+ if not os.path.isfile(SERVICE_CONF):
83+ # Landscape server will not overwrite this file on install, so we
84+ # cannot get the default values if we create it here
85+ raise ServiceConfMissing("Landscape server install failed!")
86+
87 config = ConfigParser()
88 config.read(SERVICE_CONF)
89
90diff --git a/tests/test_charm.py b/tests/test_charm.py
91index 08a4119..55a139b 100644
92--- a/tests/test_charm.py
93+++ b/tests/test_charm.py
94@@ -95,11 +95,8 @@ class TestCharm(unittest.TestCase):
95
96 with patches as mocks:
97 mocks["apt"].add_package.side_effect = PackageNotFoundError
98- harness.begin_with_initial_hooks()
99-
100- status = harness.charm.unit.status
101- self.assertIsInstance(status, BlockedStatus)
102- self.assertEqual(status.message, "Failed to install packages")
103+ self.assertRaises(PackageNotFoundError,
104+ harness.begin_with_initial_hooks)
105
106 def test_install_package_error(self):
107 harness = Harness(LandscapeServerCharm)
108@@ -116,11 +113,7 @@ class TestCharm(unittest.TestCase):
109
110 with patches as mocks:
111 mocks["apt"].add_package.side_effect = PackageError("ouch")
112- harness.begin_with_initial_hooks()
113-
114- status = harness.charm.unit.status
115- self.assertIsInstance(status, BlockedStatus)
116- self.assertEqual(status.message, "Failed to install packages")
117+ self.assertRaises(PackageError, harness.begin_with_initial_hooks)
118
119 def test_install_called_process_error(self):
120 harness = Harness(LandscapeServerCharm)
121@@ -131,11 +124,8 @@ class TestCharm(unittest.TestCase):
122 with patch("charm.check_call") as mock:
123 with patch("charm.update_service_conf"):
124 mock.side_effect = CalledProcessError(127, Mock())
125- harness.begin_with_initial_hooks()
126-
127- status = harness.charm.unit.status
128- self.assertIsInstance(status, BlockedStatus)
129- self.assertEqual(status.message, "Failed to install packages")
130+ self.assertRaises(CalledProcessError,
131+ harness.begin_with_initial_hooks)
132
133 def test_install_ssl_cert(self):
134 harness = Harness(LandscapeServerCharm)
135@@ -188,20 +178,28 @@ class TestCharm(unittest.TestCase):
136
137 def test_install_license_file_b64(self):
138 harness = Harness(LandscapeServerCharm)
139- harness.update_config({"license_file": "VEhJUyBJUyBBIExJQ0VOU0U="})
140+ license_text = "VEhJUyBJUyBBIExJQ0VOU0U"
141+ harness.update_config({"license_file": license_text})
142 relation_id = harness.add_relation("replicas", "landscape-server")
143 harness.update_relation_data(
144 relation_id, "landscape-server", {"leader-ip": "test"})
145
146 with patch.multiple(
147 "charm",
148+ apt=DEFAULT,
149+ check_call=DEFAULT,
150 update_service_conf=DEFAULT,
151+ prepend_default_settings=DEFAULT,
152 write_license_file=DEFAULT,
153 ) as mocks:
154 harness.begin_with_initial_hooks()
155
156- mocks["write_license_file"].assert_called_once_with(
157- "VEhJUyBJUyBBIExJQ0VOU0U=", 1000, 1000)
158+ mock_write = mocks["write_license_file"]
159+ self.assertEqual(len(mock_write.mock_calls), 2)
160+ self.assertEqual(mock_write.mock_calls[0].args,
161+ (license_text, 1000, 1000))
162+ self.assertEqual(mock_write.mock_calls[1].args,
163+ (license_text, 1000, 1000))
164
165 def test_update_ready_status_not_running(self):
166 self.harness.charm.unit.status = WaitingStatus()
167diff --git a/tests/test_settings_files.py b/tests/test_settings_files.py
168index e16f8f7..2b1321c 100644
169--- a/tests/test_settings_files.py
170+++ b/tests/test_settings_files.py
171@@ -210,9 +210,11 @@ class UpdateServiceConfTestCase(TestCase):
172 i += 1
173 return retval
174
175- with patch("builtins.open") as open_mock:
176- open_mock.side_effect = return_conf
177- update_service_conf({"test": {"new": "yes"}})
178+ with patch("os.path.isfile") as mock_isfile:
179+ with patch("builtins.open") as open_mock:
180+ mock_isfile.return_value = True
181+ open_mock.side_effect = return_conf
182+ update_service_conf({"test": {"new": "yes"}})
183
184 self.assertEqual(outfile.captured,
185 "[fixed]\nold = no\n\n[test]\nnew = yes\n\n")

Subscribers

People subscribed via source and target branches

to all changes: