Merge lp:~mitchburton/landscape-charm/deployment_mode into lp:landscape-charm

Proposed by Mitch Burton
Status: Merged
Approved by: Mitch Burton
Approved revision: 434
Merged at revision: 434
Proposed branch: lp:~mitchburton/landscape-charm/deployment_mode
Merge into: lp:landscape-charm
Diff against target: 323 lines (+183/-8)
6 files modified
README.md (+11/-3)
bundle.yaml (+32/-0)
config.yaml (+13/-0)
src/charm.py (+21/-2)
src/settings_files.py (+26/-0)
tests/test_settings_files.py (+80/-3)
To merge this branch: bzr merge lp:~mitchburton/landscape-charm/deployment_mode
Reviewer Review Type Date Requested Status
Kevin Nasto Approve
Review via email: mp+438647@code.launchpad.net

Commit message

Add deployment_mode and additional_service_config config vars

Description of the change

This includes changes needed to be able to set up a juju deployment in SaaS/production mode. Here's the config I've been using - feel free to change it:

landscape-server:
  openid_provider_url: https://login.ubuntu.com/
  openid_logout_url: https://login.ubuntu.com/+logout
  admin_email: <Ubuntu One email>
  admin_name: <Your name>
  admin_password: thisisatest
  additional_service_config: |
    [stores]
    main = landscape_production_main
    account-1 = landscape_production_account_1
    resource-1 = landscape_production_resource_1
    package = landscape_production_package
    session = landscape_production_session
    session-autocommit = landscape_production_session?isolation=autocommit
    knowledge = landscape_production_knowledge

steps to test:
1. deploy by running the following:

charmcraft pack
juju deploy ./landscape-server_ubuntu-22.04-amd64-arm64_ubuntu-20.04-amd64-arm64.charm --config landscape-server.yaml
juju deploy postgresql --config postgresql.yaml --series bionic
juju deploy haproxy --config haproxy.yaml --series focal
juju deploy rabbitmq-server --series focal
juju relate landscape-server:db postgresql:db-admin
juju relate landscape-server haproxy
juju relate landscape-server rabbitmq-server

2. Wait until everything is in ready status
3. update the landscape-server application config (can be production or staging):

juju config landscape-server deployment_mode=production

4. Wait for config-changed to finish
5. hook up your user to your SSO id:

juju ssh postgresql/0
sudo -u postgres psql landscape_production_main
UPDATE person SET identity='<Ubuntu One ID URL>';

6. If you want, give yourself admin privileges while you're there:

INSERT INTO person_access VALUES (1, 6, NULL);

7. Poke around by going to the HAProxy unit's IP, make sure things in general are working

To post a comment you must log in.
434. By Mitch Burton

Add bundle.yaml for quick testing; update README

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

LGTM+1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README.md'
2--- README.md 2022-12-02 00:45:26 +0000
3+++ README.md 2023-03-10 18:34:17 +0000
4@@ -33,7 +33,7 @@
5 ## Configuration
6
7 Landscape requires configuration of a license file before deployment.
8-Please sign in to your "hosted account" at
9+Please sign in to your "SaaS account" at
10 [https://landscape.canonical.com](https://landscape.canonical.com) to
11 download your license file. It can be found by following the link on
12 the left side of the page: "access the Landscape On Premises archive."
13@@ -44,7 +44,7 @@
14 deployed landscape-server application:
15
16 ```bash
17-juju config landscape-server "license-file=$(cat license-file"
18+juju config landscape-server "license_file=$(cat license-file)"
19 ```
20
21 ### SSL
22@@ -63,4 +63,12 @@
23
24 Please see the [Juju SDK docs](https://juju.is/docs/sdk) for guidelines
25 on enhancements to this charm following best practice guidelines, and
26-`CONTRIBUTING.md` for developer guidance.
27\ No newline at end of file
28+`CONTRIBUTING.md` for developer guidance.
29+
30+When developing the charm, here's a quick way to test out changes as
31+they would be deployed by `landscape-scalable`:
32+
33+```bash
34+charmcraft pack
35+juju deploy ./bundle.yaml
36+```
37\ No newline at end of file
38
39=== added file 'bundle.yaml'
40--- bundle.yaml 1970-01-01 00:00:00 +0000
41+++ bundle.yaml 2023-03-10 18:34:17 +0000
42@@ -0,0 +1,32 @@
43+description: Landscape Scalable
44+applications:
45+ postgresql:
46+ series: focal
47+ charm: ch:postgresql
48+ num_units: 1
49+ options:
50+ extra_packages: python3-apt postgresql-contrib postgresql-.*-debversion postgresql-plpython3-*
51+ max_connections: 500
52+ max_prepared_transactions: 500
53+ rabbitmq-server:
54+ series: focal
55+ charm: ch:rabbitmq-server
56+ num_units: 1
57+ haproxy:
58+ series: focal
59+ charm: ch:haproxy
60+ num_units: 1
61+ expose: true
62+ options:
63+ default_timeouts: "queue 60000, connect 5000, client 120000, server 120000"
64+ services: ""
65+ ssl_cert: SELFSIGNED
66+ global_default_bind_options: "no-tlsv10"
67+ landscape-server:
68+ series: jammy
69+ charm: ./landscape-server_ubuntu-22.04-amd64-arm64_ubuntu-20.04-amd64-arm64.charm
70+ num_units: 1
71+relations:
72+ - [landscape-server, rabbitmq-server]
73+ - [landscape-server, haproxy]
74+ - ["landscape-server:db", "postgresql:db-admin"]
75
76=== modified file 'config.yaml'
77--- config.yaml 2023-01-27 20:37:12 +0000
78+++ config.yaml 2023-03-10 18:34:17 +0000
79@@ -140,3 +140,16 @@
80 description: |
81 Comma-separated list of nagios servicegroups. If empty, the
82 nagios-context will be used as the servicegroup.
83+ deployment_mode:
84+ type: string
85+ default: standalone
86+ description: |
87+ Landscape Server tenancy mode - do not modify unless you are able
88+ to provide the additional configuration required to run
89+ Landscape in SaaS mode.
90+ additional_service_config:
91+ type: string
92+ default:
93+ description: |
94+ Additional service.conf settings to be merged with the default
95+ configuration.
96
97=== modified file 'src/charm.py'
98--- src/charm.py 2023-01-27 20:37:12 +0000
99+++ src/charm.py 2023-03-10 18:34:17 +0000
100@@ -36,8 +36,8 @@
101 ActiveStatus, BlockedStatus, Relation, MaintenanceStatus, WaitingStatus)
102
103 from settings_files import (
104- prepend_default_settings, update_default_settings, update_service_conf,
105- write_license_file, write_ssl_cert)
106+ configure_for_deployment_mode, merge_service_conf, prepend_default_settings,
107+ update_default_settings, update_service_conf, write_license_file, write_ssl_cert)
108
109 logger = logging.getLogger(__name__)
110
111@@ -136,6 +136,7 @@
112 })
113 self._stored.set_default(leader_ip="")
114 self._stored.set_default(running=False)
115+ self._stored.set_default(paused=False)
116 self._stored.set_default(default_root_url="")
117 self._stored.set_default(account_bootstrapped=False)
118
119@@ -193,6 +194,16 @@
120 if isinstance(prev_status, BlockedStatus):
121 self.unit.status = prev_status
122
123+ # Update additional configuration
124+ deployment_mode = self.model.config.get("deployment_mode")
125+ update_service_conf({"global": {"deployment-mode": deployment_mode}})
126+
127+ configure_for_deployment_mode(deployment_mode)
128+
129+ additional_config = self.model.config.get("additional_service_config")
130+ if additional_config:
131+ merge_service_conf(additional_config)
132+
133 self._update_ready_status(restart_services=True)
134
135 def _on_install(self, event: InstallEvent) -> None:
136@@ -264,6 +275,10 @@
137 self.unit.status = ActiveStatus("Unit is ready")
138 return
139
140+ if self._stored.paused:
141+ self.unit.status = MaintenanceStatus("Services stopped")
142+ return
143+
144 self._stored.running = self._start_services()
145
146 def _start_services(self) -> bool:
147@@ -273,6 +288,7 @@
148 """
149 self.unit.status = MaintenanceStatus("Starting services")
150 is_leader = self.unit.is_leader()
151+ deployment_mode = self.model.config.get("deployment_mode")
152
153 update_default_settings({
154 "RUN_ALL": "no",
155@@ -285,6 +301,7 @@
156 "RUN_CRON": "yes" if is_leader else "no",
157 "RUN_PACKAGESEARCH": "yes" if is_leader else "no",
158 "RUN_PACKAGEUPLOADSERVER": "yes" if is_leader else "no",
159+ "RUN_PPPA_PROXY": "yes" if deployment_mode != "standalone" else "no",
160 })
161
162 logger.info("Starting services")
163@@ -800,6 +817,7 @@
164 else:
165 self.unit.status = MaintenanceStatus("Services stopped")
166 self._stored.running = False
167+ self._stored.paused = True
168
169 def _resume(self, event: ActionEvent):
170 self.unit.status = MaintenanceStatus("Starting services")
171@@ -820,6 +838,7 @@
172 event.fail("Failed to start services: %s", start_result.stdout)
173 else:
174 self._stored.running = True
175+ self._stored.paused = False
176 self.unit.status = ActiveStatus("Unit is ready")
177 self._update_ready_status()
178
179
180=== modified file 'src/settings_files.py'
181--- src/settings_files.py 2023-01-03 22:10:55 +0000
182+++ src/settings_files.py 2023-03-10 18:34:17 +0000
183@@ -11,6 +11,8 @@
184 from urllib.request import urlopen
185 from urllib.error import URLError
186
187+CONFIGS_DIR = "/opt/canonical/landscape/configs"
188+
189 DEFAULT_SETTINGS = "/etc/default/landscape-server"
190
191 LICENSE_FILE = "/etc/landscape/license.txt"
192@@ -32,6 +34,30 @@
193 pass
194
195
196+def configure_for_deployment_mode(mode: str) -> None:
197+ """
198+ Places files where Landscape expects to find them for different deployment
199+ modes.
200+ """
201+ if mode == "standalone":
202+ return
203+
204+ os.symlink(os.path.join(CONFIGS_DIR, "standalone"), os.path.join(CONFIGS_DIR, mode))
205+
206+
207+def merge_service_conf(other: str) -> None:
208+ """
209+ Merges `other` into the Landscape Server configuration file,
210+ overwriting existing config.
211+ """
212+ config = ConfigParser()
213+ config.read(SERVICE_CONF)
214+ config.read_string(other)
215+
216+ with open(SERVICE_CONF, "w") as config_fp:
217+ config.write(config_fp)
218+
219+
220 def prepend_default_settings(updates: dict) -> None:
221 """
222 Adds `updates` to the start of the Landscape Server default
223
224=== modified file 'tests/test_settings_files.py'
225--- tests/test_settings_files.py 2023-01-03 22:10:55 +0000
226+++ tests/test_settings_files.py 2023-03-10 18:34:17 +0000
227@@ -9,9 +9,9 @@
228 from urllib.error import URLError
229
230 from settings_files import (
231- LICENSE_FILE, LicenseFileReadException, SSLCertReadException,
232- prepend_default_settings, update_default_settings, update_service_conf,
233- write_license_file, write_ssl_cert)
234+ CONFIGS_DIR, LICENSE_FILE, LicenseFileReadException, SSLCertReadException,
235+ configure_for_deployment_mode, merge_service_conf, prepend_default_settings,
236+ update_default_settings, update_service_conf, write_license_file, write_ssl_cert)
237
238
239 class CapturingBytesIO(BytesIO):
240@@ -44,6 +44,83 @@
241 return super().close(*args, **kwargs)
242
243
244+class ConfigureForDeploymentModeTestCase(TestCase):
245+
246+ @patch("os.symlink")
247+ def test_configure_for_deployment_mode_standalone(self, symlink_mock):
248+ """
249+ The default mode, "standalone", should result in no changes.
250+ """
251+ configure_for_deployment_mode("standalone")
252+
253+ symlink_mock.assert_not_called()
254+
255+ @patch("os.symlink")
256+ def test_configure_for_deployment_mode_other(self, symlink_mock):
257+ """
258+ Modes other than the magic "standalone" should symlink to a similarly named directory.
259+ """
260+ configure_for_deployment_mode("elite_dangerous")
261+
262+ symlink_mock.assert_called_once_with(
263+ os.path.join(CONFIGS_DIR, "standalone"),
264+ os.path.join(CONFIGS_DIR, "elite_dangerous")
265+ )
266+
267+
268+class MergeServiceConfTestCase(TestCase):
269+
270+ def test_merge_service_conf_new(self):
271+ """
272+ Tests that a new section and key are created in the existing
273+ config file.
274+ """
275+ old_conf = "[global]\nfoo = bar\nbat = baz\n"
276+ infile = StringIO(old_conf)
277+ outfile = CapturingStringIO()
278+ new_conf = "[new]\ncat = meow\n"
279+
280+ i = 0
281+
282+ def return_conf(path, *args, **kwargs):
283+ nonlocal i
284+ retval = (infile, outfile)[i]
285+ i += 1
286+ return retval
287+
288+ with patch("builtins.open") as open_mock:
289+ open_mock.side_effect = return_conf
290+ merge_service_conf(new_conf)
291+
292+ self.assertIn(old_conf, outfile.captured)
293+ self.assertIn(new_conf, outfile.captured)
294+
295+ def test_merge_service_conf_override(self):
296+ """
297+ Tests that a provided config overrides values in the old config.
298+ """
299+ old_conf = "[global]\nleft = true\ntouched = false\n"
300+ infile = StringIO(old_conf)
301+ outfile = CapturingStringIO()
302+ new_conf = "[global]\ntouched = true\n"
303+
304+ i = 0
305+
306+ def return_conf(path, *args, **kwargs):
307+ nonlocal i
308+ retval = (infile, outfile)[i]
309+ i += 1
310+ return retval
311+
312+ with patch("builtins.open") as open_mock:
313+ open_mock.side_effect = return_conf
314+ merge_service_conf(new_conf)
315+
316+ self.assertIn("left = true", outfile.captured)
317+ self.assertIn("touched = true", outfile.captured)
318+ self.assertNotIn("touched = false", outfile.captured)
319+
320+
321 class PrependDefaultSettingsTestCase(TestCase):
322
323 def test_prepend(self):

Subscribers

People subscribed via source and target branches

to all changes: