]>
git.siccegge.de Git - forks/vmdebootstrap.git/blob - vmdebootstrap
9dd9ba54a6899324ed83704861a1a9eabffd5998
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/>.
28 class VmDebootstrap(cliapp
.Application
):
30 def add_settings(self
):
31 default_arch
= 'amd64'
33 self
.settings
.boolean(['verbose'], 'report what is going on')
34 self
.settings
.string(['image'], 'put created disk image in FILE',
36 self
.settings
.bytesize(['size'],
37 'create a disk image of size SIZE (%default)',
40 self
.settings
.string(['tarball'], "tar up the disk's contents in FILE",
42 self
.settings
.string(['mirror'],
43 'use MIRROR as package source (%default)',
45 default
='http://cdn.debian.net/debian/')
46 self
.settings
.string(['arch'], 'architecture to use (%default)',
49 self
.settings
.string(['distribution'],
50 'release to use (%default)',
53 self
.settings
.string_list(['package'], 'install PACKAGE onto system')
54 self
.settings
.string_list(['custom-package'],
55 'install package in DEB file onto system '
58 self
.settings
.boolean(['no-kernel'], 'do not install a linux package')
59 self
.settings
.boolean(['enable-dhcp'], 'enable DHCP on eth0')
60 self
.settings
.string(['root-password'], 'set root password',
62 self
.settings
.boolean(['lock-root-password'],
63 'lock root account so they cannot login?')
64 self
.settings
.string(['customize'],
65 'run SCRIPT after setting up system',
67 self
.settings
.string(['hostname'],
68 'set name to HOSTNAME (%default)',
71 self
.settings
.string_list(['user'],
72 'create USER with PASSWORD',
73 metavar
='USER/PASSWORD')
74 self
.settings
.boolean(['serial-console'],
75 'configure image to use a serial console')
76 self
.settings
.boolean(['sudo'],
77 'install sudo, and if user is created, add them '
80 def process_args(self
, args
):
81 if not self
.settings
['image'] and not self
.settings
['tarball']:
82 raise cliapp
.AppException('You must give disk image filename, '
83 'or tarball filename')
84 if self
.settings
['image'] and not self
.settings
['size']:
85 raise cliapp
.AppException('If disk image is specified, '
86 'You must give image size.')
89 self
.mount_points
= []
92 if self
.settings
['image']:
93 self
.create_empty_image()
94 self
.partition_image()
96 rootdev
= self
.setup_kpartx()
98 rootdir
= self
.mount(rootdev
)
100 rootdir
= self
.mkdtemp()
101 self
.debootstrap(rootdir
)
102 self
.set_hostname(rootdir
)
103 self
.create_fstab(rootdir
)
104 self
.install_debs(rootdir
)
105 self
.set_root_password(rootdir
)
106 self
.create_users(rootdir
)
107 self
.remove_udev_persistent_rules(rootdir
)
108 self
.setup_networking(rootdir
)
109 self
.customize(rootdir
)
110 if self
.settings
['image']:
111 self
.install_extlinux(rootdev
, rootdir
)
112 if self
.settings
['tarball']:
113 self
.create_tarball(rootdir
)
114 except BaseException
, e
:
115 self
.message('EEEK! Something bad happened...')
116 self
.cleanup_system()
119 self
.cleanup_system()
121 def message(self
, msg
):
123 if self
.settings
['verbose']:
126 def runcmd(self
, argv
, stdin
='', ignore_fail
=False, **kwargs
):
127 logging
.debug('runcmd: %s %s' % (argv
, kwargs
))
128 p
= subprocess
.Popen(argv
, stdin
=subprocess
.PIPE
,
129 stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
,
131 out
, err
= p
.communicate(stdin
)
132 if p
.returncode
!= 0:
133 msg
= 'command failed: %s\n%s\n%s' % (argv
, out
, err
)
136 raise cliapp
.AppException(msg
)
140 dirname
= tempfile
.mkdtemp()
141 self
.remove_dirs
.append(dirname
)
142 logging
.debug('mkdir %s' % dirname
)
145 def mount(self
, device
):
146 self
.message('Mounting %s' % device
)
147 mount_point
= self
.mkdtemp()
148 self
.runcmd(['mount', device
, mount_point
])
149 self
.mount_points
.append(mount_point
)
150 logging
.debug('mounted %s on %s' % (device
, mount_point
))
153 def create_empty_image(self
):
154 self
.message('Creating disk image')
155 self
.runcmd(['qemu-img', 'create', '-f', 'raw',
156 self
.settings
['image'],
157 str(self
.settings
['size'])])
159 def partition_image(self
):
160 self
.message('Creating partitions')
161 self
.runcmd(['parted', '-s', self
.settings
['image'],
163 self
.runcmd(['parted', '-s', self
.settings
['image'],
164 'mkpart', 'primary', '0%', '100%'])
165 self
.runcmd(['parted', '-s', self
.settings
['image'],
166 'set', '1', 'boot', 'on'])
168 def install_mbr(self
):
169 self
.message('Installing MBR')
170 self
.runcmd(['install-mbr', self
.settings
['image']])
172 def setup_kpartx(self
):
173 out
= self
.runcmd(['kpartx', '-av', self
.settings
['image']])
174 devices
= [line
.split()[2]
175 for line
in out
.splitlines()
176 if line
.startswith('add map ')]
177 if len(devices
) != 1:
178 raise cliapp
.AppException('Surprising number of partitions')
179 return '/dev/mapper/%s' % devices
[0]
181 def mkfs(self
, device
):
182 self
.message('Creating filesystem')
183 self
.runcmd(['mkfs', '-t', 'ext2', device
])
185 def debootstrap(self
, rootdir
):
186 self
.message('Debootstrapping')
188 include
= self
.settings
['package']
190 if not self
.settings
['no-kernel']:
191 if self
.settings
['arch'] == 'i386':
194 kernel_arch
= self
.settings
['arch']
195 kernel_image
= 'linux-image-2.6-%s' % kernel_arch
196 include
.append(kernel_image
)
198 if self
.settings
['sudo'] and 'sudo' not in include
:
199 include
.append('sudo')
201 args
= ['debootstrap', '--arch=%s' % self
.settings
['arch']]
202 if include
: args
.append('--include=%s' % ','.join(include
))
203 args
+= [self
.settings
['distribution'],
204 rootdir
, self
.settings
['mirror']]
207 def set_hostname(self
, rootdir
):
208 hostname
= self
.settings
['hostname']
209 with
open(os
.path
.join(rootdir
, 'etc', 'hostname'), 'w') as f
:
210 f
.write('%s\n' % hostname
)
212 etc_hosts
= os
.path
.join(rootdir
, 'etc', 'hosts')
213 with
open(etc_hosts
, 'r') as f
:
215 with
open(etc_hosts
, 'w') as f
:
216 for line
in data
.splitlines():
217 if line
.startswith('127.0.0.1'):
218 line
+= ' %s' % hostname
219 f
.write('%s\n' % line
)
221 def create_fstab(self
, rootdir
):
222 fstab
= os
.path
.join(rootdir
, 'etc', 'fstab')
223 with
open(fstab
, 'w') as f
:
224 f
.write('proc /proc proc defaults 0 0\n')
225 f
.write('/dev/sda1 / ext4 errors=remount-ro 0 1\n')
227 def install_debs(self
, rootdir
):
228 if not self
.settings
['custom-package']:
230 self
.message('Installing custom packages')
231 tmp
= os
.path
.join(rootdir
, 'tmp', 'install_debs')
233 for deb
in self
.settings
['custom-package']:
234 shutil
.copy(deb
, tmp
)
235 filenames
= [os
.path
.join('/tmp/install_debs', os
.path
.basename(deb
))
236 for deb
in self
.settings
['custom-package']]
238 self
.runcmd_unchecked(['chroot', rootdir
, 'dpkg', '-i'] + filenames
)
239 logging
.debug('stdout:\n%s' % out
)
240 logging
.debug('stderr:\n%s' % err
)
241 out
= self
.runcmd(['chroot', rootdir
,
242 'apt-get', '-f', '--no-remove', 'install'])
243 logging
.debug('stdout:\n%s' % out
)
246 def set_root_password(self
, rootdir
):
247 if self
.settings
['root-password']:
248 self
.message('Setting root password')
249 self
.set_password(rootdir
, 'root', self
.settings
['root-password'])
250 elif self
.settings
['lock-root-password']:
251 self
.message('Locking root password')
252 self
.runcmd(['chroot', rootdir
, 'passwd', '-l', 'root'])
254 self
.message('Give root an empty password')
255 self
.delete_password(rootdir
, 'root')
257 def create_users(self
, rootdir
):
258 def create_user(user
):
259 self
.runcmd(['chroot', rootdir
, 'adduser', '--gecos', user
,
260 '--disabled-password', user
])
261 if self
.settings
['sudo']:
262 self
.runcmd(['chroot', rootdir
, 'adduser', user
, 'sudo'])
264 for userpass
in self
.settings
['user']:
266 user
, password
= userpass
.split('/', 1)
268 self
.set_password(rootdir
, user
, password
)
270 create_user(userpass
)
271 self
.delete_password(rootdir
, userpass
)
273 def set_password(self
, rootdir
, user
, password
):
274 encrypted
= crypt
.crypt(password
, '..')
275 self
.runcmd(['chroot', rootdir
, 'usermod', '-p', encrypted
, user
])
277 def delete_password(self
, rootdir
, user
):
278 self
.runcmd(['chroot', rootdir
, 'passwd', '-d', user
])
280 def remove_udev_persistent_rules(self
, rootdir
):
281 self
.message('Removing udev persistent cd and net rules')
282 for x
in ['70-persistent-cd.rules', '70-persistent-net.rules']:
283 pathname
= os
.path
.join(rootdir
, 'etc', 'udev', 'rules.d', x
)
284 if os
.path
.exists(pathname
):
285 logging
.debug('rm %s' % pathname
)
288 logging
.debug('not removing non-existent %s' % pathname
)
290 def setup_networking(self
, rootdir
):
291 self
.message('Setting up networking')
293 f
= open(os
.path
.join(rootdir
, 'etc', 'network', 'interfaces'), 'w')
295 f
.write('iface lo inet loopback\n')
297 if self
.settings
['enable-dhcp']:
299 f
.write('allow-hotplug eth0\n')
300 f
.write('iface eth0 inet dhcp\n')
304 def install_extlinux(self
, rootdev
, rootdir
):
305 self
.message('Installing extlinux')
308 dirname
= os
.path
.join(rootdir
, 'boot')
309 basenames
= os
.listdir(dirname
)
310 logging
.debug('find: %s' % basenames
)
311 for basename
in basenames
:
312 if re
.search(pattern
, basename
):
313 return os
.path
.join('boot', basename
)
314 raise cliapp
.AppException('Cannot find match: %s' % pattern
)
316 kernel_image
= find('vmlinuz-.*')
317 initrd_image
= find('initrd.img-.*')
319 out
= self
.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
320 '-s', 'UUID', rootdev
])
321 uuid
= out
.splitlines()[0].strip()
323 conf
= os
.path
.join(rootdir
, 'extlinux.conf')
324 logging
.debug('configure extlinux %s' % conf
)
332 append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
335 'kernel': kernel_image
,
336 'initrd': initrd_image
,
339 'console=ttyS0,115200' if self
.settings
['serial-console'] else '',
340 'extserial': 'serial 0 115200' if self
.settings
['serial-console'] else '',
344 if self
.settings
['serial-console']:
345 logging
.debug('adding getty to serial console')
346 inittab
= os
.path
.join(rootdir
, 'etc/inittab')
347 with
open(inittab
, 'a') as f
:
348 f
.write('\nS0:23:respawn:/sbin/getty -L ttyS0 115200 vt100\n')
350 self
.runcmd(['extlinux', '--install', rootdir
])
351 self
.runcmd(['sync'])
352 import time
; time
.sleep(2)
354 def cleanup_system(self
):
355 # Clean up after any errors.
357 self
.message('Cleaning up')
359 if self
.settings
['image']:
360 for mount_point
in self
.mount_points
:
361 self
.runcmd(['umount', mount_point
], ignore_fail
=True)
363 self
.runcmd(['kpartx', '-d', self
.settings
['image']], ignore_fail
=True)
365 for dirname
in self
.remove_dirs
:
366 shutil
.rmtree(dirname
)
368 def customize(self
, rootdir
):
369 script
= self
.settings
['customize']
371 self
.message('Running customize script %s' % script
)
372 with
open('/dev/tty', 'w') as tty
:
373 cliapp
.runcmd([script
, rootdir
], stdout
=tty
, stderr
=tty
)
375 def create_tarball(self
, rootdir
):
376 # Create a tarball of the disk's contents
377 # shell out to runcmd since it more easily handles rootdir
378 self
.message('Creating tarball of disk contents')
379 self
.runcmd(['tar', '-cf', self
.settings
['tarball'], '-C', rootdir
, '.'])
382 if __name__
== '__main__':
383 VmDebootstrap().run()