blob: c9a36c8f95b2e975ccf4e02d465847ec62776f23 [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
Chris Sosa9eb333d2013-07-17 10:00:25 -070023import time
Chris Sosada9632e2013-03-04 12:28:06 -080024
25import constants
26sys.path.append(constants.SOURCE_ROOT)
27sys.path.append(constants.CROS_PLATFORM_ROOT)
28
29from chromite.lib import cros_build_lib
Chris Sosa928085e2013-03-08 17:25:30 -080030from chromite.lib import remote_access
Chris Sosada9632e2013-03-04 12:28:06 -080031from crostestutils.lib import dev_server_wrapper
32from crostestutils.lib import mount_helper
33from crostestutils.lib import test_helper
34
35
36_LOCALHOST = 'localhost'
37_PRIVATE_KEY = os.path.join(constants.CROSUTILS_DIR, 'mod_for_test_scripts',
38 'ssh_keys', 'testing_rsa')
Chris Sosa9eb333d2013-07-17 10:00:25 -070039_MAX_SSH_ATTEMPTS = 5
40_TIME_BETWEEN_ATTEMPT = 30
Chris Sosa2c65f832013-08-15 10:49:00 -070041_CONNECT_TIMEOUT = 120
42
Chris Sosada9632e2013-03-04 12:28:06 -080043
44class TestError(Exception):
45 """Raised on any error during testing. It being raised is a test failure."""
46
47
48class DevModeTest(object):
49 """Wrapper for dev mode tests."""
50 def __init__(self, image_path, board, binhost):
51 """
52 Args:
53 image_path: Filesystem path to the image to test.
54 board: Board of the image under test.
55 binhost: Binhost override. Binhost as defined here is where dev-install
56 or gmerge go to search for binary packages. By default this will
57 be set to the devserver url of the host running this script.
58 If no override i.e. the default is ok, set to None.
59 """
60 self.image_path = image_path
61 self.board = board
62 self.binhost = binhost
63
64 self.tmpdir = tempfile.mkdtemp('DevModeTest')
Chris Sosada9632e2013-03-04 12:28:06 -080065 self.tmpkvmpid = os.path.join(self.tmpdir, 'kvm_pid')
66
67 self.working_image_path = None
68 self.devserver = None
Chris Sosa928085e2013-03-08 17:25:30 -080069 self.remote_access = None
Chris Sosada9632e2013-03-04 12:28:06 -080070 self.port = None
71
72 def Cleanup(self):
73 """Clean up any state at the end of the test."""
74 try:
75 if self.working_image_path:
76 os.remove(self.working_image_path)
77
78 if self.devserver:
79 self.devserver.Stop()
80
81 self.devserver = None
Chris Sosa5bc23bf2013-07-03 12:14:18 -070082 self._StopVM()
Chris Sosada9632e2013-03-04 12:28:06 -080083
84 if self.tmpdir:
85 shutil.rmtree(self.tmpdir, ignore_errors=True)
86
87 self.tmpdir = None
88 except Exception:
89 logging.warning('Received error during cleanup', exc_info=True)
90
91 def _SetupSSH(self):
92 """Sets up the necessary items for running ssh."""
93 self.port = self._FindUnusedPort()
Chris Sosa928085e2013-03-08 17:25:30 -080094 self.remote_access = remote_access.RemoteAccess(
95 _LOCALHOST, self.tmpdir, self.port,
96 debug_level=logging.DEBUG, interactive=False)
Chris Sosada9632e2013-03-04 12:28:06 -080097
Chris Sosab8c2af52013-07-03 10:45:39 -070098 def _WipeDevInstall(self):
99 """Wipes the devinstall state."""
Chris Sosada9632e2013-03-04 12:28:06 -0800100 r_mount_point = os.path.join(self.tmpdir, 'm')
101 s_mount_point = os.path.join(self.tmpdir, 's')
Chris Sosab8c2af52013-07-03 10:45:39 -0700102 dev_image_path = os.path.join(s_mount_point, 'dev_image')
Chris Sosada9632e2013-03-04 12:28:06 -0800103 mount_helper.MountImage(self.working_image_path,
104 r_mount_point, s_mount_point, read_only=False,
105 safe=True)
Chris Sosab8c2af52013-07-03 10:45:39 -0700106 try:
107 cros_build_lib.SudoRunCommand(['chown', '--recursive', getpass.getuser(),
108 s_mount_point], debug_level=logging.DEBUG)
109 shutil.rmtree(dev_image_path)
110 finally:
111 mount_helper.UnmountImage(r_mount_point, s_mount_point)
Chris Sosada9632e2013-03-04 12:28:06 -0800112
113 def _FindUnusedPort(self):
114 """Returns a currently unused port."""
115 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
116 s.bind((_LOCALHOST, 0))
117 port = s.getsockname()[1]
118 s.close()
119 return port
120
Chris Sosa5bc23bf2013-07-03 12:14:18 -0700121 def _RobustlyStartVMWithSSH(self):
122 """Start test copy of VM and ensure we can ssh into it.
123
124 This command is more robust than just naively starting the VM as it will
125 try to start the VM multiple times if the VM fails to start up. This is
126 inspired by retry_until_ssh in crosutils/lib/cros_vm_lib.sh.
127 """
128 for _ in range(_MAX_SSH_ATTEMPTS):
129 try:
130 cmd = ['%s/bin/cros_start_vm' % constants.CROSUTILS_DIR,
131 '--ssh_port', str(self.port),
132 '--image_path', self.working_image_path,
133 '--no_graphics',
134 '--kvm_pid', self.tmpkvmpid]
135 cros_build_lib.RunCommand(cmd, debug_level=logging.DEBUG)
136
137 # Ping the VM to ensure we can SSH into it.
Chris Sosa2c65f832013-08-15 10:49:00 -0700138 ssh_settings = remote_access.CompileSSHConnectSettings(
139 ConnectTimeout=_CONNECT_TIMEOUT)
140 self.remote_access.RemoteSh(['true'], connect_settings=ssh_settings)
Chris Sosa5bc23bf2013-07-03 12:14:18 -0700141 return
142 except cros_build_lib.RunCommandError as e:
143 logging.warning('Failed to connect to VM')
Chris Sosa9eb333d2013-07-17 10:00:25 -0700144 logging.warning(e)
Chris Sosa5bc23bf2013-07-03 12:14:18 -0700145 self._StopVM()
Chris Sosa9eb333d2013-07-17 10:00:25 -0700146 time.sleep(_TIME_BETWEEN_ATTEMPT)
Chris Sosa5bc23bf2013-07-03 12:14:18 -0700147 else:
148 raise TestError('Max attempts to connect to VM exceeded')
149
150 def _StopVM(self):
151 """Stops a running VM set up using _RobustlyStartVMWithSSH."""
152 cmd = ['%s/bin/cros_stop_vm' % constants.CROSUTILS_DIR,
153 '--kvm_pid', self.tmpkvmpid]
154 cros_build_lib.RunCommand(cmd, debug_level=logging.DEBUG)
155
Chris Sosada9632e2013-03-04 12:28:06 -0800156 def PrepareTest(self):
157 """Pre-test modification to the image and env to setup test."""
158 logging.info('Setting up the image %s for vm testing.',
159 self.image_path)
160 self._SetupSSH()
161 vm_path = test_helper.CreateVMImage(self.image_path, self.board,
162 full=False)
163
164 logging.info('Making copy of the vm image %s to manipulate.', vm_path)
David James45b55dd2013-04-24 09:16:40 -0700165 self.working_image_path = os.path.join(self.tmpdir,
166 os.path.basename(vm_path))
Chris Sosada9632e2013-03-04 12:28:06 -0800167 shutil.copyfile(vm_path, self.working_image_path)
168 logging.debug('Copy of vm image stored at %s.', self.working_image_path)
169
Chris Sosab8c2af52013-07-03 10:45:39 -0700170 logging.info('Wiping /usr/local/bin from the image.')
171 self._WipeDevInstall()
Chris Sosada9632e2013-03-04 12:28:06 -0800172
173 logging.info('Starting the vm on port %d.', self.port)
Chris Sosa5bc23bf2013-07-03 12:14:18 -0700174 self._RobustlyStartVMWithSSH()
Chris Sosa068c1e92013-03-17 22:54:20 -0700175
Chris Sosada9632e2013-03-04 12:28:06 -0800176 if not self.binhost:
177 logging.info('Starting the devserver.')
Chris Sosa90649402013-08-24 09:19:33 -0700178 self.devserver = dev_server_wrapper.DevServerWrapper(self.tmpdir)
179 self.devserver.start()
180 self.devserver.WaitUntilStarted()
Chris Sosada9632e2013-03-04 12:28:06 -0800181 self.binhost = dev_server_wrapper.DevServerWrapper.GetDevServerURL(
Chris Sosac9447962013-03-12 10:12:29 -0700182 sub_dir='static/pkgroot/%s/packages' % self.board)
Chris Sosada9632e2013-03-04 12:28:06 -0800183
184 logging.info('Using binhost %s', self.binhost)
185
186 def TestDevInstall(self):
187 """Tests that we can run dev-install and have python work afterwards."""
188 try:
189 logging.info('Running dev install in the vm.')
Chris Sosa928085e2013-03-08 17:25:30 -0800190 self.remote_access.RemoteSh(
Chris Sosada9632e2013-03-04 12:28:06 -0800191 ['bash', '-l', '-c',
192 '"/usr/bin/dev_install --yes --binhost %s"' % self.binhost])
193
194 logging.info('Verifying that python works on the image.')
Chris Sosa928085e2013-03-08 17:25:30 -0800195 self.remote_access.RemoteSh(
Chris Sosada9632e2013-03-04 12:28:06 -0800196 ['sudo', '-u', 'chronos', '--',
197 'python', '-c', '"print \'hello world\'"'])
198 except cros_build_lib.RunCommandError as e:
199 self.devserver.PrintLog()
200 logging.error('dev-install test failed. See devserver log above for more '
201 'details.')
202 raise TestError('dev-install test failed with: %s' % str(e))
203
Chris Sosada9632e2013-03-04 12:28:06 -0800204 def TestGmerge(self):
205 """Evaluates whether the test passed or failed."""
Chris Sosa928085e2013-03-08 17:25:30 -0800206 logging.info('Testing that gmerge works on the image after dev install.')
Chris Sosada9632e2013-03-04 12:28:06 -0800207 try:
Chris Sosac9447962013-03-12 10:12:29 -0700208 self.remote_access.RemoteSh(
209 ['gmerge', 'gmerge', '--accept_stable', '--usepkg',
210 '--devserver_url', self.devserver.GetDevServerURL(),
211 '--board', self.board])
Chris Sosada9632e2013-03-04 12:28:06 -0800212 except cros_build_lib.RunCommandError as e:
213 logging.error('gmerge test failed. See log for details')
214 raise TestError('gmerge test failed with: %s' % str(e))
215
216
217def main():
218 usage = ('%s <board> <path_to_[test|vm]_image>. '
219 'See --help for more options' % os.path.basename(sys.argv[0]))
220 parser = optparse.OptionParser(usage)
221 parser.add_option('--binhost', metavar='URL',
222 help='binhost override. By default, starts up a devserver '
223 'and uses it as the binhost.')
224 parser.add_option('-v', '--verbose', default=False, action='store_true',
225 help='Print out added debugging information')
226
227 (options, args) = parser.parse_args()
228
229 if len(args) != 2:
230 parser.print_usage()
231 parser.error('Need board and path to test image.')
232
233 board = args[0]
234 image_path = os.path.realpath(args[1])
235
236 test_helper.SetupCommonLoggingFormat(verbose=options.verbose)
237
238 test = DevModeTest(image_path, board, options.binhost)
239 try:
240 test.PrepareTest()
241 test.TestDevInstall()
Chris Sosa928085e2013-03-08 17:25:30 -0800242 test.TestGmerge()
Chris Sosada9632e2013-03-04 12:28:06 -0800243 logging.info('All tests passed.')
244 finally:
245 test.Cleanup()
246
247
248if __name__ == '__main__':
249 main()