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

Side by Side Diff: bin/cros_image_to_target.py

Issue 5176002: cros_image_to_target handles test images, less verbose (Closed) Base URL: ssh://git@gitrw.chromium.org:9222/crosutils.git@master
Patch Set: moved to SILENT/INFO/DEBUG constants Created 10 years, 1 month 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 | Annotate | Revision Log
« no previous file with comments | « no previous file | cros_generate_update_payload » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/python 1 #!/usr/bin/python
2 # 2 #
3 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved. 3 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
4 # Use of this source code is governed by a BSD-style license that can be 4 # Use of this source code is governed by a BSD-style license that can be
5 # found in the LICENSE file. 5 # found in the LICENSE file.
6 6
7 """Create and copy update image to target host. 7 """Create and copy update image to target host.
8 8
9 auto-update and devserver change out from beneath us often enough 9 auto-update and devserver change out from beneath us often enough
10 that despite having to duplicate a litte code, it seems that the 10 that despite having to duplicate a litte code, it seems that the
(...skipping 12 matching lines...) Expand all
23 import sys 23 import sys
24 import tempfile 24 import tempfile
25 import time 25 import time
26 import traceback 26 import traceback
27 27
28 from xml.dom import minidom 28 from xml.dom import minidom
29 29
30 30
31 # This is the default filename within the image directory to load updates from 31 # This is the default filename within the image directory to load updates from
32 DEFAULT_IMAGE_NAME = 'chromiumos_image.bin' 32 DEFAULT_IMAGE_NAME = 'chromiumos_image.bin'
33 DEFAULT_IMAGE_NAME_TEST = 'chromiumos_test_image.bin'
33 34
34 # The filenames we provide to clients to pull updates 35 # The filenames we provide to clients to pull updates
35 UPDATE_FILENAME = 'update.gz' 36 UPDATE_FILENAME = 'update.gz'
36 STATEFUL_FILENAME = 'stateful.tgz' 37 STATEFUL_FILENAME = 'stateful.tgz'
37 38
38 # How long do we wait for the server to start before launching client 39 # How long do we wait for the server to start before launching client
39 SERVER_STARTUP_WAIT = 1 40 SERVER_STARTUP_WAIT = 1
40 41
41 42
42 class Command(object): 43 class Command(object):
43 """Shell command ease-ups for Python.""" 44 """Shell command ease-ups for Python."""
44 45
45 def __init__(self, env): 46 def __init__(self, env):
46 self.env = env 47 self.env = env
47 48
48 def RunPipe(self, pipeline, infile=None, outfile=None, 49 def RunPipe(self, pipeline, infile=None, outfile=None,
49 capture=False, oneline=False): 50 capture=False, oneline=False, hide_stderr=False):
50 """Perform a command pipeline, with optional input/output filenames.""" 51 """
52 Perform a command pipeline, with optional input/output filenames.
53
54 hide_stderr Don't allow output of stderr (default False)
55 """
51 56
52 last_pipe = None 57 last_pipe = None
53 while pipeline: 58 while pipeline:
54 cmd = pipeline.pop(0) 59 cmd = pipeline.pop(0)
55 kwargs = {} 60 kwargs = {}
56 if last_pipe is not None: 61 if last_pipe is not None:
57 kwargs['stdin'] = last_pipe.stdout 62 kwargs['stdin'] = last_pipe.stdout
58 elif infile: 63 elif infile:
59 kwargs['stdin'] = open(infile, 'rb') 64 kwargs['stdin'] = open(infile, 'rb')
60 if pipeline or capture: 65 if pipeline or capture:
61 kwargs['stdout'] = subprocess.PIPE 66 kwargs['stdout'] = subprocess.PIPE
62 elif outfile: 67 elif outfile:
63 kwargs['stdout'] = open(outfile, 'wb') 68 kwargs['stdout'] = open(outfile, 'wb')
69 if hide_stderr:
70 kwargs['stderr'] = open('/dev/null', 'wb')
64 71
65 self.env.Info('Running: %s' % ' '.join(cmd)) 72 self.env.Debug('Running: %s' % ' '.join(cmd))
66 last_pipe = subprocess.Popen(cmd, **kwargs) 73 last_pipe = subprocess.Popen(cmd, **kwargs)
67 74
68 if capture: 75 if capture:
69 ret = last_pipe.communicate()[0] 76 ret = last_pipe.communicate()[0]
70 if not ret: 77 if not ret:
71 return None 78 return None
72 elif oneline: 79 elif oneline:
73 return ret.rstrip('\r\n') 80 return ret.rstrip('\r\n')
74 else: 81 else:
75 return ret 82 return ret
(...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after
132 [src, 'root@%s:%s' % 139 [src, 'root@%s:%s' %
133 (self.remote, dest)]]) 140 (self.remote, dest)]])
134 141
135 142
136 class CrosEnv(object): 143 class CrosEnv(object):
137 """Encapsulates the ChromeOS build system environment functionality.""" 144 """Encapsulates the ChromeOS build system environment functionality."""
138 145
139 REBOOT_START_WAIT = 5 146 REBOOT_START_WAIT = 5
140 REBOOT_WAIT_TIME = 60 147 REBOOT_WAIT_TIME = 60
141 148
142 def __init__(self, verbose=False): 149 SILENT = 0
150 INFO = 1
151 DEBUG = 2
152
153 def __init__(self, verbose=SILENT):
143 self.cros_root = os.path.dirname(os.path.abspath(sys.argv[0])) 154 self.cros_root = os.path.dirname(os.path.abspath(sys.argv[0]))
144 parent = os.path.dirname(self.cros_root) 155 parent = os.path.dirname(self.cros_root)
145 if os.path.exists(os.path.join(parent, 'chromeos-common.sh')): 156 if os.path.exists(os.path.join(parent, 'chromeos-common.sh')):
146 self.cros_root = parent 157 self.cros_root = parent
147 self.cmd = Command(self) 158 self.cmd = Command(self)
148 self.verbose = verbose 159 self.verbose = verbose
149 160
161 # do we have the pv progress tool? (sudo apt-get install pv)
162 self.have_pv = True
163 try:
164 self.cmd.Output('pv', '--help')
165 except OSError:
166 self.have_pv = False
167
150 def Error(self, msg): 168 def Error(self, msg):
151 print >> sys.stderr, 'ERROR: %s' % msg 169 print >> sys.stderr, 'ERROR: %s' % msg
152 170
153 def Fatal(self, msg=None): 171 def Fatal(self, msg=None):
154 if msg: 172 if msg:
155 self.Error(msg) 173 self.Error(msg)
156 sys.exit(1) 174 sys.exit(1)
157 175
158 def Info(self, msg): 176 def Info(self, msg):
159 if self.verbose: 177 if self.verbose >= CrosEnv.INFO:
160 print 'INFO: %s' % msg 178 print 'INFO: %s' % msg
161 179
180 def Debug(self, msg):
181 if self.verbose >= CrosEnv.DEBUG:
182 print 'DEBUG: %s' % msg
183
162 def CrosUtilsPath(self, filename): 184 def CrosUtilsPath(self, filename):
163 return os.path.join(self.cros_root, filename) 185 return os.path.join(self.cros_root, filename)
164 186
165 def ChrootPath(self, filename): 187 def ChrootPath(self, filename):
166 return self.CrosUtilsPath(os.path.join('..', '..', 'chroot', 188 return self.CrosUtilsPath(os.path.join('..', '..', 'chroot',
167 filename.strip(os.path.sep))) 189 filename.strip(os.path.sep)))
168 190
169 def FileOneLine(self, filename): 191 def FileOneLine(self, filename):
170 return file(filename).read().rstrip('\r\n') 192 return file(filename).read().rstrip('\r\n')
171 193
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after
248 def CreateServer(self, port, update_file, stateful_file): 270 def CreateServer(self, port, update_file, stateful_file):
249 """Start the devserver clone.""" 271 """Start the devserver clone."""
250 272
251 PingUpdateResponse.Setup(self.GetHash(update_file), 273 PingUpdateResponse.Setup(self.GetHash(update_file),
252 self.GetSha256(update_file), 274 self.GetSha256(update_file),
253 self.GetSize(update_file)) 275 self.GetSize(update_file))
254 276
255 UpdateHandler.SetupUrl('/update', PingUpdateResponse()) 277 UpdateHandler.SetupUrl('/update', PingUpdateResponse())
256 UpdateHandler.SetupUrl('/%s' % UPDATE_FILENAME, 278 UpdateHandler.SetupUrl('/%s' % UPDATE_FILENAME,
257 FileUpdateResponse(update_file, 279 FileUpdateResponse(update_file,
258 verbose=self.verbose)) 280 verbose=self.verbose,
281 have_pv=self.have_pv))
259 UpdateHandler.SetupUrl('/%s' % STATEFUL_FILENAME, 282 UpdateHandler.SetupUrl('/%s' % STATEFUL_FILENAME,
260 FileUpdateResponse(stateful_file, 283 FileUpdateResponse(stateful_file,
261 verbose=self.verbose)) 284 verbose=self.verbose,
285 have_pv=self.have_pv))
262 286
263 self.http_server = BaseHTTPServer.HTTPServer(('', port), UpdateHandler) 287 self.http_server = BaseHTTPServer.HTTPServer(('', port), UpdateHandler)
264 288
265 def StartServer(self): 289 def StartServer(self):
266 self.Info('Starting http server') 290 self.Info('Starting http server')
267 self.http_server.serve_forever() 291 self.http_server.serve_forever()
268 292
269 def GetUpdateStatus(self): 293 def GetUpdateStatus(self):
270 status = self.ssh_cmd.Output('/usr/bin/update_engine_client', '--status') 294 status = self.ssh_cmd.Output('/usr/bin/update_engine_client', '--status')
271 if not status: 295 if not status:
(...skipping 18 matching lines...) Expand all
290 self.Info('Client has not yet restarted (try %d). Waiting...' % attempt) 314 self.Info('Client has not yet restarted (try %d). Waiting...' % attempt)
291 wait_time = SSHCommand.CONNECT_TIMEOUT - (time.time() - start) 315 wait_time = SSHCommand.CONNECT_TIMEOUT - (time.time() - start)
292 if wait_time > 0: 316 if wait_time > 0:
293 time.sleep(wait_time) 317 time.sleep(wait_time)
294 318
295 return False 319 return False
296 320
297 def StartClient(self, port): 321 def StartClient(self, port):
298 """Ask the client machine to update from our server.""" 322 """Ask the client machine to update from our server."""
299 323
324 self.Info("Starting client...")
300 status = self.GetUpdateStatus() 325 status = self.GetUpdateStatus()
301 if status != 'UPDATE_STATUS_IDLE': 326 if status != 'UPDATE_STATUS_IDLE':
302 self.Error('Client update status is not IDLE: %s' % status) 327 self.Error('Client update status is not IDLE: %s' % status)
303 return False 328 return False
304 329
305 url_base = 'http://localhost:%d' % port 330 url_base = 'http://localhost:%d' % port
306 update_url = '%s/update' % url_base 331 update_url = '%s/update' % url_base
307 fd, update_log = tempfile.mkstemp(prefix='image-to-target-') 332 fd, update_log = tempfile.mkstemp(prefix='image-to-target-')
308 self.Info('Starting update on client. Client output stored to %s' % 333 self.Info('Starting update on client. Client output stored to %s' %
309 update_log) 334 update_log)
335
336 # this will make the client read the files we have set up
310 self.ssh_cmd.Run('/usr/bin/update_engine_client', '--update', 337 self.ssh_cmd.Run('/usr/bin/update_engine_client', '--update',
311 '--omaha_url', update_url, remote_tunnel=(port, port), 338 '--omaha_url', update_url, remote_tunnel=(port, port),
312 outfile=update_log) 339 outfile=update_log)
313 340
314 if self.GetUpdateStatus() != 'UPDATE_STATUS_UPDATED_NEED_REBOOT': 341 if self.GetUpdateStatus() != 'UPDATE_STATUS_UPDATED_NEED_REBOOT':
315 self.Error('Client update failed') 342 self.Error('Client update failed')
316 return False 343 return False
317 344
345 self.Info('Update complete - running update script on client')
318 self.ssh_cmd.Copy(self.CrosUtilsPath('../platform/dev/stateful_update'), 346 self.ssh_cmd.Copy(self.CrosUtilsPath('../platform/dev/stateful_update'),
319 '/tmp') 347 '/tmp')
320 if not self.ssh_cmd.Run('/tmp/stateful_update', url_base, 348 if not self.ssh_cmd.Run('/tmp/stateful_update', url_base,
321 remote_tunnel=(port, port)): 349 remote_tunnel=(port, port)):
322 self.Error('Client stateful update failed') 350 self.Error('Client stateful update failed')
323 return False 351 return False
324 352
325 self.Info('Rebooting client') 353 self.Info('Rebooting client')
326 if not self.ClientReboot(): 354 if not self.ClientReboot():
327 self.Error('Client may not have successfully rebooted...') 355 self.Error('Client may not have successfully rebooted...')
328 return False 356 return False
329 357
330 print 'Client update completed successfully!' 358 self.Info('Client update completed successfully!')
331 return True 359 return True
332 360
333 361
334 class UpdateResponse(object): 362 class UpdateResponse(object):
335 """Default response is the 404 error response.""" 363 """Default response is the 404 error response."""
336 364
337 def Reply(self, handler, send_content=True, post_data=None): 365 def Reply(self, handler, send_content=True, post_data=None):
338 handler.send_Error(404, 'File not found') 366 handler.send_error(404, 'File not found')
339 return None 367 return None
340 368
341 369
342 class FileUpdateResponse(UpdateResponse): 370 class FileUpdateResponse(UpdateResponse):
343 """Respond by sending the contents of a file.""" 371 """Respond by sending the contents of a file."""
344 372
345 def __init__(self, filename, content_type='application/octet-stream', 373 def __init__(self, filename, content_type='application/octet-stream',
346 verbose=False, blocksize=16*1024): 374 verbose=False, blocksize=16*1024, have_pv=False):
347 self.filename = filename 375 self.filename = filename
348 self.content_type = content_type 376 self.content_type = content_type
349 self.verbose = verbose 377 self.verbose = verbose
350 self.blocksize = blocksize 378 self.blocksize = blocksize
379 self.have_pv = have_pv
351 380
352 def Reply(self, handler, send_content=True, post_data=None): 381 def Reply(self, handler, send_content=True, post_data=None):
353 """Return file contents to the client. Optionally display progress.""" 382 """Return file contents to the client. Optionally display progress."""
354 383
355 try: 384 try:
356 f = open(self.filename, 'rb') 385 f = open(self.filename, 'rb')
357 except IOError: 386 except IOError:
358 return UpdateResponse.Reply(self, handler) 387 return UpdateResponse.Reply(self, handler)
359 388
360 handler.send_response(200) 389 handler.send_response(200)
361 handler.send_header('Content-type', self.content_type) 390 handler.send_header('Content-type', self.content_type)
362 filestat = os.fstat(f.fileno()) 391 filestat = os.fstat(f.fileno())
363 filesize = filestat[6] 392 filesize = filestat[6]
364 handler.send_header('Content-Length', str(filesize)) 393 handler.send_header('Content-Length', str(filesize))
365 handler.send_header('Last-Modified', 394 handler.send_header('Last-Modified',
366 handler.date_time_string(filestat.st_mtime)) 395 handler.date_time_string(filestat.st_mtime))
367 handler.end_headers() 396 handler.end_headers()
368 397
369 if not send_content: 398 if send_content:
370 return
371
372 if filesize <= self.blocksize:
373 handler.wfile.write(f.read())
374 else:
375 sent_size = 0 399 sent_size = 0
376 sent_percentage = None 400 sent_percentage = None
401
402 #TODO(sjg): this should use pv also
377 while True: 403 while True:
378 buf = f.read(self.blocksize) 404 buf = f.read(self.blocksize)
379 if not buf: 405 if not buf:
380 break 406 break
381 handler.wfile.write(buf) 407 handler.wfile.write(buf)
382 if self.verbose: 408 if self.verbose:
383 sent_size += len(buf) 409 sent_size += len(buf)
384 percentage = int(100 * sent_size / filesize) 410 percentage = int(100 * sent_size / filesize)
385 if sent_percentage != percentage: 411 if sent_percentage != percentage:
386 sent_percentage = percentage 412 sent_percentage = percentage
(...skipping 155 matching lines...) Expand 10 before | Expand all | Expand 10 after
542 usage = 'usage: %prog [options]' 568 usage = 'usage: %prog [options]'
543 parser = optparse.OptionParser(usage=usage) 569 parser = optparse.OptionParser(usage=usage)
544 parser.add_option('--board', dest='board', default=None, 570 parser.add_option('--board', dest='board', default=None,
545 help='Board platform type') 571 help='Board platform type')
546 parser.add_option('--force-mismatch', dest='force_mismatch', default=False, 572 parser.add_option('--force-mismatch', dest='force_mismatch', default=False,
547 action='store_true', 573 action='store_true',
548 help='Upgrade even if client arch does not match') 574 help='Upgrade even if client arch does not match')
549 parser.add_option('--from', dest='src', default=None, 575 parser.add_option('--from', dest='src', default=None,
550 help='Source image to install') 576 help='Source image to install')
551 parser.add_option('--image-name', dest='image_name', 577 parser.add_option('--image-name', dest='image_name',
552 default=DEFAULT_IMAGE_NAME,
553 help='Filename within image directory to load') 578 help='Filename within image directory to load')
554 parser.add_option('--port', dest='port', default=8081, type='int', 579 parser.add_option('--port', dest='port', default=8081, type='int',
555 help='TCP port to serve from and tunnel through') 580 help='TCP port to serve from and tunnel through')
556 parser.add_option('--remote', dest='remote', default=None, 581 parser.add_option('--remote', dest='remote', default=None,
557 help='Remote device-under-test IP address') 582 help='Remote device-under-test IP address')
558 parser.add_option('--server-only', dest='server_only', default=False, 583 parser.add_option('--server-only', dest='server_only', default=False,
559 action='store_true', help='Do not start client') 584 action='store_true', help='Do not start client')
560 parser.add_option('--verbose', dest='verbose', default=False, 585 parser.add_option('--verbose', dest='verbose', default=False,
586 action='store_true', help='Display progress')
587 parser.add_option('--debug', dest='debug', default=False,
561 action='store_true', help='Display running commands') 588 action='store_true', help='Display running commands')
589 parser.add_option('--test', dest='test', default=False,
590 action='store_true', help='Select test image')
562 591
563 (options, args) = parser.parse_args(argv) 592 (options, args) = parser.parse_args(argv)
564 593
565 cros_env = CrosEnv(verbose=options.verbose) 594 # we can build the test image if it doesn't exist, so remember if we want to
595 build_test_image = False
596
597 verbosity = CrosEnv.SILENT
598 if options.verbose:
599 verbosity = CrosEnv.INFO
600 if options.debug:
601 verbosity = CrosEnv.DEBUG
602 cros_env = CrosEnv(verbose=verbosity)
566 603
567 if not options.board: 604 if not options.board:
568 options.board = cros_env.GetDefaultBoard() 605 options.board = cros_env.GetDefaultBoard()
569 606
570 if not options.src: 607 if not options.src:
571 options.src = cros_env.GetLatestImage(options.board) 608 options.src = cros_env.GetLatestImage(options.board)
572 if options.src is None: 609 if options.src is None:
573 parser.error('No --from argument given and no default image found') 610 parser.error('No --from argument given and no default image found')
574 611
575 cros_env.Info('Performing update from %s' % options.src) 612 cros_env.Info('Performing update from %s' % options.src)
576 613
577 if not os.path.exists(options.src): 614 if not os.path.exists(options.src):
578 parser.error('Path %s does not exist' % options.src) 615 parser.error('Path %s does not exist' % options.src)
579 616
617 if not options.image_name:
618 # auto-select the correct image
619 if options.test:
620 options.image_name = DEFAULT_IMAGE_NAME_TEST
621
622 # we will build the test image if not found
623 build_test_image = True
624 else:
625 options.image_name = DEFAULT_IMAGE_NAME
626
580 if os.path.isdir(options.src): 627 if os.path.isdir(options.src):
581 image_directory = options.src 628 image_directory = options.src
582 image_file = os.path.join(options.src, options.image_name) 629 image_file = os.path.join(options.src, options.image_name)
583 630
584 if not os.path.exists(image_file): 631 if not os.path.exists(image_file):
632 if build_test_image:
633 # we want a test image but it doesn't exist
634 # try to build it if we can
635 cros_env.Info('Creating test image')
636 test_output = cros_env.cmd.Output(
637 cros_env.CrosUtilsPath('enter_chroot.sh'),
638 '--', './mod_image_for_test.sh',
639 '--board=%s' % options.board, '-y')
640 if not os.path.exists(image_file):
641 print test_output
642 cros_env.Fatal('Failed to create test image - please run '
643 './mod_image_for_test.sh manually inside the chroot')
585 parser.error('Image file %s does not exist' % image_file) 644 parser.error('Image file %s does not exist' % image_file)
586 else: 645 else:
587 image_file = options.src 646 image_file = options.src
588 image_directory = os.path.dirname(options.src) 647 image_directory = os.path.dirname(options.src)
589 648
649 update_file = os.path.join(image_directory, UPDATE_FILENAME)
650 stateful_file = os.path.join(image_directory, STATEFUL_FILENAME)
651
652 cros_env.Debug("Image file %s" % image_file)
653 cros_env.Debug("Update file %s" % update_file)
654 cros_env.Debug("Stateful file %s" % stateful_file)
655
590 if options.remote: 656 if options.remote:
657 cros_env.Info('Contacting client %s' % options.remote)
591 cros_env.SetRemote(options.remote) 658 cros_env.SetRemote(options.remote)
592 rel = cros_env.GetRemoteRelease() 659 rel = cros_env.GetRemoteRelease()
593 if not rel: 660 if not rel:
594 cros_env.Fatal('Could not retrieve remote lsb-release') 661 cros_env.Fatal('Could not retrieve remote lsb-release')
595 board = rel.get('CHROMEOS_RELEASE_BOARD', '(None)') 662 board = rel.get('CHROMEOS_RELEASE_BOARD', '(None)')
596 if board != options.board and not options.force_mismatch: 663 if board != options.board and not options.force_mismatch:
597 cros_env.Error('Board %s does not match expected %s' % 664 cros_env.Error('Board %s does not match expected %s' %
598 (board, options.board)) 665 (board, options.board))
599 cros_env.Error('(Use --force-mismatch option to override this)') 666 cros_env.Error('(Use --force-mismatch option to override this)')
600 cros_env.Fatal() 667 cros_env.Fatal()
601 668
602 elif not options.server_only: 669 elif not options.server_only:
603 parser.error('Either --server-only must be specified or ' 670 parser.error('Either --server-only must be specified or '
604 '--remote=<client> needs to be given') 671 '--remote=<client> needs to be given')
605 672
606 update_file = os.path.join(image_directory, UPDATE_FILENAME)
607 stateful_file = os.path.join(image_directory, STATEFUL_FILENAME)
608
609 if (not cros_env.GenerateUpdatePayload(image_file, update_file) or 673 if (not cros_env.GenerateUpdatePayload(image_file, update_file) or
610 not cros_env.BuildStateful(image_file, image_directory, stateful_file)): 674 not cros_env.BuildStateful(image_file, image_directory, stateful_file)):
611 cros_env.Fatal() 675 cros_env.Fatal()
612 676
613 cros_env.CreateServer(options.port, update_file, stateful_file) 677 cros_env.CreateServer(options.port, update_file, stateful_file)
614 678
615 exit_status = 1 679 exit_status = 1
616 if options.server_only: 680 if options.server_only:
617 child = None 681 child = None
618 else: 682 else:
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after
650 714
651 if child: 715 if child:
652 os.kill(child, 15) 716 os.kill(child, 15)
653 717
654 cros_env.Info('Server exiting with status %d' % exit_status) 718 cros_env.Info('Server exiting with status %d' % exit_status)
655 sys.exit(exit_status) 719 sys.exit(exit_status)
656 720
657 721
658 if __name__ == '__main__': 722 if __name__ == '__main__':
659 main(sys.argv) 723 main(sys.argv)
OLDNEW
« no previous file with comments | « no previous file | cros_generate_update_payload » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698