xref: /aosp_15_r20/external/autotest/site_utils/deployment/cmdvalidate.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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