blob: 8094e0c080671fa62fefd537a10d6f752057a8fb [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 Sosada9632e2013-03-04 12:28:06 -080041
42class TestError(Exception):
43 """Raised on any error during testing. It being raised is a test failure."""
44
45
46class DevModeTest(object):
47 """Wrapper for dev mode tests."""
48 def __init__(self, image_path, board, binhost):
49 """
50 Args:
51 image_path: Filesystem path to the image to test.
52 board: Board of the image under test.
53 binhost: Binhost override. Binhost as defined here is where dev-install
54 or gmerge go to search for binary packages. By default this will
55 be set to the devserver url of the host running this script.
56 If no override i.e. the default is ok, set to None.
57 """
58 self.image_path = image_path
59 self.board = board
60 self.binhost = binhost
61
62 self.tmpdir = tempfile.mkdtemp('DevModeTest')
Chris Sosada9632e2013-03-04 12:28:06 -080063 self.tmpkvmpid = os.path.join(self.tmpdir, 'kvm_pid')
64
65 self.working_image_path = None
66 self.devserver = None
Chris Sosa928085e2013-03-08 17:25:30 -080067 self.remote_access = None
Chris Sosada9632e2013-03-04 12:28:06 -080068 self.port = None
69
70 def Cleanup(self):
71 """Clean up any state at the end of the test."""
72 try:
73 if self.working_image_path:
74 os.remove(self.working_image_path)
75
76 if self.devserver:
77 self.devserver.Stop()
78
79 self.devserver = None
Chris Sosa5bc23bf2013-07-03 12:14:18 -070080 self._StopVM()
Chris Sosada9632e2013-03-04 12:28:06 -080081
82 if self.tmpdir:
83 shutil.rmtree(self.tmpdir, ignore_errors=True)
84
85 self.tmpdir = None
86 except Exception:
87 logging.warning('Received error during cleanup', exc_info=True)
88
89 def _SetupSSH(self):
90 """Sets up the necessary items for running ssh."""
91 self.port = self._FindUnusedPort()
Chris Sosa928085e2013-03-08 17:25:30 -080092 self.remote_access = remote_access.RemoteAccess(
93 _LOCALHOST, self.tmpdir, self.port,
94 debug_level=logging.DEBUG, interactive=False)
Chris Sosada9632e2013-03-04 12:28:06 -080095
Chris Sosab8c2af52013-07-03 10:45:39 -070096 def _WipeDevInstall(self):
97 """Wipes the devinstall state."""
Chris Sosada9632e2013-03-04 12:28:06 -080098 r_mount_point = os.path.join(self.tmpdir, 'm')
99 s_mount_point = os.path.join(self.tmpdir, 's')
Chris Sosab8c2af52013-07-03 10:45:39 -0700100 dev_image_path = os.path.join(s_mount_point, 'dev_image')
Chris Sosada9632e2013-03-04 12:28:06 -0800101 mount_helper.MountImage(self.working_image_path,
102 r_mount_point, s_mount_point, read_only=False,
103 safe=True)
Chris Sosab8c2af52013-07-03 10:45:39 -0700104 try:
105 cros_build_lib.SudoRunCommand(['chown', '--recursive', getpass.getuser(),
106 s_mount_point], debug_level=logging.DEBUG)
107 shutil.rmtree(dev_image_path)
108 finally:
109 mount_helper.UnmountImage(r_mount_point, s_mount_point)
Chris Sosada9632e2013-03-04 12:28:06 -0800110
111 def _FindUnusedPort(self):
112 """Returns a currently unused port."""
113 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
114 s.bind((_LOCALHOST, 0))
115 port = s.getsockname()[1]
116 s.close()
117 return port
118
Chris Sosa5bc23bf2013-07-03 12:14:18 -0700119 def _RobustlyStartVMWithSSH(self):
120 """Start test copy of VM and ensure we can ssh into it.
121
122 This command is more robust than just naively starting the VM as it will
123 try to start the VM multiple times if the VM fails to start up. This is
124 inspired by retry_until_ssh in crosutils/lib/cros_vm_lib.sh.
125 """
126 for _ in range(_MAX_SSH_ATTEMPTS):
127 try:
128 cmd = ['%s/bin/cros_start_vm' % constants.CROSUTILS_DIR,
129 '--ssh_port', str(self.port),
130 '--image_path', self.working_image_path,
131 '--no_graphics',
132 '--kvm_pid', self.tmpkvmpid]
133 cros_build_lib.RunCommand(cmd, debug_level=logging.DEBUG)
134
135 # Ping the VM to ensure we can SSH into it.
136 self.remote_access.RemoteSh(['true'])
137 return
138 except cros_build_lib.RunCommandError as e:
139 logging.warning('Failed to connect to VM')
Chris Sosa9eb333d2013-07-17 10:00:25 -0700140 logging.warning(e)
Chris Sosa5bc23bf2013-07-03 12:14:18 -0700141 self._StopVM()
Chris Sosa9eb333d2013-07-17 10:00:25 -0700142 time.sleep(_TIME_BETWEEN_ATTEMPT)
Chris Sosa5bc23bf2013-07-03 12:14:18 -0700143 else:
144 raise TestError('Max attempts to connect to VM exceeded')
145
146 def _StopVM(self):
147 """Stops a running VM set up using _RobustlyStartVMWithSSH."""
148 cmd = ['%s/bin/cros_stop_vm' % constants.CROSUTILS_DIR,
149 '--kvm_pid', self.tmpkvmpid]
150 cros_build_lib.RunCommand(cmd, debug_level=logging.DEBUG)
151
Chris Sosada9632e2013-03-04 12:28:06 -0800152 def PrepareTest(self):
153 """Pre-test modification to the image and env to setup test."""
154 logging.info('Setting up the image %s for vm testing.',
155 self.image_path)
156 self._SetupSSH()
157 vm_path = test_helper.CreateVMImage(self.image_path, self.board,
158 full=False)
159
160 logging.info('Making copy of the vm image %s to manipulate.', vm_path)
David James45b55dd2013-04-24 09:16:40 -0700161 self.working_image_path = os.path.join(self.tmpdir,
162 os.path.basename(vm_path))
Chris Sosada9632e2013-03-04 12:28:06 -0800163 shutil.copyfile(vm_path, self.working_image_path)
164 logging.debug('Copy of vm image stored at %s.', self.working_image_path)
165
Chris Sosab8c2af52013-07-03 10:45:39 -0700166 logging.info('Wiping /usr/local/bin from the image.')
167 self._WipeDevInstall()
Chris Sosada9632e2013-03-04 12:28:06 -0800168
169 logging.info('Starting the vm on port %d.', self.port)
Chris Sosa5bc23bf2013-07-03 12:14:18 -0700170 self._RobustlyStartVMWithSSH()
Chris Sosa068c1e92013-03-17 22:54:20 -0700171
Chris Sosada9632e2013-03-04 12:28:06 -0800172 if not self.binhost:
173 logging.info('Starting the devserver.')
174 self.devserver = dev_server_wrapper.DevServerWrapper(self.tmpdir)
175 self.devserver.start()
176 self.devserver.WaitUntilStarted()
177 self.binhost = dev_server_wrapper.DevServerWrapper.GetDevServerURL(
Chris Sosac9447962013-03-12 10:12:29 -0700178 sub_dir='static/pkgroot/%s/packages' % self.board)
Chris Sosada9632e2013-03-04 12:28:06 -0800179
180 logging.info('Using binhost %s', self.binhost)
181
182 def TestDevInstall(self):
183 """Tests that we can run dev-install and have python work afterwards."""
184 try:
185 logging.info('Running dev install in the vm.')
Chris Sosa928085e2013-03-08 17:25:30 -0800186 self.remote_access.RemoteSh(
Chris Sosada9632e2013-03-04 12:28:06 -0800187 ['bash', '-l', '-c',
188 '"/usr/bin/dev_install --yes --binhost %s"' % self.binhost])
189
190 logging.info('Verifying that python works on the image.')
Chris Sosa928085e2013-03-08 17:25:30 -0800191 self.remote_access.RemoteSh(
Chris Sosada9632e2013-03-04 12:28:06 -0800192 ['sudo', '-u', 'chronos', '--',
193 'python', '-c', '"print \'hello world\'"'])
194 except cros_build_lib.RunCommandError as e:
195 self.devserver.PrintLog()
196 logging.error('dev-install test failed. See devserver log above for more '
197 'details.')
198 raise TestError('dev-install test failed with: %s' % str(e))
199
Chris Sosada9632e2013-03-04 12:28:06 -0800200 def TestGmerge(self):
201 """Evaluates whether the test passed or failed."""
Chris Sosa928085e2013-03-08 17:25:30 -0800202 logging.info('Testing that gmerge works on the image after dev install.')
Chris Sosada9632e2013-03-04 12:28:06 -0800203 try:
Chris Sosac9447962013-03-12 10:12:29 -0700204 self.remote_access.RemoteSh(
205 ['gmerge', 'gmerge', '--accept_stable', '--usepkg',
206 '--devserver_url', self.devserver.GetDevServerURL(),
207 '--board', self.board])
Chris Sosada9632e2013-03-04 12:28:06 -0800208 except cros_build_lib.RunCommandError as e:
209 logging.error('gmerge test failed. See log for details')
210 raise TestError('gmerge test failed with: %s' % str(e))
211
212
213def main():
214 usage = ('%s <board> <path_to_[test|vm]_image>. '
215 'See --help for more options' % os.path.basename(sys.argv[0]))
216 parser = optparse.OptionParser(usage)
217 parser.add_option('--binhost', metavar='URL',
218 help='binhost override. By default, starts up a devserver '
219 'and uses it as the binhost.')
220 parser.add_option('-v', '--verbose', default=False, action='store_true',
221 help='Print out added debugging information')
222
223 (options, args) = parser.parse_args()
224
225 if len(args) != 2:
226 parser.print_usage()
227 parser.error('Need board and path to test image.')
228
229 board = args[0]
230 image_path = os.path.realpath(args[1])
231
232 test_helper.SetupCommonLoggingFormat(verbose=options.verbose)
233
234 test = DevModeTest(image_path, board, options.binhost)
235 try:
236 test.PrepareTest()
237 test.TestDevInstall()
Chris Sosa928085e2013-03-08 17:25:30 -0800238 test.TestGmerge()
Chris Sosada9632e2013-03-04 12:28:06 -0800239 logging.info('All tests passed.')
240 finally:
241 test.Cleanup()
242
243
244if __name__ == '__main__':
245 main()