xref: /aosp_15_r20/external/autotest/server/base_utils.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright 2008 Google Inc, Martin J. Bligh <[email protected]>,
3*9c5db199SXin Li#                Benjamin Poirier, Ryan Stutsman
4*9c5db199SXin Li# Released under the GPL v2
5*9c5db199SXin Li"""
6*9c5db199SXin LiMiscellaneous small functions.
7*9c5db199SXin Li
8*9c5db199SXin LiDO NOT import this file directly - it is mixed in by server/utils.py,
9*9c5db199SXin Liimport that instead
10*9c5db199SXin Li"""
11*9c5db199SXin Li
12*9c5db199SXin Lifrom __future__ import absolute_import
13*9c5db199SXin Lifrom __future__ import division
14*9c5db199SXin Lifrom __future__ import print_function
15*9c5db199SXin Li
16*9c5db199SXin Liimport atexit, os, re, shutil, textwrap, sys, tempfile, types
17*9c5db199SXin Liimport six
18*9c5db199SXin Li
19*9c5db199SXin Lifrom autotest_lib.client.common_lib import barrier, utils
20*9c5db199SXin Lifrom autotest_lib.server import subcommand
21*9c5db199SXin Li
22*9c5db199SXin Li
23*9c5db199SXin Li# A dictionary of pid and a list of tmpdirs for that pid
24*9c5db199SXin Li__tmp_dirs = {}
25*9c5db199SXin Li
26*9c5db199SXin Li
27*9c5db199SXin Lidef scp_remote_escape(filename):
28*9c5db199SXin Li    """
29*9c5db199SXin Li    Escape special characters from a filename so that it can be passed
30*9c5db199SXin Li    to scp (within double quotes) as a remote file.
31*9c5db199SXin Li
32*9c5db199SXin Li    Bis-quoting has to be used with scp for remote files, "bis-quoting"
33*9c5db199SXin Li    as in quoting x 2
34*9c5db199SXin Li    scp does not support a newline in the filename
35*9c5db199SXin Li
36*9c5db199SXin Li    Args:
37*9c5db199SXin Li            filename: the filename string to escape.
38*9c5db199SXin Li
39*9c5db199SXin Li    Returns:
40*9c5db199SXin Li            The escaped filename string. The required englobing double
41*9c5db199SXin Li            quotes are NOT added and so should be added at some point by
42*9c5db199SXin Li            the caller.
43*9c5db199SXin Li    """
44*9c5db199SXin Li    escape_chars= r' !"$&' "'" r'()*,:;<=>?[\]^`{|}'
45*9c5db199SXin Li
46*9c5db199SXin Li    new_name= []
47*9c5db199SXin Li    for char in filename:
48*9c5db199SXin Li        if char in escape_chars:
49*9c5db199SXin Li            new_name.append("\\%s" % (char,))
50*9c5db199SXin Li        else:
51*9c5db199SXin Li            new_name.append(char)
52*9c5db199SXin Li
53*9c5db199SXin Li    return utils.sh_escape("".join(new_name))
54*9c5db199SXin Li
55*9c5db199SXin Li
56*9c5db199SXin Lidef get(location, local_copy = False):
57*9c5db199SXin Li    """Get a file or directory to a local temporary directory.
58*9c5db199SXin Li
59*9c5db199SXin Li    Args:
60*9c5db199SXin Li            location: the source of the material to get. This source may
61*9c5db199SXin Li                    be one of:
62*9c5db199SXin Li                    * a local file or directory
63*9c5db199SXin Li                    * a URL (http or ftp)
64*9c5db199SXin Li                    * a python file-like object
65*9c5db199SXin Li
66*9c5db199SXin Li    Returns:
67*9c5db199SXin Li            The location of the file or directory where the requested
68*9c5db199SXin Li            content was saved. This will be contained in a temporary
69*9c5db199SXin Li            directory on the local host. If the material to get was a
70*9c5db199SXin Li            directory, the location will contain a trailing '/'
71*9c5db199SXin Li    """
72*9c5db199SXin Li    tmpdir = get_tmp_dir()
73*9c5db199SXin Li
74*9c5db199SXin Li    # location is a file-like object
75*9c5db199SXin Li    if hasattr(location, "read"):
76*9c5db199SXin Li        tmpfile = os.path.join(tmpdir, "file")
77*9c5db199SXin Li        tmpfileobj = open(tmpfile, 'w')
78*9c5db199SXin Li        shutil.copyfileobj(location, tmpfileobj)
79*9c5db199SXin Li        tmpfileobj.close()
80*9c5db199SXin Li        return tmpfile
81*9c5db199SXin Li
82*9c5db199SXin Li    if isinstance(location, six.string_types):
83*9c5db199SXin Li        # location is a URL
84*9c5db199SXin Li        if location.startswith('http') or location.startswith('ftp'):
85*9c5db199SXin Li            tmpfile = os.path.join(tmpdir, os.path.basename(location))
86*9c5db199SXin Li            utils.urlretrieve(location, tmpfile)
87*9c5db199SXin Li            return tmpfile
88*9c5db199SXin Li        # location is a local path
89*9c5db199SXin Li        elif os.path.exists(os.path.abspath(location)):
90*9c5db199SXin Li            if not local_copy:
91*9c5db199SXin Li                if os.path.isdir(location):
92*9c5db199SXin Li                    return location.rstrip('/') + '/'
93*9c5db199SXin Li                else:
94*9c5db199SXin Li                    return location
95*9c5db199SXin Li            tmpfile = os.path.join(tmpdir, os.path.basename(location))
96*9c5db199SXin Li            if os.path.isdir(location):
97*9c5db199SXin Li                tmpfile += '/'
98*9c5db199SXin Li                shutil.copytree(location, tmpfile, symlinks=True)
99*9c5db199SXin Li                return tmpfile
100*9c5db199SXin Li            shutil.copyfile(location, tmpfile)
101*9c5db199SXin Li            return tmpfile
102*9c5db199SXin Li        # location is just a string, dump it to a file
103*9c5db199SXin Li        else:
104*9c5db199SXin Li            tmpfd, tmpfile = tempfile.mkstemp(dir=tmpdir)
105*9c5db199SXin Li            tmpfileobj = os.fdopen(tmpfd, 'w')
106*9c5db199SXin Li            tmpfileobj.write(location)
107*9c5db199SXin Li            tmpfileobj.close()
108*9c5db199SXin Li            return tmpfile
109*9c5db199SXin Li
110*9c5db199SXin Li
111*9c5db199SXin Lidef get_tmp_dir():
112*9c5db199SXin Li    """Return the pathname of a directory on the host suitable
113*9c5db199SXin Li    for temporary file storage.
114*9c5db199SXin Li
115*9c5db199SXin Li    The directory and its content will be deleted automatically
116*9c5db199SXin Li    at the end of the program execution if they are still present.
117*9c5db199SXin Li    """
118*9c5db199SXin Li    dir_name = tempfile.mkdtemp(prefix="autoserv-")
119*9c5db199SXin Li    pid = os.getpid()
120*9c5db199SXin Li    if not pid in __tmp_dirs:
121*9c5db199SXin Li        __tmp_dirs[pid] = []
122*9c5db199SXin Li    __tmp_dirs[pid].append(dir_name)
123*9c5db199SXin Li    return dir_name
124*9c5db199SXin Li
125*9c5db199SXin Li
126*9c5db199SXin Lidef __clean_tmp_dirs():
127*9c5db199SXin Li    """Erase temporary directories that were created by the get_tmp_dir()
128*9c5db199SXin Li    function and that are still present.
129*9c5db199SXin Li    """
130*9c5db199SXin Li    pid = os.getpid()
131*9c5db199SXin Li    if pid not in __tmp_dirs:
132*9c5db199SXin Li        return
133*9c5db199SXin Li    for dir in __tmp_dirs[pid]:
134*9c5db199SXin Li        try:
135*9c5db199SXin Li            shutil.rmtree(dir)
136*9c5db199SXin Li        except OSError as e:
137*9c5db199SXin Li            if e.errno == 2:
138*9c5db199SXin Li                pass
139*9c5db199SXin Li    __tmp_dirs[pid] = []
140*9c5db199SXin Liatexit.register(__clean_tmp_dirs)
141*9c5db199SXin Lisubcommand.subcommand.register_join_hook(lambda _: __clean_tmp_dirs())
142*9c5db199SXin Li
143*9c5db199SXin Li
144*9c5db199SXin Lidef unarchive(host, source_material):
145*9c5db199SXin Li    """Uncompress and untar an archive on a host.
146*9c5db199SXin Li
147*9c5db199SXin Li    If the "source_material" is compresses (according to the file
148*9c5db199SXin Li    extension) it will be uncompressed. Supported compression formats
149*9c5db199SXin Li    are gzip and bzip2. Afterwards, if the source_material is a tar
150*9c5db199SXin Li    archive, it will be untarred.
151*9c5db199SXin Li
152*9c5db199SXin Li    Args:
153*9c5db199SXin Li            host: the host object on which the archive is located
154*9c5db199SXin Li            source_material: the path of the archive on the host
155*9c5db199SXin Li
156*9c5db199SXin Li    Returns:
157*9c5db199SXin Li            The file or directory name of the unarchived source material.
158*9c5db199SXin Li            If the material is a tar archive, it will be extracted in the
159*9c5db199SXin Li            directory where it is and the path returned will be the first
160*9c5db199SXin Li            entry in the archive, assuming it is the topmost directory.
161*9c5db199SXin Li            If the material is not an archive, nothing will be done so this
162*9c5db199SXin Li            function is "harmless" when it is "useless".
163*9c5db199SXin Li    """
164*9c5db199SXin Li    # uncompress
165*9c5db199SXin Li    if (source_material.endswith(".gz") or
166*9c5db199SXin Li            source_material.endswith(".gzip")):
167*9c5db199SXin Li        host.run('gunzip "%s"' % (utils.sh_escape(source_material)))
168*9c5db199SXin Li        source_material= ".".join(source_material.split(".")[:-1])
169*9c5db199SXin Li    elif source_material.endswith("bz2"):
170*9c5db199SXin Li        host.run('bunzip2 "%s"' % (utils.sh_escape(source_material)))
171*9c5db199SXin Li        source_material= ".".join(source_material.split(".")[:-1])
172*9c5db199SXin Li
173*9c5db199SXin Li    # untar
174*9c5db199SXin Li    if source_material.endswith(".tar"):
175*9c5db199SXin Li        retval= host.run('tar -C "%s" -xvf "%s"' % (
176*9c5db199SXin Li                utils.sh_escape(os.path.dirname(source_material)),
177*9c5db199SXin Li                utils.sh_escape(source_material),))
178*9c5db199SXin Li        source_material= os.path.join(os.path.dirname(source_material),
179*9c5db199SXin Li                retval.stdout.split()[0])
180*9c5db199SXin Li
181*9c5db199SXin Li    return source_material
182*9c5db199SXin Li
183*9c5db199SXin Li
184*9c5db199SXin Lidef get_server_dir():
185*9c5db199SXin Li    path = os.path.dirname(sys.modules['autotest_lib.server.utils'].__file__)
186*9c5db199SXin Li    return os.path.abspath(path)
187*9c5db199SXin Li
188*9c5db199SXin Li
189*9c5db199SXin Lidef find_pid(command):
190*9c5db199SXin Li    for line in utils.system_output('ps -eo pid,cmd').rstrip().split('\n'):
191*9c5db199SXin Li        (pid, cmd) = line.split(None, 1)
192*9c5db199SXin Li        if re.search(command, cmd):
193*9c5db199SXin Li            return int(pid)
194*9c5db199SXin Li    return None
195*9c5db199SXin Li
196*9c5db199SXin Li
197*9c5db199SXin Lidef default_mappings(machines):
198*9c5db199SXin Li    """
199*9c5db199SXin Li    Returns a simple mapping in which all machines are assigned to the
200*9c5db199SXin Li    same key.  Provides the default behavior for
201*9c5db199SXin Li    form_ntuples_from_machines. """
202*9c5db199SXin Li    mappings = {}
203*9c5db199SXin Li    failures = []
204*9c5db199SXin Li
205*9c5db199SXin Li    mach = machines[0]
206*9c5db199SXin Li    mappings['ident'] = [mach]
207*9c5db199SXin Li    if len(machines) > 1:
208*9c5db199SXin Li        machines = machines[1:]
209*9c5db199SXin Li        for machine in machines:
210*9c5db199SXin Li            mappings['ident'].append(machine)
211*9c5db199SXin Li
212*9c5db199SXin Li    return (mappings, failures)
213*9c5db199SXin Li
214*9c5db199SXin Li
215*9c5db199SXin Lidef form_ntuples_from_machines(machines, n=2, mapping_func=default_mappings):
216*9c5db199SXin Li    """Returns a set of ntuples from machines where the machines in an
217*9c5db199SXin Li       ntuple are in the same mapping, and a set of failures which are
218*9c5db199SXin Li       (machine name, reason) tuples."""
219*9c5db199SXin Li    ntuples = []
220*9c5db199SXin Li    (mappings, failures) = mapping_func(machines)
221*9c5db199SXin Li
222*9c5db199SXin Li    # now run through the mappings and create n-tuples.
223*9c5db199SXin Li    # throw out the odd guys out
224*9c5db199SXin Li    for key in mappings:
225*9c5db199SXin Li        key_machines = mappings[key]
226*9c5db199SXin Li        total_machines = len(key_machines)
227*9c5db199SXin Li
228*9c5db199SXin Li        # form n-tuples
229*9c5db199SXin Li        while len(key_machines) >= n:
230*9c5db199SXin Li            ntuples.append(key_machines[0:n])
231*9c5db199SXin Li            key_machines = key_machines[n:]
232*9c5db199SXin Li
233*9c5db199SXin Li        for mach in key_machines:
234*9c5db199SXin Li            failures.append((mach, "machine can not be tupled"))
235*9c5db199SXin Li
236*9c5db199SXin Li    return (ntuples, failures)
237*9c5db199SXin Li
238*9c5db199SXin Li
239*9c5db199SXin Lidef parse_machine(machine, user='root', password='', port=None):
240*9c5db199SXin Li    """
241*9c5db199SXin Li    Parse the machine string user:pass@host:port and return it separately,
242*9c5db199SXin Li    if the machine string is not complete, use the default parameters
243*9c5db199SXin Li    when appropriate.
244*9c5db199SXin Li    """
245*9c5db199SXin Li
246*9c5db199SXin Li    if '@' in machine:
247*9c5db199SXin Li        user, machine = machine.split('@', 1)
248*9c5db199SXin Li
249*9c5db199SXin Li    if ':' in user:
250*9c5db199SXin Li        user, password = user.split(':', 1)
251*9c5db199SXin Li
252*9c5db199SXin Li    # Brackets are required to protect an IPv6 address whenever a
253*9c5db199SXin Li    # [xx::xx]:port number (or a file [xx::xx]:/path/) is appended to
254*9c5db199SXin Li    # it. Do not attempt to extract a (non-existent) port number from
255*9c5db199SXin Li    # an unprotected/bare IPv6 address "xx::xx".
256*9c5db199SXin Li    # In the Python >= 3.3 future, 'import ipaddress' will parse
257*9c5db199SXin Li    # addresses; and maybe more.
258*9c5db199SXin Li    bare_ipv6 = '[' != machine[0] and re.search(r':.*:', machine)
259*9c5db199SXin Li
260*9c5db199SXin Li    # Extract trailing :port number if any.
261*9c5db199SXin Li    if not bare_ipv6 and re.search(r':\d*$', machine):
262*9c5db199SXin Li        machine, port = machine.rsplit(':', 1)
263*9c5db199SXin Li        port = int(port)
264*9c5db199SXin Li
265*9c5db199SXin Li    # Strip any IPv6 brackets (ssh does not support them).
266*9c5db199SXin Li    # We'll add them back later for rsync, scp, etc.
267*9c5db199SXin Li    if machine[0] == '[' and machine[-1] == ']':
268*9c5db199SXin Li        machine = machine[1:-1]
269*9c5db199SXin Li
270*9c5db199SXin Li    if not machine or not user:
271*9c5db199SXin Li        raise ValueError
272*9c5db199SXin Li
273*9c5db199SXin Li    return machine, user, password, port
274*9c5db199SXin Li
275*9c5db199SXin Li
276*9c5db199SXin Lidef get_public_key():
277*9c5db199SXin Li    """
278*9c5db199SXin Li    Return a valid string ssh public key for the user executing autoserv or
279*9c5db199SXin Li    autotest. If there's no DSA or RSA public key, create a DSA keypair with
280*9c5db199SXin Li    ssh-keygen and return it.
281*9c5db199SXin Li    """
282*9c5db199SXin Li
283*9c5db199SXin Li    ssh_conf_path = os.path.expanduser('~/.ssh')
284*9c5db199SXin Li
285*9c5db199SXin Li    dsa_public_key_path = os.path.join(ssh_conf_path, 'id_dsa.pub')
286*9c5db199SXin Li    dsa_private_key_path = os.path.join(ssh_conf_path, 'id_dsa')
287*9c5db199SXin Li
288*9c5db199SXin Li    rsa_public_key_path = os.path.join(ssh_conf_path, 'id_rsa.pub')
289*9c5db199SXin Li    rsa_private_key_path = os.path.join(ssh_conf_path, 'id_rsa')
290*9c5db199SXin Li
291*9c5db199SXin Li    has_dsa_keypair = os.path.isfile(dsa_public_key_path) and \
292*9c5db199SXin Li        os.path.isfile(dsa_private_key_path)
293*9c5db199SXin Li    has_rsa_keypair = os.path.isfile(rsa_public_key_path) and \
294*9c5db199SXin Li        os.path.isfile(rsa_private_key_path)
295*9c5db199SXin Li
296*9c5db199SXin Li    if has_dsa_keypair:
297*9c5db199SXin Li        print('DSA keypair found, using it')
298*9c5db199SXin Li        public_key_path = dsa_public_key_path
299*9c5db199SXin Li
300*9c5db199SXin Li    elif has_rsa_keypair:
301*9c5db199SXin Li        print('RSA keypair found, using it')
302*9c5db199SXin Li        public_key_path = rsa_public_key_path
303*9c5db199SXin Li
304*9c5db199SXin Li    else:
305*9c5db199SXin Li        print('Neither RSA nor DSA keypair found, creating DSA ssh key pair')
306*9c5db199SXin Li        utils.system('ssh-keygen -t dsa -q -N "" -f %s' % dsa_private_key_path)
307*9c5db199SXin Li        public_key_path = dsa_public_key_path
308*9c5db199SXin Li
309*9c5db199SXin Li    public_key = open(public_key_path, 'r')
310*9c5db199SXin Li    public_key_str = public_key.read()
311*9c5db199SXin Li    public_key.close()
312*9c5db199SXin Li
313*9c5db199SXin Li    return public_key_str
314