]>
git.siccegge.de Git - forks/vmdebootstrap.git/blob - vmdebootstrap
2 # Copyright 2011-2013 Lars Wirzenius
3 # Copyright 2012 Codethink Limited
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
33 class VmDebootstrap(cliapp
.Application
):
35 def add_settings(self
):
36 default_arch
= self
.runcmd(
37 ["dpkg", "--print-architecture"],
38 ignore_fail
=False).strip()
40 self
.settings
.boolean(['verbose'], 'report what is going on')
41 self
.settings
.string(['image'], 'put created disk image in FILE',
43 self
.settings
.bytesize(['size'],
44 'create a disk image of size SIZE (%default)',
47 self
.settings
.bytesize(['bootsize'],
48 'create boot partition of size SIZE (%default)',
51 self
.settings
.string(['boottype'],
52 'specify file system type for /boot/',
54 self
.settings
.string(['foreign'],
55 'set up foreign debootstrap environment using provided program (ie binfmt handler)')
56 self
.settings
.string(['variant'],
57 'select debootstrap variant it not using the default')
58 self
.settings
.boolean(
62 self
.settings
.string(['tarball'], "tar up the disk's contents in FILE",
64 self
.settings
.string(['mirror'],
65 'use MIRROR as package source (%default)',
67 default
='http://cdn.debian.net/debian/')
68 self
.settings
.string(['arch'], 'architecture to use (%default)',
71 self
.settings
.string(['distribution'],
72 'release to use (%default)',
75 self
.settings
.string_list(['package'], 'install PACKAGE onto system')
76 self
.settings
.string_list(['custom-package'],
77 'install package in DEB file onto system '
80 self
.settings
.boolean(['no-kernel'], 'do not install a linux package')
81 self
.settings
.boolean(['enable-dhcp'], 'enable DHCP on eth0')
82 self
.settings
.string(['root-password'], 'set root password',
84 self
.settings
.boolean(['lock-root-password'],
85 'lock root account so they cannot login?')
86 self
.settings
.string(['customize'],
87 'run SCRIPT after setting up system',
89 self
.settings
.string(['hostname'],
90 'set name to HOSTNAME (%default)',
93 self
.settings
.string_list(['user'],
94 'create USER with PASSWORD',
95 metavar
='USER/PASSWORD')
96 self
.settings
.boolean(['serial-console'],
97 'configure image to use a serial console')
98 self
.settings
.string(['serial-console-command'],
99 'command to manage the serial console, appended '
100 'to /etc/inittab (%default)',
102 default
='/sbin/getty -L ttyS0 115200 vt100')
103 self
.settings
.boolean(['sudo'],
104 'install sudo, and if user is created, add them '
106 self
.settings
.string(['owner'],
107 'the user who will own the image when the build '
109 self
.settings
.boolean(['squash'],
110 'use squashfs on the final image.')
111 self
.settings
.boolean(['configure-apt'],
112 'Create an apt source based on the distribution '
113 'and mirror selected.')
114 self
.settings
.boolean(['mbr'],
115 'Run install-mbr (no longer done by default)')
116 self
.settings
.boolean(['grub'],
117 'Install and configure grub2 - disables '
120 def process_args(self
, args
):
121 if not self
.settings
['image'] and not self
.settings
['tarball']:
122 raise cliapp
.AppException('You must give disk image filename, '
123 'or tarball filename')
124 if self
.settings
['image'] and not self
.settings
['size']:
125 raise cliapp
.AppException('If disk image is specified, '
126 'You must give image size.')
128 self
.remove_dirs
= []
129 self
.mount_points
= []
137 if self
.settings
['image']:
138 self
.create_empty_image()
139 self
.partition_image()
140 if self
.settings
['mbr']:
142 (rootdev
, bootdev
) = self
.setup_kpartx()
143 self
.mkfs(rootdev
, type=roottype
)
144 rootdir
= self
.mount(rootdev
)
146 if self
.settings
['boottype']:
147 boottype
= self
.settings
['boottype']
150 self
.mkfs(bootdev
, type=boottype
)
151 bootdir
= '%s/%s' % (rootdir
, 'boot/')
153 bootdir
= self
.mount(bootdev
, bootdir
)
155 rootdir
= self
.mkdtemp()
156 self
.debootstrap(rootdir
)
157 self
.set_hostname(rootdir
)
158 self
.create_fstab(rootdir
, rootdev
, roottype
, bootdev
, boottype
)
159 self
.install_debs(rootdir
)
160 self
.cleanup_apt_cache(rootdir
)
161 self
.set_root_password(rootdir
)
162 self
.create_users(rootdir
)
163 self
.remove_udev_persistent_rules(rootdir
)
164 self
.setup_networking(rootdir
)
165 if self
.settings
['configure-apt']:
166 self
.configure_apt(rootdir
)
167 self
.customize(rootdir
)
168 if self
.settings
['image']:
169 if self
.settings
['grub']:
170 self
.install_grub2(rootdev
, rootdir
)
171 elif self
.settings
['extlinux']:
172 self
.install_extlinux(rootdev
, rootdir
)
173 self
.append_serial_console(rootdir
)
174 self
.optimize_image(rootdir
)
175 if self
.settings
['squash']:
178 if self
.settings
['foreign']:
179 os
.unlink('%s/usr/bin/%s' %
180 (rootdir
, os
.path
.basename(self
.settings
['foreign'])))
182 if self
.settings
['tarball']:
183 self
.create_tarball(rootdir
)
185 if self
.settings
['owner']:
187 except BaseException
, e
:
188 self
.message('EEEK! Something bad happened...')
190 db_log
= os
.path
.join(rootdir
, 'debootstrap', 'debootstrap.log')
191 if os
.path
.exists(db_log
):
192 shutil
.copy(db_log
, os
.getcwd())
194 self
.cleanup_system()
197 self
.cleanup_system()
199 def message(self
, msg
):
201 if self
.settings
['verbose']:
204 def runcmd(self
, argv
, stdin
='', ignore_fail
=False, **kwargs
):
205 logging
.debug('runcmd: %s %s' % (argv
, kwargs
))
206 p
= subprocess
.Popen(argv
, stdin
=subprocess
.PIPE
,
207 stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
,
209 out
, err
= p
.communicate(stdin
)
210 if p
.returncode
!= 0:
211 msg
= 'command failed: %s\n%s\n%s' % (argv
, out
, err
)
214 raise cliapp
.AppException(msg
)
218 dirname
= tempfile
.mkdtemp()
219 self
.remove_dirs
.append(dirname
)
220 logging
.debug('mkdir %s' % dirname
)
223 def mount(self
, device
, path
=None):
225 mount_point
= self
.mkdtemp()
228 self
.message('Mounting %s on %s' % (device
, mount_point
))
229 self
.runcmd(['mount', device
, mount_point
])
230 self
.mount_points
.append(mount_point
)
231 logging
.debug('mounted %s on %s' % (device
, mount_point
))
234 def create_empty_image(self
):
235 self
.message('Creating disk image')
236 self
.runcmd(['qemu-img', 'create', '-f', 'raw',
237 self
.settings
['image'],
238 str(self
.settings
['size'])])
240 def partition_image(self
):
241 self
.message('Creating partitions')
242 self
.runcmd(['parted', '-s', self
.settings
['image'],
244 if self
.settings
['bootsize'] and self
.settings
['bootsize'] is not '0%':
245 bootsize
= str(self
.settings
['bootsize'] / (1024 * 1024))
246 self
.runcmd(['parted', '-s', self
.settings
['image'],
247 'mkpart', 'primary', 'fat16', '0', bootsize
])
250 self
.runcmd(['parted', '-s', self
.settings
['image'],
251 'mkpart', 'primary', bootsize
, '100%'])
252 self
.runcmd(['parted', '-s', self
.settings
['image'],
253 'set', '1', 'boot', 'on'])
255 def install_mbr(self
):
256 if os
.path
.exists("/sbin/install-mbr"):
257 self
.message('Installing MBR')
258 self
.runcmd(['install-mbr', self
.settings
['image']])
260 def setup_kpartx(self
):
261 out
= self
.runcmd(['kpartx', '-avs', self
.settings
['image']])
262 if self
.settings
['bootsize']:
270 devices
= [line
.split()[2]
271 for line
in out
.splitlines()
272 if line
.startswith('add map ')]
273 if len(devices
) != parts
:
274 raise cliapp
.AppException('Surprising number of partitions')
275 root
= '/dev/mapper/%s' % devices
[rootindex
]
276 if self
.settings
['bootsize']:
277 boot
= '/dev/mapper/%s' % devices
[bootindex
]
280 def mkfs(self
, device
, type):
281 self
.message('Creating filesystem %s' % type)
282 self
.runcmd(['mkfs', '-t', type, device
])
284 def debootstrap(self
, rootdir
):
285 self
.message('Debootstrapping')
287 if self
.settings
['foreign']:
288 necessary_packages
= []
290 necessary_packages
= ['acpid']
292 if self
.settings
['grub']:
293 necessary_packages
.append('grub2')
295 include
= self
.settings
['package']
297 if not self
.settings
['no-kernel']:
298 if self
.settings
['arch'] == 'i386':
301 kernel_arch
= self
.settings
['arch']
302 kernel_image
= 'linux-image-%s' % kernel_arch
303 include
.append(kernel_image
)
305 if self
.settings
['sudo'] and 'sudo' not in include
:
306 include
.append('sudo')
308 args
= ['debootstrap', '--arch=%s' % self
.settings
['arch']]
310 '--include=%s' % ','.join(necessary_packages
+ include
))
311 if self
.settings
['foreign']:
312 args
.append('--foreign')
313 if self
.settings
['variant']:
314 args
.append('--variant')
315 args
.append(self
.settings
['variant'])
316 args
+= [self
.settings
['distribution'],
317 rootdir
, self
.settings
['mirror']]
318 logging
.debug(" ".join(args
))
320 if self
.settings
['foreign']:
321 # First copy the binfmt handler over
322 shutil
.copy(self
.settings
['foreign'], '%s/usr/bin/' % rootdir
)
323 # Next, run the package install scripts etc.
324 self
.message('Running debootstrap second stage')
325 self
.runcmd(['chroot', rootdir
,
326 '/debootstrap/debootstrap', '--second-stage'])
328 def set_hostname(self
, rootdir
):
329 hostname
= self
.settings
['hostname']
330 with
open(os
.path
.join(rootdir
, 'etc', 'hostname'), 'w') as f
:
331 f
.write('%s\n' % hostname
)
333 etc_hosts
= os
.path
.join(rootdir
, 'etc', 'hosts')
335 with
open(etc_hosts
, 'r') as f
:
337 with
open(etc_hosts
, 'w') as f
:
338 for line
in data
.splitlines():
339 if line
.startswith('127.0.0.1'):
340 line
+= ' %s' % hostname
341 f
.write('%s\n' % line
)
345 def create_fstab(self
, rootdir
, rootdev
, roottype
, bootdev
, boottype
):
347 out
= self
.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
348 '-s', 'UUID', device
])
349 return out
.splitlines()[0].strip()
352 rootdevstr
= 'UUID=%s' % fsuuid(rootdev
)
354 rootdevstr
= '/dev/sda1'
357 bootdevstr
= 'UUID=%s' % fsuuid(bootdev
)
361 fstab
= os
.path
.join(rootdir
, 'etc', 'fstab')
362 with
open(fstab
, 'w') as f
:
363 f
.write('proc /proc proc defaults 0 0\n')
364 f
.write('%s / %s errors=remount-ro 0 1\n' % (rootdevstr
, roottype
))
366 f
.write('%s /boot %s errors=remount-ro 0 2\n' % (bootdevstr
, boottype
))
368 def install_debs(self
, rootdir
):
369 if not self
.settings
['custom-package']:
371 self
.message('Installing custom packages')
372 tmp
= os
.path
.join(rootdir
, 'tmp', 'install_debs')
374 for deb
in self
.settings
['custom-package']:
375 shutil
.copy(deb
, tmp
)
376 filenames
= [os
.path
.join('/tmp/install_debs', os
.path
.basename(deb
))
377 for deb
in self
.settings
['custom-package']]
379 self
.runcmd_unchecked(['chroot', rootdir
, 'dpkg', '-i'] + filenames
)
380 logging
.debug('stdout:\n%s' % out
)
381 logging
.debug('stderr:\n%s' % err
)
382 out
= self
.runcmd(['chroot', rootdir
,
383 'apt-get', '-f', '--no-remove', 'install'])
384 logging
.debug('stdout:\n%s' % out
)
387 def cleanup_apt_cache(self
, rootdir
):
388 out
= self
.runcmd(['chroot', rootdir
, 'apt-get', 'clean'])
389 logging
.debug('stdout:\n%s' % out
)
391 def set_root_password(self
, rootdir
):
392 if self
.settings
['root-password']:
393 self
.message('Setting root password')
394 self
.set_password(rootdir
, 'root', self
.settings
['root-password'])
395 elif self
.settings
['lock-root-password']:
396 self
.message('Locking root password')
397 self
.runcmd(['chroot', rootdir
, 'passwd', '-l', 'root'])
399 self
.message('Give root an empty password')
400 self
.delete_password(rootdir
, 'root')
402 def create_users(self
, rootdir
):
403 def create_user(user
):
404 self
.runcmd(['chroot', rootdir
, 'adduser', '--gecos', user
,
405 '--disabled-password', user
])
406 if self
.settings
['sudo']:
407 self
.runcmd(['chroot', rootdir
, 'adduser', user
, 'sudo'])
409 for userpass
in self
.settings
['user']:
411 user
, password
= userpass
.split('/', 1)
413 self
.set_password(rootdir
, user
, password
)
415 create_user(userpass
)
416 self
.delete_password(rootdir
, userpass
)
418 def set_password(self
, rootdir
, user
, password
):
419 encrypted
= crypt
.crypt(password
, '..')
420 self
.runcmd(['chroot', rootdir
, 'usermod', '-p', encrypted
, user
])
422 def delete_password(self
, rootdir
, user
):
423 self
.runcmd(['chroot', rootdir
, 'passwd', '-d', user
])
425 def remove_udev_persistent_rules(self
, rootdir
):
426 self
.message('Removing udev persistent cd and net rules')
427 for x
in ['70-persistent-cd.rules', '70-persistent-net.rules']:
428 pathname
= os
.path
.join(rootdir
, 'etc', 'udev', 'rules.d', x
)
429 if os
.path
.exists(pathname
):
430 logging
.debug('rm %s' % pathname
)
433 logging
.debug('not removing non-existent %s' % pathname
)
435 def setup_networking(self
, rootdir
):
436 self
.message('Setting up networking')
438 f
= open(os
.path
.join(rootdir
, 'etc', 'network', 'interfaces'), 'w')
440 f
.write('iface lo inet loopback\n')
442 if self
.settings
['enable-dhcp']:
444 f
.write('auto eth0\n')
445 f
.write('iface eth0 inet dhcp\n')
449 def append_serial_console(self
, rootdir
):
450 if self
.settings
['serial-console']:
451 serial_command
= self
.settings
['serial-console-command']
452 logging
.debug('adding getty to serial console')
453 inittab
= os
.path
.join(rootdir
, 'etc/inittab')
454 with
open(inittab
, 'a') as f
:
455 f
.write('\nS0:23:respawn:%s\n' % serial_command
)
457 def install_grub2(self
, rootdev
, rootdir
):
458 self
.message("Configuring grub2")
459 # rely on kpartx using consistent naming to map loop0p1 to loop0
460 install_dev
= os
.path
.join('/dev', os
.path
.basename(rootdev
)[:-2])
461 self
.runcmd(['mount', '/dev', '-t', 'devfs', '-obind',
462 '%s' % os
.path
.join(rootdir
, 'dev')])
463 self
.runcmd(['mount', '/proc', '-t', 'proc', '-obind',
464 '%s' % os
.path
.join(rootdir
, 'proc')])
465 self
.runcmd(['mount', '/sys', '-t', 'sysfs', '-obind',
466 '%s' % os
.path
.join(rootdir
, 'sys')])
468 self
.runcmd(['chroot', rootdir
, 'update-grub'])
469 self
.runcmd(['chroot', rootdir
, 'grub-install', install_dev
])
470 except cliapp
.AppException
as e
:
471 self
.message("Failed to configure grub2. Using extlinux.")
472 self
.runcmd(['umount', os
.path
.join(rootdir
, 'sys')])
473 self
.runcmd(['umount', os
.path
.join(rootdir
, 'proc')])
474 self
.runcmd(['umount', os
.path
.join(rootdir
, 'dev')])
475 self
.install_extlinux(rootdev
, rootdir
)
477 def install_extlinux(self
, rootdev
, rootdir
):
478 if not os
.path
.exists("/usr/bin/extlinux"):
479 self
.message("extlinux not installed, skipping.")
481 self
.message('Installing extlinux')
484 dirname
= os
.path
.join(rootdir
, 'boot')
485 basenames
= os
.listdir(dirname
)
486 logging
.debug('find: %s' % basenames
)
487 for basename
in basenames
:
488 if re
.search(pattern
, basename
):
489 return os
.path
.join('boot', basename
)
490 raise cliapp
.AppException('Cannot find match: %s' % pattern
)
492 kernel_image
= find('vmlinuz-.*')
493 initrd_image
= find('initrd.img-.*')
495 out
= self
.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
496 '-s', 'UUID', rootdev
])
497 uuid
= out
.splitlines()[0].strip()
499 conf
= os
.path
.join(rootdir
, 'extlinux.conf')
500 logging
.debug('configure extlinux %s' % conf
)
508 append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
511 'kernel': kernel_image
,
512 'initrd': initrd_image
,
515 'console=ttyS0,115200' if self
.settings
['serial-console'] else '',
516 'extserial': 'serial 0 115200' if self
.settings
['serial-console'] else '',
520 self
.runcmd(['extlinux', '--install', rootdir
])
521 self
.runcmd(['sync'])
524 def optimize_image(self
, rootdir
):
526 Filing up the image with zeros will increase its compression rate
528 zeros
= os
.path
.join(rootdir
, 'ZEROS')
529 self
.runcmd_unchecked(['dd', 'if=/dev/zero', 'of=' + zeros
, 'bs=1M'])
530 self
.runcmd(['rm', '-f', zeros
])
534 Run squashfs on the image.
536 if not os
.path
.exists('/usr/bin/mksquashfs'):
537 logging
.warning("Squash selected but mksquashfs not found!")
539 self
.message("Running mksquashfs")
540 suffixed
= "%s.squashfs" % self
.settings
['image']
541 self
.runcmd(['mksquashfs', self
.settings
['image'],
543 '-no-progress', '-comp', 'xz'], ignore_fail
=False)
544 os
.unlink(self
.settings
['image'])
545 self
.settings
['image'] = suffixed
547 def cleanup_system(self
):
548 # Clean up after any errors.
550 self
.message('Cleaning up')
552 # Umount in the reverse mount order
553 if self
.settings
['image']:
554 for i
in xrange(len(self
.mount_points
) - 1, -1, -1):
555 mount_point
= self
.mount_points
[i
]
557 self
.runcmd(['umount', mount_point
], ignore_fail
=False)
558 except cliapp
.AppException
:
559 logging
.debug("umount failed, sleeping and trying again")
561 self
.runcmd(['umount', mount_point
], ignore_fail
=False)
563 self
.runcmd(['kpartx', '-d', self
.settings
['image']], ignore_fail
=True)
565 for dirname
in self
.remove_dirs
:
566 shutil
.rmtree(dirname
)
568 def customize(self
, rootdir
):
569 script
= self
.settings
['customize']
571 self
.message('Running customize script %s' % script
)
572 with
open('/dev/tty', 'w') as tty
:
573 cliapp
.runcmd([script
, rootdir
], stdout
=tty
, stderr
=tty
)
575 def create_tarball(self
, rootdir
):
576 # Create a tarball of the disk's contents
577 # shell out to runcmd since it more easily handles rootdir
578 self
.message('Creating tarball of disk contents')
579 self
.runcmd(['tar', '-cf', self
.settings
['tarball'], '-C', rootdir
, '.'])
581 def chown(self
, rootdir
):
582 # Change image owner after completed build
583 self
.message("Changing owner to %s" % self
.settings
["owner"])
584 subprocess
.call(["chown",
585 self
.settings
["owner"],
586 self
.settings
["image"]])
588 def configure_apt(self
, rootdir
):
589 # use the distribution and mirror to create an apt source
590 self
.message("Configuring apt to use distribution and mirror")
591 conf
= os
.path
.join(rootdir
, 'etc', 'apt', 'sources.list.d', 'base.list')
592 logging
.debug('configure apt %s' % conf
)
595 deb %(mirror)s %(distribution)s main
596 #deb-src %(mirror)s %(distribution)s main
598 'mirror': self
.settings
['mirror'],
599 'distribution': self
.settings
['distribution']
603 if __name__
== '__main__':
604 VmDebootstrap(version
=__version__
).run()