Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(199)

Side by Side Diff: src/platform/memento_softwareupdate/autoupdated_vm_test.py

Issue 1512001: test-fixet: Automated VMWare test of AutoUpdate (Closed)
Patch Set: fixes for review Created 10 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « src/platform/installer/chromeos-common.sh ('k') | src/scripts/image_to_vmware.sh » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/usr/bin/python
2
3 # This is a fully automated VM test of autoupdate. Here's what it does:
4 # - Downloads the latest dev channel image
5 # - Creates a VMware image based on that image
6 # - Creates an update for the image under test
7 # - Creates a copy of that dev channel image that's one number higher than
8 # the image under test (called the rollback image)
9 # - Creates an update for the rollback image
10 # - Launches a local HTTP server to pretend to be the AU server
11 # - Fires up the VM and waits for it to contact the AU server
12 # - AU server gives the VM the image under test update
13 # - Waits for the image to be installed, then reboots the VM
14 # - AU server gives the VM the rollback image
15 # - Waits for the image to be installed, then reboots the VM
16 # - Waits for the image to ping the AU server with the rollback image
17 # - Done!
18
19 # Run this program by passing it a path to the build directory you want
20 # to test (this directory should contain rootfs.image).
21
22 from xml.dom import minidom
23 from BaseHTTPServer import BaseHTTPRequestHandler
24 from BaseHTTPServer import HTTPServer
25
26 import os
27 import re
28 import signal
29 import socket
30 import string
31 import subprocess
32 import sys
33 import tempfile
34 import threading
35 import time
36
37 tmp_dir = '/tmp/au_vm_test'
38 scripts_dir = '../../scripts/'
39
40 original_version = '0.0.0.0'
41 test_version = '0.0.0.0'
42
43 # This class stores the state that the server is in and serves as glue
44 # between the AU server the control of the VMware process
45 class TestState(object):
46 # States we can be in
47 INIT, \
48 INITIAL_WAIT, \
49 TEST_DOWNLOAD, \
50 TEST_WAIT, \
51 ROLLBACK_DOWNLOAD, \
52 ROLLBACK_WAIT = \
53 xrange(6)
54
55 def __init__(self, orig_vers, test_vers, rollback_vers, vm):
56 self.reboot_timeout_seconds = 60 * 4
57 self.orig_vers = orig_vers
58 self.test_vers = test_vers
59 self.rollback_vers = rollback_vers
60 self.SetState(TestState.INIT)
61 self.vm = vm
62
63 def Die(self, message):
64 print message
65 self.vm.Destroy()
66 # TODO(adlr): exit the entire process, not just this tread
67 sys.exit(1)
68
69 def SetState(self, state):
70 self.state = state
71
72 # Should be called to start the VM initially
73 def Start(self):
74 if self.state != TestState.INIT:
75 self.Die('Start called while in bad state')
76 self.SetState(TestState.INITIAL_WAIT)
77 self.vm.Launch()
78 # Kick off timer to wait for the AU ping
79 self.timer = threading.Timer(self.reboot_timeout_seconds,
80 self.StartupTimeout)
81 self.timer.start()
82
83 def StartupTimeout(self):
84 self.Die('VM Failed to start and ping' + str(id(self)))
85 # TODO(adlr): exit the entire process, not just this tread
86 sys.exit(1)
87
88 def FinishInstallTimeout(self):
89 self.vm.Shutdown()
90 self.vm.Launch()
91 self.timer = threading.Timer(60 * 5, # Sometimes VMWare is very slow
92 self.StartupTimeout)
93 self.timer.start()
94
95 # Called by AU server when an update request comes in. Should return
96 # the version that the server should return to the AU client, or
97 # None if no update.
98 def HandleUpdateRequest(self, from_version):
99 print 'HandleUpdateRequest(%s) id:%s state:%s' % \
100 (from_version, id(self), self.state)
101 ret = None
102 # Only cancel timer if we're waiting for the machine to startup
103 if self.timer != None and self.state == TestState.INITIAL_WAIT and \
104 from_version == self.orig_vers:
105 print 'Successfully booted initial'
106 self.timer.cancel()
107 self.timer = None
108 elif self.timer != None and self.state == TestState.TEST_WAIT and \
109 from_version == self.test_vers:
110 print 'Successfully booted test'
111 self.timer.cancel()
112 self.timer = None
113 elif self.timer != None and self.state == TestState.ROLLBACK_WAIT and \
114 from_version == self.rollback_vers:
115 print 'Successfully booted rollback'
116 self.timer.cancel()
117 self.timer = None
118 print 'All done!'
119 # TODO(adlr): exit the entire process, not just this tread
120 sys.exit(0)
121
122 # Pick the version to return
123 if from_version == self.orig_vers:
124 ret = self.test_vers
125 elif from_version == self.test_vers:
126 ret = self.rollback_vers
127
128 # Checks to make sure we move through states correctly
129 if from_version == self.orig_vers:
130 if self.state != TestState.INITIAL_WAIT and \
131 self.state != TestState.TEST_DOWNLOAD and \
132 self.state != TestState.INITIAL_WAIT:
133 self.Die('Error: Request from %s while in state %s' %
134 (from_version, self.state))
135 elif from_version == self.test_vers:
136 if self.state != TestState.TEST_WAIT and \
137 self.state != TestState.ROLLBACK_DOWNLOAD and \
138 self.state != TestState.ROLLBACK_WAIT:
139 self.Die('Error: Request from %s while in state %s' %
140 (from_version, self.state))
141 else:
142 print 'odd version to be pinged from: %s' % from_version
143 print 'state is %s' % self.state
144
145 # Update state if needed
146 if self.state == TestState.INITIAL_WAIT:
147 self.SetState(TestState.TEST_DOWNLOAD)
148 elif self.state == TestState.TEST_WAIT:
149 self.SetState(TestState.ROLLBACK_DOWNLOAD)
150
151 if ret is not None:
152 return ret
153 print 'Ignoring update request while in state %s' % self.state
154 return ''
155
156 # Called by AU server when the AU client has finished downloading an image
157 def ImageDownloadComplete(self):
158 print 'ImageDownloadComplete()'
159 valid_state = False
160 if self.state == TestState.TEST_DOWNLOAD:
161 valid_state = True
162 self.SetState(TestState.TEST_WAIT)
163 if self.state == TestState.ROLLBACK_DOWNLOAD:
164 valid_state = True
165 self.SetState(TestState.ROLLBACK_WAIT)
166 if not valid_state:
167 print 'Image download done called at invalid state'
168 # TODO(adlr): exit the entire process, not just this tread
169 sys.exit(1)
170 # Put a timer to reboot the VM
171 if self.timer is not None:
172 self.timer.cancel()
173 self.timer = None
174 self.timer = threading.Timer(self.reboot_timeout_seconds,
175 self.FinishInstallTimeout)
176 self.timer.start()
177 return
178
179 # This subclass of HTTPServer contains info about the versions of
180 # software that the AU server should know about. The AUServerHandler
181 # object(s) will access this data.
182 class AUHTTPServer(HTTPServer):
183 def __init__(self, ip_port, klass):
184 HTTPServer.__init__(self, ip_port, klass)
185 self.update_info = {}
186 self.files = {}
187
188 # For a given version of the software, the URL, size, and hash of the update
189 # that gives the user that version of the software.
190 def AddUpdateInfo(self, version, url, size, the_hash):
191 self.update_info[version] = (url, the_hash, size)
192 return
193
194 # For a given path part of a url, return to the client the file at file_path
195 def AddServedFile(self, url_path, file_path):
196 self.files[url_path] = file_path
197
198 def SetTestState(self, test_state):
199 self.test_state = test_state
200
201 # This class handles HTTP requests. POST requests are when the client
202 # is pinging to see if there's an update. GET requests are to download
203 # an update.
204 class AUServerHandler(BaseHTTPRequestHandler):
205 def do_GET(self):
206 self.send_response(200)
207 self.end_headers()
208 print 'GET: %s' % self.path
209
210 if self.server.files[self.path] != None:
211 print 'GET returning path %s' % self.server.files[self.path]
212 f = open(self.server.files[self.path])
213 while True:
214 data = f.read(1024 * 1024 * 8)
215 if not data:
216 break
217 self.wfile.write(data)
218 self.wfile.flush()
219 f.close()
220 self.server.test_state.ImageDownloadComplete()
221 else:
222 print 'GET returning no path'
223 self.wfile.write(self.path + '\n')
224 return
225
226 def do_POST(self):
227 # Parse the form data posted
228 post_length = int(self.headers.getheader('content-length'))
229 post_data = self.rfile.read(post_length)
230
231 update_dom = minidom.parseString(post_data)
232 root = update_dom.firstChild
233 query = root.getElementsByTagName('o:app')[0]
234 client_version = query.getAttribute('version')
235 print 'Got update request from %s' % client_version
236
237 # Send response
238 self.send_response(200)
239 self.end_headers()
240
241 new_version = self.server.test_state.HandleUpdateRequest(client_version)
242 print 'Appropriate new version is: %s' % new_version
243
244 if self.server.update_info[new_version] == None:
245 print 'Not sure how to serve reply for %s' % new_version
246 return
247
248 payload = """<?xml version="1.0" encoding="UTF-8"?>
249 <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
250 <app appid="{87efface-864d-49a5-9bb3-4b050a7c227a}" status="ok">
251 <ping status="ok"/>
252 <updatecheck
253 codebase="%s"
254 hash="%s"
255 needsadmin="false"
256 size="%s"
257 status="ok"/>
258 </app>
259 </gupdate>
260 """ % self.server.update_info[new_version]
261
262 self.wfile.write(payload)
263 return
264
265 # A wrapper for the vmplayer process. Can Launch/Shutdown a vm.
266 class vmplayer(object):
267 def __init__(self, filename):
268 self.filename = filename
269
270 # Launch may (read: probably will) return before the OS has booted
271 def Launch(self):
272 self.process = subprocess.Popen(['/usr/bin/vmplayer', self.filename])
273 self.running = True
274
275 def Destroy(self):
276 if self.running:
277 self.Shutdown()
278
279 # Shutdown will not return until the vmplayer process has fully terminated
280 # and any cleanup is done.
281 def Shutdown(self):
282 # Pretend user sent Ctrl-C to the vmplayer process
283 os.kill(self.process.pid, signal.SIGINT)
284
285 # Wait while vmplayer saves the vm state...
286 self.process.wait()
287
288 # Delete the saved vm state
289 # TODO(adlr): remove the state file from the disk
290 self.process = None
291 subprocess.check_call(['/bin/sed', '-i', '/checkpoint.vmState/d',
292 self.filename])
293 self.running = False
294
295 def MakePath(path):
296 subprocess.check_call(['/bin/mkdir', '-p', path])
297
298 def DownloadLatestImage(out_path):
299 url = 'http://codf30.jail.google.com/internal/archive/' + \
300 'x86-image-official/LATEST-dev-channel/image.zip' + \
301 '.NOT_SAFE_FOR_USB_INSTALL'
302 url = 'http://www.corp.google.com/~adlr/adlr_test_orig.zip'
303 subprocess.check_call(['/usr/bin/wget', '-O', out_path, url])
304
305 def UnzipImage(path, directory):
306 subprocess.check_call(['/usr/bin/unzip', path, '-d', directory])
307
308 # Create a stateful partition with a text file that points the AU client
309 # to localhost at local_port (which is this very script).
310 def CreateDefaultStatefulPartition(local_ip, local_port, out_dir, out_file):
311 # Create sparse file for partition
312 part_size = 512 * 1024 * 1024 # bytes, so 500 MiB
313
314 subprocess.check_call(["""#!/bin/bash
315 set -ex
316 OUT_FILE="%s"
317 STATE_DIR="%s"
318 SIZE="%s"
319 dd if=/dev/zero of="$OUT_FILE" bs=1 count=1 seek=$(($SIZE - 1))
320 mkfs.ext3 -F "$OUT_FILE"
321 mkdir -p "$STATE_DIR"
322 sudo mount -o loop "$OUT_FILE" "$STATE_DIR"
323 sudo mkdir -p "$STATE_DIR/etc"
324 cat <<EOF |sudo dd of="$STATE_DIR/etc/lsb-release"
325 CHROMEOS_AUSERVER=http://%s:%s/update
326 HTTP_SERVER_OVERRIDE=true
327 EOF
328 for i in "$STATE_DIR/etc/lsb-release" "$STATE_DIR/etc" "$STATE_DIR"; do
329 sudo chown root:root "$i"
330 if [ -d "$i" ]; then
331 sudo chmod 0755 "$i"
332 else
333 sudo chmod 0644 "$i"
334 fi
335 done
336 sudo umount -d "$STATE_DIR"
337 """ % (out_file, out_dir + '/state', part_size, local_ip, local_port)],
338 shell=True)
339 return
340
341
342 def CreateVMForImage(image_dir):
343 subprocess.check_call([scripts_dir + 'image_to_vmware.sh', '--from',
344 image_dir, '--to', image_dir, '--state_image',
345 image_dir + '/state.image'])
346 return
347
348 # Returns (size, hash, path) of the generated image.gz for the given rootfs
349 def CreateUpdateForImage(rootfs_image):
350 output = subprocess.Popen([scripts_dir + 'mk_memento_images.sh',
351 rootfs_image],
352 stdout=subprocess.PIPE).communicate()[0]
353 matches = re.search('Success. hash is ([^\n]+)', output)
354 the_hash = matches.group(1)
355 path = os.path.dirname(rootfs_image) + '/update.gz'
356 size = os.path.getsize(path)
357 return (size, the_hash, path)
358
359 # Modify rootfs 'root_img' to have a new version new_version
360 def ModifyImageForRollback(root_img, new_version):
361 subprocess.check_call(["""#!/bin/bash
362 set -ex
363 DIR=$(mktemp -d)
364 sudo mount -o loop "%s" $DIR
365 # update versions in lsb-release
366 sudo sed -i \\
367 -e 's/\\(^GOOGLE_RELEASE=\\|CHROMEOS_RELEASE_VERSION=\\).*/\\1%s/' \\
368 -e 's/^\\(CHROMEOS_RELEASE_DESCRIPTION=.*\\)/\\1-ROLLBACK/' \\
369 "$DIR"/etc/lsb-release
370 sudo umount -d $DIR
371 """ % (root_img, new_version)], shell=True)
372
373 # Returns the release version of a rootfs (e.g. 0.6.39.201003241739-a1)
374 def GetVersionForRootfs(rootfs_image):
375 mount_dir = tempfile.mkdtemp()
376 subprocess.check_call(['sudo', 'mount', '-o', 'loop,ro',
377 rootfs_image, mount_dir])
378 version = subprocess.Popen(['awk', '-F', '=',
379 '/GOOGLE_RELEASE=/{print $2}',
380 mount_dir + '/etc/lsb-release'],
381 stdout=subprocess.PIPE).communicate()[0].rstrip()
382 subprocess.check_call(['sudo', 'umount', '-d', mount_dir])
383 subprocess.check_call(['sudo', 'rm', '-rf', mount_dir])
384 return version
385
386 # For a given version, increment the last number by 1. E.g.:
387 # IncrementVersionNumber('0.23.144.842') = '0.23.144.843'
388 def IncrementVersionNumber(version):
389 parts = version.split('.')
390 parts[-1] = str(int(parts[-1]) + 1)
391 return string.join(parts, '.')
392
393 def UnpackRootfs(directory):
394 subprocess.check_call(["""#!/bin/bash -x
395 cd "%s"
396 ./unpack_partitions.sh chromiumos_image.bin
397 mv -f part_3 rootfs.image
398 """ % directory], shell=True)
399
400 def main():
401 if len(sys.argv) != 2:
402 print 'usage: %s path/to/new/image/dir' % sys.argv[0]
403 sys.exit(1)
404 orig_dir = tmp_dir + '/orig'
405 new_dir = sys.argv[1]
406 rollback_dir = tmp_dir + '/rollback'
407
408 state_image = orig_dir + '/state.image'
409 port = 8080
410
411 MakePath(tmp_dir)
412
413 # Download latest dev channel release
414 orig_zip = tmp_dir + '/orig.zip'
415 DownloadLatestImage(orig_zip)
416 UnzipImage(orig_zip, orig_dir)
417 UnpackRootfs(orig_dir)
418 orig_version = GetVersionForRootfs(orig_dir + '/rootfs.image')
419 print 'Have original image at version: %s' % orig_version
420
421 # Create new AU image
422 print 'Creating update.gz for test image'
423 UnpackRootfs(new_dir)
424 new_update_details = CreateUpdateForImage(new_dir + '/rootfs.image')
425 new_version = GetVersionForRootfs(new_dir + '/rootfs.image')
426 print 'Have test image at version: %s' % new_version
427
428 # Create rollback image
429 rollback_version = IncrementVersionNumber(new_version)
430 print 'Creating rollback image'
431 UnzipImage(orig_zip, rollback_dir)
432 UnpackRootfs(rollback_dir)
433 ModifyImageForRollback(rollback_dir + '/rootfs.image', rollback_version)
434 print 'Creating update.gz for rollback image'
435 rollback_update_details = CreateUpdateForImage(rollback_dir + '/rootfs.image')
436 print 'Have rollback image at version: %s' % rollback_version
437
438 CreateDefaultStatefulPartition(socket.gethostname(), port, orig_dir,
439 state_image)
440 CreateVMForImage(orig_dir)
441
442 player = vmplayer(orig_dir + '/chromeos.vmx')
443
444 test_state = TestState(orig_version, new_version, rollback_version, player)
445
446 server = AUHTTPServer((socket.gethostname(), port), AUServerHandler)
447
448 base_url = 'http://%s:%s' % (socket.gethostname(), port)
449
450 server.SetTestState(test_state)
451 server.AddUpdateInfo(new_version, base_url + '/' + new_version,
452 new_update_details[0], new_update_details[1])
453 server.AddUpdateInfo(rollback_version, base_url + '/' + rollback_version,
454 rollback_update_details[0], rollback_update_details[1])
455 server.AddServedFile('/' + new_version, new_update_details[2])
456 server.AddServedFile('/' + rollback_version, rollback_update_details[2])
457
458 test_state.Start()
459 print 'Starting server, use <Ctrl-C> to stop'
460 server.serve_forever()
461
462 if __name__ == '__main__':
463 main()
OLDNEW
« no previous file with comments | « src/platform/installer/chromeos-common.sh ('k') | src/scripts/image_to_vmware.sh » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698