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")
@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.4. Capturing data, doing SSH

4.1.4.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.4.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. 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.5.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.5.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'tt_power_control_mixin',
                 u'test_target_images_mixin',
                 u'test_target_console_mixin',
                 u'tt_debug_mixin'],
 ...
 '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.5.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.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(s) reserved and powered-on while I fuzz with them?

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

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.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 ttbl.tt_debug_mixin (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 tcfl.report.report_c.

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 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. 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.5. 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.6. 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.7. 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.8. 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.9. 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.file_c.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.templates["text"]["output_file_name"]
>>> tcfl.report.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 broker-file-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.broker_files.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

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.

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 broker 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:

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