blob: ca19e5f1abd1d3961a2ab5b0e1cbec259b33667d [file] [log] [blame]
Chris Sosada9632e2013-03-04 12:28:06 -08001#!/usr/bin/python
2#
3# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Integration test to test the basic functionality of dev-install and gmerge.
8
9This module contains a test that runs some sanity integration tests against
10a VM. First it starts a VM test image and turns it into a base image by wiping
11all of the stateful partition. Once done, runs dev_install to restore the
12stateful partition and then runs gmerge.
13"""
14
Chris Sosab8c2af52013-07-03 10:45:39 -070015import getpass
Chris Sosada9632e2013-03-04 12:28:06 -080016import logging
17import optparse
18import os
19import shutil
20import socket
21import sys
22import tempfile
23
24import constants
25sys.path.append(constants.SOURCE_ROOT)
26sys.path.append(constants.CROS_PLATFORM_ROOT)
27
28from chromite.lib import cros_build_lib
Chris Sosa928085e2013-03-08 17:25:30 -080029from chromite.lib import remote_access
Chris Sosada9632e2013-03-04 12:28:06 -080030from crostestutils.lib import dev_server_wrapper
31from crostestutils.lib import mount_helper
32from crostestutils.lib import test_helper
33
34
35_LOCALHOST = 'localhost'
36_PRIVATE_KEY = os.path.join(constants.CROSUTILS_DIR, 'mod_for_test_scripts',
37 'ssh_keys', 'testing_rsa')
Chris Sosa5bc23bf2013-07-03 12:14:18 -070038_MAX_SSH_ATTEMPTS = 3
Chris Sosada9632e2013-03-04 12:28:06 -080039
40class TestError(Exception):
41 """Raised on any error during testing. It being raised is a test failure."""
42
43
44class DevModeTest(object):
45 """Wrapper for dev mode tests."""
46 def __init__(self, image_path, board, binhost):
47 """
48 Args:
49 image_path: Filesystem path to the image to test.
50 board: Board of the image under test.
51 binhost: Binhost override. Binhost as defined here is where dev-install
52 or gmerge go to search for binary packages. By default this will
53 be set to the devserver url of the host running this script.
54 If no override i.e. the default is ok, set to None.
55 """
56 self.image_path = image_path
57 self.board = board
58 self.binhost = binhost
59
60 self.tmpdir = tempfile.mkdtemp('DevModeTest')
Chris Sosada9632e2013-03-04 12:28:06 -080061 self.tmpkvmpid = os.path.join(self.tmpdir, 'kvm_pid')
62
63 self.working_image_path = None
64 self.devserver = None
Chris Sosa928085e2013-03-08 17:25:30 -080065 self.remote_access = None
Chris Sosada9632e2013-03-04 12:28:06 -080066 self.port = None
67
68 def Cleanup(self):
69 """Clean up any state at the end of the test."""
70 try:
71 if self.working_image_path:
72 os.remove(self.working_image_path)
73
74 if self.devserver:
75 self.devserver.Stop()
76
77 self.devserver = None
Chris Sosa5bc23bf2013-07-03 12:14:18 -070078 self._StopVM()
Chris Sosada9632e2013-03-04 12:28:06 -080079
80 if self.tmpdir:
81 shutil.rmtree(self.tmpdir, ignore_errors=True)
82
83 self.tmpdir = None
84 except Exception:
85 logging.warning('Received error during cleanup', exc_info=True)
86
87 def _SetupSSH(self):
88 """Sets up the necessary items for running ssh."""
89 self.port = self._FindUnusedPort()
Chris Sosa928085e2013-03-08 17:25:30 -080090 self.remote_access = remote_access.RemoteAccess(
91 _LOCALHOST, self.tmpdir, self.port,
92 debug_level=logging.DEBUG, interactive=False)
Chris Sosada9632e2013-03-04 12:28:06 -080093
Chris Sosab8c2af52013-07-03 10:45:39 -070094 def _WipeDevInstall(self):
95 """Wipes the devinstall state."""
Chris Sosada9632e2013-03-04 12:28:06 -080096 r_mount_point = os.path.join(self.tmpdir, 'm')
97 s_mount_point = os.path.join(self.tmpdir, 's')
Chris Sosab8c2af52013-07-03 10:45:39 -070098 dev_image_path = os.path.join(s_mount_point, 'dev_image')
Chris Sosada9632e2013-03-04 12:28:06 -080099 mount_helper.MountImage(self.working_image_path,
100 r_mount_point, s_mount_point, read_only=False,
101 safe=True)
Chris Sosab8c2af52013-07-03 10:45:39 -0700102 try:
103 cros_build_lib.SudoRunCommand(['chown', '--recursive', getpass.getuser(),
104 s_mount_point], debug_level=logging.DEBUG)
105 shutil.rmtree(dev_image_path)
106 finally:
107 mount_helper.UnmountImage(r_mount_point, s_mount_point)
Chris Sosada9632e2013-03-04 12:28:06 -0800108
109 def _FindUnusedPort(self):
110 """Returns a currently unused port."""
111 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
112 s.bind((_LOCALHOST, 0))
113 port = s.getsockname()[1]
114 s.close()
115 return port
116
Chris Sosa5bc23bf2013-07-03 12:14:18 -0700117 def _RobustlyStartVMWithSSH(self):
118 """Start test copy of VM and ensure we can ssh into it.
119
120 This command is more robust than just naively starting the VM as it will
121 try to start the VM multiple times if the VM fails to start up. This is
122 inspired by retry_until_ssh in crosutils/lib/cros_vm_lib.sh.
123 """
124 for _ in range(_MAX_SSH_ATTEMPTS):
125 try:
126 cmd = ['%s/bin/cros_start_vm' % constants.CROSUTILS_DIR,
127 '--ssh_port', str(self.port),
128 '--image_path', self.working_image_path,
129 '--no_graphics',
130 '--kvm_pid', self.tmpkvmpid]
131 cros_build_lib.RunCommand(cmd, debug_level=logging.DEBUG)
132
133 # Ping the VM to ensure we can SSH into it.
134 self.remote_access.RemoteSh(['true'])
135 return
136 except cros_build_lib.RunCommandError as e:
137 logging.warning('Failed to connect to VM')
138 logging.debug(e)
139 self._StopVM()
140 else:
141 raise TestError('Max attempts to connect to VM exceeded')
142
143 def _StopVM(self):
144 """Stops a running VM set up using _RobustlyStartVMWithSSH."""
145 cmd = ['%s/bin/cros_stop_vm' % constants.CROSUTILS_DIR,
146 '--kvm_pid', self.tmpkvmpid]
147 cros_build_lib.RunCommand(cmd, debug_level=logging.DEBUG)
148
Chris Sosada9632e2013-03-04 12:28:06 -0800149 def PrepareTest(self):
150 """Pre-test modification to the image and env to setup test."""
151 logging.info('Setting up the image %s for vm testing.',
152 self.image_path)
153 self._SetupSSH()
154 vm_path = test_helper.CreateVMImage(self.image_path, self.board,
155 full=False)
156
157 logging.info('Making copy of the vm image %s to manipulate.', vm_path)
David James45b55dd2013-04-24 09:16:40 -0700158 self.working_image_path = os.path.join(self.tmpdir,
159 os.path.basename(vm_path))
Chris Sosada9632e2013-03-04 12:28:06 -0800160 shutil.copyfile(vm_path, self.working_image_path)
161 logging.debug('Copy of vm image stored at %s.', self.working_image_path)
162
Chris Sosab8c2af52013-07-03 10:45:39 -0700163 logging.info('Wiping /usr/local/bin from the image.')
164 self._WipeDevInstall()
Chris Sosada9632e2013-03-04 12:28:06 -0800165
166 logging.info('Starting the vm on port %d.', self.port)
Chris Sosa5bc23bf2013-07-03 12:14:18 -0700167 self._RobustlyStartVMWithSSH()
Chris Sosa068c1e92013-03-17 22:54:20 -0700168
Chris Sosada9632e2013-03-04 12:28:06 -0800169 if not self.binhost:
170 logging.info('Starting the devserver.')
171 self.devserver = dev_server_wrapper.DevServerWrapper(self.tmpdir)
172 self.devserver.start()
173 self.devserver.WaitUntilStarted()
174 self.binhost = dev_server_wrapper.DevServerWrapper.GetDevServerURL(
Chris Sosac9447962013-03-12 10:12:29 -0700175 sub_dir='static/pkgroot/%s/packages' % self.board)
Chris Sosada9632e2013-03-04 12:28:06 -0800176
177 logging.info('Using binhost %s', self.binhost)
178
179 def TestDevInstall(self):
180 """Tests that we can run dev-install and have python work afterwards."""
181 try:
182 logging.info('Running dev install in the vm.')
Chris Sosa928085e2013-03-08 17:25:30 -0800183 self.remote_access.RemoteSh(
Chris Sosada9632e2013-03-04 12:28:06 -0800184 ['bash', '-l', '-c',
185 '"/usr/bin/dev_install --yes --binhost %s"' % self.binhost])
186
187 logging.info('Verifying that python works on the image.')
Chris Sosa928085e2013-03-08 17:25:30 -0800188 self.remote_access.RemoteSh(
Chris Sosada9632e2013-03-04 12:28:06 -0800189 ['sudo', '-u', 'chronos', '--',
190 'python', '-c', '"print \'hello world\'"'])
191 except cros_build_lib.RunCommandError as e:
192 self.devserver.PrintLog()
193 logging.error('dev-install test failed. See devserver log above for more '
194 'details.')
195 raise TestError('dev-install test failed with: %s' % str(e))
196
Chris Sosada9632e2013-03-04 12:28:06 -0800197 def TestGmerge(self):
198 """Evaluates whether the test passed or failed."""
Chris Sosa928085e2013-03-08 17:25:30 -0800199 logging.info('Testing that gmerge works on the image after dev install.')
Chris Sosada9632e2013-03-04 12:28:06 -0800200 try:
Chris Sosac9447962013-03-12 10:12:29 -0700201 self.remote_access.RemoteSh(
202 ['gmerge', 'gmerge', '--accept_stable', '--usepkg',
203 '--devserver_url', self.devserver.GetDevServerURL(),
204 '--board', self.board])
Chris Sosada9632e2013-03-04 12:28:06 -0800205 except cros_build_lib.RunCommandError as e:
206 logging.error('gmerge test failed. See log for details')
207 raise TestError('gmerge test failed with: %s' % str(e))
208
209
210def main():
211 usage = ('%s <board> <path_to_[test|vm]_image>. '
212 'See --help for more options' % os.path.basename(sys.argv[0]))
213 parser = optparse.OptionParser(usage)
214 parser.add_option('--binhost', metavar='URL',
215 help='binhost override. By default, starts up a devserver '
216 'and uses it as the binhost.')
217 parser.add_option('-v', '--verbose', default=False, action='store_true',
218 help='Print out added debugging information')
219
220 (options, args) = parser.parse_args()
221
222 if len(args) != 2:
223 parser.print_usage()
224 parser.error('Need board and path to test image.')
225
226 board = args[0]
227 image_path = os.path.realpath(args[1])
228
229 test_helper.SetupCommonLoggingFormat(verbose=options.verbose)
230
231 test = DevModeTest(image_path, board, options.binhost)
232 try:
233 test.PrepareTest()
234 test.TestDevInstall()
Chris Sosa928085e2013-03-08 17:25:30 -0800235 test.TestGmerge()
Chris Sosada9632e2013-03-04 12:28:06 -0800236 logging.info('All tests passed.')
237 finally:
238 test.Cleanup()
239
240
241if __name__ == '__main__':
242 main()