def _get_plat_info(self, trace_name=None): trace_dir = self.traces_dir if trace_name: trace_dir = os.path.join(trace_dir, trace_name) path = os.path.join(trace_dir, 'plat_info.yml') return PlatformInfo.from_yaml_map(path)
def __init__(self, trace_path, plat_info=None, events=None, normalize_time=True, trace_format='FTrace', plots_dir=None, plots_prefix=''): super().__init__() logger = self.get_logger() if plat_info is None: plat_info = PlatformInfo() # The platform information used to run the experiments self.plat_info = plat_info self.normalize_time = normalize_time proxy_cls = type(self.analysis) self.events = self._process_events(events, proxy_cls) # Path to the trace file self.trace_path = trace_path # By default, use the trace dir to save plots self.plots_dir = plots_dir if plots_dir else os.path.dirname( trace_path) self.plots_prefix = plots_prefix self._parse_trace(self.trace_path, trace_format, normalize_time)
def from_one_conf(cls, path): """ Create a :class:`Target` from a single YAML configuration file. This file will be used to provide a :class:`TargetConf` and :class:`lisa.platforms.platinfo.PlatformInfo` instances. """ conf = TargetConf.from_yaml_map(path) try: plat_info = PlatformInfo.from_yaml_map(path) except Exception as e: cls.get_logger().warn('No platform information could be found: {}'.format(e)) plat_info = None return cls.from_conf(conf=conf, plat_info=plat_info)
def from_one_conf(cls, path): """ Create a :class:`Target` from a single YAML configuration file. This file will be used to provide a :class:`TargetConf` and :class:`lisa.platforms.platinfo.PlatformInfo` instances. .. note:: Only load trusted YAML files as it can lead to abritrary code execution. """ conf = TargetConf.from_yaml_map(path) try: plat_info = PlatformInfo.from_yaml_map(path) except Exception as e: # pylint: disable=broad-except cls.get_logger().warning( f'No platform information could be found: {e}') plat_info = None return cls.from_conf(conf=conf, plat_info=plat_info)
def _init_plat_info(self, plat_info=None, name=None, **kwargs): if plat_info is None: plat_info = PlatformInfo() else: # Make a copy of the PlatformInfo so we don't modify the original # one we were passed when adding the target source to it plat_info = copy.copy(plat_info) self.logger.info( f'User-defined platform information:\n{plat_info}') # Take the board name from the target configuration so it becomes # available for later inspection. That board name is mostly free form # and no specific value should be expected for a given kind of board # (i.e. a Juno board might be named "foo-bar-juno-on-my-desk") if name: plat_info.add_src('target-conf', dict(name=name)) rta_calib_res_dir = ArtifactPath.join(self._res_dir, 'rta_calib') os.makedirs(rta_calib_res_dir, exist_ok=True) plat_info.add_target_src(self, rta_calib_res_dir, **kwargs) self.plat_info = plat_info
def __init__(self, res_dir, shell_output): plat_info = PlatformInfo() super().__init__(res_dir, plat_info) self.shell_output = shell_output
def _calibrate(cls, target, res_dir): res_dir = res_dir if res_dir else target.get_res_dir("rta_calib", symlink=False) pload_regexp = re.compile(r'pLoad = ([0-9]+)ns') pload = {} logger = cls.get_logger() # Create calibration task if target.is_rooted: max_rtprio = int(target.execute('ulimit -Hr').splitlines()[0]) logger.debug(f'Max RT prio: {max_rtprio}') priority = max_rtprio + 1 if max_rtprio <= 10 else 10 sched_policy = 'FIFO' else: logger.warning( 'Will use default scheduler class instead of RT since the target is not rooted' ) priority = None sched_policy = None for cpu in target.list_online_cpus(): logger.info(f'CPU{cpu} calibration...') # RT-app will run a calibration for us, so we just need to # run a dummy task and read the output calib_task = Periodic( duty_cycle_pct=100, duration_s=0.001, period_ms=1, priority=priority, sched_policy=sched_policy, ) rta = cls.by_profile(target, name=f"rta_calib_cpu{cpu}", profile={'task1': calib_task}, calibration=f"CPU{cpu}", res_dir=res_dir) with rta, target.freeze_userspace(): # Disable CPU capacities update, since that leads to infinite # recursion rta.run(as_root=target.is_rooted, update_cpu_capacities=False) for line in rta.output.split('\n'): pload_match = re.search(pload_regexp, line) if pload_match is None: continue pload[cpu] = int(pload_match.group(1)) logger.debug(f'>>> CPU{cpu}: {pload[cpu]}') # Avoid circular import issue from lisa.platforms.platinfo import PlatformInfo snippet_plat_info = PlatformInfo({ 'rtapp': { 'calib': pload, }, }) logger.info( f'Platform info rt-app calibration configuration:\n{snippet_plat_info.to_yaml_map_str()}' ) plat_info = target.plat_info # Sanity check calibration values for asymmetric systems if we have # access to capacities try: orig_capacities = plat_info['cpu-capacities']['orig'] except KeyError: return pload capa_ploads = { capacity: {cpu: pload[cpu] for cpu in cpus} for capacity, cpus in group_by_value(orig_capacities).items() } # Find the min pload per capacity level, i.e. the fastest detected CPU. # It is more likely to represent the right pload, as it has suffered # from less IRQ slowdown or similar disturbances that might be random. capa_pload = { capacity: min(ploads.values()) for capacity, ploads in capa_ploads.items() } # Sort by capacity capa_pload_list = sorted(capa_pload.items()) # unzip the list of tuples _, pload_list = zip(*capa_pload_list) # If sorting according to capa was not equivalent to reverse sorting # according to pload (small pload=fast cpu) if list(pload_list) != sorted(pload_list, reverse=True): raise CalibrationError( 'Calibration values reports big cores less capable than LITTLE cores' ) # Check that the CPU capacities seen by rt-app are similar to the one # the kernel uses orig_capacities = plat_info['cpu-capacities']['orig'] true_capacities = cls.get_cpu_capacities_from_calibrations( orig_capacities, pload) cls.warn_capacities_mismatch(orig_capacities, true_capacities) return pload
def _calibrate(cls, target, res_dir): res_dir = res_dir if res_dir else target .get_res_dir( "rta_calib", symlink=False ) pload_regexp = re.compile(r'pLoad = ([0-9]+)ns') pload = {} logger = cls.get_logger() # Create calibration task if target.is_rooted: max_rtprio = int(target.execute('ulimit -Hr').split('\r')[0]) logger.debug('Max RT prio: {}'.format(max_rtprio)) priority = max_rtprio if max_rtprio <= 10 else 10 sched_policy = 'FIFO' else: logger.warning('Will use default scheduler class instead of RT since the target is not rooted') priority = None sched_policy = None for cpu in target.list_online_cpus(): logger.info('CPU{} calibration...'.format(cpu)) # RT-app will run a calibration for us, so we just need to # run a dummy task and read the output calib_task = Periodic( duty_cycle_pct=100, duration_s=0.001, period_ms=1, priority=priority, sched_policy=sched_policy, ) rta = cls.by_profile(target, name="rta_calib_cpu{}".format(cpu), profile={'task1': calib_task}, calibration="CPU{}".format(cpu), res_dir=res_dir) with rta, target.freeze_userspace(): # Disable CPU capacities update, since that leads to infinite # recursion rta.run(as_root=target.is_rooted, update_cpu_capacities=False) for line in rta.output.split('\n'): pload_match = re.search(pload_regexp, line) if pload_match is None: continue pload[cpu] = int(pload_match.group(1)) logger.debug('>>> CPU{}: {}'.format(cpu, pload[cpu])) # Avoid circular import issue from lisa.platforms.platinfo import PlatformInfo snippet_plat_info = PlatformInfo({ 'rtapp': { 'calib': pload, }, }) logger.info('Platform info rt-app calibration configuration:\n{}'.format( snippet_plat_info.to_yaml_map_str() )) plat_info = target.plat_info # Sanity check calibration values for asymmetric systems if we have # access to capacities try: cpu_capacities = plat_info['cpu-capacities'] except KeyError: return pload capa_ploads = { capacity: {cpu: pload[cpu] for cpu, capa in cpu_caps} for capacity, cpu_caps in groupby(cpu_capacities.items(), itemgetter(1)) } # Find the min pload per capacity level, i.e. the fastest detected CPU. # It is more likely to represent the right pload, as it has suffered # from less IRQ slowdown or similar disturbances that might be random. capa_pload = { capacity: min(ploads.values()) for capacity, ploads in capa_ploads.items() } # Sort by capacity capa_pload_list = sorted(capa_pload.items()) # unzip the list of tuples _, pload_list = zip(*capa_pload_list) # If sorting according to capa was not equivalent to reverse sorting # according to pload (small pload=fast cpu) if list(pload_list) != sorted(pload_list, reverse=True): raise CalibrationError('Calibration values reports big cores less capable than LITTLE cores') # Check that the CPU capacities seen by rt-app are similar to the one # the kernel uses true_capacities = cls.get_cpu_capacities_from_calibrations(pload) capa_factors_pct = { cpu: true_capacities[cpu] / cpu_capacities[cpu] * 100 for cpu in cpu_capacities.keys() } dispersion_pct = max(abs(100 - factor) for factor in capa_factors_pct.values()) logger.info('CPU capacities according to rt-app workload: {}'.format(true_capacities)) if dispersion_pct > 2: logger.warning('The calibration values are not inversely proportional to the CPU capacities, the duty cycles will be up to {:.2f}% off on some CPUs: {}'.format(dispersion_pct, capa_factors_pct)) if dispersion_pct > 20: logger.warning('The calibration values are not inversely proportional to the CPU capacities. Either rt-app calibration failed, or the rt-app busy loops has a very different instruction mix compared to the workload used to establish the CPU capacities: {}'.format(capa_factors_pct)) # Map of CPUs X to list of CPUs Ys that are faster than it although CPUs # of Ys have a smaller capacity than X if len(capa_ploads) > 1: faster_than_map = { cpu1: sorted( cpu2 for cpu2, pload2 in ploads2.items() # CPU2 faster than CPU1 if pload2 < pload1 ) for (capa1, ploads1), (capa2, ploads2) in itertools.permutations(capa_ploads.items()) for cpu1, pload1 in ploads1.items() # Only look at permutations in which CPUs of ploads1 are supposed # to be faster than the one in ploads2 if capa1 > capa2 } else: faster_than_map = {} # Remove empty lists faster_than_map = { cpu: faster_cpus for cpu, faster_cpus in faster_than_map.items() if faster_cpus } if faster_than_map: raise CalibrationError('Some CPUs of higher capacities are slower than other CPUs of smaller capacities: {}'.format(faster_than_map)) return pload
def main(argv=None): if argv is None: argv = sys.argv[1:] plots_map = get_plots_map() analysis_nice_name_map = { get_analysis_nice_name(name): name for name in plots_map.keys() } parser = argparse.ArgumentParser( description=""" CLI for LISA analysis plots and reports from traces. Available plots: {} """.format(get_analysis_listing(plots_map)), formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( 'trace', help='trace-cmd trace.dat, or systrace file', ) parser.add_argument( '--normalize-time', action='store_true', help= 'Normalize the time in the plot, i.e. start at 0 instead of uptime timestamp', ) parser.add_argument( '--plot', nargs=2, action='append', default=[], metavar=('PLOT', 'OUTPUT_PATH'), help= 'Create the given plot. If OUTPUT_PATH is "interactive", an interactive window will be used', ) parser.add_argument( '--plot-analysis', nargs=3, action='append', default=[], metavar=('ANALYSIS', 'OUTPUT_FOLDER_PATH', 'FORMAT'), help='Create all the plots of the given analysis', ) parser.add_argument( '--plot-all', nargs=2, metavar=('OUTPUT_FOLDER_PATH', 'FORMAT'), help='Create all the plots in the given folder', ) parser.add_argument( '--best-effort', action='store_true', help= 'Try to generate as many of the requested plots as possible without early termination.', ) parser.add_argument( '--window', nargs=2, type=float, metavar=('BEGIN', 'END'), help='Only plot data between BEGIN and END times', ) parser.add_argument( '-X', '--option', nargs=2, action='append', default=[], metavar=('OPTION_NAME', 'VALUE'), help= 'Pass extra parameters to plot methods, e.g. "-X cpu 1". Mismatching names are ignored.', ) parser.add_argument( '--matplotlib-backend', default='GTK3Agg', help='matplotlib backend to use for interactive window', ) parser.add_argument( '--plat-info', help='Platform information, necessary for some plots', ) parser.add_argument( '--xkcd', action='store_true', help='Graphs will look like XKCD plots', ) args = parser.parse_args(argv) flat_plot_map = { plot_name: meth for analysis_name, plot_list in plots_map.items() for plot_name, meth in plot_list.items() } if args.plat_info: plat_info = PlatformInfo.from_yaml_map(args.plat_info) else: plat_info = None if args.plot_all: folder, fmt = args.plot_all plot_analysis_spec_list = [(get_analysis_nice_name(analysis_name), folder, fmt) for analysis_name in plots_map.keys()] else: plot_analysis_spec_list = [] plot_analysis_spec_list.extend(args.plot_analysis) plot_spec_list = [(plot_name, os.path.join(folder, '{}.{}'.format(plot_name, fmt))) for analysis_name, folder, fmt in plot_analysis_spec_list for plot_name, meth in plots_map[ analysis_nice_name_map[analysis_name]].items()] plot_spec_list.extend(args.plot) # Build minimal event list to speed up trace loading time plot_methods = set() for plot_name, file_path in plot_spec_list: try: f = flat_plot_map[plot_name] except KeyError: error('Unknown plot "{}", see --help'.format(plot_name)) plot_methods.add(f) # If best effort is used, we don't want to trigger exceptions ahead of # time. Let it fail for individual plot methods instead, so the trace can # be used for the other events if args.best_effort: events = None else: events = set() for f in plot_methods: with contextlib.suppress(AttributeError): events.update(f.used_events.get_all_events()) events = sorted(events) print('Parsing trace events: {}'.format(', '.join(events))) trace = Trace(args.trace, plat_info=plat_info, events=events, normalize_time=args.normalize_time, write_swap=True) if args.window: window = args.window def clip(l, x, r): if x < l: return l elif x > r: return r else: return x window = ( clip(trace.window[0], window[0], trace.window[1]), clip(trace.window[0], window[1], trace.window[1]), ) # There is no overlap between trace and user window, reset to trace # window if window[0] == window[1]: print( 'Window {} does not overlap with trace time range, maybe you forgot --normalize-time ?' .format(tuple(args.window))) window = trace.window trace = trace.get_view(window) for plot_name, file_path in sorted(plot_spec_list): interactive = file_path == 'interactive' f = flat_plot_map[plot_name] if interactive: matplotlib.use(args.matplotlib_backend) file_path = None else: dirname = os.path.dirname(file_path) if dirname: os.makedirs(dirname, exist_ok=True) kwargs = make_plot_kwargs(f, file_path, interactive=interactive, extra_options=args.option) xkcd_cm = plt.xkcd() if args.xkcd else nullcontext() with handle_plot_excep(exit_on_error=not args.best_effort): with xkcd_cm: TraceAnalysisBase.call_on_trace(f, trace, kwargs) if interactive: plt.show()
def from_custom_cli(cls, argv=None, params=None) -> 'Target': """ Create a Target from command line arguments. :param argv: The list of arguments. ``sys.argv[1:]`` will be used if this is ``None``. :type argv: list(str) :param params: Dictionary of custom parameters to add to the parser. It is in the form of ``{param_name: {dict of ArgumentParser.add_argument() options}}``. :type params: dict(str, dict) :return: A tuple ``(args, target)`` .. note:: This method should not be relied upon to implement long-term scripts, it's more designed for quick scripting. """ parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description=textwrap.dedent( """ Connect to a target using the provided configuration in order to run a test. EXAMPLES --conf can point to a YAML target configuration file with all the necessary connection information: $ {script} --conf my_target.yml Alternatively, --kind must be set along the relevant credentials: $ {script} --kind linux --host 192.0.2.1 --username root --password root In both cases, --conf can also contain a PlatformInfo YAML description. """.format( script=os.path.basename(sys.argv[0]) ))) parser.add_argument("--conf", '-c', help="Path to a TargetConf and PlatformInfo yaml file. Other options will override what is specified in the file." ) parser.add_argument("--kind", "-k", choices=["android", "linux", "host"], help="The kind of target to connect to.") device_group = parser.add_mutually_exclusive_group() device_group.add_argument("--device", "-d", help="The ADB ID of the target. Superseeds --host. Only applies to Android kind.") device_group.add_argument("--host", "-n", help="The hostname/IP of the target.") parser.add_argument("--username", "-u", help="Login username. Only applies to Linux kind.") parser.add_argument("--password", "-p", help="Login password. Only applies to Linux kind.") parser.add_argument("--log-level", default='info', choices=('warning', 'info', 'debug'), help="Verbosity level of the logs.") parser.add_argument("--res-dir", "-o", help="Result directory of the created Target. If no directory is specified, a default location under $LISA_HOME will be used.") params = params or {} for param, settings in params.items(): parser.add_argument('--{}'.format(param), **settings) custom_params = {k.replace('-', '_') for k in params.keys()} # Options that are not a key in TargetConf must be listed here not_target_conf_opt = { 'platform_info', 'log_level', 'res_dir', 'conf', } not_target_conf_opt.update(custom_params) args = parser.parse_args(argv) setup_logging(level=args.log_level.upper()) target_conf = TargetConf() platform_info = None if args.conf: # Tentatively load a PlatformInfo from the conf file with contextlib.suppress(KeyError): platform_info = PlatformInfo.from_yaml_map(args.conf) # Load the TargetConf from the file, and update it with command # line arguments try: conf = TargetConf.from_yaml_map(args.conf) except KeyError: pass else: target_conf.add_src(args.conf, conf) target_conf.add_src('command-line', { k: v for k, v in vars(args).items() if v is not None and k not in not_target_conf_opt }) # Some sanity check to get better error messages if not target_conf: parser.error('--conf with target configuration or any of the connection options is required') if args.kind == 'android': if ('host' not in target_conf) and ('device' not in target_conf): parser.error('--host or --device must be specified') if args.kind == 'linux': for required in ['host', 'username', 'password']: if required not in target_conf: parser.error('--{} must be specified'.format(required)) custom_args = { param: value for param, value in vars(args).items() if param in custom_params } custom_args = argparse.Namespace(**custom_args) return custom_args, cls.from_conf(conf=target_conf, plat_info=platform_info, res_dir=args.res_dir)
def __init__(self, kind, name='<noname>', tools=[], res_dir=None, plat_info=None, workdir=None, device=None, host=None, port=None, username='******', password=None, keyfile=None, devlib_platform=None, devlib_excluded_modules=[], wait_boot=True, wait_boot_timeout=10, ): super().__init__() logger = self.get_logger() self.name = name res_dir = res_dir if res_dir else self._get_res_dir( root=os.path.join(LISA_HOME, RESULT_DIR), relative='', name='{}-{}'.format(self.__class__.__qualname__, self.name), append_time=True, symlink=True ) self._res_dir = res_dir os.makedirs(self._res_dir, exist_ok=True) if os.listdir(self._res_dir): raise ValueError('res_dir must be empty: {}'.format(self._res_dir)) if plat_info is None: plat_info = PlatformInfo() else: # Make a copy of the PlatformInfo so we don't modify the original # one we were passed when adding the target source to it plat_info = copy.copy(plat_info) logger.info('User-defined platform information:\n%s', plat_info) self.plat_info = plat_info # Take the board name from the target configuration so it becomes # available for later inspection. That board name is mostly free form # and no specific value should be expected for a given kind of board # (i.e. a Juno board might be named "foo-bar-juno-on-my-desk") if name: self.plat_info.add_src('target-conf', dict(name=name)) self._installed_tools = set() self.target = self._init_target( kind=kind, name=name, workdir=workdir, device=device, host=host, port=port, username=username, password=password, keyfile=keyfile, devlib_platform=devlib_platform, devlib_excluded_modules=devlib_excluded_modules, wait_boot=wait_boot, wait_boot_timeout=wait_boot_timeout, ) # Initialize binary tools to deploy if tools: logger.info('Tools to install: %s', tools) self.install_tools(tools) # Autodetect information from the target, after the Target is # initialized. Expensive computations are deferred so they will only be # computed when actually needed. rta_calib_res_dir = os.path.join(self._res_dir, 'rta_calib') os.makedirs(rta_calib_res_dir) self.plat_info.add_target_src(self, rta_calib_res_dir, fallback=True) logger.info('Effective platform information:\n{}'.format(self.plat_info))
from lisa.target import Target, TargetConf from lisa.platforms.platinfo import PlatformInfo ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets') HOST_TARGET_CONF = TargetConf({ 'kind': 'host', # Don't load cpufreq, it usually won't work with CI targets 'devlib': { 'excluded-modules': ['cpufreq', 'hwmon'], }, }) HOST_PLAT_INFO = PlatformInfo({ # With no cpufreq, we won't be able to do calibration. Provide dummy. 'rtapp': { 'calib': {c: 100 for c in list(range(64))}, }, }) def create_local_target(): """ :returns: A localhost :class:`lisa.target.Target` instance """ return Target.from_conf(conf=HOST_TARGET_CONF, plat_info=HOST_PLAT_INFO) class StorageTestCase(TestCase): """ A base class for tests that also provides a directory """
def __init__( self, kind, name='<noname>', tools=[], res_dir=None, plat_info=None, lazy_platinfo=False, workdir=None, device=None, host=None, port=None, username=None, password=None, keyfile=None, strict_host_check=None, devlib_platform=None, devlib_excluded_modules=[], wait_boot=True, wait_boot_timeout=10, ): # pylint: disable=dangerous-default-value super().__init__() logger = self.get_logger() self.name = name res_dir = res_dir if res_dir else self._get_res_dir( root=os.path.join(LISA_HOME, RESULT_DIR), relative='', name=f'{self.__class__.__qualname__}-{self.name}', append_time=True, symlink=True) self._res_dir = res_dir os.makedirs(self._res_dir, exist_ok=True) if os.listdir(self._res_dir): raise ValueError(f'res_dir must be empty: {self._res_dir}') if plat_info is None: plat_info = PlatformInfo() else: # Make a copy of the PlatformInfo so we don't modify the original # one we were passed when adding the target source to it plat_info = copy.copy(plat_info) logger.info(f'User-defined platform information:\n{plat_info}') self.plat_info = plat_info # Take the board name from the target configuration so it becomes # available for later inspection. That board name is mostly free form # and no specific value should be expected for a given kind of board # (i.e. a Juno board might be named "foo-bar-juno-on-my-desk") if name: self.plat_info.add_src('target-conf', dict(name=name)) self._installed_tools = set() self.target = self._init_target( kind=kind, name=name, workdir=workdir, device=device, host=host, port=port, username=username, password=password, keyfile=keyfile, strict_host_check=strict_host_check, devlib_platform=devlib_platform, wait_boot=wait_boot, wait_boot_timeout=wait_boot_timeout, ) devlib_excluded_modules = set(devlib_excluded_modules) # Sorry, can't let you do that. Messing with cgroups in a systemd # system is pretty bad idea. if self._uses_systemd: logger.warning( 'Will not load cgroups devlib module: target is using systemd, which already uses cgroups' ) devlib_excluded_modules.add('cgroups') self._devlib_loadable_modules = _DEVLIB_AVAILABLE_MODULES - devlib_excluded_modules # Initialize binary tools to deploy if tools: logger.info(f'Tools to install: {tools}') self.install_tools(tools) # Autodetect information from the target, after the Target is # initialized. Expensive computations are deferred so they will only be # computed when actually needed. rta_calib_res_dir = ArtifactPath.join(self._res_dir, 'rta_calib') os.makedirs(rta_calib_res_dir) self.plat_info.add_target_src(self, rta_calib_res_dir, deferred=lazy_platinfo, fallback=True) logger.info(f'Effective platform information:\n{self.plat_info}')
def from_cli(cls, argv=None) -> 'Target': """ Create a Target from command line arguments. :param argv: The list of arguments. ``sys.argv[1:]`` will be used if this is ``None``. :type argv: list(str) Trying to use this in a script that expects extra arguments is bound to be confusing (help message woes, argument clashes...), so for now this should only be used in scripts that only expect Target args. """ parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description=textwrap.dedent(""" Connect to a target using the provided configuration in order to run a test. EXAMPLES --target-conf can point to a YAML target configuration file with all the necessary connection information: $ {script} --target-conf my_target.yml Alternatively, --kind must be set along the relevant credentials: $ {script} --kind linux --host 192.0.2.1 --username root --password root In both cases, --platform-info can point to a PlatformInfo YAML file. """.format(script=os.path.basename(sys.argv[0])))) kind_group = parser.add_mutually_exclusive_group(required=True) kind_group.add_argument("--kind", "-k", choices=["android", "linux", "host"], help="The kind of target to connect to.") kind_group.add_argument( "--target-conf", "-t", help= "Path to a TargetConf yaml file. Superseeds other target connection related options." ) device_group = parser.add_mutually_exclusive_group() device_group.add_argument( "--device", "-d", help= "The ADB ID of the target. Superseeds --host. Only applies to Android kind." ) device_group.add_argument("--host", "-n", help="The hostname/IP of the target.") parser.add_argument("--username", "-u", help="Login username. Only applies to Linux kind.") parser.add_argument("--password", "-p", help="Login password. Only applies to Linux kind.") parser.add_argument("--platform-info", "-pi", help="Path to a PlatformInfo yaml file.") parser.add_argument("--log-level", default='info', choices=('warning', 'info', 'debug'), help="Verbosity level of the logs.") parser.add_argument( "--res-dir", "-o", help= "Result directory of the created Target. If no directory is specified, a default location under $LISA_HOME will be used." ) # Options that are not a key in TargetConf must be listed here not_target_conf_opt = ( 'platform_info', 'log_level', 'res_dir', 'target_conf', ) args = parser.parse_args(argv) setup_logging(level=args.log_level.upper()) if args.kind == 'android': if not (args.host or args.device): parser.error('--host or --device must be specified') if args.kind == 'linux': for required in ['host', 'username', 'password']: if getattr(args, required) is None: parser.error('--{} must be specified'.format(required)) platform_info = PlatformInfo.from_yaml_map( args.platform_info) if args.platform_info else None if args.target_conf: target_conf = TargetConf.from_yaml_map(args.target_conf) else: target_conf = TargetConf({ k: v for k, v in vars(args).items() if v is not None and k not in not_target_conf_opt }) return cls.from_conf(conf=target_conf, plat_info=platform_info, res_dir=args.res_dir)