4. Examples and HOWTOs

4.1. Automation/testcase script examples

4.1.1. Returning results

Testcases can return five results to the testcase runner:

  • pass
  • fail
  • error
  • blockage
  • skip

Note that any function that:

  • just returns or returns True is a pass
  • returns False is a failure
  • any other return value other than tcfl.tc.result_c will yield blockage, as it cannot be interpreted.

By raising an exception at any time in your testcase, the execution is terminated, cleanup methods called and the results / collaterals that apply collected.

This example generates a random result, running only in the local host (it is a static testcase):

class _test_pass(tcfl.tc.tc_c):
    def eval(self):
        self.report_pass("I am causing a pass by raising")
        # or you can just return nothing, that means pass
        raise tcfl.tc.pass_e("I passed")
class _test_errr(tcfl.tc.tc_c):
    def eval(self):
        self.report_error("I am causing an error by raising")
        raise tcfl.tc.error_e("I errored")
class _test_fail(tcfl.tc.tc_c):
    def eval(self):
        self.report_fail("I am causing a failure by raising")
        raise tcfl.tc.failed_e("I failed")
class _test_blck(tcfl.tc.tc_c):
    def eval(self):
        self.report_blck("I am causing a blockage by raising")
        raise tcfl.tc.blocked_e("I blocked")
class _test_skip(tcfl.tc.tc_c):
    def eval(self):
        self.report_skip("I am causing a skip by raising")
        raise tcfl.tc.skip_e("I skipped")

Execute the testcase with:

$ tcf run -vv /usr/share/tcf/examples/test_yielding_results.py
INFO2/        toplevel @local: scanning for test cases
INFO1/zom6    .../test_yielding_results.py#_test_pass @localic-localtg: will run on target group 'localic-localtg'
INFO1/kvkn    .../test_yielding_results.py#_test_blck @localic-localtg: will run on target group 'localic-localtg'
INFO1/rnpd    .../test_yielding_results.py#_test_skip @localic-localtg: will run on target group 'localic-localtg'
INFO1/enpe    .../test_yielding_results.py#_test_fail @localic-localtg: will run on target group 'localic-localtg'
INFO1/ap2n    .../test_yielding_results.py#_test_errr @localic-localtg: will run on target group 'localic-localtg'
PASS2/zom6E#1 .../test_yielding_results.py#_test_pass @localic-localtg: I am causing a pass by raising
PASS2/zom6E#1 .../test_yielding_results.py#_test_pass @localic-localtg: eval passed: I passed
PASS1/zom6    .../test_yielding_results.py#_test_pass @localic-localtg: evaluation passed 
BLCK2/kvknE#1 .../test_yielding_results.py#_test_blck @localic-localtg: I am causing a blockage by raising
BLCK2/kvknE#1 .../test_yielding_results.py#_test_blck @localic-localtg: eval blocked: I blocked
BLCK0/kvkn    .../test_yielding_results.py#_test_blck @localic-localtg: evaluation blocked 
SKIP2/rnpdE#1 .../test_yielding_results.py#_test_skip @localic-localtg: I am causing a skip by raising
SKIP2/rnpdE#1 .../test_yielding_results.py#_test_skip @localic-localtg: eval skipped: I skipped
SKIP1/rnpd    .../test_yielding_results.py#_test_skip @localic-localtg: evaluation skipped 
FAIL2/enpeE#1 .../test_yielding_results.py#_test_fail @localic-localtg: I am causing a failure by raising
FAIL2/enpeE#1 .../test_yielding_results.py#_test_fail @localic-localtg: eval failed: I failed
FAIL0/enpe    .../test_yielding_results.py#_test_fail @localic-localtg: evaluation failed 
ERRR2/ap2nE#1 .../test_yielding_results.py#_test_errr @localic-localtg: I am causing an error by raising
ERRR2/ap2nE#1 .../test_yielding_results.py#_test_errr @localic-localtg: eval errored: I errored
ERRR0/ap2n    .../test_yielding_results.py#_test_errr @localic-localtg: evaluation errored 
FAIL0/        toplevel @local: 5 tests (1 passed, 1 error, 1 failed, 1 blocked, 1 skipped, in 0:00:00.505594) - failed 

(depending on your installation method, location might be ~/.local/share/tcf/examples)

Note how tcf run reports counts on how many testcase executed, how many passed/errored/failed/blocked or skipped.

4.1.2. Tag a testcase

A testcase can be given one or more tags with the tcfl.tc.tags() decorator:

@tcfl.tc.tags("boolean_tag", "component/storage", value = 3, color = "red",
              ignore_example = True)
class _test(tcfl.tc.tc_c):
    def eval(self):
        for tag, value in self._tags.iteritems():
            self.report_info("tag %s: %s [from %s]"
                             % (tag, value[0], value[1]), level = 0)

Note the component/ANYTHING tags are special, they are interpreted as a namespace and with them, another tag called components is going to be generated listing all the components found.

Tags, for example, can be used later to filter from the command line to select all testcases with that expose a tag color with value red, in this case, only this one:

$ tcf run -vv -s 'color == "red"' /usr/share/tcf/examples/

(depending on your installation method, location might be ~/.local/share/tcf/examples)

4.1.3. Deploying OS images and files to targets over the network

TCF can do very fast OS deployment by rsyncing images over the network instead of just overwritting evertything:

  • for simple testcases that just need a target provisioned, use test case templates tc_pos_base
  • to have more control over the target selection process, use template tc_pos0_base
  • to have full control over the deployment process or find more details on how this process works in here
  • to deploy multiple targets at the same time, for client/server tests, see here
  • to copy other content to the image after deploying the OS, see this example

4.1.3.1. Quick way to deploy an OS image to a target and get a prompt

Given a target that can be provisioned with Provisioning OS, deploy to it a given image, installed in the server. Then power cycle into the installed OS.

This is a template when your testcase just needs a target, with no frills and your evaluation wants a prompt in a powered machine. In case of failures, errors or blockage, the consoles will be dumped.

class _test(tcfl.pos.tc_pos_base):
    """
    Provisiong a target, boot it, run a shell command
    """

    def eval(self, ic, target):
        target.shell.run("echo I booted", "I booted")

Execute the testcase with (where IMAGE is the name of a Linux OS image installed in the server):

$ IMAGE=clear tcf run -v /usr/share/tcf/examples/test_pos_base.py
INFO1/hxgj     .../test_pos_base.py#_test @sv3m-twgl: will run on target group 'ic=localhost/nwb target=localhost/qu06b:x86_64'
INFO1/hxgjDPOS .../test_pos_base.py#_test @sv3m-twgl|localhost/qu06b: POS: rsyncing clear:desktop:29820::x86_64 from 192.168.98.1::images to /dev/sda5
PASS1/hxgj     .../test_pos_base.py#_test @sv3m-twgl: evaluation passed
PASS0/         toplevel @local: 1 tests (1 passed, 0 error, 0 failed, 0 blocked, 0 skipped, in 0:02:54.992824) - passed

(depending on your installation method, location might be ~/.local/share/tcf/examples)

4.1.3.2. Quick way to deploy an OS image to an specific target and get a prompt

Given a target that can be provisioned with Provisioning OS, deploy to it a given image, installed in the server. Then power cycle into the installed OS.

This is a template when your testcase just needs to flash a given target and you need control over that target selection process. Otherwise is the same as tc_pos_base.

The selection is controlled by the decorators tcfl.tc.interconnect() (to request a network) and tcfl.tc.target() (to request a target):

@tcfl.tc.interconnect('ipv4_addr', mode = 'all')
@tcfl.tc.target('pos_capable and ic.id in interconnects '
                'and capture:"screen:snapshot"')
class _test(tcfl.pos.tc_pos0_base):
    ...

the filtering values come from the metadata exposed by the target, which can be seen with tcf list -vv TARGETNAME* and available to the script in target.kws or ic.kws (see here for more information). In this case:

  • select a network or interconnect (by default called ic) that exposes an IPv4 address, which by convention means it implements IPv4 networking

  • select a target that:

    • can be provisioned with Provisioning OS (pos_capable)

    • is connected to the interconnect (ic.id in interconnects indicates the target declares the network in the list of networks it is connected to; see the output of tcf list -vv TARGETNAME | grep interconnects)

    • exposes a capture interface to get screenshots from the screen; the colon : after capture acts as a regex operator; see:

      $ tcf list -vv capture | grep -w -e id: -e capture:*
       id: qu04a
       capture: vnc0:snapshot:image/png screen:snapshot:image/png
       id: qu04b
       capture: vnc0:snapshot:image/png screen:snapshot:image/png
       ...
      
@tcfl.tc.interconnect('ipv4_addr', mode = 'all')
@tcfl.tc.target('pos_capable and ic.id in interconnects '
                'and capture:"screen:snapshot"')
class _test(tcfl.pos.tc_pos0_base):
    """
    Provisiong a target, boot it, run a shell command
    """

    def eval(self, ic, target):
        target.shell.run("echo I booted", "I booted")

Execute the testcase with (where IMAGE is the name of a Linux OS image installed in the server):

$ IMAGE=clear tcf run -v /usr/share/tcf/examples/test_pos0_base.py
INFO1/yios     .../test_pos0_base.py#_test @sv3m-twgl: will run on target group 'ic=localhost/nwb target=localhost/qu06b:x86_64'
INFO1/yiosDPOS .../test_pos0_base.py#_test @sv3m-twgl|localhost/qu06b: POS: rsyncing clear:desktop:29820::x86_64 from 192.168.98.1::images to /dev/sda5
PASS1/yios     .../test_pos0_base.py#_test @sv3m-twgl: evaluation passed
PASS0/         toplevel @local: 1 tests (1 passed, 0 error, 0 failed, 0 blocked, 0 skipped, in 0:01:51.845546) - passed

(depending on your installation method, location might be ~/.local/share/tcf/examples)

4.1.3.3. Deploy an OS image to a target

Given a target that can be provisioned with Provisioning OS, deploy to it a given image, installed in the server.

  • to copy other content to the image after deploying the OS, see this example

This test will select two targets: a computer to provision and the network it is connected to; the deploying happens over the network (thus why the network is requested).

To accomplish this, the target is first booted in Provisioning mode, a version of a Linux OS whose root filesystem runs off a read-only NFS server in the target; provisioning mode is reached depending on the type of target, via PXE boot or other means. The client then can drive partitioning of the target’s storage and rsyncs root filesystem images in.

If the rootfilesystem is already initialized present, rsync will transfer only changes, or refresh, which is much faster.

This can be used to start every single test with first install a fresh OS.

@tcfl.tc.interconnect("ipv4_addr", mode = 'all')
@tcfl.tc.target("pos_capable")
class _test(tcfl.tc.tc_c):

    image_requested = None
    image = "not deployed"

    def deploy(self, ic, target):
        if self.image_requested == None:
            if not 'IMAGE' in os.environ:
                raise tcfl.tc.blocked_e(
                    "No image to install specified, set envar IMAGE")
            self.image_requested = os.environ["IMAGE"]
        # ensure network, DHCP, TFTP, etc are up and deploy
        ic.power.on()
        self.image = target.pos.deploy_image(ic, self.image_requested)
        target.report_pass("deployed %s" % self.image)

    def start(self, ic, target):
        # fire up the target, wait for a login prompt, ensure the
        # network is on so the PXE controller can direct the target
        # where to boot--also, if we skip deployment, ensures we have
        # networking on
        ic.power.on()
        target.pos.boot_normal()		# boot no Provisioning OS
        target.shell.up(user = 'root')		# login as root
        ic.release()			# if we don't need the network

    def eval(self, target):
        # do our test
        target.shell.run("echo I booted", "I booted")

    def teardown(self):
        tcfl.tl.console_dump_on_failure(self)

Execute the testcase with (where IMAGE is the name of a Linux OS image installed in the server):

$ IMAGE=clear tcf run -v /usr/share/tcf/examples/test_pos_deploy.py
INFO1/cz1c     .../test_pos_deploy.py#_test @sv3m-twgl: will run on target group 'ic=localhost/nwb target=localhost/qu06b:x86_64'
INFO1/cz1cDPOS .../test_pos_deploy.py#_test @sv3m-twgl|localhost/qu06b: POS: rsyncing clear:desktop:29820::x86_64 from 192.168.98.1::images to /dev/sda5
PASS1/cz1c     .../test_pos_deploy.py#_test @sv3m-twgl: evaluation passed 
PASS0/         toplevel @local: 1 tests (1 passed, 0 error, 0 failed, 0 blocked, 0 skipped, in 0:03:21.020510) - passed 

(depending on your installation method, location might be ~/.local/share/tcf/examples)

In general, you can use tcf run test_pos_deploy.py to provision machines any time for any reason from the command line.

4.1.3.4. Deploy an OS image to two targets simultaneously

Given two target that can be provisioned with Provisioning OS, deploy to them images available in the server.

This test will select three targets: two machines to provision and the network they are both connected to; the deploying happens over the network (thus why the it is requested).

@tcfl.tc.interconnect("ipv4_addr")
@tcfl.tc.target('pos_capable')
@tcfl.tc.target('pos_capable')
class _test(tcfl.tc.tc_c):
    """
    Provision two PC targets at the same time with the Provisioning OS
    """
    image_requested = None
    image_requested1 = None

    @tcfl.tc.serially()
    def deploy_00(self, ic):
        ic.power.on()
        if self.image_requested == None:
            if not 'IMAGE' in os.environ:
                raise tcfl.tc.blocked_e(
                    "No image to install specified, set envar IMAGE")
            self.image_requested = os.environ["IMAGE"]
        self.image_requested1 = os.environ.get("IMAGE1", self.image_requested)

    @tcfl.tc.concurrently()
    def deploy_10_target(self, ic, target):
        image = target.pos.deploy_image(ic, self.image_requested)
        target.report_pass("deployed %s" % image, dlevel = -1)

    @tcfl.tc.concurrently()
    def deploy_10_target1(self, ic, target1):
        image = target1.pos.deploy_image(ic, self.image_requested1)
        target1.report_pass("deployed %s" % image, dlevel = -1)

    @tcfl.tc.serially()
    def start_ic(self, ic):
        ic.power.on()			# in case we skip deploy

    def start_target(self, target):
        target.pos.boot_normal()
        target.shell.up(user = 'root')
        
    def start_target1(self, target1):
        target1.pos.boot_normal()
        target1.shell.up(user = 'root')

    def eval(self, target, target1):
        target.shell.run("echo I booted", "I booted")
        target1.shell.run("echo I booted", "I booted")
        
    def teardown(self):
        tcfl.tl.console_dump_on_failure(self)

This can be used to implement client/server testcases, where one target is configured as a server, the other as client and tests are executed in a private, isolated network with fresh OS instalations. It can be easily extended to any number of targets by adding more tcfl.tc.target() decorators, and deploy_targetN() and start_targetN() methods.

Execute the testcase with (where IMAGE is the name of a Linux OS image available in the server):

$ IMAGE=clear tcf run -v /usr/share/tcf/examples/test_pos_deploy_2.py
INFO1/x9uz    .../test_pos_deploy_2.py#_test @sv3m-fmav: will run on target group 'ic=localhost/nwb target=localhost/qu06b:x86_64 target1=localhost/qu05b:x86_64'
PASS1/x9uz    .../test_pos_deploy_2.py#_test @sv3m-fmav: evaluation passed 
PASS0/        toplevel @local: 1 tests (1 passed, 0 error, 0 failed, 0 blocked, 0 skipped, in 0:02:43.525650) - passed 

(depending on your installation method, location might be ~/.local/share/tcf/examples)

4.1.3.5. Boot a target in Provisioning mode

Given a target that supports Provisioning OS mode, boot it in said mode.

This allows to manipulate the target’s filesystem, as the POS boots off a filesystem in the network.

@tcfl.tc.interconnect("ipv4_addr")
@tcfl.tc.target('pos_capable')
class _test(tcfl.tc.tc_c):
    """
    Boot a target to Provisioning OS
    """

    def eval(self, ic, target):
        ic.power.on()
        target.pos.boot_to_pos()

    def teardown(self):
        tcfl.tl.console_dump_on_failure(self)

Execute the testcase with (where IMAGE is the name of a Linux OS image installed in the server):

$ tcf run -vt "nwa or qu04a" /usr/share/tcf/examples/test_pos_boot.py
$ tcf run -vt "nwa or qu04a" tcf.git/examples/test_pos_boot.py
INFO1/rdgx    tcf.git/examples/test_pos_boot.py#_test @3hyt-uo3g: will run on target group 'ic=localhost/nwa target=localhost/qu04a:x86_64'
PASS1/rdgx    tcf.git/examples/test_pos_boot.py#_test @3hyt-uo3g: evaluation passed 
PASS0/        toplevel @local: 1 tests (1 passed, 0 error, 0 failed, 0 blocked, 0 skipped, in 0:00:36.773884) - passed 

(depending on your installation method, location might be ~/.local/share/tcf/examples)

Note if you add --no-release, then you can login to the console and manipulate the target; later you will have to manually release the target and network:

$ tcf run --no-release -vt "nwa or qu04a" tcf.git/examples/test_pos_boot.py
INFO1/rdgx    tcf.git/examples/test_pos_boot.py#_test @3hyt-uo3g: will run on target group 'ic=localhost/nwa target=localhost/qu04a:x86_64'
PASS1/rdgx    tcf.git/examples/test_pos_boot.py#_test @3hyt-uo3g: evaluation passed 
INFO0/rdgx    tcf.git/examples/test_pos_boot.py#_test @3hyt-uo3g: WARNING!! not releasing targets
PASS0/        toplevel @local: 1 tests (1 passed, 0 error, 0 failed, 0 blocked, 0 skipped, in 0:00:36.773884) - passed 

$ tcf -t rdgx console-write -i qu04a
WARNING: This is a very limited interactive console
         Escape character twice ^[^[ to exit
...
My IP is 192.168.97.4
TCF Network boot to Service OS
Loading http://192.168.97.1/ttbd-pos/x86_64/vmlinuz-tcf-live... ok
Loading http://192.168.97.1/ttbd-pos/x86_64/initramfs-tcf-live...ok
...

TCF-rdgx: 4 $ ls -la
ls -la
total 28
dr-xr-x---  2 root root 4096 Jan 10 11:56 .
dr-xr-xr-x 18 root root 4096 Jan 10 11:58 ..
-rw-r--r--  1 root root   18 Feb  9  2018 .bash_logout
-rw-r--r--  1 root root  176 Feb  9  2018 .bash_profile
-rw-r--r--  1 root root  176 Feb  9  2018 .bashrc
-rw-r--r--  1 root root  100 Feb  9  2018 .cshrc
-rw-r--r--  1 root root  129 Feb  9  2018 .tcshrc
TCF-rdgx: 5 $ 
...

$ tcf -t rdgx release nwa qu04a

4.1.3.6. Send a file or directory tree during deployment to the target

Given a target that can be provisioned with Provisioning OS, send a directory tree to it during the deployment phase.

This allows to copy one or more files, directories etc to the target, right after flashing the OS, so once the target is rebooted into the provisioned OS, it is there. Note that the content itself is cached in the target (in subdirectory /persistent.tcf.d), so next time it is transferred it will be faster (with sizeable files).

You can also send/receive files via SSH once the target is running (example).

This also demonstrates a method to test if a local and remote files are the same by using the MD5 sum.

@tcfl.tc.tags(ignore_example = True)
@tcfl.tc.interconnect('ipv4_addr')
@tcfl.tc.target("pos_capable")
class _test(tcfl.pos.tc_pos0_base):
    """
    Deploy files after installing OS
    """
    image_requested = os.environ.get("IMAGE", 'clear:desktop')
    login_user = os.environ.get('LOGIN_USER', 'root')

    @tcfl.tc.serially()
    def deploy_00(self, target):
        # the format is still a wee bit pedestrian, we'll improve the
        # argument passing
        # This could be a single path not necessarily a list of them
        target.deploy_path_src = [
            # send a directory tree, the one containing this file
            self.kws['srcdir'],
            # send just a file, ../README.rst
            os.path.join(self.kws['srcdir'], "..", "README.rst")
        ]

        target.deploy_path_dest = "/home/place"
        self.deploy_image_args = dict(extra_deploy_fns = [
            tcfl.pos.deploy_path ])
	
    def eval(self, target):
        target.shell.run("find /home -ls")
        # verify the file exists and is the same
        remote = target.shell.run(
            "md5sum < /home/place/examples/data/beep.wav",
            output = True, trim = True).strip()
        local = subprocess.check_output(
            "md5sum < %s" % self.kws['srcdir'] + "/data/beep.wav",
            shell = True).strip()
        if remote != local:
            raise tcfl.tc.failed_e("MD5 mismatch (local %s remote %s)"
                                   % (local, remote))
        self.report_pass("deployed file is identical to local!", level = 1)

Execute the testcase with (where IMAGE is the name of a Linux OS image installed in the server):

$ IMAGE=clear tcf run -v /usr/share/tcf/examples/test_deploy_files.py
INFO1/ubio     .../test_deploy_files.py#_test @n6hi-da7e: will run on target group 'ic=server1/nwd target=server1/nuc-07d:x86_64'
INFO1/ubioDPOS .../test_deploy_files.py#_test @n6hi-da7e|server1/nuc-07d: POS: rsyncing clear:desktop:30080::x86_64 from 192.168.100.1::images to /dev/sda4
PASS1/ubio     .../test_deploy_files.py#_test @n6hi-da7e: deployed file is identical to local!
PASS1/ubio     .../test_deploy_files.py#_test @n6hi-da7e: evaluation passed 
PASS0/        toplevel @local: 1 tests (1 passed, 0 error, 0 failed, 0 blocked, 0 skipped, in 0:00:39.461709) - passed 

(depending on your installation method, location might be ~/.local/share/tcf/examples)

where IMAGE is the name of a Linux OS image installed in the server.

4.1.3.7. Build, install and boot a Linux kernel alongside a given OS

Given a target that can be provisioned with Provisioning OS, build a Linux kernel and modules, deploy an OS into the target along with the just built kernel/modules. Then power cycle into the installed OS with the new kernel.

From there on, different tests to exercise the new kernel could be executed etc. This is a very common pattern for rapid development of kernel code.

When the same target (for caching purposes) and build trees are used, it allows for very quick turn arounds to test new code in the hardware.

Builds on deploying an OS to a target and deploying files to a target. You can find the list of OS images installed in the server.

The kernel deployment process removes any other kernel that was available in the target’s /boot directory, replacing it with the just built one, so the bootloader is configured to boot it.

TIPS:

  • always use the same target (give -t 'nwX or TARGETX' so that the content is cached and each run doesn’t try to send the your built kernel to a new target but just the bare changes. See methods for doing this in the how-to section

  • note depending on your connection to the target, sending the code to the target can take a long time and even release the target as inactive.

    stripping the modules helps (only debug info!), as the debug info accumulates and is usually not needed in the target.

class _test(tcfl.pos.tc_pos_base):
    """
    Build, install and boot a linux kernel
    """

    def build_00(self, ic, target):
        if not 'LK_BUILDDIR' in os.environ:
            raise tcfl.tc.skip_e(
                "please export env LK_BUILDDIR pointing to path of "
                "configured, built or ready-to-build linux kernel tree")
        builddir = os.environ["LK_BUILDDIR"]
        rootdir = os.environ.get("LK_ROOTDIR", self.tmpdir + "/root")

        # update the build
        #
        ## $ make -C BUILDDIR all
        ## ...
        #
        target.report_pass("re/building kernel in %s" % builddir, dlevel = -1)
        output = subprocess.check_output(
            "${MAKE:-make} -C %s all" % builddir,
            shell = True, stderr = subprocess.STDOUT)
        target.report_pass("re/built kernel in %s" % builddir,
                           dict(output = output),
                           alevel = 0, dlevel = -2)

        target.report_pass("installing kernel to %s" % rootdir, dlevel = -1)
        # will run to install the kernel to our fake root dir
        #
        ## $ make INSTALLKERNEL=/dev/null \
        ##       INSTALL_PATH=ROOTDIR/boot INSTALL_MOD_PATH=ROOTDIR \
        ##       install modules_install
        ## sh PATH/linux.git/arch/x86/boot/install.sh 4.19.5 arch/x86/boot/bzImage \
	##    System.map "../root-linux/boot"
        ## Cannot find LILO.
        ## INSTALL arch/x86/crypto/blowfish-x86_64.ko
        ## INSTALL arch/x86/crypto/cast5-avx-x86_64.ko
        ## INSTALL arch/x86/crypto/cast6-avx-x86_64.ko
        ## INSTALL arch/x86/crypto/des3_ede-x86_64.ko
        ## INSTALL arch/x86/crypto/sha1-mb/sha1-mb.ko
        ## ...
        #
        # note that:
        #
        # - INSTALLKERNEL: shortcircuit kernel installer, not needed,
        #   since we won't boot it in the machine doing the building
        #
        # - LILO will not we found, we don't care -- we only want the
        #   files in rootdir/
        commonl.makedirs_p(rootdir + "/boot")
        output = subprocess.check_output(
            "${MAKE:-make} -C %s INSTALLKERNEL=ignoreme"
            " INSTALL_PATH=%s/boot INSTALL_MOD_PATH=%s"
            " install modules_install" % (builddir, rootdir, rootdir),
            shell = True, stderr = subprocess.STDOUT)
        target.report_pass("installed kernel to %s" % rootdir,
                           dict(output = output), dlevel = -2)

        target.report_pass("stripping debugging info")
        subprocess.check_output(
            "find %s -iname \*.ko | xargs strip --strip-debug" % rootdir,
            shell = True, stderr = subprocess.STDOUT)
        target.report_pass("stripped debugging info", dlevel = -1)
    
    def deploy_00(self, ic, target):
        # tell the deployment code to rsync our fake rootdir over the
        # /boot and /lib/modules/VERSION dirs in the target
        rootdir = os.environ.get("LK_ROOTDIR", self.tmpdir + "/root")
        target.deploy_linux_kernel_tree = rootdir
        self.deploy_image_args = dict(extra_deploy_fns = [
            tcfl.pos.deploy_linux_kernel ])
    
    def eval(self, ic, target):
        # power cycle to the new kernel
        target.shell.run("echo I booted", "I booted")
        output = target.shell.run("uname -a", output = True, trim = True)
        target.report_pass("uname -a: %s" % output.strip())

Execute the testcase with (where IMAGE is the name of a Linux OS image installed in the server):

$ mkdir -p build
$ cp CONFIGFILE build/.config
$ make -C PATH/TO/SRC/linux O=build oldconfig
$ make -C build -j all

$ LK_ROOTDIR=$PWD/build IMAGE=clear tcf run -v /usr/share/tcf/examples/test_linux_kernel.py
INFO1/ormorh    ..../test_linux_kernel.py#_test @3hyt-uo3g: will run on target group 'ic=localhost/nwa target=localhost/qu04a:x86_64'
PASS1/ormorhB   ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/qu04a: re/building kernel in /home/inaky/t/gp/build-linux
PASS0/ormorhB   ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/qu04a: re/built kernel in /home/inaky/t/gp/build-linux
PASS1/ormorhB   ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/qu04a: installing kernel to /tmp/tcf.run-X2SMmK/ormorh/root
PASS0/ormorhB   ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/qu04a: installed kernel to /tmp/tcf.run-X2SMmK/ormorh/root
PASS2/ormorhB   ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/qu04a: output: Cannot find LILO.
PASS2/ormorhB   ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/qu04a: stripping debugging info
PASS1/ormorhB   ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/qu04a: stripped debugging info
PASS1/ormorh    ..../test_linux_kernel.py#_test @3hyt-uo3g: build passed 
INFO3/ormorhD   ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/nwa: Powering on
INFO2/ormorhD   ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/nwa: Powered on
INFO3/ormorhDPOS  ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/qu04a: POS: rebooting into Provisioning OS [0/3]
INFO3/ormorhDPOS  ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/qu04a: POS: setting target to PXE boot Provisioning OS
...
INFO3/ormorhDPOS  ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/qu04a: POS: rsynced clear:live:29100::x86_64 from 192.168.97.1::images to /dev/sda5
...
PASS3/ormorhDPOS  ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/qu04a: linux kernel transferred
INFO3/ormorhDPOS  ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/qu04a: POS: configuring bootloader
...
PASS2/ormorh    ..../test_linux_kernel.py#_test @3hyt-uo3g: deploy passed 
INFO3/ormorhE#1         ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/nwa: Powering on
INFO2/ormorhE#1         ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/nwa: Powered on
...
INFO2/ormorhE#1         ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/qu04a: wrote 'echo I booted' to console 'localhost/qu04a:<default>'
PASS3/ormorhE#1         ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/qu04a: found expected `I booted` in console `localhost/qu04a:default` at 0.01s
PASS2/ormorhE#1         ..../test_linux_kernel.py#_test @3hyt-uo3g|localhost/qu04a: eval passed: found expected `I booted` in console `localhost/qu04a:default` at 0.01s
...
PASS1/ormorh    ..../test_linux_kernel.py#_test @3hyt-uo3g: evaluation passed 
INFO0/ormorh    ..../test_linux_kernel.py#_test @3hyt-uo3g: WARNING!! not releasing targets
PASS0/            toplevel @local: 1 tests (1 passed, 0 error, 0 failed, 0 blocked, 0 skipped, in 0:03:18.911685) - passed 

(depending on your installation method, location might be ~/.local/share/tcf/examples)

4.1.3.8. Build, install and boot a QEMU machine with modified BIOS

Given a QEMU target that can be provisioned with Provisioning OS, build a UEFI BIOS with the vendor field modified, provision the OS and the new BIOS, boot into it and verify via dmidecode that the BIOS reports the new vendor information.

Builds on deploying an OS to a target and deploying files to a target. You can find the list of OS images installed in the server.

This demonstrates how TCF can be used to create a fast development cycle for working on changes to the BIOS code; for using any other machine than QEMU, something with a images interface that can flash the BIOS can be used.

TIPS:

  • always use the same target (give -t 'nwX or TARGETX' so that the content is cached and each run doesn’t try to send the your built kernel to a new target but just the bare changes. See methods for doing this in the how-to section
  • if only the BIOS code is modified and there is no need to re-provision the OS, method disabled_deploy_50() can be renamed to deploy_50() to override the template inherited from tcfl.pos.tc_pos_base and skip the OS provisioning step.
class _test(tcfl.pos.tc_pos_base):
    def configure_00(self):
        if not 'EDK2_DIR' in os.environ:
            raise tcfl.tc.skip_e(
                "please export env EDK2_DIR pointing to path of "
                "configured, built or ready-to-build tree")
        self.builddir = os.environ["EDK2_DIR"]

    def build_00(self):
        # Modify the BIOS vendor string to showcase a change
        #
        # Backslashes here are killing us; the original C code is
        #
        ## #define TYPE0_STRINGS \
        ##   "EFI Development Kit II / OVMF\0"     /* Vendor */ \
        ##   "0.0.0\0"                             /* BiosVersion */ \
        ##   "02/06/2015\0"                        /* BiosReleaseDate */
        #
        # So we need to replace all in the Vendor string until the \0,
        # but we need to escape that \\ for both python and the
        # shell. bleh.
        self.shcmd_local(
            r"sed -i"
            " '/Vendor/s|.*\\\\0\"|\"I am the vendor now\\\\0\"|'"
            " '%s/OvmfPkg/SmbiosPlatformDxe/SmbiosPlatformDxe.c'"
            % self.builddir)

        #
        # Build the new BIOS
        #
        # I lifted the build instructions of the Fedora 29 spec file
        # and simplified to the max, but I only know them to work on
        # this git version; complain otherwise
        rev = subprocess.check_output(
            "git -C '%s' rev-parse HEAD" % self.builddir,
            shell = True)
        if rev.strip() != "cb5f4f45ce1fca390b99dae5c42b9c4c8b53deea":
            self.report_info(
                "WARNING!! WARNING!!! These build process only verified to"
                " workwith git version cb5f4f45ce, found %s" % rev,
                level = 0)
        env = dict(
            GCC5_X64_PREFIX = "x86_64-linux-gnu-",
            CC_FLAGS = "-t GCC5 -n 4 --cmd-len=65536 -b DEBUG --hash" ,
        )
        env['OVMF_FLAGS'] = "%(CC_FLAGS)s -FD_SIZE_2MB" % env

        self.report_pass("re/building BaseTools in %s" % self.builddir,
                         dlevel = -1)
        self.shcmd_local(
            "${MAKE:-make} -C '%s/BaseTools' -j4" % self.builddir)

        # there are warnings that otherwise kill the build
        self.shcmd_local(
            "sed -i -e 's/-Werror//' '%s/Conf/tools_def.txt'" % self.builddir)

        self.report_pass("re/building OVMF in %s" % self.builddir, dlevel = -1)
        self.shcmd_local(
            "cd %s && build $OVMF_FLAGS -a X64 -p OvmfPkg/OvmfPkgX64.dsc"
            % self.builddir)

        # Build/OvmfX64/DEBUG_GCC5/FV/OVMF_CODE.fd
        target.report_pass("built BIOS", dlevel = -1)

    def disabled_deploy_50(self, ic, target):
        # remove "disabled_" to override the method from the
        # tcfl.pos.tc_pos_base that flashes the OS--this makes the
        # scrip to only build, flash the bios, powercycle into the
        # installed OS and run the eval* steps--which works if you
        # know
        pass

    def deploy_90(self, target):
        # Flash the new BIOS before power cycling
        target.images.flash(
            {
                "bios" : os.path.join(
                    self.builddir,
                    "Build/OvmfX64/DEBUG_GCC5/FV/OVMF_CODE.fd")
            },
            upload = True)

    def eval(self, target):
        # power cycle to the new kernel
        target.shell.run("dmidecode -t bios", "Vendor: I am the vendor now2")
        target.report_pass("New BIOS is reporting via DMI/bios Vendor field")

Execute the testcase with (where IMAGE is the name of a Linux OS image installed in the server):

$ git clone https://github.com/tianocore/edk2 edk2.git
$ EDK2_DIR=$HOME/edk2.git IMAGE=clear tcf run -v /usr/share/tcf/examples/test_qemu_bios.py

(depending on your installation method, location might be ~/.local/share/tcf/examples)

4.1.4. Common patterns

4.1.4.1. Reporting subcases

A common execution pattern is that a testcase executes and produces results for multiple subcases.

In this example, given a target that can be provisioned with Provisioning OS, we create a fake testcase that generates ten subtestcase reports in individual logfiles called subN.log.

When the testcase is created, in the __init__() method, we would scan the test to discover the list of subcases that will unfold–in this example, we are faking it–this is important because it allows us to double check that all we expected to execute will.

In general, the name of the subcase is the name of the testcase plus a dot something (in this case we append .subN); then it is added to the subtestcase dictionary self.subtc by creating an instancel of tcfl.tc.subtc_c, which implements this pattern.

class _test(tcfl.pos.tc_pos_base):
    def __init__(self, name, tc_file_path, origin):
        tcfl.pos.tc_pos_base.__init__(self, name, tc_file_path, origin)

        # "scan" for subcases (in our case, we know they'll be sub0 to sub9)
        for i in range(3):
            subcase = "sub%d" % i
            self.subtc[subcase] = tcfl.tc.subtc_c(self.name + "." + subcase,
                                                  tc_file_path, origin, self)

    def eval_00(self, target):

        # Run our imaginary testcase in the target
        #
        # this script (our 'testcase') creates N files subX.log on
        # which the fist line is 0, 1 or 2 (pass, fail, error), a fake
        # summary and therest are a random made up log file we want to
        # report. Eg:
        #
        #   2
        #   Summary for subtest 5
        #   zsh syslinux zoneinfo hwdata file pixmaps drirc.d icons mime-packages
        #   gir-1.0 nano gtk-2.0 graphite2 libinput GConf misc vpnc bash-completion
        #   scan-view openldap security screen wayland cups opt-viewer znc pam.d
        #   libtool aclocal keyutils
        #
        # for a list of words that works everywhere, we just list /usr/share
        # `shuf` picks 30 random lines from input and `fmt` makes a
        # paragraph out of that.
        target.shell.run('for((n = 0; n < %d; n++));'
                         ' do ('
                         '  echo $((RANDOM %% 3)); '
                         '  echo Summary for subtest $n; '
                         '  /bin/ls /usr/share | shuf -n 30 | fmt'
                         ' ) > sub$n.log;'
                         'done' % len(self.subtc))

        # cat each log file to tell what happened? we know the log
        # file names, so we can just iterate in python -- in other
        # cases, we might have to list files in the target to find the
        # log files, or scan through a big log file that has
        # indications of where the output for one subcase start and
        # ends.
        for n in range(len(self.subtc)):
            subcase_name = "sub%d" % n
            output = target.shell.run('cat %s.log' % subcase_name,
                                      output = True, trim = True)
            # first line is result, parse it
            result, summary, log = output.split('\n', 2)
            # translate the result to a TCF result
            result = result.strip()
            if result == "0":
                _result = tcfl.tc.result_c(passed = 1)
            elif result == "1":
                _result = tcfl.tc.result_c(failed = 1)
            elif result == "2":
                _result = tcfl.tc.result_c(errors = 1)
            else:
                raise AssertionError("unknown result from command output '%s'"
                                     % result)

            # For each subcase's output, update the subcase report
            self.subtc[subcase_name].update(_result, summary, log)

Execute the testcase with (where IMAGE is the name of a Linux OS image installed in the server):

$ IMAGE=clear tcf run -v /usr/share/tcf/examples/test_subcases.py

(depending on your installation method, location might be ~/.local/share/tcf/examples)

where IMAGE is the name of a Linux OS image installed in the server.

4.1.5. Capturing data, doing SSH

4.1.5.1. Reproducing audio and capturing the output

Given a target from which we can record audio, play a beep sound, record it, and then compare with the original

The target selection for this test is any target that can be provisioned with Provisioning OS and supports capture of audio over capturer called front_astream, which is usually connected to the front audio output of the target.

The sound file is a beep, located in the examples/data subdirectory and is sent to the target during the deployment phase, after flashing the image.

When the OS boots, the beep.wav file is already in /home, ready to be played. The test starts recording the target’s audio output, plays the beep, and then downloads the recording.

@tcfl.tc.tags(ignore_example = True)
@tcfl.tc.interconnect('ipv4_addr', mode = 'all')
@tcfl.tc.target("pos_capable and capture:'front_astream:stream' "
                "and ic.id in interconnects")
class _test(tcfl.pos.tc_pos0_base):
    """
    Simple audio test

    Play a beep while capturing the audio output, ensure they match
    """

    image_requested = os.environ.get("IMAGE", 'clear:desktop')
    login_user = os.environ.get('LOGIN_USER', 'root')

    @tcfl.tc.serially()
    def deploy_00(self, target):
        # the format is still a wee bit pedestrian, we'll improve the
        # argument passing
        target.deploy_path_src = self.kws['srcdir'] + "/data/beep.wav"
        target.deploy_path_dest = "/home/"
        self.deploy_image_args = dict(extra_deploy_fns = [
            tcfl.pos.deploy_path ])
	
    def eval(self, target):
        target.capture.start("front_astream")
        target.shell.run("aplay -D front /home/beep.wav" )
        target.capture.get("front_astream",
                           self.report_file_prefix + "capture.wav")

Execute the testcase with (where IMAGE is the name of a Linux OS image installed in the server):

$ IMAGE=clear tcf run -vv /usr/share/tcf/examples/test_audio_capture.py

(depending on your installation method, location might be ~/.local/share/tcf/examples)

where IMAGE is the name of a Linux OS image installed in the server.

4.1.5.2. Enabling target’s SSH server and executing an SSH command

Given a target that can be provisioned with Provisioning OS, execute a command with SSH.

This allows to login via SSH, copy and rsync files around, etc.

@tcfl.tc.tags(ignore_example = True)
@tcfl.tc.interconnect('ipv4_addr')
@tcfl.tc.target("pos_capable")
class _test(tcfl.pos.tc_pos0_base):
    """
    Deploy files after installing OS
    """
    image_requested = os.environ.get("IMAGE", 'clear:desktop')
    login_user = os.environ.get('LOGIN_USER', 'root')

    @tcfl.tc.serially()
    def deploy_00(self, target):
        # the format is still a wee bit pedestrian, we'll improve the
        # argument passing
        # This could be a single path not necessarily a list of them
        target.deploy_path_src = [
            # send a directory tree, the one containing this file
            self.kws['srcdir'],
            # send just a file, ../README.rst
            os.path.join(self.kws['srcdir'], "..", "README.rst")
        ]

        target.deploy_path_dest = "/home/place"
        self.deploy_image_args = dict(extra_deploy_fns = [
            tcfl.pos.deploy_path ])
	
    def eval(self, target):
        target.shell.run("find /home -ls")
        # verify the file exists and is the same
        remote = target.shell.run(
            "md5sum < /home/place/examples/data/beep.wav",
            output = True, trim = True).strip()
        local = subprocess.check_output(
            "md5sum < %s" % self.kws['srcdir'] + "/data/beep.wav",
            shell = True).strip()
        if remote != local:
            raise tcfl.tc.failed_e("MD5 mismatch (local %s remote %s)"
                                   % (local, remote))
        self.report_pass("deployed file is identical to local!", level = 1)

Execute the testcase with (where IMAGE is the name of a Linux OS image installed in the server):

$ IMAGE=clear  tcf run -Dvvvt 'nwa or qu04a' tcf.git/examples/test_ssh_in.py
INFO1/l79r    .../test_ssh_in.py#_test @zsqj-uwny: will run on target group 'ic=SERVER/nwa target=SERVER/qu04a:x86_64'
PASS1/l79r    .../test_ssh_in.py#_test @zsqj-uwny: evaluation passed 
PASS0/        toplevel @local: 1 tests (1 passed, 0 error, 0 failed, 0 blocked, 0 skipped, in 0:00:42.127021) - passed 

(depending on your installation method, location might be ~/.local/share/tcf/examples)

Note that once you have enabled the SSH server, you can use in the script the functions enabled by the target.ssh interface. As well, as long as the target and the network are on, you can create a tunnel through the server to access remotely:

$ tcf acquire nwa qu04a
$ tcf power-on nwa qu04a

Find the IPv4 address:

$ tcf tcf list -vv qu04a | grep ipv4_addr
interconnects.nwa.ipv4_addr: 192.168.97.4

Establish a tunnel to the SSH port:

$ tcf tunnel-add qu04a 22 tcp 192.168.97.4
SERVERNAME:20250

SSH into the target:

$ ssh -p 20250 root@SERVERNAME
...

Similarly scp -P 20250 root@SERVERNAME:REMOTEFILE . or rsync over SSH.

Learn more about tunnels here.

4.1.5.3. Different options of accessing a target via SSH

Given a target that can be provisioned with Provisioning OS, setup its SSH server and run commands, copy files around.

Note that accessing a target over ssh with automation is not as straightforward as doing it by hand, since humans are way slower than automation. We also tend to assume passwords and keys are setup, hostnames availables and server started and ready to go. Those are the most common source of issues.

class _test(tcfl.pos.tc_pos_base):
    """
    Exercise different SSH calls with the SSH extension on a PC target
    that is provisioned to a Linux OS (Clear, by default)
    """
    image_requested = os.environ.get("IMAGE", 'clear:desktop')

    def eval_00_setup(self, ic, target):
        # setup the SSH server to allow login as root with no password
        tcfl.tl.linux_ssh_root_nopwd(target)
        target.shell.run("systemctl restart sshd")
        target.shell.run(		# wait for sshd to be ready
            "while ! curl -s http://localhost:22 | /usr/bin/fgrep SSH-2.0; do"
            " sleep 1s; done", timeout = 10)
        # Tell the tunnelling system which IP address to use
        # Note the client running this can't connect directly to the
        # DUT because the DUT is connected to an isolated
        # NUT. However, the server is connceted to the NUT and can
        # bridge us. With this we tell the tunneling system which ip
        # address to use.
        target.tunnel.ip_addr = target.addr_get(ic, "ipv4")

    def eval_01_run_ssh_commands(self, target):
        #
        # Run commands over SSH
        #
        # https://intel.github.io/tcf/doc/09-api.html?highlight=ssh#tcfl.target_ext_ssh.ssh.check_output
        #target.ssh._ssh_cmdline_options.append("-v")	# DEBUG login problems
        #target.ssh._ssh_cmdline_options.append("-v")	# DEBUG login problems
        output = target.ssh.check_output("echo hello")
        assert 'hello' in output

        # Alternative way to do it https://intel.github.io/tcf/doc/04-HOWTOs.html?highlight=ssh#linux-targets-ssh-login-from-a-testcase-client
        # by hand

        # create a tunnel from server_name:server_port -> to target:22
        server_name = target.rtb.parsed_url.hostname
        server_port = target.tunnel.add(22)
        output = subprocess.check_output(
            "/usr/bin/ssh -p %d root@%s echo hello"
            % (server_port, server_name),
            shell = True)
        # this is done behind the doors of TCF, it doesn't know that
        # it was run, so report about it
        target.report_info("Ran SSH: %s" % output)
        assert 'hello' in output

    def eval_02_call(self, target):
        self.ts = "%f" % time.time()
        if target.ssh.call("true"):
            self.report_pass("true over SSH passed")
        if not target.ssh.call("false"):
            self.report_pass("false over SSH passed")

    def eval_03_check_output(self, target):
        self.ts = "%f" % time.time()
        target.ssh.check_output("echo -n %s > somefile" % self.ts)
        self.report_pass("created a file with SSH command")

    def eval_04_check_output(self, target):
        output = target.ssh.check_output("echo example output")
        self.report_pass("SSH check_output returns: %s" % output.strip())

    def eval_05_copy_from(self, target):
        target.ssh.copy_from("somefile", self.tmpdir)
        with open(os.path.join(self.tmpdir, "somefile")) as f:
            read_ts = f.read()
            target.report_info("read ts is %s, gen is %s" % (read_ts, self.ts))
            if read_ts != self.ts:
                raise tcfl.tc.failed_e(
                    "The timestamp read from the file we copied from "
                    "the target (%s) differs from the one we generated (%s)"
                    % (read_ts, self.ts))
        self.report_pass("File created with SSH command copied back ok")

    def eval_06_copy_to(self, target):
        # test copying file relative to the script source
        target.ssh.copy_to('data/beep.wav')
        base_file = os.path.basename(__file__)	# this file
        target.ssh.copy_to(base_file)
        copied_file = os.path.join(self.tmpdir, base_file)
        target.ssh.copy_from(base_file, copied_file)

        orig_hash = commonl.hash_file(hashlib.sha256(), __file__)
        copied_hash = commonl.hash_file(hashlib.sha256(), copied_file)
        if orig_hash.digest() != copied_hash.digest():
            raise tcfl.tc.failed_e("Hashes in copied files changed")
        self.report_pass("Bigger file copied around is identical")


    def eval_07_tree_copy(self, target):
        copied_subdir = self.kws['tmpdir'] + "/dest"

        # Copy a tree to remote, then copy it back
        target.shell.run("rm -rf subdir")
        shutil.rmtree(copied_subdir, True)
        target.ssh.copy_to(self.kws['srcdir_abs'], "subdir", recursive = True)
        target.ssh.copy_from("subdir", copied_subdir, recursive = True)

        # Generate MD5 signatures of the python files in the same order
        local_md5 = self.shcmd_local(
            r"find %(srcdir)s -type f -iname \*.py"
            " | sort | xargs cat | md5sum").strip()
        copied_md5 = self.shcmd_local(
            r"find %(tmpdir)s/dest -type f -iname \*.py"
            " | sort | xargs cat | md5sum").strip()

        self.report_info("local_md5 %s" % local_md5, dlevel = 1)
        self.report_info("copied_md5 %s" % copied_md5, dlevel = 1)

        if local_md5 != copied_md5:
            local_list = self.shcmd_local(
                r"find %(srcdir)s -type f -iname \*.py | sort").strip()
            copied_list = self.shcmd_local(
                r"find %(tmpdir)s/dest -type f -iname \*.py | sort").strip()
            raise tcfl.tc.failed_e(
                "local and copied MD5s differ",
                dict(local_list = local_list, copied_list = copied_list))

        self.report_pass("tree copy passed")

    def teardown(self):
        tcfl.tl.console_dump_on_failure(self)

Execute the testcase with (where IMAGE is the name of a Linux OS image installed in the server):

$ tcf run -v /usr/share/tcf/examples/test_linux_ssh.py
INFO1/q4ux      ..../test_linux_ssh.py#_test @t7rd-4e2t: will run on target group 'ic=jfsotc11/nwk target=jfsotc11/nuc-70k:x86_64'
INFO1/q4uxDPOS  ..../test_linux_ssh.py#_test @t7rd-4e2t|jfsotc11/nuc-70k: POS: rsyncing clear:server:30590::x86_64 from 192.168.107.1::images to /dev/sda6
PASS1/q4ux      ..../test_linux_ssh.py#_test @t7rd-4e2t: evaluation passed
PASS0/  toplevel @local: 1 tests (1 passed, 0 error, 0 failed, 0 blocked, 0 skipped, in 0:02:20.841000) - passed

(depending on your installation method, location might be ~/.local/share/tcf/examples)

where IMAGE is the name of a Linux OS image installed in the server.

4.1.6. Keywords that are available to this testcase while running on a target

Any of the keywords reported here can be used in a testcase script, in multiple places of TCF configuration as Python templates with fields such as %(tc_name)s and in report templates.

class _test(tcfl.tc.tc_c):
    ...
    dev eval(self, target, target1):
        ...
        something = self.kws[KEYWORDZ]
        ...
        somethingelse = target.kws[KEYWORDY]
        ...
        andthis = target1.kws[KEYWORDZ]
        ...

4.1.6.1. Static testcases (no targets, run local)

Notice the test group values are slightly different between the multiple targets, the single target or no targets (static) cases.

@tcfl.tc.tags(build_only = True, ignore_example = True)
class _test(tcfl.tc.tc_c):
    def build(self):
        self.report_info("Keywords for testcase:\n%s"
                         % pprint.pformat(self.kws),
                         level = 0)

Execute the testcase with:

$ tcf run -vv /usr/share/tcf/examples/test_dump_kws.py
INFO0/vxmvB   /usr/share/tcf/examples/test_dump_kws.py#_test @localic-localtg: Keywords for testcase:
{'cwd': '/home/inaky/z/s/local',
 'runid': '',
 'srcdir': '../../../../../usr/share/tcf/examples',
 'srcdir_abs': '/usr/share/tcf/examples',
 'target_group_info': 'localic-localtg',
 'target_group_name': 'localic-localtg',
 'target_group_servers': '',
 'target_group_targets': '',
 'target_group_types': 'static',
 'tc_hash': 'vxmv',
 'tc_name': '/usr/share/tcf/examples/test_dump_kws.py#_test',
 'tc_name_short': '/usr/share/tcf/examples/test_dump_kws.py#_test',
 'tc_origin': '/usr/share/tcf/examples/test_dump_kws.py:46',
 'thisfile': '/usr/share/tcf/examples/test_dump_kws.py',
 'tmpdir': '/tmp/tcf.run-9tJyXx/vxmv',
 'type': 'static'}
PASS0/        toplevel @local: 1 tests (1 passed, 0 error, 0 failed, 0 blocked, 0 skipped, in 0:00:00.302539) - passed

(depending on your installation method, location might be ~/.local/share/tcf/examples)

4.1.6.2. Testcase using one target

Note the data offered for the target is a superse of the testcase’s augmented with all the target metadata exported by the server

@tcfl.tc.target()
@tcfl.tc.tags(build_only = True, ignore_example = True)
class _test(tcfl.tc.tc_c):
    def build(self, target):
        self.report_info("Keywords for testcase:\n%s"
                         % pprint.pformat(self.kws),
                         level = 0)
        target.report_info("Keywords for target 0:\n%s"
                           % pprint.pformat(target.kws),
                           level = 0)

Execute the testcase with:

$ tcf run -vv /usr/share/tcf/examples/test_dump_kws_one_target.py
INFO0/gcoyBwifr       /usr/share/tcf/examples/test_dump_kws_one_target.py#_test @localhost/qz31b-x86: Keywords for testcase:
{'cwd': '/home/inaky/z/s/local',
 'runid': '',
 'srcdir': '../../../../../usr/share/tcf/examples',
 'srcdir_abs': '/usr/share/tcf/examples',
 ...
 'target_group_targets': u'localhost/qz31b-x86:x86',
 'target_group_types': u'qemu-zephyr-x86',
 'tc_hash': 'gcoy',
 'tc_name': '/usr/share/tcf/examples/test_dump_kws_one_target.py#_test',
 'tc_name_short': '/usr/share/tcf/examples/test_dump_kws_one_target.py#_test',
 'tc_origin': '/usr/share/tcf/examples/test_dump_kws_one_target.py:50',
 'thisfile': '/usr/share/tcf/examples/test_dump_kws_one_target.py',
 'tmpdir': '/tmp/tcf.run-DmwH93/gcoy',
 'type': u'qemu-zephyr-x86'}
INFO0/gcoyBwifr       /usr/share/tcf/examples/test_dump_kws_one_target.py#_test @localhost/qz31b-x86: Keywords for target 0:
{u'board': u'qemu_x86',
 'bsp': u'x86',
 u'bsp_models': {u'x86': [u'x86']},
 u'bsps': {u'x86': {u'board': u'qemu_x86',
                    u'console': u'x86',
 ...
 u'interconnects': {u'nwb': {u'ic_index': 31,
                             u'ipv4_addr': u'192.168.98.31',
                             u'ipv4_prefix_len': 24,
                             u'ipv6_addr': u'fc00::62:1f',
                             u'ipv6_prefix_len': 112,
                             u'mac_addr': u'02:62:00:00:00:1f'}},
 u'interfaces': [u'power',
                 u'images',
                 u'console',
                 u'debug'],
 ...
 'url': u'https://localhost:5000/ttb-v1/targets/qz31b-x86',
 u'zephyr_board': u'qemu_x86',
 u'zephyr_kernelname': u'zephyr.elf'}
PASS0/        toplevel @local: 1 tests (1 passed, 0 error, 0 failed, 0 blocked, 0 skipped, in 0:00:00.302253) - passed

(depending on your installation method, location might be ~/.local/share/tcf/examples)

4.1.6.3. Testcase using two targets

Note n this case the target group names are listing two targets and each target obejct has different values.

@tcfl.tc.target()
@tcfl.tc.target()
@tcfl.tc.tags(build_only = True, ignore_example = True)
class _test(tcfl.tc.tc_c):
    def build(self, target, target1):
        self.report_info("Keywords for testcase:\n%s"
                         % pprint.pformat(self.kws),
                         level = 0)
        target.report_info("Keywords for target 0:\n%s"
                           % pprint.pformat(target.kws),
                           level = 0)
        target1.report_info("Keywords for target 1:\n%s"
                            % pprint.pformat(target1.kws),
                            level = 0)

Execute the testcase with:

$ tcf run -vv /usr/share/tcf/examples/test_dump_kws_twp_targets.py
INFO0/ato4B   /usr/share/tcf/examples/test_dump_kws_two_targets.py#_test @2psg: Keywords for testcase:
{'cwd': '/home/inaky/z/s/local',
 ...}
INFO0/ato4B   /usr/share/tcf/examples/test_dump_kws_two_targets.py#_test @2psg|localhost/qz33b-arm: Keywords for target 0:
{u'board': u'qemu_cortex_m3',
 'bsp': u'arm',
 u'bsp_models': {u'arm': [u'arm']},
 u'bsps': {u'arm': {u'board': u'qemu_cortex_m3',
                    u'console': u'arm',
                    u'kernelname': u'zephyr.elf',
 ...
 u'zephyr_board': u'qemu_cortex_m3',
 u'zephyr_kernelname': u'zephyr.elf'}
INFO0/ato4B   /usr/share/tcf/examples/test_dump_kws_two_targets.py#_test @2psg|localhost/qz31a-x86: Keywords for target 1:
{u'board': u'qemu_x86',
 'bsp': u'x86',
 u'bsp_models': {u'x86': [u'x86']},
 u'bsps': {u'x86': {u'board': u'qemu_x86',
                    u'console': u'x86',
                    u'kernelname': u'zephyr.elf',
                    u'quark_se_stub': False,
 ...
 u'zephyr_board': u'qemu_x86',
 u'zephyr_kernelname': u'zephyr.elf'}
PASS0/        toplevel @local: 1 tests (1 passed, 0 error, 0 failed, 0 blocked, 0 skipped, in 0:00:00.417956) - passed

(depending on your installation method, location might be ~/.local/share/tcf/examples)

4.1.7. Testcase drivers

Testcase drivers load and execute existing testcases.

4.1.7.1. Impromptu testcase driver to execute and report Yocto/OE ptest-runner testcases

This is a simple driver for executing Yocto/OE testcases, which are usually installed in the system already.

The testsuites available can be listed with:

ptest-runner -l
Available ptests:
acl   /usr/lib/acl/ptest/run-ptest
attr  /usr/lib/attr/ptest/run-ptest
bash  /usr/lib/bash/ptest/run-ptest
bluez5        /usr/lib/bluez5/ptest/
...

Each suite contains one or more subcases, so the layout is like:

  • TESTSUITE1 - SUBCASE1 - SUBCASE2 - SUBCASE2 - …
  • TESTSUITE2 - SUBCASE1 - …
  • TESTSUITE3 - SUBCASE1 - SUBCASE2 - …

Subcases will be reported as described in :ref:<example_subcases>, but because we have no way to discover the number of subcases (before or after provisioning, powering on and contacting the system), the total number of reported testcases will vary wildly if the system cannot be provisioned or powered on or if the testcase execution timesout.

Note this also servers as an example of an impromptu test driver.

To execute, the testcase.

  • Find and run all the testsuites installed in the target:

    $ IMAGE=yocto:core-image-sato-sdk-ptest     tcf run -v test_ptest_runner.py
    
  • run testsuites zlib and gzip only:

    $ IMAGE=yocto:core-image-sato-sdk-ptest     tcf run -v test_ptest_runner.py#zlib#gzip
    
  • run testsuites zlib and gzip on one side and valgrind and bash in another:

    $ IMAGE=yocto:core-image-sato-sdk-ptest     tcf run -v test_ptest_runner.py#zlib#gzip test_ptest_runner.py#valgrind#bash
    

(where IMAGE is the name of a Yocto Linux OS image installed in the server).

(depending on your installation method, location might be ~/.local/share/tcf/examples)

examples.test_ptest_runner.timeouts = {'bash': 120, 'lzo': 60, 'mdadm': 240, 'valgrind': 300}

Timeouts per suite name (in seconds)

When the default is too short, or needs ajustment

examples.test_ptest_runner.timeout_default = 30

Default timeout (seconds) to execute a ptest

4.1.8. Creating a file in the target from the command line

The following Unix shell construct:

$ cat <<EOF > somefile
line1
line2
line3
EOF

can also be done in a TCF script:

but since the shell console is actually typing the characters, it is slightly more reliable to use:

x04 is the ASCII end-of-transmission character, Ctrl-D. This is the equivalent of typing the file contents on the command line.

4.2. TCF client tricks

4.2.1. Where is the TCF client configuration taken from?

tcf reads configuration files from (in this order):

  • .tcf (a subdirectory of the current working directory)
  • ~/.tcf
  • ~/.local/etc/tcf (if installed in user’s home only with python setup.py install –user or pip install –user)
  • /etc/tcf (if installed globally, eg with a package manager)

Configuration files are called conf_WHATEVER.py and imported in alphabetical order from each directory before proceeding to the next one. They are written in plain Python code, so you can do anything, even extend TCF from them. The module tcfl.config provides access to functions to set TCF’s configuration.

You can add new paths to parse with --config-path and force specific files to be read with --config-file. See tcf –help.

4.2.2. How do I list which targets I own?

Run:

$ tcf list -v 'owner:"MYNAME"'

MYNAME is whatever identifier you used to login.

4.2.3. How do I release a target I don’t own?

Someone owns the target and they have gone home…:

$ tcf release -f TARGETNAME

But this only works if you have admin permissions.

The exception is if you have locked yourself the target with a ticket (used by tcf run and others so that the same user running different processes in parallel can still exclude itself from overusing a target). It will say something like:

requests.exceptions.HTTPError: 400: TARGETNAME: tried to use busy target (owned by 'MYUSERNAME:TICKETSTRING')

As a user, you can always force release any of your own locks with -f or with -t TICKETSTRING:

$ tcf -t TICKETSTRING release TARGETNAME

4.2.4. How do I release all the targets I own?

Run:

$ tcf release -f $(tcf list 'owner:"MYNAME"')
  • MYNAME is whatever identifier you used to login
  • tcf list ‘owner:”MYNAME”’ lists which targets you currently own

4.2.5. How do I keep a target acquired/reserved after tcf run is done?

Giving –no-release to tcf run will keep the target acquired after the scrip execution concludes. Note however that it will be acquired by USERNAME::term:`HASHID` (what’s a hashid?).

For example, if we were using targets nwa and qu04a to boot in provisioning mode, the hashid could be ormorh:

$ tcf run --no-release -vvt 'nwa or qu04a' /usr/share/examples/test_pos_boot.py
...
INFO1/ormorh    ..../test_pos_boot.py#_test @3hyt-uo3g: will run on target group 'ic=localhost/nwa target=localhost/qu04a:x86_64'
...

note how the hashid is ormorh in this case and thus, upon completion:

$ tcf list -v owner
localhost/nwa [USERNAME:ormorh] ON
localhost/qu04a [USERNAME:ormorh] ON

to maintain the target acquired and powered while potentially debugging or testing other things, use a while loop which keeps acquiring with the same hashid. This tells the daemon we are actively using the target and won’t release for us. In a separate console, run:

$ while tcf -t ormorh acquire nwa or qu04a; do sleep 10s; done

now you can access the console, do captures or interact with the target in any other way, remembering to specify the ticket:

$ tcf -t ormorh console-write -i qu04a
$ tcf -t ormorh capture-get qu04a screen screencap.png
...

or via SSH, first we have to ask the server to create a tunnel for us from to the target’s SSH port:

$ tcf list -vv qu04a | grep ipv4_addr
interconnects.nwa.ipv4_addr: 192.168.97.4
$ tcf tunnel-add qu04a 22 tcp 192.168.97.4
SERVERNAME:19893
$ ssh -p 19893 root@SERVERNAME

Note

make sure you specify the user to login as; it likely won’t be the same as in your client machine.

Release the target so it can be used by someone else:

$ kill -9 %1               # kill the while loop that keeps it acquired
$ tcf release -f $(tcf list 'owner:"YOURNAME"')

Some details:

  • use while tcf acquire vs while true; do tcf acquire because that way, if the server fails, connection drops (eg: you close your laptop), then the process tops and won’t restart unless you do it manually.
  • if you depend on the network, do not forget to also acquire the network, otherwise it will be powered off and routing won’t work.

4.2.5.1. Alternative method without hashids

First make sure you are not blocking anyone:

$ while tcf acquire TARGET1 TARGET2 TARGET3...; do sleep 15s; done &
$ tcf_pid=$!

this keeps acquiring the target every 10 seconds, which tells the server you are actively using it, so it won’t release them nor power them off.

When done:

$ kill $tcf_pid
$ tcf release TARGET1 TARGET2 TARGET3...

You can tcf run without releasing them:

$ tcf -t " " run --no-release -vvt "TARGET1 or TARGET2 or TARGET3 ..." test_mytc.py

Note the following:

  • -t " " tells TCF to use no ticket, as we have made the reservation with no ticket
  • --no-release tells TCF to not release the targets when done

this way, once the test completes (let’s say, with failure), we can log in via the serial console to do some debugging:

$ tcf console-write -i TARGET2

Do not forget to kill the while process and release the targets when done, otherwise others won’t be able to use them. If someone has left a target taken, it can be released following these instructions

4.2.6. How can I debug a target?

TCF provides for means to connect remote debuggers to targets that support them; if the target supports the debug interface (which you can find with tcf list -vv TARGETNAME | grep interfaces).

How it is done and what are the capabilities depends on the target, but in general, assuming you have a target with an image deployed:

$ tcf acquire TARGETNAME
$ tcf debug-start TARGETNAME
$ tcf power-on TARGETNAME
$ tcf debug-info TARGETNAME
GDB server: tcp:myhostname:3744 (when target is on; currently ON)

at this point, the target is waiting for the debugger to connect before powering up, so start (in this case) GDB pointing it to the elf version of the image file uploaded and issue:

gdb> target remote tcp:myhostname:3744

Some targets might support starting debugging after power up.

Find more:

4.2.6.1. Zephyr debugging with TCF run

When using targets in a high usage environment, it is easier to use TCF run and a few switches:

  • Make sure the target is acquired for a max of 30min while you work with it:

    $ for ((count = 0; count < 240; count++)); do tcf -t 1234 acquire TARGETNAME; sleep 15s; done &
    

    be sure to kill this process when done, to free it for other people; every 15s this re-acquires, as a way to tell the daemon you are still using it, to not free it from you.

    Note -t 1234; this says use ticket 1234 to reserve this target; we’ll use it later.

  • Create a temporary directory:

    $ mkdir tmp
    
  • Build and deploy:

    $ tcf -t 1234 run -vvvv -E --tmpdir tmp -t TARGETNAME --no-release PATH-TO-TC
    

    note the following:

    • -t 1234 says to use ticket 1234, as the one we used for the reservation

    • -E tells it not to evaluate – it will just build and deploy / flash

    • –no-release says do not release the target when done (because you want to do other stuff, like debug)

    • In the case of Zephyr, the ELF file will be in tmp/1234/outdir-1234-SOMETHING/zephyr/zephyr.elf, which you can find with:

      $ find tmp/1234/ -iname zephyr.elf
      tmp/1234/outdir-1234-j38h-quark_d2000_crb/zephyr/zephyr.elf
      
  • Tell the target to start debugging:

    $ tcf -t 1234 debug-start TARGETNAME
    
  • Now reset / power cycle it, so it goes fresh to start. Because we told it to start debugging, it will start but stop the CPU until you attach a debugger (only for OpenOCD targets or targets that support debugging, anyway):

    $ tcf -t 1234 reset TARGETNAME
    $ tcf -t 1234 debug-info TARGETNAME
    OpenOCD telnet server: srrsotc03.iind.intel.com 20944
    GDB server: x86: tcp:srrsotc03.iind.intel.com:20946
    Debugging available as target is ON
    
  • Note we now can start the debugger; find it first:

    $ find /opt/zephyr-sdk-0.9.5/ -iname \*gdb
    ...
    /opt/zephyr-sdk-0.9.5/sysroots/x86_64-pokysdk-linux/usr/bin/i586-zephyr-elf/i586-zephyr-elf-gdb
    ...
    

    run the debugger to the ELF file we found above:

    $ /opt/zephyr-sdk-0.9.5/sysroots/x86_64-pokysdk-linux/usr/bin/i586-zephyr-elf/i586-zephyr-elf-gdb \
        tmp/1234/outdir-1234-j38h-quark_d2000_crb/zephyr/zephyr.elf
    ...
    Reading symbols from tmp/1234/outdir-1234-j38h-quark_d2000_crb/zephyr/zephyr.elf...done.
    

    tell the debugger to connect to the GDB server found by running debug-info before:

    (gdb) target remote tcp:srrsotc03.iind.intel.com:20946
    Remote debugging using tcp:srrsotc03.iind.intel.com:20946
    0x0000fff0 in ?? ()
    

    Debug away!:

    (gdb) b _main
    Breakpoint 1 at 0x180f71: file /home/inaky/z/kernel.git/kernel/init.c, line 182.
    (gdb) c
    Continuing.
    target running
    redirect to PM, tapstatus=0x08302c1c
    hit software breakpoint at 0x00180f71
    
    Breakpoint 1, _main (unused1=0x0, unused2=0x0, unused3=unused3@entry=0x0) at /home/inaky/z/kernel.git/kernel/init.c:182
    182 {
    (gdb)
    ...
    
  • on a separate terminal, you can:

    • read the target’s console output with:

      $ tcf console-read --follow TARGETNAME
      
    • issue CPU resets, halts, resumes or OpenOCD commands (for targets that support it):

      $ tcf debug-reset TARGETNAME
      $ tcf debug-halt TARGETNAME
      $ tcf debug-resume TARGETNAME
      $ tcf debug-openocd TARGETNAME OPENOCDCOMMAND
      

Note that resetting or power-cycling the board will create a new GDB target with a different port, so you will have to reconnect that GDB wo the new target remote reported by tcf debug-info.

4.2.7. How can I quickly flash a Linux target

When your server and targets are configured for Provisisoning OS support (target exports the pos_capable tag in tcf list -vv TARGET), you can quickly flash the target target1A, which is connected to network nwA with:

$ IMAGE=fedora::29 tcf run -vvvt 'nwA or target1A' /usr/share/tcf/examples/test_pos_deploy.py

to find our which images your server has available

To find out available images:

$ tcf run /usr/share/tcf/examples/test_pos_list_images.py
server10/nwa clear:live:25550::x86_64
server10/nwa clear:live:25890::x86_64
server10/nwa fedora::29::x86_64
server10/nwa yocto:core-minimal:2.5.1::x86_64
PASS0/        toplevel @local: 1 tests (1 passed, 0 error, 0 failed, 0 blocked, 0 skipped, in 0:00:06.635452) - passed

See how to install more images.

4.2.8. How can I quickly build and deploy an image to a Zephyr target?

You can use the boilerplate testcase test_zephyr_boots.py, which will build any Zephyr app and try to boot it and see if it prints the Zephyr boot banner:

$ ZEPHYR_APP=path/to/source tcf run -t TARGETNAME /usr/share/tcf/examples/test_zephyr_boots.py
$ tcf acquire TARGENAME
$ <work on it> ...

TCF will build your code configuring it properly for the chosen target and deploy it. You want to inmediately acquire so it is not powered-off by the daemon.

4.2.9. TCF’s run says something failed, can I get more detail?

FIXME: update

TCF’s run tries to be quiet to the console, so when you run a lot of tests on a lot of targets, the forest lets you see the trees.

When you need more detail, you can:

  • add -vs after run (but then it gives you detail everywhere)

  • log to a file with –log-file=FILENAME and when something fails, grep for it:

    FAIL0/kaoe ../Makefile[quark]@.../frankie: (dynamic) build failed
    PASS1/tctp ../Makefile[x86]@.../mv-03: (dynamic) build passed
    

    that build failed; take those four letters next to the FAIL0 message (kaoe) – that’s a unique identifier for each message, and look for it with grep, printing 30 lines of context before the match:

    $ grep -B 100 kaoe FILENAME
    ....
    FAIL3/iigx ../Makefile[quark]@.../frankie: @build failed [2] ('make -j -C samples/hello_world/nanokernel BOARD=quark_se_sss_ctb  O=outdir-httpsSERVER5000ttb-v0targetsfrankie-quark_se_sss_ctb-quark_se_sss_ctb' from /home/inaky/z/kernel.git/samples/.tcdefaults:48)
    FAIL3/iigx ../Makefile[quark]@.../frankie: output: FF make: Entering directory '/home/inaky/z/kernel.git/samples/hello_world/nanokernel'
    FAIL3/iigx ../Makefile[quark]@.../frankie: output: FF make[1]: Entering directory '/home/inaky/z/kernel.git'
    ...
    

    most likely, the complete failure message will be right before the final failure message – and you can now tell what happened. In this case, there is no good configuration for the chosen target

    The output driver can be changed to lay out the information diferently; look at more information on report drivers.

4.2.10. Linux targets: Common tricks

4.2.10.1. Linux targets: sending Ctrl-C to a target

Trick over the serial console is that it is a pure pipe, there is no special characters. So a quick way to do it is:

$ tcf console-write TARGETNAME $(echo -e \\x03)

where that \x03 is the hex code of Ctrl-C. man ascii can tell you the quick shortcuts for others.

4.2.10.2. Linux targets: running a Linux shell command

Try:

$ tcf console-write TARGETNAME "ping -c 3 localhost"

Note that once the command is sent, the console, for whatever the target cares, is still connected, even if the console-write command returned for you. The command might still be executing; see Sending a Ctrl-C is not as in a usual, synchronous, Linux console.

From a script, you can use tcfl.tc.target_c.shell.run or tcfl.tc.target_c.send():

...
@tcfl.tc.target("linux")
class some_test(tcfl.tc.tc_c):

    def eval_something(self, target):
        ...
        target.shell.send("ping localhost")
        target.shell.expect("3 packets transmitted, 3 received")
        ...
        # better to use
        ...
        target.shell.run("ping -c 3 localhost",
                         "3 packets transmitted, 3 received")

you can also get the output by adding output = True:

...
output = target.shell.run("ping -c 3 localhost",
                          "3 packets transmitted, 3 received",
                          output = True)
...

4.2.10.3. Linux targets: ssh login from a testcase / client

The ttbd server can create tunnels that allow you to reach the target’s ports, assuming the target is

  • connected to a network to which the server is also connected
  • on and listening on a port

In your test scripts, use the tunnel extension to create a port redirection, adding to your script:

...
@tcfl.tc.interconnect("ipv4_addr")
@tcfl.tc.target(...)
class some_test(tcfl.tc.tc_c):

    def eval_something(self, ic, target):
        ...
        # ensure target and interconnect is powered up and the
        # script is logged in.
        # Indicate to the tunnel system the target's address in the
        # interconnect
        target.tunnel.ip_addr = target.addr_get(ic, "ipv4")

        # create a tunnel from server_name:server_port -> to target:22
        server_name = target.rtb.parsed_url.hostname
        server_port = target.tunnel.add(22)

        # use SSH to get the content's of the target's /etc/passwd
        output = subprocess.check_output(
                     "ssh -p %d root@%s cat /etc/passwd"
                     % (server_port, server_name),
                     shell = True)

Tunnels can also be created with the command line:

$ tcf tunnel-add TARGETNAME 22 tcp TARGETADDR
SERVERNAME:19893
$ ssh -p 19893 root@SERVERNAME cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
...

Note you might need first the steps in the next section to allow SSH to login with a passwordless root.

4.2.10.4. Linux targets: restarting the SSH daemon

See the examples in tcfl.tl.linux_ssh_root_nopwd().

4.2.10.5. How do I boot a target to PXE boot mode manually?

A target can be told to boot to PXE Provisioning OS by issuing the following commands:

$ while tcf acquire NETWORKNAME TARGETNAME; do sleep 10s; done &
$ tcf power-on NETWORKNAME
$ tcf property-set TARGETNAME pos_mode pxe
$ tcf power-cycle TARGETNAME

The while loop in the background keeps the target acquired, do not forget to release them by killing it. Then we ensure the network is on and finally, we set the target to POS mode PXE and we power cycle the target.

To have it boot back in local mode:

$ tcf property-set TARGETNAME pos_mode
$ tcf power-cycle TARGETNAME

4.2.10.6. POS: booting from HTTP or TFTP

When a target is given a syslinux configuration file to boot from, the places where it loads the Provisioning OS kernels can be forced with pos_http_url_prefix.

By default (no prefix) the boot loader will tend to load with TFTP. But by specifying an HTTP or FTP URL prefix, it can boot over any of those protocols (which can be faster).

When specified in an interconnect’s tags, it will be taken as default for that interconnect, but this can be overriden specifying it for each target; for the example in POS: Configuring networks, we can add:

pos_http_url_prefix = "http://192.168.97.1/ttbd-pos/%(bsp)s/"

This will replace in the syslinux configuration file any occurrence of pos_http_url_prefix with http://192.168.97.1/ttbd-pos/ARCHNAME/, where ARCHNAME is the architecture of the target.

4.2.10.7. Linux targets: POS: setting default linux kernel options

When a Linux target is botted using POS, default kernel options can be fed to the POS scripts for bootloader consumption by setting the following tags or properties (all optional):

  • linux_serial_console_default: sets which is the default serial console. For /dev/NAME, specify NAME, as this will be given as the Linux kernel command line option console=NAME,115200
  • linux_options_append: a space separated string with any other Linux kernel command line options to add.

For example, when adding the target:

>>> ttbl.config.target_add(
>>>     ...
>>>     tags = {
>>>         ...
>>>         'linux_serial_console_default': 'ttyS2',
>>>         'linux_options_append': 'rw foo=bar',
>>>         ...
>>>     })

which can also be done once the target is added with tags_update:

>>> ttbl.config.targets['TARGETNAME'].tags_update({
>>>         ...
>>>         'linux_serial_console_default': 'ttyS2',
>>>         'linux_options_append': 'rw foo=bar',
>>>         ...
>>>     })

4.2.10.8. Linux targets: using proxies

NUTs to which targets are connected are usually setup very isolated from upstream or other networks; there is a common practice to declare a proxy availability in an interconnect by it exporting any of the following variables:

$ tcf list -vv nwb | grep -i proxy
ftp_proxy: http://192.168.98.1:911
http_proxy: http://192.168.98.1:911
https_proxy: http://192.168.98.1:911

so in a test script running a Linux target, one could do:

@tcfl.tc.interconnect("ipv4_addr")
@tcfl.tc.target('pos_capable')
class _test(tcfl.tc.tc_c):

    def eval_something(self, ic, target):
        ...
        if 'http_proxy' in ic.kws:
           target.shell.run("export http_proxy=%s" % ic.kws.get('http_proxy'))
           target.shell.run("export HTTP_PROXY=%s" % ic.kws.get('http_proxy'))
        if 'https_proxy' in ic.kws:
           target.shell.run("export https_proxy=%s" % ic.kws.get('https_proxy'))
           target.shell.run("export HTTPS_PROXY=%s" % ic.kws.get('https_proxy'))
        ...

note however, that those settings will apply only to the shell being run in that console. You can make more permanent settings in the target by for example, modifying /etc/bashrc:

@tcfl.tc.interconnect("ipv4_addr")
@tcfl.tc.target('pos_capable')
class _test(tcfl.tc.tc_c):

    def eval_something(self, ic, target):
        ...
        if 'http_proxy' in ic.kws:
           target.shell.run("echo http_proxy=%s >> /etc/bashrc" % ic.kws.get('http_proxy'))
           target.shell.run("echo HTTP_PROXY=%s >> /etc/bashrc" % ic.kws.get('http_proxy'))
        if 'https_proxy' in ic.kws:
           target.shell.run("echo https_proxy=%s >> /etc/bashrc" % ic.kws.get('https_proxy'))
           target.shell.run("echo HTTPS_PROXY=%s >> /etc/bashrc" % ic.kws.get('https_proxy'))
        ...

and those will also apply if your script logs in via SSH or other methods.

4.2.10.9. Linux targets: removing the root password

If your target is not connected to any networks or to an isolated network, you can remove the root password.

...
@tcfl.tc.interconnect("ipv4_addr")
@tcfl.tc.target("linux")
class some_test(tcfl.tc.tc_c):

    # ensure target is powered up and the script is logged in
    def eval_something(self, ic, target):
        ...
        target.shell.run("passwd -d root")
        ...

or from the console:

$ tcf console-write TARGETNAME "passwd -d root"
$ tcf console-read TARGETNAME
...
# passwd -d root
Removing password for user root.
passwd: Success

4.2.10.10. Linux targets: allowing SSH as root with no passwords

Most Linux deployments default configure SSH to be very conservative; for testing, you might want to open it up.

To allow login in with SSH, add to your test script:

...
@tcfl.tc.interconnect("ipv4_addr")
@tcfl.tc.target("linux")
class some_test(tcfl.tc.tc_c):

    # ensure target is powered up and the script is logged in
    def eval_something(self, ic, target):
        target.shell.run("""\
cat <<EOF >> /etc/ssh/sshd_config
PermitRootLogin yes
PermitEmptyPasswords yes
EOF""")
        target.shell.run("systemctl restart sshd")

or using the library function tcfl.tl.linux_ssh_root_nopwd:

import tcfl.tl
...
@tcfl.tc.interconnect("ipv4_addr")
@tcfl.tc.target("linux")
class some_test(tcfl.tc.tc_c):

    # ensure target is powered up and the script is logged in
    def eval_something(self, ic, target):
        ...
        tcfl.tl.linux_ssh_root_nopwd(target)
        ...
        target.shell.run("systemctl restart sshd")

or even having that done in deployment time when flashing with POS:

@tcfl.tc.interconnect("ipv4_addr")
@tcfl.tc.target('pos_capable', mode = 'any')
class _test(tcfl.tc.tc_c):

    def deploy(self, ic, target):
        # ensure network, DHCP, TFTP, etc are up and deploy
        ic.power.on()
        ic.report_pass("powered on")

        image = target.pos.deploy_image(
            ic, "clear",
            extra_deploy_fns = [ tcfl.pos.deploy_linux_ssh_root_nopwd ])

or from the shell:

$ tcf console-write TARGETNAME "echo PermitRootLogin yes >> /etc/ssh/sshd_config"
$ tcf console-write TARGETNAME "echo PermitEmptyPasswords yes >> /etc/ssh/sshd_config"

4.2.11. How do I change the default timeout in my test scripts

The default timeout different parts of the tcf run engines wait for the target to respond can be changed by setting the variable self.tls.expecter.timeout (note self is a testcase class):

class some_tc(tcfl.tc.tc_c):
    ...
    def eval_some(self):
        # wait a max of 40 seconds
        self.tls.expecter.timeout = 40
        ...

It is a bit awkward and we’ll make a better way to do it. Other places that take a timeout parameter that has to be less than self.tls.expecter.timeout:

4.2.12. Making the client always generate report files

tcf run will normally generate a report file if a testcase does not pass. If you want report files generated always, you can add to any configuration file:

tcfl.report_jinja2.driver.templates['text']['report_pass'] = True

Reporting is handled by the reporting API and the report files are created by the Jinja2 reporter based on a template called text.

4.2.13. Splitting report files by domain

You could want to break your testcases by a domain, mapping to any categories and you can want the report files to be stored in specific subdirectories.

You can define a hook that calculates that domain and generates metadata for it, so the templating engine can use it

>>> ...
>>> tcfl.tc.tc_c.hook_pre.append(_my_hook_fn)
>>> filename = tcfl.report_jinja2.driver.templates["text"]["output_file_name"]
>>> tcfl.report_jinja2.driver.templates["text"]["output_file_name"] = \
>>>     "%(category)s/" + file_name

The _my_hook_fn() would look as:

>>> def _my_hook_fn(testcase):
>>>     # define some calculations that generates category
>>>     testcase.tag_set("category", categoryvalue)

If the data needed is not available until after the testcase executes, you can use reporting hooks.

4.2.14. Capturing network traffic

TCF servers (ttbd) can capture the traffic in a :term:NUT network if they are connected to it.

For this to happen, when the network is powered up, it must contain a property called tcpdump set to a file name where to capture it:

$ tcf property-set nwb tcpdump somename.cap
$ tcf power-cycle nwb

when all the network traffic is done, it can be downloaded:

$ tcf power-off nwb
$ tcf store-dnload nwb somename.cap local_somename.cap

which now can be opened with wireshark to see what happened (or analyzed with other tools).

In a script, ensure your start routine contains:

>>> class sometest(tcfl.tc.tc_c):
>>>
>>>     def start_something(self, ic, ...):
>>>         ...
>>>         # before powering up the interconnect
>>>         ic.property_set('tcpdump', self.kws['tc_hash'] + ".cap")
>>>         ...
>>>         ic.power.cycle()

and on teardown:

>>>
>>>     def teardown_whatever(self, ic, ...):
>>>         ic.store.dnload(
>>>              self.kws['tc_hash'] + ".cap",
>>>              "report-%(runid)s:%(tc_hash)s.tcpdump" % self.kws)
>>>         self.report_info("tcpdump available in file "
>>>                          "report-%(runid)s:%(tc_hash)s.tcpdump" % self.kws)

the file will be made available in the same directory where tcf run was executed from.

4.2.15. Continuous Integration

TCF run can be used in a CI system to run testcases as part of the continuous integration process. A few helpful tricks:

4.2.15.1. Generate a unique ID for each run and feed it to TCF

It is common practice to generate a unique ID for each build or continuous integration run. It should include:

  • timestamp (YYMMDD-HHMM): allows to tell when the build happened and to map to logs in other parts of the system; might not be sufficient if more than one build can be started in the same hour/minute/second
  • monotonic counter (BBB): CI engines like Jenkins will refer to builds by their internal build monotonic counter–it also helps distinguish in the unlikely case two builds were started on the same second (or minute)
  • branch/project identifier (PROJECT-BRANCH): if a single build might be running on multiple branches or projects, it helps to add a short version of it – thus, when the Unique ID is propagated to other parts of the CI system, we can see who is causing whatever action.

tcf run supports the concept of a RunID, which will be then used in all the reports.

A good RunID specification for TCF run would be something like:

PROJECT-BRANCH-YYMMDD-HHMM-BBB

it is a good idea to also give it to the hashing engine as salt so that the hash identifiers used to acquire targets don’t conflict with other projects that might be using the same testcase. e.g.:

$ tcf run --hash-salt PROJECT-BRANCH-YYMMDD-HHMM-BBB \
    --runid PROJECT-BRANCH-YYMMDD-HHMM-BBB -v path/to/testcases

add to the hash salt any other factors that might contribute to the same testcase/target combination being run as the same but that shall be considered different (eg: using a different toolchain).

4.2.15.2. Splitting in multiple shards

When running CI in multiple slaves in parallel, the CI engine can tell tcf run to only run an specific shard of the whole list of testcases. Assuming all the slaves have the same list of testcases, the list will be evenly split:

slave1$ tcf run --shard 1-3 --runid X path/to/testcases
slave2$ tcf run --shard 2-3 --runid X path/to/testcases
slave3$ tcf run --shard 3-3 --runid X path/to/testcases

will split the deck of testcases in 3 shards and run one on each slave in parallel.

Note that if the availability of targets to run the shards doesn’t allow them to run testcases in parallel, you might not gain much by the paralallelization of tcf run.

4.2.15.3. Controlling output location

tcf run can be given --log-dir to specify the location where most default output files will be placed, including:

  • failure/error/block/skip reports
  • tcpdump outputs

this defaults to the directory where tcf run was invoked from.

4.3. General catchas

Some common issues that make it automating hard

4.3.1. Hidden characters in console output, ANSI menus

This happens very commonly when console prompts or text produce ANSI escape sequences to colorize output; as a human on the console, we see clearly the root@hostname:DIR prompt, but our regex for the console is expecting:

root@SOMETHING:DIR

however, the chain of bytes that the serial port is reading might be

ESC[ 38;5;2rootESC[ 38;5;2SOMETHING:DIR

(where ESC is the escape character, ASCII 0x1b dev 27) and hence why the regular expression is not working.

Parsing ANSI escape sequences is quite tricky and if this is a command prompt, a more simple sollution is to remove them from the prompt configuration.

An option to see them is:

tcf console-read --follow TARGET | cat -A

-A in cat will escape the ANSI characters for you.

When trying to automate an application that implements an ANSI TUI (Text User Interface) with menus and such, it becomes quite complicated. For example, BIOS over serials.

The application might be sending strings such as:

  • ^[[X;YHSTRING put STRING in X,Y
  • ^[[0m normal letters
  • ^[[1m bold letters
  • ^[[37m white FG
  • ^[[40m black BG

sometimes text is intersped in ANSI escape sequences (especially with very awkward software) that print STRING like this:

^[[1mS^[[1mT^[[1mR^[[1mI^[[1mN^[[1mSG

to match this against a regular expression, you need to do:

re.compile("\x1b\[1mS\x1b\[1mT\x1b\[1mR\x1b\[1mI\x1b\[1mN\x1b\[1mSG")

for example

Another problem is the sequence of characters you will see on the screen menu but how they come out in the serial port might be different; it usually helps to open two terminals, side by side and in one open the TUI via the TCF console:

$ tcf console-write -i TARGET

and on another, just the byte stream:

$ tcf console-read --follow TARGET | cat -A

interacting with it on the first console will give you an idea of what is actually printed that can be used to latch on.

4.3.2. Newlines when reading output of a command

Let’s say we are reading the top level tree of a git directory:

>>> tree = subprocess.check_output(
>>>      [
>>>          'git', 'rev-parse', '--flags', '--show-toplevel',
>>>          'SOMEFILENAME'
>>>      ],
>>>      stderr = subprocess.STDOUT,
>>>      cwd = os.path.dirname('SOMEFILENAME')
>>> )

then we try to use tree and it does not exist. Why? because:

$ git rev-parse --flags --show-toplevel SOMEFILENAME
SOMEPATH
$

which means that after SOMEPATH there is a newline and thus the output we are getting is SOMEPATH\n. So strip it:

>>> tree = tree.strip()

4.4. General Linux system

4.4.1. Setup proxies

Note

this applies to setting the proxy for the server; if you need to set proxies during testcase execution, see here

If your network requires proxy support:

  • From the GUI: log in as a normal user to the graphical interface

    1. On the top right click the configuration arrow, select * → configuration icon → select network → select proxies*

    2. Set:

      • Method Manual
      • Ignore Hosts: 127.0.0.1, localhost, 192.168.0.0/16
  • From the terminal, (when remotedly logged in, optional) add to ~/.bashrc or to /etc/bashrc (for system wide, headless machines):

    export NO_PROXY=${NO_PROXY:-'localhost,192.168.0.0/16,127.0.0.0/8,::1'}
    export no_proxy=$NO_PROXY
    

    (note the ${VAR:-DEFAULT} syntax is so to avoid these settings to interfere with those that might be set when you login via the GUI).

Configure sudo to pass proxy configuration:

# cat <<EOF > /etc/sudoers.d/proxy
# Keep proxy configuration through sudo, so we don't need to specify -E
# to carry it
Defaults env_keep += "ALL_PROXY FTP_PROXY HTTP_PROXY HTTPS_PROXY NO_PROXY"
Defaults env_keep += "all_proxy ftp_proxy http_proxy https_proxy no_proxy"
EOF

This ensures the proxy configuration is kept by sudo (versus having to run sudo -E) so tools that use the network don’t get stuck with network access.

Why?

  • 127.0.0.1/8 and 192.168.0.0/16 will be all networks we’ll use internally from our server, so they don’t need to be proxyed.

4.4.2. How do I update my TCF installation?

You can check if there is a new version available with:

# dnf check-update --refresh | grep TCF

if so, you can update to it with:

# dnf update --best ttbd ttbd-zephyr tcf tcf-zephyr
# systemctl restart ttbd@production

Note that if there is a major version change (from v0 to v1, or v1 to v2, etc), more steps have to be done, which will be like (example is from v0 to v1)

# rpm -e tcf-repo-v0
# rpm -i https://RPMREPOHOST/repo/tcf-repo-v0.11-1-1.noarch.rpm
# dnf update --best

This is so because we release the TCF stable branches as separate RPM repositories, for simplicity.

Note you might need to clean the metadata caches with:

# dnf clean all

to force an update of the metadata to be pulled from the servers.

4.4.3. Configuring a USB or other network adapter for an infrastructure network

Test targets or infrastructure (like power control switches) that require IP connectivity can be connected to your server via a dedicated network interface and switch.

Warning

do not connect infrastructure and test devices to the same network! You have to keep them separated.

You will need:

  • a network interface (USB, PCI, etc)
  • its MAC address
  • an IP address range (recommended 192.168.X.0/24)
  1. Identify the network interface you will use, using tools such as:

    # ifconfig -a
    # ip addr
    
  2. Now configure it as described in Configuring a static interface Via NetworkManager’s nmcli:

    # nmcli con add type ethernet con-name TCF-infrastructure ifname IFNAME ip4 192.168.0.20X/24
    # nmcli con up TCF-infrastructure
    

    note that you can also use VLANs if you add with type vlan id NN dev IFNAME for VLAN number NN:

    # nmcli con add type vlan con-name TCF-Infrastructure dev enp0s20u4 id 4 ip4 192.168.4.209/24
    

Conventions for assignment of addresses in the infastructure network:

  • use IPv4 (easier)
  • use a local network (eg: 192.168.x.0/24)
  • servers get IP addresses > 192.168.x.200 (try to establish a server naming conventions where numbers are assigned, server1, server2, etc) and thus their IP addresses are .201, .202, etc…
  • PDUs and other equipment use IP addresses > 192.168.x.100

Warning

Keep this switch isolated from upstream routers; connect only test-targets OR infrastructure elements to it.

4.4.4. Configuring network interfaces with NetworkManager and nmcli

NetworkManager will by default take control of most network interfaces in the system; some adjustments might be needed.

4.4.4.1. Disabling NetworkManager from controlling an interface

Network interfaces which connect the server to a NUT (network under test) need to be left alone by NetworkManager. NetworkManager can be told to ignore a network device in two ways:

  • run the command:

    # nmcli dev set IFNAME managed no
    
  • or create a configuration file:

    $ sudo tee /etc/NetworkManager/conf.d/disabled.conf <<EOF
    [keyfile]
    unmanaged-devices=mac:00:10:60:31:a4:ba
    EOF
    

    Or edit said file and add mac:MACADDR statements separated by semicolons to the unmanaged-devices line.

4.4.4.2. Configuring a static interface Via NetworkManager’s nmcli

To configure an internal network interface IFNAME for internal network for infrastructure control 192.168.2.0/24 with IP .205 , run:

# nmcli con add type ethernet con-name NETWORKNAME ifname IFNAME ip4 192.168.2.205/24
# nmcli con up NETWORKNAME

note that you can also use VLANs if you add with id NN for VLAN number NN:

# nmcli con add type vlan con-name NETWORKNAME dev IFNAME id NN ip4 192.168.2.205/24

4.4.5. Generating an SSL certificate

To use secure layers HTTPS connections between daemon and client, you must have a valid certificate and key, or use an autosigned certificate. This can be fed to the target server with the options --ssl-crt and --ssl-key.

If you want to create your own certificate you must have installed OpenSSL:

  1. Generate a private key:

    $ openssl genrsa -des3 -out server.key 1024
    
  2. Generate a CSR:

    $ openssl req -new -key server.key -out server.csr
    
  3. Remove Passphrase from key:

    $ cp server.key server.key.org
    $ openssl rsa -in server.key.org -out server.key
    
  4. Generate self signed certificate:

    $ openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
    

4.4.6. Configuring and reloading UDEV

udev is the Linux low-level service that acts when devices are added/removed from/to the system to set them up. TCF relies on it to create device alias named after the targets to make administration easier.

  • udev rule configuration files are stored in /etc/udev/rules.d/ and are called NN-FILENAME.rules, where NN is a number to sort inclusion. Most commonly you can use 90-ttbd.rules

  • When the rule file is changed, it can be reloaded with:

    # udevadm control --reload-rules
    
  • Information about a given device can be obtained with:

    $ udevadm info /dev/snd/controlC0
    P: /devices/pci0000:00/0000:00:1f.3/sound/card0/controlC0
    N: snd/controlC0
    S: snd/by-path/pci-0000:00:1f.3
    E: DEVLINKS=/dev/snd/by-path/pci-0000:00:1f.3
    E: DEVNAME=/dev/snd/controlC0
    E: DEVPATH=/devices/pci0000:00/0000:00:1f.3/sound/card0/controlC0
    E: ID_PATH=pci-0000:00:1f.3
    E: ID_PATH_TAG=pci-0000_00_1f_3
    E: MAJOR=116
    E: MINOR=11
    E: SUBSYSTEM=sound
    E: TAGS=:uaccess:
    E: USEC_INITIALIZED=30391111
    

    those KEYS= we can use to match in the udev rule files to do actions.

  • Verbosity can be controlled with:

    # udevadm control -l LEVEL    (see --help)
    

4.4.7. Finding USB device information

To find information about USB devices (serial numbers, paths) use any of these methods.

Note it is also possible that a device declares a serial number but it is all the same for every device (for example, in the FlySwatter2 and other FTDI based hardware). In FTDI based case, it is possible to re-flash a new serial number with these instructions

Note

use these methods in a Linux machine that is not running TTBD actively, as it will keep producing messages in the kernel output and it will be difficult to tell which device is the right one.

4.4.7.1. Finding USB device information with dmesg

  1. disconnect the device

  2. run dmesg -w in a console to see the kernel log, hit enter a few times to create space

  3. plug the device, see a message pop up with device data from the kernel

  4. Note the device data, along:

    usb 1-1.4.4.4.1: new full-speed USB device number 62 using ehci-pci
    usb 1-1.4.4.4.1: New USB device found, idVendor=2a03, idProduct=003d
    usb 1-1.4.4.4.1: New USB device strings: Mfr=1, Product=2, SerialNumber=220
    usb 1-1.4.4.4.1: Product: Arduino Due Prog. Port
    usb 1-1.4.4.4.1: Manufacturer: Arduino (www.arduino.org)
    usb 1-1.4.4.4.1: SerialNumber: 85439303033351E06162
    

    in this example, ignore the SerialNumber where it says New USB device Strings and focus on the following one.

    If there is no serial number, there will be something like:

    New USB device strings: Mfr=XYZ, Product=XYZ, SerialNumber=0
    

    and no line with just SerialNumber on its own will appear.

4.4.7.2. Finding USB device information with lsusb.py

lsusb.py -iu provides a tree display of the USB connected devices:

usb1            1d6b:0002 09  2.00  480MBit/s 0mA 1IFs (ehci_hcd 0000:00:1a.7) hub
 1-1            05e3:0608 09  2.00  480MBit/s 100mA 1IFs (Genesys Logic, Inc. Hub) hub
  1-1.1         2001:f103 09  2.00  480MBit/s 0mA 1IFs (D-Link Corp. DUB-H7 7-port USB 2.0 hub) hub
   1-1.1.1      0424:2514 09  2.00  480MBit/s 2mA 1IFs (Standard Microsystems Corp. USB 2.0 Hub) hub
    1-1.1.1.1   2a03:003d 02  1.10   12MBit/s 100mA 2IFs (Arduino (www.arduino.org) Arduino Due Prog. Port 85439303033351E01192)
   1-1.1.2      0424:2514 09  2.00  480MBit/s 2mA 1IFs (Standard Microsystems Corp. USB 2.0 Hub) hub
    1-1.1.2.4   04d8:f2f7 00  2.00   12MBit/s 100mA 1IFs (Yepkit Lda. YKUSH YK20946)
     1-1.1.2.4:1.0(IF) 03:00:00 2EPs (Human Interface Device:No Subclass:None)
   1-1.1.5      0403:6001 00  2.00   12MBit/s 90mA 1IFs (FTDI FT232R USB UART A5026SO1)

if we know the brand of our device, we can look it up; in the excerpt tree below, we can see, for example, a YKUSH on 1-1.1.2.4; next to the name of the device is the serial number (if available), YK20946 (in this example).

4.4.7.3. Finding USB device information with udevadm

Once we know a device node of any time has been established (eg: /dev/ttyUSB9), use udevadm info to find information about it:

# udevadm info /dev/ttyUSB0   # or whichever device node (eg: /dev/video or /dev/snd/XYZ)
P: /devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2:1.0/ttyUSB0/tty/ttyUSB0
N: ttyUSB0
S: serial/by-id/pci-Prolific_Technology_Inc._USB-Serial_Controller-if00-port0
S: serial/by-path/pci-0000:00:14.0-usb-0:2:1.0-port0
...
E: ID_SERIAL_SHORT=OR0497598
E: ID_PATH=pci-0000:00:14.0-usb-0:2:1.0
E: ID_PATH_TAG=pci-0000_00_14_0-usb-0_2_1_0
E: ID_PCI_CLASS_FROM_DATABASE=Serial bus controller
E: ID_PCI_INTERFACE_FROM_DATABASE=XHCI
E: ID_PCI_SUBCLASS_FROM_DATABASE=USB controller
...

those values can be used to match in udev

4.4.8. udev configuration of serial port

Methods for configuring a serial port by name. When a USB serial port is connected to the system, it is assigned a non-predictable name (eg: /dev/ttyUSB14 or /dev/ttyACM3) which will change each time is plugged (or not).

In order to have an stable name that consistently represents the device we are connecting, follow any of the following recipes, based on the capabilities of the USB device you are plugging to the system.

4.4.8.1. udev configuration of serial port based on serial number

Given a target name (TARGETNAME) we are going to name a serial port assigned to it /dev/tty-TARGETNAME based on the serial number of the USB device (that can be found using the tricks above) in /etc/udev/rules.d/90-ttbd.rules:

SUBSYSTEM == "tty", ENV{ID_SERIAL_SHORT} == "SERIALNUMBER", \
  SYMLINK += "tty-TARGETNAME"

Remember to update udev:

# udevadm control --reload-rules

4.4.8.2. udev configuration of serial port without serial number based on path

This is used for USB serial dongles that have no unique USB serial number.

Warning

This approach is very risky–any physical changes in the positions of cables or logical changes in enumeration order when kernel boots will change the path and might render your configuration inoperative or working incorrectly.

Given a target name (TARGETNAME) we are going to name a serial port assigned to it /dev/tty-TARGETNAME based on the path the USB device is connected, as most serial cables have no serial number; in /etc/udev/rules.d/90-ttbd.rules:

SUBSYSTEM == "tty", ENV{ID_PATH} == "*-usb-0:1.2.2:1.0", \
  SYMLINK += "tty-TARGETNAME"

find the path by plugging the device, using dmesg -w to find which /dev/ttyUSBX (or /dev/ttyACMX) name is given and then running:

# udevadm info /dev/ttyUSBX | grep ID_PATH=
ID_PATH=pciblahblah-usb-0:1.2.2:1.0

reload udev config:

# udevadm control --reload-rules

replug the USB dongle or device and verify the symlink /dev/tty-TARGETNAME is there.

If not found, use syslog or journalctl -r and udevadm control –log-level debug to find our what is going on when the device is plugged.

4.4.8.3. udev configuration of serial port based on sibling’s serial number

This is used for USB-to-TTY serial dongles that have no unique USB serial number.

Given a target name (TARGETNAME) we are going to name a USB-to-TTY serial port assigned to it /dev/tty-TARGETNAME based on the USB serial number of another device (the sibling) connected to the same USB hub.

The sibling USB device has to have a unique USB serial number. By knowing in which port our the USB-to-TTY serial dongle is, we can piggy back on the other sibling device’s USB serial number.

Thus, if we move the USB hub around, it’ll still have the same name, as long as we don’t change the port to which it is connected.

In /etc/udev/rules.d/90-ttbd.rules:

# When the USB-TTL port is connected to the hub with serial number
# YKXXXXX
SUBSYSTEM == "tty", \
    PROGRAM = "/usr/bin/usb-sibling-by-serial YKXXXXX", \
    ENV{ID_PATH} == "*2:1.0", \
    SYMLINK += "tty-TARGETNAME"

note the *2:1.0; that selects port 2, where we connect the serial port adapter. Port 1 would be *1:1.0, etc.

Remember to update udev:

# udevadm control --reload-rules

4.4.9. Finding the serial number of a Devantech USBRLY08B relay controller

Plug your board to the system and list:

$ lsusb.py | grep -i Devantech
 1-1.1.1       04d8:ffee 02  2.00   12MBit/s 100mA 2IFs (Devantech Ltd. USB-RLY08 00023456)

00023456 is the serial number; if there are multiple devices and you are not sure which one is it, disconnect the one you care for and run:

$ lsusb.py | grep -i Devantech > before

now reconnect it and run:

$ lsusb.py | grep -i Devantech > after

diff the files before and after:

$ diff before after
3d2
<  1-1.1.1       04d8:ffee 02  2.00   12MBit/s 100mA 2IFs (Devantech Ltd. USB-RLY08 00023456)

or run dmesg -w on the terminal, unplug and plug the device, see the kernel message about the new USB device, note the serial number.

4.4.10. Finding the serial number of an YKUSH hub

Methods to find the serial number of an YKUSH hub:

  • plug your YKush hub to the system and list:

    # lsusb.py | grep YKUSH | tee before
      2-2.1.1.4    04d8:f2f7 00  2.00   12MBit/s 100mA 1IF  (Yepkit Lda. YKUSH YK21297)
      2-2.1.2.4    04d8:f2f7 00  2.00   12MBit/s 100mA 1IF  (Yepkit Lda. YKUSH YK21292)
      2-2.1.3.4    04d8:f2f7 00  2.00   12MBit/s 100mA 1IF  (Yepkit Lda. YKUSH YK21290)
      2-2.1.4.4    04d8:f2f7 00  2.00   12MBit/s 100mA 1IF  (Yepkit Lda. YKUSH YK21294)
     2-2.3.4       04d8:f2f7 00  2.00   12MBit/s 100mA 1IF  (Yepkit Lda. YKUSH YK21293)
    

    you might have many; to tell which one is the one in your hand, you can just unplug it and list again:

    # lsusb.py | grep YKUSH | tee after
     2-2.1.1.4    04d8:f2f7 00  2.00   12MBit/s 100mA 1IF  (Yepkit Lda. YKUSH YK21297)
     2-2.1.2.4    04d8:f2f7 00  2.00   12MBit/s 100mA 1IF  (Yepkit Lda. YKUSH YK21292)
     2-2.1.4.4    04d8:f2f7 00  2.00   12MBit/s 100mA 1IF  (Yepkit Lda. YKUSH YK21294)
    2-2.3.4       04d8:f2f7 00  2.00   12MBit/s 100mA 1IF  (Yepkit Lda. YKUSH YK21293)
    

    to (quickly) find out which one was unplugged, diff the files before and after:

    # diff before after
    3d2
    <     2-2.1.3.4    04d8:f2f7 00  2.00   12MBit/s 100mA 1IF  (Yepkit Lda. YKUSH YK21290)
    

    the line that was removed when we unplugged was the one for YK21290, which is the serial number of the hub.

  • run dmesg -w on a terminal and plug the device, see the kernel message about the new USB device, note the serial number

Note

the hub itself has no serial number, but an internal device connected to its downstream port number 4 does have the YK34567 serial number.

4.5. ttbd: TCF server configuration and tricks

4.5.1. Starting more that one instance

Most setups will only have one instance, the production instance; however, two more are recommended:

  • infrastructure: handles power to USB hubs, equipment, normal and power switching to reset them in case) of issues, individual raw access to all the power switching units connected throughout the system, network switches, etc
  • staging: targets whose drivers are being developed before moving into production (optional)

To bring up an instance (repeat these steps replacing production with INSTANCENAME for other instances):

  1. Create the instance’s configuration directory:

    # install -d -m 2775 -o ttbd -g ttbd /etc/ttbd-production
    

    systemd will create the runtime directories needed when starting ttbd in /var/run/ttbd-production and /var/cache/ttbd-production as they will be wiped by the system on restart.

  2. Each instance listens on a different port, so we create the initial server configuration /etc/ttbd-production/conf_00_bind.py:

    host = "0.0.0.0"           # Listen on all interfaces
    port = 5000
    

    infrastructure is usually assigned 4999, staging 5001. Enable access through the firewall to said ports:

    # firewall-cmd --add-port=4999-5001/tcp --permanent
    
  1. Enable and start the instance:

    # systemctl enable ttbd@production
    # systemctl start ttbd@production
    

    systemd runs the daemon run as user ttbd, group ttbd and the following supplemental groups:

    • root: to be able to scan USB devices
    • dialout: to be able to access serial devices and USB connected serial devices (for consoles, JTAGs, etc)
    • ttbd: to access configuration and other files

    See log output with journalctl -fu ttbd@NAME. Diagnose issues starting with systemd in troubleshooting. Further configuration tips.

  2. At this point you could create or copy existing conf_*.py configuration files to /etc/ttbd-production and restart the service with:

    # systemctl restart ttbd@production
    

    If you have none, that is ok, we’ll add them in the next sections.

Note that now, none can access the server yet because there is no way to authenticate with it :) Let’s add some configuration.

4.5.2. Configure authentication for local users (optional)

A quick way to allow any user in the local machine to use the server without authenticating is to request local authentication for 127.0.0.1; run:

# echo 'local_auth.append("127.0.0.1")' \
  > /etc/ttbd-production/conf_00_auth_local.py
# systemctl restart ttbd@production

Now login in should work with no need to input anything (in this case, there will be no output either):

$ tcf -iu https://localhost:5000 login

Note

feel free to ignore the error message about ZEPHYR_BASE not being defined; it is a glitch that will be fixed. You can work around it by running:

$ export ZEPHYR_BASE=

You can configure local TCF clients to access the local instance by default (more):

# mkdir -p /etc/tcf
# echo "tcfl.config.url_add('https://localhost:5000', ssl_ignore = True)" \
  >> /etc/tcf/conf_local.py

4.5.3. Configure simple authentication / for Jenkins jobs (optional)

You can setup static accounts for users or Jenkins autobuilders with hardcoded passwords by creating a file /etc/ttbd-production/conf_00_auth_localdb.py with the contents:

import ttbl.auth_localdb

ttbl.config.add_authenticator(ttbl.auth_localdb.authenticator_localdb_c(
     "Jenkins and other",
     [
         [ 'usera', 'PASSWORDA', 'user', ],
         [ 'superuserB', 'PASSWORDB', 'user', 'admin', ],
         [ 'jenkins1', 'PASSWORD1', 'user', ],
         [ 'jenkins2', 'PASSWORD2', 'user', ],
         ...
     ]))

Restart to read the configuration:

# systemctl restart ttbd@production

4.5.4. Configure authentication against LDAP

Copy the configuration example /etc/ttbd/production/example_conf_05_auth_ldap.py and modify it to suit your LDAP setup:

# cp /etc/ttbd-production/example_conf_05_auth_ldap.py /etc/ttbd-production/conf_05_auth_ldap.py
# <edit away>
# systemctl restart ttbd@production

Authorized users (users in the declared LDAP groups in the configurtion file) should now be able to login with:

$ tcf login LDAPLOGIN

This file can be tweaked to fit your authentication needs. For example, you might want to change the name of the LDAP groups your users need to be members off.

If this does not work, most likely the configuration files where not loaded properly; check the daemon output (troubleshooting and troubleshooting LDAP).

4.5.5. Configuring a target for just controlling power to something

In your ttbd’s conf_SOMETHING.py config file, add:

ttbl.config.target_add(
    ttbl.tt.tt_power(
      "TARGETNAME",
      power_control = PC_OBJECT,
      power = True
    ),
    tags = dict(idle_poweroff = 0)
)

this creates a target called TARGETNAME that you can power on, off or cycle. The power argument indicates what do do with the power at startup (True turn it on, False turn it off, None–or omitting this argument, leave it as is). tags = dict(idle_poweroff = 32) is used to have TARGETNAME not being powered off when idle.

Now, PC_OBJECT is the actual implementation of power control and you can make it be things like:

ttbl.pc.dlwps7("http://admin:1234@sp3/5")

This would make TARGETNAME’s power be controlled by plug #5 of the Digital Logger Web Power Switch 7 named sp3 (setup instructions). Because this is a normal, 120V plug, if a light bulb were connected to it:

ttbl.config.target_add(
  ttbl.tt.tt_power(
    "Entrance_light",
    power_control = ttbl.pc.dlwps7("http://admin:1234@sp3/5"),
    power = True
  ),
  tags = dict(idle_poweroff = 0)
)

and like this, the target Entrance_light can be switched on or off with tcf power-on Entrance_light and tcf power-off Entrance_light.

It could also be:

ttbl.pc_ykush.ykush("YK21080", 3)

which means that power to TARGETNAME would be implemented by powering on or off port #3 of the YKush power-switching hub with serial number YK21080 (setup instructions).

Other power controller implementations are possible, of course, by subclassing ttbl.tt_power_control_impl.

4.5.6. Configuring a Linux target to power on/off with serial console

Building on the previous example, we can use the ttbl.tt.tt_serial object to create a target that provides serial consoles, can be powered on or off and we can interact with the target over the serial console.

If we have a Linux machine which is installed with a distro that provides serial console access, then this will work:

Bill of materials

  • a Linux machine
  • a serial console to the physical Linux machine (if your machine doesn’t have serial ports, a USB null modem or two USB serial dongles with a NULL modem adapter will do.
  • one available port on a power switch, to turn the physical machine on/off (eg, a DLWPS7)

Connecting the test target fixture

  1. Configure the Linux machine to:

    • always power on when AC power is on

    • provide a serial console on the serial port; this can be done by adding console=ttyS0,115200 and/or console=ttyUSB0,115200 to the kernel command line.

      In modern systemd-enable distributions, also with:

      # systemd enable agetty@ttyUSB0
      # systemd enable agetty@ttyS0
      
  1. Connect the serial dongle cables to the physical target and to the server
  2. Connect the physical target to port PORT of power switch POWERSWITCH

Configuring the system for the fixture

  1. Choose a name for the target: linux-NN (where NN is a number)

  2. Configure udev to add a name for the serial device for the board’s serial console so it can be easily found at /dev/tty-TARGETNAME. Follow these instructions using the serial dongle’s serial number.

  3. Add a configuration block to the server configuration file:

    ttbl.config.target_add(
         ttbl.tt.tt_serial(
             "linux-NN",
             power_control = [
                    ttbl.cm_serial.pc(),
                    ttbl.pc.dlwps7("http://admin:1234@POWERSWITCH/PORT"),
                    ttbl.pc.delay(5),
             ],
             serial_ports = [
                 "pc",
                 { "port": "/dev/tty-linux-NN", "baudrate": 115200 }
             ]),
         tags = {
             'linux': True,
             'bsp_models': { 'x86_64': None },
             'bsps': {
                 'x86_64': {
                     'linux': True,
                     'console': 'x86_64',
                 }
             }
         },
         target_type = "linux-DISTRONAME-VERSION")
    

    where of course, DISTRONAME-VERSION matches the linux distribution and verison installed.

4.5.7. Configure physical Linux targets with a fixed Live filesystem

A physical Linux machine can be booted with the TCF-live image that will always boot fresh to the same state.

This builds on the previous section.

Bill of materials

  • a Linux machine w at least 2G RAM (harddrive optional, but recommended)
  • a USB drive (at least 2G)
  • a serial console to the physical Linux machine (if your machine doesn’t have serial ports, a USB null modem or two USB serial dongles with a NULL modem adapter will do.
  • one available port on a power switch, to turn the physical machine on/off (eg, a DLWPS7)

Connecting the test target fixture

  1. Generate a TCF Live ISO image following these steps

  2. Initialize the USB drive with the image (assuming it is at /dev/sdb):

    # livecd-iso-to-disk --format --reset-mbr tcf-live/tcf-live.iso /dev/sdb
    
  3. Plug the USB drive to the physical Linux target, make sure it boots

  4. Configure the Linux machine to:

    • boot USB first
    • always power on when AC power is on
  5. Create two physical partitions for large file storage during tests and swap:

    • boot the image, connect via standard console or serial console

    • partition the disk:

      $ parted DEVICE -s mklabel gpt                # make a new partition table
      $ parted DEVICE -s mkpart logical linux-swap 0% 10G # make a partition
      $ parted DEVICE -s name 1 TCF-swap            # name it
      $ parted DEVICE -s mkpart logical btrfs 10G 100% # make a partition
      $ parted DEVICE -s name 2 TCF-home            # name it
      

    TCF-home will be always cleaned up and mounted as /home and the swap will be activated.

  6. Connect the serial dongle cables to the physical target and to the server

  7. Connect the physical target to port PORT of power switch POWERSWITCH

Configuring the system for the fixture

  1. Choose a name for the target: linux-NN (where NN is a number)

  2. Configure udev to add a name for the serial device for the board’s serial console so it can be easily found at /dev/tty-TARGETNAME. Follow these instructions using the board’s serial number.

  3. Add a configuration block to the server configuration file:

    ttbl.config.target_add(
         ttbl.tt.tt_serial(
             "linux-NN",
             power_control = [
                    ttbl.cm_serial.pc(),
                    ttbl.pc.dlwps7("http://admin:1234@POWERSWITCH/PORT"),
                    ttbl.pc.delay(5),
             ],
             serial_ports = [
                 "pc",
                 { "port": "/dev/tty-linux-NN", "baudrate": 115200 }
             ]),
         tags = {
             'linux': True,
             'bsp_models': { 'x86_64': None },
             'bsps': {
                 'x86_64': {
                     'linux': True,
                     'console': 'x86_64',
                 }
             }
         },
         target_type = "linux-fedora-x86_64")
    

4.5.8. Adding tags to a target

Once a target is configured, new tags can be added to it using ttbl.test_target.tags_update() in any configuration file (preferrably next to the target definition); for example, if in /etc/ttbd-production/conf_10_targets.py you had the statement:

arduino101_add(name = "a101-15",
               fs2_serial = "a101-15-fs2",
               ykush_url = "http://admin:1234@HOSTNAME/PORT",
               ykush_serial = "YK24439")

you can add the tags fixture_spi_basic_0 (as a boolean that defaults to True and tempsetting (as an integer) with:

arduino101_add(name = "a101-15",
               fs2_serial = "a101-15-fs2",
               ykush_url = "http://admin:1234@HOSTNAME/PORT",
               ykush_serial = "YK24439")
tcfl.config.targets['a101-15'].tags_update(dict(
    fixture_spi_basic_0 = True,
    tempsetting = 32))

4.5.9. How do I disable a target by default?

When a target has to be disabled by default, add this to the configuration file:

ttbl.config.targets['TARGETNAME'].disable("")

the target will be loaded and the configuration will be accesible, however, tcf clients that select targets automatically (list, run) will not use it unless -a is given.

This is used for targets that are misbehaving for any reason but still need to be connected to debug. They can be manually enabled/disabled with:

$ tcf disable TARGETNAME
$ tcf enable TARGETNAME

The user has to have admin capabilities in the TTBD server to run this operation.

4.5.10. Allowing the server to be used remotely

By default, the server is configured to only listen on local ports, thus only accessible from the server itself.

To allow the server to be accessible on all the network interfaces of the machine on TCP port 5000, create /etc/ttbd-production/conf_00_bind.py with the content:

host = "0.0.0.0"
port = 5000

Now restart the daemon and verify it restarted properly:

# systemctl restart ttbd@production
# journalctl -eu ttbd@production

As well, ensure the server’s firewall allows the given ports to be accessible. In Fedora 25:

# dnf install -y firewall-config
$ firewall-config

In the current firewall zone, add in Ports a range 4999-5001, type TCP.

Now select in the menu Options > Runtime to permanent to ensure the changes are permanent and next time the server restarts they are applied.

4.5.11. Increasing the verbosity of the server

When debugging issues with the server, you might have to increase its verbosity; for that, more -v have to be given to the command line. For that, edit /etc/systemd/system/ttbd@.service to add more -v to the ExecStart line.

Reread systemd’s configuration and restart the server:

# systemctl daemon-reload
# systemctl restart ttbd@production

Remember to toggle it back to the default -vv–it gets chatty.

4.6. Manual installation of TCF from source

4.6.1. Creation and setup of user ttbd

The ttbd user is used to store TCF’s software and files, and the ttbd group to give normal user access to said files.

To create it (if not yet created):

# useradd -G kvm ttbd

Allow members of group ttbd access to ttbd’s home so they can write files needed for the deployment in there; never shall need to login as the ttbd user:

# chmod g+ws ~ttbd

Allow other users access to ttbd’s files:

# usermod -aG ttbd USER1 USER2 ….  # Make USERs members of ttbdgroup

4.6.2. Install required software

Run:

# dnf install -y openocd make gcc-c++ python-ldap pyserial \
  python-requests git python-werkzeug python-tornado python-flask \
  python-flask-login python-flask-principal pyusb python-pexpect \
  pyOpenSSL

to install software packages required by TCF’s server:

  • openocd is used to interact with targets
  • gcc-c++ and make are used to build
  • Git is a source control manager we’ll use to obtain TCF’s source
  • python-ldap, pyserial and python-requests are Python libraries TCF relies on
  • python-werkzeug, python-flask* and python-tornado are the HTTP server framework TTBD uses to serve data.
  • pyusb is the library used to access USB devices from Python
  • python-pexpect is a expect-like language implementation in Python

You might need to install support packages that are not in distributions dependning on what you want to run with TCF.

4.6.3. Remove conflicting packages

Remove Modem Manager, as it interferes with the serial ports:

# dnf remove -y ModemManager

this won’t be needed if you will only use QEMU devices.

4.6.4. Install TCF

Obtain the code

$ git clone git://GITHOST/tcf tcf.git

Install the TCF client

Follow the steps on the quickstart to install the client from source.

$ cd tcf.git $ python setup.py install –user

Note

you will also need to install the Zephyr SDK 0.9 to /opt/zephyr-sdk-0.9.5 if you want to build Zephyr OS apps and other dependencies:

Fedora:

# dnf install -y make python-requests python-ply cmake \
  PyYAML python2-junit_xml python2-jinja2

Ubuntu:

# apt-get install -y python-ply python-requests make \
  python-junit.xml python-jinja2

Install the server

Install requirements: sdnotify, a library to help TCF’s server TTBD integrate with systemd:

$ sudo pip install sdnotify

Now the server itself:

$ cd tcf.git/ttbd
$ sudo python setup.py install

Note the server needs configuration of SELinux, kernel credentials, UIDs and GIDs for operation – so running it off the RPM package can get complicated.

4.7. Manual installation of support packages

These are dependencies needed when certain kind of test hardware is going to be connected or certain OSes / testcases are to be used:

  • Arduino Due boards: Bossa command line
  • Zephyr SDK for Arduino 101, Intel Quark, FRDM, SAM e70, others: Zephyr SDK
  • ESP32
  • NiosII on Altera MAX10: Quartus programmer and NiosII CPU image

4.7.1. Bossac: Arduino Due flasher

If Arduino Due is going to be used, a special tool for flashing has to be installed–this has to be built from source as the branch that supports the Arduino Due hasn’t been merged into mainline as of writing these instructions:

  1. Get the requiements and the code:

    # sudo dnf install -y gcc-c++ wxGTK-devel
    $ git clone https://github.com/shumatech/BOSSA.git bossac.git
    $ cd bossac.git
    $ git checkout -f 1.6.1-arduino-19-gae08c63
    
  2. Build and install:

    $ make bin/bossac
    $ sudo install -o root -g root -m 0755 bin/bossac /usr/local/bin
    
  3. (optional) Create a fast RPM with FPM:

    $ chmod 0755 bin/bossac
    $ fpm  -n bossac -v $(git describe --tags) -s dir -t rpm bin/bossac
    

4.7.2. Zephyr SDK

To build some Zephyr OS apps/testcases or to flash certain hardware, you will need this SDK:

  1. Download the Zephyr SDK from https://www.zephyrproject.org/downloads/tools

  2. Install in /opt/zephyr-sdk-VERSION:

    # chmod a+x zephyr-sdk-0.9.5-setup.run
    # ./zephyr-sdk-0.9.5-setup.run -- -y -d /opt/zephyr-sdk-0.9.5
    
  3. (optional) Create a fast RPM with FPM:

    $ fpm -n zephyr-sdk-0.9.5 -v 0.9.5 \
    >     --rpm-rpmbuild-define '_build_id_links alldebug' \
    >     -s dir -C / -t rpm opt/zephyr-sdk-0.9.5
    

    _build_id_links alldebug is needed to disable generation of build symlinks in /usr/lib/.build-id. Because the SDK packs a lot of files that are similar/identical to those present in the system, it will conflict.

4.7.3. Arduino Builder 1.6.13

The Arduino Builder will be needed by the TCF client to build .ino files into appplications that can be flashed into targets for test.

  1. Download the Arduino IDE package from https://www.arduino.cc/download_handler.php?f=/arduino-1.6.13-linux64.tar.xz

  2. Extract:

    # tar xf arduino-1.6.13-linux64.tar.xz -C /opt
    
  3. (optional) Create a fast RPM with FPM:

    $ fpm -n tcf-arduino-builder-1.6.13 -v 1.6.13 -s dir -C / -t rpm opt/arduino-1.6.13/
    

4.7.4. tunslip6

tunslip6 is used to create a SLIP interface for a QEMU virtual machine and connecting it to a TAP interface. This code has been floating around for different small OSes that also run in QEMU, so there is a few versions.

The one we currently use is the one used by the Zephyr project at the net-tools repository:

which has added functionality to defer configuration to external parties (ttbd in this case) and some strenghtening to deal with race conditions.

  1. Clone:

    $ git clone http://github.com/zephyrproject-rtos/net-tools
    
  2. Build:

    $ cd net-tools
    $ make tunslip6
    # make install
    
  3. (optional) Create a fast RPM with FPM:

    $ install -m 0755 tunslip6 -D root/usr/bin/tunslip6
    $ fpm -n tunslip6 -v $(git describe --always) -s dir -C root -t rpm usr/bin
    

4.7.5. Xtensa ESP32

In order to build (TCF client) and deploy/flash (ttbd server) for ESP32 boards, you will need the xtensa-esp32 SDK and the ESP-IDF libraries:

  • xtensa-esp32 SDK

    1. Download from https://dl.espressif.com/dl/xtensa-esp32-elf-linux64-1.22.0-59.tar.gz

    2. Extract to /opt/xtensa-esp32-elf

      # tar xf xtensa-esp32-elf-linux64-1.22.0-59.tar.gz -C /opt

    3. Add to /etc/environment:

      ESPRESSIF_TOOLCHAIN_PATH=/opt/xtensa-esp32-elf
      
  • ESP-IDF: This is Xtensa’s IOT framework that is used by Zephyr and others

    1. Clone to /opt/esp-idf.git:

      $ rm -rf /opt/esp-idf.git
      $ git clone --recursive https://github.com/espressif/esp-idf.git /opt/esp-idf.git
      $ (cd /opt/esp-idf.git && git checkout -f $(ESP_IDF_REV))
      
    2. Add to /etc/environment:

      ESP_IDF_PATH=/opt/esp-idf.git
      

4.7.6. libcoap

Use the following script to create the RPM:

.. code-block:: sh

# Use absolute dirs, libtool needs it dir=$PWD/libcoap.git rootdir=$PWD/root-libcoap.git

rm -rf $dir git clone –recursive -b dtls https://github.com/obgm/libcoap.git $dir

rm -rf $rootdir mkdir -p $rootdir

(cd $dir; ./autogen.sh) sed -i ‘s|prefix = @prefix@|prefix = $(DESTDIR)/@prefix@|g’ $(find $dir/ext/tinydtls -iname Makefile.in) (cd $dir; ./configure –disable-shared –disable-documentation –prefix=/usr) make -C $dir all make -C $dir DESTDIR=$rootdir install rm -rf $rootdir/usr/share ver=${ver:-$(git -C $dir describe –tags)} fpm -n libcoap -v $ver -s dir -t rpm -C $rootdir

Invoke as:

$ ./mklibcoap.sh

4.7.7. FPM, fast package manager

Note

this is only optional and only needed if you are building RPMs for distribution; please ignore otherwise

  1. Install dependencies, clone the code, build and install:

    $ sudo dnf install -y ruby-devel rpm-build
    $ git clone https://github.com/jordansissel/fpm fpm.git
    $ make -C fpm.git install
    

4.8. Platform firmware / BIOS update procedures

4.8.1. Updating the serial number and / or description of a FTDI serial device

These devices are used in multiple USB to serial dongles and embedded in a number of devices, for example:

  • Flyswatter JTAGs, which come all with the same serial number (usually FS20000).
  • standalone dongles
  • MCU boards, embedded computers

These usually come as USB vendor ID 0x0403, product ID starting with 0x6NNN.

When there are multiple FTDI devices that have the same serial number, the system needs to be able to tell them apart, so we can flash a new serial number in them, which we usually make match the target name, or whatever is needed.

Warning

this process should be done in a separate machine; if you do it in a server with multiple of these devices connected, the tool can’t tell them apart and might flash the wrong device.

To flash a new serial number or descriptionusing ftdi_eeprom on your laptop (Windows utility):

$ sudo dnf install -y libftdi-devel
$ cat > file.conf <<EOF
vendor_id=0x0403
product_id=0x6010
serial="NEWSERIALNUMBER"
use_serial=true
EOF

Now plug the USB cable to your server or laptop, making sure it is the only one and run, as super user:

# ftdi_eeprom --flash-eeprom file.conf

Reconnect it to have the system read the new serial number / description.

Notes:

  • if you have the unit you are re-flashing connected to a USB power switching hub (like a YKush), make sure to power it on and to power off any other device that has a 0x0403/6010 USB vendor ID / product ID code, which you can find with:

    $ lsusb.py  | grep 0403:6010
      1-1.2.1      0403:6010 00  2.00  480MBit/s 0mA 2IFs (Acme Inc. Flyswatter2 Flyswatter2-galileo-04)
    
  • make NEWSERIALNUMBER shorter if you receive this error message:

    FTDI eeprom generator v0.17(c) Intra2net AG and the libftdi developers <opensource@intra2net.com (opensource%40intra2net.com)>
    FTDI read eeprom: 0
    EEPROM size: 128
    Sorry, the eeprom can only contain 128 bytes (100 bytes for your strings).
    You need to short your string by: -1 bytes
    FTDI close: 0
    
  • For Flyswatter2 devices, add:

    product="Flyswatter2"
    

    to the configuration file so the product name is set to Flyswatter2, needed for OpenOCD to find the device.

4.8.2. Updating the serial number and / or description of a CP210x serial device

Some hardware use serial-to-USB converters based on the CP210x series by Silicon Labs.

If the serial number programed on it is not unique enough, it can be programmed with the tool cp120x-program, available from http://cp210x-program.sourceforge.net/.

Once installed:

  1. identify the device to operate with:

    $ lsusb | grep -i CP210x
    Bus 002 Device 085: ID 10c4:ea60 Cygnal Integrated Products, Inc. CP210x UART Bridge / myAVR mySmartUSB light
    

    Note

    your device might show differnt vendor or product ID and strings, in such case, adjust your grepping.

  2. take the bus and device numbers (002/085 in the example) and feed it to the cp219x-program tool with the new serial number you want:

    # cp210x-program -m 002/085 -w --set-serial-number "NEWSERIALNUMBER"
    

    it is always a good idea to set as new serial number the name of the target it is going to be assigned to.

  3. Reconnect the device and verify the new serial number is set with lsusb.py:

    $ lsusb.py -ciu
    ...
      2-2.4          10c4:ea60 00  1.10   12MBit/s 100mA 1IF  (Silicon Labs CP2102 USB to UART Bridge Controller esp32-39)
        2-2.4:1.0      (IF) ff:00:00 2EPs (Vendor Specific Class) cp210x ttyUSB0
    ..
    

    in this case, we set esp32-39 as new serial number, which is displayed at the end of the line.

4.8.3. Updating the firmware in an Arduino101 to factory settings

This is needed to operate with Zephyr OS > v1.5.0-349-gecf96d2, which dropped support for the older Arduino101 Zephyr boot ROM.

  • Download from https://software.intel.com/en-us/node/675552 the package arduino101-factory_recovery-flashpack.tar.bz2 and decompress to your home directory:

    $ cd
    $ tar xf LOCATION/arduino101-factory_recovery-flashpack.tar.bz2
    
  • Ensure your TTBD server is >= v0.10 (dated 9/21/16 or later)

  • Disable you Arduino101 target (to avoid it being used by other automated runs) and acquire it:

    $ tcf disable arduino101-NN
    $ tcf acquire arduino101-NN
    
  • Flash the new Boot ROM and bootloader:

    $ tcf images-upload-set arduino101-NN \
      rom:$HOME/arduino101-factory_recovery-flashpack/images/firmware/FSRom.bin \
      bootloader:$HOME/arduino101-factory_recovery-flashpack/images/firmware/bootloader_quark.bin
    
  • Release the target and enable it:

    $ tcf release arduino101-NN
    $ tcf enable arduino101-NN
    
  • Test with:

    $ cd LOCATION/OF/ZEPHYR/KERNEL
    $ export ZEPHYR_BASE=$PWD
    $ tcf run -v -t arduino101-NN samples/hello_world
    

4.8.4. Updating the firmware in an Quark C1000 reference boards

This updates to the QSMI v1.3 bootrom support, needed to operate with Zephyr OS > v1.5.0, which dropps support older ROMs.

  • Download from https://github.com/quark-mcu/qmsi/releases/tag/v1.3.0 the file quark_se_rom-v1.3.0.bin to your home directory:

    $ wget https://github.com/quark-mcu/qmsi/releases/download/v1.3.0/quark_se_rom.bin \
        -O ~/quark_se_rom-v1.3.0.bin
    
  • Ensure your TTBD server is >= v0.10 (dated 11/01/16 or later)

  • Disable you Quark C1000 target (to avoid it being used by other automated runs), maybe wait for no still-running jobs are using it (use tcf list -v qc1000-NN to see if it is acquired by anyone):

    $ tcf disable qc1000-NN
    

For the actual flashing process, there is a list of steps that have to be performed that are needed to wipe the flash and reset the board in such a way that it puts it in a receptive state. This implies running specific OpenOCD commands and modifying the way the OpenOCD driver operates on the board to maintain those settings.

  1. Use the script tcf-qc1000-fw-upload.sh TARGETNAME FWFILE to update the Boot ROM:

    $ tcf-qc1000-fw-upload.sh qc1000-NN $HOME/quark_se_rom.bin
    

    In case of failure, retry one or two times, as some commands get stuck. If after the retries it still fails, it might be time to try remediation steps as described below.

  2. Verify operation with running a Zephyr test case:

    $ cd LOCATION/OF/ZEPHYR/KERNEL
    $ export ZEPHYR_BASE=$PWD
    $ tcf run -vat qc1000-NN  /usr/share/tcf/examples/test_healtcheck.py
    

    Testcase should PASS. Note the -a, so TCF can use a disabled target. If they fail and tcf console-read qc1000-NN reports garbage instead of some ASCII text, it is possible board timings are messed up. Go back to flashing the Boot ROM and maybe use the remediation steps described below.

  3. Re-enable the target:

    $ tcf enable qc1000-NN
    

The process is thus concluced

4.8.4.1. Remediation steps

In some situations it has been seen that the stock OpenOCD version distributed with the Zephyr SDK or most system cannot flash properly the QC1000.

In said case, we can try with the ISSM version of OpenOCD:

  1. obtain the ISSM toolchain package for Linux from https://software.intel.com/en-us/articles/issm-toolchain-only-download

  2. Install in your system in path opt with:

    # tar xf PATH/TO/issm-toolchain-linux-2016-05-12-pub.tar.gz  -C /opt
    
  3. Disable ISSM’s OpenOCD using a TCL port, otherwise the TCF server, TTBD, will not be able to talk to it:

    # sed -i 's/tcl_port/#tcl_port/' \
      /opt/issm-toolchain-linux-2016-05-12/tools/debugger/openocd/scripts/board/quark*.cfg
    
  4. Alter the TCF’s configuration of each QC1000 target to be updated so it uses the OpenOCD from ISSM; in file /etc/ttbd-production/conf_FILE.py:

    quark_c1000_add(
        "TARGETNAME",
        serial_number = "SERIALNUMBER",
        ykush_serial = "YKUSHHUBSERIALNUMBER",
        ykush_port_board = YKUSHHUBPORT,
        openocd_path = "/opt/issm-toolchain-linux-2016-05-12/tools/debugger/openocd/bin/openocd",
        openocd_scripts = "/opt/issm-toolchain-linux-2016-05-12/tools/debugger/openocd/scripts")
    
  5. Restart the server:

    # systemctl restart ttbd@production
    
  6. Run a healthcheck on the target:

    $ tcf run -vat TARGETNAME  /usr/share/tcf/examples/test_healtcheck.py
    

    In case of trouble, diagnose by looking at the journal:

    $ journalctl -aeu ttbd@production
    
  1. Retry the firmware update script
  2. Upon success, revert the configuration change and restart the server

4.8.5. Updating the firmware in an Quark D2000 reference boards

This is needed to operate with Zephyr OS.

  • Download from https://github.com/quark-mcu/qm-bootloader/releases/tag/v1.3.0 the file quark_d2000_rom.bin to your home directory:

    $ wget https://github.com/quark-mcu/qm-bootloader/releases/download/v1.4.0/quark_d2000_rom_fm_hmac.bin
    
  • Ensure your TTBD server is >= v0.10 (dated 9/21/16 or later)

  • Disable you Quark D2000 target (to avoid it being used by other automated runs) and acquire it:

    $ tcf disable qd2000-NN
    $ tcf acquire qd2000-NN
    
  • Flash the new Boot ROM and bootloader:

    $ tcf images-upload-set qd2000-NN rom:quark_d2000_rom_fm_hmac.bin
    
  • Release the target:

    $ tcf release qd2000-NN
    
  • Verify operation with running a Zephyr test case:

      $ export ZEPHYR_BASE=LOCATION/OF/ZEPHYR/KERNEL
      $ tcf run -vat qd2000-NN  /usr/share/tcf/examples/test_healtcheck.py
    
    Testcase should `PASS`. Note the `-a`, so TCF can use a disabled
    target. If they fail and `tcf console-read qc1000-NN` reports
    garbage instead of some ASCII text, it is possible board timings
    are messed up. Go back to flashing the Boot ROM and maybe use the
    remediation steps described below.
    
  • Enable the target:

    $ tcf enable qd2000-NN
    

4.8.6. Updating the FPGA image in Synopsys EMSK boards

  1. You will need a Synopsys account; register and wait for them to accept you

  2. Use this account to download their newest firmware. Currently set to v2.2.

  3. Now download the Lab Tools 14.7 or newer (You may have to create a Xilinx account here to access this download)

    If you need installation instruction you can find them here under Appendix: C (page 86) of https://www.embarc.org/pdf/ARC_EM_Starter_Kit_UserGuide.pdf (important note, when running this it seems you have to use Windows and the Impact 32 bit version. The 64 bit version seems to fail out on certain steps you need to use).

  4. Now just go through the flashing instructions located on page 93 towards the bottom, called SPI Flash-Programming Sequence https://www.embarc.org/pdf/ARC_EM_Starter_Kit_UserGuide.pdf

    If you need another resource the synopsys instructions can be found here under (page 86) Appendix: C for installing the tools, and (page 93 bottom) under SPI Flash-Programming Sequence. https://www.embarc.org/pdf/ARC_EM_Starter_Kit_UserGuide.pdf

Note

the switch configuration we are currently using is ARC_EM9D for the 9D model.

Here is the switch configuration for SW1 (bit 1 is switch one; bit 2 is switch 2)

Bit 1 Bit2 Configuration
OFF OFF ARC_EM7D
ON OFF ARC_EM9D
OFF ON ARC_EM11D
ON ON Reserved

4.9. Creating images for the Provisioning OS

For provisioning using Provisioning OS, images have to be extracted and installed in the server (or an rsync server as described in the setup guide).

The images for provisioning are a flat root filesystem that is rsync’ed by with tcfl.pos.

Extracting them can be a little bit tricky, but there are different methodologies that allow automating the process.

4.9.1. Linux Live images

When images are Linux Live filesystems, they can usually be extracted easily, using the /usr/share/tcf/tcf-image-setup.sh script, which understands most Live images.

See the examples.

4.9.2. Linux Kickstart images using QEMU

Linux distributions that can be installed via kickstart can use /usr/share/tcf/kickstart-install.sh, which uses QEMU to run the installation with a built in kickstart, creating a qcow2 file image that then tcf-image-setup.sh can install.

kickstart-install.sh creates a kickstart file in a drive that is passed to the virtual machine, so there is no need for PXE servers. It extracts the kernel and initrd from the ISO image as well.

See the examples.

4.9.3. Manual image extraction using QEMU

  1. create a 20G virtual disk:

    $ qemu-img create -f qcow2 ubuntu-18.10.qcow2 20G
    $ qemu-img create -f qcow2 Fedora-Workstation-29.qcow2 20G
    
  2. Install using QEMU all with default options (click next). Power off the machine when done instead of power cycling:

    $ qemu-system-x86_64 --enable-kvm -m 2048 -hdah ubuntu-18.10.qcow2 -cdrom ubuntu-18.10-desktop-amd64.iso
    $ qemu-system-x86_64 --enable-kvm -m 2048 -hda Fedora-Workstation-29.qcow2 -cdrom Fedora-Workstation-Live-x86_64-29-1.2.iso
    

    Key thing here is to make sure everything is contained in a single partition (first partition).

    For Ubuntu 18.10:

    • select install
    • select any language and keyboard layout
    • Normal installation
    • Erase disk and install Ubuntu
    • Create a user ‘Test User’, with any password
    • when asked to restart, restart, but close QEMU before it actually starts again

    For Fedora:

    • turn off networking
    • select install to hard drive
    • select english keyboard
    • select installation destination, “CUSTOM” storage configuration > DONE
    • Select Standard partition
    • Click on + to add a partition, mount it on /, 20G in size (the system later will add boot and swap, we only want what goes in the root partition). Select DONE
    • Click BEGIN INSTALLATION
    • Click QUIT when done
    • Power off the VM
  3. Create image:

    $ /usr/share/tcf/tcf-iamge-setup.sh ubuntu:desktop:18.10::x86_64 ubuntu-18.10.qcow2