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
=== modified file 'README.md'
--- README.md 2022-12-02 00:45:26 +0000
+++ README.md 2023-03-10 18:34:17 +0000
@@ -33,7 +33,7 @@
33## Configuration33## Configuration
3434
35Landscape requires configuration of a license file before deployment.35Landscape requires configuration of a license file before deployment.
36Please sign in to your "hosted account" at36Please sign in to your "SaaS account" at
37[https://landscape.canonical.com](https://landscape.canonical.com) to37[https://landscape.canonical.com](https://landscape.canonical.com) to
38download your license file. It can be found by following the link on38download your license file. It can be found by following the link on
39the left side of the page: "access the Landscape On Premises archive."39the left side of the page: "access the Landscape On Premises archive."
@@ -44,7 +44,7 @@
44deployed landscape-server application:44deployed landscape-server application:
4545
46```bash46```bash
47juju config landscape-server "license-file=$(cat license-file"47juju config landscape-server "license_file=$(cat license-file)"
48```48```
4949
50### SSL50### SSL
@@ -63,4 +63,12 @@
6363
64Please see the [Juju SDK docs](https://juju.is/docs/sdk) for guidelines64Please see the [Juju SDK docs](https://juju.is/docs/sdk) for guidelines
65on enhancements to this charm following best practice guidelines, and65on enhancements to this charm following best practice guidelines, and
66`CONTRIBUTING.md` for developer guidance.
67\ No newline at end of file66\ No newline at end of file
67`CONTRIBUTING.md` for developer guidance.
68
69When developing the charm, here's a quick way to test out changes as
70they would be deployed by `landscape-scalable`:
71
72```bash
73charmcraft pack
74juju deploy ./bundle.yaml
75```
68\ No newline at end of file76\ No newline at end of file
6977
=== added file 'bundle.yaml'
--- bundle.yaml 1970-01-01 00:00:00 +0000
+++ bundle.yaml 2023-03-10 18:34:17 +0000
@@ -0,0 +1,32 @@
1description: Landscape Scalable
2applications:
3 postgresql:
4 series: focal
5 charm: ch:postgresql
6 num_units: 1
7 options:
8 extra_packages: python3-apt postgresql-contrib postgresql-.*-debversion postgresql-plpython3-*
9 max_connections: 500
10 max_prepared_transactions: 500
11 rabbitmq-server:
12 series: focal
13 charm: ch:rabbitmq-server
14 num_units: 1
15 haproxy:
16 series: focal
17 charm: ch:haproxy
18 num_units: 1
19 expose: true
20 options:
21 default_timeouts: "queue 60000, connect 5000, client 120000, server 120000"
22 services: ""
23 ssl_cert: SELFSIGNED
24 global_default_bind_options: "no-tlsv10"
25 landscape-server:
26 series: jammy
27 charm: ./landscape-server_ubuntu-22.04-amd64-arm64_ubuntu-20.04-amd64-arm64.charm
28 num_units: 1
29relations:
30 - [landscape-server, rabbitmq-server]
31 - [landscape-server, haproxy]
32 - ["landscape-server:db", "postgresql:db-admin"]
033
=== modified file 'config.yaml'
--- config.yaml 2023-01-27 20:37:12 +0000
+++ config.yaml 2023-03-10 18:34:17 +0000
@@ -140,3 +140,16 @@
140 description: |140 description: |
141 Comma-separated list of nagios servicegroups. If empty, the141 Comma-separated list of nagios servicegroups. If empty, the
142 nagios-context will be used as the servicegroup.142 nagios-context will be used as the servicegroup.
143 deployment_mode:
144 type: string
145 default: standalone
146 description: |
147 Landscape Server tenancy mode - do not modify unless you are able
148 to provide the additional configuration required to run
149 Landscape in SaaS mode.
150 additional_service_config:
151 type: string
152 default:
153 description: |
154 Additional service.conf settings to be merged with the default
155 configuration.
143156
=== modified file 'src/charm.py'
--- src/charm.py 2023-01-27 20:37:12 +0000
+++ src/charm.py 2023-03-10 18:34:17 +0000
@@ -36,8 +36,8 @@
36 ActiveStatus, BlockedStatus, Relation, MaintenanceStatus, WaitingStatus)36 ActiveStatus, BlockedStatus, Relation, MaintenanceStatus, WaitingStatus)
3737
38from settings_files import (38from settings_files import (
39 prepend_default_settings, update_default_settings, update_service_conf,39 configure_for_deployment_mode, merge_service_conf, prepend_default_settings,
40 write_license_file, write_ssl_cert)40 update_default_settings, update_service_conf, write_license_file, write_ssl_cert)
4141
42logger = logging.getLogger(__name__)42logger = logging.getLogger(__name__)
4343
@@ -136,6 +136,7 @@
136 })136 })
137 self._stored.set_default(leader_ip="")137 self._stored.set_default(leader_ip="")
138 self._stored.set_default(running=False)138 self._stored.set_default(running=False)
139 self._stored.set_default(paused=False)
139 self._stored.set_default(default_root_url="")140 self._stored.set_default(default_root_url="")
140 self._stored.set_default(account_bootstrapped=False)141 self._stored.set_default(account_bootstrapped=False)
141142
@@ -193,6 +194,16 @@
193 if isinstance(prev_status, BlockedStatus):194 if isinstance(prev_status, BlockedStatus):
194 self.unit.status = prev_status195 self.unit.status = prev_status
195196
197 # Update additional configuration
198 deployment_mode = self.model.config.get("deployment_mode")
199 update_service_conf({"global": {"deployment-mode": deployment_mode}})
200
201 configure_for_deployment_mode(deployment_mode)
202
203 additional_config = self.model.config.get("additional_service_config")
204 if additional_config:
205 merge_service_conf(additional_config)
206
196 self._update_ready_status(restart_services=True)207 self._update_ready_status(restart_services=True)
197208
198 def _on_install(self, event: InstallEvent) -> None:209 def _on_install(self, event: InstallEvent) -> None:
@@ -264,6 +275,10 @@
264 self.unit.status = ActiveStatus("Unit is ready")275 self.unit.status = ActiveStatus("Unit is ready")
265 return276 return
266277
278 if self._stored.paused:
279 self.unit.status = MaintenanceStatus("Services stopped")
280 return
281
267 self._stored.running = self._start_services()282 self._stored.running = self._start_services()
268283
269 def _start_services(self) -> bool:284 def _start_services(self) -> bool:
@@ -273,6 +288,7 @@
273 """288 """
274 self.unit.status = MaintenanceStatus("Starting services")289 self.unit.status = MaintenanceStatus("Starting services")
275 is_leader = self.unit.is_leader()290 is_leader = self.unit.is_leader()
291 deployment_mode = self.model.config.get("deployment_mode")
276292
277 update_default_settings({293 update_default_settings({
278 "RUN_ALL": "no",294 "RUN_ALL": "no",
@@ -285,6 +301,7 @@
285 "RUN_CRON": "yes" if is_leader else "no",301 "RUN_CRON": "yes" if is_leader else "no",
286 "RUN_PACKAGESEARCH": "yes" if is_leader else "no",302 "RUN_PACKAGESEARCH": "yes" if is_leader else "no",
287 "RUN_PACKAGEUPLOADSERVER": "yes" if is_leader else "no",303 "RUN_PACKAGEUPLOADSERVER": "yes" if is_leader else "no",
304 "RUN_PPPA_PROXY": "yes" if deployment_mode != "standalone" else "no",
288 })305 })
289306
290 logger.info("Starting services")307 logger.info("Starting services")
@@ -800,6 +817,7 @@
800 else:817 else:
801 self.unit.status = MaintenanceStatus("Services stopped")818 self.unit.status = MaintenanceStatus("Services stopped")
802 self._stored.running = False819 self._stored.running = False
820 self._stored.paused = True
803821
804 def _resume(self, event: ActionEvent):822 def _resume(self, event: ActionEvent):
805 self.unit.status = MaintenanceStatus("Starting services")823 self.unit.status = MaintenanceStatus("Starting services")
@@ -820,6 +838,7 @@
820 event.fail("Failed to start services: %s", start_result.stdout)838 event.fail("Failed to start services: %s", start_result.stdout)
821 else:839 else:
822 self._stored.running = True840 self._stored.running = True
841 self._stored.paused = False
823 self.unit.status = ActiveStatus("Unit is ready")842 self.unit.status = ActiveStatus("Unit is ready")
824 self._update_ready_status()843 self._update_ready_status()
825844
826845
=== modified file 'src/settings_files.py'
--- src/settings_files.py 2023-01-03 22:10:55 +0000
+++ src/settings_files.py 2023-03-10 18:34:17 +0000
@@ -11,6 +11,8 @@
11from urllib.request import urlopen11from urllib.request import urlopen
12from urllib.error import URLError12from urllib.error import URLError
1313
14CONFIGS_DIR = "/opt/canonical/landscape/configs"
15
14DEFAULT_SETTINGS = "/etc/default/landscape-server"16DEFAULT_SETTINGS = "/etc/default/landscape-server"
1517
16LICENSE_FILE = "/etc/landscape/license.txt"18LICENSE_FILE = "/etc/landscape/license.txt"
@@ -32,6 +34,30 @@
32 pass34 pass
3335
3436
37def configure_for_deployment_mode(mode: str) -> None:
38 """
39 Places files where Landscape expects to find them for different deployment
40 modes.
41 """
42 if mode == "standalone":
43 return
44
45 os.symlink(os.path.join(CONFIGS_DIR, "standalone"), os.path.join(CONFIGS_DIR, mode))
46
47
48def merge_service_conf(other: str) -> None:
49 """
50 Merges `other` into the Landscape Server configuration file,
51 overwriting existing config.
52 """
53 config = ConfigParser()
54 config.read(SERVICE_CONF)
55 config.read_string(other)
56
57 with open(SERVICE_CONF, "w") as config_fp:
58 config.write(config_fp)
59
60
35def prepend_default_settings(updates: dict) -> None:61def prepend_default_settings(updates: dict) -> None:
36 """62 """
37 Adds `updates` to the start of the Landscape Server default63 Adds `updates` to the start of the Landscape Server default
3864
=== modified file 'tests/test_settings_files.py'
--- tests/test_settings_files.py 2023-01-03 22:10:55 +0000
+++ tests/test_settings_files.py 2023-03-10 18:34:17 +0000
@@ -9,9 +9,9 @@
9from urllib.error import URLError9from urllib.error import URLError
1010
11from settings_files import (11from settings_files import (
12 LICENSE_FILE, LicenseFileReadException, SSLCertReadException,12 CONFIGS_DIR, LICENSE_FILE, LicenseFileReadException, SSLCertReadException,
13 prepend_default_settings, update_default_settings, update_service_conf,13 configure_for_deployment_mode, merge_service_conf, prepend_default_settings,
14 write_license_file, write_ssl_cert)14 update_default_settings, update_service_conf, write_license_file, write_ssl_cert)
1515
1616
17class CapturingBytesIO(BytesIO):17class CapturingBytesIO(BytesIO):
@@ -44,6 +44,83 @@
44 return super().close(*args, **kwargs)44 return super().close(*args, **kwargs)
4545
4646
47class ConfigureForDeploymentModeTestCase(TestCase):
48
49 @patch("os.symlink")
50 def test_configure_for_deployment_mode_standalone(self, symlink_mock):
51 """
52 The default mode, "standalone", should result in no changes.
53 """
54 configure_for_deployment_mode("standalone")
55
56 symlink_mock.assert_not_called()
57
58 @patch("os.symlink")
59 def test_configure_for_deployment_mode_other(self, symlink_mock):
60 """
61 Modes other than the magic "standalone" should symlink to a similarly named directory.
62 """
63 configure_for_deployment_mode("elite_dangerous")
64
65 symlink_mock.assert_called_once_with(
66 os.path.join(CONFIGS_DIR, "standalone"),
67 os.path.join(CONFIGS_DIR, "elite_dangerous")
68 )
69
70
71class MergeServiceConfTestCase(TestCase):
72
73 def test_merge_service_conf_new(self):
74 """
75 Tests that a new section and key are created in the existing
76 config file.
77 """
78 old_conf = "[global]\nfoo = bar\nbat = baz\n"
79 infile = StringIO(old_conf)
80 outfile = CapturingStringIO()
81 new_conf = "[new]\ncat = meow\n"
82
83 i = 0
84
85 def return_conf(path, *args, **kwargs):
86 nonlocal i
87 retval = (infile, outfile)[i]
88 i += 1
89 return retval
90
91 with patch("builtins.open") as open_mock:
92 open_mock.side_effect = return_conf
93 merge_service_conf(new_conf)
94
95 self.assertIn(old_conf, outfile.captured)
96 self.assertIn(new_conf, outfile.captured)
97
98 def test_merge_service_conf_override(self):
99 """
100 Tests that a provided config overrides values in the old config.
101 """
102 old_conf = "[global]\nleft = true\ntouched = false\n"
103 infile = StringIO(old_conf)
104 outfile = CapturingStringIO()
105 new_conf = "[global]\ntouched = true\n"
106
107 i = 0
108
109 def return_conf(path, *args, **kwargs):
110 nonlocal i
111 retval = (infile, outfile)[i]
112 i += 1
113 return retval
114
115 with patch("builtins.open") as open_mock:
116 open_mock.side_effect = return_conf
117 merge_service_conf(new_conf)
118
119 self.assertIn("left = true", outfile.captured)
120 self.assertIn("touched = true", outfile.captured)
121 self.assertNotIn("touched = false", outfile.captured)
122
123
47class PrependDefaultSettingsTestCase(TestCase):124class PrependDefaultSettingsTestCase(TestCase):
48125
49 def test_prepend(self):126 def test_prepend(self):

Subscribers

People subscribed via source and target branches

to all changes: