blob: d27707c89a456f560a34e40996d59fc7b6de1d5e [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
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 Sosaa404a382013-08-22 11:28:38 -0700178 self.devserver = dev_server_wrapper.DevServerWrapper()
179 self.devserver.Start()
Chris Sosada9632e2013-03-04 12:28:06 -0800180 self.binhost = dev_server_wrapper.DevServerWrapper.GetDevServerURL(
Chris Sosac9447962013-03-12 10:12:29 -0700181 sub_dir='static/pkgroot/%s/packages' % self.board)
Chris Sosada9632e2013-03-04 12:28:06 -0800182
183 logging.info('Using binhost %s', self.binhost)
184
185 def TestDevInstall(self):
186 """Tests that we can run dev-install and have python work afterwards."""
187 try:
188 logging.info('Running dev install in the vm.')
Chris Sosa928085e2013-03-08 17:25:30 -0800189 self.remote_access.RemoteSh(
Chris Sosada9632e2013-03-04 12:28:06 -0800190 ['bash', '-l', '-c',
191 '"/usr/bin/dev_install --yes --binhost %s"' % self.binhost])
192
193 logging.info('Verifying that python works on the image.')
Chris Sosa928085e2013-03-08 17:25:30 -0800194 self.remote_access.RemoteSh(
Chris Sosada9632e2013-03-04 12:28:06 -0800195 ['sudo', '-u', 'chronos', '--',
196 'python', '-c', '"print \'hello world\'"'])
197 except cros_build_lib.RunCommandError as e:
198 self.devserver.PrintLog()
199 logging.error('dev-install test failed. See devserver log above for more '
200 'details.')
201 raise TestError('dev-install test failed with: %s' % str(e))
202
Chris Sosada9632e2013-03-04 12:28:06 -0800203 def TestGmerge(self):
204 """Evaluates whether the test passed or failed."""
Chris Sosa928085e2013-03-08 17:25:30 -0800205 logging.info('Testing that gmerge works on the image after dev install.')
Chris Sosada9632e2013-03-04 12:28:06 -0800206 try:
Chris Sosac9447962013-03-12 10:12:29 -0700207 self.remote_access.RemoteSh(
208 ['gmerge', 'gmerge', '--accept_stable', '--usepkg',
209 '--devserver_url', self.devserver.GetDevServerURL(),
210 '--board', self.board])
Chris Sosada9632e2013-03-04 12:28:06 -0800211 except cros_build_lib.RunCommandError as e:
212 logging.error('gmerge test failed. See log for details')
213 raise TestError('gmerge test failed with: %s' % str(e))
214
215
216def main():
217 usage = ('%s <board> <path_to_[test|vm]_image>. '
218 'See --help for more options' % os.path.basename(sys.argv[0]))
219 parser = optparse.OptionParser(usage)
220 parser.add_option('--binhost', metavar='URL',
221 help='binhost override. By default, starts up a devserver '
222 'and uses it as the binhost.')
223 parser.add_option('-v', '--verbose', default=False, action='store_true',
224 help='Print out added debugging information')
225
226 (options, args) = parser.parse_args()
227
228 if len(args) != 2:
229 parser.print_usage()
230 parser.error('Need board and path to test image.')
231
232 board = args[0]
233 image_path = os.path.realpath(args[1])
234
235 test_helper.SetupCommonLoggingFormat(verbose=options.verbose)
236
237 test = DevModeTest(image_path, board, options.binhost)
238 try:
239 test.PrepareTest()
240 test.TestDevInstall()
Chris Sosa928085e2013-03-08 17:25:30 -0800241 test.TestGmerge()
Chris Sosada9632e2013-03-04 12:28:06 -0800242 logging.info('All tests passed.')
243 finally:
244 test.Cleanup()
245
246
247if __name__ == '__main__':
248 main()