Merge lp:~mvo/ubuntu/vivid/ubuntu-core-upgrader/chainsaw-removal into lp:ubuntu/vivid/ubuntu-core-upgrader

Proposed by Michael Vogt
Status: Merged
Merged at revision: 19
Proposed branch: lp:~mvo/ubuntu/vivid/ubuntu-core-upgrader/chainsaw-removal
Merge into: lp:ubuntu/vivid/ubuntu-core-upgrader
Diff against target: 1556 lines (+48/-1183)
5 files modified
bin/ubuntu-core-upgrade (+0/-16)
functional/test_upgrader.py (+8/-4)
ubuntucoreupgrader/tests/test_upgrader.py (+7/-20)
ubuntucoreupgrader/tests/utils.py (+0/-26)
ubuntucoreupgrader/upgrader.py (+33/-1117)
To merge this branch: bzr merge lp:~mvo/ubuntu/vivid/ubuntu-core-upgrader/chainsaw-removal
Reviewer Review Type Date Requested Status
James Hunt (community) Approve
Review via email: mp+251778@code.launchpad.net

Description of the change

As discussed on the g+ call.

This branch removes the inplace upgrade code. Its currently not used
and if we need it again we can resurrect it from the bzr history.

It also simplifies the logic to find what to mount/remount by simply
looking at /writable/cache/system.

I might have missed bits that are no longer needed, not sure. I tested with a empty b partition and it works for me for that. But more real world testing will certainly not hurt :)

To post a comment you must log in.
Revision history for this message
James Hunt (jamesodhunt) wrote :

Yeah - slash and burn! Less code is good :-)

Just a few comments (extra code to zap :)...

- As the in-place upgrade code has now gone, you can also zap:
  - reboot_reason
  - reboot_reasons
  - --check-reboot
  - --no-reboot
  - --reboot-delay

- the man page (man/ubuntu-core-upgrade.1) needs an update to reflect the reduced set of command-line options.

23. By Michael Vogt on 2015-03-05

remove more unused code

Revision history for this message
Michael Vogt (mvo) wrote :

Thanks, this is updated now and I did light testing. If it look good otherwise I will do some additional test runs and merge/upload.

Revision history for this message
James Hunt (jamesodhunt) wrote :

LGTM. thanks.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/ubuntu-core-upgrade'
2--- bin/ubuntu-core-upgrade 2015-03-03 16:42:09 +0000
3+++ bin/ubuntu-core-upgrade 2015-03-05 13:24:48 +0000
4@@ -56,10 +56,6 @@
5 if options.debug:
6 log.setLevel(logging.DEBUG)
7
8- if options.dry_run or options.check_reboot:
9- # use default logger which displays output to stderr only.
10- return
11-
12 # send data to syslog...
13 handler = logging.handlers.SysLogHandler(address='/dev/log')
14
15@@ -105,26 +101,14 @@
16 def main():
17 options = parse_args(sys.argv[1:])
18
19- if options.reboot_delay and options.reboot_delay < 0:
20- sys.exit('ERROR: must specify a positive number')
21-
22 if not os.path.exists(options.root_dir):
23 sys.exit('ERROR: root directory does not exist: {}'
24 .format(options.root_dir))
25
26- if options.check_reboot:
27- # check options must be inert
28- options.dry_run = True
29-
30 if options.dry_run or options.root_dir != '/':
31 prepare_upgrade(options)
32 sys.exit(0)
33
34- if options.show_other_details:
35- upgrader = Upgrader(options, [], None)
36- upgrader.show_other_partition_details()
37- sys.exit(0)
38-
39 if not options.cmdfile:
40 sys.exit('ERROR: need command file')
41
42
43=== modified file 'functional/test_upgrader.py'
44--- functional/test_upgrader.py 2015-03-04 13:53:01 +0000
45+++ functional/test_upgrader.py 2015-03-05 13:24:48 +0000
46@@ -24,12 +24,9 @@
47 import sys
48 import os
49 import logging
50-import tarfile
51 import unittest
52 import shutil
53
54-from unittest.mock import patch
55-
56 from ubuntucoreupgrader.upgrader import (
57 Upgrader,
58 parse_args,
59@@ -39,7 +36,13 @@
60 module_dir = os.path.normpath(os.path.realpath(base_dir + os.sep + '..'))
61 sys.path.append(base_dir)
62
63-from ubuntucoreupgrader.tests.utils import *
64+from ubuntucoreupgrader.tests.utils import (
65+ make_tmp_dir,
66+ append_file,
67+ TEST_DIR_MODE,
68+ create_file,
69+ UbuntuCoreUpgraderTestCase,
70+ )
71
72 CMD_FILE = 'ubuntu_command'
73
74@@ -75,6 +78,7 @@
75
76 upgrader = Upgrader(parse_args(args), commands, [])
77 upgrader.get_cache_dir = mock_get_cache_dir
78+ upgrader.MOUNTPOINT_CMD = "true"
79 upgrader.run()
80
81 shutil.rmtree(cache_dir)
82
83=== modified file 'ubuntucoreupgrader/tests/test_upgrader.py'
84--- ubuntucoreupgrader/tests/test_upgrader.py 2015-03-04 13:53:01 +0000
85+++ ubuntucoreupgrader/tests/test_upgrader.py 2015-03-05 13:24:48 +0000
86@@ -31,8 +31,6 @@
87 import shutil
88 import sys
89
90-from unittest.mock import patch
91-
92 from ubuntucoreupgrader.upgrader import (
93 tar_generator,
94 Upgrader,
95@@ -42,7 +40,11 @@
96 base_dir = os.path.abspath(os.path.dirname(__file__))
97 sys.path.append(base_dir)
98
99-from ubuntucoreupgrader.tests.utils import *
100+from ubuntucoreupgrader.tests.utils import (
101+ create_file,
102+ make_tmp_dir,
103+ UbuntuCoreUpgraderTestCase,
104+ )
105
106 # file mode to use for creating test directories.
107 TEST_DIR_MODE = 0o750
108@@ -192,19 +194,6 @@
109 return l
110
111
112-def mock_get_root_partitions_by_label():
113- matches = []
114-
115- matches.append(('system-a', '/dev/sda3', '/'))
116- matches.append(('system-b', '/dev/sda4', '/writable/cache/system'))
117-
118- return matches
119-
120-
121-def mock_make_mount_private(target):
122- pass
123-
124-
125 class UbuntuCoreUpgraderObectTestCase(UbuntuCoreUpgraderTestCase):
126
127 def mock_get_cache_dir(self):
128@@ -216,10 +205,6 @@
129 os.makedirs(self.sys_dir, exist_ok=True)
130 return self.cache_dir
131
132- @patch('ubuntucoreupgrader.upgrader.get_root_partitions_by_label',
133- mock_get_root_partitions_by_label)
134- @patch('ubuntucoreupgrader.upgrader.make_mount_private',
135- mock_make_mount_private)
136 def test_create_object(self):
137 root_dir = make_tmp_dir()
138
139@@ -249,6 +234,7 @@
140
141 upgrader = Upgrader(options, commands, [])
142 upgrader.get_cache_dir = self.mock_get_cache_dir
143+ upgrader.MOUNTPOINT_CMD = "true"
144 upgrader.run()
145
146 path = os.path.join(root_dir, file)
147@@ -256,5 +242,6 @@
148
149 shutil.rmtree(root_dir)
150
151+
152 if __name__ == "__main__":
153 unittest.main()
154
155=== modified file 'ubuntucoreupgrader/tests/utils.py'
156--- ubuntucoreupgrader/tests/utils.py 2015-03-04 13:53:01 +0000
157+++ ubuntucoreupgrader/tests/utils.py 2015-03-05 13:24:48 +0000
158@@ -22,8 +22,6 @@
159 import shutil
160 import unittest
161
162-from unittest.mock import patch
163-
164 # file mode to use for creating test directories.
165 TEST_DIR_MODE = 0o750
166
167@@ -77,13 +75,6 @@
168 return matches
169
170
171-def mock_make_mount_private(target):
172- '''
173- NOP implementation.
174- '''
175- pass
176-
177-
178 class UpdateTree():
179 '''
180 Representation of a directory tree that will be converted into an
181@@ -255,20 +246,6 @@
182 # it.
183 self.victim_dir = make_tmp_dir(tag='victim')
184
185- self.patch_get_root_partitions_by_label = \
186- patch('ubuntucoreupgrader.upgrader.get_root_partitions_by_label',
187- mock_get_root_partitions_by_label)
188-
189- self.patch_make_mount_private = \
190- patch('ubuntucoreupgrader.upgrader.make_mount_private',
191- mock_make_mount_private)
192-
193- self.mock_get_root_partitions_by_label = \
194- self.patch_get_root_partitions_by_label.start()
195-
196- self.mock_make_mount_private = \
197- self.patch_make_mount_private.start()
198-
199 def tearDown(self):
200 '''
201 Test cleanup.
202@@ -284,9 +261,6 @@
203 shutil.rmtree(self.victim_dir)
204 self.victim_dir = None
205
206- self.patch_get_root_partitions_by_label.stop()
207- self.patch_make_mount_private.stop()
208-
209 def run(self, result=None):
210 self.currentResult = result
211 unittest.TestCase.run(self, result)
212
213=== modified file 'ubuntucoreupgrader/upgrader.py'
214--- ubuntucoreupgrader/upgrader.py 2015-03-04 11:53:25 +0000
215+++ ubuntucoreupgrader/upgrader.py 2015-03-05 13:24:48 +0000
216@@ -40,34 +40,22 @@
217 import sys
218 import os
219 import logging
220-import re
221 import shutil
222 import subprocess
223 import tempfile
224 import tarfile
225-import stat
226-import errno
227-import dbus
228 import argparse
229
230-from enum import Enum
231-from time import sleep
232-
233 script_name = os.path.basename(__file__)
234
235 log = logging.getLogger()
236
237-# seconds to wait before a reboot (should one be required)
238-DEFAULT_REBOOT_DELAY = 2
239-
240 DEFAULT_ROOT = '/'
241
242 # Name of the writable user data partition label as created by
243 # ubuntu-device-flash(1).
244 WRITABLE_DATA_LABEL = 'writable'
245
246-WRITABLE_MOUNTPOINT = '/writable'
247-
248 # name of primary root filesystem partition label as created by
249 # ubuntu-device-flash(1).
250 SYSTEM_DATA_A_LABEL = 'system-a'
251@@ -113,24 +101,6 @@
252 parser = argparse.ArgumentParser(description='System image Upgrader')
253
254 parser.add_argument(
255- '--check-reboot',
256- action='store_true',
257- help='''
258- Attempt to determine if a reboot may be required.
259-
260- This option is similar to --dry-run: no system changes are made.
261-
262- Note that the result of this command cannot be definitive since a
263- reboot would be triggered if a service failed to start (but this
264- option does not attempt to restart any services).
265- ''')
266-
267- parser.add_argument(
268- '--clean-only',
269- action='store_true',
270- help='Clean up from a previous upgrader run, but do not upgrade)')
271-
272- parser.add_argument(
273 '--debug',
274 nargs='?', const=1, default=0, type=int,
275 help='Dump debug info (specify numeric value to increase verbosity)')
276@@ -144,41 +114,16 @@
277 ''')
278
279 parser.add_argument(
280- '--force-inplace-upgrade',
281- action='store_true',
282- help='Apply an upgrade to current rootfs even if running " \
283- "on dual rootfs system')
284-
285- parser.add_argument(
286 '--leave-files',
287 action='store_true',
288 help='Do not remove the downloaded system image files after upgrade')
289
290 parser.add_argument(
291- '--no-reboot',
292- action='store_true',
293- help='Do not reboot even if one would normally be required')
294-
295- parser.add_argument(
296- '--reboot-delay',
297- type=int, default=DEFAULT_REBOOT_DELAY,
298- help='''
299- Wait for specified number of seconds before rebooting
300- (default={})
301- '''.format(DEFAULT_REBOOT_DELAY))
302-
303- parser.add_argument(
304 '--root-dir',
305 default=DEFAULT_ROOT,
306 help='Specify an alternative root directory (for testing ONLY)')
307
308 parser.add_argument(
309- '--show-other-details',
310- action='store_true',
311- help='Dump the details of the system-image vesion on the " \
312- "other root partition')
313-
314- parser.add_argument(
315 '-t', '--tmpdir',
316 help='Specify name for pre-existing temporary directory to use')
317
318@@ -210,100 +155,6 @@
319 return path[len(prefix)-1:]
320
321
322-def get_command(pid):
323- '''
324- Returns full path to binary associated with @pid, or None if the
325- lookup failed.
326- '''
327- try:
328- exe = os.readlink('/proc/' + str(pid) + '/exe')
329- except:
330- # pid probably went away
331- return None
332-
333- return exe
334-
335-
336-def get_root_partitions_by_label():
337- '''
338- Returns a list of tuples of the recognised root filesystem partitions
339- available on this system. The tuples contain the following triplet:
340-
341- ( <partition-name>, <full-device-path>, <mountpoint> )
342-
343- '''
344- cmd = 'lsblk'
345- recognised = (SYSTEM_DATA_A_LABEL, SYSTEM_DATA_B_LABEL)
346-
347- matches = []
348-
349- args = [cmd, '--ascii', '--output-all', '--pairs']
350- log.debug('running: {}'.format(args))
351-
352- proc = subprocess.Popen(args,
353- stdout=subprocess.PIPE,
354- stderr=subprocess.DEVNULL,
355- universal_newlines=True)
356-
357- if proc.wait() != 0:
358- return matches
359-
360- stdout = proc.communicate()[0]
361- lines = stdout.split('\n')
362-
363- for line in lines:
364- line = line.rstrip()
365- if not line:
366- continue
367-
368- fields = {}
369-
370- # split the line into 'NAME="quoted value"' fields
371- results = re.findall(r'(?:[^\s"]|"(?:[^"])*")+', line)
372- for result in results:
373- name, value = result.split('=')
374-
375- # remove quotes
376- value = value.lstrip('"').rstrip('"')
377-
378- fields[name] = value
379-
380- if fields['NAME'] != fields['KNAME']:
381- # For SCSI devices, these fields match
382- continue
383-
384- if 'LABEL' in fields and fields['LABEL'] in recognised:
385- # reconstructing the device name like this is valid
386- # for SCSI devices
387- device = '/dev/{}'.format(fields['NAME'])
388-
389- if not os.path.exists(device):
390- continue
391- matches.append((fields['LABEL'],
392- device,
393- fields['MOUNTPOINT']))
394-
395- return matches
396-
397-
398-def uses_ab_partitions():
399- '''
400- Returns: True if the system uses A/B partitions, else False.
401- '''
402- return len(get_root_partitions_by_label()) == 2
403-
404-
405-def get_writable_disk():
406- '''
407- Establish the disk partition for the writable user data partition.
408- '''
409- cmd = "blkid -L '{}' 2>/dev/null".format(WRITABLE_DATA_LABEL)
410- log.debug('running: {}'.format(cmd))
411-
412- output = subprocess.getoutput(cmd)
413- return output.rstrip()
414-
415-
416 def fsck(device):
417 '''
418 Run fsck(8) on specified device.
419@@ -414,31 +265,6 @@
420 mount(source, target, 'bind')
421
422
423-def mount_all():
424- '''
425- Mount all filesystems if not already mounted.
426- '''
427-
428- args = ['mount', '-a']
429- log.debug('running: {}'.format(args))
430- proc = subprocess.Popen(args,
431- stderr=subprocess.PIPE,
432- universal_newlines=True)
433-
434- if proc.wait() != 0:
435- stderr = proc.communicate()[1]
436- log.error('failed to mount all ({}): {}'
437- .format(args, stderr))
438-
439-
440-def lazy_unmount(target):
441- '''
442- async unmount the specified mount target.
443- '''
444-
445- unmount(target, ['--lazy'])
446-
447-
448 def unmount(target, options=None):
449 '''
450 Unmount the specified mount target, using the specified list of
451@@ -473,355 +299,6 @@
452 # sys.exit(1)
453
454
455-def make_mount_private(target):
456- '''
457- Make the specified filesystem private.
458- '''
459-
460- args = ['mount', '--make-rprivate', target]
461- log.debug('running: {}'.format(args))
462-
463- proc = subprocess.Popen(args,
464- stderr=subprocess.PIPE,
465- universal_newlines=True)
466-
467- if proc.wait() != 0:
468- stderr = proc.communicate()[1]
469- log.error('failed to make {} private ({}): {}'
470- .format(target, args, stderr))
471-
472-
473-def lazy_unmount_specified(mounts):
474- '''
475- async nnmount all targets specified by @mounts.
476- '''
477-
478- for mount in mounts:
479- lazy_unmount(mount)
480-
481-
482-def get_writable_mounts():
483- '''
484- Returns a list of (bind) mounts whose source location is the
485- writable partition.
486-
487- Note that the list of bind mounts is derived from the current
488- mounts. This is safer than simply checking fstab since it is
489- guaranteed correct.
490-
491- Note also that the list is reverse sorted so that assuming the list
492- is processed in order, child mounts will be handled before parent
493- mounts.
494- '''
495- disk = get_writable_disk()
496- file = '/proc/mounts'
497-
498- mounts = []
499-
500- try:
501- with open(file, 'r') as fh:
502- for mount in fh.readlines():
503- mount = mount.strip()
504- fields = mount.split()
505-
506- # only consider user data mounts
507- if fields[0] != disk:
508- continue
509-
510- # ignore the primary mount
511- if fields[1] == WRITABLE_MOUNTPOINT:
512- continue
513-
514- # ignore root
515- if fields[1] == '/' or fields[1] == '/root':
516- continue
517-
518- mounts.append(fields[1])
519- except:
520- sys.exit('Failed to read command file: {}'.format(file))
521-
522- # Reverse sort to ensure each child mount is handled before its
523- # parent.
524- return sorted(mounts, reverse=True)
525-
526-
527-def get_affected_pids(unpack_inodes):
528- '''
529- @unpack_inodes: list of inodes representing the files the system image
530- upgrade will modify.
531-
532- Returns a dict keyed by filename whose value is an array of pids
533- that are using the file currently.
534-
535- '''
536-
537- # Command to list all open files as quickly as possible.
538- #
539- # XXX: Note that although we nominally only care about files open on
540- # the rootfs, we don't constrain lsof to only consider such files
541- # since the system-images contain files that are mounted in writable
542- # partitions (examples being /var/log/dpkg.log and
543- # /var/log/apt/history.log).
544- #
545- # By considering all files, we increase the likelihood of a reboot
546- # but in doing avoid unexpected system behaviour (where a process is
547- # seeking through an old copy of a config file for example).
548- #
549- # Discard stderr to avoid the annoying
550- # 'lsof: WARNING can't stat() ...' messages.
551- cmd = 'lsof -lnPR 2>/dev/null'
552-
553- log.debug('running: {}'.format(cmd))
554-
555- output = subprocess.getoutput(cmd)
556-
557- # Hash showing which processes are using files that the
558- # upgrader needs to replace.
559- #
560- # key: filename.
561- # value: array of pids.
562- pid_file_map = {}
563-
564- # Read the lsof output. Note that we do _not_ ignore deleted files
565- # since in fact the files we are looking for have been deleted
566- # (unlinked, but not fully).
567- for line in output.split('\n'):
568- fields = line.split()
569-
570- # ignore header
571- if line.startswith('COMMAND'):
572- continue
573-
574- # ignore files with no inode
575- if fields[5] in ('netlink', 'unknown'):
576- continue
577-
578- pid = int(fields[1])
579-
580- # Deleted files have one less field (size/offset).
581- if len(fields) == 9 and fields[4] == 'DEL':
582-
583- potential_inode = fields[7]
584-
585- if not potential_inode.isdigit():
586- continue
587-
588- inode = int(potential_inode)
589- file = fields[8]
590- else:
591- potential_inode = fields[8]
592-
593- if not potential_inode.isdigit():
594- continue
595-
596- inode = int(potential_inode)
597- file = fields[9]
598-
599- # ignore kernel threads
600- if file == '/':
601- continue
602-
603- # ignore anything that doesn't look like a file
604- if file[0] != '/':
605- continue
606-
607- # ignore files that don't relate to files the upgrade will
608- # change.
609- if inode not in unpack_inodes:
610- continue
611-
612- # create a hash of arrays / dict of lists
613- if file not in pid_file_map:
614- pid_file_map[file] = []
615-
616- pid_file_map[file].append(pid)
617-
618- return pid_file_map
619-
620-
621-class Systemd():
622- '''
623- Interface to systemd init daemon.
624-
625- The upgrader talks to systemd via its private socket since this
626- protects the upgrader...
627-
628- - from issues with a broken dbus-daemon (admittedly unlikely).
629-
630- - from getting disconnected from systemd should the dbus-daemon need
631- to be restarted by the upgrader if the dbus-daemon is holding inodes
632- open (much more likely to happen).
633-
634- - against changes in the format of systemd's command-line tooling
635- output (as happened when 'systemctl show' output changed between
636- systemd 208 and 215).
637-
638- '''
639-
640- SYSTEMD_BUS_NAME = 'org.freedesktop.systemd1'
641- SYSTEMD_MGR_INTERFACE = 'org.freedesktop.systemd1.Manager'
642- SYSTEMD_UNIT_INTERFACE = 'org.freedesktop.systemd1.Unit'
643- SYSTEMD_OBJECT_PATH = '/org/freedesktop/systemd1'
644-
645- SYSTEMD_PRIVATE_SOCKET = 'unix:path=/run/systemd/private'
646-
647- FREEDESKTOP_PROPERTIES = 'org.freedesktop.DBus.Properties'
648-
649- def __init__(self):
650- self.connection = \
651- dbus.connection.Connection(self.SYSTEMD_PRIVATE_SOCKET)
652- self.proxy = self.connection.get_object(self.SYSTEMD_BUS_NAME,
653- self.SYSTEMD_OBJECT_PATH)
654- self.properties = dbus.Interface(self.proxy,
655- self.FREEDESKTOP_PROPERTIES)
656-
657- self.interface = dbus.Interface(self.proxy,
658- self.SYSTEMD_MGR_INTERFACE)
659-
660- # method for handling service start/stop/restart
661- self.mode = 'replace'
662-
663- def version(self):
664- '''
665- Returns the currently running version of systemd.
666- '''
667- return self.properties.Get(self.SYSTEMD_MGR_INTERFACE, 'Version')
668-
669- def find_unit(self, pid):
670- '''
671- Find the systemd unit associated with @pid.
672-
673- Returns D-Bus object path for service associated with @pid,
674- or None.
675- '''
676- try:
677- return self.interface.GetUnitByPID(pid)
678- except:
679- return None
680-
681- def get_service(self, name):
682- '''
683- @name: D-Bus path for service.
684-
685- Return the unit associated with the specified @name.
686- '''
687- unit = self.connection.get_object(self.SYSTEMD_BUS_NAME,
688- name)
689- interface = dbus.Interface(unit, self.SYSTEMD_UNIT_INTERFACE)
690- return interface
691-
692- def stop_service(self, name):
693- '''
694- Stop the specified service.
695-
696- @name: D-Bus path for service.
697-
698- Returns: True on success, else False
699- '''
700-
701- log.debug('stopping service {}'.format(name))
702-
703- interface = self.get_service(name)
704-
705- try:
706- interface.Stop(self.mode)
707- return True
708- except:
709- return False
710-
711- def start_service(self, name):
712- '''
713- Start the specified service.
714-
715- @name: D-Bus path for service.
716-
717- Returns: True on success, else False
718- '''
719-
720- log.debug('starting service {}'.format(name))
721-
722- interface = self.get_service(name)
723-
724- try:
725- interface.Start(self.mode)
726- return True
727- except:
728- return False
729-
730- def restart_service(self, name):
731- '''
732- Restart the specified service.
733-
734- @name: D-Bus path for service
735- (for example, "/org/freedesktop/systemd1/unit/cron_2eservice").
736-
737- Returns: True on success, else False
738- '''
739- try:
740- interface = self.get_service(name)
741- interface.Restart(self.mode)
742- return True
743- except:
744- return False
745-
746- def stop_service_by_pid(self, pid):
747- '''
748- Stop service specified by @pid.
749- '''
750- unit = self.find_unit(pid)
751- self.stop_service(unit)
752-
753- # FIXME: should return new pid
754- def restart_service_by_pid(self, pid):
755- '''
756- Restart service specified by @pid.
757- '''
758- unit = self.find_unit(pid)
759- self.restart_service(unit)
760-
761- # FIXME:
762- #
763- # We should use D-Bus here, rather than calling the binary.
764- #
765- # However, even if we use D-Bus, currently the D-Bus logic will fail
766- # since after a restart, systemd will sever all D-Bus connections
767- # and even after closing the connection, deleting the
768- # appropriate objects, and recreating those objects, method calls
769- # (such as self.version()) hang for approximately 30 seconds and
770- # then fail with:
771- #
772- # dbus.exceptions.DBusException: org.freedesktop.DBus.Error.NoReply
773- #
774- def restart_manager(self):
775- '''
776- Cause systemd to re-exec itself.
777-
778- Returns: True on success, else False.
779- '''
780-
781- # FIXME:
782- #
783- # Don't attempt to restart for now to avoid disrupting the
784- # existing D-Bus connection.
785- log.error('FIXME: not restarting systemd')
786- return False
787-
788- log.debug('restarting systemd service manager')
789-
790- args = ['systemctl', 'daemon-reexec']
791- log.debug('running: {}'.format(args))
792- proc = subprocess.Popen(args,
793- stderr=subprocess.PIPE,
794- universal_newlines=True)
795- if proc.wait() != 0:
796- stderr = proc.communicate()[1]
797- log.error('failed to restart systemd ({}): {}'
798- .format(args, stderr))
799- return False
800-
801- return True
802-
803-
804 def tar_generator(tar, cache_dir, removed_files, root_dir):
805 '''
806 Generator function to handle extracting members from the system
807@@ -942,29 +419,7 @@
808
809 DIR_MODE = 0o750
810
811- def update_timestamp(self):
812- '''
813- Update the timestamp file to record the time the last upgrade
814- completed successfully.
815- '''
816- file = os.path.join(self.get_cache_dir(), self.TIMESTAMP_FILE)
817- open(file, 'w').close()
818-
819- def get_cache_dir(self):
820- '''
821- Returns the full path to the cache directory, which is used as a
822- scratch pad, for downloading new images to and bind mounting the
823- rootfs.
824- '''
825- return self.options.tmpdir \
826- if self.options.tmpdir \
827- else self.DEFAULT_CACHE_DIR
828-
829- def get_mount_target(self):
830- '''
831- Get the full path to the mount target directory.
832- '''
833- return os.path.join(self.get_cache_dir(), self.MOUNT_TARGET)
834+ MOUNTPOINT_CMD = "mountpoint"
835
836 def __init__(self, options, commands, remove_list):
837 """
838@@ -983,25 +438,6 @@
839 'update': self._cmd_update,
840 }
841
842- # Records why a reboot is required (or REBOOT_NONE
843- # if no reboot necessary)
844- self.reboot_reasons = Enum('REBOOT_REASON',
845- 'REBOOT_NONE ' +
846- 'REBOOT_BOOTME ' +
847- 'REBOOT_SERVICE ' +
848- 'REBOOT_OPEN_FILE ' +
849- 'REBOOT_NEW_IMAGE ')
850-
851- # List of recognised methods the upgrader supports
852- # to upgrade a system.
853- #
854- # If dual root filesystem partitions are found, the upgrader
855- # will upgrade to the "other" partition. Otherwise, an in-place
856- # upgrade will be applied.
857- self.upgrade_types = Enum('UPGRADE_TYPE',
858- 'UPGRADE_IN_PLACE ' +
859- 'UPGRADE_AB_PARTITIONS ')
860-
861 self.options = options
862
863 assert('root_dir' in self.options)
864@@ -1014,18 +450,8 @@
865 self.remove_list = remove_list
866 self.full_image = False
867
868- # path => inode map set by save_links()
869- self.file_map = {}
870-
871 self.lost_found = '/lost+found'
872
873- # Set on any of the following:
874- #
875- # - Failure to restart all services.
876- # - Failure to determine service associated with a pid.
877- # - System-image requested it ('bootme' flag).
878- self.reboot_reason = self.reboot_reasons.REBOOT_NONE
879-
880 self.removed_file = TAR_FILE_REMOVED_FILE
881
882 # Identify the directory the files the command file refers to
883@@ -1046,29 +472,29 @@
884 # Note: Only used by UPGRADE_IN_PLACE.
885 self.other_is_empty = False
886
887- def set_reboot_reason(self, reason):
888- '''
889- Set the reboot reason, if not already set. This ensures the
890- primary reason is retained.
891- '''
892- if self.reboot_reason != self.reboot_reasons.REBOOT_NONE:
893- # ignore
894- return
895-
896- self.reboot_reason == reason
897-
898- def determine_upgrade_type(self):
899-
900- self.current_rootfs_device = self.get_rootfs()
901-
902- # Determine what sort of upgrade will be performed
903- if uses_ab_partitions():
904- self.upgrade_type = self.upgrade_types.UPGRADE_AB_PARTITIONS
905- self.other_rootfs_device = self.get_other_rootfs()
906- self.rootfs_to_modify = self.other_rootfs_device
907- else:
908- self.upgrade_type = self.upgrade_types.UPGRADE_IN_PLACE
909- self.rootfs_to_modify = self.current_rootfs_device
910+ def update_timestamp(self):
911+ '''
912+ Update the timestamp file to record the time the last upgrade
913+ completed successfully.
914+ '''
915+ file = os.path.join(self.get_cache_dir(), self.TIMESTAMP_FILE)
916+ open(file, 'w').close()
917+
918+ def get_cache_dir(self):
919+ '''
920+ Returns the full path to the cache directory, which is used as a
921+ scratch pad, for downloading new images to and bind mounting the
922+ rootfs.
923+ '''
924+ return self.options.tmpdir \
925+ if self.options.tmpdir \
926+ else self.DEFAULT_CACHE_DIR
927+
928+ def get_mount_target(self):
929+ '''
930+ Get the full path to the mount target directory.
931+ '''
932+ return os.path.join(self.get_cache_dir(), self.MOUNT_TARGET)
933
934 def prepare(self):
935 '''
936@@ -1076,65 +502,23 @@
937 '''
938
939 target = self.get_mount_target()
940-
941- # Note that we need the stat of the mountpoint,
942- # *NOT* the device itself.
943- self.mountpoint_stat = os.stat(target)
944-
945- self.determine_upgrade_type()
946-
947- log.debug('system upgrade type is {}'.format(self.upgrade_type))
948- if self.options.debug > 1:
949- log.debug('current rootfs device is {}'
950- .format(self.current_rootfs_device))
951- log.debug('rootfs to update is {}'
952- .format(self.rootfs_to_modify))
953-
954- if self.options.force_inplace_upgrade:
955- self.upgrade_type = self.upgrade_types.UPGRADE_IN_PLACE
956- self.rootfs_to_modify = self.current_rootfs_device
957- log.debug('forcing upgrade type to be {}'
958- .format(self.upgrade_type))
959+ if subprocess.call([self.MOUNTPOINT_CMD, "-q", target]) != 0:
960+ raise Exception(
961+ "The {} directory is not a mountpoint".format(target))
962
963 if self.options.dry_run:
964 return
965
966- if self.options.clean_only:
967- self.cleanup_inodes()
968- return
969-
970 if self.options.root_dir != '/':
971 # Don't modify root when running in test mode
972 return
973
974- if self.upgrade_type != self.upgrade_types.UPGRADE_IN_PLACE:
975- return
976-
977- # Necessary since systemd makes the rootfs shared, which allows
978- # mount operations visible across all mount namespaces.
979- log.debug('making {} private'.format(self.options.root_dir))
980-
981- make_mount_private(self.options.root_dir)
982-
983- # Unmount all the bind mounts to avoid any possibility
984- # of writing to the writable partition.
985- mounts = get_writable_mounts()
986-
987- log.debug('unmounting writable partitions')
988-
989- lazy_unmount_specified(mounts)
990-
991- self.cleanup_inodes()
992-
993 def run(self):
994 '''
995 Execute the commands in the command file
996 '''
997 self.prepare()
998
999- if self.options.clean_only:
1000- return
1001-
1002 for cmdline in self.commands:
1003 cmdline = cmdline.strip()
1004
1005@@ -1155,10 +539,6 @@
1006 Final tidy-up.
1007 '''
1008
1009- if self.options.dry_run:
1010- self.show_outcome()
1011- return
1012-
1013 if self.options.leave_files:
1014 log.debug('not removing files')
1015 else:
1016@@ -1170,42 +550,8 @@
1017 if self.options.root_dir != '/':
1018 return
1019
1020- if self.upgrade_type == self.upgrade_types.UPGRADE_IN_PLACE:
1021- log.debug('remounting all writable partitions')
1022- mount_all()
1023-
1024- try:
1025- prefix = self.get_saved_link_dir_prefix()
1026- os.rmdir(prefix)
1027- except:
1028- # there must be remaining links, so leave them to be cleaned
1029- # up via the initramfs.
1030- pass
1031-
1032- self.show_outcome()
1033-
1034 self.update_timestamp()
1035
1036- if self.reboot_reason == self.reboot_reasons.REBOOT_NONE:
1037- return
1038-
1039- # A system with A/B partitions should not automatically reboot;
1040- # let snappy advise the admin that a reboot should be performed.
1041- if self.upgrade_type == \
1042- self.upgrade_types.UPGRADE_AB_PARTITIONS:
1043- return
1044-
1045- if self.options.no_reboot:
1046- log.warning('Not rebooting at user request')
1047- return
1048-
1049- reboot_delay = self.options.reboot_delay
1050- # give the admin a chance to see the message
1051- log.debug('Waiting for {} seconds before rebooting'
1052- .format(reboot_delay))
1053- sleep(reboot_delay)
1054- os.system('/sbin/reboot')
1055-
1056 def _cmd_format(self, args):
1057 try:
1058 target = args[0]
1059@@ -1265,9 +611,6 @@
1060
1061 self.remount_rootfs(writable=True)
1062
1063- if self.upgrade_type != self.upgrade_types.UPGRADE_AB_PARTITIONS:
1064- return
1065-
1066 if self.other_is_empty:
1067 # Copy current rootfs data to the other rootfs's blank
1068 # partition.
1069@@ -1300,11 +643,12 @@
1070 target = self.get_mount_target()
1071
1072 if writable:
1073+ root = subprocess.check_output(
1074+ ["findmnt", "-o", "SOURCE", "-n", target]).strip()
1075+
1076 # ro->rw so need to fsck first.
1077 unmount(target)
1078
1079- root = self.get_other_rootfs()
1080-
1081 # needs to be mounted writable, so check it first!
1082 fsck(root)
1083
1084@@ -1321,129 +665,6 @@
1085 # rw->ro so no fsck required.
1086 remount(target, "ro")
1087
1088- def get_rootfs(self):
1089- '''
1090- Returns the full path to the currently booted root partition.
1091- '''
1092- roots = get_root_partitions_by_label()
1093- return roots[0][1] if roots[0][2] else roots[1][1]
1094-
1095- def get_other_rootfs(self):
1096- '''
1097- Returns the full device path to the "other" partition for
1098- systems that have dual root filesystems (A/B partitions)
1099- '''
1100- roots = get_root_partitions_by_label()
1101- return roots[0][1] if not roots[0][2] else roots[1][1]
1102-
1103- def show_other_partition_details(self):
1104- '''
1105- Mount the other partition and dump the contents of the
1106- channel.ini file to stdout.
1107- '''
1108- self.determine_upgrade_type()
1109-
1110- if not uses_ab_partitions():
1111- log.error('System does not have dual root partitions')
1112- sys.exit(1)
1113-
1114- target = self.get_mount_target()
1115-
1116- file = os.path.normpath('{}/{}'
1117- .format(target,
1118- SYSTEM_IMAGE_CHANNEL_CONFIG))
1119-
1120- log.debug('Reading file from other partition: {}'.format(file))
1121-
1122- try:
1123- with open(file, 'r') as f:
1124- sys.stdout.write(f.read())
1125- except:
1126- # no output by default, denoting an error
1127- log.debug('Cannot find file on other partition: {}'
1128- .format(file))
1129-
1130- def show_outcome(self):
1131- '''
1132- Show the user whether a reboot is required and if so, why.
1133- '''
1134- log.info('System update completed for rootfs on {}'
1135- .format(self.rootfs_to_modify))
1136-
1137- if self.reboot_reason == self.reboot_reasons.REBOOT_NONE:
1138- log.info('System update completed - no reboot required')
1139- else:
1140- log.warning('System update requires a reboot to finalise')
1141- log.warning('(Reboot reason: {})'
1142- .format(self.reboot_reason))
1143-
1144- def get_saved_link_path(self, file):
1145- '''
1146- Returns the full path to the hard-link copy of the inode
1147- associated with @file.
1148- '''
1149- prefix = self.get_saved_link_dir_prefix()
1150- link_path = os.path.normpath('{}/{}'.format(prefix, file))
1151-
1152- return link_path
1153-
1154- def get_saved_link_dir(self, file):
1155- '''
1156- Returns the full path to the directory where the hard-link inode
1157- copy for file @file is located.
1158- '''
1159- prefix = self.get_saved_link_dir_prefix()
1160- dirname = os.path.dirname(file)
1161- saved_link_dirname = os.path.normpath('{}{}'.format(prefix, dirname))
1162-
1163- return saved_link_dirname
1164-
1165- def get_saved_link_dir_prefix(self):
1166- '''
1167- Returns the full path to the directory under which the
1168- hard-link inode file copies are stored.
1169- '''
1170- self.make_lost_and_found()
1171-
1172- return os.path.join(self.lost_found,
1173- 'ubuntu-core-upgrader')
1174-
1175- def make_lost_and_found(self):
1176- '''
1177- Create a lost+found directory on the root filesystem.
1178- '''
1179- if os.path.exists(self.lost_found) and \
1180- os.path.isdir(self.lost_found) and \
1181- not os.path.islink(self.lost_found):
1182- return
1183-
1184- cwd = os.getcwd()
1185- os.chdir('/')
1186-
1187- if os.path.islink(self.lost_found) or os.path.isfile(self.lost_found):
1188- os.remove(self.lost_found)
1189-
1190- cmd = 'mklost+found'
1191- args = [cmd]
1192- proc = subprocess.Popen(args,
1193- stderr=subprocess.PIPE,
1194- universal_newlines=True)
1195- if proc.wait() != 0:
1196- stderr = proc.communicate()[1]
1197- log.error('failed to run {}: {}'.format(cmd, stderr))
1198- sys.exit(1)
1199-
1200- os.chdir(cwd)
1201-
1202- # create a subdirectory to work in
1203- dir = os.path.join(self.lost_found, 'ubuntu-core-upgrader')
1204- try:
1205- os.makedirs(dir, mode=self.DIR_MODE, exist_ok=True)
1206- except Exception as e:
1207- log.error('cannot create directory {}: {}'
1208- .format(dir, e))
1209- sys.exit(1)
1210-
1211 def get_file_contents(self, tar, file):
1212 '''
1213 @tar: tarfile object.
1214@@ -1467,188 +688,6 @@
1215
1216 return lines
1217
1218- def cleanup_inodes(self):
1219- '''
1220- Remove stale inodes from below '/lost+found'.
1221-
1222- If the upgrader has already been run and forced a reboot
1223- due to unknown processes holding open inodes, hard links will
1224- still exist below '/lost+found'.
1225-
1226- These need to be removed before the current upgrade process can
1227- continue since it the next upgrade may affect the same files as
1228- last time.
1229-
1230- Cleanup up via the upgrader isn't ideal since inodes are being
1231- wasted in the time window between calls to the upgrader.
1232- However, the only other alternatives are to perform the
1233- cleanup at boot (initramfs) or shutdown and these options are
1234- not ideal as problems could result in an unbootable system (they
1235- would also require yet more toggling of the root FS to rw and we
1236- try to avoid that wherever possible).
1237- '''
1238- # Create the directory the file needs to live under
1239- path = self.get_saved_link_dir_prefix()
1240-
1241- if os.path.exists(path):
1242- log.debug('Cleaning up stale inodes from previous run')
1243- shutil.rmtree(path)
1244-
1245- def save_links(self, files):
1246- '''
1247- @files: list of files that will be modified by the
1248- upgrade.
1249-
1250- Create a new (hard) link to all files that will be modified by
1251- the system-image upgrade.
1252-
1253- Also updates the file_map which maps the paths for the files to
1254- change to their current inode.
1255-
1256- We cannot just remove files from the root partition blindly
1257- since the chances are some of the files we wish to remove are
1258- currently being used by running processes (either directly via
1259- open(2) calls, or indirectly via the link loader pulling in
1260- required shared libraries).
1261-
1262- Technically, we *can* remove any file, but we will never be able
1263- to make the filesystem read-only again as it is now in an
1264- inconsistent state (since the unlinked inodes are still in use).
1265- This manifests itself with the dreaded 'mount: / busy' message
1266- when attempting to flip the rootfs to be read-only once again.
1267-
1268- The solution is to hard-link all the files that we are about to
1269- either change or delete into '/lost+found/', retaining their
1270- original directory structure. We can then unlink the master copy,
1271- but retain another copy of the same inode.
1272-
1273- The kernel is then happy to allow us to remount the rootfs
1274- read-only once again.
1275- '''
1276-
1277- for file in files:
1278-
1279- if file.startswith('/dev/') and not os.path.exists(file):
1280- continue
1281-
1282- if file.startswith('/run/'):
1283- continue
1284-
1285- if not os.path.exists(file):
1286- # ignore files that don't exist - they must be new files
1287- # that are about to be created by the new system image.
1288- continue
1289-
1290- st = os.stat(file)
1291- mode = st.st_mode
1292-
1293- if stat.S_ISDIR(mode):
1294- # ignore directories (and sym-links to directories)
1295- continue
1296-
1297- if stat.S_ISCHR(mode) or stat.S_ISBLK(mode):
1298- # ignore devices
1299- continue
1300-
1301- # save original inode number
1302- self.file_map[file] = st.st_ino
1303-
1304- if self.options.dry_run:
1305- continue
1306-
1307- # File is crosses the filesytem boundary,
1308- # so can only be ignored.
1309- #
1310- # It will either be a sym-link or a bind-mount
1311- # unrelated to those specified by writable-paths(5).
1312- if st.st_dev != self.mountpoint_stat.st_dev:
1313- if self.options.debug > 1:
1314- log.debug('Ignoring cross-FS file: {}'
1315- .format(file))
1316- continue
1317-
1318- # Create the directory the file needs to live under
1319- saved_link_dirname = self.get_saved_link_dir(file)
1320-
1321- # First, create the directory structure.
1322- #
1323- # Note that we cannot handle the deletion of directories that
1324- # a process has open (readdir(2)) as you can't create a hard
1325- # link to a directory.
1326- #
1327- # FIXME: consider:
1328- # - sym links to files.
1329- # - sym links to directories.
1330- #
1331- try:
1332- if self.options.debug > 1:
1333- log.debug('creating directory {}'
1334- .format(saved_link_dirname))
1335- os.makedirs(saved_link_dirname,
1336- mode=self.DIR_MODE,
1337- exist_ok=True)
1338- except Exception as e:
1339- log.error('failed to create directory {}: {}'
1340- .format(saved_link_dirname, e))
1341- sys.exit(1)
1342-
1343- link_path = self.get_saved_link_path(file)
1344-
1345- # Now, create a hard link to the original file in the
1346- # directory.
1347- log.debug('linking {} to {}'.format(file, link_path))
1348-
1349- try:
1350- os.link(file, link_path)
1351- except Exception as e:
1352- log.error('failed to create link for file {}: {}'
1353- .format(file, e))
1354- sys.exit(1)
1355-
1356- def remove_links(self, exclude_list):
1357- '''
1358- @exclude_list: list of files whose hard links below /lost+found should
1359- NOT be removed. Note the each element is a full rootfs path.
1360-
1361- Remove all hard links to inodes below /lost+found except those
1362- specified by the files in "exclude_list.
1363- '''
1364-
1365- if self.options.dry_run:
1366- return
1367-
1368- prefix = self.get_saved_link_dir_prefix()
1369-
1370- # Remove all the files that are not in @exclude_list
1371- for root, dirs, files in os.walk(self.get_saved_link_dir_prefix(),
1372- topdown=False):
1373- for file in files:
1374- link_path = os.path.join(root, file)
1375-
1376- path = link_path[len(prefix):]
1377-
1378- if path in exclude_list:
1379- log.debug('not removing in-use file {}'
1380- .format(file))
1381- continue
1382-
1383- log.debug('removing link {}'.format(link_path))
1384- os.unlink(link_path)
1385-
1386- # Now, remove all the directories we can.
1387- # We KISS by just attempting to remove every directory. Removals
1388- # will fail if the directory is not empty (as will happen when
1389- # they contain a link to a file that is still in use), so we
1390- # ignore such errors but any other is fatal.
1391- for root, dirs, files in os.walk(self.get_saved_link_dir_prefix(),
1392- topdown=False):
1393- for dir in dirs:
1394- try:
1395- os.rmdir(os.path.join(root, dir))
1396- except OSError as e:
1397- if e.errno != errno.ENOTEMPTY:
1398- raise e
1399-
1400 def _cmd_update(self, args):
1401 '''
1402 Unpack a new system image.
1403@@ -1700,35 +739,7 @@
1404 else:
1405 to_remove = []
1406
1407- if self.upgrade_type == self.upgrade_types.UPGRADE_IN_PLACE:
1408- # Add all files to remove not already in the list of all
1409- # files that will be affected by the upgrade. This overall
1410- # list allows the upgrader to backup files in the case of
1411- # in-place upgrades.
1412- for f in to_remove:
1413-
1414- system_path = remove_prefix(f)
1415-
1416- # path should now be absolute
1417- if not system_path.startswith('/'):
1418- continue
1419-
1420- # ignore relative paths
1421- if '../' in system_path:
1422- continue
1423-
1424- if system_path not in all_files:
1425- all_files.append(f)
1426-
1427- log.debug('saving inode details')
1428- self.save_links(all_files)
1429-
1430- if self.upgrade_type == self.upgrade_types.UPGRADE_AB_PARTITIONS:
1431- # An update to the "other" partition should always flag
1432- # a reboot since the image on that partition is newer.
1433- self.reboot_reason = self.reboot_reasons.REBOOT_NEW_IMAGE
1434-
1435- if not self.full_image and not self.options.check_reboot:
1436+ if not self.full_image:
1437
1438 if found_removed_file:
1439 log.debug('processing {} file'
1440@@ -1783,9 +794,8 @@
1441 .format(final, e))
1442
1443 if self.options.dry_run:
1444- if not self.options.check_reboot:
1445- log.info('DRY-RUN: would apply the following files:')
1446- tar.list(verbose=True)
1447+ log.info('DRY-RUN: would apply the following files:')
1448+ tar.list(verbose=True)
1449 else:
1450 log.debug('starting unpack')
1451 # see bug #1408579, this forces tarfile to use tarinfo.gid
1452@@ -1801,42 +811,6 @@
1453 self.options.root_dir))
1454 tar.close()
1455
1456- if self.upgrade_type == self.upgrade_types.UPGRADE_AB_PARTITIONS:
1457- # Nothing further to do
1458- return
1459-
1460- # Look for pids that still have the original inodes for the old
1461- # file versions open.
1462- self.pid_file_map = get_affected_pids(list(self.file_map.values()))
1463-
1464- if len(self.pid_file_map):
1465- log.debug('processes are using new image files from {}'
1466- .format(file))
1467- self.set_reboot_reason(self.reboot_reasons.REBOOT_OPEN_FILE)
1468- else:
1469- log.debug('no processes are using new image files from {}'
1470- .format(file))
1471-
1472- # Remove what hard links we can (namely all those that do not
1473- # relate to running processes that have the files open).
1474- if self.upgrade_type == self.upgrade_types.UPGRADE_IN_PLACE:
1475- self.remove_links(list(self.pid_file_map.keys()))
1476-
1477- if len(self.pid_file_map.keys()) == 0:
1478- # there were no processes holding inodes open, so nothing
1479- # more to be done.
1480- return
1481-
1482- if self.options.check_reboot:
1483- return
1484-
1485- if not self.restart_associated_services():
1486- self.set_reboot_reason(self.reboot_reasons.REBOOT_SERVICE)
1487- else:
1488- # if all services were restarted, all remaining hard-links can
1489- # be removed, hence pass an empty exclude list.
1490- self.remove_links([])
1491-
1492 def sync_partitions(self):
1493 '''
1494 Copy all rootfs data from the current partition to the other
1495@@ -1873,61 +847,3 @@
1496 unmount(bindmount_rootfs_dir)
1497 os.rmdir(bindmount_rootfs_dir)
1498 self.other_is_empty = False
1499-
1500- def restart_associated_service(self, pid, file):
1501- '''
1502- Lookup the service associated with @pid and restart.
1503-
1504- Returns True on success and False if either service could not be
1505- established, or the restart failed.
1506- '''
1507-
1508- if pid == 1:
1509- # As always, this is special :)
1510- if self.options.dry_run:
1511- log.info('DRY-RUN: would restart systemd (pid {})'
1512- .format(pid))
1513- return True
1514- else:
1515- log.debug('restarting systemd (pid {})' .format(pid))
1516- return self.systemd.restart_manager()
1517- else:
1518- service = self.systemd.find_unit(pid)
1519- if not service:
1520- log.debug('cannot determine service for pid {}'
1521- .format(pid))
1522- # trigger a reboot
1523- return False
1524-
1525- if self.options.dry_run:
1526- log.info('DRY-RUN: would restart service {} (pid {})'
1527- .format(service, pid))
1528- return True
1529- else:
1530- cmd = get_command(pid) or '<<UNKNOWN>>'
1531- log.debug('restarting service {} ({}, pid {}) '
1532- 'holding file {} open'
1533- .format(service, cmd, pid, file))
1534-
1535- return self.systemd.restart_service(service)
1536-
1537- def restart_associated_services(self):
1538- '''
1539- Restart all services associated with the pids in self.pid_file_map.
1540-
1541- Returns False if any service failed to start, else True.
1542- '''
1543- failed = False
1544-
1545- self.systemd = Systemd()
1546-
1547- log.debug('System is using systemd version {}'
1548- .format(self.systemd.version()))
1549-
1550- for file in self.pid_file_map:
1551- for pid in self.pid_file_map[file]:
1552- ret = self.restart_associated_service(pid, file)
1553- if not ret:
1554- failed = True
1555-
1556- return not failed

Subscribers

People subscribed via source and target branches

to all changes: