blob: c75f4993985d09a9dd037c5cd833ddd678e1a849 [file] [log] [blame]
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +08001# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
Ruben Rodriguez Buchillon0902f092018-09-19 15:03:17 +08004"""Common code for servo parsing support."""
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +08005
6import argparse
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +08007import logging
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +08008import os
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +08009import sys
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +080010import textwrap
11
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +080012import client
13import servo_logging
14import servodutil
15
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +080016if os.getuid():
17 DEFAULT_RC_FILE = '/home/%s/.servodrc' % os.getenv('USER', '')
18else:
19 DEFAULT_RC_FILE = '/home/%s/.servodrc' % os.getenv('SUDO_USER', '')
20
21
22class ServodParserHelpFormatter(argparse.RawDescriptionHelpFormatter,
23 argparse.ArgumentDefaultsHelpFormatter):
24 """Servod help formatter.
25
26 Combines ability for raw description printing (needed to have control over
27 how to print examples) and default argument printing, printing the default
28 which each argument.
29 """
30 pass
31
32
33class ServodParserError(Exception):
34 """Error class for Servod parsing errors."""
35 pass
36
37
38class _BaseServodParser(argparse.ArgumentParser):
39 """Extension to ArgumentParser that allows for examples in the description.
40
41 _BaseServodParser allows for a list of example tuples, where
42 element[0]: is the cmdline invocation
43 element[1]: is a comment to explain what the invocation does.
44
45 For example (loosely based on servod.)
46 ('-b board', 'Start servod with the configuation for board |board|')
47 would print the following help message:
48 ...
49
50 Examples:
51 > servod -b board
52 Start servod with the configuration for board |board|
53
54 Optional Arguments...
55
56 see servod, or dut_control for more examples.
57 """
58
Ruben Rodriguez Buchillon0902f092018-09-19 15:03:17 +080059 def __init__(self, description='', examples=None, **kwargs):
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +080060 """Initialize _BaseServodParser by setting description and formatter.
61
62 Args:
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +080063 description: description of the program
64 examples: list of tuples where the first element is the cmdline example,
65 and the second element is a comment explaining the example.
66 %(prog)s will be prepended to each example if it does not
67 start with %(prog)s.
Ruben Rodriguez Buchillon0902f092018-09-19 15:03:17 +080068 **kwargs: keyword arguments forwarded to ArgumentParser
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +080069 """
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +080070 # Initialize logging up here first to ensure log messages from parsing
71 # can go through.
72 loglevel, fmt = servo_logging.LOGLEVEL_MAP[servo_logging.DEFAULT_LOGLEVEL]
73 logging.basicConfig(loglevel=loglevel, format=fmt)
74 self._logger = logging.getLogger(type(self).__name__)
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +080075 # Generate description.
76 description_lines = textwrap.wrap(description)
77 # Setting it into the kwargs here ensures that we overwrite an potentially
78 # passed in and undesired formatter class.
79 kwargs['formatter_class'] = ServodParserHelpFormatter
80 if examples:
81 # Extra newline to separate description from examples.
82 description_lines.append('\n')
83 description_lines.append('Examples:')
84 for example, comment in examples:
85 if not example.startswith('%(prog)s'):
86 example = '%(prog)s ' + example
87 example_lines = [' > ' + example]
88 example_lines.extend(textwrap.wrap(comment))
89 description_lines.append('\n\t'.join(example_lines))
90 description = '\n'.join(description_lines)
91 kwargs['description'] = description
Ruben Rodriguez Buchillon0902f092018-09-19 15:03:17 +080092 super(_BaseServodParser, self).__init__(**kwargs)
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +080093
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +080094 @staticmethod
95 def _HandleEnvVar(cmdline, env_var, flag, pri_flags, logger, cast_type=None):
96 """Look for non-defined options in the environment and add to command line.
97
98 If |env_var| is defined in the environment and none of the variables defined
99 in |pri_flags| are in the command line, then add --|flag| |$(env_var)| to
100 the command line.
101
102 Notes:
103 - Modifies cmdline if name needs to be appended and an environment variable
104 is defined.
105
106 Args:
107 cmdline: the list of cmdline arguments
108 env_var: environment variable name to search
109 flag: flag name to add the environment variable under
110 pri_flags: list of flags in the cmdline that have priority over the
111 environment variable. If any found, the environment variable
112 will not be pulled in.
113 logger: logger instance to use
114 cast_type: optional callable to verify the environment flag is castable
115 to a desired type
116 Returns:
117 tuple: (cmdline, flag_added)
118 cmdline - Reference back to the cmdline passed in
119 flag_added - True if the flag was added, or if a collision flag was
120 found, false otherwise
121 """
122 if any(cl_flag in pri_flags for cl_flag in cmdline):
123 return (cmdline, True)
124 flag_added = False
125 # If port is not defined, attempt to retrieve it from the environment.
126 env_var_val = os.getenv(env_var)
127 if env_var_val:
128 try:
129 # Casting to int first to ensure the value is int-able and suitable
130 # to be a port option.
131 if cast_type:
132 cast_type(env_var_val)
133 cmdline.extend([flag, env_var_val])
134 logger.info("Adding '--%s %s' to the cmdline.", flag, env_var_val)
135 flag_added = True
136 except ValueError:
137 logger.warning('Ignoring environment %r definition %r', env_var,
138 env_var_val)
139 return (cmdline, flag_added)
140
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800141
142class BaseServodParser(_BaseServodParser):
143 """BaseServodParser handling common arguments in the servod cmdline tools."""
144
Ruben Rodriguez Buchillon0902f092018-09-19 15:03:17 +0800145 def __init__(self, **kwargs):
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800146 """Initialize by adding common arguments.
147
148 Adds:
149 - host/port arguments to find/initialize a servod instance
150 - debug argument to toggle debug message printing
151 - name/rcfile arguments to handle servodrc configurations
152
153 Args:
Ruben Rodriguez Buchillon0902f092018-09-19 15:03:17 +0800154 **kwargs: keyword arguments forwarded to _BaseServodParser
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800155 """
Ruben Rodriguez Buchillon0902f092018-09-19 15:03:17 +0800156 super(BaseServodParser, self).__init__(**kwargs)
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800157 self.add_argument('-d', '--debug', action='store_true', default=False,
158 help='enable debug messages')
159 self.add_argument('--host', default='localhost', type=str,
160 help='hostname of the servod server.')
161 self.add_argument('-p', '--port', default=None, type=int,
162 help='port of the servod server.')
163
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800164 @staticmethod
165 def HandlePortEnvVar(cmdline=None, pri_flags=None, logger=logging):
166 """Probe SERVOD_PORT environment variable.
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800167
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800168 SERVOD_PORT environment variable will be used if defined and port not in
169 the cmdline
Ruben Rodriguez Buchillon0902f092018-09-19 15:03:17 +0800170
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800171 Args:
172 cmdline: the list of cmdline arguments
173 pri_flags: list of flags in the cmdline that have priority over the
174 environment variable. If any found, the environment variable
175 will not be pulled in.
176 logger: logger instance to use
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800177
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800178 Returns:
179 tuple: (cmdline, port_defined)
180 cmdline - Reference back to the cmdline passed in
181 port_defined - bool showing if after this port is now in the cmdline
182 """
183 if not pri_flags:
184 pri_flags = ['-p', '--port']
185 if cmdline is None:
186 cmdline = sys.argv[1:0]
187 return _BaseServodParser._HandleEnvVar(cmdline=cmdline,
188 env_var='SERVOD_PORT', flag='-p',
189 pri_flags=pri_flags, logger=logger,
190 cast_type=int)
191
192
193class _ServodRCParser(_BaseServodParser):
194 """Base class to build Servod parsers to natively handle servorc.
195
196 This class overwrites parse_args & parse_known_args to:
197 - handle SERVOD_NAME environment variable
198 - parse & substitute in the servorc file on matches
199
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800200 """
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800201
202 def __init__(self, **kwargs):
203 super(_ServodRCParser, self).__init__(**kwargs)
204 self.add_argument('--rcfile', type=str, default=DEFAULT_RC_FILE,
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800205 help='servo description file for multi-servo operation.')
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800206 self.add_argument('-n', '--name', type=str,
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800207 help='symbolic name of the servo board, '
208 'used as a config shortcut, could also be supplied '
209 'through environment variable SERVOD_NAME')
210
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800211 @staticmethod
212 def HandleNameEnvVar(cmdline=None, pri_flags=None, logger=logging):
213 """Probe SERVOD_NAME environment variable.
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800214
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800215 SERVOD_NAME environment variables can be used if -s/--serialname
216 and --name command line switches are not set.
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800217
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800218
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800219 Args:
220 cmdline: the list of cmdline arguments
221 pri_flags: list of flags in the cmdline that have priority over the
222 environment variable. If any found, the environment variable
223 will not be pulled in.
224 logger: logger instance to use
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800225
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800226 Returns:
227 tuple: (cmdline, name_defined)
228 cmdline - Reference back to the cmdline passed in
229 name_defined - bool showing if name or serialname (a unique id)
230 is now defined in the cmdline
231 """
232 if not pri_flags:
233 pri_flags = ['-n', '--name', '-s', '--serialname']
234 if cmdline is None:
235 cmdline = sys.argv[1:0]
236 return _BaseServodParser._HandleEnvVar(cmdline=cmdline,
237 env_var='SERVOD_NAME',
238 flag='-n', pri_flags=pri_flags,
239 logger=logger)
240
241 @staticmethod
242 def PostProcessRCElements(options, rcpath=None, logger=logging):
243 """Handle 'name' in |options| by substituting it with the intended config.
244
245 This replaces the name option in the options with the intended serialname
246 for that name if one can be found. If a board file is also specified in the
247 rc file it appends that to the options too, which can be ignored if not
248 needed.
249
250 Note: this function changes the content of options.
251
252 Args:
253 options: argparse Namespace of options to process.
254 rcpath: optional rcfile path if it's not stored under options.rcfile
255 logger: logger instance to use
256
257 Returns:
258 Reference back to the same options passed in.
259
260 Raises:
261 ServodParserError: if -n/--name and -s/--serialname both defined
262 ServodParserError: if name in options doesn't show up in servodrc
263 """
264 if options.name and options.serialname:
265 # NOTE(coconutruben): This is temporary until the CL that splits
266 # parsing inside of servod.py removes the need for this.
267 raise ServodParserError('error: argument -s/--serialname not allowed with'
268 ' argument -n/--name.')
269 if not rcpath:
270 rcpath = options.rcfile
271 rcd = _ServodRCParser.ParseRC(rcpath, logger=logger)
272 rc = None
273 if options.name:
274 if options.name not in rcd:
275 raise ServodParserError('Name %r not in rc at %r' % (options.name,
276 rcpath))
277 rc = rcd[options.name]
278 if 'sn' in rc:
279 setattr(options, 'serialname', rc['sn'])
280 elif options.serialname:
281 # srcs meaning serialname runtime configurations (rcs).
282 srcs = [(name, rc) for name, rc in rcd.iteritems() if
283 rc['sn'] == options.serialname]
284 if srcs:
285 logger.info('Found servodrc entry %r for serialname %r. Using it.',
286 srcs[0][0], options.serialname)
287 rc = srcs[0][1]
288 if rc:
289 for elem in ['board', 'model']:
290 # Unlike serialname explicit overwrites of board and model in the
291 # cmdline are fine as the name flag is still useful to refer to a
292 # serialname.
293 if elem in rc and hasattr(options, elem):
294 if not getattr(options, elem):
295 logger.info('Setting %r to %r in the options as indicated by '
296 'servodrc file.', elem, rc[elem])
297 setattr(options, elem, rc[elem])
298 else:
299 if getattr(options, elem) != rc[elem]:
300 logger.warning('Ignoring rc configured %r name %r for servo %r. '
301 'Option already defined on the command line as %r',
302 elem, rc[elem], rc['sn'], getattr(options, elem))
303 return options
304
305 def parse_known_args(self, args=None, namespace=None):
306 """Overwrite from Argumentparser to handle servo rc.
307
308 Note: this also overwrites parse_args as parse_args just calls
309 parse_known_args and throws an error if there's anything inside of
310 xtra.
311
312 Args:
313 args: list of cmdline elements
314 namespace: namespace to place the results into
315
316 Returns:
317 tuple (options, xtra) the result from parsing known args
318 """
319 args, _ = _ServodRCParser.HandleNameEnvVar(args, logger=self._logger)
320 result = super(_ServodRCParser, self).parse_known_args(args=args,
321 namespace=namespace)
322 opts, xtra = result
323 opts = _ServodRCParser.PostProcessRCElements(opts, logger=self._logger)
324 return (opts, xtra)
325
326 @staticmethod
327 def ParseRC(rc_file, logger=logging):
328 """Parse servodrc configuration file.
329
330 The format of the configuration file is described above in comments to
331 DEFAULT_RC_FILE. If the file is not found or is mis-formatted, a warning is
332 printed but the program tries to continue.
333
334 Args:
335 rc_file: a string, name of the file storing the configuration
336 logger: logger instance to use
337
338 Returns:
339 a dictionary, where keys are symbolic servo names, and values are
340 dictionaries representing servo parameters read from the config file,
341 keyed by strings 'sn' (for serial number), 'port', 'board', and 'model'.
342 """
343
344 if not os.path.isfile(rc_file):
345 return {}
346 rcd = {} # Dictionary representing the rc file contents.
347 other_attributes = ['port', 'board', 'model']
348 with open(rc_file) as f:
349 for rc_line in f:
350 line = rc_line.split('#')[0].strip()
351 if not line:
352 continue
353 elts = [x.strip() for x in line.split(',')]
354 name = elts[0]
355 if not name or len(elts) < 2 or any(' ' in x for x in elts):
356 logger.warning('ignoring rc line %r', rc_line.rstrip())
357 continue
358 rcd[name] = {'sn': elts[1]}
359 # Initialize all to None
360 rcd[name].update({att: None for att in other_attributes})
361 rcd[name].update(dict(zip(other_attributes, elts[2:])))
362 # +2 comes from name & serialname
363 if len(elts) > len(other_attributes) + 2:
364 extra_info = elts[len(other_attributes) + 2:]
365 logger.warning('discarding %r for for %r', ', '.join(extra_info),
366 name)
367 return rcd
368
369
370class BaseServodRCParser(_ServodRCParser):
371 """BaseServodParser extension to also natively handle servo rc config."""
372
373 def __init__(self, **kwargs):
374 """Create a ServodRCParser that has the BaseServodParser args.
375
376 Args:
377 **kwargs: keyword arguments forwarded to _BaseServodParser
378 """
379 base_parser = BaseServodParser(add_help=False)
380 if 'parents' not in kwargs:
381 kwargs['parents'] = []
382 kwargs['parents'].append(base_parser)
383 super(BaseServodRCParser, self).__init__(**kwargs)
384
385
386class ServodClientParser(BaseServodRCParser):
387 """Parser to use for servod client cmdline tools.
388
389 This parser adds servoscratch serialname<>port conversion to allow
390 for servod client cmdline tools to address servod using a servo device's
391 serialname as well.
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800392 """
393
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800394 def __init__(self, **kwargs):
395 """Create a ServodRCParser that has the BaseServodParser args.
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800396
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800397 Args:
398 **kwargs: keyword arguments forwarded to _BaseServodParser
399 """
400 super(ServodClientParser, self).__init__(**kwargs)
401 self.add_argument('-s', '--serialname', default=None, type=str,
402 help='device serialname stored in eeprom. Ignored '
403 'if port is already defined.')
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800404
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800405 def _MapSNToPort(self, opts):
406 """Helper to map the serialname in opts to the port its running on.
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800407
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800408 Args:
409 opts: ArgumentParser Namespace after parsing.
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800410
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800411 Returns:
412 opts: reference back to passed in opts
413
414 Raises:
415 Forces a program exit if |opts.serialname| is not found in the servo
416 scratch
417 """
418 scratch = servodutil.ServoScratch()
419 try:
420 entry = scratch.FindById(opts.serialname)
421 except servodutil.ServodUtilError:
422 self.error('No servod instance running for device with serialname: %r' %
423 opts.serialname)
424 opts.port = int(entry['port'])
425 return opts
426
427 def parse_known_args(self, args=None, namespace=None):
428 """Overwrite from Argumentparser to handle servo scratch logic.
429
430 If port is not defined and serialname is defined, and serialname has
431 no scratch entry, this will raise an error & terminate the program.
432
433 If there was neither a serialname nor a port, set the port to the
434 default port.
435
436 Note: this also overwrites parse_args as parse_args just calls
437 parse_known_args and throws an error if there's anything inside of
438 xtra.
439
440 Args:
441 args: list of cmdline elements
442 namespace: namespace to place the results into
443
444 Returns:
445 tuple (opts, xtra) the result from parsing known args
446 """
447 args, _ = BaseServodParser.HandlePortEnvVar(args, logger=self._logger)
448 rslt = super(ServodClientParser, self).parse_known_args(args=args,
449 namespace=namespace)
450 opts, xtra = rslt
451 if not opts.port and opts.serialname:
452 # only overwrite the port if no port explicitly set
453 opts = self._MapSNToPort(opts)
454 if not opts.port:
455 opts.port = client.DEFAULT_PORT
456 return (opts, xtra)