blob: 6de7de22491207b5fe7b90653d32959ed00fb03a [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 Buchillon63e38602018-09-19 10:36:58 +08009import textwrap
10
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +080011import client
12import servo_logging
13import servodutil
14
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +080015if os.getuid():
16 DEFAULT_RC_FILE = '/home/%s/.servodrc' % os.getenv('USER', '')
17else:
18 DEFAULT_RC_FILE = '/home/%s/.servodrc' % os.getenv('SUDO_USER', '')
19
20
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +080021PORT_ENV_VAR = 'SERVOD_PORT'
22NAME_ENV_VAR = 'SERVOD_NAME'
23
24
25ARG_BY_USER_MARKER = 'supplied_by_user'
26
27
28def ArgMarkedAsUserSupplied(namespace, arg_name):
29 """Query whether an argument that uses StoreAndMarkAction is user supplied."""
30 marker_name = '%s_%s' % (arg_name, ARG_BY_USER_MARKER)
31 return hasattr(namespace, marker_name)
32
33# pylint: disable=protected-access
34# Need to expand the StoreAction of the parser.
35class StoreAndMarkAction(argparse._StoreAction):
36 """Helper to mark arguments whether they were supplied by the user.
37
38 If an argument is supplied by the user instead of using defaults or RC,
39 add another option with the name |arg|_supplied_by_user.
40 """
41
42 def __call__(self, parser, namespace, values, option_string=None):
43 """Extend default __call__ implementation."""
44 # This sets the |values| to |self.dest|.
45 super(StoreAndMarkAction, self).__call__(parser=parser, namespace=namespace,
46 values=values,
47 option_string=option_string)
48 marker_name = '%s_%s' % (self.dest, ARG_BY_USER_MARKER)
49 setattr(namespace, marker_name, True)
50
51
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +080052class ServodParserHelpFormatter(argparse.RawDescriptionHelpFormatter,
53 argparse.ArgumentDefaultsHelpFormatter):
54 """Servod help formatter.
55
56 Combines ability for raw description printing (needed to have control over
57 how to print examples) and default argument printing, printing the default
58 which each argument.
59 """
60 pass
61
62
63class ServodParserError(Exception):
64 """Error class for Servod parsing errors."""
65 pass
66
67
68class _BaseServodParser(argparse.ArgumentParser):
69 """Extension to ArgumentParser that allows for examples in the description.
70
71 _BaseServodParser allows for a list of example tuples, where
72 element[0]: is the cmdline invocation
73 element[1]: is a comment to explain what the invocation does.
74
75 For example (loosely based on servod.)
76 ('-b board', 'Start servod with the configuation for board |board|')
77 would print the following help message:
78 ...
79
80 Examples:
81 > servod -b board
82 Start servod with the configuration for board |board|
83
84 Optional Arguments...
85
86 see servod, or dut_control for more examples.
87 """
88
Ruben Rodriguez Buchillon0902f092018-09-19 15:03:17 +080089 def __init__(self, description='', examples=None, **kwargs):
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +080090 """Initialize _BaseServodParser by setting description and formatter.
91
92 Args:
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +080093 description: description of the program
94 examples: list of tuples where the first element is the cmdline example,
95 and the second element is a comment explaining the example.
96 %(prog)s will be prepended to each example if it does not
97 start with %(prog)s.
Ruben Rodriguez Buchillon0902f092018-09-19 15:03:17 +080098 **kwargs: keyword arguments forwarded to ArgumentParser
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +080099 """
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800100 # Initialize logging up here first to ensure log messages from parsing
101 # can go through.
102 loglevel, fmt = servo_logging.LOGLEVEL_MAP[servo_logging.DEFAULT_LOGLEVEL]
103 logging.basicConfig(loglevel=loglevel, format=fmt)
104 self._logger = logging.getLogger(type(self).__name__)
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800105 # Generate description.
106 description_lines = textwrap.wrap(description)
107 # Setting it into the kwargs here ensures that we overwrite an potentially
108 # passed in and undesired formatter class.
109 kwargs['formatter_class'] = ServodParserHelpFormatter
110 if examples:
111 # Extra newline to separate description from examples.
112 description_lines.append('\n')
113 description_lines.append('Examples:')
114 for example, comment in examples:
115 if not example.startswith('%(prog)s'):
116 example = '%(prog)s ' + example
117 example_lines = [' > ' + example]
118 example_lines.extend(textwrap.wrap(comment))
119 description_lines.append('\n\t'.join(example_lines))
120 description = '\n'.join(description_lines)
121 kwargs['description'] = description
Ruben Rodriguez Buchillon0902f092018-09-19 15:03:17 +0800122 super(_BaseServodParser, self).__init__(**kwargs)
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800123
124
125class BaseServodParser(_BaseServodParser):
126 """BaseServodParser handling common arguments in the servod cmdline tools."""
127
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800128 def __init__(self, add_port=True, **kwargs):
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800129 """Initialize by adding common arguments.
130
131 Adds:
132 - host/port arguments to find/initialize a servod instance
133 - debug argument to toggle debug message printing
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800134
135 Args:
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800136 add_port: bool, whether to add --port to the parser. A caller might want
137 to add port themselves either to rename it (servod-port),
138 or to create mutual exclusion with serialname and name (clients)
Ruben Rodriguez Buchillon0902f092018-09-19 15:03:17 +0800139 **kwargs: keyword arguments forwarded to _BaseServodParser
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800140 """
Ruben Rodriguez Buchillon0902f092018-09-19 15:03:17 +0800141 super(BaseServodParser, self).__init__(**kwargs)
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800142 self.add_argument('-d', '--debug', action='store_true', default=False,
143 help='enable debug messages')
144 self.add_argument('--host', default='localhost', type=str,
145 help='hostname of the servod server.')
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800146 if add_port:
147 BaseServodParser.AddRCEnabledPortArg(self)
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800148
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800149 @staticmethod
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800150 def AddRCEnabledPortArg(parser, port_flags=['-p', '--port']):
151 """Add the port to the argparser.
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800152
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800153 Set the default to environment variable ENV_PORT_NAME if defined
154
155 Note: while this helper does allow for arbitrary flags for the port
156 variable, the destination is still set to 'port'. It's on the caller to
157 ensure that there is no conflict.
Ruben Rodriguez Buchillon0902f092018-09-19 15:03:17 +0800158
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800159 Args:
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800160 parser: parser or group to add argument to
161 port_flags: optional, list, if the flags for the port should be different
162 than the default ones.
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800163 """
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800164 # pylint: disable=dangerous-default-value
165 # Having the default flags here simplifies the code logic.
166 default = os.environ.get(PORT_ENV_VAR, client.DEFAULT_PORT)
167 parser.add_argument(*port_flags, default=default, type=int, dest='port',
168 action=StoreAndMarkAction,
169 help='port of the servod server. Can also be supplied '
170 'through environment variable ' + PORT_ENV_VAR)
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800171
172
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800173class ServodRCParser(_BaseServodParser):
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800174 """Base class to build Servod parsers to natively handle servorc.
175
176 This class overwrites parse_args & parse_known_args to:
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800177 - handle NAME_ENV_VAR environment variable
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800178 - parse & substitute in the servorc file on matches
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800179 """
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800180
181 def __init__(self, **kwargs):
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800182 super(ServodRCParser, self).__init__(**kwargs)
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800183 self.add_argument('--rcfile', type=str, default=DEFAULT_RC_FILE,
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800184 help='servo description file for multi-servo operation.')
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800185 # name and serialname are both ways to ID a servo device
186 self._id_group = self.add_mutually_exclusive_group()
187 self._id_group.add_argument('-s', '--serialname', default=None, type=str,
188 help='device serialname stored in eeprom.')
189 ServodRCParser.AddRCEnabledNameArg(self._id_group)
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800190
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800191 @staticmethod
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800192 def AddRCEnabledNameArg(parser, name_flags=['-n', '--name']):
193 """Add the name to the argparser.
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800194
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800195 Set the default to environment variable ENV_VAR_NAME if defined
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800196
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800197 Note: while this helper does allow for arbitrary flags for the name
198 variable, the destination is still set to 'name'. It's on the caller to
199 ensure that there is no conflict.
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800200
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800201 Args:
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800202 parser: parser or group to add argument to
203 name_flags: optional, list, if the flags for the name should be different
204 than the default ones.
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800205 """
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800206 # pylint: disable=dangerous-default-value
207 # Having the default flags here simplifies the code logic.
208 default = os.environ.get(NAME_ENV_VAR, '')
209 parser.add_argument(*name_flags, default=default, type=str, dest='name',
210 help='symbolic name of the servo board, '
211 'used as a config shortcut, could also be supplied '
212 'through environment variable ' + NAME_ENV_VAR)
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800213
214 @staticmethod
215 def PostProcessRCElements(options, rcpath=None, logger=logging):
216 """Handle 'name' in |options| by substituting it with the intended config.
217
218 This replaces the name option in the options with the intended serialname
219 for that name if one can be found. If a board file is also specified in the
220 rc file it appends that to the options too, which can be ignored if not
221 needed.
222
223 Note: this function changes the content of options.
224
225 Args:
226 options: argparse Namespace of options to process.
227 rcpath: optional rcfile path if it's not stored under options.rcfile
228 logger: logger instance to use
229
230 Returns:
231 Reference back to the same options passed in.
232
233 Raises:
234 ServodParserError: if -n/--name and -s/--serialname both defined
235 ServodParserError: if name in options doesn't show up in servodrc
236 """
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800237 if not rcpath:
238 rcpath = options.rcfile
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800239 rcd = ServodRCParser.ParseRC(rcpath, logger=logger)
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800240 rc = None
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800241 if not options.serialname and options.name:
242 # |name| can be set through the commandline or through an environment
243 # variable. If it's set through the commandline, serialname cannot have
244 # been set. However, if serialname is set and name is also set (through
245 # the environment variable) name gets ignored.
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800246 if options.name not in rcd:
247 raise ServodParserError('Name %r not in rc at %r' % (options.name,
248 rcpath))
249 rc = rcd[options.name]
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800250 # For an rc to exist, 'sn' has to be a part of it
251 setattr(options, 'serialname', rc['sn'])
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800252 elif options.serialname:
253 # srcs meaning serialname runtime configurations (rcs).
254 srcs = [(name, rc) for name, rc in rcd.iteritems() if
255 rc['sn'] == options.serialname]
256 if srcs:
257 logger.info('Found servodrc entry %r for serialname %r. Using it.',
258 srcs[0][0], options.serialname)
259 rc = srcs[0][1]
260 if rc:
261 for elem in ['board', 'model']:
262 # Unlike serialname explicit overwrites of board and model in the
263 # cmdline are fine as the name flag is still useful to refer to a
264 # serialname.
265 if elem in rc and hasattr(options, elem):
266 if not getattr(options, elem):
267 logger.info('Setting %r to %r in the options as indicated by '
268 'servodrc file.', elem, rc[elem])
269 setattr(options, elem, rc[elem])
270 else:
271 if getattr(options, elem) != rc[elem]:
272 logger.warning('Ignoring rc configured %r name %r for servo %r. '
273 'Option already defined on the command line as %r',
274 elem, rc[elem], rc['sn'], getattr(options, elem))
275 return options
276
277 def parse_known_args(self, args=None, namespace=None):
278 """Overwrite from Argumentparser to handle servo rc.
279
280 Note: this also overwrites parse_args as parse_args just calls
281 parse_known_args and throws an error if there's anything inside of
282 xtra.
283
284 Args:
285 args: list of cmdline elements
286 namespace: namespace to place the results into
287
288 Returns:
289 tuple (options, xtra) the result from parsing known args
290 """
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800291 opts, xtra = _BaseServodParser.parse_known_args(self, args=args,
292 namespace=namespace)
293 opts = ServodRCParser.PostProcessRCElements(opts, logger=self._logger)
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800294 return (opts, xtra)
295
296 @staticmethod
297 def ParseRC(rc_file, logger=logging):
298 """Parse servodrc configuration file.
299
300 The format of the configuration file is described above in comments to
301 DEFAULT_RC_FILE. If the file is not found or is mis-formatted, a warning is
302 printed but the program tries to continue.
303
304 Args:
305 rc_file: a string, name of the file storing the configuration
306 logger: logger instance to use
307
308 Returns:
309 a dictionary, where keys are symbolic servo names, and values are
310 dictionaries representing servo parameters read from the config file,
311 keyed by strings 'sn' (for serial number), 'port', 'board', and 'model'.
312 """
313
314 if not os.path.isfile(rc_file):
315 return {}
316 rcd = {} # Dictionary representing the rc file contents.
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800317 attributes = ['name', 'sn', 'port', 'board', 'model']
318 # These attributes have to be defined for a line to be valid.
319 required_attributes = ['name', 'sn']
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800320 with open(rc_file) as f:
321 for rc_line in f:
322 line = rc_line.split('#')[0].strip()
323 if not line:
324 continue
325 elts = [x.strip() for x in line.split(',')]
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800326 if len(elts) < len(required_attributes):
327 logger.warning('ignoring rc line %r. Not all required '
328 'attributes defined %r.', rc_line.rstrip(),
329 required_attributes)
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800330 continue
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800331 # Initialize all to None that are not in elts
332 line_content = dict(zip(attributes, elts + [None] * len(attributes)))
333 # All required attributes are defined. Store the entry.
334 name = line_content.pop('name')
335 if len(elts) > len(attributes):
336 extra_info = elts[len(attributes):]
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800337 logger.warning('discarding %r for for %r', ', '.join(extra_info),
338 name)
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800339 rcd[name] = line_content
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800340 return rcd
341
342
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800343class ServodClientParser(ServodRCParser):
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800344 """Parser to use for servod client cmdline tools.
345
346 This parser adds servoscratch serialname<>port conversion to allow
347 for servod client cmdline tools to address servod using a servo device's
348 serialname as well.
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800349 """
350
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800351 def __init__(self, scratch=None, **kwargs):
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800352 """Create a ServodRCParser that has the BaseServodParser args.
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800353
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800354 (for testing) pass a scratch directory instead of the global default.
355
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800356 Args:
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800357 scratch: scratch directory to use
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800358 **kwargs: keyword arguments forwarded to _BaseServodParser
359 """
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800360 # BaseServodParser is used here to get the common arguments. Later,
361 # the ServodClientParser adds port itself, because from a client perspective
362 # there is mutual exclusion between --port/--serialname/--name as they serve
363 # one purpose: to identify an instance.
364 self._scratchdir = scratch
365 base_parser = BaseServodParser(add_port=False, add_help=False)
366 if 'parents' not in kwargs:
367 kwargs['parents'] = []
368 kwargs['parents'].append(base_parser)
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800369 super(ServodClientParser, self).__init__(**kwargs)
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800370 # Add --port to the |_id_group| to ensure exclusion with name and
371 # serialname.
372 BaseServodParser.AddRCEnabledPortArg(self._id_group)
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800373
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800374 def _MapSNToPort(self, opts):
375 """Helper to map the serialname in opts to the port its running on.
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800376
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800377 Args:
378 opts: ArgumentParser Namespace after parsing.
Ruben Rodriguez Buchillon63e38602018-09-19 10:36:58 +0800379
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800380 Returns:
381 opts: reference back to passed in opts
382
383 Raises:
384 Forces a program exit if |opts.serialname| is not found in the servo
385 scratch
386 """
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800387 # Passing None here uses the default production logic while passing any
388 # other directory can be used for testing. No need to check whether
389 # |self._scratchdir| is None.
390 scratch = servodutil.ServoScratch(self._scratchdir)
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800391 try:
392 entry = scratch.FindById(opts.serialname)
393 except servodutil.ServodUtilError:
394 self.error('No servod instance running for device with serialname: %r' %
395 opts.serialname)
396 opts.port = int(entry['port'])
397 return opts
398
399 def parse_known_args(self, args=None, namespace=None):
400 """Overwrite from Argumentparser to handle servo scratch logic.
401
402 If port is not defined and serialname is defined, and serialname has
403 no scratch entry, this will raise an error & terminate the program.
404
405 If there was neither a serialname nor a port, set the port to the
406 default port.
407
408 Note: this also overwrites parse_args as parse_args just calls
409 parse_known_args and throws an error if there's anything inside of
410 xtra.
411
412 Args:
413 args: list of cmdline elements
414 namespace: namespace to place the results into
415
416 Returns:
417 tuple (opts, xtra) the result from parsing known args
418 """
Ruben Rodriguez Buchillon7be98d72018-09-19 18:03:36 +0800419 opts, xtra = _BaseServodParser.parse_known_args(self, args=args,
420 namespace=namespace)
421 opts = ServodRCParser.PostProcessRCElements(opts, logger=self._logger)
422 if opts.serialname:
423 # If serialname is set, this means that either serialname or name was used
424 # to find it, and therefore port cannot have been set by the user due to
425 # mutual exclusion.
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800426 opts = self._MapSNToPort(opts)
Ruben Rodriguez Buchillon90662392018-09-19 16:28:38 +0800427 return (opts, xtra)