Merge lp:~bregma/libertine/track-apt-progress into lp:libertine

Proposed by Stephen M. Webb
Status: Merged
Approved by: Christopher Townsend
Approved revision: 139
Merged at revision: 135
Proposed branch: lp:~bregma/libertine/track-apt-progress
Merge into: lp:libertine
Diff against target: 584 lines (+241/-204)
2 files modified
python/libertine/Libertine.py (+228/-200)
tools/libertine-container-manager (+13/-4)
To merge this branch: bzr merge lp:~bregma/libertine/track-apt-progress
Reviewer Review Type Date Requested Status
Christopher Townsend Approve
Review via email: mp+278984@code.launchpad.net

Commit message

refactor container code to deduplicate lx/chroot code and add enhanced progress reporting to package management

Description of the change

(1) Refactor some of the code in the Python3 libertine package to deduplicate code common to both.
(2) Add an (optional) enahnced verbose output from APT commands that can be used by a front end to give ongoing package installation/update/removeal feedback (to be added in a later MP).

This MP is mostly foundational prep work for a couple of later MPs that do actual visible changes to te software.

To post a comment you must log in.
Revision history for this message
Stephen M. Webb (bregma) wrote :

May as well mention this MP contains a couple of bugs that were fixed in the follow-on MP https://code.launchpad.net/~bregma/libertine/refactor-container-back-ends/+merge/278985 -- in particular, a missing space in the userdel command in the LXC back end and a missing wait while starting the LXC container.

Revision history for this message
Christopher Townsend (townsend) wrote :

Looks good. A couple of nitpicks inline.

138. By Stephen M. Webb

libertine-container-manager: fixed help message for --quiet option

139. By Stephen M. Webb

libertine-container-manager: fixed a pep8 nit

Revision history for this message
Christopher Townsend (townsend) wrote :

Ok, LGTM now.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'python/libertine/Libertine.py'
2--- python/libertine/Libertine.py 2015-11-23 20:04:30 +0000
3+++ python/libertine/Libertine.py 2015-12-01 12:40:06 +0000
4@@ -12,6 +12,8 @@
5 # You should have received a copy of the GNU General Public License along
6 # with this program. If not, see <http://www.gnu.org/licenses/>.
7
8+import abc
9+import contextlib
10 import crypt
11 import json
12 import lxc
13@@ -124,29 +126,6 @@
14 fd.write("s0_mode = 3")
15
16
17-def start_container_for_update(container):
18- if not container.running:
19- print("Starting the container...")
20- if not container.start():
21- print("Container failed to start")
22- return False
23- if not container.wait("RUNNING", 10):
24- print("Container failed to enter the RUNNING state")
25- return False
26-
27- if not container.get_ips(timeout=30):
28- print("Not able to connect to the network.")
29- return False
30-
31- container.attach_wait(lxc.attach_run_command,
32- ["umount", "/tmp/.X11-unix"])
33-
34- container.attach_wait(lxc.attach_run_command,
35- ["apt-get", "update"])
36-
37- return True
38-
39-
40 def lxc_container(container_id):
41 config_path = libertine.utils.get_libertine_containers_dir_path()
42 container = lxc.Container(container_id, config_path)
43@@ -154,18 +133,126 @@
44 return container
45
46
47-class LibertineLXC(object):
48+def apt_args_for_verbosity_level(verbosity):
49+ """
50+ Maps numeric verbosity levels onto APT command-line arguments.
51+
52+ :param verbosity: 0 is quiet, 1 is normal, 2 is incontinent
53+ """
54+ return {
55+ 0: '--quiet=2',
56+ 1: '--assume-yes',
57+ 2: '--quiet=1 --assume-yes --option APT::Status-Fd=1'
58+ }.get(verbosity, '')
59+
60+
61+def apt_command_prefix(verbosity):
62+ return '/usr/bin/apt ' + apt_args_for_verbosity_level(verbosity) + ' '
63+
64+
65+class BaseContainer(metaclass=abc.ABCMeta):
66+ """
67+ An abstract base container to provide common functionality for all
68+ concrete container types.
69+
70+ :param container_id: The machine-readable container name.
71+ """
72 def __init__(self, container_id):
73 self.container_id = container_id
74+
75+ def create_libertine_container(self, password=None, verbosity=1):
76+ pass
77+
78+ def destroy_libertine_container(self, verbosity=1):
79+ pass
80+
81+ def is_running(self):
82+ """
83+ Indicates if the container is 'running'. The definition of 'running'
84+ depends on the type of the container.
85+ """
86+ return False
87+
88+ def start_container(self):
89+ """
90+ Starts the container. To start the container means to put it into a
91+ 'running' state, the meaning of which depends on the type of the
92+ container.
93+ """
94+ pass
95+
96+ def stop_container(self):
97+ """
98+ Stops the container. The opposite of start_container().
99+ """
100+ pass
101+
102+ @abc.abstractmethod
103+ def run_in_container(self, command_string):
104+ """
105+ Runs a command inside the container context.
106+
107+ :param command_string: The command line to execute in the container context.
108+ """
109+ pass
110+
111+ def update_packages(self, verbosity=1):
112+ """
113+ Updates all packages installed in the container.
114+
115+ :param verbosity: the chattiness of the output on a range from 0 to 2
116+ """
117+ self.run_in_container(apt_command_prefix(verbosity) + 'update')
118+ self.run_in_container(apt_command_prefix(verbosity) + 'upgrade')
119+
120+ def install_package(self, package_name, verbosity=1):
121+ """
122+ Installs a named package in the container.
123+
124+ :param package_name: The name of the package as APT understands it.
125+ :param verbosity: the chattiness of the output on a range from 0 to 2
126+ """
127+ return self.run_in_container(apt_command_prefix(verbosity) + "install --no-install-recommends '" + package_name + "'") == 0
128+
129+
130+class LibertineLXC(BaseContainer):
131+ """
132+ A concrete container type implemented using an LXC container.
133+ """
134+
135+ def __init__(self, container_id):
136+ super().__init__(container_id)
137 self.container = lxc_container(container_id)
138 self.series = get_container_distro(container_id)
139
140+ def is_running(self):
141+ return self.container.running
142+
143+ def start_container(self):
144+ if not self.container.running:
145+ if not self.container.start():
146+ raise RuntimeError("Container failed to start")
147+ if not self.container.wait("RUNNING", 10):
148+ raise RuntimeError("Container failed to enter the RUNNING state")
149+
150+ if not self.container.get_ips(timeout=30):
151+ raise RuntimeError("Not able to connect to the network.")
152+
153+ self.run_in_container("umount /tmp/.X11-unix")
154+
155+ def stop_container(self):
156+ self.container.stop()
157+
158+ def run_in_container(self, command_string):
159+ cmd_args = shlex.split(command_string)
160+ return self.container.attach_wait(lxc.attach_run_command, cmd_args)
161+
162 def destroy_libertine_container(self):
163 if self.container.defined:
164 self.container.stop()
165 self.container.destroy()
166
167- def create_libertine_container(self, password=None):
168+ def create_libertine_container(self, password=None, verbosity=1):
169 if password is None:
170 return
171
172@@ -211,23 +298,21 @@
173 self.create_libertine_config()
174
175 if self.container.start():
176- self.container.attach_wait(lxc.attach_run_command,
177- ["userdel", "-r", "ubuntu"])
178-
179- self.container.attach_wait(lxc.attach_run_command,
180- ["useradd", "-u", str(user_id), "-p", crypt.crypt(password),
181- "-G", "sudo", str(username)])
182+ self.run_in_container("userdel-r ubuntu")
183+ self.run_in_container("useradd -u {} -p {} -G sudo {}".format(str(user_id), crypt.crypt(password), str(username)))
184+
185+ if verbosity == 1:
186+ print("Updating the contents of the container after creation...")
187+ self.update_packages(verbosity)
188+
189+ if verbosity == 1:
190+ print("Installing Compiz as the Xmir window manager...")
191+ self.install_package('compiz', verbosity)
192+ create_compiz_config(self.container.name)
193+
194+ self.container.stop()
195 else:
196- print("Container failed to start.")
197-
198- print("Updating the contents of the container after creation...")
199- self.update_libertine_container()
200-
201- print("Installing Compiz as the Xmir window manager...")
202- self.install_package('compiz')
203- create_compiz_config(self.container_id)
204-
205- self.container.stop()
206+ raise RuntimeError("Container failed to start.")
207
208 def create_libertine_config(self):
209 user_id = os.getuid()
210@@ -261,82 +346,35 @@
211 # Dump it all to disk
212 self.container.save_config()
213
214- def update_libertine_container(self):
215- # Update packages inside the LXC
216- stop_container = not self.container.running
217-
218- if not start_container_for_update(self.container):
219- return (False, "Container did not start")
220-
221- print("Updating packages inside the LXC...")
222-
223- self.container.attach_wait(lxc.attach_run_command,
224- ["apt-get", "dist-upgrade", "-y"])
225-
226- # Stopping the container
227- if stop_container:
228- self.container.stop()
229-
230- def install_package(self, package_name):
231- stop_container = False
232-
233- if not self.container.running:
234- if not start_container_for_update(self.container):
235- return (False, "Container did not start")
236- stop_container = True
237-
238- retval = self.container.attach_wait(lxc.attach_run_command,
239- ["apt-get", "install", "-y", package_name])
240-
241- if stop_container:
242- self.container.stop()
243-
244- if retval != 0:
245- return False
246-
247- return True
248-
249- def remove_package(self, package_name):
250- stop_container = False
251-
252- if not self.container.running:
253- if not start_container_for_update(self.container):
254- return (False, "Container did not start")
255- stop_container = True
256-
257- retval = self.container.attach_wait(lxc.attach_run_command,
258- ["apt-get", "remove", "-y", package_name])
259-
260- if stop_container:
261- self.container.stop()
262-
263- def search_package_cache(self, search_string):
264- stop_container = False
265-
266- if not self.container.running:
267- if not start_container_for_update(self.container):
268- return (False, "Container did not start")
269- stop_container = True
270-
271- retval = self.container.attach_wait(lxc.attach_run_command,
272- ["apt-cache", "search", search_string])
273-
274- if stop_container:
275- self.container.stop()
276-
277-
278-class LibertineChroot(object):
279+
280+class LibertineChroot(BaseContainer):
281+ """
282+ A concrete container type implemented using a plain old chroot.
283+ """
284+
285 def __init__(self, container_id):
286- self.container_id = container_id
287+ super().__init__(container_id)
288 self.series = get_container_distro(container_id)
289 self.chroot_path = libertine.utils.get_libertine_container_rootfs_path(container_id)
290 os.environ['FAKECHROOT_CMD_SUBST'] = '$FAKECHROOT_CMD_SUBST:/usr/bin/chfn=/bin/true'
291 os.environ['DEBIAN_FRONTEND'] = 'noninteractive'
292
293+ def run_in_container(self, command_string):
294+ cmd_args = shlex.split(command_string)
295+ if self.series == "trusty":
296+ proot_cmd = '/usr/bin/proot'
297+ if not os.path.isfile(proot_cmd) or not os.access(proot_cmd, os.X_OK):
298+ raise RuntimeError('executable proot not found')
299+ command_prefix = proot_cmd + " -b /usr/lib/locale -S " + self.chroot_path
300+ else:
301+ command_prefix = "fakechroot fakeroot chroot " + self.chroot_path
302+ args = shlex.split(command_prefix + ' ' + command_string)
303+ cmd = subprocess.Popen(args).wait()
304+
305 def destroy_libertine_container(self):
306 shutil.rmtree(self.chroot_path)
307
308- def create_libertine_container(self, password=None):
309+ def create_libertine_container(self, password=None, verbosity=1):
310 installed_release = self.series
311
312 # Create the actual chroot
313@@ -370,7 +408,8 @@
314 else:
315 archive = "deb http://archive.ubuntu.com/ubuntu "
316
317- print("Updating chroot's sources.list entries...")
318+ if verbosity == 1:
319+ print("Updating chroot's sources.list entries...")
320 with open(os.path.join(self.chroot_path, 'etc', 'apt', 'sources.list'), 'a') as fd:
321 fd.write(archive + installed_release + " universe\n")
322 fd.write(archive + installed_release + "-updates main\n")
323@@ -418,97 +457,58 @@
324 args = shlex.split(command_line)
325 cmd = subprocess.Popen(args).wait()
326
327- print("Updating the contents of the container after creation...")
328- self.update_libertine_container()
329-
330- self.install_package("libnss-extrausers")
331-
332- print("Installing Compiz as the Xmir window manager...")
333- self.install_package("compiz")
334+ if verbosity == 1:
335+ print("Updating the contents of the container after creation...")
336+ self.update_packages(verbosity)
337+ self.install_package("libnss-extrausers", verbosity)
338+
339+ if verbosity == 1:
340+ print("Installing Compiz as the Xmir window manager...")
341+ self.install_package("compiz", verbosity)
342 create_compiz_config(self.container_id)
343
344 # Check if the container was created as root and chown the user directories as necessary
345 chown_recursive_dirs(libertine.utils.get_libertine_container_userdata_dir_path(self.container_id))
346
347- def update_libertine_container(self):
348- if self.series == "trusty":
349- proot_cmd = '/usr/bin/proot'
350- if not os.path.isfile(proot_cmd) or not os.access(proot_cmd, os.X_OK):
351- raise RuntimeError('executable proot not found')
352- command_line = proot_cmd + " -b /usr/lib/locale -S " + self.chroot_path + " apt-get update"
353- else:
354- command_line = "fakechroot fakeroot chroot " + self.chroot_path + " /usr/bin/apt-get update"
355- args = shlex.split(command_line)
356- cmd = subprocess.Popen(args).wait()
357-
358- if self.series == "trusty":
359- proot_cmd = '/usr/bin/proot'
360- if not os.path.isfile(proot_cmd) or not os.access(proot_cmd, os.X_OK):
361- raise RuntimeError('executable proot not found')
362- command_line = proot_cmd + " -b /usr/lib/locale -S " + self.chroot_path + " apt-get dist-upgrade -y"
363- else:
364- command_line = "fakechroot fakeroot chroot " + self.chroot_path + " /usr/bin/apt-get dist-upgrade -y"
365- args = shlex.split(command_line)
366- cmd = subprocess.Popen(args).wait()
367-
368- def install_package(self, package_name):
369- if self.series == "trusty":
370- proot_cmd = '/usr/bin/proot'
371- if not os.path.isfile(proot_cmd) or not os.access(proot_cmd, os.X_OK):
372- raise RuntimeError('executable proot not found')
373- command_line = proot_cmd + " -b /usr/lib/locale -S " + self.chroot_path + " apt-get install -y " + package_name
374- else:
375- command_line = "fakechroot fakeroot chroot " + self.chroot_path + " /usr/bin/apt-get install -y " + package_name
376- args = shlex.split(command_line)
377- cmd = subprocess.Popen(args)
378- cmd.wait()
379-
380- if cmd.returncode != 0:
381- return False
382- else:
383- return True
384-
385- def remove_package(self, package_name):
386- command_line = "fakechroot fakeroot chroot " + self.chroot_path + " /usr/bin/apt-get remove -y " + package_name
387- args = shlex.split(command_line)
388- cmd = subprocess.Popen(args)
389- cmd.wait()
390-
391- def search_package_cache(self, search_string):
392- command_line = "fakechroot fakeroot chroot " + self.chroot_path + " /usr/bin/apt-cache search " + search_string
393- args = shlex.split(command_line)
394- cmd = subprocess.Popen(args)
395- cmd.wait()
396-
397-
398-class LibertineMock(object):
399+
400+class LibertineMock(BaseContainer):
401+ """
402+ A concrete mock container type. Used for unit testing.
403+ """
404 def __init__(self, container_id):
405- self.container_id = container_id
406-
407- def destroy_libertine_container(self):
408- return True
409-
410- def create_libertine_container(self, password=None):
411- return True
412-
413- def update_libertine_container(self):
414- return True
415-
416- def install_package(self, package_name):
417- return True
418-
419- def remove_package(self, package_name):
420- return True
421-
422- def search_package_cache(self, search_string):
423- return True
424+ super().__init__(container_id)
425+
426+ def run_in_container(self, command_string):
427+ return 0
428+
429+
430+class ContainerRunning(contextlib.ExitStack):
431+ """
432+ Helper object providing a running container context.
433+
434+ Starts the container running if it's not already running, and shuts it down
435+ when the context is destroyed if it was not running at context creation.
436+ """
437+ def __init__(self, container):
438+ super().__init__()
439+ if not container.is_running():
440+ container.start_container()
441+ self.callback(lambda: container.stop_container())
442
443
444 class LibertineContainer(object):
445 """
446 A sandbox for DEB-packaged X11-based applications.
447 """
448+
449 def __init__(self, container_id, container_type="lxc"):
450+ """
451+ Initializes the container object.
452+
453+ :param container_id: The machine-readable container name.
454+ :param container_type: One of the supported container types (lxc,
455+ chroot, mock).
456+ """
457 super().__init__()
458 if container_type == "lxc":
459 self.container = LibertineLXC(container_id)
460@@ -517,22 +517,50 @@
461 elif container_type == "mock":
462 self.container = LibertineMock(container_id)
463 else:
464- print("Unsupported container type %s" % container_type)
465+ raise RuntimeError("Unsupported container type %s" % container_type)
466
467 def destroy_libertine_container(self):
468+ """
469+ Destroys the container and releases all its system resources.
470+ """
471 self.container.destroy_libertine_container()
472
473- def create_libertine_container(self, password=None):
474- self.container.create_libertine_container(password)
475-
476- def update_libertine_container(self):
477- self.container.update_libertine_container()
478-
479- def install_package(self, package_name):
480- return self.container.install_package(package_name)
481-
482- def remove_package(self, package_name):
483- self.container.remove_package(package_name)
484+ def create_libertine_container(self, password=None, verbosity=1):
485+ """
486+ Creates the container.
487+ """
488+ self.container.create_libertine_container(password, verbosity)
489+
490+ def update_libertine_container(self, verbosity=1):
491+ """
492+ Updates the contents of the container.
493+ """
494+ with ContainerRunning(self.container):
495+ self.container.update_packages(verbosity)
496+
497+ def install_package(self, package_name, verbosity=1):
498+ """
499+ Installs a package in the container.
500+ """
501+ with ContainerRunning(self.container):
502+ return self.container.install_package(package_name, verbosity)
503+
504+ def remove_package(self, package_name, verbosity=1):
505+ """
506+ Removes a package from the container.
507+
508+ :param package_name: The name of the package to be removed.
509+ :param verbosity: The verbosity level of the progress reporting.
510+ """
511+ with ContainerRunning(self.container):
512+ self.container.run_in_container(apt_command_prefix(verbosity) + "remove '" + package_name + "'") == 0
513
514 def search_package_cache(self, search_string):
515- self.container.search_package_cache(search_string)
516+ """
517+ Searches the container's package cache for a named package.
518+
519+ :param search_string: the (regex) to use to search the package cache.
520+ The regex is quoted to sanitize it.
521+ """
522+ with ContainerRunning(self.container):
523+ self.container.run_in_container("/usr/bin/apt-cache search '" + search_string + "'")
524
525=== modified file 'tools/libertine-container-manager'
526--- tools/libertine-container-manager 2015-11-23 20:04:30 +0000
527+++ tools/libertine-container-manager 2015-12-01 12:40:06 +0000
528@@ -263,7 +263,7 @@
529
530 container = LibertineContainer(args.id, args.type)
531 update_container_install_status(args.id, "installing")
532- container.create_libertine_container(password)
533+ container.create_libertine_container(password, args.verbosity)
534 update_container_install_status(args.id, "ready")
535
536
537@@ -295,7 +295,7 @@
538 container = LibertineContainer(args.id, get_container_type(args.id))
539
540 update_package_install_status(args.id, args.package, "installing")
541- if not container.install_package(args.package):
542+ if not container.install_package(args.package, args.verbosity):
543 delete_package(args.id, args.package)
544 sys.exit(1)
545
546@@ -314,7 +314,7 @@
547 sys.exit(1)
548
549 container = LibertineContainer(args.id, get_container_type(args.id))
550- container.remove_package(args.package)
551+ container.remove_package(args.package, args.verbosity)
552
553 delete_package(args.id, args.package)
554
555@@ -338,7 +338,7 @@
556 args.id = get_default_container_id()
557
558 container = LibertineContainer(args.id, get_container_type(args.id))
559- container.update_libertine_container()
560+ container.update_libertine_container(args.verbosity)
561
562
563 def list(args):
564@@ -349,6 +349,12 @@
565
566 if __name__ == '__main__':
567 parser = argparse.ArgumentParser(description="Legacy X application support for Unity 8")
568+ parser.add_argument('-q', '--quiet',
569+ action='store_const', dest='verbosity', const=0,
570+ help=('do not print status updates on stdout'))
571+ parser.add_argument('-v', '--verbose',
572+ action='store_const', dest='verbosity', const=2,
573+ help=('extra verbose output'))
574 subparsers = parser.add_subparsers(dest="subparser_name")
575
576 # Handle the create command and its options
577@@ -434,4 +440,7 @@
578 parser_list.set_defaults(func=list)
579
580 args = parser.parse_args()
581+ if args.verbosity is None:
582+ args.verbosity = 1
583+
584 args.func(args)

Subscribers

People subscribed via source and target branches