Merge lp:~silverdrake11/landscape-charm/bootstrap_account_charm_lndeng-150 into lp:~mitchburton/landscape-charm/op-framework

Proposed by Kevin Nasto
Status: Merged
Merged at revision: 432
Proposed branch: lp:~silverdrake11/landscape-charm/bootstrap_account_charm_lndeng-150
Merge into: lp:~mitchburton/landscape-charm/op-framework
Diff against target: 288 lines (+230/-1)
3 files modified
config.yaml (+26/-1)
src/charm.py (+61/-0)
tests/test_charm.py (+143/-0)
To merge this branch: bzr merge lp:~silverdrake11/landscape-charm/bootstrap_account_charm_lndeng-150
Reviewer Review Type Date Requested Status
Mitch Burton Approve
Review via email: mp+436475@code.launchpad.net

Commit message

Added bootstrap account feature to server charm

To post a comment you must log in.
Revision history for this message
Mitch Burton (mitchburton) wrote :

Works great, good job!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'config.yaml'
2--- config.yaml 2022-10-18 18:27:37 +0000
3+++ config.yaml 2023-01-27 22:11:21 +0000
4@@ -60,7 +60,32 @@
5 type: string
6 default:
7 description: |
8- The email address that Landscape emails will appear to come from.
9+ The initial email address that Landscape emails will appear to come from.
10+ Note this value cannot be set more than once.
11+ admin_email:
12+ type: string
13+ default:
14+ description: |
15+ The email address of the initial account administrator. Note this value
16+ cannot be set more than once.
17+ admin_name:
18+ type: string
19+ default:
20+ description: |
21+ The full name of the initial account administrator. Note this value
22+ cannot be set more than once.
23+ admin_password:
24+ type: string
25+ default:
26+ description: |
27+ The initial password of the account administrator. Note this value cannot
28+ be set more than once.
29+ registration_key:
30+ type: string
31+ default:
32+ description: |
33+ The initial account registration key. Note this value cannot be set more
34+ than once.
35 smtp_relay_host:
36 type: string
37 default: ""
38
39=== modified file 'src/charm.py'
40--- src/charm.py 2023-01-25 19:57:43 +0000
41+++ src/charm.py 2023-01-27 22:11:21 +0000
42@@ -49,6 +49,7 @@
43 NRPE_D_DIR = "/etc/nagios/nrpe.d"
44 POSTFIX_CF = "/etc/postfix/main.cf"
45 SCHEMA_SCRIPT = "/usr/bin/landscape-schema"
46+BOOTSTRAP_ACCOUNT_SCRIPT = "/opt/canonical/landscape/bootstrap-account"
47
48 LANDSCAPE_PACKAGES = (
49 "landscape-server",
50@@ -135,6 +136,8 @@
51 })
52 self._stored.set_default(leader_ip="")
53 self._stored.set_default(running=False)
54+ self._stored.set_default(default_root_url="")
55+ self._stored.set_default(account_bootstrapped=False)
56
57 self.landscape_uid = user_exists("landscape").pw_uid
58 self.root_gid = group_exists("root").gr_gid
59@@ -185,6 +188,8 @@
60 "package-upload": {"root-url": root_url},
61 })
62
63+ self._bootstrap_account()
64+
65 if isinstance(prev_status, BlockedStatus):
66 self.unit.status = prev_status
67
68@@ -387,6 +392,7 @@
69 # Update root_url, if not provided.
70 if not self.model.config.get("root_url"):
71 url = f'https://{event.relation.data[event.unit]["public-address"]}/'
72+ self._stored.default_root_url = url
73 update_service_conf({
74 "global": {"root-url": url},
75 "api": {"root-url": url},
76@@ -725,6 +731,61 @@
77 "OpenID configuration requires both 'openid_provider_url' and "
78 "'openid_logout_url'")
79
80+ def _bootstrap_account(self):
81+ """If admin account details are provided, create admin"""
82+ if not self.unit.is_leader():
83+ return
84+ if self._stored.account_bootstrapped: # Admin already created
85+ return
86+ karg = {} # Keyword args for command line
87+ karg["admin_email"] = self.model.config.get("admin_email")
88+ karg["admin_name"] = self.model.config.get("admin_name")
89+ karg["admin_password"] = self.model.config.get("admin_password")
90+ required_args = karg.values()
91+ if not any(required_args): # Return since no args are specified
92+ return
93+ if not all(required_args): # Some required args are missing
94+ logger.error(
95+ "Admin email, name, and password required for bootstrap account"
96+ )
97+ return
98+ karg["root_url"] = self.model.config.get("root_url")
99+ if not karg["root_url"]:
100+ default_root_url = self._stored.default_root_url
101+ if default_root_url:
102+ karg["root_url"] = default_root_url
103+ else:
104+ logger.error("Bootstrap account waiting on default root url..")
105+ return
106+ karg["registration_key"] = self.model.config.get("registration_key")
107+ karg["system_email"] = self.model.config.get("system_email")
108+
109+ # Collect command line arguments
110+ args = [BOOTSTRAP_ACCOUNT_SCRIPT]
111+ for key, value in karg.items():
112+ if not value:
113+ continue
114+ args.append("--" + key)
115+ args.append(value)
116+
117+ try:
118+ logger.info(args)
119+ result = subprocess.run(args, capture_output=True, text=True)
120+ except FileNotFoundError:
121+ logger.error("Bootstrap script not found!")
122+ logger.error(BOOTSTRAP_ACCOUNT_SCRIPT)
123+ return
124+ logger.info(result.stdout)
125+ if result.returncode:
126+ if "DuplicateAccountError" in result.stderr:
127+ logger.error("Cannot bootstrap b/c account is already there!")
128+ self._stored.account_bootstrapped = True
129+ else:
130+ logger.error(result.stderr)
131+ else:
132+ logger.info("Admin account successfully bootstrapped!")
133+ self._stored.account_bootstrapped = True
134+
135 def _pause(self, event: ActionEvent) -> None:
136 self.unit.status = MaintenanceStatus("Stopping services")
137 event.log("Stopping services")
138
139=== modified file 'tests/test_charm.py'
140--- tests/test_charm.py 2023-01-25 19:57:43 +0000
141+++ tests/test_charm.py 2023-01-27 22:11:21 +0000
142@@ -941,3 +941,146 @@
143 "host": "test",
144 },
145 })
146+
147+
148+class TestBootstrapAccount(unittest.TestCase):
149+ def setUp(self):
150+ self.harness = Harness(LandscapeServerCharm)
151+ self.addCleanup(self.harness.cleanup)
152+
153+ self.harness.model.get_binding = Mock(
154+ return_value=Mock(bind_address="123.123.123.123")
155+ )
156+ self.harness.add_relation("replicas", "landscape-server")
157+ self.harness.set_leader()
158+
159+ self.process_mock = patch("subprocess.run").start()
160+ self.log_mock = patch("charm.logger.error").start()
161+
162+ self.addCleanup(patch.stopall)
163+
164+ self.harness.begin()
165+
166+ def test_bootstrap_account_doesnt_run_with_missing_configs(self):
167+ self.harness.update_config(
168+ {"admin_email": "hello@ubuntu.com", "admin_name": "Hello Ubuntu"}
169+ )
170+ self.assertIn("password required", self.log_mock.call_args.args[0])
171+ self.process_mock.assert_not_called()
172+
173+ def test_bootstrap_account_doesnt_run_with_missing_rooturl(self):
174+ self.harness.update_config(
175+ {
176+ "admin_email": "hello@ubuntu.com",
177+ "admin_name": "Hello Ubuntu",
178+ "admin_password": "password",
179+ }
180+ )
181+ self.assertIn("root url", self.log_mock.call_args.args[0])
182+ self.process_mock.assert_not_called()
183+
184+ def test_bootstrap_account_default_root_url_is_used(self):
185+ self.harness.charm._stored.default_root_url = "https://hello.lxd"
186+ self.harness.update_config(
187+ {
188+ "admin_email": "hello@ubuntu.com",
189+ "admin_name": "Hello Ubuntu",
190+ "admin_password": "password",
191+ }
192+ )
193+ self.assertIn(
194+ self.harness.charm._stored.default_root_url,
195+ self.process_mock.call_args.args[0],
196+ )
197+
198+ def test_bootstrap_account_config_url_over_default(self):
199+ """If config root url and default root url exists, use config url"""
200+ self.harness.charm._stored.default_root_url = "https://hello.lxd"
201+ config_root_url = "https://www.landscape.com"
202+ self.harness.update_config(
203+ {
204+ "admin_email": "hello@ubuntu.com",
205+ "admin_name": "Hello Ubuntu",
206+ "admin_password": "password",
207+ "root_url": config_root_url,
208+ }
209+ )
210+ self.assertIn(config_root_url, self.process_mock.call_args.args[0])
211+
212+ def test_bootstrap_account_runs_once_with_correct_args(self):
213+ """
214+ Test that bootstrap account runs with correct args and that it can't
215+ run again after a successful run
216+ """
217+ self.process_mock.return_value.returncode = 0 # Success
218+ admin_email = "hello@ubuntu.com"
219+ admin_name = "Hello Ubuntu"
220+ admin_password = "password"
221+ root_url = "https://www.landscape.com"
222+ config = {
223+ "admin_email": admin_email,
224+ "admin_name": admin_name,
225+ "admin_password": admin_password,
226+ "root_url": root_url,
227+ }
228+ self.harness.update_config(config)
229+ self.assertEqual(
230+ [
231+ "/opt/canonical/landscape/bootstrap-account",
232+ "--admin_email",
233+ admin_email,
234+ "--admin_name",
235+ admin_name,
236+ "--admin_password",
237+ admin_password,
238+ "--root_url",
239+ root_url,
240+ ],
241+ self.process_mock.call_args.args[0],
242+ )
243+ self.harness.update_config(config)
244+ self.process_mock.assert_called_once()
245+
246+ def test_bootstrap_account_runs_twice_if_error(self):
247+ """
248+ If there's an error ensure that bootstrap account runs again and not
249+ a third time if successful
250+ """
251+ self.process_mock.return_value.returncode = 1 # Error here
252+ admin_email = "hello@ubuntu.com"
253+ admin_name = "Hello Ubuntu"
254+ admin_password = "password"
255+ root_url = "https://www.landscape.com"
256+ config = {
257+ "admin_email": admin_email,
258+ "admin_name": admin_name,
259+ "admin_password": admin_password,
260+ "root_url": root_url,
261+ }
262+ self.harness.update_config(config)
263+ self.process_mock.return_value.returncode = 0
264+ self.harness.update_config(config)
265+ self.harness.update_config(config) # Third time
266+ self.assertEqual(self.process_mock.call_count, 2)
267+
268+ def test_bootstrap_account_cannot_run_if_already_bootstrapped(self):
269+ """
270+ If user already has created an account outside of the charm,
271+ then the bootstrap account cannot run again
272+ """
273+ self.process_mock.return_value.returncode = 1 # Error here
274+ self.process_mock.return_value.stderr = "DuplicateAccountError"
275+ admin_email = "hello@ubuntu.com"
276+ admin_name = "Hello Ubuntu"
277+ admin_password = "password"
278+ root_url = "https://www.landscape.com"
279+ config = {
280+ "admin_email": admin_email,
281+ "admin_name": admin_name,
282+ "admin_password": admin_password,
283+ "root_url": root_url,
284+ }
285+ self.harness.update_config(config)
286+ self.harness.update_config(config)
287+ self.harness.update_config(config) # Third time
288+ self.process_mock.assert_called_once()

Subscribers

People subscribed via source and target branches

to all changes: