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