Merge lp:~chris.macnaughton/openstack-mojo-specs/upgrade-spec into lp:openstack-mojo-specs

Proposed by Chris MacNaughton
Status: Merged
Merged at revision: 333
Proposed branch: lp:~chris.macnaughton/openstack-mojo-specs/upgrade-spec
Merge into: lp:openstack-mojo-specs
Diff against target: 357 lines (+297/-3)
6 files modified
helper/bundles/ubuntu-lite.yaml (+5/-0)
helper/collect/collect-ubuntu-lite (+1/-0)
helper/scripts/upgrade_all_units.py (+13/-0)
helper/utils/mojo_utils.py (+261/-3)
specs/dev/series_upgrade_ubuntu_lite/SPEC_INFO.txt (+1/-0)
specs/dev/series_upgrade_ubuntu_lite/manifest (+16/-0)
To merge this branch: bzr merge lp:~chris.macnaughton/openstack-mojo-specs/upgrade-spec
Reviewer Review Type Date Requested Status
David Ames (community) Approve
Review via email: mp+337646@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Ryan Beisner (1chb1n) wrote :
Revision history for this message
David Ames (thedac) wrote :

I understand this is a WIP. Just some notes for the next revision(s).

Most (all?) juju ssh $UNIT sudo $CMD should be juju run --unit $UNIT $CMD. You get root for free and various --format output options more convenient for python.

A number of WIP print statements that either need logging statements or removal before landing. Not also the use of logging.error or logging.warn where appropriate.

The juju series-update commands, when is it safe to set this for an application that may have multiple units?

Individual try/except blocks look good. But it would make sense to have a top level try/except that determines success or failure based on raised exceptions in nested tries.

Larger question. At what point does it make more sense to create a bash script and scp it to the unit to run. Rather, than piecemeal commands over ssh? That might be a cleaner approach and it would be re-usable outside mojo.

review: Needs Fixing
Revision history for this message
Chris MacNaughton (chris.macnaughton) wrote :

Thanks for the suggestions, in addition to the comments inline, changes are incoming!

Several comments inline; regarding making a bash script rather than this approach, at the moment it is fairly isolated, and runnable with the script in helper/scripts/upgrade_all_units.py. One of the goals of this bit of code is that we will be able to migrate it easily into zaza (or just something else) at some point in the future, as a generic set of helper utilities that are consumable outside of mojo.

Revision history for this message
Chris MacNaughton (chris.macnaughton) :
330. By Chris MacNaughton

tidy a bit

331. By Chris MacNaughton

update handlers to get units started correctly

332. By Chris MacNaughton

updating series upgrade spec

333. By Chris MacNaughton

rename upgrade script

Revision history for this message
David Ames (thedac) wrote :

Need to fix linting errors. I expect this might be challenging as most of them are due to long lines in the bash snippets.

> Most (all?) juju ssh $UNIT sudo $CMD should be juju run --unit $UNIT $CMD. You get root for free and various --format output options more convenient for python.

The do-release-upgrade and the loop check after reboot makes sense to use SSH. The others, reboot and lsb_release check, make more sense as juju run.

Note: it turns out juju run --unit runs as root but juju run --machine runs as ubuntu user. Just for consistency.

> A number of WIP print statements that either need logging statements or removal before landing. Not also the use of logging.error or logging.warn where appropriate.

Still true. Some are commented but they should be removed or made logging statements. Use of logging.error and logging.warn suggestions still in effect.

review: Needs Fixing
334. By Chris MacNaughton

update to resolve lint issues and resolve discussions

- remove prints
- shift to use `juju run` where possible

Revision history for this message
Chris MacNaughton (chris.macnaughton) wrote :

A couple of things left to discuss:

> The juju series-update commands, when is it safe to set this for an application that may have multiple units?

At the moment, there is no possible config into the actual callable script, although it will only iterate through units, so, depending on the HA model of the hosted application, there shouldn't be service disruption as we will only be upgrading a single unit at a time for now.

> Individual try/except blocks look good. But it would make sense to have a top level try/except that determines success or failure based on raised exceptions in nested tries.

I'm open to working through doing something like this if we decide that it's necessary; however, I'm nervous to make this code perfect, given that the Juju core team has picked up a roadmap item that will reduce our "series_upgrade" function to something along the lines of `juju series-upgrade $machine_id`, at which point we will be removing almost all of the upgrade code. To make the cleanup of that easier, in the last commit I have comment fenced off the upgrade code, although I propose that it could be moved into a new helper alongside the mojo_utils.py for now to ensure isolation.

> Larger question. At what point does it make more sense to create a bash script and scp it to the unit to run. Rather, than piecemeal commands over ssh? That might be a cleaner approach and it would be re-usable outside mojo.

Rather than scp'ing a bash script, I am considering shifting the series of `juju ssh`'d commands into building a bash script that gets rendered (depends entirely on machine numbers, unit names & numbers, etc) and putting that into /tmp to execute this, as it would ease the run time a bit; however, the amount of time spent running these scripts pales in comparison to the runtime of the `do-release-upgrade`, so I'm not sure of the real value, except to improve readability (a good value, but questionable to me given that this code has been written to be deleted).

Revision history for this message
David Ames (thedac) wrote :

I think we are good at this point. The previous comment were topics I felt were answered previously. This looks good. I'll be merging. Several specs all back to trunk.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'helper/bundles/ubuntu-lite.yaml'
2--- helper/bundles/ubuntu-lite.yaml 1970-01-01 00:00:00 +0000
3+++ helper/bundles/ubuntu-lite.yaml 2018-03-20 07:12:59 +0000
4@@ -0,0 +1,5 @@
5+series: "{{ series }}"
6+services:
7+ ubuntu-lite:
8+ charm: ubuntu-lite
9+ num_units: 3
10
11=== added file 'helper/collect/collect-ubuntu-lite'
12--- helper/collect/collect-ubuntu-lite 1970-01-01 00:00:00 +0000
13+++ helper/collect/collect-ubuntu-lite 2018-03-20 07:12:59 +0000
14@@ -0,0 +1,1 @@
15+ubuntu-lite cs:~jameinel/ubuntu-lite
16
17=== added file 'helper/scripts/upgrade_all_units.py'
18--- helper/scripts/upgrade_all_units.py 1970-01-01 00:00:00 +0000
19+++ helper/scripts/upgrade_all_units.py 2018-03-20 07:12:59 +0000
20@@ -0,0 +1,13 @@
21+#!/usr/bin/env python
22+import sys
23+import utils.mojo_utils as mojo_utils
24+import os
25+
26+
27+def main(argv):
28+ mojo_utils.setup_logging()
29+ mojo_utils.upgrade_all_units()
30+
31+
32+if __name__ == "__main__":
33+ sys.exit(main(sys.argv))
34
35=== modified file 'helper/utils/mojo_utils.py'
36--- helper/utils/mojo_utils.py 2018-01-19 12:15:27 +0000
37+++ helper/utils/mojo_utils.py 2018-03-20 07:12:59 +0000
38@@ -1,5 +1,5 @@
39 #!/usr/bin/env python
40-
41+import six
42 import logging
43 import os
44 import shutil
45@@ -456,8 +456,8 @@
46 files.append(os.path.join(os.path.dirname(__file__), filename))
47 # Up one directory from called file
48 files.append(os.path.join(
49- os.path.dirname(os.path.dirname(__file__)),
50- filename))
51+ os.path.dirname(os.path.dirname(__file__)),
52+ filename))
53
54 for file_path in files:
55 if os.path.isfile(file_path):
56@@ -554,6 +554,264 @@
57 time.sleep(30)
58
59
60+# Begin upgrade code
61+
62+def do_release_upgrade(unit):
63+ """Runs do-release-upgrade noninteractive"""
64+ logging.info('Upgrading ' + unit)
65+ subprocess.call([kiki.cmd(), 'run', '--unit', unit, 'status-set',
66+ 'maintenance', 'Doing release upgrade'])
67+ cmd = [kiki.cmd(), 'ssh', unit, 'sudo',
68+ 'do-release-upgrade', '-f', 'DistUpgradeViewNonInteractive']
69+ try:
70+ subprocess.check_call(cmd)
71+ except subprocess.CalledProcessError as e:
72+ logging.warn("Failed do-release-upgrade for {}".format(unit))
73+ logging.warn(e)
74+ return False
75+ finally:
76+ subprocess.call([kiki.cmd(), 'run', '--unit', unit, 'status-set',
77+ 'active'])
78+ return True
79+
80+
81+def reboot(unit):
82+ """Reboot machine"""
83+ cmd = [kiki.cmd(), 'run', '--unit', unit, 'sudo', 'reboot', '&&', 'exit']
84+ try:
85+ subprocess.check_call(cmd)
86+ except subprocess.CalledProcessError as e:
87+ logging.info(e)
88+ pass
89+
90+
91+def upgrade_machine(app_name, unit, machine, machine_num):
92+ """Run the upgrade process for a single machine"""
93+ cmd = [kiki.cmd(), 'run', '--unit', unit, 'status-set',
94+ 'maintenance', 'Upgrading series']
95+ subprocess.call(cmd)
96+ if not do_release_upgrade(unit):
97+ return False
98+ if machine["series"] == "trusty":
99+ upstart_to_systemd(machine_num)
100+ cmd = [kiki.cmd(), 'run', '--unit', unit, 'status-set',
101+ 'active']
102+ subprocess.call(cmd)
103+ logging.debug("Rebooting")
104+ reboot(unit)
105+ cmd = [kiki.cmd(), "ssh", unit, "exit"]
106+ while(True):
107+ try:
108+ subprocess.check_call(cmd)
109+ break
110+ except subprocess.CalledProcessError:
111+ logging.debug("Waiting 2 more seconds")
112+ time.sleep(2)
113+ update_machine_series(app_name, machine_num)
114+ return True
115+
116+
117+def update_machine_series(app_name, machine_num):
118+ cmd = [kiki.cmd(), 'run', '--machine',
119+ machine_num, 'lsb_release', '-c', '-s']
120+ codename = subprocess.check_output(cmd)
121+ if six.PY3:
122+ codename = codename.decode('utf-8')
123+ codename = codename.strip()
124+ logging.debug("Telling juju that {} series is {}".format(
125+ machine_num, codename))
126+ cmd = [kiki.cmd(), 'update-series', str(machine_num), codename]
127+ subprocess.call(cmd)
128+ cmd = [kiki.cmd(), 'update-series', app_name, codename]
129+ subprocess.call(cmd)
130+
131+
132+SYSTEMD_JUJU_MACHINE_AGENT_SCRIPT = """#!/usr/bin/env bash
133+
134+# Set up logging.
135+touch '/var/log/juju/machine-{machine_id}.log'
136+chown syslog:syslog '/var/log/juju/machine-{machine_id}.log'
137+chmod 0600 '/var/log/juju/machine-{machine_id}.log'
138+exec >> '/var/log/juju/machine-{machine_id}.log'
139+exec 2>&1
140+
141+# Run the script.
142+'/var/lib/juju/tools/machine-{machine_id}/jujud' machine --data-dir '/var/lib/juju' --machine-id {machine_id} --debug
143+""" # nopep8
144+
145+SYSTEMD_JUJU_UNIT_AGENT_SCRIPT = """#!/usr/bin/env bash
146+
147+# Set up logging.
148+touch '/var/log/juju/unit-{application_name}-{application_number}.log'
149+chown syslog:syslog '/var/log/juju/unit-{application_name}-{application_number}.log'
150+chmod 0600 '/var/log/juju/unit-{application_name}-{application_number}.log'
151+exec >> '/var/log/juju/unit-{application_name}-{application_number}.log'
152+exec 2>&1
153+
154+# Run the script.
155+'/var/lib/juju/tools/unit-{application_name}-{application_number}/jujud' unit --data-dir '/var/lib/juju' --unit-name {application} --debug
156+""" # nopep8
157+
158+SYSTEMD_JUJU_MACHINE_INIT_FILE = """[Unit]
159+Description=juju agent for machine-{name}
160+After=syslog.target
161+After=network.target
162+After=systemd-user-sessions.service
163+
164+[Service]
165+LimitNOFILE=20000
166+ExecStart=/var/lib/juju/init/jujud-machine-{name}/exec-start.sh
167+Restart=on-failure
168+TimeoutSec=300
169+
170+[Install]
171+WantedBy=multi-user.target
172+""" # nopep8
173+
174+
175+SYSTEMD_JUJU_UNIT_INIT_FILE = """[Unit]
176+Description=juju unit agent for unit-{application_name}-{application_number}
177+After=syslog.target
178+After=network.target
179+After=systemd-user-sessions.service
180+
181+[Service]
182+Environment="JUJU_CONTAINER_TYPE="
183+ExecStart=/var/lib/juju/init/jujud-unit-{application_name}-{application_number}/exec-start.sh
184+Restart=on-failure
185+TimeoutSec=300
186+
187+[Install]
188+WantedBy=multi-user.target
189+"""
190+
191+
192+def upstart_to_systemd(machine_number):
193+ """Upgrade upstart scripts to Systemd after upgrade from Trusty"""
194+ base_command = [kiki.cmd(), 'run', '--machine', str(machine_number), '--']
195+ commands = [
196+ base_command + [
197+ "sudo", "mkdir", "-p",
198+ "/var/lib/juju/init/jujud-machine-{}".format(machine_number)],
199+ base_command + [
200+ 'echo', SYSTEMD_JUJU_MACHINE_AGENT_SCRIPT.format(
201+ machine_id=machine_number),
202+ '|', 'sudo', 'tee',
203+ ('/var/lib/juju/init/jujud-machine-{machine_id}'
204+ '/exec-start.sh').format(
205+ machine_id=machine_number)],
206+ base_command + [
207+ 'echo', SYSTEMD_JUJU_MACHINE_INIT_FILE.format(
208+ name=machine_number), '|', 'sudo', 'tee',
209+ ('/var/lib/juju/init/jujud-machine-{name}'
210+ '/jujud-machine-{name}.service').format(
211+ name=machine_number)],
212+ base_command + [
213+ 'sudo', 'chmod', '755',
214+ ('/var/lib/juju/init/jujud-machine-{machine_id}/'
215+ 'exec-start.sh').format(machine_id=machine_number)],
216+ base_command + [
217+ 'sudo', 'ln', '-s',
218+ ('/var/lib/juju/init/jujud-machine-{machine_id}/'
219+ 'jujud-machine-{machine_id}.service').format(
220+ machine_id=machine_number
221+ ), '/etc/systemd/system/'],
222+ base_command + [
223+ 'sudo', 'ln', '-s',
224+ ('/var/lib/juju/init/jujud-machine-{machine_id}/'
225+ 'jujud-machine-{machine_id}.service').format(
226+ machine_id=machine_number),
227+ ('/etc/systemd/system/multi-user.target.wants/'
228+ 'jujud-machine-{machine_id}.service').format(
229+ machine_id=machine_number
230+ )
231+ ]
232+ ]
233+ commands += units_upstart_to_systemd_commands(machine_number)
234+ for cmd in commands:
235+ try:
236+ subprocess.check_call(cmd)
237+ except subprocess.CalledProcessError as e:
238+ logging.warn(e)
239+ return False
240+
241+
242+def units_upstart_to_systemd_commands(machine_number):
243+ """Upgrade a specific application unit from Upstart to Systemd"""
244+ units = get_juju_status(unit=str(machine_number))["applications"]
245+ base_command = [kiki.cmd(), 'run', '--machine', str(machine_number), '--']
246+ commands = []
247+ for (name, app_unit) in units.iteritems():
248+ for (unit_name, unit) in app_unit["units"].iteritems():
249+ logging.debug("Updating {} [{}]".format(name, unit_name))
250+ app_number = unit_name.split("/")[-1]
251+ systemd_file_name = ("jujud-unit-{app_name}"
252+ "-{app_number}.service").format(
253+ app_name=name, app_number=app_number)
254+ systemd_file_path = ('/var/lib/juju/init/jujud-unit-{app_name}'
255+ '-{app_number}/{file_name}').format(
256+ app_name=name,
257+ app_number=app_number,
258+ file_name=systemd_file_name)
259+ commands += [
260+ base_command + [
261+ "sudo", "mkdir", "-p",
262+ "/var/lib/juju/init/jujud-unit-{}-{}".format(
263+ name, app_number)],
264+ base_command + [
265+ 'echo', SYSTEMD_JUJU_UNIT_AGENT_SCRIPT.format(
266+ application=unit_name,
267+ application_name=name,
268+ application_number=app_number),
269+ '|', 'sudo', 'tee',
270+ ('/var/lib/juju/init/jujud-unit-{app_name}-'
271+ '{app_number}/exec-start.sh').format(
272+ app_name=name, app_number=app_number)],
273+ base_command + [
274+ 'echo', SYSTEMD_JUJU_UNIT_INIT_FILE.format(
275+ application_name=name, application_number=app_number),
276+ '|', 'sudo', 'tee', systemd_file_path],
277+ base_command + [
278+ 'sudo', 'chmod', '755',
279+ ('/var/lib/juju/init/jujud-unit-{app_name}-'
280+ '{app_number}/exec-start.sh').format(
281+ app_name=name, app_number=app_number)],
282+ base_command + [
283+ 'sudo', 'ln', '-s', systemd_file_path,
284+ '/etc/systemd/system/'],
285+ base_command + [
286+ 'sudo', 'ln', '-s', systemd_file_path,
287+ ('/etc/systemd/system/multi-user.target.wants/'
288+ '{file_name}').format(
289+ machine_id=machine_number,
290+ file_name=systemd_file_name)
291+ ]
292+ ]
293+ return commands
294+
295+
296+def upgrade_all_units(juju_status=None):
297+ if not juju_status:
298+ juju_status = get_juju_status()
299+ # Upgrade the rest
300+ upgraded_machines = []
301+ for (app_name, details) in juju_status['applications'].items():
302+ for name, unit_details in details['units'].items():
303+ logging.debug("Details for {}: {}".format(name, unit_details))
304+ machine_id = unit_details["machine"]
305+ if machine_id in upgraded_machines:
306+ continue
307+ if not upgrade_machine(
308+ app_name,
309+ name,
310+ juju_status["machines"][machine_id],
311+ machine_id
312+ ):
313+ logging.warn("No series upgrade found for {}".format(name))
314+ upgraded_machines.append(machine_id)
315+# End upgrade code
316+
317+
318 def parse_mojo_arg(options, mojoarg, multiargs=False):
319 if mojoarg.upper() in os.environ:
320 if multiargs:
321
322=== added directory 'specs/dev/series_upgrade_ubuntu_lite'
323=== added file 'specs/dev/series_upgrade_ubuntu_lite/SPEC_INFO.txt'
324--- specs/dev/series_upgrade_ubuntu_lite/SPEC_INFO.txt 1970-01-01 00:00:00 +0000
325+++ specs/dev/series_upgrade_ubuntu_lite/SPEC_INFO.txt 2018-03-20 07:12:59 +0000
326@@ -0,0 +1,1 @@
327+Deploys ubuntu-lite, and upgrades the charm between series.
328
329=== added symlink 'specs/dev/series_upgrade_ubuntu_lite/check_juju.py'
330=== target is u'../../../helper/tests/check_juju.py'
331=== added symlink 'specs/dev/series_upgrade_ubuntu_lite/collect-ubuntu-lite'
332=== target is u'../../../helper/collect/collect-ubuntu-lite'
333=== added file 'specs/dev/series_upgrade_ubuntu_lite/manifest'
334--- specs/dev/series_upgrade_ubuntu_lite/manifest 1970-01-01 00:00:00 +0000
335+++ specs/dev/series_upgrade_ubuntu_lite/manifest 2018-03-20 07:12:59 +0000
336@@ -0,0 +1,16 @@
337+# Collect the charm
338+collect config=collect-ubuntu-lite
339+
340+# Deploy ubuntu-lite bundle
341+deploy config=ubuntu-lite.yaml delay=0 wait=False
342+
343+# Wait for deployment to settle
344+verify config=check_juju.py
345+
346+# Upgrade units
347+script config=scripts/upgrade_all_units.py
348+
349+# Wait for deployment to settle
350+verify config=check_juju.py
351+
352+# Success
353
354=== added symlink 'specs/dev/series_upgrade_ubuntu_lite/scripts'
355=== target is u'../../../helper/scripts'
356=== added symlink 'specs/dev/series_upgrade_ubuntu_lite/ubuntu-lite.yaml'
357=== target is u'../../../helper/bundles/ubuntu-lite.yaml'

Subscribers

People subscribed via source and target branches