1# Copyright 2015 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Function tests of lxc module. To be able to run this test, following setup
6is required:
7  1. lxc is installed.
8  2. Autotest code exists in /usr/local/autotest, with site-packages installed.
9     (run utils/build_externals.py)
10  3. The user runs the test should have sudo access. Run the test with sudo.
11Note that the test does not require Autotest database and frontend.
12"""
13
14
15import argparse
16import logging
17import os
18import sys
19import tempfile
20import time
21
22import common
23from autotest_lib.client.bin import utils
24from autotest_lib.site_utils import lxc
25
26
27TEST_JOB_ID = 123
28# Create a temp directory for functional tests. The directory is not under /tmp
29# for Moblab to be able to run the test.
30TEMP_DIR = tempfile.mkdtemp(dir=lxc.DEFAULT_CONTAINER_PATH,
31                            prefix='container_test_')
32RESULT_PATH = os.path.join(TEMP_DIR, 'results', str(TEST_JOB_ID))
33# Link to download a test package of autotest server package.
34# Ideally the test should stage a build on devserver and download the
35# autotest_server_package from devserver. This test is focused on testing
36# container, so it's prefered to avoid dependency on devserver.
37AUTOTEST_SERVER_PKG = ('http://storage.googleapis.com/chromeos-image-archive/'
38                       'autotest-containers/autotest_server_package.tar.bz2')
39
40# Test log file to be created in result folder, content is `test`.
41TEST_LOG = 'test.log'
42# Name of test script file to run in container.
43TEST_SCRIPT = 'test.py'
44# Test script to run in container to verify autotest code setup.
45TEST_SCRIPT_CONTENT = """
46import sys
47
48# Test import
49import common
50import chromite
51from autotest_lib.server import utils
52from autotest_lib.site_utils import lxc
53
54with open(sys.argv[1], 'w') as f:
55    f.write('test')
56
57# Test installing packages
58lxc.install_packages(['atop', 'libxslt-dev'], ['selenium', 'numpy'])
59
60"""
61# Name of the test control file.
62TEST_CONTROL_FILE = 'attach.1'
63TEST_DUT = '172.27.213.193'
64TEST_RESULT_PATH = lxc.RESULT_DIR_FMT % TEST_JOB_ID
65# Test autoserv command.
66AUTOSERV_COMMAND = (('/usr/bin/python -u /usr/local/autotest/server/autoserv '
67                     '-p -r %(result_path)s/%(test_dut)s -m %(test_dut)s '
68                     '-u debug_user -l test -s -P %(job_id)s-debug_user/'
69                     '%(test_dut)s -n %(result_path)s/%(test_control_file)s '
70                     '--verify_job_repo_url') %
71                     {'job_id': TEST_JOB_ID,
72                      'result_path': TEST_RESULT_PATH,
73                      'test_dut': TEST_DUT,
74                      'test_control_file': TEST_CONTROL_FILE})
75# Content of the test control file.
76TEST_CONTROL_CONTENT = """
77def run(machine):
78    job.run_test('dummy_PassServer',
79                 host=hosts.create_host(machine))
80
81parallel_simple(run, machines)
82"""
83
84
85def setup_logging(log_level=logging.INFO):
86    """Direct logging to stdout.
87
88    @param log_level: Level of logging to redirect to stdout, default to INFO.
89    """
90    logger = logging.getLogger()
91    logger.setLevel(log_level)
92    handler = logging.StreamHandler(sys.stdout)
93    handler.setLevel(log_level)
94    formatter = logging.Formatter('%(asctime)s %(message)s')
95    handler.setFormatter(formatter)
96    logger.handlers = []
97    logger.addHandler(handler)
98
99
100def setup_base(bucket):
101    """Test setup base container works.
102
103    @param bucket: ContainerBucket to interact with containers.
104    """
105    logging.info('Rebuild base container in folder %s.', bucket.container_path)
106    bucket.setup_base()
107    containers = bucket.get_all()
108    logging.info('Containers created: %s', containers.keys())
109
110
111def setup_test(bucket, name, skip_cleanup):
112    """Test container can be created from base container.
113
114    @param bucket: ContainerBucket to interact with containers.
115    @param name: Name of the test container.
116    @param skip_cleanup: Set to True to skip cleanup, used to troubleshoot
117                         container failures.
118
119    @return: A Container object created for the test container.
120    """
121    logging.info('Create test container.')
122    os.makedirs(RESULT_PATH)
123    container = bucket.setup_test(name, TEST_JOB_ID, AUTOTEST_SERVER_PKG,
124                                  RESULT_PATH, skip_cleanup=skip_cleanup)
125
126    # Inject "AUTOSERV/testing_mode: True" in shadow config to test autoserv.
127    container.attach_run('echo $\'[AUTOSERV]\ntesting_mode: True\' >>'
128                         ' /usr/local/autotest/shadow_config.ini')
129    return container
130
131
132def test_share(container):
133    """Test container can share files with the host.
134
135    @param container: The test container.
136    """
137    logging.info('Test files written to result directory can be accessed '
138                 'from the host running the container..')
139    host_test_script = os.path.join(RESULT_PATH, TEST_SCRIPT)
140    with open(host_test_script, 'w') as script:
141        script.write(TEST_SCRIPT_CONTENT)
142
143    container_result_path = lxc.RESULT_DIR_FMT % TEST_JOB_ID
144    container_test_script = os.path.join(container_result_path, TEST_SCRIPT)
145    container_test_script_dest = os.path.join('/usr/local/autotest/utils/',
146                                              TEST_SCRIPT)
147    container_test_log = os.path.join(container_result_path, TEST_LOG)
148    host_test_log = os.path.join(RESULT_PATH, TEST_LOG)
149    # Move the test script out of result folder as it needs to import common.
150    container.attach_run('mv %s %s' % (container_test_script,
151                                       container_test_script_dest))
152    container.attach_run('python %s %s' % (container_test_script_dest,
153                                           container_test_log))
154    if not os.path.exists(host_test_log):
155        raise Exception('Results created in container can not be accessed from '
156                        'the host.')
157    with open(host_test_log, 'r') as log:
158        if log.read() != 'test':
159            raise Exception('Failed to read the content of results in '
160                            'container.')
161
162
163def test_autoserv(container):
164    """Test container can run autoserv command.
165
166    @param container: The test container.
167    """
168    logging.info('Test autoserv command.')
169    logging.info('Create test control file.')
170    host_control_file = os.path.join(RESULT_PATH, TEST_CONTROL_FILE)
171    with open(host_control_file, 'w') as control_file:
172        control_file.write(TEST_CONTROL_CONTENT)
173
174    logging.info('Run autoserv command.')
175    container.attach_run(AUTOSERV_COMMAND)
176
177    logging.info('Confirm results are available from host.')
178    # Read status.log to check the content is not empty.
179    container_status_log = os.path.join(TEST_RESULT_PATH, TEST_DUT,
180                                        'status.log')
181    status_log = container.attach_run(command='cat %s' % container_status_log
182                                      ).stdout
183    if len(status_log) < 10:
184        raise Exception('Failed to read status.log in container.')
185
186
187def test_package_install(container):
188    """Test installing package in container.
189
190    @param container: The test container.
191    """
192    # Packages are installed in TEST_SCRIPT_CONTENT. Verify the packages in
193    # this method.
194    container.attach_run('which atop')
195    container.attach_run('python -c "import selenium"')
196
197
198def test_ssh(container, remote):
199    """Test container can run ssh to remote server.
200
201    @param container: The test container.
202    @param remote: The remote server to ssh to.
203
204    @raise: error.CmdError if container can't ssh to remote server.
205    """
206    logging.info('Test ssh to %s.', remote)
207    container.attach_run('ssh %s -a -x -o StrictHostKeyChecking=no '
208                         '-o BatchMode=yes -o UserKnownHostsFile=/dev/null '
209                         '-p 22 "true"' % remote)
210
211
212def parse_options():
213    """Parse command line inputs.
214    """
215    parser = argparse.ArgumentParser()
216    parser.add_argument('-d', '--dut', type=str,
217                        help='Test device to ssh to.',
218                        default=None)
219    parser.add_argument('-r', '--devserver', type=str,
220                        help='Test devserver to ssh to.',
221                        default=None)
222    parser.add_argument('-v', '--verbose', action='store_true',
223                        default=False,
224                        help='Print out ALL entries.')
225    parser.add_argument('-s', '--skip_cleanup', action='store_true',
226                        default=False,
227                        help='Skip deleting test containers.')
228    return parser.parse_args()
229
230
231def main(options):
232    """main script.
233
234    @param options: Options to run the script.
235    """
236    # Force to run the test as superuser.
237    # TODO(dshi): crbug.com/459344 Set remove this enforcement when test
238    # container can be unprivileged container.
239    if utils.sudo_require_password():
240        logging.warn('SSP requires root privilege to run commands, please '
241                     'grant root access to this process.')
242        utils.run('sudo true')
243
244    setup_logging(log_level=(logging.DEBUG if options.verbose
245                             else logging.INFO))
246
247    bucket = lxc.ContainerBucket(TEMP_DIR)
248
249    setup_base(bucket)
250    container_test_name = (lxc.TEST_CONTAINER_NAME_FMT %
251                           (TEST_JOB_ID, time.time(), os.getpid()))
252    container = setup_test(bucket, container_test_name, options.skip_cleanup)
253    test_share(container)
254    test_autoserv(container)
255    if options.dut:
256        test_ssh(container, options.dut)
257    if options.devserver:
258        test_ssh(container, options.devserver)
259    # Packages are installed in TEST_SCRIPT, verify the packages are installed.
260    test_package_install(container)
261    logging.info('All tests passed.')
262
263
264if __name__ == '__main__':
265    options = parse_options()
266    try:
267        main(options)
268    finally:
269        if not options.skip_cleanup:
270            logging.info('Cleaning up temporary directory %s.', TEMP_DIR)
271            try:
272                lxc.ContainerBucket(TEMP_DIR).destroy_all()
273            finally:
274                utils.run('sudo rm -rf "%s"' % TEMP_DIR)
275