Merge lp:~brian-murray/click/chroots into lp:click

Proposed by Brian Murray
Status: Merged
Merged at revision: 303
Proposed branch: lp:~brian-murray/click/chroots
Merge into: lp:click
Diff against target: 377 lines (+368/-0)
2 files modified
click/chroot.py (+277/-0)
click/commands/chroot.py (+91/-0)
To merge this branch: bzr merge lp:~brian-murray/click/chroots
Reviewer Review Type Date Requested Status
Colin Watson Approve
Review via email: mp+188879@code.launchpad.net

Description of the change

This branch adds chroot (using schroot) management and creation to click.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

Thanks! I'm looking over this; I won't finish until tomorrow. In the meantime, do you think that instead of this pattern:

  subprocess.check_output(command).decode('utf-8')

... you could use this pattern:

  subprocess.check_output(command, universal_newlines=True)

I use this elsewhere because it tends to be a better match to other Python 2 code (click still supports Python 2.7).

lp:~brian-murray/click/chroots updated
293. By Brian Murray

call subprocess.check_output with universal_newlines=True

Revision history for this message
Colin Watson (cjwatson) wrote :
Download full text (4.2 KiB)

 review needs-fixing

This looks very nearly there; thanks a lot! There are just a few
details to clean up.

On Wed, Oct 02, 2013 at 04:13:25PM -0000, Brian Murray wrote:
> + proxy = subprocess.check_output(
> + ["apt-config", "shell", "x", "Acquire::HTTP::Proxy"]
> + ).decode('utf-8').replace('x=', '').strip()

  $ apt-config shell x Dir
  x='/'

That implies quoting may need to be involved here. Using python-apt
would be cleaner, but is an extra dependency. Hmm. How about something
like this instead:

  proxy = subprocess.check_output(
      'unset x; eval "$(apt-config shell x Acquire::HTTP::Proxy)"; echo "$x"',
      shell=True, universal_newlines=True).strip()

? I've certainly written cleaner code ... but this seems to avoid
potential quoting problems.

> + build_pkgs = [
> + "build-essential", "fakeroot",
> + "apt-utils", "pkg-create-dbgsym",
> + "pkgbinarymangler", "g++-%s" % target_tuple,
> + "pkg-config-%s" % target_tuple,
> + "dpkg-cross", "libc-dev:%s" % self.target_arch
> + ]

Let's drop pkg-create-dbgsym and pkgbinarymangler; we don't need them
for click packages.

> + shutil.copy("/etc/localtime", "%s/etc/" % mount)
> + shutil.copy("/etc/timezone", "%s/etc/" % mount)

Nit: I'd use shutil.copy2. Also, /etc/localtime may be a symlink; check
whether shutil.copy2 preserves those, and if not you'll probably need
some special-casing.

> + chroot_config = "/etc/schroot/chroot.d/%s" % self.full_name
> + with open(chroot_config, "w") as target:
> + admin_groups = "sbuild,root"

The sbuild group shouldn't be involved here.

> + print("[%s]" % self.full_name, file=target)
> + print("description=Build chroot for click packages on %s" %
> + self.target_arch, file=target)
> + for group in ["groups", "root-groups", "source-root-users",
> + "source-root-groups"]:
> + print("%s=%s" % (group, admin_groups), file=target)

I realise this is translated from mk-sbuild, but I'd prefer not to add
things described as group names to source-root-users; even if they
happen to coincide now it's a bit of a trap for the future.

For this application, I would suggest that we want the calling user to
have full access, and also the root user for good measure (it could also

> + print("type=directory", file=target)
> + print("profile=sbuild", file=target)
> + print("union-type=overlayfs", file=target)
> + print("directory=%s" % mount, file=target)

Let's drop profile=sbuild. We don't need it here, and it has unhelpful
(for this application) differences from the default profile such as not
bind-mounting the user's home directory.

> + def run(self, *args):
> + if not self.exists():
> + raise ClickChrootException(
> + "Chroot %s does not exist" % self.full_name)
> + command = ["schroot", "-c", self.full_name, "--directory=/", "--"]

Why the --directory=/ here? I'm concerned that that will make it
difficult to do simple "run make in ...

Read more...

review: Needs Fixing
lp:~brian-murray/click/chroots updated
294. By Brian Murray

modifications based on reviewer feedback

295. By Brian Murray

modify how apt-config is used to check for a proxy

296. By Brian Murray

fix argparse imports

Revision history for this message
Brian Murray (brian-murray) wrote :
Download full text (5.0 KiB)

On Fri, Oct 04, 2013 at 08:45:46AM -0000, Colin Watson wrote:
> Review: Needs Fixing
>
> review needs-fixing
>
> This looks very nearly there; thanks a lot! There are just a few
> details to clean up.
>
> On Wed, Oct 02, 2013 at 04:13:25PM -0000, Brian Murray wrote:
> > + proxy = subprocess.check_output(
> > + ["apt-config", "shell", "x", "Acquire::HTTP::Proxy"]
> > + ).decode('utf-8').replace('x=', '').strip()
>
> $ apt-config shell x Dir
> x='/'
>
> That implies quoting may need to be involved here. Using python-apt
> would be cleaner, but is an extra dependency. Hmm. How about something
> like this instead:
>
> proxy = subprocess.check_output(
> 'unset x; eval "$(apt-config shell x Acquire::HTTP::Proxy)"; echo "$x"',
> shell=True, universal_newlines=True).strip()
>
> ? I've certainly written cleaner code ... but this seems to avoid
> potential quoting problems.

Changed.

>
> > + build_pkgs = [
> > + "build-essential", "fakeroot",
> > + "apt-utils", "pkg-create-dbgsym",
> > + "pkgbinarymangler", "g++-%s" % target_tuple,
> > + "pkg-config-%s" % target_tuple,
> > + "dpkg-cross", "libc-dev:%s" % self.target_arch
> > + ]
>
> Let's drop pkg-create-dbgsym and pkgbinarymangler; we don't need them
> for click packages.

Changed.

> > + shutil.copy("/etc/localtime", "%s/etc/" % mount)
> > + shutil.copy("/etc/timezone", "%s/etc/" % mount)
>
> Nit: I'd use shutil.copy2. Also, /etc/localtime may be a symlink; check
> whether shutil.copy2 preserves those, and if not you'll probably need
> some special-casing.

Done.

> > + chroot_config = "/etc/schroot/chroot.d/%s" % self.full_name
> > + with open(chroot_config, "w") as target:
> > + admin_groups = "sbuild,root"
>
> The sbuild group shouldn't be involved here.

Done.

> > + print("[%s]" % self.full_name, file=target)
> > + print("description=Build chroot for click packages on %s" %
> > + self.target_arch, file=target)
> > + for group in ["groups", "root-groups", "source-root-users",
> > + "source-root-groups"]:
> > + print("%s=%s" % (group, admin_groups), file=target)
>
> I realise this is translated from mk-sbuild, but I'd prefer not to add
> things described as group names to source-root-users; even if they
> happen to coincide now it's a bit of a trap for the future.
>
> For this application, I would suggest that we want the calling user to
> have full access, and also the root user for good measure (it could also

This seems to have been cut off.

> > + print("type=directory", file=target)
> > + print("profile=sbuild", file=target)
> > + print("union-type=overlayfs", file=target)
> > + print("directory=%s" % mount, file=target)
>
> Let's drop profile=sbuild. We don't need it here, and it has unhelpful
> (for this application) differences from the default profile such as not
> bind-mounting the user's home directory.

The sbuild profile contains a different nssdatabases profil...

Read more...

Revision history for this message
Colin Watson (cjwatson) wrote :

On Mon, Oct 07, 2013 at 09:00:45PM -0000, Brian Murray wrote:
> On Fri, Oct 04, 2013 at 08:45:46AM -0000, Colin Watson wrote:
> > I realise this is translated from mk-sbuild, but I'd prefer not to add
> > things described as group names to source-root-users; even if they
> > happen to coincide now it's a bit of a trap for the future.
> >
> > For this application, I would suggest that we want the calling user to
> > have full access, and also the root user for good measure (it could also
>
> This seems to have been cut off.

I'm not sure what I meant to say there. I don't think it was very
important.

> > > + def run(self, *args):
> > > + if not self.exists():
> > > + raise ClickChrootException(
> > > + "Chroot %s does not exist" % self.full_name)
> > > + command = ["schroot", "-c", self.full_name, "--directory=/", "--"]
> >
> > Why the --directory=/ here? I'm concerned that that will make it
> > difficult to do simple "run make in this directory"-type things, which
> > is a primary goal of this system.
> >
> > Perhaps this was a consequence of profile=sbuild, since not having your
> > home directory mounted would be a bit of a problem. If so, try dropping
> > this argument after dropping that profile.
>
> This is still an issue with my setup because of my encrypted home
> directory. Do you have any suggestions on how we may deal with users
> that also have one?

Maybe you need to bind-mount $HOME rather than /home?

Revision history for this message
Colin Watson (cjwatson) wrote :

I made various further improvements and merged this. Thanks a lot!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'click/chroot.py'
2--- click/chroot.py 1970-01-01 00:00:00 +0000
3+++ click/chroot.py 2013-10-07 20:57:16 +0000
4@@ -0,0 +1,277 @@
5+# Copyright (C) 2013 Canonical Ltd.
6+# Authors: Colin Watson <cjwatson@ubuntu.com>,
7+# Brian Murray <brian@ubuntu.com>
8+#
9+# This program is free software: you can redistribute it and/or modify
10+# it under the terms of the GNU General Public License as published by
11+# the Free Software Foundation; version 3 of the License.
12+#
13+# This program is distributed in the hope that it will be useful,
14+# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+# GNU General Public License for more details.
17+#
18+# You should have received a copy of the GNU General Public License
19+# along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
21+"""Chroot management for building Click packages."""
22+
23+from __future__ import print_function
24+
25+__metaclass__ = type
26+__all__ = [
27+ "ClickChroot",
28+ "ClickChrootException",
29+ ]
30+
31+
32+import os
33+import shutil
34+import stat
35+import subprocess
36+
37+
38+extra_packages = {
39+ "ubuntu-sdk-13.10": [
40+ "libqt5opengl5-dev:TARGET",
41+ "libqt5webkit5-dev:TARGET",
42+ "libqt5xmlpatterns5-dev:TARGET",
43+ "qt5-default:TARGET",
44+ "qt5-qmake:TARGET",
45+ "qtbase5-dev:TARGET",
46+ "qtdeclarative5-dev:TARGET",
47+ "qtquick1-5-dev:TARGET",
48+ "qtscript5-dev:TARGET",
49+ "qtsensors5-dev:TARGET",
50+ ],
51+ }
52+
53+
54+class ClickChrootException(Exception):
55+ pass
56+
57+
58+class ClickChroot:
59+ def __init__(self, series, target_arch, framework, name=None):
60+ if name is None:
61+ name = "click"
62+ self.series = series
63+ self.target_arch = target_arch
64+ self.framework = framework
65+ self.name = name
66+ self.native_arch = subprocess.check_output(
67+ ["dpkg", "--print-architecture"],
68+ universal_newlines=True).strip()
69+ self.chroots_dir = "/var/lib/schroot/chroots"
70+ # this doesn't work because we are running this under sudo
71+ if 'DEBOOTSTRAP_MIRROR' in os.environ:
72+ self.archive = os.environ['DEBOOTSTRAP_MIRROR']
73+ else:
74+ self.archive = "http://archive.ubuntu.com/ubuntu"
75+
76+ def _generate_sources(self, series, native_arch, target_arch, components):
77+ ports_mirror = "http://ports.ubuntu.com/ubuntu-ports"
78+ pockets = ['%s' % series]
79+ for pocket in ['updates', 'security']:
80+ pockets.append('%s-%s' % (series, pocket))
81+ sources = []
82+ if target_arch in ['armhf']:
83+ for pocket in pockets:
84+ sources.append("deb [arch=%s] %s %s %s" %
85+ (target_arch, ports_mirror, pocket, components))
86+ sources.append("deb-src %s %s %s" %
87+ (ports_mirror, pocket, components))
88+ if native_arch in ['i386', 'amd64']:
89+ for pocket in pockets:
90+ sources.append("deb [arch=%s] %s %s %s" %
91+ (native_arch, self.archive, pocket, components))
92+ sources.append("deb-src %s %s %s" %
93+ (self.archive, pocket, components))
94+ return sources
95+
96+ @property
97+ def full_name(self):
98+ return "%s-%s-%s-%s-%s" % (
99+ self.name, self.framework, self.series, self.native_arch,
100+ self.target_arch)
101+
102+ def exists(self):
103+ command = ["schroot", "-c", self.full_name, "-i"]
104+ with open("/dev/null", "w") as devnull:
105+ return subprocess.call(
106+ command, stdout=devnull, stderr=devnull) == 0
107+
108+ def create(self):
109+ if self.exists():
110+ raise ClickChrootException(
111+ "Chroot %s already exists" % self.full_name)
112+ components = ["main", "restricted", "universe", "multiverse"]
113+ mount = "%s/%s" % (self.chroots_dir, self.full_name)
114+ proxy = None
115+ if not proxy and "http_proxy" in os.environ:
116+ proxy = os.environ["http_proxy"]
117+ if not proxy:
118+ proxy = subprocess.check_output(
119+ 'unset x; eval "$(apt-config shell x Acquire::HTTP::Proxy)"; echo "$x"',
120+ shell=True, universal_newlines=True).strip()
121+ with open("/dev/null", "w") as devnull:
122+ target_tuple = subprocess.check_output(
123+ ["dpkg-architecture", "-a%s" % self.target_arch,
124+ "-qDEB_HOST_GNU_TYPE"], stderr=devnull,
125+ universal_newlines=True).strip()
126+ build_pkgs = [
127+ "build-essential", "fakeroot",
128+ "apt-utils", "g++-%s" % target_tuple,
129+ "pkg-config-%s" % target_tuple,
130+ "dpkg-cross", "libc-dev:%s" % self.target_arch
131+ ]
132+ for package in extra_packages.get(self.framework, []):
133+ package = package.replace(":TARGET", ":%s" % self.target_arch)
134+ build_pkgs.append(package)
135+ os.makedirs(mount)
136+ subprocess.check_call([
137+ "debootstrap",
138+ "--arch", self.native_arch,
139+ "--variant=buildd",
140+ "--components=%s" % ','.join(components),
141+ self.series,
142+ mount,
143+ self.archive
144+ ])
145+ sources = self._generate_sources(self.series, self.native_arch,
146+ self.target_arch,
147+ ' '.join(components))
148+ with open("%s/etc/apt/sources.list" % mount, "w") as sources_list:
149+ for line in sources:
150+ print(line, file=sources_list)
151+ shutil.copy2("/etc/localtime", "%s/etc/" % mount)
152+ shutil.copy2("/etc/timezone", "%s/etc/" % mount)
153+ chroot_config = "/etc/schroot/chroot.d/%s" % self.full_name
154+ with open(chroot_config, "w") as target:
155+ admin_groups = "root"
156+ print("[%s]" % self.full_name, file=target)
157+ print("description=Build chroot for click packages on %s" %
158+ self.target_arch, file=target)
159+ for group in ["groups", "root-groups", "source-root-users",
160+ "source-root-groups"]:
161+ print("%s=%s" % (group, admin_groups), file=target)
162+ print("type=directory", file=target)
163+ print("profile=default", file=target)
164+ print("# Not protocols or services see ", file=target)
165+ print("# debian bug 557730", file=target)
166+ print("setup.nssdatabases=sbuild/nssdatabases",
167+ file=target)
168+ print("union-type=overlayfs", file=target)
169+ print("directory=%s" % mount, file=target)
170+ daemon_policy = "%s/usr/sbin/policy-rc.d" % mount
171+ with open(daemon_policy, "w") as policy:
172+ print("#!/bin/sh", file=policy)
173+ print("while true; do", file=policy)
174+ print(' case "$1" in', file=policy)
175+ print(" -*) shift ;;", file=policy)
176+ print(" makedev) exit 0;;", file=policy)
177+ print(" x11-common) exit 0;;", file=policy)
178+ print(" *) exit 101;;", file=policy)
179+ print(" esac", file=policy)
180+ print("done", file=policy)
181+ os.remove("%s/sbin/initctl" % mount)
182+ os.symlink("%s/bin/true" % mount, "%s/sbin/initctl" % mount)
183+ finish_script = "%s/finish.sh" % mount
184+ with open(finish_script, 'w') as finish:
185+ print("#!/bin/bash", file=finish)
186+ print("set -e", file=finish)
187+ if proxy:
188+ print("mkdir -p /etc/apt/apt.conf.d", file=finish)
189+ print("cat > /etc/apt/apt.conf.d/99-click-chroot-proxy <<EOF",
190+ file=finish)
191+ print("// proxy settings copied by click-chroot", file=finish)
192+ print('Acquire { HTTP { Proxy "%s"; }; };' % proxy,
193+ file=finish)
194+ print("EOF", file=finish)
195+ print("# Configure target arch", file=finish)
196+ print("dpkg --add-architecture %s" % self.target_arch,
197+ file=finish)
198+ print("# Reload package lists", file=finish)
199+ print("apt-get update || true", file=finish)
200+ print("# Pull down signature requirements", file=finish)
201+ print("apt-get -y --force-yes install \
202+gnupg ubuntu-keyring", file=finish)
203+ print("# Reload package lists", file=finish)
204+ print("apt-get update || true", file=finish)
205+ print("# Disable debconf questions so that automated \
206+builds won't prompt", file=finish)
207+ print("echo set debconf/frontend Noninteractive | \
208+debconf-communicate", file=finish)
209+ print("echo set debconf/priority critical | \
210+debconf-communicate", file=finish)
211+ print("# Install basic build tool set to match buildd",
212+ file=finish)
213+ print("apt-get -y --force-yes install %s"
214+ % ' '.join(build_pkgs), file=finish)
215+ print("# Set up expected /dev entries", file=finish)
216+ print("if [ ! -r /dev/stdin ]; \
217+then ln -s /proc/self/fd/0 /dev/stdin; fi", file=finish)
218+ print("if [ ! -r /dev/stdout ]; \
219+then ln -s /proc/self/fd/1 /dev/stdout; fi", file=finish)
220+ print("if [ ! -r /dev/stderr ]; \
221+then ln -s /proc/self/fd/2 /dev/stderr; fi", file=finish)
222+ print("# Clean up", file=finish)
223+ print("rm /finish.sh", file=finish)
224+ print("apt-get clean", file=finish)
225+ os.chmod(finish_script, stat.S_IEXEC)
226+ command = ["/finish.sh"]
227+ self.maint(*command)
228+
229+ def run(self, *args):
230+ if not self.exists():
231+ raise ClickChrootException(
232+ "Chroot %s does not exist" % self.full_name)
233+ command = ["schroot", "-c", self.full_name, "--directory=/", "--"]
234+ command.extend(args)
235+ subprocess.check_call(command)
236+
237+ def maint(self, *args):
238+ command = [
239+ # directory is a workaround for the click or user's home directory
240+ # not existing
241+ "schroot", "-c", "source:%s" % self.full_name,
242+ "-u", "root", "--directory=/", "--",
243+ ]
244+ command.extend(args)
245+ subprocess.check_call(command)
246+
247+ def install(self, *pkgs):
248+ if not self.exists():
249+ raise ClickChrootException(
250+ "Chroot %s does not exist" % self.full_name)
251+ self.update()
252+ command = ["apt-get", "install", "--yes"]
253+ command.extend(pkgs)
254+ self.maint(*command)
255+ self.clean()
256+
257+ def clean(self):
258+ command = ["apt-get", "clean"]
259+ self.maint(*command)
260+
261+ def update(self):
262+ command = ["apt-get", "update", "--yes"]
263+ self.maint(*command)
264+
265+ def upgrade(self):
266+ if not self.exists():
267+ raise ClickChrootException(
268+ "Chroot %s does not exist" % self.full_name)
269+ self.update()
270+ command = ["apt-get", "dist-upgrade", "--yes"]
271+ self.maint(*command)
272+ self.clean()
273+
274+ def destroy(self):
275+ if not self.exists():
276+ raise ClickChrootException(
277+ "Chroot %s does not exist" % self.full_name)
278+ chroot_config = "/etc/schroot/chroot.d/%s" % self.full_name
279+ os.remove(chroot_config)
280+ mount = "%s/%s" % (self.chroots_dir, self.full_name)
281+ shutil.rmtree(mount)
282
283=== added file 'click/commands/chroot.py'
284--- click/commands/chroot.py 1970-01-01 00:00:00 +0000
285+++ click/commands/chroot.py 2013-10-07 20:57:16 +0000
286@@ -0,0 +1,91 @@
287+#! /usr/bin/python3
288+
289+# Copyright (C) 2013 Canonical Ltd.
290+# Author: Brian Murray <brian@ubuntu.com>
291+
292+# This program is free software: you can redistribute it and/or modify
293+# it under the terms of the GNU General Public License as published by
294+# the Free Software Foundation; version 3 of the License.
295+#
296+# This program is distributed in the hope that it will be useful,
297+# but WITHOUT ANY WARRANTY; without even the implied warranty of
298+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
299+# GNU General Public License for more details.
300+#
301+# You should have received a copy of the GNU General Public License
302+# along with this program. If not, see <http://www.gnu.org/licenses/>.
303+
304+"""Use and manage a Click chroot."""
305+
306+from __future__ import print_function
307+
308+from argparse import ArgumentParser, REMAINDER
309+
310+from click.chroot import ClickChroot
311+
312+
313+def create(args):
314+ ClickChroot("saucy", args.architecture,
315+ "ubuntu-sdk-13.10").create()
316+
317+
318+def install(args):
319+ packages = args.packages
320+ ClickChroot("saucy", args.architecture,
321+ "ubuntu-sdk-13.10").install(*packages)
322+
323+
324+def destroy(args):
325+ # ask for confirmation?
326+ ClickChroot("saucy", args.architecture,
327+ "ubuntu-sdk-13.10").destroy()
328+
329+
330+def execute(args):
331+ # not sure what to do wrt setting up the env
332+ program = args.program
333+ ClickChroot("saucy", args.architecture,
334+ "ubuntu-sdk-13.10").run(*program)
335+
336+
337+def upgrade(args):
338+ ClickChroot("saucy", args.architecture,
339+ "ubuntu-sdk-13.10").upgrade()
340+
341+
342+def run(argv):
343+ parser = ArgumentParser("click chroot")
344+ subparsers = parser.add_subparsers(
345+ description="management subcommands",
346+ help="valid commands")
347+ parser.add_argument(
348+ "-a", "--architecture", required=True,
349+ help="architecture for the chroot")
350+ create_parser = subparsers.add_parser(
351+ "create",
352+ help="create a chroot of the provided architecture")
353+ create_parser.set_defaults(func=create)
354+ destroy_parser = subparsers.add_parser(
355+ "destroy",
356+ help="destroy the chroot")
357+ destroy_parser.set_defaults(func=destroy)
358+ upgrade_parser = subparsers.add_parser(
359+ "upgrade",
360+ help="upgrade the chroot")
361+ upgrade_parser.set_defaults(func=upgrade)
362+ install_parser = subparsers.add_parser(
363+ "install",
364+ help="install packages in the chroot")
365+ install_parser.add_argument(
366+ "packages", nargs="+",
367+ help="packages to install")
368+ install_parser.set_defaults(func=install)
369+ execute_parser = subparsers.add_parser(
370+ "run",
371+ help="run a program in the chroot")
372+ execute_parser.add_argument(
373+ "program", nargs=REMAINDER,
374+ help="program to run with arguments")
375+ execute_parser.set_defaults(func=execute)
376+ args = parser.parse_args(argv)
377+ args.func(args)

Subscribers

People subscribed via source and target branches

to all changes: