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
diff --git a/src/charm.py b/src/charm.py
index d883217..313208b 100755
--- a/src/charm.py
+++ b/src/charm.py
@@ -204,8 +204,6 @@ class LandscapeServerCharm(CharmBase):
204 "package-upload": {"root-url": root_url},204 "package-upload": {"root-url": root_url},
205 })205 })
206206
207 self._bootstrap_account()
208
209 config_host = self.model.config.get("db_host")207 config_host = self.model.config.get("db_host")
210 schema_password = self.model.config.get("db_schema_password")208 schema_password = self.model.config.get("db_schema_password")
211 landscape_password = self.model.config.get("db_landscape_password")209 landscape_password = self.model.config.get("db_landscape_password")
@@ -230,6 +228,8 @@ class LandscapeServerCharm(CharmBase):
230 else:228 else:
231 return229 return
232230
231 self._bootstrap_account()
232
233 if isinstance(prev_status, BlockedStatus):233 if isinstance(prev_status, BlockedStatus):
234 self.unit.status = prev_status234 self.unit.status = prev_status
235235
@@ -245,22 +245,9 @@ class LandscapeServerCharm(CharmBase):
245 # Add the Landscape Server PPA and install via apt.245 # Add the Landscape Server PPA and install via apt.
246 check_call(["add-apt-repository", "-y", landscape_ppa])246 check_call(["add-apt-repository", "-y", landscape_ppa])
247 apt.add_package(["landscape-server", "landscape-hashids"])247 apt.add_package(["landscape-server", "landscape-hashids"])
248 except PackageNotFoundError:248 except (PackageNotFoundError, PackageError, CalledProcessError) as exc:
249 logger.error("Landscape package not found in package cache "249 logger.error("Failed to install packages")
250 "or on system")250 raise exc # This will trigger juju's exponential retry
251 self.unit.status = BlockedStatus("Failed to install packages")
252 return
253 except PackageError as e:
254 logger.error(
255 "Could not install landscape-server package. Reason: %s",
256 e.message)
257 self.unit.status = BlockedStatus("Failed to install packages")
258 return
259 except CalledProcessError as e:
260 logger.error("Package install failed with return code %d",
261 e.returncode)
262 self.unit.status = BlockedStatus("Failed to install packages")
263 return
264251
265 # Write the config-provided SSL certificate, if it exists.252 # Write the config-provided SSL certificate, if it exists.
266 config_ssl_cert = self.model.config["ssl_cert"]253 config_ssl_cert = self.model.config["ssl_cert"]
@@ -417,10 +404,12 @@ class LandscapeServerCharm(CharmBase):
417 def _migrate_schema_bootstrap(self):404 def _migrate_schema_bootstrap(self):
418 """405 """
419 Migrates schema along with the bootstrap command which ensures that the406 Migrates schema along with the bootstrap command which ensures that the
420 databases along with the landscape user exists. Returns True on success407 databases along with the landscape user exists. In addition creates
408 admin if configured. Returns True on success
421 """409 """
422 try:410 try:
423 check_call([SCHEMA_SCRIPT, "--bootstrap"])411 check_call([SCHEMA_SCRIPT, "--bootstrap"])
412 self._bootstrap_account()
424 return True413 return True
425 except CalledProcessError as e:414 except CalledProcessError as e:
426 logger.error(415 logger.error(
diff --git a/src/settings_files.py b/src/settings_files.py
index 6eb7231..ba8211d 100644
--- a/src/settings_files.py
+++ b/src/settings_files.py
@@ -37,6 +37,10 @@ class SSLCertReadException(Exception):
37 pass37 pass
3838
3939
40class ServiceConfMissing(Exception):
41 pass
42
43
40def configure_for_deployment_mode(mode: str) -> None:44def configure_for_deployment_mode(mode: str) -> None:
41 """45 """
42 Places files where Landscape expects to find them for different deployment46 Places files where Landscape expects to find them for different deployment
@@ -111,6 +115,11 @@ def update_service_conf(updates: dict) -> None:
111 `updates` is a mapping of {section => {key => value}}, to be applied115 `updates` is a mapping of {section => {key => value}}, to be applied
112 to the config file.116 to the config file.
113 """117 """
118 if not os.path.isfile(SERVICE_CONF):
119 # Landscape server will not overwrite this file on install, so we
120 # cannot get the default values if we create it here
121 raise ServiceConfMissing("Landscape server install failed!")
122
114 config = ConfigParser()123 config = ConfigParser()
115 config.read(SERVICE_CONF)124 config.read(SERVICE_CONF)
116125
diff --git a/tests/test_charm.py b/tests/test_charm.py
index 08a4119..55a139b 100644
--- a/tests/test_charm.py
+++ b/tests/test_charm.py
@@ -95,11 +95,8 @@ class TestCharm(unittest.TestCase):
9595
96 with patches as mocks:96 with patches as mocks:
97 mocks["apt"].add_package.side_effect = PackageNotFoundError97 mocks["apt"].add_package.side_effect = PackageNotFoundError
98 harness.begin_with_initial_hooks()98 self.assertRaises(PackageNotFoundError,
9999 harness.begin_with_initial_hooks)
100 status = harness.charm.unit.status
101 self.assertIsInstance(status, BlockedStatus)
102 self.assertEqual(status.message, "Failed to install packages")
103100
104 def test_install_package_error(self):101 def test_install_package_error(self):
105 harness = Harness(LandscapeServerCharm)102 harness = Harness(LandscapeServerCharm)
@@ -116,11 +113,7 @@ class TestCharm(unittest.TestCase):
116113
117 with patches as mocks:114 with patches as mocks:
118 mocks["apt"].add_package.side_effect = PackageError("ouch")115 mocks["apt"].add_package.side_effect = PackageError("ouch")
119 harness.begin_with_initial_hooks()116 self.assertRaises(PackageError, harness.begin_with_initial_hooks)
120
121 status = harness.charm.unit.status
122 self.assertIsInstance(status, BlockedStatus)
123 self.assertEqual(status.message, "Failed to install packages")
124117
125 def test_install_called_process_error(self):118 def test_install_called_process_error(self):
126 harness = Harness(LandscapeServerCharm)119 harness = Harness(LandscapeServerCharm)
@@ -131,11 +124,8 @@ class TestCharm(unittest.TestCase):
131 with patch("charm.check_call") as mock:124 with patch("charm.check_call") as mock:
132 with patch("charm.update_service_conf"):125 with patch("charm.update_service_conf"):
133 mock.side_effect = CalledProcessError(127, Mock())126 mock.side_effect = CalledProcessError(127, Mock())
134 harness.begin_with_initial_hooks()127 self.assertRaises(CalledProcessError,
135128 harness.begin_with_initial_hooks)
136 status = harness.charm.unit.status
137 self.assertIsInstance(status, BlockedStatus)
138 self.assertEqual(status.message, "Failed to install packages")
139129
140 def test_install_ssl_cert(self):130 def test_install_ssl_cert(self):
141 harness = Harness(LandscapeServerCharm)131 harness = Harness(LandscapeServerCharm)
@@ -188,20 +178,28 @@ class TestCharm(unittest.TestCase):
188178
189 def test_install_license_file_b64(self):179 def test_install_license_file_b64(self):
190 harness = Harness(LandscapeServerCharm)180 harness = Harness(LandscapeServerCharm)
191 harness.update_config({"license_file": "VEhJUyBJUyBBIExJQ0VOU0U="})181 license_text = "VEhJUyBJUyBBIExJQ0VOU0U"
182 harness.update_config({"license_file": license_text})
192 relation_id = harness.add_relation("replicas", "landscape-server")183 relation_id = harness.add_relation("replicas", "landscape-server")
193 harness.update_relation_data(184 harness.update_relation_data(
194 relation_id, "landscape-server", {"leader-ip": "test"})185 relation_id, "landscape-server", {"leader-ip": "test"})
195186
196 with patch.multiple(187 with patch.multiple(
197 "charm",188 "charm",
189 apt=DEFAULT,
190 check_call=DEFAULT,
198 update_service_conf=DEFAULT,191 update_service_conf=DEFAULT,
192 prepend_default_settings=DEFAULT,
199 write_license_file=DEFAULT,193 write_license_file=DEFAULT,
200 ) as mocks:194 ) as mocks:
201 harness.begin_with_initial_hooks()195 harness.begin_with_initial_hooks()
202196
203 mocks["write_license_file"].assert_called_once_with(197 mock_write = mocks["write_license_file"]
204 "VEhJUyBJUyBBIExJQ0VOU0U=", 1000, 1000)198 self.assertEqual(len(mock_write.mock_calls), 2)
199 self.assertEqual(mock_write.mock_calls[0].args,
200 (license_text, 1000, 1000))
201 self.assertEqual(mock_write.mock_calls[1].args,
202 (license_text, 1000, 1000))
205203
206 def test_update_ready_status_not_running(self):204 def test_update_ready_status_not_running(self):
207 self.harness.charm.unit.status = WaitingStatus()205 self.harness.charm.unit.status = WaitingStatus()
diff --git a/tests/test_settings_files.py b/tests/test_settings_files.py
index e16f8f7..2b1321c 100644
--- a/tests/test_settings_files.py
+++ b/tests/test_settings_files.py
@@ -210,9 +210,11 @@ class UpdateServiceConfTestCase(TestCase):
210 i += 1210 i += 1
211 return retval211 return retval
212212
213 with patch("builtins.open") as open_mock:213 with patch("os.path.isfile") as mock_isfile:
214 open_mock.side_effect = return_conf214 with patch("builtins.open") as open_mock:
215 update_service_conf({"test": {"new": "yes"}})215 mock_isfile.return_value = True
216 open_mock.side_effect = return_conf
217 update_service_conf({"test": {"new": "yes"}})
216218
217 self.assertEqual(outfile.captured,219 self.assertEqual(outfile.captured,
218 "[fixed]\nold = no\n\n[test]\nnew = yes\n\n")220 "[fixed]\nold = no\n\n[test]\nnew = yes\n\n")

Subscribers

People subscribed via source and target branches

to all changes: