lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | """ |
| 3 | Simple script to setup unattended installs on KVM guests. |
| 4 | """ |
| 5 | # -*- coding: utf-8 -*- |
| 6 | import os, sys, shutil, tempfile, re |
| 7 | import common |
| 8 | |
| 9 | |
| 10 | class SetupError(Exception): |
| 11 | """ |
| 12 | Simple wrapper for the builtin Exception class. |
| 13 | """ |
| 14 | pass |
| 15 | |
| 16 | |
| 17 | class UnattendedInstall(object): |
| 18 | """ |
| 19 | Creates a floppy disk image that will contain a config file for unattended |
| 20 | OS install. Optionally, sets up a PXE install server using qemu built in |
| 21 | TFTP and DHCP servers to install a particular operating system. The |
| 22 | parameters to the script are retrieved from environment variables. |
| 23 | """ |
| 24 | def __init__(self): |
| 25 | """ |
| 26 | Gets params from environment variables and sets class attributes. |
| 27 | """ |
| 28 | script_dir = os.path.dirname(sys.modules[__name__].__file__) |
| 29 | kvm_test_dir = os.path.abspath(os.path.join(script_dir, "..")) |
| 30 | images_dir = os.path.join(kvm_test_dir, 'images') |
| 31 | self.deps_dir = os.path.join(kvm_test_dir, 'deps') |
| 32 | self.unattended_dir = os.path.join(kvm_test_dir, 'unattended') |
| 33 | |
lmr | a29a5cb | 2010-03-18 02:39:34 +0000 | [diff] [blame] | 34 | tftp_root = os.environ.get('KVM_TEST_tftp', '') |
| 35 | if tftp_root: |
lmr | c4e1d21 | 2009-10-13 21:13:00 +0000 | [diff] [blame] | 36 | self.tftp_root = os.path.join(kvm_test_dir, tftp_root) |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 37 | if not os.path.isdir(self.tftp_root): |
| 38 | os.makedirs(self.tftp_root) |
lmr | a29a5cb | 2010-03-18 02:39:34 +0000 | [diff] [blame] | 39 | else: |
| 40 | self.tftp_root = tftp_root |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 41 | |
lmr | a29a5cb | 2010-03-18 02:39:34 +0000 | [diff] [blame] | 42 | self.kernel_args = os.environ.get('KVM_TEST_kernel_args', '') |
| 43 | self.finish_program= os.environ.get('KVM_TEST_finish_program', '') |
| 44 | cdrom_iso = os.environ.get('KVM_TEST_cdrom') |
| 45 | self.unattended_file = os.environ.get('KVM_TEST_unattended_file') |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 46 | |
lmr | a29a5cb | 2010-03-18 02:39:34 +0000 | [diff] [blame] | 47 | self.qemu_img_bin = os.environ.get('KVM_TEST_qemu_img_binary') |
lmr | fb79e64 | 2010-01-18 05:24:11 +0000 | [diff] [blame] | 48 | if not os.path.isabs(self.qemu_img_bin): |
| 49 | self.qemu_img_bin = os.path.join(kvm_test_dir, self.qemu_img_bin) |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 50 | self.cdrom_iso = os.path.join(kvm_test_dir, cdrom_iso) |
| 51 | self.floppy_mount = tempfile.mkdtemp(prefix='floppy_', dir='/tmp') |
| 52 | self.cdrom_mount = tempfile.mkdtemp(prefix='cdrom_', dir='/tmp') |
lmr | ee1e40f | 2010-06-10 15:32:27 +0000 | [diff] [blame^] | 53 | self.nfs_mount = tempfile.mkdtemp(prefix='nfs_', dir='/tmp') |
| 54 | floppy_name = os.environ['KVM_TEST_floppy'] |
| 55 | self.floppy_img = os.path.join(kvm_test_dir, floppy_name) |
lmr | a29a5cb | 2010-03-18 02:39:34 +0000 | [diff] [blame] | 56 | floppy_dir = os.path.dirname(self.floppy_img) |
| 57 | if not os.path.isdir(floppy_dir): |
| 58 | os.makedirs(floppy_dir) |
| 59 | |
| 60 | self.pxe_dir = os.environ.get('KVM_TEST_pxe_dir', '') |
| 61 | self.pxe_image = os.environ.get('KVM_TEST_pxe_image', '') |
| 62 | self.pxe_initrd = os.environ.get('KVM_TEST_pxe_initrd', '') |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 63 | |
lmr | ee1e40f | 2010-06-10 15:32:27 +0000 | [diff] [blame^] | 64 | self.medium = os.environ.get('KVM_TEST_medium', '') |
| 65 | self.url = os.environ.get('KVM_TEST_url', '') |
| 66 | self.kernel = os.environ.get('KVM_TEST_kernel', '') |
| 67 | self.initrd = os.environ.get('KVM_TEST_initrd', '') |
| 68 | self.nfs_server = os.environ.get('KVM_TEST_nfs_server', '') |
| 69 | self.nfs_dir = os.environ.get('KVM_TEST_nfs_dir', '') |
| 70 | self.image_path = kvm_test_dir |
| 71 | self.kernel_path = os.path.join(self.image_path, self.kernel) |
| 72 | self.initrd_path = os.path.join(self.image_path, self.initrd) |
| 73 | |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 74 | |
| 75 | def create_boot_floppy(self): |
| 76 | """ |
| 77 | Prepares a boot floppy by creating a floppy image file, mounting it and |
| 78 | copying an answer file (kickstarts for RH based distros, answer files |
| 79 | for windows) to it. After that the image is umounted. |
| 80 | """ |
| 81 | print "Creating boot floppy" |
| 82 | |
| 83 | if os.path.exists(self.floppy_img): |
| 84 | os.remove(self.floppy_img) |
| 85 | |
lmr | b010c7e | 2010-02-24 11:54:30 +0000 | [diff] [blame] | 86 | c_cmd = '%s create -f raw %s 1440k' % (self.qemu_img_bin, |
| 87 | self.floppy_img) |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 88 | if os.system(c_cmd): |
| 89 | raise SetupError('Could not create floppy image.') |
| 90 | |
| 91 | f_cmd = 'mkfs.msdos -s 1 %s' % self.floppy_img |
| 92 | if os.system(f_cmd): |
| 93 | raise SetupError('Error formatting floppy image.') |
| 94 | |
lmr | b010c7e | 2010-02-24 11:54:30 +0000 | [diff] [blame] | 95 | try: |
| 96 | m_cmd = 'mount -o loop %s %s' % (self.floppy_img, self.floppy_mount) |
| 97 | if os.system(m_cmd): |
| 98 | raise SetupError('Could not mount floppy image.') |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 99 | |
lmr | b010c7e | 2010-02-24 11:54:30 +0000 | [diff] [blame] | 100 | if self.unattended_file.endswith('.sif'): |
| 101 | dest_fname = 'winnt.sif' |
| 102 | setup_file = 'winnt.bat' |
| 103 | setup_file_path = os.path.join(self.unattended_dir, setup_file) |
| 104 | setup_file_dest = os.path.join(self.floppy_mount, setup_file) |
| 105 | shutil.copyfile(setup_file_path, setup_file_dest) |
| 106 | elif self.unattended_file.endswith('.ks'): |
lmr | a29a5cb | 2010-03-18 02:39:34 +0000 | [diff] [blame] | 107 | # Red Hat kickstart install |
lmr | b010c7e | 2010-02-24 11:54:30 +0000 | [diff] [blame] | 108 | dest_fname = 'ks.cfg' |
| 109 | elif self.unattended_file.endswith('.xml'): |
lmr | a29a5cb | 2010-03-18 02:39:34 +0000 | [diff] [blame] | 110 | if self.tftp_root is '': |
| 111 | # Windows unattended install |
| 112 | dest_fname = "autounattend.xml" |
| 113 | else: |
| 114 | # SUSE autoyast install |
| 115 | dest_fname = "autoinst.xml" |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 116 | |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 117 | dest = os.path.join(self.floppy_mount, dest_fname) |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 118 | |
lmr | b010c7e | 2010-02-24 11:54:30 +0000 | [diff] [blame] | 119 | # Replace KVM_TEST_CDKEY (in the unattended file) with the cdkey |
lmr | ee1e40f | 2010-06-10 15:32:27 +0000 | [diff] [blame^] | 120 | # provided for this test and replace the KVM_TEST_MEDIUM with |
| 121 | # the tree url or nfs address provided for this test. |
lmr | b010c7e | 2010-02-24 11:54:30 +0000 | [diff] [blame] | 122 | unattended_contents = open(self.unattended_file).read() |
| 123 | dummy_cdkey_re = r'\bKVM_TEST_CDKEY\b' |
| 124 | real_cdkey = os.environ.get('KVM_TEST_cdkey') |
| 125 | if re.search(dummy_cdkey_re, unattended_contents): |
| 126 | if real_cdkey: |
| 127 | unattended_contents = re.sub(dummy_cdkey_re, real_cdkey, |
| 128 | unattended_contents) |
| 129 | else: |
| 130 | print ("WARNING: 'cdkey' required but not specified for " |
| 131 | "this unattended installation") |
lmr | ee1e40f | 2010-06-10 15:32:27 +0000 | [diff] [blame^] | 132 | |
| 133 | dummy_re = r'\bKVM_TEST_MEDIUM\b' |
| 134 | if self.medium == "cdrom": |
| 135 | content = "cdrom" |
| 136 | elif self.medium == "url": |
| 137 | content = "url --url %s" % self.url |
| 138 | elif self.medium == "nfs": |
| 139 | content = "nfs --server=%s --dir=%s" % (self.nfs_server, self.nfs_dir) |
| 140 | else: |
| 141 | raise SetupError("Unexpected installation medium %s" % self.url) |
| 142 | |
| 143 | unattended_contents = re.sub(dummy_re, content, unattended_contents) |
lmr | b010c7e | 2010-02-24 11:54:30 +0000 | [diff] [blame] | 144 | |
lmr | ee1e40f | 2010-06-10 15:32:27 +0000 | [diff] [blame^] | 145 | print unattended_contents |
lmr | b010c7e | 2010-02-24 11:54:30 +0000 | [diff] [blame] | 146 | # Write the unattended file contents to 'dest' |
| 147 | open(dest, 'w').write(unattended_contents) |
| 148 | |
| 149 | if self.finish_program: |
| 150 | dest_fname = os.path.basename(self.finish_program) |
| 151 | dest = os.path.join(self.floppy_mount, dest_fname) |
| 152 | shutil.copyfile(self.finish_program, dest) |
| 153 | |
| 154 | finally: |
| 155 | u_cmd = 'umount %s' % self.floppy_mount |
| 156 | if os.system(u_cmd): |
| 157 | raise SetupError('Could not unmount floppy at %s.' % |
| 158 | self.floppy_mount) |
| 159 | self.cleanup(self.floppy_mount) |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 160 | |
| 161 | os.chmod(self.floppy_img, 0755) |
| 162 | |
| 163 | print "Boot floppy created successfuly" |
| 164 | |
| 165 | |
| 166 | def setup_pxe_boot(self): |
| 167 | """ |
| 168 | Sets up a PXE boot environment using the built in qemu TFTP server. |
| 169 | Copies the PXE Linux bootloader pxelinux.0 from the host (needs the |
| 170 | pxelinux package or equivalent for your distro), and vmlinuz and |
| 171 | initrd.img files from the CD to a directory that qemu will serve trough |
| 172 | TFTP to the VM. |
| 173 | """ |
| 174 | print "Setting up PXE boot using TFTP root %s" % self.tftp_root |
| 175 | |
| 176 | pxe_file = None |
| 177 | pxe_paths = ['/usr/lib/syslinux/pxelinux.0', |
| 178 | '/usr/share/syslinux/pxelinux.0'] |
| 179 | for path in pxe_paths: |
| 180 | if os.path.isfile(path): |
| 181 | pxe_file = path |
| 182 | break |
| 183 | |
| 184 | if not pxe_file: |
| 185 | raise SetupError('Cannot find PXE boot loader pxelinux.0. Make ' |
| 186 | 'sure pxelinux or equivalent package for your ' |
| 187 | 'distro is installed.') |
| 188 | |
| 189 | pxe_dest = os.path.join(self.tftp_root, 'pxelinux.0') |
| 190 | shutil.copyfile(pxe_file, pxe_dest) |
| 191 | |
lmr | b010c7e | 2010-02-24 11:54:30 +0000 | [diff] [blame] | 192 | try: |
| 193 | m_cmd = 'mount -t iso9660 -v -o loop,ro %s %s' % (self.cdrom_iso, |
| 194 | self.cdrom_mount) |
| 195 | if os.system(m_cmd): |
| 196 | raise SetupError('Could not mount CD image %s.' % |
| 197 | self.cdrom_iso) |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 198 | |
lmr | a29a5cb | 2010-03-18 02:39:34 +0000 | [diff] [blame] | 199 | pxe_dir = os.path.join(self.cdrom_mount, self.pxe_dir) |
| 200 | pxe_image = os.path.join(pxe_dir, self.pxe_image) |
| 201 | pxe_initrd = os.path.join(pxe_dir, self.pxe_initrd) |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 202 | |
lmr | b010c7e | 2010-02-24 11:54:30 +0000 | [diff] [blame] | 203 | if not os.path.isdir(pxe_dir): |
| 204 | raise SetupError('The ISO image does not have a %s dir. The ' |
| 205 | 'script assumes that the cd has a %s dir ' |
| 206 | 'where to search for the vmlinuz image.' % |
lmr | a29a5cb | 2010-03-18 02:39:34 +0000 | [diff] [blame] | 207 | (self.pxe_dir, self.pxe_dir)) |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 208 | |
lmr | b010c7e | 2010-02-24 11:54:30 +0000 | [diff] [blame] | 209 | if not os.path.isfile(pxe_image) or not os.path.isfile(pxe_initrd): |
| 210 | raise SetupError('The location %s is lacking either a vmlinuz ' |
| 211 | 'or a initrd.img file. Cannot find a PXE ' |
lmr | a29a5cb | 2010-03-18 02:39:34 +0000 | [diff] [blame] | 212 | 'image to proceed.' % self.pxe_dir) |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 213 | |
lmr | b010c7e | 2010-02-24 11:54:30 +0000 | [diff] [blame] | 214 | tftp_image = os.path.join(self.tftp_root, 'vmlinuz') |
| 215 | tftp_initrd = os.path.join(self.tftp_root, 'initrd.img') |
| 216 | shutil.copyfile(pxe_image, tftp_image) |
| 217 | shutil.copyfile(pxe_initrd, tftp_initrd) |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 218 | |
lmr | b010c7e | 2010-02-24 11:54:30 +0000 | [diff] [blame] | 219 | finally: |
| 220 | u_cmd = 'umount %s' % self.cdrom_mount |
| 221 | if os.system(u_cmd): |
| 222 | raise SetupError('Could not unmount CD at %s.' % |
| 223 | self.cdrom_mount) |
| 224 | self.cleanup(self.cdrom_mount) |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 225 | |
| 226 | pxe_config_dir = os.path.join(self.tftp_root, 'pxelinux.cfg') |
| 227 | if not os.path.isdir(pxe_config_dir): |
| 228 | os.makedirs(pxe_config_dir) |
| 229 | pxe_config_path = os.path.join(pxe_config_dir, 'default') |
| 230 | |
| 231 | pxe_config = open(pxe_config_path, 'w') |
| 232 | pxe_config.write('DEFAULT pxeboot\n') |
| 233 | pxe_config.write('TIMEOUT 20\n') |
| 234 | pxe_config.write('PROMPT 0\n') |
| 235 | pxe_config.write('LABEL pxeboot\n') |
| 236 | pxe_config.write(' KERNEL vmlinuz\n') |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 237 | pxe_config.write(' APPEND initrd=initrd.img %s\n' % |
| 238 | self.kernel_args) |
| 239 | pxe_config.close() |
| 240 | |
| 241 | print "PXE boot successfuly set" |
| 242 | |
lmr | b010c7e | 2010-02-24 11:54:30 +0000 | [diff] [blame] | 243 | |
lmr | ee1e40f | 2010-06-10 15:32:27 +0000 | [diff] [blame^] | 244 | def setup_url(self): |
| 245 | """ |
| 246 | Download the vmlinuz and initrd.img from URL |
| 247 | """ |
| 248 | print "Downloading the vmlinuz and initrd.img" |
| 249 | os.chdir(self.image_path) |
| 250 | |
| 251 | kernel_fetch_cmd = "wget -q %s/isolinux/%s" % (self.url, self.kernel) |
| 252 | initrd_fetch_cmd = "wget -q %s/isolinux/%s" % (self.url, self.initrd) |
| 253 | |
| 254 | if os.path.exists(self.kernel): |
| 255 | os.unlink(self.kernel) |
| 256 | if os.path.exists(self.initrd): |
| 257 | os.unlink(self.initrd) |
| 258 | |
| 259 | if os.system(kernel_fetch_cmd) != 0: |
| 260 | raise SetupError("Could not fetch vmlinuz from %s" % self.url) |
| 261 | if os.system(initrd_fetch_cmd) != 0: |
| 262 | raise SetupError("Could not fetch initrd.img from %s" % self.url) |
| 263 | |
| 264 | print "Downloading finish" |
| 265 | |
| 266 | def setup_nfs(self): |
| 267 | """ |
| 268 | Copy the vmlinuz and initrd.img from nfs. |
| 269 | """ |
| 270 | print "Copying the vmlinuz and initrd.img from nfs" |
| 271 | |
| 272 | m_cmd = "mount %s:%s %s -o ro" % (self.nfs_server, self.nfs_dir, self.nfs_mount) |
| 273 | if os.system(m_cmd): |
| 274 | raise SetupError('Could not mount nfs server.') |
| 275 | |
| 276 | kernel_fetch_cmd = "cp %s/isolinux/%s %s" % (self.nfs_mount, |
| 277 | self.kernel, |
| 278 | self.image_path) |
| 279 | initrd_fetch_cmd = "cp %s/isolinux/%s %s" % (self.nfs_mount, |
| 280 | self.initrd, |
| 281 | self.image_path) |
| 282 | |
| 283 | try: |
| 284 | if os.system(kernel_fetch_cmd): |
| 285 | raise SetupError("Could not copy the vmlinuz from %s" % |
| 286 | self.nfs_mount) |
| 287 | if os.system(initrd_fetch_cmd): |
| 288 | raise SetupError("Could not copy the initrd.img from %s" % |
| 289 | self.nfs_mount) |
| 290 | finally: |
| 291 | u_cmd = "umount %s" % self.nfs_mount |
| 292 | if os.system(u_cmd): |
| 293 | raise SetupError("Could not unmont nfs at %s" % self.nfs_mount) |
| 294 | self.cleanup(self.nfs_mount) |
| 295 | |
lmr | b010c7e | 2010-02-24 11:54:30 +0000 | [diff] [blame] | 296 | def cleanup(self, mount): |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 297 | """ |
lmr | b010c7e | 2010-02-24 11:54:30 +0000 | [diff] [blame] | 298 | Clean up a previously used mountpoint. |
| 299 | |
| 300 | @param mount: Mountpoint to be cleaned up. |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 301 | """ |
lmr | b010c7e | 2010-02-24 11:54:30 +0000 | [diff] [blame] | 302 | if os.path.isdir(mount): |
| 303 | if os.path.ismount(mount): |
| 304 | print "Path %s is still mounted, please verify" % mount |
| 305 | else: |
| 306 | print "Removing mount point %s" % mount |
| 307 | os.rmdir(mount) |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 308 | |
| 309 | |
| 310 | def setup(self): |
| 311 | print "Starting unattended install setup" |
| 312 | |
| 313 | print "Variables set:" |
lmr | ee1e40f | 2010-06-10 15:32:27 +0000 | [diff] [blame^] | 314 | print " medium: " + str(self.medium) |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 315 | print " qemu_img_bin: " + str(self.qemu_img_bin) |
| 316 | print " cdrom iso: " + str(self.cdrom_iso) |
| 317 | print " unattended_file: " + str(self.unattended_file) |
| 318 | print " kernel_args: " + str(self.kernel_args) |
| 319 | print " tftp_root: " + str(self.tftp_root) |
| 320 | print " floppy_mount: " + str(self.floppy_mount) |
| 321 | print " floppy_img: " + str(self.floppy_img) |
| 322 | print " finish_program: " + str(self.finish_program) |
lmr | a29a5cb | 2010-03-18 02:39:34 +0000 | [diff] [blame] | 323 | print " pxe_dir: " + str(self.pxe_dir) |
| 324 | print " pxe_image: " + str(self.pxe_image) |
| 325 | print " pxe_initrd: " + str(self.pxe_initrd) |
lmr | ee1e40f | 2010-06-10 15:32:27 +0000 | [diff] [blame^] | 326 | print " url: " + str(self.url) |
| 327 | print " kernel: " + str(self.kernel) |
| 328 | print " initrd: " + str(self.initrd) |
| 329 | print " nfs_server: " + str(self.nfs_server) |
| 330 | print " nfs_dir: " + str(self.nfs_dir) |
| 331 | print " nfs_mount: " + str(self.nfs_mount) |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 332 | |
lmr | ee1e40f | 2010-06-10 15:32:27 +0000 | [diff] [blame^] | 333 | if self.unattended_file and self.floppy_img is not None: |
| 334 | self.create_boot_floppy() |
| 335 | if self.medium == "cdrom": |
| 336 | if self.tftp_root: |
| 337 | self.setup_pxe_boot() |
| 338 | elif self.medium == "url": |
| 339 | self.setup_url() |
| 340 | elif self.medium == "nfs": |
| 341 | self.setup_nfs() |
| 342 | else: |
| 343 | raise SetupError("Unexpected installation method %s" % |
| 344 | self.medium) |
lmr | 5d73e2f | 2009-10-09 20:46:36 +0000 | [diff] [blame] | 345 | print "Unattended install setup finished successfuly" |
| 346 | |
| 347 | |
| 348 | if __name__ == "__main__": |
| 349 | os_install = UnattendedInstall() |
| 350 | os_install.setup() |