1*9c5db199SXin Li# Copyright 2015 The Chromium OS Authors. All rights reserved. 2*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 3*9c5db199SXin Li# found in the LICENSE file. 4*9c5db199SXin Li 5*9c5db199SXin Li"""Argument validation for the DUT deployment tool. 6*9c5db199SXin Li 7*9c5db199SXin LiArguments for the DUT deployment commands require more processing than 8*9c5db199SXin Lican readily be done by `ArgumentParser.parse_args()`. The post-parsing 9*9c5db199SXin Livalidation process not only checks that arguments have allowable values, 10*9c5db199SXin Libut also may perform a dialog with the user to ask for missing arguments. 11*9c5db199SXin LiFinally, it adds in information needed by `install.install_duts()`. 12*9c5db199SXin Li 13*9c5db199SXin LiThe interactive dialog is invoked if the board and hostnames are omitted 14*9c5db199SXin Lifrom the command line. The dialog, if invoked, will get the following 15*9c5db199SXin Liinformation from the user: 16*9c5db199SXin Li * (required) Board of the DUTs to be deployed. 17*9c5db199SXin Li * (required) Hostnames of the DUTs to be deployed. 18*9c5db199SXin Li * (optional) Version of the test image to be made the stable 19*9c5db199SXin Li repair image for the board to be deployed. If omitted, the 20*9c5db199SXin Li existing setting is retained. 21*9c5db199SXin Li""" 22*9c5db199SXin Li 23*9c5db199SXin Liimport collections 24*9c5db199SXin Liimport csv 25*9c5db199SXin Liimport datetime 26*9c5db199SXin Liimport os 27*9c5db199SXin Liimport re 28*9c5db199SXin Liimport subprocess 29*9c5db199SXin Liimport sys 30*9c5db199SXin Li 31*9c5db199SXin Liimport dateutil.tz 32*9c5db199SXin Li 33*9c5db199SXin Liimport common 34*9c5db199SXin Lifrom autotest_lib.server.hosts import servo_constants 35*9c5db199SXin Li 36*9c5db199SXin Li# _BUILD_URI_FORMAT 37*9c5db199SXin Li# A format template for a Google storage URI that designates 38*9c5db199SXin Li# one build. The template is to be filled in with a board 39*9c5db199SXin Li# name and build version number. 40*9c5db199SXin Li 41*9c5db199SXin Li_BUILD_URI_FORMAT = 'gs://chromeos-image-archive/%s-release/%s' 42*9c5db199SXin Li 43*9c5db199SXin Li 44*9c5db199SXin Li# _BUILD_PATTERNS 45*9c5db199SXin Li# For user convenience, argument parsing allows various formats 46*9c5db199SXin Li# for build version strings. The function _normalize_build_name() 47*9c5db199SXin Li# is used to convert the recognized syntaxes into the name as 48*9c5db199SXin Li# it appears in Google storage. 49*9c5db199SXin Li# 50*9c5db199SXin Li# _BUILD_PATTERNS describe the recognized syntaxes for user-supplied 51*9c5db199SXin Li# build versions, and information about how to convert them. See the 52*9c5db199SXin Li# normalize function for details. 53*9c5db199SXin Li# 54*9c5db199SXin Li# For user-supplied build versions, the following forms are supported: 55*9c5db199SXin Li# #### - Indicates a canary; equivalent to ####.0.0. 56*9c5db199SXin Li# ####.#.# - A full build version without the leading R##- prefix. 57*9c5db199SXin Li# R##-###.#.# - Canonical form of a build version. 58*9c5db199SXin Li 59*9c5db199SXin Li_BUILD_PATTERNS = [ 60*9c5db199SXin Li (re.compile(r'^R\d+-\d+\.\d+\.\d+$'), None), 61*9c5db199SXin Li (re.compile(r'^\d+\.\d+\.\d+$'), 'LATEST-%s'), 62*9c5db199SXin Li (re.compile(r'^\d+$'), 'LATEST-%s.0.0'), 63*9c5db199SXin Li] 64*9c5db199SXin Li 65*9c5db199SXin Li 66*9c5db199SXin Li# _VALID_HOSTNAME_PATTERNS 67*9c5db199SXin Li# A list of REs describing patterns that are acceptable as names 68*9c5db199SXin Li# for DUTs in the test lab. Names that don't match one of the 69*9c5db199SXin Li# patterns will be rejected as invalid. 70*9c5db199SXin Li 71*9c5db199SXin Li_VALID_HOSTNAME_PATTERNS = [ 72*9c5db199SXin Li re.compile(r'chromeos\d+-row\d+-rack\d+-host\d+') 73*9c5db199SXin Li] 74*9c5db199SXin Li 75*9c5db199SXin Li 76*9c5db199SXin Li# _EXPECTED_NUMBER_OF_HOST_INFO 77*9c5db199SXin Li# The number of items per line when parsing the hostname_file csv file. 78*9c5db199SXin Li_EXPECTED_NUMBER_OF_HOST_INFO = 8 79*9c5db199SXin Li 80*9c5db199SXin Li# HostInfo 81*9c5db199SXin Li# Namedtuple to store host info for processing when creating host in the afe. 82*9c5db199SXin LiHostInfo = collections.namedtuple('HostInfo', ['hostname', 'host_attr_dict']) 83*9c5db199SXin Li 84*9c5db199SXin Li 85*9c5db199SXin Lidef _build_path_exists(board, buildpath): 86*9c5db199SXin Li """Return whether a given build file exists in Google storage. 87*9c5db199SXin Li 88*9c5db199SXin Li The `buildpath` refers to a specific file associated with 89*9c5db199SXin Li release builds for `board`. The path may be one of the "LATEST" 90*9c5db199SXin Li files (e.g. "LATEST-7356.0.0"), or it could refer to a build 91*9c5db199SXin Li artifact (e.g. "R46-7356.0.0/image.zip"). 92*9c5db199SXin Li 93*9c5db199SXin Li The function constructs the full GS URI from the arguments, and 94*9c5db199SXin Li then tests for its existence with `gsutil ls`. 95*9c5db199SXin Li 96*9c5db199SXin Li @param board Board to be tested. 97*9c5db199SXin Li @param buildpath Partial path of a file in Google storage. 98*9c5db199SXin Li 99*9c5db199SXin Li @return Return a true value iff the designated file exists. 100*9c5db199SXin Li """ 101*9c5db199SXin Li try: 102*9c5db199SXin Li gsutil_cmd = [ 103*9c5db199SXin Li 'gsutil', 'ls', 104*9c5db199SXin Li _BUILD_URI_FORMAT % (board, buildpath) 105*9c5db199SXin Li ] 106*9c5db199SXin Li status = subprocess.call(gsutil_cmd, 107*9c5db199SXin Li stdout=open('/dev/null', 'w'), 108*9c5db199SXin Li stderr=subprocess.STDOUT) 109*9c5db199SXin Li return status == 0 110*9c5db199SXin Li except: 111*9c5db199SXin Li return False 112*9c5db199SXin Li 113*9c5db199SXin Li 114*9c5db199SXin Lidef _normalize_build_name(board, build): 115*9c5db199SXin Li """Convert a user-supplied build version to canonical form. 116*9c5db199SXin Li 117*9c5db199SXin Li Canonical form looks like R##-####.#.#, e.g. R46-7356.0.0. 118*9c5db199SXin Li Acceptable user-supplied forms are describe under 119*9c5db199SXin Li _BUILD_PATTERNS, above. The returned value will be the name of 120*9c5db199SXin Li a directory containing build artifacts from a release builder 121*9c5db199SXin Li for the board. 122*9c5db199SXin Li 123*9c5db199SXin Li Walk through `_BUILD_PATTERNS`, trying to convert a user 124*9c5db199SXin Li supplied build version name into a directory name for valid 125*9c5db199SXin Li build artifacts. Searching stops at the first pattern matched, 126*9c5db199SXin Li regardless of whether the designated build actually exists. 127*9c5db199SXin Li 128*9c5db199SXin Li `_BUILD_PATTERNS` is a list of tuples. The first element of the 129*9c5db199SXin Li tuple is an RE describing a valid user input. The second 130*9c5db199SXin Li element of the tuple is a format pattern for a "LATEST" filename 131*9c5db199SXin Li in storage that can be used to obtain the full build version 132*9c5db199SXin Li associated with the user supplied version. If the second element 133*9c5db199SXin Li is `None`, the user supplied build version is already in canonical 134*9c5db199SXin Li form. 135*9c5db199SXin Li 136*9c5db199SXin Li @param board Board to be tested. 137*9c5db199SXin Li @param build User supplied version name. 138*9c5db199SXin Li 139*9c5db199SXin Li @return Return the name of a directory in canonical form, or 140*9c5db199SXin Li `None` if the build doesn't exist. 141*9c5db199SXin Li """ 142*9c5db199SXin Li for regex, fmt in _BUILD_PATTERNS: 143*9c5db199SXin Li if not regex.match(build): 144*9c5db199SXin Li continue 145*9c5db199SXin Li if fmt is not None: 146*9c5db199SXin Li try: 147*9c5db199SXin Li gsutil_cmd = [ 148*9c5db199SXin Li 'gsutil', 'cat', 149*9c5db199SXin Li _BUILD_URI_FORMAT % (board, fmt % build) 150*9c5db199SXin Li ] 151*9c5db199SXin Li return subprocess.check_output( 152*9c5db199SXin Li gsutil_cmd, stderr=open('/dev/null', 'w')) 153*9c5db199SXin Li except: 154*9c5db199SXin Li return None 155*9c5db199SXin Li elif _build_path_exists(board, '%s/image.zip' % build): 156*9c5db199SXin Li return build 157*9c5db199SXin Li else: 158*9c5db199SXin Li return None 159*9c5db199SXin Li return None 160*9c5db199SXin Li 161*9c5db199SXin Li 162*9c5db199SXin Lidef _validate_board(board): 163*9c5db199SXin Li """Return whether a given board exists in Google storage. 164*9c5db199SXin Li 165*9c5db199SXin Li For purposes of this function, a board exists if it has a 166*9c5db199SXin Li "LATEST-main" file in its release builder's directory. 167*9c5db199SXin Li 168*9c5db199SXin Li N.B. For convenience, this function prints an error message 169*9c5db199SXin Li on stderr in certain failure cases. This is currently useful 170*9c5db199SXin Li for argument processing, but isn't really ideal if the callers 171*9c5db199SXin Li were to get more complicated. 172*9c5db199SXin Li 173*9c5db199SXin Li @param board The board to be tested for existence. 174*9c5db199SXin Li @return Return a true value iff the board exists. 175*9c5db199SXin Li """ 176*9c5db199SXin Li # In this case, the board doesn't exist, but we don't want 177*9c5db199SXin Li # an error message. 178*9c5db199SXin Li if board is None: 179*9c5db199SXin Li return False 180*9c5db199SXin Li 181*9c5db199SXin Li # Check Google storage; report failures on stderr. 182*9c5db199SXin Li if _build_path_exists(board, 'LATEST-main'): 183*9c5db199SXin Li return True 184*9c5db199SXin Li else: 185*9c5db199SXin Li sys.stderr.write('Board %s doesn\'t exist.\n' % board) 186*9c5db199SXin Li return False 187*9c5db199SXin Li 188*9c5db199SXin Li 189*9c5db199SXin Lidef _validate_build(board, build): 190*9c5db199SXin Li """Return whether a given build exists in Google storage. 191*9c5db199SXin Li 192*9c5db199SXin Li N.B. For convenience, this function prints an error message 193*9c5db199SXin Li on stderr in certain failure cases. This is currently useful 194*9c5db199SXin Li for argument processing, but isn't really ideal if the callers 195*9c5db199SXin Li were to get more complicated. 196*9c5db199SXin Li 197*9c5db199SXin Li @param board The board to be tested for a build 198*9c5db199SXin Li @param build The version of the build to be tested for. This 199*9c5db199SXin Li build may be in a user-specified (non-canonical) 200*9c5db199SXin Li form. 201*9c5db199SXin Li @return If the given board+build exists, return its canonical 202*9c5db199SXin Li (normalized) version string. If the build doesn't 203*9c5db199SXin Li exist, return a false value. 204*9c5db199SXin Li """ 205*9c5db199SXin Li canonical_build = _normalize_build_name(board, build) 206*9c5db199SXin Li if not canonical_build: 207*9c5db199SXin Li sys.stderr.write( 208*9c5db199SXin Li 'Build %s is not a valid build version for %s.\n' % 209*9c5db199SXin Li (build, board)) 210*9c5db199SXin Li return canonical_build 211*9c5db199SXin Li 212*9c5db199SXin Li 213*9c5db199SXin Lidef _validate_hostname(hostname): 214*9c5db199SXin Li """Return whether a given hostname is valid for the test lab. 215*9c5db199SXin Li 216*9c5db199SXin Li This is a validity check meant to guarantee that host names follow 217*9c5db199SXin Li naming requirements for the test lab. 218*9c5db199SXin Li 219*9c5db199SXin Li N.B. For convenience, this function prints an error message 220*9c5db199SXin Li on stderr in certain failure cases. This is currently useful 221*9c5db199SXin Li for argument processing, but isn't really ideal if the callers 222*9c5db199SXin Li were to get more complicated. 223*9c5db199SXin Li 224*9c5db199SXin Li @param hostname The host name to be checked. 225*9c5db199SXin Li @return Return a true value iff the hostname is valid. 226*9c5db199SXin Li """ 227*9c5db199SXin Li for p in _VALID_HOSTNAME_PATTERNS: 228*9c5db199SXin Li if p.match(hostname): 229*9c5db199SXin Li return True 230*9c5db199SXin Li sys.stderr.write( 231*9c5db199SXin Li 'Hostname %s doesn\'t match a valid location name.\n' % 232*9c5db199SXin Li hostname) 233*9c5db199SXin Li return False 234*9c5db199SXin Li 235*9c5db199SXin Li 236*9c5db199SXin Lidef _is_hostname_file_valid(hostname_file): 237*9c5db199SXin Li """Check that the hostname file is valid. 238*9c5db199SXin Li 239*9c5db199SXin Li The hostname file is deemed valid if: 240*9c5db199SXin Li - the file exists. 241*9c5db199SXin Li - the file is non-empty. 242*9c5db199SXin Li 243*9c5db199SXin Li @param hostname_file Filename of the hostname file to check. 244*9c5db199SXin Li 245*9c5db199SXin Li @return `True` if the hostname file is valid, False otherse. 246*9c5db199SXin Li """ 247*9c5db199SXin Li return os.path.exists(hostname_file) and os.path.getsize(hostname_file) > 0 248*9c5db199SXin Li 249*9c5db199SXin Li 250*9c5db199SXin Lidef _validate_arguments(arguments): 251*9c5db199SXin Li """Check command line arguments, and account for defaults. 252*9c5db199SXin Li 253*9c5db199SXin Li Check that all command-line argument constraints are satisfied. 254*9c5db199SXin Li If errors are found, they are reported on `sys.stderr`. 255*9c5db199SXin Li 256*9c5db199SXin Li If there are any fields with defined defaults that couldn't be 257*9c5db199SXin Li calculated when we constructed the argument parser, calculate 258*9c5db199SXin Li them now. 259*9c5db199SXin Li 260*9c5db199SXin Li @param arguments Parsed results from 261*9c5db199SXin Li `ArgumentParser.parse_args()`. 262*9c5db199SXin Li @return Return `True` if there are no errors to report, or 263*9c5db199SXin Li `False` if there are. 264*9c5db199SXin Li """ 265*9c5db199SXin Li # If both hostnames and hostname_file are specified, complain about that. 266*9c5db199SXin Li if arguments.hostnames and arguments.hostname_file: 267*9c5db199SXin Li sys.stderr.write( 268*9c5db199SXin Li 'DUT hostnames and hostname file both specified, only ' 269*9c5db199SXin Li 'specify one or the other.\n') 270*9c5db199SXin Li return False 271*9c5db199SXin Li if (arguments.hostname_file and 272*9c5db199SXin Li not _is_hostname_file_valid(arguments.hostname_file)): 273*9c5db199SXin Li sys.stderr.write( 274*9c5db199SXin Li 'Specified hostname file must exist and be non-empty.\n') 275*9c5db199SXin Li return False 276*9c5db199SXin Li if (not arguments.hostnames and not arguments.hostname_file and 277*9c5db199SXin Li (arguments.board or arguments.build)): 278*9c5db199SXin Li sys.stderr.write( 279*9c5db199SXin Li 'DUT hostnames are required with board or build.\n') 280*9c5db199SXin Li return False 281*9c5db199SXin Li if arguments.board is not None: 282*9c5db199SXin Li if not _validate_board(arguments.board): 283*9c5db199SXin Li return False 284*9c5db199SXin Li if (arguments.build is not None and 285*9c5db199SXin Li not _validate_build(arguments.board, arguments.build)): 286*9c5db199SXin Li return False 287*9c5db199SXin Li return True 288*9c5db199SXin Li 289*9c5db199SXin Li 290*9c5db199SXin Lidef _read_with_prompt(input, prompt): 291*9c5db199SXin Li """Print a prompt and then read a line of text. 292*9c5db199SXin Li 293*9c5db199SXin Li @param input File-like object from which to read the line. 294*9c5db199SXin Li @param prompt String to print to stderr prior to reading. 295*9c5db199SXin Li @return Returns a string, stripped of whitespace. 296*9c5db199SXin Li """ 297*9c5db199SXin Li full_prompt = '%s> ' % prompt 298*9c5db199SXin Li sys.stderr.write(full_prompt) 299*9c5db199SXin Li return input.readline().strip() 300*9c5db199SXin Li 301*9c5db199SXin Li 302*9c5db199SXin Lidef _read_board(input, default_board): 303*9c5db199SXin Li """Read a valid board name from user input. 304*9c5db199SXin Li 305*9c5db199SXin Li Prompt the user to supply a board name, and read one line. If 306*9c5db199SXin Li the line names a valid board, return the board name. If the 307*9c5db199SXin Li line is blank and `default_board` is a non-empty string, returns 308*9c5db199SXin Li `default_board`. Retry until a valid input is obtained. 309*9c5db199SXin Li 310*9c5db199SXin Li `default_board` isn't checked; the caller is responsible for 311*9c5db199SXin Li ensuring its validity. 312*9c5db199SXin Li 313*9c5db199SXin Li @param input File-like object from which to read the 314*9c5db199SXin Li board. 315*9c5db199SXin Li @param default_board Value to return if the user enters a 316*9c5db199SXin Li blank line. 317*9c5db199SXin Li @return Returns `default_board` or a validated board name. 318*9c5db199SXin Li """ 319*9c5db199SXin Li if default_board: 320*9c5db199SXin Li board_prompt = 'board name [%s]' % default_board 321*9c5db199SXin Li else: 322*9c5db199SXin Li board_prompt = 'board name' 323*9c5db199SXin Li new_board = None 324*9c5db199SXin Li while not _validate_board(new_board): 325*9c5db199SXin Li new_board = _read_with_prompt(input, board_prompt).lower() 326*9c5db199SXin Li if new_board: 327*9c5db199SXin Li sys.stderr.write('Checking for valid board.\n') 328*9c5db199SXin Li elif default_board: 329*9c5db199SXin Li return default_board 330*9c5db199SXin Li return new_board 331*9c5db199SXin Li 332*9c5db199SXin Li 333*9c5db199SXin Lidef _read_build(input, board): 334*9c5db199SXin Li """Read a valid build version from user input. 335*9c5db199SXin Li 336*9c5db199SXin Li Prompt the user to supply a build version, and read one line. 337*9c5db199SXin Li If the line names an existing version for the given board, 338*9c5db199SXin Li return the canonical build version. If the line is blank, 339*9c5db199SXin Li return `None` (indicating the build shouldn't change). 340*9c5db199SXin Li 341*9c5db199SXin Li @param input File-like object from which to read the build. 342*9c5db199SXin Li @param board Board for the build. 343*9c5db199SXin Li @return Returns canonical build version, or `None`. 344*9c5db199SXin Li """ 345*9c5db199SXin Li build = False 346*9c5db199SXin Li prompt = 'build version (optional)' 347*9c5db199SXin Li while not build: 348*9c5db199SXin Li build = _read_with_prompt(input, prompt) 349*9c5db199SXin Li if not build: 350*9c5db199SXin Li return None 351*9c5db199SXin Li sys.stderr.write('Checking for valid build.\n') 352*9c5db199SXin Li build = _validate_build(board, build) 353*9c5db199SXin Li return build 354*9c5db199SXin Li 355*9c5db199SXin Li 356*9c5db199SXin Lidef _read_model(input, default_model): 357*9c5db199SXin Li """Read a valid model name from user input. 358*9c5db199SXin Li 359*9c5db199SXin Li Prompt the user to supply a model name, and read one line. If 360*9c5db199SXin Li the line names a valid model, return the model name. If the 361*9c5db199SXin Li line is blank and `default_model` is a non-empty string, returns 362*9c5db199SXin Li `default_model`. Retry until a valid input is obtained. 363*9c5db199SXin Li 364*9c5db199SXin Li `default_model` isn't checked; the caller is responsible for 365*9c5db199SXin Li ensuring its validity. 366*9c5db199SXin Li 367*9c5db199SXin Li @param input File-like object from which to read the 368*9c5db199SXin Li model. 369*9c5db199SXin Li @param default_model Value to return if the user enters a 370*9c5db199SXin Li blank line. 371*9c5db199SXin Li @return Returns `default_model` or a model name. 372*9c5db199SXin Li """ 373*9c5db199SXin Li model_prompt = 'model name' 374*9c5db199SXin Li if default_model: 375*9c5db199SXin Li model_prompt += ' [%s]' % default_model 376*9c5db199SXin Li new_model = None 377*9c5db199SXin Li # TODO(guocb): create a real model validator 378*9c5db199SXin Li _validate_model = lambda x: x 379*9c5db199SXin Li 380*9c5db199SXin Li while not _validate_model(new_model): 381*9c5db199SXin Li new_model = _read_with_prompt(input, model_prompt).lower() 382*9c5db199SXin Li if new_model: 383*9c5db199SXin Li sys.stderr.write("It's your responsiblity to ensure validity of " 384*9c5db199SXin Li "model name.\n") 385*9c5db199SXin Li elif default_model: 386*9c5db199SXin Li return default_model 387*9c5db199SXin Li return new_model 388*9c5db199SXin Li 389*9c5db199SXin Li 390*9c5db199SXin Lidef _read_hostnames(input): 391*9c5db199SXin Li """Read a list of host names from user input. 392*9c5db199SXin Li 393*9c5db199SXin Li Prompt the user to supply a list of host names. Any number of 394*9c5db199SXin Li lines are allowed; input is terminated at the first blank line. 395*9c5db199SXin Li Any number of hosts names are allowed on one line. Names are 396*9c5db199SXin Li separated by whitespace. 397*9c5db199SXin Li 398*9c5db199SXin Li Only valid host names are accepted. Invalid host names are 399*9c5db199SXin Li ignored, and a warning is printed. 400*9c5db199SXin Li 401*9c5db199SXin Li @param input File-like object from which to read the names. 402*9c5db199SXin Li @return Returns a list of validated host names. 403*9c5db199SXin Li """ 404*9c5db199SXin Li hostnames = [] 405*9c5db199SXin Li y_n = 'yes' 406*9c5db199SXin Li while not 'no'.startswith(y_n): 407*9c5db199SXin Li sys.stderr.write('enter hosts (blank line to end):\n') 408*9c5db199SXin Li while True: 409*9c5db199SXin Li new_hosts = input.readline().strip().split() 410*9c5db199SXin Li if not new_hosts: 411*9c5db199SXin Li break 412*9c5db199SXin Li for h in new_hosts: 413*9c5db199SXin Li if _validate_hostname(h): 414*9c5db199SXin Li hostnames.append(h) 415*9c5db199SXin Li if not hostnames: 416*9c5db199SXin Li sys.stderr.write('Must provide at least one hostname.\n') 417*9c5db199SXin Li continue 418*9c5db199SXin Li prompt = 'More hosts? [y/N]' 419*9c5db199SXin Li y_n = _read_with_prompt(input, prompt).lower() or 'no' 420*9c5db199SXin Li return hostnames 421*9c5db199SXin Li 422*9c5db199SXin Li 423*9c5db199SXin Lidef _read_arguments(input, arguments): 424*9c5db199SXin Li """Dialog to read all needed arguments from the user. 425*9c5db199SXin Li 426*9c5db199SXin Li The user is prompted in turn for a board, a build, a model, and 427*9c5db199SXin Li hostnames. Responses are stored in `arguments`. The user is 428*9c5db199SXin Li given opportunity to accept or reject the responses before 429*9c5db199SXin Li continuing. 430*9c5db199SXin Li 431*9c5db199SXin Li @param input File-like object from which to read user 432*9c5db199SXin Li responses. 433*9c5db199SXin Li @param arguments Namespace object returned from 434*9c5db199SXin Li `ArgumentParser.parse_args()`. Results are 435*9c5db199SXin Li stored here. 436*9c5db199SXin Li """ 437*9c5db199SXin Li y_n = 'no' 438*9c5db199SXin Li while not 'yes'.startswith(y_n): 439*9c5db199SXin Li arguments.board = _read_board(input, arguments.board) 440*9c5db199SXin Li arguments.build = _read_build(input, arguments.board) 441*9c5db199SXin Li arguments.model = _read_model(input, arguments.model) 442*9c5db199SXin Li prompt = '%s build %s? [Y/n]' % ( 443*9c5db199SXin Li arguments.board, arguments.build) 444*9c5db199SXin Li y_n = _read_with_prompt(input, prompt).lower() or 'yes' 445*9c5db199SXin Li arguments.hostnames = _read_hostnames(input) 446*9c5db199SXin Li 447*9c5db199SXin Li 448*9c5db199SXin Lidef _parse_hostname_file_line(hostname_file_row): 449*9c5db199SXin Li """ 450*9c5db199SXin Li Parse a line from the hostname_file and return a dict of the info. 451*9c5db199SXin Li 452*9c5db199SXin Li @param hostname_file_row: List of strings from each line in the hostname 453*9c5db199SXin Li file. 454*9c5db199SXin Li 455*9c5db199SXin Li @returns a NamedTuple of (hostname, host_attr_dict). host_attr_dict is a 456*9c5db199SXin Li dict of host attributes for the host. 457*9c5db199SXin Li """ 458*9c5db199SXin Li if len(hostname_file_row) != _EXPECTED_NUMBER_OF_HOST_INFO: 459*9c5db199SXin Li raise Exception('hostname_file line has unexpected number of items ' 460*9c5db199SXin Li '%d (expect %d): %s' % 461*9c5db199SXin Li (len(hostname_file_row), _EXPECTED_NUMBER_OF_HOST_INFO, 462*9c5db199SXin Li hostname_file_row)) 463*9c5db199SXin Li # The file will have the info in the following order: 464*9c5db199SXin Li # 0: board 465*9c5db199SXin Li # 1: dut hostname 466*9c5db199SXin Li # 2: dut/v4 mac address 467*9c5db199SXin Li # 3: dut ip 468*9c5db199SXin Li # 4: labstation hostname 469*9c5db199SXin Li # 5: servo serial 470*9c5db199SXin Li # 6: servo mac address 471*9c5db199SXin Li # 7: servo ip 472*9c5db199SXin Li return HostInfo( 473*9c5db199SXin Li hostname=hostname_file_row[1], 474*9c5db199SXin Li host_attr_dict={servo_constants.SERVO_HOST_ATTR: hostname_file_row[4], 475*9c5db199SXin Li servo_constants.SERVO_SERIAL_ATTR: hostname_file_row[5]}) 476*9c5db199SXin Li 477*9c5db199SXin Li 478*9c5db199SXin Lidef _get_upload_basename(arguments): 479*9c5db199SXin Li """Get base name for logs upload. 480*9c5db199SXin Li 481*9c5db199SXin Li @param arguments Namespace object returned from argument parsing. 482*9c5db199SXin Li @return A filename as a string. 483*9c5db199SXin Li """ 484*9c5db199SXin Li time_format = '%Y-%m-%dT%H%M%S.%f%z' 485*9c5db199SXin Li timestamp = datetime.datetime.now(dateutil.tz.tzlocal()).strftime( 486*9c5db199SXin Li time_format) 487*9c5db199SXin Li return '{time}-{board}'.format(time=timestamp, board=arguments.board) 488*9c5db199SXin Li 489*9c5db199SXin Li 490*9c5db199SXin Lidef _parse_hostname_file(hostname_file): 491*9c5db199SXin Li """ 492*9c5db199SXin Li Parse the hostname_file and return a list of dicts for each line. 493*9c5db199SXin Li 494*9c5db199SXin Li @param hostname_file: CSV file that contains all the goodies. 495*9c5db199SXin Li 496*9c5db199SXin Li @returns a list of dicts where each line is broken down into a dict. 497*9c5db199SXin Li """ 498*9c5db199SXin Li host_info_list = [] 499*9c5db199SXin Li # First line will be the header, no need to parse that. 500*9c5db199SXin Li first_line_skipped = False 501*9c5db199SXin Li with open(hostname_file) as f: 502*9c5db199SXin Li hostname_file_reader = csv.reader(f) 503*9c5db199SXin Li for row in hostname_file_reader: 504*9c5db199SXin Li if not first_line_skipped: 505*9c5db199SXin Li first_line_skipped = True 506*9c5db199SXin Li continue 507*9c5db199SXin Li host_info_list.append(_parse_hostname_file_line(row)) 508*9c5db199SXin Li 509*9c5db199SXin Li return host_info_list 510*9c5db199SXin Li 511*9c5db199SXin Li 512*9c5db199SXin Lidef validate_arguments(arguments): 513*9c5db199SXin Li """Validate parsed arguments for a repair or deployment command. 514*9c5db199SXin Li 515*9c5db199SXin Li The `arguments` parameter represents a `Namespace` object returned 516*9c5db199SXin Li by `cmdparse.parse_command()`. Check this for mandatory arguments; 517*9c5db199SXin Li if they're missing, execute a dialog with the user to read them from 518*9c5db199SXin Li `sys.stdin`. 519*9c5db199SXin Li 520*9c5db199SXin Li Once all arguments are known to be filled in, validate the values, 521*9c5db199SXin Li and fill in additional information that couldn't be processed at 522*9c5db199SXin Li parsing time. 523*9c5db199SXin Li 524*9c5db199SXin Li @param arguments Standard `Namespace` object as returned by 525*9c5db199SXin Li `cmdparse.parse_command()`. 526*9c5db199SXin Li """ 527*9c5db199SXin Li if not arguments.board or not arguments.model: 528*9c5db199SXin Li _read_arguments(sys.stdin, arguments) 529*9c5db199SXin Li elif not _validate_arguments(arguments): 530*9c5db199SXin Li return None 531*9c5db199SXin Li 532*9c5db199SXin Li arguments.upload_basename = _get_upload_basename(arguments) 533*9c5db199SXin Li if not arguments.logdir: 534*9c5db199SXin Li arguments.logdir = os.path.join(os.environ['HOME'], 535*9c5db199SXin Li 'Documents', 536*9c5db199SXin Li arguments.upload_basename) 537*9c5db199SXin Li os.makedirs(arguments.logdir) 538*9c5db199SXin Li elif not os.path.isdir(arguments.logdir): 539*9c5db199SXin Li os.mkdir(arguments.logdir) 540*9c5db199SXin Li 541*9c5db199SXin Li if arguments.hostname_file: 542*9c5db199SXin Li # Populate arguments.hostnames with the hostnames from the file. 543*9c5db199SXin Li hostname_file_info_list = _parse_hostname_file(arguments.hostname_file) 544*9c5db199SXin Li arguments.hostnames = [host_info.hostname 545*9c5db199SXin Li for host_info in hostname_file_info_list] 546*9c5db199SXin Li arguments.host_info_list = hostname_file_info_list 547*9c5db199SXin Li else: 548*9c5db199SXin Li arguments.host_info_list = [] 549*9c5db199SXin Li return arguments 550