]> git.siccegge.de Git - forks/vmdebootstrap.git/blob - vmdebootstrap
Specific support for wheezy networking.
[forks/vmdebootstrap.git] / vmdebootstrap
1 #! /usr/bin/python
2 # Copyright 2011-2013 Lars Wirzenius
3 # Copyright 2012 Codethink Limited
4 # Copyright 2014 Neil Williams <codehelp@debian.org>
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 import cliapp
20 import crypt
21 import logging
22 import os
23 import re
24 import shutil
25 import datetime
26 import subprocess
27 import tempfile
28 import time
29 from distro_info import DebianDistroInfo, UbuntuDistroInfo
30
31
32 __version__ = '0.8'
33
34 # pylint: disable=invalid-name,line-too-long,missing-docstring,too-many-branches
35
36
37 class VmDebootstrap(cliapp.Application): # pylint: disable=too-many-public-methods
38
39 def __init__(self, progname=None, version=__version__, description=None, epilog=None):
40 super(VmDebootstrap, self).__init__(progname, version, description, epilog)
41 self.remove_dirs = []
42 self.mount_points = []
43 self.debian_info = DebianDistroInfo()
44 self.ubuntu_info = UbuntuDistroInfo()
45
46 def add_settings(self):
47 default_arch = subprocess.check_output(
48 ["dpkg", "--print-architecture"]).strip()
49
50 self.settings.boolean(
51 ['verbose'], 'report what is going on')
52 self.settings.string(
53 ['image'], 'put created disk image in FILE',
54 metavar='FILE')
55 self.settings.bytesize(
56 ['size'],
57 'create a disk image of size SIZE (%default)',
58 metavar='SIZE',
59 default='1G')
60 self.settings.bytesize(
61 ['bootsize'],
62 'create boot partition of size SIZE (%default)',
63 metavar='BOOTSIZE',
64 default='0%')
65 self.settings.string(
66 ['boottype'],
67 'specify file system type for /boot/',
68 default='ext2')
69 self.settings.bytesize(
70 ['bootoffset'],
71 'Space to leave at start of the image for bootloader',
72 default='0')
73 self.settings.string(
74 ['part-type'],
75 'Partition type to use for this image',
76 default='msdos')
77 self.settings.string(
78 ['roottype'],
79 'specify file system type for /',
80 default='ext4')
81 self.settings.bytesize(
82 ['swap'],
83 'create swap space of size SIZE (min 256Mb)')
84 self.settings.string(
85 ['foreign'],
86 'set up foreign debootstrap environment using provided program (ie binfmt handler)')
87 self.settings.string(
88 ['variant'],
89 'select debootstrap variant it not using the default')
90 self.settings.boolean(
91 ['extlinux'],
92 'install extlinux?',
93 default=True)
94 self.settings.string(
95 ['tarball'],
96 "tar up the disk's contents in FILE",
97 metavar='FILE')
98 self.settings.string(
99 ['apt-mirror'],
100 'configure apt to use MIRROR',
101 metavar='URL')
102 self.settings.string(
103 ['mirror'],
104 'use MIRROR as package source (%default)',
105 metavar='URL',
106 default='http://http.debian.net/debian/')
107 self.settings.string(
108 ['arch'],
109 'architecture to use (%default)',
110 metavar='ARCH',
111 default=default_arch)
112 self.settings.string(
113 ['distribution'],
114 'release to use (%default)',
115 metavar='NAME',
116 default='stable')
117 self.settings.string_list(
118 ['package'],
119 'install PACKAGE onto system')
120 self.settings.string_list(
121 ['custom-package'],
122 'install package in DEB file onto system (not from mirror)',
123 metavar='DEB')
124 self.settings.boolean(
125 ['no-kernel'],
126 'do not install a linux package')
127 self.settings.string(
128 ['kernel-package'],
129 'install PACKAGE instead of the default kernel package',
130 metavar='PACKAGE')
131 self.settings.boolean(
132 ['enable-dhcp'],
133 'enable DHCP on eth0')
134 self.settings.string(
135 ['root-password'],
136 'set root password',
137 metavar='PASSWORD')
138 self.settings.boolean(
139 ['lock-root-password'],
140 'lock root account so they cannot login?')
141 self.settings.string(
142 ['customize'],
143 'run SCRIPT after setting up system',
144 metavar='SCRIPT')
145 self.settings.string(
146 ['hostname'],
147 'set name to HOSTNAME (%default)',
148 metavar='HOSTNAME',
149 default='debian')
150 self.settings.string_list(
151 ['user'],
152 'create USER with PASSWORD',
153 metavar='USER/PASSWORD')
154 self.settings.boolean(
155 ['serial-console'],
156 'configure image to use a serial console')
157 self.settings.string(
158 ['serial-console-command'],
159 'command to manage the serial console, appended to /etc/inittab (%default)',
160 metavar='COMMAND',
161 default='/sbin/getty -L ttyS0 115200 vt100')
162 self.settings.boolean(
163 ['sudo'],
164 'install sudo, and if user is created, add them to sudo group')
165 self.settings.string(
166 ['owner'],
167 'the user who will own the image when the build is complete.')
168 self.settings.boolean(
169 ['squash'],
170 'use squashfs on the final image.')
171 self.settings.boolean(
172 ['configure-apt'],
173 'Create an apt source based on the distribution and mirror selected.')
174 self.settings.boolean(
175 ['mbr'],
176 'Run install-mbr (default if extlinux used)')
177 self.settings.boolean(
178 ['grub'],
179 'Install and configure grub2 - disables extlinux.')
180 self.settings.boolean(
181 ['sparse'],
182 'Do not fill the image with zeros to keep a sparse disk image',
183 default=False)
184 self.settings.boolean(
185 ['pkglist'],
186 'Create a list of package names included in the image.')
187 self.settings.boolean(
188 ['no-acpid'],
189 'do not install the acpid package',
190 default=False)
191
192 def process_args(self, args): # pylint: disable=too-many-branches,too-many-statements
193 if not self.settings['image'] and not self.settings['tarball']:
194 raise cliapp.AppException(
195 'You must give disk image filename, or tarball filename')
196 if self.settings['image'] and not self.settings['size']:
197 raise cliapp.AppException(
198 'If disk image is specified, you must give image size.')
199 if not self.debian_info.valid(self.settings['distribution']):
200 if not self.ubuntu_info.valid(self.settings['distribution']):
201 raise cliapp.AppException(
202 '%s is not a valid Debian or Ubuntu suite or codename.'
203 % self.settings['distribution'])
204 rootdir = None
205 try:
206 rootdev = None
207 roottype = self.settings['roottype']
208 bootdev = None
209 boottype = None
210 if self.settings['image']:
211 self.create_empty_image()
212 self.partition_image()
213 if self.settings['mbr'] or self.settings['extlinux']:
214 self.install_mbr()
215 (rootdev, bootdev, swapdev) = self.setup_kpartx()
216 if self.settings['swap'] > 0:
217 self.message("Creating swap space")
218 self.runcmd(['mkswap', swapdev])
219 self.mkfs(rootdev, fstype=roottype)
220 rootdir = self.mount(rootdev)
221 if bootdev:
222 if self.settings['boottype']:
223 boottype = self.settings['boottype']
224 else:
225 boottype = 'ext2'
226 self.mkfs(bootdev, fstype=boottype)
227 bootdir = '%s/%s' % (rootdir, 'boot/')
228 os.mkdir(bootdir)
229 self.mount(bootdev, bootdir)
230 else:
231 rootdir = self.mkdtemp()
232 self.debootstrap(rootdir)
233 self.set_hostname(rootdir)
234 self.create_fstab(rootdir, rootdev, roottype, bootdev, boottype)
235 self.install_debs(rootdir)
236 self.cleanup_apt_cache(rootdir)
237 self.set_root_password(rootdir)
238 self.create_users(rootdir)
239 self.remove_udev_persistent_rules(rootdir)
240 self.setup_networking(rootdir)
241 if self.settings['configure-apt'] or self.settings['apt-mirror']:
242 self.configure_apt(rootdir)
243 self.customize(rootdir)
244 self.update_initramfs(rootdir)
245
246 if self.settings['image']:
247 if self.settings['grub']:
248 self.install_grub2(rootdev, rootdir)
249 elif self.settings['extlinux']:
250 self.install_extlinux(rootdev, rootdir)
251 self.append_serial_console(rootdir)
252 self.optimize_image(rootdir)
253 if self.settings['squash']:
254 self.squash()
255 if self.settings['pkglist']:
256 self.list_installed_pkgs(rootdir)
257
258 if self.settings['foreign']:
259 os.unlink('%s/usr/bin/%s' %
260 (rootdir, os.path.basename(self.settings['foreign'])))
261
262 if self.settings['tarball']:
263 self.create_tarball(rootdir)
264
265 if self.settings['owner']:
266 self.chown()
267 except BaseException as e:
268 self.message('EEEK! Something bad happened...')
269 if rootdir:
270 db_log = os.path.join(rootdir, 'debootstrap', 'debootstrap.log')
271 if os.path.exists(db_log):
272 shutil.copy(db_log, os.getcwd())
273 self.message(e)
274 self.cleanup_system()
275 raise
276 else:
277 self.cleanup_system()
278
279 def message(self, msg):
280 logging.info(msg)
281 if self.settings['verbose']:
282 print msg
283
284 def runcmd(self, argv, stdin='', ignore_fail=False, env=None, **kwargs):
285 logging.debug('runcmd: %s %s %s', argv, env, kwargs)
286 p = subprocess.Popen(argv, stdin=subprocess.PIPE,
287 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
288 env=env, **kwargs)
289 out, err = p.communicate(stdin)
290 if p.returncode != 0:
291 msg = 'command failed: %s\n%s\n%s' % (argv, out, err)
292 logging.error(msg)
293 if not ignore_fail:
294 raise cliapp.AppException(msg)
295 return out
296
297 def mkdtemp(self):
298 dirname = tempfile.mkdtemp()
299 self.remove_dirs.append(dirname)
300 logging.debug('mkdir %s', dirname)
301 return dirname
302
303 def mount(self, device, path=None):
304 if not path:
305 mount_point = self.mkdtemp()
306 else:
307 mount_point = path
308 self.message('Mounting %s on %s' % (device, mount_point))
309 self.runcmd(['mount', device, mount_point])
310 self.mount_points.append(mount_point)
311 logging.debug('mounted %s on %s', device, mount_point)
312 return mount_point
313
314 def create_empty_image(self):
315 self.message('Creating disk image')
316 self.runcmd(['qemu-img', 'create', '-f', 'raw',
317 self.settings['image'],
318 str(self.settings['size'])])
319
320 def partition_image(self):
321 """
322 Uses fat16 (msdos) partitioning by default, use part-type to change.
323 If bootoffset is specified, the first actual partition
324 starts at that offset to allow customisation scripts to
325 put bootloader images into the space, e.g. u-boot.
326 """
327 self.message('Creating partitions')
328 self.runcmd(['parted', '-s', self.settings['image'],
329 'mklabel', self.settings['part-type']])
330 partoffset = 0
331 bootsize = 0
332 extent = '100%'
333 swap = 256 * 1024 * 1024
334 if self.settings['swap'] > 0:
335 if self.settings['swap'] > swap:
336 swap = self.settings['swap']
337 else:
338 # minimum 256Mb as default qemu ram is 128Mb
339 logging.debug("Setting minimum 256Mb swap space")
340 extent = "%s%%" % int(100 * (self.settings['size'] - swap) / self.settings['size'])
341 if self.settings['bootoffset'] and self.settings['bootoffset'] is not '0':
342 # turn v.small offsets into something at least possible to create.
343 if self.settings['bootoffset'] < 1048576:
344 partoffset = 1
345 logging.info(
346 "Setting bootoffset %smib to allow for %s bytes",
347 partoffset, self.settings['bootoffset'])
348 else:
349 partoffset = self.settings['bootoffset'] / (1024 * 1024)
350 self.message("Using bootoffset: %smib %s bytes" % (partoffset, self.settings['bootoffset']))
351 if self.settings['bootsize'] and self.settings['bootsize'] is not '0%':
352 if self.settings['grub'] and not partoffset:
353 partoffset = 1
354 bootsize = self.settings['bootsize'] / (1024 * 1024)
355 bootsize += partoffset
356 self.message("Using bootsize %smib: %s bytes" % (bootsize, self.settings['bootsize']))
357 logging.debug("Starting boot partition at %sMb", bootsize)
358 self.runcmd(['parted', '-s', self.settings['image'],
359 'mkpart', 'primary', 'fat16', str(partoffset), str(bootsize)])
360 logging.debug("Starting root partition at %sMb", partoffset)
361 self.runcmd(['parted', '-s', self.settings['image'],
362 'mkpart', 'primary', str(bootsize), extent])
363 else:
364 self.runcmd(['parted', '-s', self.settings['image'],
365 'mkpart', 'primary', '0%', extent])
366 self.runcmd(['parted', '-s', self.settings['image'],
367 'set', '1', 'boot', 'on'])
368 if self.settings['swap'] > 0:
369 logging.debug("Creating swap partition")
370 self.runcmd(['parted', '-s', self.settings['image'],
371 'mkpart', 'primary', 'linux-swap', extent, '100%'])
372
373 def update_initramfs(self, rootdir):
374 cmd = os.path.join('usr', 'sbin', 'update-initramfs')
375 if os.path.exists(os.path.join(rootdir, cmd)):
376 self.message("Updating the initramfs")
377 self.runcmd(['chroot', rootdir, cmd, '-u'])
378
379 def install_mbr(self):
380 if os.path.exists("/sbin/install-mbr"):
381 self.message('Installing MBR')
382 self.runcmd(['install-mbr', self.settings['image']])
383 else:
384 msg = "mbr enabled but /sbin/install-mbr not found" \
385 " - please install the mbr package."
386 raise cliapp.AppException(msg)
387
388 def setup_kpartx(self):
389 bootindex = None
390 swapindex = None
391 out = self.runcmd(['kpartx', '-avs', self.settings['image']])
392 if self.settings['bootsize'] and self.settings['swap'] > 0:
393 bootindex = 0
394 rootindex = 1
395 swapindex = 2
396 parts = 3
397 elif self.settings['bootsize']:
398 bootindex = 0
399 rootindex = 1
400 parts = 2
401 elif self.settings['swap'] > 0:
402 rootindex = 0
403 swapindex = 1
404 parts = 2
405 else:
406 rootindex = 0
407 parts = 1
408 boot = None
409 swap = None
410 devices = [line.split()[2]
411 for line in out.splitlines()
412 if line.startswith('add map ')]
413 if len(devices) != parts:
414 msg = 'Surprising number of partitions - check output of losetup -a'
415 logging.debug("%s", self.runcmd(['losetup', '-a']))
416 logging.debug("%s: devices=%s parts=%s", msg, devices, parts)
417 raise cliapp.AppException(msg)
418 root = '/dev/mapper/%s' % devices[rootindex]
419 if self.settings['bootsize']:
420 boot = '/dev/mapper/%s' % devices[bootindex]
421 if self.settings['swap'] > 0:
422 swap = '/dev/mapper/%s' % devices[swapindex]
423 return root, boot, swap
424
425 def mkfs(self, device, fstype):
426 self.message('Creating filesystem %s' % fstype)
427 self.runcmd(['mkfs', '-t', fstype, device])
428
429 def suite_to_codename(self, distro):
430 suite = self.debian_info.codename(distro, datetime.date.today())
431 if not suite:
432 return distro
433 return suite
434
435 def was_oldstable(self, limit):
436 suite = self.suite_to_codename(self.settings['distribution'])
437 # this check is only for debian
438 if not self.debian_info.valid(suite):
439 return False
440 return suite == self.debian_info.old(limit)
441
442 def was_stable(self, limit):
443 suite = self.suite_to_codename(self.settings['distribution'])
444 # this check is only for debian
445 if not self.debian_info.valid(suite):
446 return False
447 return suite == self.debian_info.stable(limit)
448
449 def debootstrap(self, rootdir):
450 msg = "(%s)" % self.settings['variant'] if self.settings['variant'] else ''
451 self.message('Debootstrapping %s %s' % (self.settings['distribution'], msg))
452
453 include = self.settings['package']
454
455 if not self.settings['foreign'] and not self.settings['no-acpid']:
456 include.append('acpid')
457
458 if self.settings['grub']:
459 include.append('grub-pc')
460
461 if not self.settings['no-kernel']:
462 if self.settings['kernel-package']:
463 kernel_image = self.settings['kernel-package']
464 else:
465 if self.settings['arch'] == 'i386':
466 # wheezy (which became oldstable on 04/25/2015) used '486'
467 if self.was_oldstable(datetime.date(2015, 4, 26)):
468 kernel_arch = '486'
469 else:
470 kernel_arch = '586'
471 elif self.settings['arch'] == 'armhf':
472 kernel_arch = 'armmp'
473 else:
474 kernel_arch = self.settings['arch']
475 kernel_image = 'linux-image-%s' % kernel_arch
476 include.append(kernel_image)
477
478 if self.settings['sudo'] and 'sudo' not in include:
479 include.append('sudo')
480
481 args = ['debootstrap', '--arch=%s' % self.settings['arch']]
482
483 if self.settings['package']:
484 args.append(
485 '--include=%s' % ','.join(include))
486 if self.settings['foreign']:
487 args.append('--foreign')
488 if self.settings['variant']:
489 args.append('--variant')
490 args.append(self.settings['variant'])
491 args += [self.settings['distribution'],
492 rootdir, self.settings['mirror']]
493 logging.debug(" ".join(args))
494 self.runcmd(args)
495 if self.settings['foreign']:
496 # set a noninteractive debconf environment for secondstage
497 env = {
498 "DEBIAN_FRONTEND": "noninteractive",
499 "DEBCONF_NONINTERACTIVE_SEEN": "true",
500 "LC_ALL": "C"
501 }
502 # add the mapping to the complete environment.
503 env.update(os.environ)
504 # First copy the binfmt handler over
505 self.message('Setting up binfmt handler')
506 shutil.copy(self.settings['foreign'], '%s/usr/bin/' % rootdir)
507 # Next, run the package install scripts etc.
508 self.message('Running debootstrap second stage')
509 self.runcmd(['chroot', rootdir,
510 '/debootstrap/debootstrap', '--second-stage'],
511 env=env)
512
513 def set_hostname(self, rootdir):
514 hostname = self.settings['hostname']
515 with open(os.path.join(rootdir, 'etc', 'hostname'), 'w') as f:
516 f.write('%s\n' % hostname)
517
518 etc_hosts = os.path.join(rootdir, 'etc', 'hosts')
519 try:
520 with open(etc_hosts, 'r') as f:
521 data = f.read()
522 with open(etc_hosts, 'w') as f:
523 for line in data.splitlines():
524 if line.startswith('127.0.0.1'):
525 line += ' %s' % hostname
526 f.write('%s\n' % line)
527 except IOError:
528 pass
529
530 def create_fstab(self, rootdir, rootdev, roottype, bootdev, boottype): # pylint: disable=too-many-arguments
531 def fsuuid(device):
532 out = self.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
533 '-s', 'UUID', device])
534 return out.splitlines()[0].strip()
535
536 if rootdev:
537 rootdevstr = 'UUID=%s' % fsuuid(rootdev)
538 else:
539 rootdevstr = '/dev/sda1'
540
541 if bootdev:
542 bootdevstr = 'UUID=%s' % fsuuid(bootdev)
543 else:
544 bootdevstr = None
545
546 fstab = os.path.join(rootdir, 'etc', 'fstab')
547 with open(fstab, 'w') as f:
548 f.write('proc /proc proc defaults 0 0\n')
549 f.write('%s / %s errors=remount-ro 0 1\n' % (rootdevstr, roottype))
550 if bootdevstr:
551 f.write('%s /boot %s errors=remount-ro 0 2\n' % (bootdevstr, boottype))
552 if self.settings['swap'] > 0:
553 f.write("/dev/sda3 swap swap defaults 0 0\n")
554 elif self.settings['swap'] > 0:
555 f.write("/dev/sda2 swap swap defaults 0 0\n")
556
557 def install_debs(self, rootdir):
558 if not self.settings['custom-package']:
559 return
560 self.message('Installing custom packages')
561 tmp = os.path.join(rootdir, 'tmp', 'install_debs')
562 os.mkdir(tmp)
563 for deb in self.settings['custom-package']:
564 shutil.copy(deb, tmp)
565 filenames = [os.path.join('/tmp/install_debs', os.path.basename(deb))
566 for deb in self.settings['custom-package']]
567 out, err, _ = \
568 self.runcmd_unchecked(['chroot', rootdir, 'dpkg', '-i'] + filenames)
569 logging.debug('stdout:\n%s', out)
570 logging.debug('stderr:\n%s', err)
571 out = self.runcmd(['chroot', rootdir,
572 'apt-get', '-f', '--no-remove', 'install'])
573 logging.debug('stdout:\n%s', out)
574 shutil.rmtree(tmp)
575
576 def cleanup_apt_cache(self, rootdir):
577 out = self.runcmd(['chroot', rootdir, 'apt-get', 'clean'])
578 logging.debug('stdout:\n%s', out)
579
580 def set_root_password(self, rootdir):
581 if self.settings['root-password']:
582 self.message('Setting root password')
583 self.set_password(rootdir, 'root', self.settings['root-password'])
584 elif self.settings['lock-root-password']:
585 self.message('Locking root password')
586 self.runcmd(['chroot', rootdir, 'passwd', '-l', 'root'])
587 else:
588 self.message('Give root an empty password')
589 self.delete_password(rootdir, 'root')
590
591 def create_users(self, rootdir):
592 def create_user(user):
593 self.runcmd(['chroot', rootdir, 'adduser', '--gecos', user,
594 '--disabled-password', user])
595 if self.settings['sudo']:
596 self.runcmd(['chroot', rootdir, 'adduser', user, 'sudo'])
597
598 for userpass in self.settings['user']:
599 if '/' in userpass:
600 user, password = userpass.split('/', 1)
601 create_user(user)
602 self.set_password(rootdir, user, password)
603 else:
604 create_user(userpass)
605 self.delete_password(rootdir, userpass)
606
607 def set_password(self, rootdir, user, password):
608 encrypted = crypt.crypt(password, '..')
609 self.runcmd(['chroot', rootdir, 'usermod', '-p', encrypted, user])
610
611 def delete_password(self, rootdir, user):
612 self.runcmd(['chroot', rootdir, 'passwd', '-d', user])
613
614 def remove_udev_persistent_rules(self, rootdir):
615 self.message('Removing udev persistent cd and net rules')
616 for x in ['70-persistent-cd.rules', '70-persistent-net.rules']:
617 pathname = os.path.join(rootdir, 'etc', 'udev', 'rules.d', x)
618 if os.path.exists(pathname):
619 logging.debug('rm %s', pathname)
620 os.remove(pathname)
621 else:
622 logging.debug('not removing non-existent %s', pathname)
623
624 def setup_networking(self, rootdir):
625 self.message('Setting up networking')
626
627 # unconditionally write for wheezy (which became oldstable on 04/25/2015)
628 if self.was_oldstable(datetime.date(2015, 4, 26)):
629 with open(os.path.join(
630 rootdir, 'etc', 'network', 'interfaces'), 'w') as netfile:
631 netfile.write('source /etc/network/interfaces.d/*\n')
632 os.mkdir(os.path.join(rootdir, 'etc', 'network', 'interfaces.d'))
633
634 elif not os.path.exists(os.path.join(rootdir, 'etc', 'network', 'interfaces')):
635 with open(os.path.join(
636 rootdir, 'etc', 'network', 'interfaces'), 'w') as netfile:
637 netfile.write('source-directory /etc/network/interfaces.d\n')
638
639 with open(os.path.join(
640 rootdir, 'etc', 'network', 'interfaces.d', 'setup'), 'w') as eth:
641 eth.write('auto lo\n')
642 eth.write('iface lo inet loopback\n')
643
644 if self.settings['enable-dhcp']:
645 eth.write('\n')
646 eth.write('auto eth0\n')
647 eth.write('iface eth0 inet dhcp\n')
648
649 def append_serial_console(self, rootdir):
650 if self.settings['serial-console']:
651 serial_command = self.settings['serial-console-command']
652 logging.debug('adding getty to serial console')
653 inittab = os.path.join(rootdir, 'etc/inittab')
654 # to autologin, serial_command can contain '-a root'
655 with open(inittab, 'a') as f:
656 f.write('\nS0:23:respawn:%s\n' % serial_command)
657
658 # pylint: disable=no-self-use
659 def _grub_serial_console(self, rootdir):
660 cmdline = 'GRUB_CMDLINE_LINUX_DEFAULT="console=tty0 console=tty1 console=ttyS0,38400n8"'
661 terminal = 'GRUB_TERMINAL="serial gfxterm"'
662 command = 'GRUB_SERIAL_COMMAND="serial --speed=38400 --unit=0 --parity=no --stop=1"'
663 grub_cfg = os.path.join(rootdir, 'etc', 'default', 'grub')
664 logging.debug("Allowing serial output in grub config %s", grub_cfg)
665 with open(grub_cfg, 'a+') as cfg:
666 cfg.write("# %s serial support\n" % os.path.basename(__file__))
667 cfg.write("%s\n" % cmdline)
668 cfg.write("%s\n" % terminal)
669 cfg.write("%s\n" % command)
670
671 def install_grub2(self, rootdev, rootdir):
672 self.message("Configuring grub2")
673 # rely on kpartx using consistent naming to map loop0p1 to loop0
674 install_dev = os.path.join('/dev', os.path.basename(rootdev)[:-2])
675 self.runcmd(['mount', '/dev', '-t', 'devfs', '-obind',
676 '%s' % os.path.join(rootdir, 'dev')])
677 self.runcmd(['mount', '/proc', '-t', 'proc', '-obind',
678 '%s' % os.path.join(rootdir, 'proc')])
679 self.runcmd(['mount', '/sys', '-t', 'sysfs', '-obind',
680 '%s' % os.path.join(rootdir, 'sys')])
681 if self.settings['serial-console']:
682 self._grub_serial_console(rootdir)
683
684 try:
685 self.runcmd(['chroot', rootdir, 'update-grub'])
686 self.runcmd(['chroot', rootdir, 'grub-install', install_dev])
687 except cliapp.AppException:
688 self.message("Failed. Is grub2-common installed? Using extlinux.")
689 self.install_extlinux(rootdev, rootdir)
690 self.runcmd(['umount', os.path.join(rootdir, 'sys')])
691 self.runcmd(['umount', os.path.join(rootdir, 'proc')])
692 self.runcmd(['umount', os.path.join(rootdir, 'dev')])
693
694 def install_extlinux(self, rootdev, rootdir):
695 if not os.path.exists("/usr/bin/extlinux"):
696 self.message("extlinux not installed, skipping.")
697 return
698 self.message('Installing extlinux')
699
700 def find(pattern):
701 dirname = os.path.join(rootdir, 'boot')
702 basenames = os.listdir(dirname)
703 logging.debug('find: %s', basenames)
704 for basename in basenames:
705 if re.search(pattern, basename):
706 return os.path.join('boot', basename)
707 raise cliapp.AppException('Cannot find match: %s' % pattern)
708
709 try:
710 kernel_image = find('vmlinuz-.*')
711 initrd_image = find('initrd.img-.*')
712 except cliapp.AppException as e:
713 self.message("Unable to find kernel. Not installing extlinux.")
714 logging.debug("No kernel found. %s. Skipping install of extlinux.", e)
715 return
716
717 out = self.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
718 '-s', 'UUID', rootdev])
719 uuid = out.splitlines()[0].strip()
720
721 conf = os.path.join(rootdir, 'extlinux.conf')
722 logging.debug('configure extlinux %s', conf)
723 kserial = 'console=ttyS0,115200' if self.settings['serial-console'] else ''
724 extserial = 'serial 0 115200' if self.settings['serial-console'] else ''
725 msg = '''
726 default linux
727 timeout 1
728
729 label linux
730 kernel %(kernel)s
731 append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
732 %(extserial)s
733 ''' % {
734 'kernel': kernel_image, # pylint: disable=bad-continuation
735 'initrd': initrd_image, # pylint: disable=bad-continuation
736 'uuid': uuid, # pylint: disable=bad-continuation
737 'kserial': kserial, # pylint: disable=bad-continuation
738 'extserial': extserial, # pylint: disable=bad-continuation
739 } # pylint: disable=bad-continuation
740 logging.debug("extlinux config:\n%s", msg)
741
742 # python multiline string substitution is just ugly.
743 # use an external file or live with the mangling, no point in
744 # mangling the string to remove spaces just to keep it pretty in source.
745 f = open(conf, 'w')
746 f.write(msg)
747
748 self.runcmd(['extlinux', '--install', rootdir])
749 self.runcmd(['sync'])
750 time.sleep(2)
751
752 def optimize_image(self, rootdir):
753 """
754 Filing up the image with zeros will increase its compression rate
755 """
756 if not self.settings['sparse']:
757 zeros = os.path.join(rootdir, 'ZEROS')
758 self.runcmd_unchecked(['dd', 'if=/dev/zero', 'of=' + zeros, 'bs=1M'])
759 self.runcmd(['rm', '-f', zeros])
760
761 def squash(self):
762 """
763 Run squashfs on the image.
764 """
765 if not os.path.exists('/usr/bin/mksquashfs'):
766 logging.warning("Squash selected but mksquashfs not found!")
767 return
768 self.message("Running mksquashfs")
769 suffixed = "%s.squashfs" % self.settings['image']
770 self.runcmd(['mksquashfs', self.settings['image'],
771 suffixed,
772 '-no-progress', '-comp', 'xz'], ignore_fail=False)
773 os.unlink(self.settings['image'])
774 self.settings['image'] = suffixed
775
776 def cleanup_system(self):
777 # Clean up after any errors.
778
779 self.message('Cleaning up')
780
781 # Umount in the reverse mount order
782 if self.settings['image']:
783 for i in range(len(self.mount_points) - 1, -1, -1):
784 mount_point = self.mount_points[i]
785 try:
786 self.runcmd(['umount', mount_point], ignore_fail=False)
787 except cliapp.AppException:
788 logging.debug("umount failed, sleeping and trying again")
789 time.sleep(5)
790 self.runcmd(['umount', mount_point], ignore_fail=False)
791
792 self.runcmd(['kpartx', '-d', self.settings['image']], ignore_fail=True)
793
794 for dirname in self.remove_dirs:
795 shutil.rmtree(dirname)
796
797 def customize(self, rootdir):
798 script = self.settings['customize']
799 if not script:
800 return
801 if not os.path.exists(script):
802 example = os.path.join("/usr/share/vmdebootstrap/examples/", script)
803 if not os.path.exists(example):
804 self.message("Unable to find %s" % script)
805 return
806 script = example
807 self.message('Running customize script %s' % script)
808 logging.info("rootdir=%s image=%s", rootdir, self.settings['image'])
809 with open('/dev/tty', 'w') as tty:
810 try:
811 cliapp.runcmd([script, rootdir, self.settings['image']], stdout=tty, stderr=tty)
812 except IOError:
813 subprocess.call([script, rootdir, self.settings['image']])
814
815 def create_tarball(self, rootdir):
816 # Create a tarball of the disk's contents
817 # shell out to runcmd since it more easily handles rootdir
818 self.message('Creating tarball of disk contents')
819 self.runcmd(['tar', '-cf', self.settings['tarball'], '-C', rootdir, '.'])
820
821 def chown(self):
822 # Change image owner after completed build
823 if self.settings['image']:
824 filename = self.settings['image']
825 elif self.settings['tarball']:
826 filename = self.settings['tarball']
827 else:
828 return
829 self.message("Changing owner to %s" % self.settings["owner"])
830 subprocess.call(["chown", self.settings["owner"], filename])
831
832 def list_installed_pkgs(self, rootdir):
833 # output the list of installed packages for sources identification
834 self.message("Creating a list of installed binary package names")
835 out = self.runcmd(['chroot', rootdir,
836 'dpkg-query', '-W', "-f='${Package}.deb\n'"])
837 with open('dpkg.list', 'w') as dpkg:
838 dpkg.write(out)
839
840 def configure_apt(self, rootdir):
841 # use the distribution and mirror to create an apt source
842 self.message("Configuring apt to use distribution and mirror")
843 conf = os.path.join(rootdir, 'etc', 'apt', 'sources.list.d', 'base.list')
844 logging.debug('configure apt %s', conf)
845 mirror = self.settings['mirror']
846 if self.settings['apt-mirror']:
847 mirror = self.settings['apt-mirror']
848 self.message("Setting apt mirror to %s" % mirror)
849 os.unlink(os.path.join(rootdir, 'etc', 'apt', 'sources.list'))
850 f = open(conf, 'w')
851 line = 'deb %s %s main\n' % (mirror, self.settings['distribution'])
852 f.write(line)
853 line = '#deb-src %s %s main\n' % (mirror, self.settings['distribution'])
854 f.write(line)
855 f.close()
856 # ensure the apt sources have valid lists
857 self.runcmd(['chroot', rootdir, 'apt-get', '-qq', 'update'])
858
859 if __name__ == '__main__':
860 VmDebootstrap(version=__version__).run()