1#!/usr/bin/python2
2# Copyright 2015 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Function tests of lxc module. To be able to run this test, following setup
7is required:
8  1. lxc is installed.
9  2. Autotest code exists in /usr/local/autotest, with site-packages installed.
10     (run utils/build_externals.py)
11  3. The user runs the test should have sudo access. Run the test with sudo.
12Note that the test does not require Autotest database and frontend.
13"""
14
15
16import argparse
17import logging
18import os
19import tempfile
20
21import common
22from autotest_lib.client.bin import utils
23from autotest_lib.client.common_lib import error
24from autotest_lib.site_utils import lxc
25from autotest_lib.site_utils.lxc import base_image
26from autotest_lib.site_utils.lxc import unittest_setup
27
28
29TEST_JOB_ID = 123
30TEST_JOB_FOLDER = '123-debug_user'
31# Create a temp directory for functional tests. The directory is not under /tmp
32# for Moblab to be able to run the test.
33# But first, ensure that the containing directory exists:
34
35if not os.path.exists(lxc.DEFAULT_CONTAINER_PATH):
36    os.makedirs(lxc.DEFAULT_CONTAINER_PATH)
37TEMP_DIR = tempfile.mkdtemp(dir=lxc.DEFAULT_CONTAINER_PATH,
38                            prefix='container_test_')
39RESULT_PATH = os.path.join(TEMP_DIR, 'results', str(TEST_JOB_ID))
40# Link to download a test package of autotest server package.
41# Ideally the test should stage a build on devserver and download the
42# autotest_server_package from devserver. This test is focused on testing
43# container, so it's prefered to avoid dependency on devserver.
44AUTOTEST_SERVER_PKG = ('http://storage.googleapis.com/abci-ssp/'
45                       'autotest-containers/autotest_server_package.tar.bz2')
46
47# Test log file to be created in result folder, content is `test`.
48TEST_LOG = 'test.log'
49# Name of test script file to run in container.
50TEST_SCRIPT = 'test.py'
51# Test script to run in container to verify autotest code setup.
52TEST_SCRIPT_CONTENT = """
53import socket
54import sys
55
56# Test import
57import common
58import chromite
59
60# This test has to be before the import of autotest_lib, because ts_mon requires
61# httplib2 module in chromite/third_party. The one in Autotest site-packages is
62# out dated.
63%(ts_mon_test)s
64
65from autotest_lib.server import utils
66from autotest_lib.site_utils import lxc
67
68with open(sys.argv[1], 'w') as f:
69    f.write('test')
70
71# Confirm hostname starts with `test-`
72if not socket.gethostname().startswith('test-'):
73    raise Exception('The container\\\'s hostname must start with `test-`.')
74
75# Test installing packages
76lxc.install_packages(['atop'], ['acora'])
77
78"""
79
80TEST_SCRIPT_CONTENT_TS_MON = """
81# Test ts_mon metrics can be set up.
82from chromite.lib import ts_mon_config
83ts_mon_config.SetupTsMonGlobalState('some_test', suppress_exception=False)
84"""
85
86CREATE_FAKE_TS_MON_CONFIG_SCRIPT = 'create_fake_key.py'
87
88CREATE_FAKE_TS_MON_CONFIG_SCRIPT_CONTENT = """
89import os
90import rsa
91
92EXPECTED_TS_MON_CONFIG_NAME = '/etc/chrome-infra/ts-mon.json'
93
94FAKE_TS_MON_CONFIG_CONTENT = '''
95    {
96        "credentials":"/tmp/service_account_prodx_mon.json",
97        "endpoint":"https://xxx.googleapis.com/v1:insert",
98        "use_new_proto": true
99    }'''
100
101FAKE_SERVICE_ACCOUNT_CRED_JSON = '''
102    {
103        "type": "service_account",
104        "project_id": "test_project",
105        "private_key_id": "aaa",
106        "private_key": "%s",
107        "client_email": "xxx",
108        "client_id": "111",
109        "auth_uri": "https://accounts.google.com/o/oauth2/auth",
110        "token_uri": "https://accounts.google.com/o/oauth2/token",
111        "auth_provider_x509_cert_url":
112                "https://www.googleapis.com/oauth2/v1/certs",
113        "client_x509_cert_url":
114                "https://www.googleapis.com/robot/v1/metadata/x509/xxx"
115    }'''
116
117
118TEST_KEY = '''------BEGIN PRIVATE KEY-----
119MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzg4K2SXqf9LAM
12052a/t2HfpY5y49sbrgRb1llP6c8RVWhUX/pGdjbcIM97+1CJEWBN8Vmraoe4+71o
1211idTPehJfHRNeyXQUnro8CmnSxE9tLHtdKj0pzvO+yqT66O6Iw1aUAIX+dG4Us9Q
122Z22ypFHaJ74lKw9JFwAFTJ/TF1rXUXqgufYTNNqP3Ra7wCHF8BmtjwRYAlvsR9CO
123c4eVC1+qhq/8/EOMCgF/rsbZW93r/nz5xgsSX0k6WkAz5WX2mniHfmBFpmr039jZ
1240eI1mEMGDAYuUn05++dNveo/ZOZj3wBlFzyfNSeeWJB5SdKPTvN3H/Iu0Aw+Rtb6
125szwNClaFAgMBAAECggEAHZ8cjVRUJ/tiJorzlTyfKZ6hwhsPv4JIRVg6LhnceZWA
126jPW2cHSWyl2epyx55lhH7iyeeY7vXOqrX1aBMDb1stSWw2dH/tdxYSkqEmksa+R6
127fL6kl5RV5epjpPt77Z3VmPq9UbP/M310qKWcgB8lw4wN0AfKMqsZLYauk9BVhNRu
128Bgah9O7BmcXS+mp49w0Xyfo1UBvzW8R6UnBhHbf9aOY8ObMD0Jj/wDjlYMqSSIKR
1299/8GZWQEKe6q0PyRRdNNtdzbpBrR0fIw6/T9pfDR2fBAcpNvD50eJk2jRiRDTWFJ
130rVSc0bvZFb74Rc3LbMSXW/6Kb7I2IG1XsWw7nxp92QKBgQDgzdIxZrkNZ3Tbuzng
131SG4atjnaCXoekOHK7VZVYd30S0AAizeGu1sjpUVQgsf+qkFskXAQp2/2f+Wiuq2G
132+nJYvXwZ/r9IcUs/oD3Fa2ezCVz1N/HOSPFAZK9XZuZbL8sXEYIPGJWH5F8Sanmb
133xNp9IUynlpwgM2JlZNeTCkv4PQKBgQDMbL/AF3LSpKvwi+QvYVkX/gChQmNMr4pP
134TM/GI4D03tNrzsut3oerKMUw0c5MxonkAJpuACN6baRyBOBxRYQSt8wWkORg9iqy
135a7aHnQqIGRafydW1/Snhr2DJSSaViHfO0oaA1r61zgMUTnSGb3UjyxJQp65dvPac
136BhpR9wpz6QKBgQDR2S/CL8rEqXObfi1roREu3DYqw7f8enBb1qtFrsLbPbd0CoD9
137wz0zjB6lJj/9CP9jkmwTD8njR8ab3jkIDBfboJ4NQhFbVW7R6QpglH9L0Iy2189g
138KhUScCqBoyubqYSidxR6dQ94uATLkxsL/nmaXxBITL5XDMBoN/dIak86XQKBgDqa
139oo4LKtvAYZpgQFZk7gm2w693PMhrOpdpSddfrkSE7M9nRXTe6r3ivkU0oJPaBwXa
140Nmt6lrEuZYpaY42VhDtpfZSqjQ5PBAaKYpWWK8LAjn/YeO/nV+5fPLv3wJv1t4MP
141T4f4CExOdwuHQliX81kDioicyZwN5BTumvUMgW6hAoGAF29kI1KthKaHN9P1DchI
142qqoHb9FPdZ5I6HDQpn6fr9ut7+9kVqexUrQ2AMvcVei6gDWW6P3yDCdTKcV9qtts
1431JOP2aSmXvibflx/bNfnhu988qJDhJ3CCjfc79fjwntUIXNPsFmwC9W5lnlSMKHM
144rH4RdmnjeCIG1PZ35m/yUSU=
145-----END PRIVATE KEY-----'''
146
147if not os.path.exists(EXPECTED_TS_MON_CONFIG_NAME):
148    try:
149        os.makedirs(os.path.dirname(EXPECTED_TS_MON_CONFIG_NAME))
150    except OSError:
151        # Directory already exists.
152        pass
153
154    with open(EXPECTED_TS_MON_CONFIG_NAME, 'w') as f:
155        f.write(FAKE_TS_MON_CONFIG_CONTENT)
156    with open ('/tmp/service_account_prodx_mon.json', 'w') as f:
157        f.write(FAKE_SERVICE_ACCOUNT_CRED_JSON % repr(TEST_KEY)[2:-1])
158"""
159
160# Name of the test control file.
161TEST_CONTROL_FILE = 'attach.1'
162TEST_DUT = '172.27.213.193'
163TEST_RESULT_PATH = lxc.RESULT_DIR_FMT % TEST_JOB_FOLDER
164# Test autoserv command.
165AUTOSERV_COMMAND = (('/usr/bin/python -u /usr/local/autotest/server/autoserv '
166                     '-p -r %(result_path)s/%(test_dut)s -m %(test_dut)s '
167                     '-u debug_user -l test -s -P %(job_id)s-debug_user/'
168                     '%(test_dut)s -n %(result_path)s/%(test_control_file)s '
169                     '--verify_job_repo_url') %
170                    {'job_id': TEST_JOB_ID,
171                     'result_path': TEST_RESULT_PATH,
172                     'test_dut': TEST_DUT,
173                     'test_control_file': TEST_CONTROL_FILE})
174# Content of the test control file.
175TEST_CONTROL_CONTENT = """
176def run(machine):
177    job.run_test('dummy_PassServer',
178                 host=hosts.create_host(machine))
179
180parallel_simple(run, machines)
181"""
182
183
184def setup_base(container_path):
185    """Test setup base container works.
186
187    @param bucket: ContainerBucket to interact with containers.
188    """
189    logging.info('Rebuild base container in folder %s.', container_path)
190    image = base_image.BaseImage(container_path, lxc.BASE)
191    image.setup()
192    logging.info('Base container created: %s', image.get().name)
193
194
195def setup_test(bucket, container_id, skip_cleanup):
196    """Test container can be created from base container.
197
198    @param bucket: ContainerBucket to interact with containers.
199    @param container_id: ID of the test container.
200    @param skip_cleanup: Set to True to skip cleanup, used to troubleshoot
201                         container failures.
202
203    @return: A Container object created for the test container.
204    """
205    logging.info('Create test container.')
206    os.makedirs(RESULT_PATH)
207    container = bucket.setup_test(container_id, TEST_JOB_ID,
208                                  AUTOTEST_SERVER_PKG, RESULT_PATH,
209                                  skip_cleanup=skip_cleanup,
210                                  job_folder=TEST_JOB_FOLDER,
211                                  dut_name='192.168.0.3')
212
213    # Inject "AUTOSERV/testing_mode: True" in shadow config to test autoserv.
214    container.attach_run('echo $\'[AUTOSERV]\ntesting_mode: True\' >>'
215                         ' /usr/local/autotest/shadow_config.ini')
216
217    if not utils.is_moblab():
218        # Create fake '/etc/chrome-infra/ts-mon.json' if it doesn't exist.
219        create_key_script = os.path.join(
220            RESULT_PATH, CREATE_FAKE_TS_MON_CONFIG_SCRIPT)
221        with open(create_key_script, 'w') as script:
222            script.write(CREATE_FAKE_TS_MON_CONFIG_SCRIPT_CONTENT)
223        container_result_path = lxc.RESULT_DIR_FMT % TEST_JOB_FOLDER
224        container_create_key_script = os.path.join(
225            container_result_path, CREATE_FAKE_TS_MON_CONFIG_SCRIPT)
226        container.attach_run('python %s' % container_create_key_script)
227
228    return container
229
230
231def test_share(container):
232    """Test container can share files with the host.
233
234    @param container: The test container.
235    """
236    logging.info('Test files written to result directory can be accessed '
237                 'from the host running the container..')
238    host_test_script = os.path.join(RESULT_PATH, TEST_SCRIPT)
239    with open(host_test_script, 'w') as script:
240        if utils.is_moblab():
241            script.write(TEST_SCRIPT_CONTENT % {'ts_mon_test': ''})
242        else:
243            script.write(TEST_SCRIPT_CONTENT %
244                         {'ts_mon_test': TEST_SCRIPT_CONTENT_TS_MON})
245
246    container_result_path = lxc.RESULT_DIR_FMT % TEST_JOB_FOLDER
247    container_test_script = os.path.join(container_result_path, TEST_SCRIPT)
248    container_test_script_dest = os.path.join('/usr/local/autotest/utils/',
249                                              TEST_SCRIPT)
250    container_test_log = os.path.join(container_result_path, TEST_LOG)
251    host_test_log = os.path.join(RESULT_PATH, TEST_LOG)
252    # Move the test script out of result folder as it needs to import common.
253    container.attach_run('mv %s %s' % (container_test_script,
254                                       container_test_script_dest))
255    container.attach_run('python %s %s' % (container_test_script_dest,
256                                           container_test_log))
257    if not os.path.exists(host_test_log):
258        raise Exception('Results created in container can not be accessed from '
259                        'the host.')
260    with open(host_test_log, 'r') as log:
261        if log.read() != 'test':
262            raise Exception('Failed to read the content of results in '
263                            'container.')
264
265
266def test_autoserv(container):
267    """Test container can run autoserv command.
268
269    @param container: The test container.
270    """
271    logging.info('Test autoserv command.')
272    logging.info('Create test control file.')
273    host_control_file = os.path.join(RESULT_PATH, TEST_CONTROL_FILE)
274    with open(host_control_file, 'w') as control_file:
275        control_file.write(TEST_CONTROL_CONTENT)
276
277    logging.info('Run autoserv command.')
278    container.attach_run(AUTOSERV_COMMAND)
279
280    logging.info('Confirm results are available from host.')
281    # Read status.log to check the content is not empty.
282    container_status_log = os.path.join(TEST_RESULT_PATH, TEST_DUT,
283                                        'status.log')
284    status_log = container.attach_run(command='cat %s' % container_status_log
285                                      ).stdout
286    if len(status_log) < 10:
287        raise Exception('Failed to read status.log in container.')
288
289
290def test_package_install(container):
291    """Test installing package in container.
292
293    @param container: The test container.
294    """
295    # Packages are installed in TEST_SCRIPT_CONTENT. Verify the packages in
296    # this method.
297    container.attach_run('which atop')
298    container.attach_run('python -c "import acora"')
299
300
301def test_ssh(container, remote):
302    """Test container can run ssh to remote server.
303
304    @param container: The test container.
305    @param remote: The remote server to ssh to.
306
307    @raise: error.CmdError if container can't ssh to remote server.
308    """
309    logging.info('Test ssh to %s.', remote)
310    container.attach_run('ssh %s -a -x -o StrictHostKeyChecking=no '
311                         '-o BatchMode=yes -o UserKnownHostsFile=/dev/null '
312                         '-p 22 "true"' % remote)
313
314
315def parse_options():
316    """Parse command line inputs.
317    """
318    parser = argparse.ArgumentParser()
319    parser.add_argument('-d', '--dut', type=str,
320                        help='Test device to ssh to.',
321                        default=None)
322    parser.add_argument('-r', '--devserver', type=str,
323                        help='Test devserver to ssh to.',
324                        default=None)
325    parser.add_argument('-v', '--verbose', action='store_true',
326                        default=False,
327                        help='Print out ALL entries.')
328    parser.add_argument('-s', '--skip_cleanup', action='store_true',
329                        default=False,
330                        help='Skip deleting test containers.')
331    return parser.parse_args()
332
333
334def main(options):
335    """main script.
336
337    @param options: Options to run the script.
338    """
339    # Verify that the test is running as the correct user.
340    unittest_setup.verify_user()
341
342    log_level = (logging.DEBUG if options.verbose else logging.INFO)
343    unittest_setup.setup_logging(log_level)
344
345    setup_base(TEMP_DIR)
346    bucket = lxc.ContainerBucket(TEMP_DIR)
347
348    container_id = lxc.ContainerId.create(TEST_JOB_ID)
349    container = setup_test(bucket, container_id, options.skip_cleanup)
350    test_share(container)
351    test_autoserv(container)
352    if options.dut:
353        test_ssh(container, options.dut)
354    if options.devserver:
355        test_ssh(container, options.devserver)
356    # Packages are installed in TEST_SCRIPT, verify the packages are installed.
357    test_package_install(container)
358    logging.info('All tests passed.')
359
360
361if __name__ == '__main__':
362    options = parse_options()
363    try:
364        main(options)
365    except:
366        # If the cleanup code below raises additional errors, they obfuscate the
367        # actual error in the test.  Highlight the error to aid in debugging.
368        logging.exception('ERROR:\n%s', error.format_error())
369        raise
370    finally:
371        if not options.skip_cleanup:
372            logging.info('Cleaning up temporary directory %s.', TEMP_DIR)
373            try:
374                lxc.ContainerBucket(TEMP_DIR).destroy_all()
375            finally:
376                utils.run('sudo rm -rf "%s"' % TEMP_DIR)
377