• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
18import datetime
19import json
20import logging
21import os
22import re
23import shutil
24import sys
25import time
26import unittest
27
28import devlib
29from devlib.utils.misc import memoized
30from devlib import Platform, TargetError
31from trappy.stats.Topology import Topology
32
33from wlgen import RTA
34from energy import EnergyMeter
35from energy_model import EnergyModel
36from conf import JsonConf
37from platforms.juno_energy import juno_energy
38from platforms.hikey_energy import hikey_energy
39from platforms.pixel_energy import pixel_energy
40
41USERNAME_DEFAULT = 'root'
42PASSWORD_DEFAULT = ''
43WORKING_DIR_DEFAULT = '/data/local/schedtest'
44FTRACE_EVENTS_DEFAULT = ['sched:*']
45FTRACE_BUFSIZE_DEFAULT = 10240
46OUT_PREFIX = 'results'
47LATEST_LINK = 'results_latest'
48
49basepath = os.path.dirname(os.path.realpath(__file__))
50basepath = basepath.replace('/libs/utils', '')
51
52def 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
59class ShareState(object):
60    __shared_state = {}
61
62    def __init__(self):
63        self.__dict__ = self.__shared_state
64
65class 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
1035IFCFG_BCAST_RE = re.compile(
1036    r'Bcast:(.*) '
1037)
1038
1039# vim :set tabstop=4 shiftwidth=4 expandtab
1040