File: UpgradeTestBackendQemu.py

package info (click to toggle)
update-manager 0.68.debian-7
  • links: PTS
  • area: main
  • in suites: lenny
  • size: 6,796 kB
  • ctags: 814
  • sloc: python: 5,646; xml: 1,571; sh: 433; makefile: 356; ansic: 264
file content (442 lines) | stat: -rw-r--r-- 17,001 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
# qemu backend

from UpgradeTestBackend import UpgradeTestBackend
from DistUpgradeConfigParser import DistUpgradeConfig

import ConfigParser
import subprocess
import os
import os.path
import shutil
import glob
import time
import signal
import signal
import crypt
import tempfile
import copy

# TODO:
# - refactor and move common code to UpgradeTestBackend
# - convert ChrootNonInteractive 
# - benchmark qemu/qemu+kqemu/kvm/chroot
# - write tests (unittest, doctest?)
# - instead of copy the file in upgrade() use -snapshot
# - when 0.9.0 is available use "-no-reboot" and make
#   the scripts reboot, this this will exit qemu
# - offer "test-upgrade" feature on real system, run it
#   as "qemu -hda /dev/hda -snapshot foo -append init=/upgrade-test"
#   (this *should* write the stuff to the snapshot file
# - add a "kvm" mode to the backend (qemu/kvm should have identical
#   command line options
# - setup a ssh daemon in target and use that to run the commands
# - find a better way to know when a install is finished
# - add "runInTarget()" that will write a marker file so that we can
#   re-run a command if it fails the first time (or fails because
#   a fsck was done and reboot needed in the VM etc)
# - start a X session with the gui-upgrader in a special
#   "non-interactive" mode to see if the gui upgrade would work too


class UpgradeTestBackendQemu(UpgradeTestBackend):
    " very hacky qemu backend - need qemu >= 0.9.0"

    # FIXME: make this part of the config file
    qemu_binary = "qemu"
    #qemu_binary = "kvm"
    
    qemu_options = [
        "-m","512",      # memory to use
        "-localtime",
        "-vnc","localhost:0",
        "-redir","tcp:54321::22", # ssh login possible (localhost 54321) available
        "-no-reboot",    # exit on reboot
#        "-no-kvm",      # crashes sometimes with kvm HW
        ]

    def __init__(self, profile, basedir):
        UpgradeTestBackend.__init__(self, profile, basedir)
        self.qemu_pid = None
        self.ssh_key = os.path.dirname(profile)+"/ssh-key"
        # setup mount dir/imagefile location
        tmpdir = self.config.getWithDefault("NonInteractive","Tempdir",None)
        if tmpdir is None:
            tmpdir = tempfile.mkdtemp()
        self.image = os.path.join(tmpdir,"qemu-upgrade-test.image")
        self.target = os.path.join(tmpdir, "qemu-upgrade-test")
        if not os.path.exists(self.target):
            os.makedirs(self.target)

    def _getEnvWithProxy(self):
        env = copy.copy(os.environ)
        try:
            env["http_proxy"] = self.config.get("NonInteractive","Proxy")
        except ConfigParser.NoOptionError:
            pass
        return env
        
    def _runAptInTarget(self, command, cmd_options=[]):
        # run apt, retry up to 5 times (network failures, timeouts etc)
        for i in range(5):
            ret = subprocess.call(["chroot", self.target,
                                   "/usr/bin/apt-get",
                                   command]+ self.apt_options + cmd_options,
                                  env=self._getEnvWithProxy())
            if ret == 0:
                break
        return ret

    def _getProxyLine(self):
        if self.config.has_option("NonInteractive","Proxy"):
            return "export http_proxy=%s" % self.config.get("NonInteractive","Proxy")
        return ""

    def _copyToImage(self, fromF, toF):
        ret = subprocess.call(["scp",
                               "-P","54321",
                               "-q","-q", # shut it up
                               "-i",self.ssh_key,
                               "-o", "StrictHostKeyChecking=no",
                               "-o", "UserKnownHostsFile=%s" % os.path.dirname(self.profile)+"/known_hosts",
                               fromF,
                               "root@localhost:%s" %  toF,
                               ])
        return ret

    def _runInImage(self, command, withProxy=True):
        # ssh -l root -p 54321 localhost -i profile/server/ssh_key
        #     -o StrictHostKeyChecking=no
        #
        # FIXME: proxy handling sux big time
        # we can't use subprocess.call(..,env=) here as ssh strips
        # the environment by default
        cmd = command
        try:
            if withProxy:
                cmd = ["http_proxy=%s" % self.config.get("NonInteractive","Proxy")] + command
        except ConfigParser.NoOptionError:
            pass
        ret = subprocess.call(["ssh",
                               "-l","root",
                               "-p","54321",
                               "localhost",
                               "-q","-q", # shut it up
                               "-i",self.ssh_key,
                               "-o", "StrictHostKeyChecking=no",
                               "-o", "UserKnownHostsFile=%s" % os.path.dirname(self.profile)+"/known_hosts",
                               ]+cmd)
        return ret

    def bootstrap(self):
        mirror = self.config.get("NonInteractive","Mirror")
        basepkg = self.config.get("NonInteractive","BasePkg")
        size = int(self.config.getWithDefault("NonInteractive","ImageSize","4000"))
        
        arch = "i386"

        if not os.path.exists(self.target):
            os.mkdir(self.target)
        # make sure we have nothing mounted left
        subprocess.call(["umount",self.target])
        # create image
        # FIXME: - make a proper parition table to get grub installed
        #        - create swap (use hdb for that)
        res = subprocess.call(["qemu-img", "create", self.image, "%sM" % size])
        assert(res == 0)
        # make fs
        res = subprocess.call(["mkfs.ext2","-F",self.image])
        assert(res == 0)
        # now mount it
        res = subprocess.call(["mount","-o","loop,rw",self.image, self.target])
        assert(res == 0)
        # FIXME: what we *really* want here is a d-i install with
        #        proper pre-seeding, but debootstrap will have to do for now
        #        best from a netboot install so that we do not have to
        #        do anthing here 
        res = subprocess.call(["debootstrap", "--arch", arch, self.fromDist, self.target, mirror], env=self._getEnvWithProxy())
        assert(res == 0)
        
        # copy the stuff from toChroot/
        os.chdir("toChroot/")
        for (dirpath, dirnames, filenames) in os.walk("."):
            for name in filenames:
                if not os.path.exists(os.path.join(self.target,dirpath,name)):
                    print "Copying '%s' to chroot" % os.path.join(self.target,dirpath,name)
                    shutil.copy(os.path.join(dirpath,name), os.path.join(self.target,dirpath,name))
        os.chdir("..")

        # setup fstab
        open(self.target+"/etc/fstab","w").write("""
proc /proc proc defaults 0 0
/dev/hda / ext3 defaults,errors=remount-ro 0 0
""")
        # modify /etc/network/interfaces
        # qemu is friendly and give us a network connection,
        # we get IP 10.0.2.15 from the DHCP, the host computer
        # gets IP 10.0.2.2 (DNS 10.0.2.3)
        open(self.target+"/etc/hosts","w").write("""
127.0.0.1 localhost.localdomain localhost
""")
        open(self.target+"/etc/network/interfaces","w").write("""
auto lo eth0
iface lo inet loopback
iface eth0 inet dhcp
""")
        # add proxy and settings
        open(self.target+"/etc/profile","a").write("""
%s
export DEBIAN_FRONTEND=noninteractive
export APT_LISTCHANGES_FRONTEND=none
""" % self._getProxyLine())

        # generate ssh keypair with empty passphrase
        if not os.path.exists(self.ssh_key):
            # *sigh* can't use subprocess.call() here, its too clever
            # and strips away the "" after the -N
            #print 'ssh-keygen -q -f %s -N ""' % self.ssh_key
            ret = os.system('ssh-keygen -q -f %s -N ""' % self.ssh_key)
            assert(ret==0)
        os.mkdir(self.target+"/root/.ssh")
        shutil.copy(self.ssh_key+".pub", self.target+"/root/.ssh/authorized_keys")
        
        # FIXME: - what we really want here is to download a kernel-image
        #          to some dir, boot from the dir and run the install
        #          of a new image inside the bootstraped dir (+run grub)
        #        - install grub/lilo/... as well
        res = self._runAptInTarget("clean")
        assert(res == 0)
        res = self._runAptInTarget("install", ["linux-image-generic"])
        assert(res == 0)
        # openssh-server does fail in invoke-rc.d, ignore for now
        res = self._runAptInTarget("install", ["openssh-server"])
        os.mkdir(self.target+"/upgrade-tester")
        # write the first-boot script
#        first_boot=target+"/upgrade-tester/first-boot"
        # FIXME: this below is all wrong, it should work like this:
        #        install first_boot script that installs/sets up
        #        ssh-server for root login with ssh keys
        #        and from that point on use it to run all other
        #        commands (provide runInTarget())
        # 
        # FIXME: - install NonInteractive/BasePkg
        #        - make sure the thing re-tries, with proxies
        #          a read quite often fails (that we run the install twice)
#        open(first_boot,"w").write("""
#!/bin/sh
#LOG=/var/log/dist-upgrade/first-boot.log
#
# proxy (if required)
#%s
#
#mkdir /var/log/dist-upgrade
#apt-get update > $LOG
#apt-get install -y python-apt >> $LOG
#apt-get install -y grub >> $LOG
#apt-get install -y %s >> $LOG
#apt-get install -y %s >> $LOG
#
#reboot
#""" % (self._getProxyLine(), basepkg, basepkg))
#        os.chmod(first_boot, 0755)
#
#        # run the first-boot script
#        open(target+"/etc/rc.local","w").write("""
#/upgrade-tester/first-boot
#""")


        # we do not really need this at this point, use d-i with
        # pre-seeding instead, this solves the same problem nicely.
        # 
        # install a partition table (taken from qemu-make-debian-root
        # install it in front of the ext2 image
        #HEADS=16
        #SECTORS=63
        # 512 bytes in a sector: cancel the 512 with one of the 1024s...
        #CYLINDERS=(( size * 1024 * 2 / (HEADS * SECTORS) ))
        #ret = subprocess.call(["dd","if=/dev/zero","of=%s" % image,
        #                       "bs=512", "count=2"])
        #assert(ret == 0)
        #ret = subprocess.call(["dd","if=%s" % image,"of=%s" % image,
        #                       "seek=2", "bs=512"])
        #assert(ret == 0)
        # install a bootsector
        #res = subprocess.call(["install-mbr","-f",image])
        #assert(res == 0)
        #cmd="echo '63,' | sfdisk -uS -H%s -S%s -C%s %s" % (HEADS, SECTORS, CYLINDERS, image)
        #subprocess.call(cmd, shell=True)
        # remount, ro to read the kernel (sync + mount -o remount,ro might
        # work as well)

        subprocess.call(["sync"])
        subprocess.call(["umount", self.target])
        subprocess.call(["e2fsck", "-p", "-f", "-v", self.image])
        # FIXME: find a way to figure if the bootstrap was a success
        subprocess.call(["umount", self.target])
        res = subprocess.call(["mount","-o","loop,ro",self.image, self.target])
        assert(res == 0)
        # now start it
        self.start()

        # FIXME: setup proxy
        pass

        # setup root pw
        print "adding user 'test' to virtual machine"
        ret = self._runInImage(["useradd","-p",crypt.crypt("test","sa"),"test"])
        assert(ret == 0)

        # install some useful stuff (and set DEBIAN_FRONTEND and
        # debconf priority)
        ret = self._runInImage(["apt-get","update"])
        assert(ret == 0)
        ret = self._runInImage(["APT_LISTCHANGES=none","DEBIAN_FRONTEND=noninteractive","apt-get","install", "-y",basepkg])
        assert(ret == 0)

        CMAX = 4000
        pkgs =  self.config.getListFromFile("NonInteractive","AdditionalPkgs")
        while(len(pkgs)) > 0:
            print "installing additonal: %s" % pkgs[:CMAX]
            ret= self._runInImage(["apt-get","install","-y"]+pkgs[:CMAX])
            print "apt(2) returned: %s" % ret
            if ret != 0:
                #self._cacheDebs(tmpdir)
                return False
            pkgs = pkgs[CMAX+1:]

        if self.config.has_option("NonInteractive","PostBootstrapScript"):
            script = self.config.get("NonInteractive","PostBootstrapScript")
            if os.path.exists(script):
                self._copyToImage(script, "/tmp")
                self._runInImage([os.path.join("/tmp",script)])
            else:
                print "WARNING: %s not found" % script

        print "Cleaning image"
        ret = self._runInImage(["apt-get","clean"])
        assert(ret == 0)
        return True

    def start(self):
        if self.qemu_pid != None:
            return True
        subprocess.call(["umount", self.target])
        res = subprocess.call(["mount","-o","loop,ro", self.image, self.target])
        assert(res == 0)
        self.qemu_pid = subprocess.Popen([self.qemu_binary,
                               "-hda", self.image,
                               "-kernel", "%s/boot/vmlinuz" % self.target,
                               "-initrd", "%s/boot/initrd.img" % self.target,
                               "-append", "root=/dev/hda",
                               ]+self.qemu_options)
        
        # spin here until ssh has come up
        # FIXME: not nice, see if there is a better way and add watchdog
        ret = 1
        while ret != 0:
            ret = self._runInImage(["/bin/true"])
        return True

    def stop(self):
        " we stop because we run with -no-reboot"
        # FIXME: consider using killall qemu instead
        if self.qemu_pid:
            self._runInImage(["/sbin/reboot"])
            print "waiting for qemu to shutdown"
            self.qemu_pid.wait()
            self.qemu_pid = None

    def upgrade(self):
        # copy the upgrade into target+/upgrader-tester/
        # modify /etc/rc.local to run 
        #  (cd /dist-upgrader ; ./dist-upgrade.py)

        # stop any runing virtual machine
        self.stop()

        # FIXME: make this more clever
        subprocess.call(["umount",self.target])
        res = subprocess.call(["mount","-o","loop",self.image, self.target])
        assert(res == 0)

        upgrade_tester_dir = os.path.join(self.target,"upgrade-tester")
        for f in glob.glob("%s/*" % self.basefilesdir):
            if not os.path.isdir(f):
                shutil.copy(f, upgrade_tester_dir)
        # copy the profile
        if os.path.exists(self.profile):
            print "Copying '%s' to '%s' " % (self.profile, upgrade_tester_dir)
            shutil.copy(self.profile, upgrade_tester_dir)
        # clean from any leftover pyc files
        for f in glob.glob(upgrade_tester_dir+"/*.pyc"):
            os.unlink(f)
        # make sure we run the upgrade
#        open(self.target+"/etc/rc.local","w").write("""
##!/bin/sh#
#
#LOG=/var/log/dist-upgrade/out.log#
#
##proxy (if required)
#%s#
#
#mkdir /var/log/dist-upgrade
#(cd /upgrade-tester ; ./dist-upgrade.py >> $LOG)#
#
#touch /upgrade-tester/upgrade-finished
#reboot
#""" % self._getProxyLine())

        # remount, ro to read the kernel (sync + mount -o remount,ro might
        # work as well)
        subprocess.call(["umount", self.target])
        res = subprocess.call(["mount","-o","loop,ro",self.image, self.target])
        assert(res == 0)

        print "starting new qemu instance"
        # start qemu
        self.start()

        # start the upgrader
        ret = self._runInImage(["(%s ;cd /upgrade-tester/ ; ./dist-upgrade.py; sync)" % self._getProxyLine()], withProxy=False)
        # FIXME: - do something useful with ret
        #        - reboot and see what things look like

        self.stop()

        subprocess.call(["umount", self.target])
        res = subprocess.call(["mount","-o","loop,ro",self.image, self.target])
        assert(res == 0)

        # copy the result
        for f in glob.glob(self.target+"/var/log/dist-upgrade/*"):
            print "copying result to: ", self.resultdir
            shutil.copy(f, self.resultdir)

        return True

    def test(self):
        # FIXME: add some tests here to see if the upgrade worked
        # this should include:
        # - new kernel is runing (run uname -r in target)
        # - did it sucessfully rebootet
        # - is X runing
        # ...
        return True
        

if __name__ == "__main__":
    import sys
    
    # FIXME: very rough proof of conecpt, unify with the chroot
    #        and automatic-upgrade code
    # see also /usr/sbin/qemu-make-debian-root
    
    qemu = UpgradeTestBackendQemu(sys.argv[1],".")
    #qemu.bootstrap()
    #qemu.start()
    #qemu._runInImage(["ls","/"])
    #qemu.stop()
    qemu.upgrade()

    # FIXME: now write something into rc.local again and run reboot
    #        and see if we come up with the new kernel