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