blob: b0eb3c59f20167beeffb4fa28858bb7a70935ec5 [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
Yu-Ju Hong29111982013-12-20 15:04:41 -080030from chromite.lib import dev_server_wrapper
Chris Sosa928085e2013-03-08 17:25:30 -080031from chromite.lib import remote_access
Chris Sosada9632e2013-03-04 12:28:06 -080032from 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
Yu-Ju Hong5ed02452014-01-30 09:05:00 -0800142 except (cros_build_lib.RunCommandError,
143 remote_access.SSHConnectionError) as e:
Chris Sosa5bc23bf2013-07-03 12:14:18 -0700144 logging.warning('Failed to connect to VM')
Chris Sosa9eb333d2013-07-17 10:00:25 -0700145 logging.warning(e)
Chris Sosa5bc23bf2013-07-03 12:14:18 -0700146 self._StopVM()
Chris Sosa9eb333d2013-07-17 10:00:25 -0700147 time.sleep(_TIME_BETWEEN_ATTEMPT)
Chris Sosa5bc23bf2013-07-03 12:14:18 -0700148 else:
149 raise TestError('Max attempts to connect to VM exceeded')
150
151 def _StopVM(self):
152 """Stops a running VM set up using _RobustlyStartVMWithSSH."""
153 cmd = ['%s/bin/cros_stop_vm' % constants.CROSUTILS_DIR,
154 '--kvm_pid', self.tmpkvmpid]
155 cros_build_lib.RunCommand(cmd, debug_level=logging.DEBUG)
156
Chris Sosada9632e2013-03-04 12:28:06 -0800157 def PrepareTest(self):
158 """Pre-test modification to the image and env to setup test."""
159 logging.info('Setting up the image %s for vm testing.',
160 self.image_path)
161 self._SetupSSH()
162 vm_path = test_helper.CreateVMImage(self.image_path, self.board,
163 full=False)
164
165 logging.info('Making copy of the vm image %s to manipulate.', vm_path)
David James45b55dd2013-04-24 09:16:40 -0700166 self.working_image_path = os.path.join(self.tmpdir,
167 os.path.basename(vm_path))
Chris Sosada9632e2013-03-04 12:28:06 -0800168 shutil.copyfile(vm_path, self.working_image_path)
169 logging.debug('Copy of vm image stored at %s.', self.working_image_path)
170
Chris Sosab8c2af52013-07-03 10:45:39 -0700171 logging.info('Wiping /usr/local/bin from the image.')
172 self._WipeDevInstall()
Chris Sosada9632e2013-03-04 12:28:06 -0800173
174 logging.info('Starting the vm on port %d.', self.port)
Chris Sosa5bc23bf2013-07-03 12:14:18 -0700175 self._RobustlyStartVMWithSSH()
Chris Sosa068c1e92013-03-17 22:54:20 -0700176
Chris Sosada9632e2013-03-04 12:28:06 -0800177 if not self.binhost:
178 logging.info('Starting the devserver.')
Chris Sosaa404a382013-08-22 11:28:38 -0700179 self.devserver = dev_server_wrapper.DevServerWrapper()
180 self.devserver.Start()
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\'"'])
Yu-Ju Hong5ed02452014-01-30 09:05:00 -0800198 except (cros_build_lib.RunCommandError,
199 remote_access.SSHConnectionError) as e:
Chris Sosada9632e2013-03-04 12:28:06 -0800200 self.devserver.PrintLog()
201 logging.error('dev-install test failed. See devserver log above for more '
202 'details.')
203 raise TestError('dev-install test failed with: %s' % str(e))
204
Chris Sosada9632e2013-03-04 12:28:06 -0800205 def TestGmerge(self):
206 """Evaluates whether the test passed or failed."""
Chris Sosa928085e2013-03-08 17:25:30 -0800207 logging.info('Testing that gmerge works on the image after dev install.')
Chris Sosada9632e2013-03-04 12:28:06 -0800208 try:
Chris Sosac9447962013-03-12 10:12:29 -0700209 self.remote_access.RemoteSh(
210 ['gmerge', 'gmerge', '--accept_stable', '--usepkg',
211 '--devserver_url', self.devserver.GetDevServerURL(),
212 '--board', self.board])
Yu-Ju Hong5ed02452014-01-30 09:05:00 -0800213 except (cros_build_lib.RunCommandError,
214 remote_access.SSHConnectionError) as e:
Chris Sosada9632e2013-03-04 12:28:06 -0800215 logging.error('gmerge test failed. See log for details')
216 raise TestError('gmerge test failed with: %s' % str(e))
217
218
219def main():
220 usage = ('%s <board> <path_to_[test|vm]_image>. '
221 'See --help for more options' % os.path.basename(sys.argv[0]))
222 parser = optparse.OptionParser(usage)
223 parser.add_option('--binhost', metavar='URL',
224 help='binhost override. By default, starts up a devserver '
225 'and uses it as the binhost.')
226 parser.add_option('-v', '--verbose', default=False, action='store_true',
227 help='Print out added debugging information')
228
229 (options, args) = parser.parse_args()
230
231 if len(args) != 2:
232 parser.print_usage()
233 parser.error('Need board and path to test image.')
234
235 board = args[0]
236 image_path = os.path.realpath(args[1])
237
238 test_helper.SetupCommonLoggingFormat(verbose=options.verbose)
239
240 test = DevModeTest(image_path, board, options.binhost)
241 try:
242 test.PrepareTest()
243 test.TestDevInstall()
Chris Sosa928085e2013-03-08 17:25:30 -0800244 test.TestGmerge()
Chris Sosada9632e2013-03-04 12:28:06 -0800245 logging.info('All tests passed.')
246 finally:
247 test.Cleanup()
248
249
250if __name__ == '__main__':
251 main()