]>
git.siccegge.de Git - forks/vmdebootstrap.git/blob - vmdebootstrap
7c73568081b75a9d31f1b4a9b6215129a16b408a
2 # Copyright 2011, 2012 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/>.
31 class VmDebootstrap(cliapp
.Application
):
33 def add_settings(self
):
34 default_arch
= 'amd64'
36 self
.settings
.boolean(['verbose'], 'report what is going on')
37 self
.settings
.string(['image'], 'put created disk image in FILE',
39 self
.settings
.bytesize(['size'],
40 'create a disk image of size SIZE (%default)',
43 self
.settings
.bytesize(['bootsize'],
44 'create boot partition of size SIZE (%default)',
47 self
.settings
.string(['boottype'],
48 'specify file system type for /boot/',
50 self
.settings
.string(['foreign'],
51 'set up foreign debootstrap environment using provided program (ie binfmt handler)')
52 self
.settings
.string(['variant'],
53 'select debootstrap variant it not using the default')
54 self
.settings
.boolean(['no-extlinux'], 'do not install extlinux')
55 self
.settings
.string(['tarball'], "tar up the disk's contents in FILE",
57 self
.settings
.string(['mirror'],
58 'use MIRROR as package source (%default)',
60 default
='http://cdn.debian.net/debian/')
61 self
.settings
.string(['arch'], 'architecture to use (%default)',
64 self
.settings
.string(['distribution'],
65 'release to use (%default)',
68 self
.settings
.string_list(['package'], 'install PACKAGE onto system')
69 self
.settings
.string_list(['custom-package'],
70 'install package in DEB file onto system '
73 self
.settings
.boolean(['no-kernel'], 'do not install a linux package')
74 self
.settings
.boolean(['enable-dhcp'], 'enable DHCP on eth0')
75 self
.settings
.string(['root-password'], 'set root password',
77 self
.settings
.boolean(['lock-root-password'],
78 'lock root account so they cannot login?')
79 self
.settings
.string(['customize'],
80 'run SCRIPT after setting up system',
82 self
.settings
.string(['hostname'],
83 'set name to HOSTNAME (%default)',
86 self
.settings
.string_list(['user'],
87 'create USER with PASSWORD',
88 metavar
='USER/PASSWORD')
89 self
.settings
.boolean(['serial-console'],
90 'configure image to use a serial console')
91 self
.settings
.string(['serial-console-command'],
92 'command to manage the serial console, appended '
93 'to /etc/inittab (%default)',
95 default
='/sbin/getty -L ttyS0 115200 vt100')
96 self
.settings
.boolean(['sudo'],
97 'install sudo, and if user is created, add them '
100 def process_args(self
, args
):
101 if not self
.settings
['image'] and not self
.settings
['tarball']:
102 raise cliapp
.AppException('You must give disk image filename, '
103 'or tarball filename')
104 if self
.settings
['image'] and not self
.settings
['size']:
105 raise cliapp
.AppException('If disk image is specified, '
106 'You must give image size.')
108 self
.remove_dirs
= []
109 self
.mount_points
= []
116 if self
.settings
['image']:
117 self
.create_empty_image()
118 self
.partition_image()
120 (rootdev
,bootdev
) = self
.setup_kpartx()
121 self
.mkfs(rootdev
, type=roottype
)
122 rootdir
= self
.mount(rootdev
)
124 if self
.settings
['boottype']:
125 boottype
= self
.settings
['boottype']
128 self
.mkfs(bootdev
, type=boottype
)
129 bootdir
= '%s/%s' % (rootdir
, 'boot/')
131 bootdir
= self
.mount(bootdev
, bootdir
)
133 rootdir
= self
.mkdtemp()
134 self
.debootstrap(rootdir
)
135 self
.set_hostname(rootdir
)
136 self
.create_fstab(rootdir
, rootdev
, roottype
, bootdev
, boottype
)
137 self
.install_debs(rootdir
)
138 self
.cleanup_apt_cache(rootdir
)
139 self
.set_root_password(rootdir
)
140 self
.create_users(rootdir
)
141 self
.remove_udev_persistent_rules(rootdir
)
142 self
.setup_networking(rootdir
)
143 self
.customize(rootdir
)
144 if self
.settings
['image']:
145 if not self
.settings
['no-extlinux']:
146 self
.install_extlinux(rootdev
, rootdir
)
147 self
.optimize_image(rootdir
)
149 if self
.settings
['foreign']:
150 os
.unlink('%s/usr/bin/%s' %
151 (rootdir
, os
.path
.basename(self
.settings
['foreign'])))
153 if self
.settings
['tarball']:
154 self
.create_tarball(rootdir
)
155 except BaseException
, e
:
156 self
.message('EEEK! Something bad happened...')
157 self
.cleanup_system()
160 self
.cleanup_system()
162 def message(self
, msg
):
164 if self
.settings
['verbose']:
167 def runcmd(self
, argv
, stdin
='', ignore_fail
=False, **kwargs
):
168 logging
.debug('runcmd: %s %s' % (argv
, kwargs
))
169 p
= subprocess
.Popen(argv
, stdin
=subprocess
.PIPE
,
170 stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
,
172 out
, err
= p
.communicate(stdin
)
173 if p
.returncode
!= 0:
174 msg
= 'command failed: %s\n%s\n%s' % (argv
, out
, err
)
177 raise cliapp
.AppException(msg
)
181 dirname
= tempfile
.mkdtemp()
182 self
.remove_dirs
.append(dirname
)
183 logging
.debug('mkdir %s' % dirname
)
186 def mount(self
, device
, path
=None):
188 mount_point
= self
.mkdtemp()
191 self
.message('Mounting %s on %s' % (device
,mount_point
))
192 self
.runcmd(['mount', device
, mount_point
])
193 self
.mount_points
.append(mount_point
)
194 logging
.debug('mounted %s on %s' % (device
, mount_point
))
197 def create_empty_image(self
):
198 self
.message('Creating disk image')
199 self
.runcmd(['qemu-img', 'create', '-f', 'raw',
200 self
.settings
['image'],
201 str(self
.settings
['size'])])
203 def partition_image(self
):
204 self
.message('Creating partitions')
205 self
.runcmd(['parted', '-s', self
.settings
['image'],
207 if self
.settings
['bootsize'] and self
.settings
['bootsize'] is not '0%':
208 bootsize
=str(self
.settings
['bootsize']/(1024*1024))
209 self
.runcmd(['parted', '-s', self
.settings
['image'],
210 'mkpart', 'primary', 'fat16', '0', bootsize
])
213 self
.runcmd(['parted', '-s', self
.settings
['image'],
214 'mkpart', 'primary', bootsize
, '100%'])
215 self
.runcmd(['parted', '-s', self
.settings
['image'],
216 'set', '1', 'boot', 'on'])
218 def install_mbr(self
):
219 self
.message('Installing MBR')
220 self
.runcmd(['install-mbr', self
.settings
['image']])
222 def setup_kpartx(self
):
223 out
= self
.runcmd(['kpartx', '-av', self
.settings
['image']])
224 if self
.settings
['bootsize']:
232 devices
= [line
.split()[2]
233 for line
in out
.splitlines()
234 if line
.startswith('add map ')]
235 if len(devices
) != parts
:
236 raise cliapp
.AppException('Surprising number of partitions')
237 root
= '/dev/mapper/%s' % devices
[rootindex
]
238 if self
.settings
['bootsize']:
239 boot
= '/dev/mapper/%s' % devices
[bootindex
]
242 def mkfs(self
, device
, type):
243 self
.message('Creating filesystem %s' % type)
244 self
.runcmd(['mkfs', '-t', type, device
])
246 def debootstrap(self
, rootdir
):
247 self
.message('Debootstrapping')
249 if self
.settings
['foreign']:
250 necessary_packages
= []
252 necessary_packages
= ['acpid']
254 include
= self
.settings
['package']
256 if not self
.settings
['no-kernel']:
257 if self
.settings
['arch'] == 'i386':
260 kernel_arch
= self
.settings
['arch']
261 kernel_image
= 'linux-image-%s' % kernel_arch
262 include
.append(kernel_image
)
264 if self
.settings
['sudo'] and 'sudo' not in include
:
265 include
.append('sudo')
267 args
= ['debootstrap', '--arch=%s' % self
.settings
['arch']]
269 '--include=%s' % ','.join(necessary_packages
+ include
))
270 if self
.settings
['foreign']:
271 args
.append('--foreign')
272 if self
.settings
['variant']:
273 args
.append('--variant')
274 args
.append(self
.settings
['variant'])
275 args
+= [self
.settings
['distribution'],
276 rootdir
, self
.settings
['mirror']]
278 if self
.settings
['foreign']:
279 # First copy the binfmt handler over
280 shutil
.copy(self
.settings
['foreign'], '%s/usr/bin/' % rootdir
)
281 # Next, run the package install scripts etc.
282 self
.runcmd(['chroot', rootdir
,
283 '/debootstrap/debootstrap', '--second-stage'])
285 def set_hostname(self
, rootdir
):
286 hostname
= self
.settings
['hostname']
287 with
open(os
.path
.join(rootdir
, 'etc', 'hostname'), 'w') as f
:
288 f
.write('%s\n' % hostname
)
290 etc_hosts
= os
.path
.join(rootdir
, 'etc', 'hosts')
291 with
open(etc_hosts
, 'r') as f
:
293 with
open(etc_hosts
, 'w') as f
:
294 for line
in data
.splitlines():
295 if line
.startswith('127.0.0.1'):
296 line
+= ' %s' % hostname
297 f
.write('%s\n' % line
)
299 def create_fstab(self
, rootdir
, rootdev
, roottype
, bootdev
, boottype
):
301 out
= self
.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
302 '-s', 'UUID', device
])
303 return out
.splitlines()[0].strip()
306 rootdevstr
= 'UUID=%s' % fsuuid(rootdev
)
308 rootdevstr
= '/dev/sda1'
311 bootdevstr
= 'UUID=%s' % fsuuid(bootdev
)
315 fstab
= os
.path
.join(rootdir
, 'etc', 'fstab')
316 with
open(fstab
, 'w') as f
:
317 f
.write('proc /proc proc defaults 0 0\n')
318 f
.write('%s / %s errors=remount-ro 0 1\n' % (rootdevstr
, roottype
))
320 f
.write('%s /boot %s errors=remount-ro 0 2\n' % (bootdevstr
, boottype
))
322 def install_debs(self
, rootdir
):
323 if not self
.settings
['custom-package']:
325 self
.message('Installing custom packages')
326 tmp
= os
.path
.join(rootdir
, 'tmp', 'install_debs')
328 for deb
in self
.settings
['custom-package']:
329 shutil
.copy(deb
, tmp
)
330 filenames
= [os
.path
.join('/tmp/install_debs', os
.path
.basename(deb
))
331 for deb
in self
.settings
['custom-package']]
333 self
.runcmd_unchecked(['chroot', rootdir
, 'dpkg', '-i'] + filenames
)
334 logging
.debug('stdout:\n%s' % out
)
335 logging
.debug('stderr:\n%s' % err
)
336 out
= self
.runcmd(['chroot', rootdir
,
337 'apt-get', '-f', '--no-remove', 'install'])
338 logging
.debug('stdout:\n%s' % out
)
341 def cleanup_apt_cache(self
, rootdir
):
342 out
= self
.runcmd(['chroot', rootdir
, 'apt-get', 'clean'])
343 logging
.debug('stdout:\n%s' % out
)
345 def set_root_password(self
, rootdir
):
346 if self
.settings
['root-password']:
347 self
.message('Setting root password')
348 self
.set_password(rootdir
, 'root', self
.settings
['root-password'])
349 elif self
.settings
['lock-root-password']:
350 self
.message('Locking root password')
351 self
.runcmd(['chroot', rootdir
, 'passwd', '-l', 'root'])
353 self
.message('Give root an empty password')
354 self
.delete_password(rootdir
, 'root')
356 def create_users(self
, rootdir
):
357 def create_user(user
):
358 self
.runcmd(['chroot', rootdir
, 'adduser', '--gecos', user
,
359 '--disabled-password', user
])
360 if self
.settings
['sudo']:
361 self
.runcmd(['chroot', rootdir
, 'adduser', user
, 'sudo'])
363 for userpass
in self
.settings
['user']:
365 user
, password
= userpass
.split('/', 1)
367 self
.set_password(rootdir
, user
, password
)
369 create_user(userpass
)
370 self
.delete_password(rootdir
, userpass
)
372 def set_password(self
, rootdir
, user
, password
):
373 encrypted
= crypt
.crypt(password
, '..')
374 self
.runcmd(['chroot', rootdir
, 'usermod', '-p', encrypted
, user
])
376 def delete_password(self
, rootdir
, user
):
377 self
.runcmd(['chroot', rootdir
, 'passwd', '-d', user
])
379 def remove_udev_persistent_rules(self
, rootdir
):
380 self
.message('Removing udev persistent cd and net rules')
381 for x
in ['70-persistent-cd.rules', '70-persistent-net.rules']:
382 pathname
= os
.path
.join(rootdir
, 'etc', 'udev', 'rules.d', x
)
383 if os
.path
.exists(pathname
):
384 logging
.debug('rm %s' % pathname
)
387 logging
.debug('not removing non-existent %s' % pathname
)
389 def setup_networking(self
, rootdir
):
390 self
.message('Setting up networking')
392 f
= open(os
.path
.join(rootdir
, 'etc', 'network', 'interfaces'), 'w')
394 f
.write('iface lo inet loopback\n')
396 if self
.settings
['enable-dhcp']:
398 f
.write('auto eth0\n')
399 f
.write('iface eth0 inet dhcp\n')
403 def install_extlinux(self
, rootdev
, rootdir
):
404 self
.message('Installing extlinux')
407 dirname
= os
.path
.join(rootdir
, 'boot')
408 basenames
= os
.listdir(dirname
)
409 logging
.debug('find: %s' % basenames
)
410 for basename
in basenames
:
411 if re
.search(pattern
, basename
):
412 return os
.path
.join('boot', basename
)
413 raise cliapp
.AppException('Cannot find match: %s' % pattern
)
415 kernel_image
= find('vmlinuz-.*')
416 initrd_image
= find('initrd.img-.*')
418 out
= self
.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
419 '-s', 'UUID', rootdev
])
420 uuid
= out
.splitlines()[0].strip()
422 conf
= os
.path
.join(rootdir
, 'extlinux.conf')
423 logging
.debug('configure extlinux %s' % conf
)
431 append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
434 'kernel': kernel_image
,
435 'initrd': initrd_image
,
438 'console=ttyS0,115200' if self
.settings
['serial-console'] else '',
439 'extserial': 'serial 0 115200' if self
.settings
['serial-console'] else '',
443 if self
.settings
['serial-console']:
444 serial_command
= self
.settings
['serial-console-command']
445 logging
.debug('adding getty to serial console')
446 inittab
= os
.path
.join(rootdir
, 'etc/inittab')
447 with
open(inittab
, 'a') as f
:
448 f
.write('\nS0:23:respawn:%s\n' % serial_command
)
450 self
.runcmd(['extlinux', '--install', rootdir
])
451 self
.runcmd(['sync'])
452 import time
; time
.sleep(2)
454 def optimize_image(self
, rootdir
):
456 Filing up the image with zeros will increase its compression rate
458 zeros
= os
.path
.join(rootdir
, 'ZEROS')
459 self
.runcmd_unchecked(['dd', 'if=/dev/zero', 'of=' + zeros
, 'bs=1M'])
460 self
.runcmd(['rm', '-f', zeros
])
463 def cleanup_system(self
):
464 # Clean up after any errors.
466 self
.message('Cleaning up')
468 # Umount in the reverse mount order
469 if self
.settings
['image']:
470 for i
in xrange(len(self
.mount_points
) - 1, -1, -1):
471 mount_point
= self
.mount_points
[i
]
472 self
.runcmd(['umount', mount_point
], ignore_fail
=True)
474 self
.runcmd(['kpartx', '-d', self
.settings
['image']], ignore_fail
=True)
476 for dirname
in self
.remove_dirs
:
477 shutil
.rmtree(dirname
)
479 def customize(self
, rootdir
):
480 script
= self
.settings
['customize']
482 self
.message('Running customize script %s' % script
)
483 with
open('/dev/tty', 'w') as tty
:
484 cliapp
.runcmd([script
, rootdir
], stdout
=tty
, stderr
=tty
)
486 def create_tarball(self
, rootdir
):
487 # Create a tarball of the disk's contents
488 # shell out to runcmd since it more easily handles rootdir
489 self
.message('Creating tarball of disk contents')
490 self
.runcmd(['tar', '-cf', self
.settings
['tarball'], '-C', rootdir
, '.'])
493 if __name__
== '__main__':
494 VmDebootstrap(version
=__version__
).run()