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