Home | History | Annotate | Download | only in utils
      1 # SPDX-License-Identifier: Apache-2.0
      2 #
      3 # Copyright (C) 2015, ARM Limited and contributors.
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the "License"); you may
      6 # not use this file except in compliance with the License.
      7 # You may obtain a copy of the License at
      8 #
      9 # http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     13 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 # See the License for the specific language governing permissions and
     15 # limitations under the License.
     16 #
     17 
     18 import datetime
     19 import json
     20 import logging
     21 import os
     22 import re
     23 import shutil
     24 import sys
     25 import time
     26 import unittest
     27 
     28 import devlib
     29 from devlib.utils.misc import memoized
     30 from devlib import Platform, TargetError
     31 from trappy.stats.Topology import Topology
     32 
     33 from wlgen import RTA
     34 from energy import EnergyMeter
     35 from energy_model import EnergyModel
     36 from conf import JsonConf
     37 from platforms.juno_energy import juno_energy
     38 from platforms.hikey_energy import hikey_energy
     39 from platforms.pixel_energy import pixel_energy
     40 
     41 USERNAME_DEFAULT = 'root'
     42 PASSWORD_DEFAULT = ''
     43 WORKING_DIR_DEFAULT = '/data/local/schedtest'
     44 FTRACE_EVENTS_DEFAULT = ['sched:*']
     45 FTRACE_BUFSIZE_DEFAULT = 10240
     46 OUT_PREFIX = 'results'
     47 LATEST_LINK = 'results_latest'
     48 
     49 basepath = os.path.dirname(os.path.realpath(__file__))
     50 basepath = basepath.replace('/libs/utils', '')
     51 
     52 def os_which(file):
     53     for path in os.environ["PATH"].split(os.pathsep):
     54         if os.path.exists(os.path.join(path, file)):
     55            return os.path.join(path, file)
     56 
     57     return None
     58 
     59 class ShareState(object):
     60     __shared_state = {}
     61 
     62     def __init__(self):
     63         self.__dict__ = self.__shared_state
     64 
     65 class TestEnv(ShareState):
     66     """
     67     Represents the environment configuring LISA, the target, and the test setup
     68 
     69     The test environment is defined by:
     70 
     71     - a target configuration (target_conf) defining which HW platform we
     72       want to use to run the experiments
     73     - a test configuration (test_conf) defining which SW setups we need on
     74       that HW target
     75     - a folder to collect the experiments results, which can be specified
     76       using the test_conf::results_dir option and is by default wiped from
     77       all the previous contents (if wipe=True)
     78 
     79     :param target_conf:
     80         Configuration defining the target to run experiments on. May be
     81 
     82             - A dict defining the values directly
     83             - A path to a JSON file containing the configuration
     84             - ``None``, in which case $LISA_HOME/target.config is used.
     85 
     86         You need to provide the information needed to connect to the
     87         target. For SSH targets that means "host", "username" and
     88         either "password" or "keyfile". All other fields are optional if
     89         the relevant features aren't needed. Has the following keys:
     90 
     91         **host**
     92             Target IP or MAC address for SSH access
     93         **username**
     94             For SSH access
     95         **keyfile**
     96             Path to SSH key (alternative to password)
     97         **password**
     98             SSH password (alternative to keyfile)
     99         **device**
    100             Target Android device ID if using ADB
    101         **port**
    102             Port for Android connection default port is 5555
    103         **ANDROID_HOME**
    104             Path to Android SDK. Defaults to ``$ANDROID_HOME`` from the
    105             environment.
    106         **rtapp-calib**
    107             Calibration values for RT-App. If unspecified, LISA will
    108             calibrate RT-App on the target. A message will be logged with
    109             a value that can be copied here to avoid having to re-run
    110             calibration on subsequent tests.
    111         **tftp**
    112             Directory path containing kernels and DTB images for the
    113             target. LISA does *not* manage this TFTP server, it must be
    114             provided externally. Optional.
    115 
    116     :param test_conf: Configuration of software for target experiments. Takes
    117                       the same form as target_conf. Fields are:
    118 
    119         **modules**
    120             Devlib modules to be enabled. Default is []
    121         **exclude_modules**
    122             Devlib modules to be disabled. Default is [].
    123         **tools**
    124             List of tools (available under ./tools/$ARCH/) to install on
    125             the target. Names, not paths (e.g. ['ftrace']). Default is [].
    126         **ping_time**, **reboot_time**
    127             Override parameters to :meth:`reboot` method
    128         **__features__**
    129             List of test environment features to enable. Options are:
    130 
    131             "no-kernel"
    132                 do not deploy kernel/dtb images
    133             "no-reboot"
    134                 do not force reboot the target at each configuration change
    135             "debug"
    136                 enable debugging messages
    137 
    138         **ftrace**
    139             Configuration for ftrace. Dictionary with keys:
    140 
    141             events
    142                 events to enable.
    143             functions
    144                 functions to enable in the function tracer. Optional.
    145             buffsize
    146                 Size of buffer. Default is 10240.
    147 
    148         **systrace**
    149             Configuration for systrace. Dictionary with keys:
    150             categories:
    151                 overide the list of categories enabled
    152             extra_categories:
    153                 append to the default list of categories
    154             extra_events:
    155                 additional ftrace events to manually enable during systrac'ing
    156             buffsize:
    157                 Size of ftrace buffer that systrace uses
    158 
    159         **results_dir**
    160             location of results of the experiments
    161 
    162     :param wipe: set true to cleanup all previous content from the output
    163                  folder
    164     :type wipe: bool
    165 
    166     :param force_new: Create a new TestEnv object even if there is one available
    167                       for this session.  By default, TestEnv only creates one
    168                       object per session, use this to override this behaviour.
    169     :type force_new: bool
    170     """
    171 
    172     _initialized = False
    173 
    174     def __init__(self, target_conf=None, test_conf=None, wipe=True,
    175                  force_new=False):
    176         super(TestEnv, self).__init__()
    177 
    178         if self._initialized and not force_new:
    179             return
    180 
    181         self.conf = {}
    182         self.test_conf = {}
    183         self.target = None
    184         self.ftrace = None
    185         self.workdir = WORKING_DIR_DEFAULT
    186         self.__installed_tools = set()
    187         self.__modules = []
    188         self.__connection_settings = None
    189         self._calib = None
    190 
    191         # Keep track of target IP and MAC address
    192         self.ip = None
    193         self.mac = None
    194 
    195         # Keep track of last installed kernel
    196         self.kernel = None
    197         self.dtb = None
    198 
    199         # Energy meter configuration
    200         self.emeter = None
    201 
    202         # The platform descriptor to be saved into the results folder
    203         self.platform = {}
    204 
    205         # Keep track of android support
    206         self.LISA_HOME = os.environ.get('LISA_HOME', '/vagrant')
    207         self.ANDROID_HOME = os.environ.get('ANDROID_HOME', None)
    208         self.CATAPULT_HOME = os.environ.get('CATAPULT_HOME',
    209                 os.path.join(self.LISA_HOME, 'tools', 'catapult'))
    210 
    211         # Setup logging
    212         self._log = logging.getLogger('TestEnv')
    213 
    214         # Compute base installation path
    215         self._log.info('Using base path: %s', basepath)
    216 
    217         # Setup target configuration
    218         if isinstance(target_conf, dict):
    219             self._log.info('Loading custom (inline) target configuration')
    220             self.conf = target_conf
    221         elif isinstance(target_conf, str):
    222             self._log.info('Loading custom (file) target configuration')
    223             self.conf = self.loadTargetConfig(target_conf)
    224         elif target_conf is None:
    225             self._log.info('Loading default (file) target configuration')
    226             self.conf = self.loadTargetConfig()
    227         self._log.debug('Target configuration %s', self.conf)
    228 
    229         # Setup test configuration
    230         if test_conf:
    231             if isinstance(test_conf, dict):
    232                 self._log.info('Loading custom (inline) test configuration')
    233                 self.test_conf = test_conf
    234             elif isinstance(test_conf, str):
    235                 self._log.info('Loading custom (file) test configuration')
    236                 self.test_conf = self.loadTargetConfig(test_conf)
    237             else:
    238                 raise ValueError('test_conf must be either a dictionary or a filepath')
    239             self._log.debug('Test configuration %s', self.conf)
    240 
    241         # Setup target working directory
    242         if 'workdir' in self.conf:
    243             self.workdir = self.conf['workdir']
    244 
    245         # Initialize binary tools to deploy
    246         test_conf_tools = self.test_conf.get('tools', [])
    247         target_conf_tools = self.conf.get('tools', [])
    248         self.__tools = list(set(test_conf_tools + target_conf_tools))
    249 
    250         # Initialize ftrace events
    251         # test configuration override target one
    252         if 'ftrace' in self.test_conf:
    253             self.conf['ftrace'] = self.test_conf['ftrace']
    254         if self.conf.get('ftrace'):
    255             self.__tools.append('trace-cmd')
    256 
    257         # Initialize features
    258         if '__features__' not in self.conf:
    259             self.conf['__features__'] = []
    260 
    261         self._init()
    262 
    263         # Initialize FTrace events collection
    264         self._init_ftrace(True)
    265 
    266         # Initialize RT-App calibration values
    267         self.calibration()
    268 
    269         # Initialize local results folder
    270         # test configuration overrides target one
    271         self.res_dir = (self.test_conf.get('results_dir') or
    272                         self.conf.get('results_dir'))
    273 
    274         if self.res_dir and not os.path.isabs(self.res_dir):
    275             self.res_dir = os.path.join(basepath, 'results', self.res_dir)
    276         else:
    277             self.res_dir = os.path.join(basepath, OUT_PREFIX)
    278             self.res_dir = datetime.datetime.now()\
    279                             .strftime(self.res_dir + '/%Y%m%d_%H%M%S')
    280 
    281         if wipe and os.path.exists(self.res_dir):
    282             self._log.warning('Wipe previous contents of the results folder:')
    283             self._log.warning('   %s', self.res_dir)
    284             shutil.rmtree(self.res_dir, ignore_errors=True)
    285         if not os.path.exists(self.res_dir):
    286             os.makedirs(self.res_dir)
    287 
    288         res_lnk = os.path.join(basepath, LATEST_LINK)
    289         if os.path.islink(res_lnk):
    290             os.remove(res_lnk)
    291         os.symlink(self.res_dir, res_lnk)
    292 
    293         # Initialize energy probe instrument
    294         self._init_energy(True)
    295 
    296         self._log.info('Set results folder to:')
    297         self._log.info('   %s', self.res_dir)
    298         self._log.info('Experiment results available also in:')
    299         self._log.info('   %s', res_lnk)
    300 
    301         self._initialized = True
    302 
    303     def loadTargetConfig(self, filepath='target.config'):
    304         """
    305         Load the target configuration from the specified file.
    306 
    307         :param filepath: Path of the target configuration file. Relative to the
    308                          root folder of the test suite.
    309         :type filepath: str
    310 
    311         """
    312 
    313         # Loading default target configuration
    314         conf_file = os.path.join(basepath, filepath)
    315 
    316         self._log.info('Loading target configuration [%s]...', conf_file)
    317         conf = JsonConf(conf_file)
    318         conf.load()
    319         return conf.json
    320 
    321     def _init(self, force = False):
    322 
    323         # Initialize target
    324         self._init_target(force)
    325 
    326         # Initialize target Topology for behavior analysis
    327         CLUSTERS = []
    328 
    329         # Build topology for a big.LITTLE systems
    330         if self.target.big_core and \
    331            (self.target.abi == 'arm64' or self.target.abi == 'armeabi'):
    332             # Populate cluster for a big.LITTLE platform
    333             if self.target.big_core:
    334                 # Load cluster of LITTLE cores
    335                 CLUSTERS.append(
    336                     [i for i,t in enumerate(self.target.core_names)
    337                                 if t == self.target.little_core])
    338                 # Load cluster of big cores
    339                 CLUSTERS.append(
    340                     [i for i,t in enumerate(self.target.core_names)
    341                                 if t == self.target.big_core])
    342         # Build topology for an SMP systems
    343         elif not self.target.big_core or \
    344              self.target.abi == 'x86_64':
    345             for c in set(self.target.core_clusters):
    346                 CLUSTERS.append(
    347                     [i for i,v in enumerate(self.target.core_clusters)
    348                                 if v == c])
    349         self.topology = Topology(clusters=CLUSTERS)
    350         self._log.info('Topology:')
    351         self._log.info('   %s', CLUSTERS)
    352 
    353         # Initialize the platform descriptor
    354         self._init_platform()
    355 
    356 
    357     def _init_target(self, force = False):
    358 
    359         if not force and self.target is not None:
    360             return self.target
    361 
    362         self.__connection_settings = {}
    363 
    364         # Configure username
    365         if 'username' in self.conf:
    366             self.__connection_settings['username'] = self.conf['username']
    367         else:
    368             self.__connection_settings['username'] = USERNAME_DEFAULT
    369 
    370         # Configure password or SSH keyfile
    371         if 'keyfile' in self.conf:
    372             self.__connection_settings['keyfile'] = self.conf['keyfile']
    373         elif 'password' in self.conf:
    374             self.__connection_settings['password'] = self.conf['password']
    375         else:
    376             self.__connection_settings['password'] = PASSWORD_DEFAULT
    377 
    378         # Configure port
    379         if 'port' in self.conf:
    380             self.__connection_settings['port'] = self.conf['port']
    381 
    382         # Configure the host IP/MAC address
    383         if 'host' in self.conf:
    384             try:
    385                 if ':' in self.conf['host']:
    386                     (self.mac, self.ip) = self.resolv_host(self.conf['host'])
    387                 else:
    388                     self.ip = self.conf['host']
    389                 self.__connection_settings['host'] = self.ip
    390             except KeyError:
    391                 raise ValueError('Config error: missing [host] parameter')
    392 
    393         try:
    394             platform_type = self.conf['platform']
    395         except KeyError:
    396             raise ValueError('Config error: missing [platform] parameter')
    397 
    398         if platform_type.lower() == 'android':
    399             self.ANDROID_HOME = self.conf.get('ANDROID_HOME',
    400                                               self.ANDROID_HOME)
    401             if self.ANDROID_HOME:
    402                 self._adb = os.path.join(self.ANDROID_HOME,
    403                                          'platform-tools', 'adb')
    404                 self._fastboot = os.path.join(self.ANDROID_HOME,
    405                                               'platform-tools', 'fastboot')
    406                 os.environ['ANDROID_HOME'] = self.ANDROID_HOME
    407                 os.environ['CATAPULT_HOME'] = self.CATAPULT_HOME
    408             else:
    409                 self._log.info('Android SDK not found as ANDROID_HOME not defined, using PATH for platform tools')
    410                 self._adb = os_which('adb')
    411                 self._fastboot = os_which('fastboot')
    412                 if self._adb:
    413                     self._log.info('Using adb from ' + self._adb)
    414                 if self._fastboot:
    415                     self._log.info('Using fastboot from ' + self._fastboot)
    416 
    417             self._log.info('External tools using:')
    418             self._log.info('   ANDROID_HOME: %s', self.ANDROID_HOME)
    419             self._log.info('   CATAPULT_HOME: %s', self.CATAPULT_HOME)
    420 
    421             if not os.path.exists(self._adb):
    422                 raise RuntimeError('\nADB binary not found\n\t{}\ndoes not exists!\n\n'
    423                                    'Please configure ANDROID_HOME to point to '
    424                                    'a valid Android SDK installation folder.'\
    425                                    .format(self._adb))
    426 
    427         ########################################################################
    428         # Board configuration
    429         ########################################################################
    430 
    431         # Setup board default if not specified by configuration
    432         self.nrg_model = None
    433         platform = None
    434         self.__modules = []
    435         if 'board' not in self.conf:
    436             self.conf['board'] = 'UNKNOWN'
    437 
    438         # Initialize TC2 board
    439         if self.conf['board'].upper() == 'TC2':
    440             platform = devlib.platform.arm.TC2()
    441             self.__modules = ['bl', 'hwmon', 'cpufreq']
    442 
    443         # Initialize JUNO board
    444         elif self.conf['board'].upper() in ('JUNO', 'JUNO2'):
    445             platform = devlib.platform.arm.Juno()
    446             self.nrg_model = juno_energy
    447             self.__modules = ['bl', 'hwmon', 'cpufreq']
    448 
    449         # Initialize OAK board
    450         elif self.conf['board'].upper() == 'OAK':
    451             platform = Platform(model='MT8173')
    452             self.__modules = ['bl', 'cpufreq']
    453 
    454         # Initialized HiKey board
    455         elif self.conf['board'].upper() == 'HIKEY':
    456             self.nrg_model = hikey_energy
    457             self.__modules = [ "cpufreq", "cpuidle" ]
    458             platform = Platform(model='hikey')
    459 
    460         # Initialize Pixel phone
    461         elif self.conf['board'].upper() == 'PIXEL':
    462             self.nrg_model = pixel_energy
    463             self.__modules = ['bl', 'cpufreq']
    464             platform = Platform(model='pixel')
    465 
    466         elif self.conf['board'] != 'UNKNOWN':
    467             # Initilize from platform descriptor (if available)
    468             board = self._load_board(self.conf['board'])
    469             if board:
    470                 core_names=board['cores']
    471                 platform = Platform(
    472                     model=self.conf['board'],
    473                     core_names=core_names,
    474                     core_clusters = self._get_clusters(core_names),
    475                     big_core=board.get('big_core', None)
    476                 )
    477                 self.__modules=board.get('modules', [])
    478 
    479         ########################################################################
    480         # Modules configuration
    481         ########################################################################
    482 
    483         modules = set(self.__modules)
    484 
    485         # Refine modules list based on target.conf
    486         modules.update(self.conf.get('modules', []))
    487         # Merge tests specific modules
    488         modules.update(self.test_conf.get('modules', []))
    489 
    490         remove_modules = set(self.conf.get('exclude_modules', []) +
    491                              self.test_conf.get('exclude_modules', []))
    492         modules.difference_update(remove_modules)
    493 
    494         self.__modules = list(modules)
    495         self._log.info('Devlib modules to load: %s', self.__modules)
    496 
    497         ########################################################################
    498         # Devlib target setup (based on target.config::platform)
    499         ########################################################################
    500 
    501         # If the target is Android, we need just (eventually) the device
    502         if platform_type.lower() == 'android':
    503             self.__connection_settings = None
    504             device = 'DEFAULT'
    505             if 'device' in self.conf:
    506                 device = self.conf['device']
    507                 self.__connection_settings = {'device' : device}
    508             elif 'host' in self.conf:
    509                 host = self.conf['host']
    510                 port = '5555'
    511                 if 'port' in self.conf:
    512                     port = str(self.conf['port'])
    513                 device = '{}:{}'.format(host, port)
    514                 self.__connection_settings = {'device' : device}
    515             self._log.info('Connecting Android target [%s]', device)
    516         else:
    517             self._log.info('Connecting %s target:', platform_type)
    518             for key in self.__connection_settings:
    519                 self._log.info('%10s : %s', key,
    520                                self.__connection_settings[key])
    521 
    522         self._log.info('Connection settings:')
    523         self._log.info('   %s', self.__connection_settings)
    524 
    525         if platform_type.lower() == 'linux':
    526             self._log.debug('Setup LINUX target...')
    527             if "host" not in self.__connection_settings:
    528                 raise ValueError('Missing "host" param in Linux target conf')
    529 
    530             self.target = devlib.LinuxTarget(
    531                     platform = platform,
    532                     connection_settings = self.__connection_settings,
    533                     load_default_modules = False,
    534                     modules = self.__modules)
    535         elif platform_type.lower() == 'android':
    536             self._log.debug('Setup ANDROID target...')
    537             self.target = devlib.AndroidTarget(
    538                     platform = platform,
    539                     connection_settings = self.__connection_settings,
    540                     load_default_modules = False,
    541                     modules = self.__modules)
    542         elif platform_type.lower() == 'host':
    543             self._log.debug('Setup HOST target...')
    544             self.target = devlib.LocalLinuxTarget(
    545                     platform = platform,
    546                     load_default_modules = False,
    547                     modules = self.__modules)
    548         else:
    549             raise ValueError('Config error: not supported [platform] type {}'\
    550                     .format(platform_type))
    551 
    552         self._log.debug('Checking target connection...')
    553         self._log.debug('Target info:')
    554         self._log.debug('      ABI: %s', self.target.abi)
    555         self._log.debug('     CPUs: %s', self.target.cpuinfo)
    556         self._log.debug(' Clusters: %s', self.target.core_clusters)
    557 
    558         self._log.info('Initializing target workdir:')
    559         self._log.info('   %s', self.target.working_directory)
    560 
    561         self.target.setup()
    562         self.install_tools(self.__tools)
    563 
    564         # Verify that all the required modules have been initialized
    565         for module in self.__modules:
    566             self._log.debug('Check for module [%s]...', module)
    567             if not hasattr(self.target, module):
    568                 self._log.warning('Unable to initialize [%s] module', module)
    569                 self._log.error('Fix your target kernel configuration or '
    570                                 'disable module from configuration')
    571                 raise RuntimeError('Failed to initialized [{}] module, '
    572                         'update your kernel or test configurations'.format(module))
    573 
    574         if not self.nrg_model:
    575             try:
    576                 self._log.info('Attempting to read energy model from target')
    577                 self.nrg_model = EnergyModel.from_target(self.target)
    578             except (TargetError, RuntimeError, ValueError) as e:
    579                 self._log.error("Couldn't read target energy model: %s", e)
    580 
    581     def install_tools(self, tools):
    582         """
    583         Install tools additional to those specified in the test config 'tools'
    584         field
    585 
    586         :param tools: The list of names of tools to install
    587         :type tools: list(str)
    588         """
    589         tools = set(tools)
    590 
    591         # Add tools dependencies
    592         if 'rt-app' in tools:
    593             tools.update(['taskset', 'trace-cmd', 'perf', 'cgroup_run_into.sh'])
    594 
    595         # Remove duplicates and already-instaled tools
    596         tools.difference_update(self.__installed_tools)
    597 
    598         tools_to_install = []
    599         for tool in tools:
    600             binary = '{}/tools/scripts/{}'.format(basepath, tool)
    601             if not os.path.isfile(binary):
    602                 binary = '{}/tools/{}/{}'\
    603                          .format(basepath, self.target.abi, tool)
    604             tools_to_install.append(binary)
    605 
    606         for tool_to_install in tools_to_install:
    607             self.target.install(tool_to_install)
    608 
    609         self.__installed_tools.update(tools)
    610 
    611     def ftrace_conf(self, conf):
    612         self._init_ftrace(True, conf)
    613 
    614     def _init_ftrace(self, force=False, conf=None):
    615 
    616         if not force and self.ftrace is not None:
    617             return self.ftrace
    618 
    619         if conf is None and 'ftrace' not in self.conf:
    620             return None
    621 
    622         if conf is not None:
    623             ftrace = conf
    624         else:
    625             ftrace = self.conf['ftrace']
    626 
    627         events = FTRACE_EVENTS_DEFAULT
    628         if 'events' in ftrace:
    629             events = ftrace['events']
    630 
    631         functions = None
    632         if 'functions' in ftrace:
    633             functions = ftrace['functions']
    634 
    635         buffsize = FTRACE_BUFSIZE_DEFAULT
    636         if 'buffsize' in ftrace:
    637             buffsize = ftrace['buffsize']
    638 
    639         self.ftrace = devlib.FtraceCollector(
    640             self.target,
    641             events      = events,
    642             functions   = functions,
    643             buffer_size = buffsize,
    644             autoreport  = False,
    645             autoview    = False
    646         )
    647 
    648         if events:
    649             self._log.info('Enabled tracepoints:')
    650             for event in events:
    651                 self._log.info('   %s', event)
    652         if functions:
    653             self._log.info('Kernel functions profiled:')
    654             for function in functions:
    655                 self._log.info('   %s', function)
    656 
    657         return self.ftrace
    658 
    659     def _init_energy(self, force):
    660 
    661         # Initialize energy probe to board default
    662         self.emeter = EnergyMeter.getInstance(self.target, self.conf, force,
    663                                               self.res_dir)
    664 
    665     def _init_platform_bl(self):
    666         self.platform = {
    667             'clusters' : {
    668                 'little'    : self.target.bl.littles,
    669                 'big'       : self.target.bl.bigs
    670             },
    671             'freqs' : {
    672                 'little'    : self.target.bl.list_littles_frequencies(),
    673                 'big'       : self.target.bl.list_bigs_frequencies()
    674             }
    675         }
    676         self.platform['cpus_count'] = \
    677             len(self.platform['clusters']['little']) + \
    678             len(self.platform['clusters']['big'])
    679 
    680     def _init_platform_smp(self):
    681         self.platform = {
    682             'clusters' : {},
    683             'freqs' : {}
    684         }
    685         for cpu_id,node_id in enumerate(self.target.core_clusters):
    686             if node_id not in self.platform['clusters']:
    687                 self.platform['clusters'][node_id] = []
    688             self.platform['clusters'][node_id].append(cpu_id)
    689 
    690         if 'cpufreq' in self.target.modules:
    691             # Try loading frequencies using the cpufreq module
    692             for cluster_id in self.platform['clusters']:
    693                 core_id = self.platform['clusters'][cluster_id][0]
    694                 self.platform['freqs'][cluster_id] = \
    695                     self.target.cpufreq.list_frequencies(core_id)
    696         else:
    697             self._log.warning('Unable to identify cluster frequencies')
    698 
    699         # TODO: get the performance boundaries in case of intel_pstate driver
    700 
    701         self.platform['cpus_count'] = len(self.target.core_clusters)
    702 
    703     def _load_em(self, board):
    704         em_path = os.path.join(basepath,
    705                 'libs/utils/platforms', board.lower() + '.json')
    706         self._log.debug('Trying to load default EM from %s', em_path)
    707         if not os.path.exists(em_path):
    708             return None
    709         self._log.info('Loading default EM:')
    710         self._log.info('   %s', em_path)
    711         board = JsonConf(em_path)
    712         board.load()
    713         if 'nrg_model' not in board.json:
    714             return None
    715         return board.json['nrg_model']
    716 
    717     def _load_board(self, board):
    718         board_path = os.path.join(basepath,
    719                 'libs/utils/platforms', board.lower() + '.json')
    720         self._log.debug('Trying to load board descriptor from %s', board_path)
    721         if not os.path.exists(board_path):
    722             return None
    723         self._log.info('Loading board:')
    724         self._log.info('   %s', board_path)
    725         board = JsonConf(board_path)
    726         board.load()
    727         if 'board' not in board.json:
    728             return None
    729         return board.json['board']
    730 
    731     def _get_clusters(self, core_names):
    732         idx = 0
    733         clusters = []
    734         ids_map = { core_names[0] : 0 }
    735         for name in core_names:
    736             idx = ids_map.get(name, idx+1)
    737             ids_map[name] = idx
    738             clusters.append(idx)
    739         return clusters
    740 
    741     def _init_platform(self):
    742         if 'bl' in self.target.modules:
    743             self._init_platform_bl()
    744         else:
    745             self._init_platform_smp()
    746 
    747         # Adding energy model information
    748         if 'nrg_model' in self.conf:
    749             self.platform['nrg_model'] = self.conf['nrg_model']
    750         # Try to load the default energy model (if available)
    751         else:
    752             self.platform['nrg_model'] = self._load_em(self.conf['board'])
    753 
    754         # Adding topology information
    755         self.platform['topology'] = self.topology.get_level("cluster")
    756 
    757         # Adding kernel build information
    758         kver = self.target.kernel_version
    759         self.platform['kernel'] = {t: getattr(kver, t, None)
    760             for t in [
    761                 'release', 'version',
    762                 'version_number', 'major', 'minor',
    763                 'rc', 'sha1', 'parts'
    764             ]
    765         }
    766         self.platform['abi'] = self.target.abi
    767         self.platform['os'] = self.target.os
    768 
    769         self._log.debug('Platform descriptor initialized\n%s', self.platform)
    770         # self.platform_dump('./')
    771 
    772     def platform_dump(self, dest_dir, dest_file='platform.json'):
    773         plt_file = os.path.join(dest_dir, dest_file)
    774         self._log.debug('Dump platform descriptor in [%s]', plt_file)
    775         with open(plt_file, 'w') as ofile:
    776             json.dump(self.platform, ofile, sort_keys=True, indent=4)
    777         return (self.platform, plt_file)
    778 
    779     def calibration(self, force=False):
    780         """
    781         Get rt-app calibration. Run calibration on target if necessary.
    782 
    783         :param force: Always run calibration on target, even if we have not
    784                       installed rt-app or have already run calibration.
    785         :returns: A dict with calibration results, which can be passed as the
    786                   ``calibration`` parameter to :class:`RTA`, or ``None`` if
    787                   force=False and we have not installed rt-app.
    788         """
    789 
    790         if not force and self._calib:
    791             return self._calib
    792 
    793         required = force or 'rt-app' in self.__installed_tools
    794 
    795         if not required:
    796             self._log.debug('No RT-App workloads, skipping calibration')
    797             return
    798 
    799         if not force and 'rtapp-calib' in self.conf:
    800             self._log.warning('Using configuration provided RTApp calibration')
    801             self._calib = {
    802                     int(key): int(value)
    803                     for key, value in self.conf['rtapp-calib'].items()
    804                 }
    805         else:
    806             self._log.info('Calibrating RTApp...')
    807             self._calib = RTA.calibrate(self.target)
    808 
    809         self._log.info('Using RT-App calibration values:')
    810         self._log.info('   %s',
    811                        "{" + ", ".join('"%r": %r' % (key, self._calib[key])
    812                                        for key in sorted(self._calib)) + "}")
    813         return self._calib
    814 
    815     def resolv_host(self, host=None):
    816         """
    817         Resolve a host name or IP address to a MAC address
    818 
    819         .. TODO Is my networking terminology correct here?
    820 
    821         :param host: IP address or host name to resolve. If None, use 'host'
    822                     value from target_config.
    823         :type host: str
    824         """
    825         if host is None:
    826             host = self.conf['host']
    827 
    828         # Refresh ARP for local network IPs
    829         self._log.debug('Collecting all Bcast address')
    830         output = os.popen(r'ifconfig').read().split('\n')
    831         for line in output:
    832             match = IFCFG_BCAST_RE.search(line)
    833             if not match:
    834                 continue
    835             baddr = match.group(1)
    836             try:
    837                 cmd = r'nmap -T4 -sP {}/24 &>/dev/null'.format(baddr.strip())
    838                 self._log.debug(cmd)
    839                 os.popen(cmd)
    840             except RuntimeError:
    841                 self._log.warning('Nmap not available, try IP lookup using broadcast ping')
    842                 cmd = r'ping -b -c1 {} &>/dev/null'.format(baddr)
    843                 self._log.debug(cmd)
    844                 os.popen(cmd)
    845 
    846         return self.parse_arp_cache(host)
    847 
    848     def parse_arp_cache(self, host):
    849         output = os.popen(r'arp -n')
    850         if ':' in host:
    851             # Assuming this is a MAC address
    852             # TODO add a suitable check on MAC address format
    853             # Query ARP for the specified HW address
    854             ARP_RE = re.compile(
    855                 r'([^ ]*).*({}|{})'.format(host.lower(), host.upper())
    856             )
    857             macaddr = host
    858             ipaddr = None
    859             for line in output:
    860                 match = ARP_RE.search(line)
    861                 if not match:
    862                     continue
    863                 ipaddr = match.group(1)
    864                 break
    865         else:
    866             # Assuming this is an IP address
    867             # TODO add a suitable check on IP address format
    868             # Query ARP for the specified IP address
    869             ARP_RE = re.compile(
    870                 r'{}.*ether *([0-9a-fA-F:]*)'.format(host)
    871             )
    872             macaddr = None
    873             ipaddr = host
    874             for line in output:
    875                 match = ARP_RE.search(line)
    876                 if not match:
    877                     continue
    878                 macaddr = match.group(1)
    879                 break
    880             else:
    881                 # When target is accessed via WiFi, there is not MAC address
    882                 # reported by arp. In these cases we can know only the IP
    883                 # of the remote target.
    884                 macaddr = 'UNKNOWN'
    885 
    886         if not ipaddr or not macaddr:
    887             raise ValueError('Unable to lookup for target IP/MAC address')
    888         self._log.info('Target (%s) at IP address: %s', macaddr, ipaddr)
    889         return (macaddr, ipaddr)
    890 
    891     def reboot(self, reboot_time=120, ping_time=15):
    892         """
    893         Reboot target.
    894 
    895         :param boot_time: Time to wait for the target to become available after
    896                           reboot before declaring failure.
    897         :param ping_time: Period between attempts to ping the target while
    898                           waiting for reboot.
    899         """
    900         # Send remote target a reboot command
    901         if self._feature('no-reboot'):
    902             self._log.warning('Reboot disabled by conf features')
    903         else:
    904             if 'reboot_time' in self.conf:
    905                 reboot_time = int(self.conf['reboot_time'])
    906 
    907             if 'ping_time' in self.conf:
    908                 ping_time = int(self.conf['ping_time'])
    909 
    910             # Before rebooting make sure to have IP and MAC addresses
    911             # of the target
    912             (self.mac, self.ip) = self.parse_arp_cache(self.ip)
    913 
    914             self.target.execute('sleep 2 && reboot -f &', as_root=True)
    915 
    916             # Wait for the target to complete the reboot
    917             self._log.info('Waiting up to %s[s] for target [%s] to reboot...',
    918                            reboot_time, self.ip)
    919 
    920             ping_cmd = "ping -c 1 {} >/dev/null".format(self.ip)
    921             elapsed = 0
    922             start = time.time()
    923             while elapsed <= reboot_time:
    924                 time.sleep(ping_time)
    925                 self._log.debug('Trying to connect to [%s] target...', self.ip)
    926                 if os.system(ping_cmd) == 0:
    927                     break
    928                 elapsed = time.time() - start
    929             if elapsed > reboot_time:
    930                 if self.mac:
    931                     self._log.warning('target [%s] not responding to PINGs, '
    932                                       'trying to resolve MAC address...',
    933                                       self.ip)
    934                     (self.mac, self.ip) = self.resolv_host(self.mac)
    935                 else:
    936                     self._log.warning('target [%s] not responding to PINGs, '
    937                                       'trying to continue...',
    938                                       self.ip)
    939 
    940         # Force re-initialization of all the devlib modules
    941         force = True
    942 
    943         # Reset the connection to the target
    944         self._init(force)
    945 
    946         # Initialize FTrace events collection
    947         self._init_ftrace(force)
    948 
    949         # Initialize energy probe instrument
    950         self._init_energy(force)
    951 
    952     def install_kernel(self, tc, reboot=False):
    953         """
    954         Deploy kernel and DTB via TFTP, optionally rebooting
    955 
    956         :param tc: Dicionary containing optional keys 'kernel' and 'dtb'. Values
    957                    are paths to the binaries to deploy.
    958         :type tc: dict
    959 
    960         :param reboot: Reboot thet target after deployment
    961         :type reboot: bool
    962         """
    963 
    964         # Default initialize the kernel/dtb settings
    965         tc.setdefault('kernel', None)
    966         tc.setdefault('dtb', None)
    967 
    968         if self.kernel == tc['kernel'] and self.dtb == tc['dtb']:
    969             return
    970 
    971         self._log.info('Install kernel [%s] on target...', tc['kernel'])
    972 
    973         # Install kernel/dtb via FTFP
    974         if self._feature('no-kernel'):
    975             self._log.warning('Kernel deploy disabled by conf features')
    976 
    977         elif 'tftp' in self.conf:
    978             self._log.info('Deploy kernel via TFTP...')
    979 
    980             # Deploy kernel in TFTP folder (mandatory)
    981             if 'kernel' not in tc or not tc['kernel']:
    982                 raise ValueError('Missing "kernel" parameter in conf: %s',
    983                         'KernelSetup', tc)
    984             self.tftp_deploy(tc['kernel'])
    985 
    986             # Deploy DTB in TFTP folder (if provided)
    987             if 'dtb' not in tc or not tc['dtb']:
    988                 self._log.debug('DTB not provided, using existing one')
    989                 self._log.debug('Current conf:\n%s', tc)
    990                 self._log.warning('Using pre-installed DTB')
    991             else:
    992                 self.tftp_deploy(tc['dtb'])
    993 
    994         else:
    995             raise ValueError('Kernel installation method not supported')
    996 
    997         # Keep track of last installed kernel
    998         self.kernel = tc['kernel']
    999         if 'dtb' in tc:
   1000             self.dtb = tc['dtb']
   1001 
   1002         if not reboot:
   1003             return
   1004 
   1005         # Reboot target
   1006         self._log.info('Rebooting taget...')
   1007         self.reboot()
   1008 
   1009 
   1010     def tftp_deploy(self, src):
   1011         """
   1012         .. TODO
   1013         """
   1014 
   1015         tftp = self.conf['tftp']
   1016 
   1017         dst = tftp['folder']
   1018         if 'kernel' in src:
   1019             dst = os.path.join(dst, tftp['kernel'])
   1020         elif 'dtb' in src:
   1021             dst = os.path.join(dst, tftp['dtb'])
   1022         else:
   1023             dst = os.path.join(dst, os.path.basename(src))
   1024 
   1025         cmd = 'cp {} {} && sync'.format(src, dst)
   1026         self._log.info('Deploy %s into %s', src, dst)
   1027         result = os.system(cmd)
   1028         if result != 0:
   1029             self._log.error('Failed to deploy image: %s', src)
   1030             raise ValueError('copy error')
   1031 
   1032     def _feature(self, feature):
   1033         return feature in self.conf['__features__']
   1034 
   1035 IFCFG_BCAST_RE = re.compile(
   1036     r'Bcast:(.*) '
   1037 )
   1038 
   1039 # vim :set tabstop=4 shiftwidth=4 expandtab
   1040