[rtems-tools commit] Add TFTP as a back end option for testing. Add telnet as a console option.

Chris Johns chrisj at rtems.org
Thu Sep 21 08:28:57 UTC 2017


Module:    rtems-tools
Branch:    master
Commit:    3a867a4919d84f6695c81105faafc06727fe56dd
Changeset: http://git.rtems.org/rtems-tools/commit/?id=3a867a4919d84f6695c81105faafc06727fe56dd

Author:    Chris Johns <chrisj at rtems.org>
Date:      Thu Sep 21 18:26:20 2017 +1000

Add TFTP as a back end option for testing. Add telnet as a console option.

TFTP runs a local TFTP server on port 69 or another specified port and
serves each test for any requested file.

Telnet is now a console option.

---

 tester/rt/config.py                          |  89 +++-
 tester/rt/console.py                         |  36 +-
 tester/rt/report.py                          |  21 +-
 tester/rt/stty.py                            |  19 +-
 tester/rt/telnet.py                          |  99 +++++
 tester/rt/test.py                            |  50 +--
 tester/rt/tftp.py                            | 208 ++++++++++
 tester/rt/tftpy/COPYING                      |  21 +
 tester/rt/tftpy/README                       | 115 ++++++
 tester/rt/tftpy/TftpClient.py                | 102 +++++
 tester/rt/tftpy/TftpContexts.py              | 406 ++++++++++++++++++
 tester/rt/tftpy/TftpPacketFactory.py         |  42 ++
 tester/rt/tftpy/TftpPacketTypes.py           | 461 +++++++++++++++++++++
 tester/rt/tftpy/TftpServer.py                | 254 ++++++++++++
 tester/rt/tftpy/TftpShared.py                |  88 ++++
 tester/rt/tftpy/TftpStates.py                | 598 +++++++++++++++++++++++++++
 tester/rt/tftpy/__init__.py                  |  26 ++
 tester/rtems/testing/bsps/beagleboneblack.mc |  57 +++
 tester/rtems/testing/tftp.cfg                |  57 +++
 19 files changed, 2663 insertions(+), 86 deletions(-)

diff --git a/tester/rt/config.py b/tester/rt/config.py
index 579ee08..b423a5a 100644
--- a/tester/rt/config.py
+++ b/tester/rt/config.py
@@ -1,6 +1,6 @@
 #
 # RTEMS Tools Project (http://www.rtems.org/)
-# Copyright 2013-2014 Chris Johns (chrisj at rtems.org)
+# Copyright 2013-2017 Chris Johns (chrisj at rtems.org)
 # All rights reserved.
 #
 # This file is part of the RTEMS Tools package in 'rtems-tools'.
@@ -46,6 +46,7 @@ from rtemstoolkit import path
 
 from . import console
 from . import gdb
+from . import tftp
 
 timeout = 15
 
@@ -54,19 +55,24 @@ class file(config.file):
 
     _directives = ['%execute',
                    '%gdb',
+                   '%tftp',
                    '%console']
 
-    def __init__(self, report, name, opts, _directives = _directives):
+    def __init__(self, index, report, name, opts, _directives = _directives):
         super(file, self).__init__(name, opts, directives = _directives)
         self.lock = threading.Lock()
         self.realtime_trace = self.debug_trace('output')
         self.process = None
         self.console = None
         self.output = None
+        self.index = index
         self.report = report
         self.name = name
         self.timedout = False
+        self.test_started = False
         self.kill_good = False
+        self.kill_on_end = False
+        self.test_label = None
 
     def __del__(self):
         if self.console:
@@ -92,6 +98,29 @@ class file(config.file):
         except:
             pass
 
+    def _target_reset(self):
+        if self.defined('target_reset_command'):
+            reset_cmd = self.expand('%{target_reset_command}').strip()
+            if len(reset_cmd) > 0:
+                rs_proc = execute.capture_execution()
+                ec, proc, output = rs_proc.open(reset_cmd, shell = True)
+                self._capture_console('target reset: %s: %s' % (reset_cmd, output))
+
+    def _output_length(self):
+        self._lock()
+        if self.test_started:
+            l = len(self.output)
+            self._unlock()
+            return l
+        self._unlock()
+        return 0
+
+    def _capture_console(self, text):
+        text = [('>', l) for l in text.replace(chr(13), '').splitlines()]
+        if self.output is not None:
+            self._realtime_trace(text)
+            self.output += text
+
     def _dir_console(self, data):
         if self.console is not None:
             raise error.general(self._name_line_msg('console already configured'))
@@ -152,6 +181,28 @@ class file(config.file):
             if self.console:
                 self.console.close()
 
+    def _dir_tftp(self, data, total, index, exe, bsp_arch, bsp):
+        if len(data) != 2:
+            raise error.general('invalid %tftp arguments')
+        try:
+            port = int(data[1])
+        except:
+            raise error.general('invalid %tftp port')
+        self.kill_on_end = True
+        self.process = tftp.tftp(bsp_arch, bsp,
+                                 trace = self.debug_trace('gdb'))
+        if not self.in_error:
+            if self.console:
+                self.console.open()
+            self.process.open(executable = data[0],
+                              port = port,
+                              output_length = self._output_length,
+                              console = self.capture_console,
+                              timeout = (int(self.expand('%{timeout}')),
+                                         self._timeout))
+            if self.console:
+                self.console.close()
+
     def _directive_filter(self, results, directive, info, data):
         if results[0] == 'directive':
             _directive = results[1]
@@ -168,24 +219,30 @@ class file(config.file):
             else:
                 self._lock()
                 try:
+                    self.output = []
                     total = int(self.expand('%{test_total}'))
                     index = int(self.expand('%{test_index}'))
                     exe = self.expand('%{test_executable}')
                     bsp_arch = self.expand('%{bsp_arch}')
                     bsp = self.expand('%{bsp}')
                     self.report.start(index, total, exe, exe, bsp_arch, bsp)
-                    self.output = []
+                    if self.index == 1:
+                        self._target_reset()
                 finally:
                     self._unlock()
                 if _directive == '%execute':
                     self._dir_execute(ds, total, index, exe, bsp_arch, bsp)
                 elif _directive == '%gdb':
                     self._dir_gdb(ds, total, index, exe, bsp_arch, bsp)
+                elif _directive == '%tftp':
+                    self._dir_tftp(ds, total, index, exe, bsp_arch, bsp)
                 else:
                     raise error.general(self._name_line_msg('invalid directive'))
                 self._lock()
                 try:
-                    self.report.end(exe, self.output)
+                    status = self.report.end(exe, self.output)
+                    if status == 'timeout':
+                        self._target_reset()
                     self.process = None
                     self.output = None
                 finally:
@@ -201,22 +258,36 @@ class file(config.file):
         self.load(self.name)
 
     def capture(self, text):
-        ok_to_kill = '*** TEST STATE: USER_INPUT' in text or '*** TEST STATE: BENCHMARK' in text
+        if not self.test_started:
+            self.test_started = '*** BEGIN OF TEST ' in text
+        ok_to_kill = '*** TEST STATE: USER_INPUT' in text or \
+                     '*** TEST STATE: BENCHMARK' in text
+        reset_target = False
+        if ok_to_kill:
+            reset_target = True
+        if self.kill_on_end:
+            if self.test_label is None:
+                s = text.find('*** BEGIN OF TEST ')
+                if s >= 0:
+                    e = text[s + 3:].find('***')
+                    if e >= 0:
+                        self.test_label = text[s + len('*** BEGIN OF TEST '):s + e + 3 - 1]
+            if not ok_to_kill and self.test_label is not None:
+                ok_to_kill = '*** END OF TEST %s ***' % (self.test_label) in text
         text = [(']', l) for l in text.replace(chr(13), '').splitlines()]
         self._lock()
         if self.output is not None:
             self._realtime_trace(text)
             self.output += text
+        if reset_target:
+            self._target_reset()
         if ok_to_kill:
             self._ok_kill()
         self._unlock()
 
     def capture_console(self, text):
-        text = [('>', l) for l in text.replace(chr(13), '').splitlines()]
         self._lock()
-        if self.output is not None:
-            self._realtime_trace(text)
-            self.output += text
+        self._capture_console(text)
         self._unlock()
 
     def debug_trace(self, flag):
diff --git a/tester/rt/console.py b/tester/rt/console.py
index ca66e66..0545ace 100644
--- a/tester/rt/console.py
+++ b/tester/rt/console.py
@@ -39,14 +39,16 @@ import os
 import threading
 import time
 
+from rtemstoolkit import path
+
+from . import telnet
+
 #
 # Not available on Windows. Not sure what this means.
 #
 if os.name != 'nt':
-    import fcntl
     from . import stty
 else:
-    fcntl = None
     stty = None
 
 def save():
@@ -84,42 +86,27 @@ class stdio(console):
         super(stdio, self).__init__('stdio', trace)
 
 class tty(console):
-    '''TTY console connects to serial ports.'''
-
-    raw = 'B115200,~BRKINT,IGNBRK,IGNCR,~ICANON,~ISIG,~IEXTEN,~ECHO,CLOCAL,~CRTSCTS'
+    '''TTY console connects to the target's console.'''
 
     def __init__(self, dev, output, setup = None, trace = False):
         self.tty = None
         self.read_thread = None
         self.dev = dev
         self.output = output
-        if setup is None:
-            self.setup = raw
-        else:
-            self.setup = setup
+        self.setup = setup
         super(tty, self).__init__(dev, trace)
 
     def __del__(self):
         super(tty, self).__del__()
-        if self._tracing():
-            print(':: tty close', self.dev)
-        if fcntl is not None:
-            fcntl.fcntl(me.tty.fd, fcntl.F_SETFL,
-                        fcntl.fcntl(me.tty.fd, fcntl.F_GETFL) & ~os.O_NONBLOCK)
         self.close()
 
     def open(self):
         def _readthread(me, x):
-            if self._tracing():
-                print(':: tty runner started', self.dev)
-            if fcntl is not None:
-                fcntl.fcntl(me.tty.fd, fcntl.F_SETFL,
-                            fcntl.fcntl(me.tty.fd, fcntl.F_GETFL) | os.O_NONBLOCK)
             line = ''
             while me.running:
                 time.sleep(0.05)
                 try:
-                    data = me.tty.fd.read()
+                    data = me.tty.read()
                 except IOError as ioe:
                     if ioe.errno == errno.EAGAIN:
                         continue
@@ -134,11 +121,10 @@ class tty(console):
                     if c == '\n':
                         me.output(line)
                         line = ''
-            if self._tracing():
-                print(':: tty runner finished', self.dev)
-        if self._tracing():
-            print(':: tty open', self.dev)
-        self.tty = stty.tty(self.dev)
+        if stty and path.exists(self.dev):
+            self.tty = stty.tty(self.dev)
+        else:
+            self.tty = telnet.tty(self.dev)
         self.tty.set(self.setup)
         self.tty.on()
         self.read_thread = threading.Thread(target = _readthread,
diff --git a/tester/rt/report.py b/tester/rt/report.py
index 28a75c7..d5bfd48 100644
--- a/tester/rt/report.py
+++ b/tester/rt/report.py
@@ -40,6 +40,13 @@ from rtemstoolkit import error
 from rtemstoolkit import log
 from rtemstoolkit import path
 
+#
+# Maybe this should be a configuration.
+#
+test_fail_excludes = [
+    'minimum'
+]
+
 class report(object):
     '''RTEMS Testing report.'''
 
@@ -110,14 +117,14 @@ class report(object):
         for line in output:
             if line[0] == ']':
                 if line[1].startswith('*** '):
+                    if line[1][4:].startswith('BEGIN OF '):
+                        start = True
                     if line[1][4:].startswith('END OF '):
                         end = True
                     if line[1][4:].startswith('TEST STATE:'):
                         state = line[1][15:].strip()
                     if line[1][4:].startswith('TIMEOUT TIMEOUT'):
                         timeout = True
-                    else:
-                        start = True
             prefixed_output += [line[0] + ' ' + line[1]]
         self.lock.acquire()
         if name not in self.results:
@@ -140,8 +147,13 @@ class report(object):
                     status = 'failed'
                     self.failed += 1
             else:
-                status = 'invalid'
-                self.invalids += 1
+                exe_name = name.split('.')[0]
+                if exe_name in test_fail_excludes:
+                    status = 'passed'
+                    self.passed += 1
+                else:
+                    status = 'invalid'
+                    self.invalids += 1
         else:
             if state == 'EXPECTED_FAIL':
                 if start and end:
@@ -170,6 +182,7 @@ class report(object):
         if self.name_max_len < len(path.basename(name)):
             self.name_max_len = len(path.basename(name))
         self.lock.release()
+        return status
 
     def log(self, name, mode):
         if mode != 'none':
diff --git a/tester/rt/stty.py b/tester/rt/stty.py
index 55c4ed6..b393dbf 100644
--- a/tester/rt/stty.py
+++ b/tester/rt/stty.py
@@ -32,6 +32,7 @@
 # RTEMS Testing Consoles
 #
 
+import fcntl
 import os
 import sys
 import termios
@@ -59,6 +60,8 @@ def restore(attributes):
 
 class tty:
 
+    raw = 'B115200,~BRKINT,IGNBRK,IGNCR,~ICANON,~ISIG,~IEXTEN,~ECHO,CLOCAL,~CRTSCTS'
+
     def __init__(self, dev):
         if host.is_windows:
             raise error.general('termios not support on host')
@@ -79,12 +82,21 @@ class tty:
         try:
             self.default_attr = termios.tcgetattr(self.fd)
         except:
+            close(self.fd)
             raise error.general('cannot get termios attrs: %s' % (dev))
+        try:
+            fcntl.fcntl(self.fd, fcntl.F_SETFL,
+                        fcntl.fcntl(self.fd, fcntl.F_GETFL) | os.O_NONBLOCK)
+        except:
+            close(self.fd)
+            raise error.general('cannot make tty non-blocking: %s' % (dev))
         self.attr = self.default_attr
 
     def __del__(self):
         if self.fd and self.default_attr:
             try:
+                fcntl.fcntl(self.fd, fcntl.F_SETFL,
+                            fcntl.fcntl(self.fd, fcntl.F_GETFL) & ~os.O_NONBLOCK)
                 self.fd.close()
             except:
                 pass
@@ -487,7 +499,9 @@ class tty:
     def vtime(self, _vtime):
         self.attr[6][termios.VTIME] = _vtime
 
-    def set(self, flags):
+    def set(self, flags = None):
+        if flags is None:
+            flags = self.raw
         for f in flags.split(','):
             if len(f) < 2:
                 raise error.general('invalid flag: %s' % (f))
@@ -543,6 +557,9 @@ class tty:
             raise error.general('unknown tty flag: %s' % (f))
         self._update()
 
+    def read(self):
+        return self.fs.read()
+
 if __name__ == "__main__":
     if len(sys.argv) == 2:
         t = tty(sys.argv[1])
diff --git a/tester/rt/telnet.py b/tester/rt/telnet.py
new file mode 100644
index 0000000..a7f16cd
--- /dev/null
+++ b/tester/rt/telnet.py
@@ -0,0 +1,99 @@
+#
+# RTEMS Tools Project (http://www.rtems.org/)
+# Copyright 2013-2017 Chris Johns (chrisj at rtems.org)
+# All rights reserved.
+#
+# This file is part of the RTEMS Tools package in 'rtems-tools'.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+
+#
+# RTEMS Testing Consoles
+#
+
+import os
+import sys
+import telnetlib
+
+from rtemstoolkit import error
+from rtemstoolkit import host
+from rtemstoolkit import path
+
+class tty:
+
+    def __init__(self, dev):
+        self.dev = dev
+        self.conn = None
+        ds = dev.split(':')
+        self.host = ds[0]
+        if len(ds) == 1:
+            self.port = 23
+        else:
+            try:
+                self.port = int(ds[1])
+            except:
+                raise error.general('invalid port: %s' % (dev))
+        try:
+            self.conn = telnetlib.Telnet(self.host, self.port, 5)
+        except IOError as ioe:
+            raise error.general('opening telnet dev: %s: %s' % (dev, ioe))
+        except:
+            raise error.general('opening telnet dev: %s: unknown' % (dev))
+
+    def __del__(self):
+        if self.conn:
+            try:
+                self.conn.close()
+            except:
+                pass
+
+    def __str__(self):
+        s = 'host: %s port: %d' % ((self.host, self.port))
+        return s
+
+    def off(self):
+        self.is_on = False
+
+    def on(self):
+        self.is_on = True
+
+    def set(self, flags):
+        pass
+
+    def read(self):
+        try:
+            data = self.conn.read_very_eager()
+        except EOFError:
+            data = ''
+        return data
+
+if __name__ == "__main__":
+    if len(sys.argv) == 2:
+        import time
+        t = tty(sys.argv[1])
+        print(t)
+        while True:
+            time.sleep(0.05)
+            c = t.read()
+            sys.stdout.write(c)
diff --git a/tester/rt/test.py b/tester/rt/test.py
index 7a35f59..c3d8069 100644
--- a/tester/rt/test.py
+++ b/tester/rt/test.py
@@ -41,6 +41,7 @@ import time
 from rtemstoolkit import error
 from rtemstoolkit import log
 from rtemstoolkit import path
+from rtemstoolkit import reraise
 from rtemstoolkit import stacktraces
 from rtemstoolkit import version
 
@@ -50,51 +51,6 @@ from . import console
 from . import options
 from . import report
 
-#
-# The following fragment is taken from https://bitbucket.org/gutworth/six
-# to raise an exception. The python2 code cause a syntax error with python3.
-#
-# Copyright (c) 2010-2016 Benjamin Peterson
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
-# Taken from six.
-#
-if sys.version_info[0] == 3:
-    def _test_reraise(tp, value, tb = None):
-        raise value.with_traceback(tb)
-else:
-    def exec_(_code_, _globs_ = None, _locs_ = None):
-        if _globs_ is None:
-            frame = sys._getframe(1)
-            _globs_ = frame.f_globals
-            if _locs_ is None:
-                _locs_ = frame.f_locals
-            del frame
-        elif _locs_ is None:
-            _locs_ = _globs_
-        exec("""exec _code_ in _globs_, _locs_""")
-
-    exec_("""def _test_reraise(tp, value, tb = None):
-    raise tp, value, tb
-""")
-
 class test(object):
     def __init__(self, index, total, report, executable, rtems_tools, bsp, bsp_config, opts):
         self.index = index
@@ -116,7 +72,7 @@ class test(object):
             if not path.isdir(rtems_tools_bin):
                 raise error.general('cannot find RTEMS tools path: %s' % (rtems_tools_bin))
             self.opts.defaults['rtems_tools'] = rtems_tools_bin
-        self.config = config.file(self.report, self.bsp_config, self.opts)
+        self.config = config.file(index, self.report, self.bsp_config, self.opts)
 
     def run(self):
         if self.config:
@@ -165,7 +121,7 @@ class test_run(object):
 
     def reraise(self):
         if self.result is not None:
-            _test_reraise(*self.result)
+            reraise.reraise(*self.result)
 
     def kill(self):
         if self.test:
diff --git a/tester/rt/tftp.py b/tester/rt/tftp.py
new file mode 100644
index 0000000..ae0c4f3
--- /dev/null
+++ b/tester/rt/tftp.py
@@ -0,0 +1,208 @@
+#
+# RTEMS Tools Project (http://www.rtems.org/)
+# Copyright 2013-2017 Chris Johns (chrisj at rtems.org)
+# All rights reserved.
+#
+# This file is part of the RTEMS Tools package in 'rtems-tools'.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+
+#
+# RTEMS Testing TFTP Interface
+#
+
+from __future__ import print_function
+
+import errno
+import logging
+import threading
+import time
+import sys
+
+from rtemstoolkit import error
+from rtemstoolkit import reraise
+
+#
+# Support to handle use in a package and as a unit test.
+# If there is a better way to let us know.
+#
+try:
+    from . import tftpy
+except (ValueError, SystemError):
+    import tftpy
+
+class tftp(object):
+    '''RTEMS Testing TFTP base.'''
+
+    def __init__(self, bsp_arch, bsp, trace = False):
+        self.trace = trace
+        self.lock_trace = False
+        self.lock = threading.RLock()
+        self.bsp = bsp
+        self.bsp_arch = bsp_arch
+        self._init()
+
+    def __del__(self):
+        self.kill()
+
+    def _init(self):
+        self.output_length = None
+        self.console = None
+        self.server = None
+        self.port = 0
+        self.exe = None
+        self.timeout = None
+        self.timer = None
+        self.running = False
+        self.finished = False
+        self.caught = None
+
+    def _lock(self, msg):
+        if self.lock_trace:
+            print('|[   LOCK:%s ]|' % (msg))
+        self.lock.acquire()
+
+    def _unlock(self, msg):
+        if self.lock_trace:
+            print('|] UNLOCK:%s [|' % (msg))
+        self.lock.release()
+
+    def _finished(self):
+        self.server = None
+        self.exe = None
+
+    def _stop(self):
+        try:
+            if self.server:
+                self.server.stop(now = True)
+        except:
+            pass
+
+    def _kill(self):
+        self._stop()
+        while self.running or not self.finished:
+            self._unlock('_kill')
+            time.sleep(0.1)
+            self._lock('_kill')
+
+    def _timeout(self):
+        self._stop()
+        if self.timeout is not None:
+            self.timeout()
+
+    def _exe_handle(self, req_file, raddress, rport):
+        self._lock('_exe_handle')
+        exe = self.exe
+        self.exe = None
+        self._unlock('_exe_handle')
+        if exe is not None:
+            if self.console:
+                self.console('tftp: %s' % (exe))
+            return open(exe, "rb")
+        self._stop()
+        return None
+
+    def _listener(self):
+        tftpy.log.setLevel(100)
+        try:
+            self.server = tftpy.TftpServer(tftproot = '.',
+                                           dyn_file_func = self._exe_handle)
+        except tftpy.TftpException as te:
+            raise error.general('tftp: %s' % (str(te)))
+        if self.server is not None:
+            try:
+                self.server.listen('0.0.0.0', self.port, 0.5)
+            except tftpy.TftpException as te:
+                raise error.general('tftp: %s' % (str(te)))
+            except IOError as ie:
+                if ie.errno == errno.EACCES:
+                    raise error.general('tftp: permissions error: check tftp server port')
+                raise error.general('tftp: io error: %s' % (str(ie)))
+
+    def _runner(self):
+        self._lock('_runner')
+        self.running = True
+        self._unlock('_runner')
+        caught = None
+        try:
+            self._listener()
+        except:
+            caught = sys.exc_info()
+        self._lock('_runner')
+        self._init()
+        self.running = False
+        self.finished = True
+        self.caught = caught
+        self._unlock('_runner')
+
+    def open(self, executable, port, output_length, console, timeout):
+        self._lock('_open')
+        if self.exe is not None:
+            self._unlock('_open')
+            raise error.general('tftp: already running')
+        self._init()
+        self.output_length = output_length
+        self.console = console
+        self.port = port
+        self.exe = executable
+        self.timeout = timeout[1]
+        self.listener = threading.Thread(target = self._runner,
+                                         name = 'tftp-listener')
+        self.listener.start()
+        step = 1.0
+        period = timeout[0]
+        output_len = self.output_length()
+        while not self.finished and period > 0:
+            current_length = self.output_length()
+            if output_length != current_length:
+                period = timeout[0]
+            output_length = current_length
+            if period < step:
+                period = 0
+            else:
+                period -= step
+            self._unlock('_open')
+            time.sleep(step)
+            self._lock('_open')
+        if not self.finished and period == 0:
+            self._timeout()
+        caught = self.caught
+        self.caught = None
+        self._unlock('_open')
+        if caught is not None:
+            reraise.reraise(*caught)
+
+    def kill(self):
+        self._lock('kill')
+        self._kill()
+        self._unlock('kill')
+
+if __name__ == "__main__":
+    import sys
+    if len(sys.argv) > 1:
+        executable = sys.argv[1]
+    else:
+        executable = None
+    t = tftp('arm', 'beagleboneblack')
+    t.open(executable)
diff --git a/tester/rt/tftpy/COPYING b/tester/rt/tftpy/COPYING
new file mode 100644
index 0000000..c9f2c9c
--- /dev/null
+++ b/tester/rt/tftpy/COPYING
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2009 Michael P. Soulier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/tester/rt/tftpy/README b/tester/rt/tftpy/README
new file mode 100644
index 0000000..ad7a871
--- /dev/null
+++ b/tester/rt/tftpy/README
@@ -0,0 +1,115 @@
+Copyright, Michael P. Soulier, 2010.
+
+About Release 0.6.2:
+====================
+Maintenance release to fix a couple of reported issues.
+
+About Release 0.6.1:
+====================
+Maintenance release to fix several reported problems, including a rollover
+at 2^16 blocks, and some contributed work on dynamic file objects.
+
+About Release 0.6.0:
+====================
+Maintenance update to fix several reported issues, including proper
+retransmits on timeouts, and further expansion of unit tests.
+
+About Release 0.5.1:
+====================
+Maintenance update to fix a bug in the server, overhaul the documentation for
+the website, fix a typo in the unit tests, fix a failure to set default
+blocksize, and a divide by zero error in speed calculations for very short
+transfers.
+
+Also, this release adds support for input/output in client as stdin/stdout
+
+About Release 0.5.0:
+====================
+Complete rewrite of the state machine.
+Now fully implements downloading and uploading.
+
+About Release 0.4.6:
+====================
+Feature release to add the tsize option. 
+Thanks to Kuba Kończyk for the patch.
+
+About Release 0.4.5:
+====================
+Bugfix release for compatability issues on Win32, among other small issues.
+
+About Release 0.4.4:
+====================
+Bugfix release for poor tolerance of unsupported options in the server.
+
+About Release 0.4.3:
+====================
+Bugfix release for an issue with the server's detection of the end of the file
+during a download.
+
+About Release 0.4.2:
+====================
+Bugfix release for some small installation issues with earlier Python
+releases.
+
+About Release 0.4.1:
+====================
+Bugfix release to fix the installation path, with some restructuring into a
+tftpy package from the single module used previously.
+
+About Release 0.4:
+==================
+This release adds a TftpServer class with a sample implementation in bin.
+The server uses a single thread with multiple handlers and a select() loop to
+handle multiple clients simultaneously.
+
+Only downloads are supported at this time.
+
+About Release 0.3:
+==================
+This release fixes a major RFC 1350 compliance problem with the remote TID.
+
+About Release 0.2:
+==================
+This release adds variable block sizes, and general option support,
+implementing RFCs 2347 and 2348. This is accessible in the TftpClient class
+via the options dict, or in the sample client via the --blocksize option.
+
+About Release 0.1:
+==================
+
+This is an initial release in the spirit of "release early, release often".
+Currently the sample client works, supporting RFC 1350. The server is not yet
+implemented, and RFC 2347 and 2348 support (variable block sizes) is underway,
+planned for 0.2.
+
+About Tftpy:
+============
+
+Purpose:
+--------
+Tftpy is a TFTP library for the Python programming language. It includes
+client and server classes, with sample implementations. Hooks are included for
+easy inclusion in a UI for populating progress indicators. It supports RFCs
+1350, 2347, 2348 and the tsize option from RFC 2349.
+
+Dependencies:
+-------------
+Python 2.3+, hopefully. Let me know if it fails to work.
+
+Trifles:
+--------
+Home page: http://tftpy.sf.net/
+Project page: http://sourceforge.net/projects/tftpy/
+
+License is the MIT License
+
+See COPYING in this distribution.
+
+Limitations:
+------------
+- Only 'octet' mode is supported.
+- The only options supported are blksize and tsize.
+
+Author:
+=======
+Michael P. Soulier <msoulier at digitaltorque.ca>
diff --git a/tester/rt/tftpy/TftpClient.py b/tester/rt/tftpy/TftpClient.py
new file mode 100644
index 0000000..2763dda
--- /dev/null
+++ b/tester/rt/tftpy/TftpClient.py
@@ -0,0 +1,102 @@
+"""This module implements the TFTP Client functionality. Instantiate an
+instance of the client, and then use its upload or download method. Logging is
+performed via a standard logging object set in TftpShared."""
+
+from __future__ import absolute_import, division, print_function, unicode_literals
+import types
+from .TftpShared import *
+from .TftpPacketTypes import *
+from .TftpContexts import TftpContextClientDownload, TftpContextClientUpload
+
+class TftpClient(TftpSession):
+    """This class is an implementation of a tftp client. Once instantiated, a
+    download can be initiated via the download() method, or an upload via the
+    upload() method."""
+
+    def __init__(self, host, port=69, options={}, localip = ""):
+        TftpSession.__init__(self)
+        self.context = None
+        self.host = host
+        self.iport = port
+        self.filename = None
+        self.options = options
+        self.localip = localip
+        if 'blksize' in self.options:
+            size = self.options['blksize']
+            tftpassert(types.IntType == type(size), "blksize must be an int")
+            if size < MIN_BLKSIZE or size > MAX_BLKSIZE:
+                raise TftpException("Invalid blksize: %d" % size)
+
+    def download(self, filename, output, packethook=None, timeout=SOCK_TIMEOUT):
+        """This method initiates a tftp download from the configured remote
+        host, requesting the filename passed. It writes the file to output,
+        which can be a file-like object or a path to a local file. If a
+        packethook is provided, it must be a function that takes a single
+        parameter, which will be a copy of each DAT packet received in the
+        form of a TftpPacketDAT object. The timeout parameter may be used to
+        override the default SOCK_TIMEOUT setting, which is the amount of time
+        that the client will wait for a receive packet to arrive.
+
+        Note: If output is a hyphen, stdout is used."""
+        # We're downloading.
+        log.debug("Creating download context with the following params:")
+        log.debug("host = %s, port = %s, filename = %s" % (self.host, self.iport, filename))
+        log.debug("options = %s, packethook = %s, timeout = %s" % (self.options, packethook, timeout))
+        self.context = TftpContextClientDownload(self.host,
+                                                 self.iport,
+                                                 filename,
+                                                 output,
+                                                 self.options,
+                                                 packethook,
+                                                 timeout,
+                                                 localip = self.localip)
+        self.context.start()
+        # Download happens here
+        self.context.end()
+
+        metrics = self.context.metrics
+
+        log.info('')
+        log.info("Download complete.")
+        if metrics.duration == 0:
+            log.info("Duration too short, rate undetermined")
+        else:
+            log.info("Downloaded %.2f bytes in %.2f seconds" % (metrics.bytes, metrics.duration))
+            log.info("Average rate: %.2f kbps" % metrics.kbps)
+        log.info("%.2f bytes in resent data" % metrics.resent_bytes)
+        log.info("Received %d duplicate packets" % metrics.dupcount)
+
+    def upload(self, filename, input, packethook=None, timeout=SOCK_TIMEOUT):
+        """This method initiates a tftp upload to the configured remote host,
+        uploading the filename passed. It reads the file from input, which
+        can be a file-like object or a path to a local file. If a packethook
+        is provided, it must be a function that takes a single parameter,
+        which will be a copy of each DAT packet sent in the form of a
+        TftpPacketDAT object. The timeout parameter may be used to override
+        the default SOCK_TIMEOUT setting, which is the amount of time that
+        the client will wait for a DAT packet to be ACKd by the server.
+
+        Note: If input is a hyphen, stdin is used."""
+        self.context = TftpContextClientUpload(self.host,
+                                               self.iport,
+                                               filename,
+                                               input,
+                                               self.options,
+                                               packethook,
+                                               timeout,
+                                               localip = self.localip)
+        self.context.start()
+        # Upload happens here
+        self.context.end()
+
+        metrics = self.context.metrics
+
+        log.info('')
+        log.info("Upload complete.")
+        if metrics.duration == 0:
+            log.info("Duration too short, rate undetermined")
+        else:
+            log.info("Uploaded %d bytes in %.2f seconds" % (metrics.bytes, metrics.duration))
+            log.info("Average rate: %.2f kbps" % metrics.kbps)
+        log.info("%.2f bytes in resent data" % metrics.resent_bytes)
+        log.info("Resent %d packets" % metrics.dupcount)
diff --git a/tester/rt/tftpy/TftpContexts.py b/tester/rt/tftpy/TftpContexts.py
new file mode 100644
index 0000000..271441b
--- /dev/null
+++ b/tester/rt/tftpy/TftpContexts.py
@@ -0,0 +1,406 @@
+"""This module implements all contexts for state handling during uploads and
+downloads, the main interface to which being the TftpContext base class.
+
+The concept is simple. Each context object represents a single upload or
+download, and the state object in the context object represents the current
+state of that transfer. The state object has a handle() method that expects
+the next packet in the transfer, and returns a state object until the transfer
+is complete, at which point it returns None. That is, unless there is a fatal
+error, in which case a TftpException is returned instead."""
+
+from __future__ import absolute_import, division, print_function, unicode_literals
+from .TftpShared import *
+from .TftpPacketTypes import *
+from .TftpPacketFactory import TftpPacketFactory
+from .TftpStates import *
+import socket, time, sys
+
+###############################################################################
+# Utility classes
+###############################################################################
+
+class TftpMetrics(object):
+    """A class representing metrics of the transfer."""
+    def __init__(self):
+        # Bytes transferred
+        self.bytes = 0
+        # Bytes re-sent
+        self.resent_bytes = 0
+        # Duplicate packets received
+        self.dups = {}
+        self.dupcount = 0
+        # Times
+        self.start_time = 0
+        self.end_time = 0
+        self.duration = 0
+        # Rates
+        self.bps = 0
+        self.kbps = 0
+        # Generic errors
+        self.errors = 0
+
+    def compute(self):
+        # Compute transfer time
+        self.duration = self.end_time - self.start_time
+        if self.duration == 0:
+            self.duration = 1
+        log.debug("TftpMetrics.compute: duration is %s", self.duration)
+        self.bps = (self.bytes * 8.0) / self.duration
+        self.kbps = self.bps / 1024.0
+        log.debug("TftpMetrics.compute: kbps is %s", self.kbps)
+        for key in self.dups:
+            self.dupcount += self.dups[key]
+
+    def add_dup(self, pkt):
+        """This method adds a dup for a packet to the metrics."""
+        log.debug("Recording a dup of %s", pkt)
+        s = str(pkt)
+        if s in self.dups:
+            self.dups[s] += 1
+        else:
+            self.dups[s] = 1
+        tftpassert(self.dups[s] < MAX_DUPS, "Max duplicates reached")
+
+###############################################################################
+# Context classes
+###############################################################################
+
+class TftpContext(object):
+    """The base class of the contexts."""
+
+    def __init__(self, host, port, timeout, localip = ""):
+        """Constructor for the base context, setting shared instance
+        variables."""
+        self.file_to_transfer = None
+        self.fileobj = None
+        self.options = None
+        self.packethook = None
+        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        if localip != "":
+            self.sock.bind((localip, 0))
+        self.sock.settimeout(timeout)
+        self.timeout = timeout
+        self.state = None
+        self.next_block = 0
+        self.factory = TftpPacketFactory()
+        # Note, setting the host will also set self.address, as it's a property.
+        self.host = host
+        self.port = port
+        # The port associated with the TID
+        self.tidport = None
+        # Metrics
+        self.metrics = TftpMetrics()
+        # Fluag when the transfer is pending completion.
+        self.pending_complete = False
+        # Time when this context last received any traffic.
+        # FIXME: does this belong in metrics?
+        self.last_update = 0
+        # The last packet we sent, if applicable, to make resending easy.
+        self.last_pkt = None
+        # Count the number of retry attempts.
+        self.retry_count = 0
+
+    def getBlocksize(self):
+        """Fetch the current blocksize for this session."""
+        return int(self.options.get('blksize', 512))
+
+    def __del__(self):
+        """Simple destructor to try to call housekeeping in the end method if
+        not called explicitely. Leaking file descriptors is not a good
+        thing."""
+        self.end()
+
+    def checkTimeout(self, now):
+        """Compare current time with last_update time, and raise an exception
+        if we're over the timeout time."""
+        log.debug("checking for timeout on session %s", self)
+        if now - self.last_update > self.timeout:
+            raise TftpTimeout("Timeout waiting for traffic")
+
+    def start(self):
+        raise NotImplementedError("Abstract method")
+
+    def end(self):
+        """Perform session cleanup, since the end method should always be
+        called explicitely by the calling code, this works better than the
+        destructor."""
+        log.debug("in TftpContext.end")
+        self.sock.close()
+        if self.fileobj is not None and not self.fileobj.closed:
+            log.debug("self.fileobj is open - closing")
+            self.fileobj.close()
+
+    def gethost(self):
+        "Simple getter method for use in a property."
+        return self.__host
+
+    def sethost(self, host):
+        """Setter method that also sets the address property as a result
+        of the host that is set."""
+        self.__host = host
+        self.address = socket.gethostbyname(host)
+
+    host = property(gethost, sethost)
+
+    def setNextBlock(self, block):
+        if block >= 2 ** 16:
+            log.debug("Block number rollover to 0 again")
+            block = 0
+        self.__eblock = block
+
+    def getNextBlock(self):
+        return self.__eblock
+
+    next_block = property(getNextBlock, setNextBlock)
+
+    def cycle(self):
+        """Here we wait for a response from the server after sending it
+        something, and dispatch appropriate action to that response."""
+        try:
+            (buffer, (raddress, rport)) = self.sock.recvfrom(MAX_BLKSIZE)
+        except socket.timeout:
+            log.warn("Timeout waiting for traffic, retrying...")
+            raise TftpTimeout("Timed-out waiting for traffic")
+
+        # Ok, we've received a packet. Log it.
+        log.debug("Received %d bytes from %s:%s",
+                        len(buffer), raddress, rport)
+        # And update our last updated time.
+        self.last_update = time.time()
+
+        # Decode it.
+        recvpkt = self.factory.parse(buffer)
+
+        # Check for known "connection".
+        if raddress != self.address:
+            log.warn("Received traffic from %s, expected host %s. Discarding"
+                        % (raddress, self.host))
+
+        if self.tidport and self.tidport != rport:
+            log.warn("Received traffic from %s:%s but we're "
+                        "connected to %s:%s. Discarding."
+                        % (raddress, rport,
+                        self.host, self.tidport))
+
+        # If there is a packethook defined, call it. We unconditionally
+        # pass all packets, it's up to the client to screen out different
+        # kinds of packets. This way, the client is privy to things like
+        # negotiated options.
+        if self.packethook:
+            self.packethook(recvpkt)
+
+        # And handle it, possibly changing state.
+        self.state = self.state.handle(recvpkt, raddress, rport)
+        # If we didn't throw any exceptions here, reset the retry_count to
+        # zero.
+        self.retry_count = 0
+
+class TftpContextServer(TftpContext):
+    """The context for the server."""
+    def __init__(self,
+                 host,
+                 port,
+                 timeout,
+                 root,
+                 dyn_file_func=None,
+                 upload_open=None):
+        TftpContext.__init__(self,
+                             host,
+                             port,
+                             timeout,
+                             )
+        # At this point we have no idea if this is a download or an upload. We
+        # need to let the start state determine that.
+        self.state = TftpStateServerStart(self)
+
+        self.root = root
+        self.dyn_file_func = dyn_file_func
+        self.upload_open = upload_open
+
+    def __str__(self):
+        return "%s:%s %s" % (self.host, self.port, self.state)
+
+    def start(self, buffer):
+        """Start the state cycle. Note that the server context receives an
+        initial packet in its start method. Also note that the server does not
+        loop on cycle(), as it expects the TftpServer object to manage
+        that."""
+        log.debug("In TftpContextServer.start")
+        self.metrics.start_time = time.time()
+        log.debug("Set metrics.start_time to %s", self.metrics.start_time)
+        # And update our last updated time.
+        self.last_update = time.time()
+
+        pkt = self.factory.parse(buffer)
+        log.debug("TftpContextServer.start() - factory returned a %s", pkt)
+
+        # Call handle once with the initial packet. This should put us into
+        # the download or the upload state.
+        self.state = self.state.handle(pkt,
+                                       self.host,
+                                       self.port)
+
+    def end(self):
+        """Finish up the context."""
+        TftpContext.end(self)
+        self.metrics.end_time = time.time()
+        log.debug("Set metrics.end_time to %s", self.metrics.end_time)
+        self.metrics.compute()
+
+class TftpContextClientUpload(TftpContext):
+    """The upload context for the client during an upload.
+    Note: If input is a hyphen, then we will use stdin."""
+    def __init__(self,
+                 host,
+                 port,
+                 filename,
+                 input,
+                 options,
+                 packethook,
+                 timeout,
+                 localip = ""):
+        TftpContext.__init__(self,
+                             host,
+                             port,
+                             timeout,
+                             localip)
+        self.file_to_transfer = filename
+        self.options = options
+        self.packethook = packethook
+        # If the input object has a read() function,
+        # assume it is file-like.
+        if hasattr(input, 'read'):
+            self.fileobj = input
+        elif input == '-':
+            self.fileobj = sys.stdin
+        else:
+            self.fileobj = open(input, "rb")
+
+        log.debug("TftpContextClientUpload.__init__()")
+        log.debug("file_to_transfer = %s, options = %s" %
+            (self.file_to_transfer, self.options))
+
+    def __str__(self):
+        return "%s:%s %s" % (self.host, self.port, self.state)
+
+    def start(self):
+        log.info("Sending tftp upload request to %s" % self.host)
+        log.info("    filename -> %s" % self.file_to_transfer)
+        log.info("    options -> %s" % self.options)
+
+        self.metrics.start_time = time.time()
+        log.debug("Set metrics.start_time to %s" % self.metrics.start_time)
+
+        # FIXME: put this in a sendWRQ method?
+        pkt = TftpPacketWRQ()
+        pkt.filename = self.file_to_transfer
+        pkt.mode = "octet" # FIXME - shouldn't hardcode this
+        pkt.options = self.options
+        self.sock.sendto(pkt.encode().buffer, (self.host, self.port))
+        self.next_block = 1
+        self.last_pkt = pkt
+        # FIXME: should we centralize sendto operations so we can refactor all
+        # saving of the packet to the last_pkt field?
+
+        self.state = TftpStateSentWRQ(self)
+
+        while self.state:
+            try:
+                log.debug("State is %s" % self.state)
+                self.cycle()
+            except TftpTimeout as err:
+                log.error(str(err))
+                self.retry_count += 1
+                if self.retry_count >= TIMEOUT_RETRIES:
+                    log.debug("hit max retries, giving up")
+                    raise
+                else:
+                    log.warn("resending last packet")
+                    self.state.resendLast()
+
+    def end(self):
+        """Finish up the context."""
+        TftpContext.end(self)
+        self.metrics.end_time = time.time()
+        log.debug("Set metrics.end_time to %s" % self.metrics.end_time)
+        self.metrics.compute()
+
+
+class TftpContextClientDownload(TftpContext):
+    """The download context for the client during a download.
+    Note: If output is a hyphen, then the output will be sent to stdout."""
+    def __init__(self,
+                 host,
+                 port,
+                 filename,
+                 output,
+                 options,
+                 packethook,
+                 timeout,
+                 localip = ""):
+        TftpContext.__init__(self,
+                             host,
+                             port,
+                             timeout,
+                             localip)
+        # FIXME: should we refactor setting of these params?
+        self.file_to_transfer = filename
+        self.options = options
+        self.packethook = packethook
+        # If the output object has a write() function,
+        # assume it is file-like.
+        if hasattr(output, 'write'):
+            self.fileobj = output
+        # If the output filename is -, then use stdout
+        elif output == '-':
+            self.fileobj = sys.stdout
+        else:
+            self.fileobj = open(output, "wb")
+
+        log.debug("TftpContextClientDownload.__init__()")
+        log.debug("file_to_transfer = %s, options = %s" %
+            (self.file_to_transfer, self.options))
+
+    def __str__(self):
+        return "%s:%s %s" % (self.host, self.port, self.state)
+
+    def start(self):
+        """Initiate the download."""
+        log.info("Sending tftp download request to %s" % self.host)
+        log.info("    filename -> %s" % self.file_to_transfer)
+        log.info("    options -> %s" % self.options)
+
+        self.metrics.start_time = time.time()
+        log.debug("Set metrics.start_time to %s" % self.metrics.start_time)
+
+        # FIXME: put this in a sendRRQ method?
+        pkt = TftpPacketRRQ()
+        pkt.filename = self.file_to_transfer
+        pkt.mode = "octet" # FIXME - shouldn't hardcode this
+        pkt.options = self.options
+        self.sock.sendto(pkt.encode().buffer, (self.host, self.port))
+        self.next_block = 1
+        self.last_pkt = pkt
+
+        self.state = TftpStateSentRRQ(self)
+
+        while self.state:
+            try:
+                log.debug("State is %s" % self.state)
+                self.cycle()
+            except TftpTimeout as err:
+                log.error(str(err))
+                self.retry_count += 1
+                if self.retry_count >= TIMEOUT_RETRIES:
+                    log.debug("hit max retries, giving up")
+                    raise
+                else:
+                    log.warn("resending last packet")
+                    self.state.resendLast()
+
+    def end(self):
+        """Finish up the context."""
+        TftpContext.end(self)
+        self.metrics.end_time = time.time()
+        log.debug("Set metrics.end_time to %s" % self.metrics.end_time)
+        self.metrics.compute()
diff --git a/tester/rt/tftpy/TftpPacketFactory.py b/tester/rt/tftpy/TftpPacketFactory.py
new file mode 100644
index 0000000..928fe07
--- /dev/null
+++ b/tester/rt/tftpy/TftpPacketFactory.py
@@ -0,0 +1,42 @@
+"""This module implements the TftpPacketFactory class, which can take a binary
+buffer, and return the appropriate TftpPacket object to represent it, via the
+parse() method."""
+
+from __future__ import absolute_import, division, print_function, unicode_literals
+from .TftpShared import *
+from .TftpPacketTypes import *
+
+class TftpPacketFactory(object):
+    """This class generates TftpPacket objects. It is responsible for parsing
+    raw buffers off of the wire and returning objects representing them, via
+    the parse() method."""
+    def __init__(self):
+        self.classes = {
+            1: TftpPacketRRQ,
+            2: TftpPacketWRQ,
+            3: TftpPacketDAT,
+            4: TftpPacketACK,
+            5: TftpPacketERR,
+            6: TftpPacketOACK
+            }
+
+    def parse(self, buffer):
+        """This method is used to parse an existing datagram into its
+        corresponding TftpPacket object. The buffer is the raw bytes off of
+        the network."""
+        log.debug("parsing a %d byte packet" % len(buffer))
+        (opcode,) = struct.unpack("!H", buffer[:2])
+        log.debug("opcode is %d" % opcode)
+        packet = self.__create(opcode)
+        packet.buffer = buffer
+        return packet.decode()
+
+    def __create(self, opcode):
+        """This method returns the appropriate class object corresponding to
+        the passed opcode."""
+        tftpassert(opcode in self.classes,
+                   "Unsupported opcode: %d" % opcode)
+
+        packet = self.classes[opcode]()
+
+        return packet
diff --git a/tester/rt/tftpy/TftpPacketTypes.py b/tester/rt/tftpy/TftpPacketTypes.py
new file mode 100644
index 0000000..e45bb02
--- /dev/null
+++ b/tester/rt/tftpy/TftpPacketTypes.py
@@ -0,0 +1,461 @@
+"""This module implements the packet types of TFTP itself, and the
+corresponding encode and decode methods for them."""
+
+from __future__ import absolute_import, division, print_function, unicode_literals
+import struct
+import sys
+from .TftpShared import *
+
+class TftpSession(object):
+    """This class is the base class for the tftp client and server. Any shared
+    code should be in this class."""
+    # FIXME: do we need this anymore?
+    pass
+
+class TftpPacketWithOptions(object):
+    """This class exists to permit some TftpPacket subclasses to share code
+    regarding options handling. It does not inherit from TftpPacket, as the
+    goal is just to share code here, and not cause diamond inheritance."""
+
+    def __init__(self):
+        self.options = {}
+
+    def setoptions(self, options):
+        log.debug("in TftpPacketWithOptions.setoptions")
+        log.debug("options: %s" % options)
+        myoptions = {}
+        for key in options:
+            newkey = str(key)
+            myoptions[newkey] = str(options[key])
+            log.debug("populated myoptions with %s = %s"
+                         % (newkey, myoptions[newkey]))
+
+        log.debug("setting options hash to: %s" % myoptions)
+        self._options = myoptions
+
+    def getoptions(self):
+        log.debug("in TftpPacketWithOptions.getoptions")
+        return self._options
+
+    # Set up getter and setter on options to ensure that they are the proper
+    # type. They should always be strings, but we don't need to force the
+    # client to necessarily enter strings if we can avoid it.
+    options = property(getoptions, setoptions)
+
+    def decode_options(self, buffer):
+        """This method decodes the section of the buffer that contains an
+        unknown number of options. It returns a dictionary of option names and
+        values."""
+        format = "!"
+        options = {}
+
+        log.debug("decode_options: buffer is: %s" % repr(buffer))
+        log.debug("size of buffer is %d bytes" % len(buffer))
+        if len(buffer) == 0:
+            log.debug("size of buffer is zero, returning empty hash")
+            return {}
+
+        # Count the nulls in the buffer. Each one terminates a string.
+        log.debug("about to iterate options buffer counting nulls")
+        length = 0
+        for c in buffer:
+            if ord(c) == 0:
+                log.debug("found a null at length %d" % length)
+                if length > 0:
+                    format += "%dsx" % length
+                    length = -1
+                else:
+                    raise TftpException("Invalid options in buffer")
+            length += 1
+
+        log.debug("about to unpack, format is: %s" % format)
+        mystruct = struct.unpack(format, buffer)
+
+        tftpassert(len(mystruct) % 2 == 0,
+                   "packet with odd number of option/value pairs")
+
+        for i in range(0, len(mystruct), 2):
+            log.debug("setting option %s to %s" % (mystruct[i], mystruct[i+1]))
+            options[mystruct[i]] = mystruct[i+1]
+
+        return options
+
+class TftpPacket(object):
+    """This class is the parent class of all tftp packet classes. It is an
+    abstract class, providing an interface, and should not be instantiated
+    directly."""
+    def __init__(self):
+        self.opcode = 0
+        self.buffer = None
+
+    def encode(self):
+        """The encode method of a TftpPacket takes keyword arguments specific
+        to the type of packet, and packs an appropriate buffer in network-byte
+        order suitable for sending over the wire.
+
+        This is an abstract method."""
+        raise NotImplementedError("Abstract method")
+
+    def decode(self):
+        """The decode method of a TftpPacket takes a buffer off of the wire in
+        network-byte order, and decodes it, populating internal properties as
+        appropriate. This can only be done once the first 2-byte opcode has
+        already been decoded, but the data section does include the entire
+        datagram.
+
+        This is an abstract method."""
+        raise NotImplementedError("Abstract method")
+
+class TftpPacketInitial(TftpPacket, TftpPacketWithOptions):
+    """This class is a common parent class for the RRQ and WRQ packets, as
+    they share quite a bit of code."""
+    def __init__(self):
+        TftpPacket.__init__(self)
+        TftpPacketWithOptions.__init__(self)
+        self.filename = None
+        self.mode = None
+
+    def encode(self):
+        """Encode the packet's buffer from the instance variables."""
+        tftpassert(self.filename, "filename required in initial packet")
+        tftpassert(self.mode, "mode required in initial packet")
+        # Make sure filename and mode are bytestrings.
+        self.filename = self.filename.encode('ascii')
+        self.mode = self.mode.encode('ascii')
+
+        ptype = None
+        if self.opcode == 1: ptype = "RRQ"
+        else:                ptype = "WRQ"
+        log.debug("Encoding %s packet, filename = %s, mode = %s"
+                     % (ptype, self.filename, self.mode))
+        for key in self.options:
+            log.debug("    Option %s = %s" % (key, self.options[key]))
+
+        format = b"!H"
+        format += b"%dsx" % len(self.filename)
+        if self.mode == b"octet":
+            format += b"5sx"
+        else:
+            raise AssertionError("Unsupported mode: %s" % self.mode)
+        # Add options.
+        options_list = []
+        if len(self.options.keys()) > 0:
+            log.debug("there are options to encode")
+            for key in self.options:
+                # Populate the option name
+                format += b"%dsx" % len(key)
+                options_list.append(key.encode('ascii'))
+                # Populate the option value
+                format += b"%dsx" % len(self.options[key].encode('ascii'))
+                options_list.append(self.options[key].encode('ascii'))
+
+        log.debug("format is %s" % format)
+        log.debug("options_list is %s" % options_list)
+        log.debug("size of struct is %d" % struct.calcsize(format))
+
+        self.buffer = struct.pack(format,
+                                  self.opcode,
+                                  self.filename,
+                                  self.mode,
+                                  *options_list)
+
+        log.debug("buffer is %s" % repr(self.buffer))
+        return self
+
+    def decode(self):
+        tftpassert(self.buffer, "Can't decode, buffer is empty")
+
+        # FIXME - this shares a lot of code with decode_options
+        nulls = 0
+        format = ""
+        nulls = length = tlength = 0
+        log.debug("in decode: about to iterate buffer counting nulls")
+        subbuf = self.buffer[2:]
+        for c in subbuf:
+            if sys.version_info[0] <= 2:
+                c = ord(c)
+            if c == 0:
+                nulls += 1
+                log.debug("found a null at length %d, now have %d"
+                             % (length, nulls))
+                format += "%dsx" % length
+                length = -1
+                # At 2 nulls, we want to mark that position for decoding.
+                if nulls == 2:
+                    break
+            length += 1
+            tlength += 1
+
+        log.debug("hopefully found end of mode at length %d" % tlength)
+        # length should now be the end of the mode.
+        tftpassert(nulls == 2, "malformed packet")
+        shortbuf = subbuf[:tlength+1]
+        log.debug("about to unpack buffer with format: %s" % format)
+        log.debug("unpacking buffer: " + repr(shortbuf))
+        mystruct = struct.unpack(format, shortbuf)
+
+        tftpassert(len(mystruct) == 2, "malformed packet")
+        self.filename = mystruct[0]
+        self.mode = mystruct[1].lower() # force lc - bug 17
+        log.debug("set filename to %s" % self.filename)
+        log.debug("set mode to %s" % self.mode)
+
+        self.options = self.decode_options(subbuf[tlength+1:])
+        return self
+
+class TftpPacketRRQ(TftpPacketInitial):
+    """
+::
+
+            2 bytes    string   1 byte     string   1 byte
+            -----------------------------------------------
+    RRQ/  | 01/02 |  Filename  |   0  |    Mode    |   0  |
+    WRQ     -----------------------------------------------
+    """
+    def __init__(self):
+        TftpPacketInitial.__init__(self)
+        self.opcode = 1
+
+    def __str__(self):
+        s = 'RRQ packet: filename = %s' % self.filename
+        s += ' mode = %s' % self.mode
+        if self.options:
+            s += '\n    options = %s' % self.options
+        return s
+
+class TftpPacketWRQ(TftpPacketInitial):
+    """
+::
+
+            2 bytes    string   1 byte     string   1 byte
+            -----------------------------------------------
+    RRQ/  | 01/02 |  Filename  |   0  |    Mode    |   0  |
+    WRQ     -----------------------------------------------
+    """
+    def __init__(self):
+        TftpPacketInitial.__init__(self)
+        self.opcode = 2
+
+    def __str__(self):
+        s = 'WRQ packet: filename = %s' % self.filename
+        s += ' mode = %s' % self.mode
+        if self.options:
+            s += '\n    options = %s' % self.options
+        return s
+
+class TftpPacketDAT(TftpPacket):
+    """
+::
+
+            2 bytes    2 bytes       n bytes
+            ---------------------------------
+    DATA  | 03    |   Block #  |    Data    |
+            ---------------------------------
+    """
+    def __init__(self):
+        TftpPacket.__init__(self)
+        self.opcode = 3
+        self.blocknumber = 0
+        self.data = None
+
+    def __str__(self):
+        s = 'DAT packet: block %s' % self.blocknumber
+        if self.data:
+            s += '\n    data: %d bytes' % len(self.data)
+        return s
+
+    def encode(self):
+        """Encode the DAT packet. This method populates self.buffer, and
+        returns self for easy method chaining."""
+        if len(self.data) == 0:
+            log.debug("Encoding an empty DAT packet")
+        format = "!HH%ds" % len(self.data)
+        self.buffer = struct.pack(format,
+                                  self.opcode,
+                                  self.blocknumber,
+                                  self.data)
+        return self
+
+    def decode(self):
+        """Decode self.buffer into instance variables. It returns self for
+        easy method chaining."""
+        # We know the first 2 bytes are the opcode. The second two are the
+        # block number.
+        (self.blocknumber,) = struct.unpack("!H", self.buffer[2:4])
+        log.debug("decoding DAT packet, block number %d" % self.blocknumber)
+        log.debug("should be %d bytes in the packet total"
+                     % len(self.buffer))
+        # Everything else is data.
+        self.data = self.buffer[4:]
+        log.debug("found %d bytes of data"
+                     % len(self.data))
+        return self
+
+class TftpPacketACK(TftpPacket):
+    """
+::
+
+            2 bytes    2 bytes
+            -------------------
+    ACK   | 04    |   Block #  |
+            --------------------
+    """
+    def __init__(self):
+        TftpPacket.__init__(self)
+        self.opcode = 4
+        self.blocknumber = 0
+
+    def __str__(self):
+        return 'ACK packet: block %d' % self.blocknumber
+
+    def encode(self):
+        log.debug("encoding ACK: opcode = %d, block = %d"
+                     % (self.opcode, self.blocknumber))
+        self.buffer = struct.pack("!HH", self.opcode, self.blocknumber)
+        return self
+
+    def decode(self):
+        if len(self.buffer) > 4:
+            log.debug("detected TFTP ACK but request is too large, will truncate")
+            log.debug("buffer was: %s", repr(self.buffer))
+            self.buffer = self.buffer[0:4]
+        self.opcode, self.blocknumber = struct.unpack("!HH", self.buffer)
+        log.debug("decoded ACK packet: opcode = %d, block = %d"
+                     % (self.opcode, self.blocknumber))
+        return self
+
+class TftpPacketERR(TftpPacket):
+    """
+::
+
+            2 bytes  2 bytes        string    1 byte
+            ----------------------------------------
+    ERROR | 05    |  ErrorCode |   ErrMsg   |   0  |
+            ----------------------------------------
+
+    Error Codes
+
+    Value     Meaning
+
+    0         Not defined, see error message (if any).
+    1         File not found.
+    2         Access violation.
+    3         Disk full or allocation exceeded.
+    4         Illegal TFTP operation.
+    5         Unknown transfer ID.
+    6         File already exists.
+    7         No such user.
+    8         Failed to negotiate options
+    """
+    def __init__(self):
+        TftpPacket.__init__(self)
+        self.opcode = 5
+        self.errorcode = 0
+        # FIXME: We don't encode the errmsg...
+        self.errmsg = None
+        # FIXME - integrate in TftpErrors references?
+        self.errmsgs = {
+            1: b"File not found",
+            2: b"Access violation",
+            3: b"Disk full or allocation exceeded",
+            4: b"Illegal TFTP operation",
+            5: b"Unknown transfer ID",
+            6: b"File already exists",
+            7: b"No such user",
+            8: b"Failed to negotiate options"
+            }
+
+    def __str__(self):
+        s = 'ERR packet: errorcode = %d' % self.errorcode
+        s += '\n    msg = %s' % self.errmsgs.get(self.errorcode, '')
+        return s
+
+    def encode(self):
+        """Encode the DAT packet based on instance variables, populating
+        self.buffer, returning self."""
+        format = "!HH%dsx" % len(self.errmsgs[self.errorcode])
+        log.debug("encoding ERR packet with format %s" % format)
+        self.buffer = struct.pack(format,
+                                  self.opcode,
+                                  self.errorcode,
+                                  self.errmsgs[self.errorcode])
+        return self
+
+    def decode(self):
+        "Decode self.buffer, populating instance variables and return self."
+        buflen = len(self.buffer)
+        tftpassert(buflen >= 4, "malformed ERR packet, too short")
+        log.debug("Decoding ERR packet, length %s bytes" % buflen)
+        if buflen == 4:
+            log.debug("Allowing this affront to the RFC of a 4-byte packet")
+            format = "!HH"
+            log.debug("Decoding ERR packet with format: %s" % format)
+            self.opcode, self.errorcode = struct.unpack(format,
+                                                        self.buffer)
+        else:
+            log.debug("Good ERR packet > 4 bytes")
+            format = "!HH%dsx" % (len(self.buffer) - 5)
+            log.debug("Decoding ERR packet with format: %s" % format)
+            self.opcode, self.errorcode, self.errmsg = struct.unpack(format,
+                                                                     self.buffer)
+        log.error("ERR packet - errorcode: %d, message: %s"
+                     % (self.errorcode, self.errmsg))
+        return self
+
+class TftpPacketOACK(TftpPacket, TftpPacketWithOptions):
+    """
+::
+
+    +-------+---~~---+---+---~~---+---+---~~---+---+---~~---+---+
+    |  opc  |  opt1  | 0 | value1 | 0 |  optN  | 0 | valueN | 0 |
+    +-------+---~~---+---+---~~---+---+---~~---+---+---~~---+---+
+    """
+    def __init__(self):
+        TftpPacket.__init__(self)
+        TftpPacketWithOptions.__init__(self)
+        self.opcode = 6
+
+    def __str__(self):
+        return 'OACK packet:\n    options = %s' % self.options
+
+    def encode(self):
+        format = "!H" # opcode
+        options_list = []
+        log.debug("in TftpPacketOACK.encode")
+        for key in self.options:
+            log.debug("looping on option key %s" % key)
+            log.debug("value is %s" % self.options[key])
+            format += "%dsx" % len(key)
+            format += "%dsx" % len(self.options[key])
+            options_list.append(key)
+            options_list.append(self.options[key])
+        self.buffer = struct.pack(format, self.opcode, *options_list)
+        return self
+
+    def decode(self):
+        self.options = self.decode_options(self.buffer[2:])
+        return self
+
+    def match_options(self, options):
+        """This method takes a set of options, and tries to match them with
+        its own. It can accept some changes in those options from the server as
+        part of a negotiation. Changed or unchanged, it will return a dict of
+        the options so that the session can update itself to the negotiated
+        options."""
+        for name in self.options:
+            if name in options:
+                if name == 'blksize':
+                    # We can accept anything between the min and max values.
+                    size = int(self.options[name])
+                    if size >= MIN_BLKSIZE and size <= MAX_BLKSIZE:
+                        log.debug("negotiated blksize of %d bytes", size)
+                        options['blksize'] = size
+                    else:
+                        raise TftpException("blksize %s option outside allowed range" % size)
+                elif name == 'tsize':
+                    size = int(self.options[name])
+                    if size < 0:
+                        raise TftpException("Negative file sizes not supported")
+                else:
+                    raise TftpException("Unsupported option: %s" % name)
+        return True
diff --git a/tester/rt/tftpy/TftpServer.py b/tester/rt/tftpy/TftpServer.py
new file mode 100644
index 0000000..07c2107
--- /dev/null
+++ b/tester/rt/tftpy/TftpServer.py
@@ -0,0 +1,254 @@
+"""This module implements the TFTP Server functionality. Instantiate an
+instance of the server, and then run the listen() method to listen for client
+requests. Logging is performed via a standard logging object set in
+TftpShared."""
+
+from __future__ import absolute_import, division, print_function, unicode_literals
+import socket, os, time
+import select
+import threading
+from errno import EINTR
+from .TftpShared import *
+from .TftpPacketTypes import *
+from .TftpPacketFactory import TftpPacketFactory
+from .TftpContexts import TftpContextServer
+
+class TftpServer(TftpSession):
+    """This class implements a tftp server object. Run the listen() method to
+    listen for client requests.
+
+    tftproot is the path to the tftproot directory to serve files from and/or
+    write them to.
+
+    dyn_file_func is a callable that takes a requested download
+    path that is not present on the file system and must return either a
+    file-like object to read from or None if the path should appear as not
+    found. This permits the serving of dynamic content.
+
+    upload_open is a callable that is triggered on every upload with the
+    requested destination path and server context. It must either return a
+    file-like object ready for writing or None if the path is invalid."""
+
+    def __init__(self,
+                 tftproot='/tftpboot',
+                 dyn_file_func=None,
+                 upload_open=None):
+        self.listenip = None
+        self.listenport = None
+        self.sock = None
+        # FIXME: What about multiple roots?
+        self.root = os.path.abspath(tftproot)
+        self.dyn_file_func = dyn_file_func
+        self.upload_open = upload_open
+        # A dict of sessions, where each session is keyed by a string like
+        # ip:tid for the remote end.
+        self.sessions = {}
+        # A threading event to help threads synchronize with the server
+        # is_running state.
+        self.is_running = threading.Event()
+
+        self.shutdown_gracefully = False
+        self.shutdown_immediately = False
+
+        for name in 'dyn_file_func', 'upload_open':
+            attr = getattr(self, name)
+            if attr and not callable(attr):
+                raise TftpException, "%s supplied, but it is not callable." % (
+                    name,)
+        if os.path.exists(self.root):
+            log.debug("tftproot %s does exist", self.root)
+            if not os.path.isdir(self.root):
+                raise TftpException("The tftproot must be a directory.")
+            else:
+                log.debug("tftproot %s is a directory" % self.root)
+                if os.access(self.root, os.R_OK):
+                    log.debug("tftproot %s is readable" % self.root)
+                else:
+                    raise TftpException("The tftproot must be readable")
+                if os.access(self.root, os.W_OK):
+                    log.debug("tftproot %s is writable" % self.root)
+                else:
+                    log.warning("The tftproot %s is not writable" % self.root)
+        else:
+            raise TftpException("The tftproot does not exist.")
+
+    def listen(self, listenip="", listenport=DEF_TFTP_PORT,
+               timeout=SOCK_TIMEOUT):
+        """Start a server listening on the supplied interface and port. This
+        defaults to INADDR_ANY (all interfaces) and UDP port 69. You can also
+        supply a different socket timeout value, if desired."""
+        tftp_factory = TftpPacketFactory()
+
+        # Don't use new 2.5 ternary operator yet
+        # listenip = listenip if listenip else '0.0.0.0'
+        if not listenip: listenip = '0.0.0.0'
+        log.info("Server requested on ip %s, port %s" % (listenip, listenport))
+        try:
+            # FIXME - sockets should be non-blocking
+            self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+            self.sock.bind((listenip, listenport))
+            _, self.listenport = self.sock.getsockname()
+        except socket.error as err:
+            # Reraise it for now.
+            raise err
+
+        self.is_running.set()
+
+        log.info("Starting receive loop...")
+        while True:
+            log.debug("shutdown_immediately is %s" % self.shutdown_immediately)
+            log.debug("shutdown_gracefully is %s" % self.shutdown_gracefully)
+            if self.shutdown_immediately:
+                log.warn("Shutting down now. Session count: %d" %
+                         len(self.sessions))
+                self.sock.close()
+                for key in self.sessions:
+                    self.sessions[key].end()
+                self.sessions = []
+                break
+
+            elif self.shutdown_gracefully:
+                if not self.sessions:
+                    log.warn("In graceful shutdown mode and all "
+                             "sessions complete.")
+                    self.sock.close()
+                    break
+
+            # Build the inputlist array of sockets to select() on.
+            inputlist = []
+            inputlist.append(self.sock)
+            for key in self.sessions:
+                inputlist.append(self.sessions[key].sock)
+
+            # Block until some socket has input on it.
+            log.debug("Performing select on this inputlist: %s", inputlist)
+            try:
+                readyinput, readyoutput, readyspecial = \
+                        select.select(inputlist, [], [], timeout)
+            except select.error as err:
+                if err[0] == EINTR:
+                    # Interrupted system call
+                    log.debug("Interrupted syscall, retrying")
+                    continue
+                else:
+                    raise
+
+            deletion_list = []
+
+            # Handle the available data, if any. Maybe we timed-out.
+            for readysock in readyinput:
+                # Is the traffic on the main server socket? ie. new session?
+                if readysock == self.sock:
+                    log.debug("Data ready on our main socket")
+                    buffer, (raddress, rport) = self.sock.recvfrom(MAX_BLKSIZE)
+
+                    log.debug("Read %d bytes", len(buffer))
+
+                    if self.shutdown_gracefully:
+                        log.warn("Discarding data on main port, "
+                                 "in graceful shutdown mode")
+                        continue
+
+                    # Forge a session key based on the client's IP and port,
+                    # which should safely work through NAT.
+                    key = "%s:%s" % (raddress, rport)
+
+                    if not key in self.sessions:
+                        log.debug("Creating new server context for "
+                                     "session key = %s" % key)
+                        self.sessions[key] = TftpContextServer(raddress,
+                                                               rport,
+                                                               timeout,
+                                                               self.root,
+                                                               self.dyn_file_func,
+                                                               self.upload_open)
+                        try:
+                            self.sessions[key].start(buffer)
+                        except TftpException as err:
+                            deletion_list.append(key)
+                            log.error("Fatal exception thrown from "
+                                      "session %s: %s" % (key, str(err)))
+                    else:
+                        log.warn("received traffic on main socket for "
+                                 "existing session??")
+                    log.info("Currently handling these sessions:")
+                    for session_key, session in self.sessions.items():
+                        log.info("    %s" % session)
+
+                else:
+                    # Must find the owner of this traffic.
+                    for key in self.sessions:
+                        if readysock == self.sessions[key].sock:
+                            log.debug("Matched input to session key %s"
+                                % key)
+                            try:
+                                self.sessions[key].cycle()
+                                if self.sessions[key].state == None:
+                                    log.info("Successful transfer.")
+                                    deletion_list.append(key)
+                            except TftpException as err:
+                                deletion_list.append(key)
+                                log.error("Fatal exception thrown from "
+                                          "session %s: %s"
+                                          % (key, str(err)))
+                            # Break out of for loop since we found the correct
+                            # session.
+                            break
+                    else:
+                        log.error("Can't find the owner for this packet. "
+                                  "Discarding.")
+
+            log.debug("Looping on all sessions to check for timeouts")
+            now = time.time()
+            for key in self.sessions:
+                try:
+                    self.sessions[key].checkTimeout(now)
+                except TftpTimeout as err:
+                    log.error(str(err))
+                    self.sessions[key].retry_count += 1
+                    if self.sessions[key].retry_count >= TIMEOUT_RETRIES:
+                        log.debug("hit max retries on %s, giving up" %
+                            self.sessions[key])
+                        deletion_list.append(key)
+                    else:
+                        log.debug("resending on session %s" % self.sessions[key])
+                        self.sessions[key].state.resendLast()
+
+            log.debug("Iterating deletion list.")
+            for key in deletion_list:
+                log.info('')
+                log.info("Session %s complete" % key)
+                if key in self.sessions:
+                    log.debug("Gathering up metrics from session before deleting")
+                    self.sessions[key].end()
+                    metrics = self.sessions[key].metrics
+                    if metrics.duration == 0:
+                        log.info("Duration too short, rate undetermined")
+                    else:
+                        log.info("Transferred %d bytes in %.2f seconds"
+                            % (metrics.bytes, metrics.duration))
+                        log.info("Average rate: %.2f kbps" % metrics.kbps)
+                    log.info("%.2f bytes in resent data" % metrics.resent_bytes)
+                    log.info("%d duplicate packets" % metrics.dupcount)
+                    log.debug("Deleting session %s" % key)
+                    del self.sessions[key]
+                    log.debug("Session list is now %s" % self.sessions)
+                else:
+                    log.warn(
+                        "Strange, session %s is not on the deletion list" % key)
+
+        self.is_running.clear()
+
+        log.debug("server returning from while loop")
+        self.shutdown_gracefully = self.shutdown_immediately = False
+
+    def stop(self, now=False):
+        """Stop the server gracefully. Do not take any new transfers,
+        but complete the existing ones. If force is True, drop everything
+        and stop. Note, immediately will not interrupt the select loop, it
+        will happen when the server returns on ready data, or a timeout.
+        ie. SOCK_TIMEOUT"""
+        if now:
+            self.shutdown_immediately = True
+        else:
+            self.shutdown_gracefully = True
diff --git a/tester/rt/tftpy/TftpShared.py b/tester/rt/tftpy/TftpShared.py
new file mode 100644
index 0000000..6252ebd
--- /dev/null
+++ b/tester/rt/tftpy/TftpShared.py
@@ -0,0 +1,88 @@
+"""This module holds all objects shared by all other modules in tftpy."""
+
+from __future__ import absolute_import, division, print_function, unicode_literals
+import logging
+from logging.handlers import RotatingFileHandler
+
+LOG_LEVEL = logging.NOTSET
+MIN_BLKSIZE = 8
+DEF_BLKSIZE = 512
+MAX_BLKSIZE = 65536
+SOCK_TIMEOUT = 5
+MAX_DUPS = 20
+TIMEOUT_RETRIES = 5
+DEF_TFTP_PORT = 69
+
+# A hook for deliberately introducing delay in testing.
+DELAY_BLOCK = 0
+
+# Initialize the logger.
+logging.basicConfig()
+
+# The logger used by this library. Feel free to clobber it with your own, if
+# you like, as long as it conforms to Python's logging.
+log = logging.getLogger('tftpy')
+
+def create_streamhandler():
+    """add create_streamhandler output logging.DEBUG msg to stdout.
+    """
+    console = logging.StreamHandler()
+    console.setLevel(logging.INFO)
+    formatter = logging.Formatter('%(levelname)-8s %(message)s')
+    console.setFormatter(formatter)
+    return console
+
+def create_rotatingfilehandler(path, maxbytes=10*1024*1024, count=20):
+    """
+    add create_rotatingfilehandler record the logging.DEBUG msg to logfile. you can change the maxsize (10*1024*1024)
+    and amount of the logfiles
+    """
+    Rthandler = RotatingFileHandler(path, maxbytes, count)
+    Rthandler.setLevel(logging.INFO)
+    formatter = logging.Formatter('%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s')
+    Rthandler.setFormatter(formatter)
+    return Rthandler
+
+def addHandler(hdlr):
+    """add handler methods
+    More details see the page:
+    https://docs.python.org/2/library/logging.handlers.html#module-logging.handlers
+    """
+    log.addHandler(hdlr)
+
+def tftpassert(condition, msg):
+    """This function is a simple utility that will check the condition
+    passed for a false state. If it finds one, it throws a TftpException
+    with the message passed. This just makes the code throughout cleaner
+    by refactoring."""
+    if not condition:
+        raise TftpException(msg)
+
+def setLogLevel(level):
+    """This function is a utility function for setting the internal log level.
+    The log level defaults to logging.NOTSET, so unwanted output to stdout is
+    not created."""
+    log.setLevel(level)
+
+class TftpErrors(object):
+    """This class is a convenience for defining the common tftp error codes,
+    and making them more readable in the code."""
+    NotDefined = 0
+    FileNotFound = 1
+    AccessViolation = 2
+    DiskFull = 3
+    IllegalTftpOp = 4
+    UnknownTID = 5
+    FileAlreadyExists = 6
+    NoSuchUser = 7
+    FailedNegotiation = 8
+
+class TftpException(Exception):
+    """This class is the parent class of all exceptions regarding the handling
+    of the TFTP protocol."""
+    pass
+
+class TftpTimeout(TftpException):
+    """This class represents a timeout error waiting for a response from the
+    other end."""
+    pass
diff --git a/tester/rt/tftpy/TftpStates.py b/tester/rt/tftpy/TftpStates.py
new file mode 100644
index 0000000..801e970
--- /dev/null
+++ b/tester/rt/tftpy/TftpStates.py
@@ -0,0 +1,598 @@
+"""This module implements all state handling during uploads and downloads, the
+main interface to which being the TftpState base class.
+
+The concept is simple. Each context object represents a single upload or
+download, and the state object in the context object represents the current
+state of that transfer. The state object has a handle() method that expects
+the next packet in the transfer, and returns a state object until the transfer
+is complete, at which point it returns None. That is, unless there is a fatal
+error, in which case a TftpException is returned instead."""
+
+from __future__ import absolute_import, division, print_function, unicode_literals
+from .TftpShared import *
+from .TftpPacketTypes import *
+import os
+
+###############################################################################
+# State classes
+###############################################################################
+
+class TftpState(object):
+    """The base class for the states."""
+
+    def __init__(self, context):
+        """Constructor for setting up common instance variables. The involved
+        file object is required, since in tftp there's always a file
+        involved."""
+        self.context = context
+
+    def handle(self, pkt, raddress, rport):
+        """An abstract method for handling a packet. It is expected to return
+        a TftpState object, either itself or a new state."""
+        raise NotImplementedError("Abstract method")
+
+    def handleOACK(self, pkt):
+        """This method handles an OACK from the server, syncing any accepted
+        options."""
+        if pkt.options.keys() > 0:
+            if pkt.match_options(self.context.options):
+                log.info("Successful negotiation of options")
+                # Set options to OACK options
+                self.context.options = pkt.options
+                for key in self.context.options:
+                    log.info("    %s = %s" % (key, self.context.options[key]))
+            else:
+                log.error("Failed to negotiate options")
+                raise TftpException("Failed to negotiate options")
+        else:
+            raise TftpException("No options found in OACK")
+
+    def returnSupportedOptions(self, options):
+        """This method takes a requested options list from a client, and
+        returns the ones that are supported."""
+        # We support the options blksize and tsize right now.
+        # FIXME - put this somewhere else?
+        accepted_options = {}
+        for option in options:
+            if option == 'blksize':
+                # Make sure it's valid.
+                if int(options[option]) > MAX_BLKSIZE:
+                    log.info("Client requested blksize greater than %d "
+                             "setting to maximum" % MAX_BLKSIZE)
+                    accepted_options[option] = MAX_BLKSIZE
+                elif int(options[option]) < MIN_BLKSIZE:
+                    log.info("Client requested blksize less than %d "
+                             "setting to minimum" % MIN_BLKSIZE)
+                    accepted_options[option] = MIN_BLKSIZE
+                else:
+                    accepted_options[option] = options[option]
+            elif option == 'tsize':
+                log.debug("tsize option is set")
+                accepted_options['tsize'] = 0
+            else:
+                log.info("Dropping unsupported option '%s'" % option)
+        log.debug("Returning these accepted options: %s", accepted_options)
+        return accepted_options
+
+    def sendDAT(self):
+        """This method sends the next DAT packet based on the data in the
+        context. It returns a boolean indicating whether the transfer is
+        finished."""
+        finished = False
+        blocknumber = self.context.next_block
+        # Test hook
+        if DELAY_BLOCK and DELAY_BLOCK == blocknumber:
+            import time
+            log.debug("Deliberately delaying 10 seconds...")
+            time.sleep(10)
+        dat = None
+        blksize = self.context.getBlocksize()
+        buffer = self.context.fileobj.read(blksize)
+        log.debug("Read %d bytes into buffer", len(buffer))
+        if len(buffer) < blksize:
+            log.info("Reached EOF on file %s"
+                % self.context.file_to_transfer)
+            finished = True
+        dat = TftpPacketDAT()
+        dat.data = buffer
+        dat.blocknumber = blocknumber
+        self.context.metrics.bytes += len(dat.data)
+        log.debug("Sending DAT packet %d", dat.blocknumber)
+        self.context.sock.sendto(dat.encode().buffer,
+                                 (self.context.host, self.context.tidport))
+        if self.context.packethook:
+            self.context.packethook(dat)
+        self.context.last_pkt = dat
+        return finished
+
+    def sendACK(self, blocknumber=None):
+        """This method sends an ack packet to the block number specified. If
+        none is specified, it defaults to the next_block property in the
+        parent context."""
+        log.debug("In sendACK, passed blocknumber is %s", blocknumber)
+        if blocknumber is None:
+            blocknumber = self.context.next_block
+        log.info("Sending ack to block %d" % blocknumber)
+        ackpkt = TftpPacketACK()
+        ackpkt.blocknumber = blocknumber
+        self.context.sock.sendto(ackpkt.encode().buffer,
+                                 (self.context.host,
+                                  self.context.tidport))
+        self.context.last_pkt = ackpkt
+
+    def sendError(self, errorcode):
+        """This method uses the socket passed, and uses the errorcode to
+        compose and send an error packet."""
+        log.debug("In sendError, being asked to send error %d", errorcode)
+        errpkt = TftpPacketERR()
+        errpkt.errorcode = errorcode
+        self.context.sock.sendto(errpkt.encode().buffer,
+                                 (self.context.host,
+                                  self.context.tidport))
+        self.context.last_pkt = errpkt
+
+    def sendOACK(self):
+        """This method sends an OACK packet with the options from the current
+        context."""
+        log.debug("In sendOACK with options %s", self.context.options)
+        pkt = TftpPacketOACK()
+        pkt.options = self.context.options
+        self.context.sock.sendto(pkt.encode().buffer,
+                                 (self.context.host,
+                                  self.context.tidport))
+        self.context.last_pkt = pkt
+
+    def resendLast(self):
+        "Resend the last sent packet due to a timeout."
+        log.warn("Resending packet %s on sessions %s"
+            % (self.context.last_pkt, self))
+        self.context.metrics.resent_bytes += len(self.context.last_pkt.buffer)
+        self.context.metrics.add_dup(self.context.last_pkt)
+        sendto_port = self.context.tidport
+        if not sendto_port:
+            # If the tidport wasn't set, then the remote end hasn't even
+            # started talking to us yet. That's not good. Maybe it's not
+            # there.
+            sendto_port = self.context.port
+        self.context.sock.sendto(self.context.last_pkt.encode().buffer,
+                                 (self.context.host, sendto_port))
+        if self.context.packethook:
+            self.context.packethook(self.context.last_pkt)
+
+    def handleDat(self, pkt):
+        """This method handles a DAT packet during a client download, or a
+        server upload."""
+        log.info("Handling DAT packet - block %d" % pkt.blocknumber)
+        log.debug("Expecting block %s", self.context.next_block)
+        if pkt.blocknumber == self.context.next_block:
+            log.debug("Good, received block %d in sequence", pkt.blocknumber)
+
+            self.sendACK()
+            self.context.next_block += 1
+
+            log.debug("Writing %d bytes to output file", len(pkt.data))
+            self.context.fileobj.write(pkt.data)
+            self.context.metrics.bytes += len(pkt.data)
+            # Check for end-of-file, any less than full data packet.
+            if len(pkt.data) < self.context.getBlocksize():
+                log.info("End of file detected")
+                return None
+
+        elif pkt.blocknumber < self.context.next_block:
+            if pkt.blocknumber == 0:
+                log.warn("There is no block zero!")
+                self.sendError(TftpErrors.IllegalTftpOp)
+                raise TftpException("There is no block zero!")
+            log.warn("Dropping duplicate block %d" % pkt.blocknumber)
+            self.context.metrics.add_dup(pkt)
+            log.debug("ACKing block %d again, just in case", pkt.blocknumber)
+            self.sendACK(pkt.blocknumber)
+
+        else:
+            # FIXME: should we be more tolerant and just discard instead?
+            msg = "Whoa! Received future block %d but expected %d" \
+                % (pkt.blocknumber, self.context.next_block)
+            log.error(msg)
+            raise TftpException(msg)
+
+        # Default is to ack
+        return TftpStateExpectDAT(self.context)
+
+class TftpServerState(TftpState):
+    """The base class for server states."""
+
+    def __init__(self, context):
+        TftpState.__init__(self, context)
+
+        # This variable is used to store the absolute path to the file being
+        # managed.
+        self.full_path = None
+
+    def serverInitial(self, pkt, raddress, rport):
+        """This method performs initial setup for a server context transfer,
+        put here to refactor code out of the TftpStateServerRecvRRQ and
+        TftpStateServerRecvWRQ classes, since their initial setup is
+        identical. The method returns a boolean, sendoack, to indicate whether
+        it is required to send an OACK to the client."""
+        options = pkt.options
+        sendoack = False
+        if not self.context.tidport:
+            self.context.tidport = rport
+            log.info("Setting tidport to %s" % rport)
+
+        log.debug("Setting default options, blksize")
+        self.context.options = { 'blksize': DEF_BLKSIZE }
+
+        if options:
+            log.debug("Options requested: %s", options)
+            supported_options = self.returnSupportedOptions(options)
+            self.context.options.update(supported_options)
+            sendoack = True
+
+        # FIXME - only octet mode is supported at this time.
+        if pkt.mode != 'octet':
+            #self.sendError(TftpErrors.IllegalTftpOp)
+            #raise TftpException("Only octet transfers are supported at this time.")
+            log.warning("Received non-octet mode request. I'll reply with binary data.")
+
+        # test host/port of client end
+        if self.context.host != raddress or self.context.port != rport:
+            self.sendError(TftpErrors.UnknownTID)
+            log.error("Expected traffic from %s:%s but received it "
+                            "from %s:%s instead."
+                            % (self.context.host,
+                               self.context.port,
+                               raddress,
+                               rport))
+            # FIXME: increment an error count?
+            # Return same state, we're still waiting for valid traffic.
+            return self
+
+        log.debug("Requested filename is %s", pkt.filename)
+
+        # Build the filename on this server and ensure it is contained
+        # in the specified root directory.
+        #
+        # Filenames that begin with server root are accepted. It's
+        # assumed the client and server are tightly connected and this
+        # provides backwards compatibility.
+        #
+        # Filenames otherwise are relative to the server root. If they
+        # begin with a '/' strip it off as otherwise os.path.join will
+        # treat it as absolute (regardless of whether it is ntpath or
+        # posixpath module
+        if pkt.filename.startswith(self.context.root.encode()):
+            full_path = pkt.filename
+        else:
+            full_path = os.path.join(self.context.root, pkt.filename.decode().lstrip('/'))
+
+        # Use abspath to eliminate any remaining relative elements
+        # (e.g. '..') and ensure that is still within the server's
+        # root directory
+        self.full_path = os.path.abspath(full_path)
+        log.debug("full_path is %s", full_path)
+        if self.full_path.startswith(self.context.root):
+            log.info("requested file is in the server root - good")
+        else:
+            log.warn("requested file is not within the server root - bad")
+            self.sendError(TftpErrors.IllegalTftpOp)
+            raise TftpException("bad file path")
+
+        self.context.file_to_transfer = pkt.filename
+
+        return sendoack
+
+
+class TftpStateServerRecvRRQ(TftpServerState):
+    """This class represents the state of the TFTP server when it has just
+    received an RRQ packet."""
+    def handle(self, pkt, raddress, rport):
+        "Handle an initial RRQ packet as a server."
+        log.debug("In TftpStateServerRecvRRQ.handle")
+        sendoack = self.serverInitial(pkt, raddress, rport)
+        path = self.full_path
+        log.info("Opening file %s for reading" % path)
+        if os.path.exists(path):
+            # Note: Open in binary mode for win32 portability, since win32
+            # blows.
+            self.context.fileobj = open(path, "rb")
+        elif self.context.dyn_file_func:
+            log.debug("No such file %s but using dyn_file_func", path)
+            self.context.fileobj = \
+                self.context.dyn_file_func(self.context.file_to_transfer, raddress=raddress, rport=rport)
+
+            if self.context.fileobj is None:
+                log.debug("dyn_file_func returned 'None', treating as "
+                          "FileNotFound")
+                self.sendError(TftpErrors.FileNotFound)
+                raise TftpException("File not found: %s" % path)
+        else:
+            self.sendError(TftpErrors.FileNotFound)
+            raise TftpException("File not found: %s" % path)
+
+        # Options negotiation.
+        if sendoack and self.context.options.has_key('tsize'):
+            # getting the file size for the tsize option. As we handle
+            # file-like objects and not only real files, we use this seeking
+            # method instead of asking the OS
+            self.context.fileobj.seek(0, os.SEEK_END)
+            tsize = str(self.context.fileobj.tell())
+            self.context.fileobj.seek(0, 0)
+            self.context.options['tsize'] = tsize
+
+        if sendoack:
+            # Note, next_block is 0 here since that's the proper
+            # acknowledgement to an OACK.
+            # FIXME: perhaps we do need a TftpStateExpectOACK class...
+            self.sendOACK()
+            # Note, self.context.next_block is already 0.
+        else:
+            self.context.next_block = 1
+            log.debug("No requested options, starting send...")
+            self.context.pending_complete = self.sendDAT()
+        # Note, we expect an ack regardless of whether we sent a DAT or an
+        # OACK.
+        return TftpStateExpectACK(self.context)
+
+        # Note, we don't have to check any other states in this method, that's
+        # up to the caller.
+
+class TftpStateServerRecvWRQ(TftpServerState):
+    """This class represents the state of the TFTP server when it has just
+    received a WRQ packet."""
+    def make_subdirs(self):
+        """The purpose of this method is to, if necessary, create all of the
+        subdirectories leading up to the file to the written."""
+        # Pull off everything below the root.
+        subpath = self.full_path[len(self.context.root):]
+        log.debug("make_subdirs: subpath is %s", subpath)
+        # Split on directory separators, but drop the last one, as it should
+        # be the filename.
+        dirs = subpath.split(os.sep)[:-1]
+        log.debug("dirs is %s", dirs)
+        current = self.context.root
+        for dir in dirs:
+            if dir:
+                current = os.path.join(current, dir)
+                if os.path.isdir(current):
+                    log.debug("%s is already an existing directory", current)
+                else:
+                    os.mkdir(current, 0o700)
+
+    def handle(self, pkt, raddress, rport):
+        "Handle an initial WRQ packet as a server."
+        log.debug("In TftpStateServerRecvWRQ.handle")
+        sendoack = self.serverInitial(pkt, raddress, rport)
+        path = self.full_path
+        if self.context.upload_open:
+            f = self.context.upload_open(path, self.context)
+            if f is None:
+                self.sendError(TftpErrors.AccessViolation)
+                raise TftpException, "Dynamic path %s not permitted" % path
+            else:
+                self.context.fileobj = f
+        else:
+            log.info("Opening file %s for writing" % path)
+            if os.path.exists(path):
+                # FIXME: correct behavior?
+                log.warn("File %s exists already, overwriting..." % (
+                    self.context.file_to_transfer))
+            # FIXME: I think we should upload to a temp file and not overwrite
+            # the existing file until the file is successfully uploaded.
+            self.make_subdirs()
+            self.context.fileobj = open(path, "wb")
+
+        # Options negotiation.
+        if sendoack:
+            log.debug("Sending OACK to client")
+            self.sendOACK()
+        else:
+            log.debug("No requested options, expecting transfer to begin...")
+            self.sendACK()
+        # Whether we're sending an oack or not, we're expecting a DAT for
+        # block 1
+        self.context.next_block = 1
+        # We may have sent an OACK, but we're expecting a DAT as the response
+        # to either the OACK or an ACK, so lets unconditionally use the
+        # TftpStateExpectDAT state.
+        return TftpStateExpectDAT(self.context)
+
+        # Note, we don't have to check any other states in this method, that's
+        # up to the caller.
+
+class TftpStateServerStart(TftpState):
+    """The start state for the server. This is a transitory state since at
+    this point we don't know if we're handling an upload or a download. We
+    will commit to one of them once we interpret the initial packet."""
+    def handle(self, pkt, raddress, rport):
+        """Handle a packet we just received."""
+        log.debug("In TftpStateServerStart.handle")
+        if isinstance(pkt, TftpPacketRRQ):
+            log.debug("Handling an RRQ packet")
+            return TftpStateServerRecvRRQ(self.context).handle(pkt,
+                                                               raddress,
+                                                               rport)
+        elif isinstance(pkt, TftpPacketWRQ):
+            log.debug("Handling a WRQ packet")
+            return TftpStateServerRecvWRQ(self.context).handle(pkt,
+                                                               raddress,
+                                                               rport)
+        else:
+            self.sendError(TftpErrors.IllegalTftpOp)
+            raise TftpException("Invalid packet to begin up/download: %s" % pkt)
+
+class TftpStateExpectACK(TftpState):
+    """This class represents the state of the transfer when a DAT was just
+    sent, and we are waiting for an ACK from the server. This class is the
+    same one used by the client during the upload, and the server during the
+    download."""
+    def handle(self, pkt, raddress, rport):
+        "Handle a packet, hopefully an ACK since we just sent a DAT."
+        if isinstance(pkt, TftpPacketACK):
+            log.debug("Received ACK for packet %d" % pkt.blocknumber)
+            # Is this an ack to the one we just sent?
+            if self.context.next_block == pkt.blocknumber:
+                if self.context.pending_complete:
+                    log.info("Received ACK to final DAT, we're done.")
+                    return None
+                else:
+                    log.debug("Good ACK, sending next DAT")
+                    self.context.next_block += 1
+                    log.debug("Incremented next_block to %d",
+                        self.context.next_block)
+                    self.context.pending_complete = self.sendDAT()
+
+            elif pkt.blocknumber < self.context.next_block:
+                log.warn("Received duplicate ACK for block %d"
+                    % pkt.blocknumber)
+                self.context.metrics.add_dup(pkt)
+
+            else:
+                log.warn("Oooh, time warp. Received ACK to packet we "
+                         "didn't send yet. Discarding.")
+                self.context.metrics.errors += 1
+            return self
+        elif isinstance(pkt, TftpPacketERR):
+            log.error("Received ERR packet from peer: %s" % str(pkt))
+            raise TftpException("Received ERR packet from peer: %s" % str(pkt))
+        else:
+            log.warn("Discarding unsupported packet: %s" % str(pkt))
+            return self
+
+class TftpStateExpectDAT(TftpState):
+    """Just sent an ACK packet. Waiting for DAT."""
+    def handle(self, pkt, raddress, rport):
+        """Handle the packet in response to an ACK, which should be a DAT."""
+        if isinstance(pkt, TftpPacketDAT):
+            return self.handleDat(pkt)
+
+        # Every other packet type is a problem.
+        elif isinstance(pkt, TftpPacketACK):
+            # Umm, we ACK, you don't.
+            self.sendError(TftpErrors.IllegalTftpOp)
+            raise TftpException("Received ACK from peer when expecting DAT")
+
+        elif isinstance(pkt, TftpPacketWRQ):
+            self.sendError(TftpErrors.IllegalTftpOp)
+            raise TftpException("Received WRQ from peer when expecting DAT")
+
+        elif isinstance(pkt, TftpPacketERR):
+            self.sendError(TftpErrors.IllegalTftpOp)
+            raise TftpException("Received ERR from peer: " + str(pkt))
+
+        else:
+            self.sendError(TftpErrors.IllegalTftpOp)
+            raise TftpException("Received unknown packet type from peer: " + str(pkt))
+
+class TftpStateSentWRQ(TftpState):
+    """Just sent an WRQ packet for an upload."""
+    def handle(self, pkt, raddress, rport):
+        """Handle a packet we just received."""
+        if not self.context.tidport:
+            self.context.tidport = rport
+            log.debug("Set remote port for session to %s", rport)
+
+        # If we're going to successfully transfer the file, then we should see
+        # either an OACK for accepted options, or an ACK to ignore options.
+        if isinstance(pkt, TftpPacketOACK):
+            log.info("Received OACK from server")
+            try:
+                self.handleOACK(pkt)
+            except TftpException:
+                log.error("Failed to negotiate options")
+                self.sendError(TftpErrors.FailedNegotiation)
+                raise
+            else:
+                log.debug("Sending first DAT packet")
+                self.context.pending_complete = self.sendDAT()
+                log.debug("Changing state to TftpStateExpectACK")
+                return TftpStateExpectACK(self.context)
+
+        elif isinstance(pkt, TftpPacketACK):
+            log.info("Received ACK from server")
+            log.debug("Apparently the server ignored our options")
+            # The block number should be zero.
+            if pkt.blocknumber == 0:
+                log.debug("Ack blocknumber is zero as expected")
+                log.debug("Sending first DAT packet")
+                self.context.pending_complete = self.sendDAT()
+                log.debug("Changing state to TftpStateExpectACK")
+                return TftpStateExpectACK(self.context)
+            else:
+                log.warn("Discarding ACK to block %s" % pkt.blocknumber)
+                log.debug("Still waiting for valid response from server")
+                return self
+
+        elif isinstance(pkt, TftpPacketERR):
+            self.sendError(TftpErrors.IllegalTftpOp)
+            raise TftpException("Received ERR from server: %s" % pkt)
+
+        elif isinstance(pkt, TftpPacketRRQ):
+            self.sendError(TftpErrors.IllegalTftpOp)
+            raise TftpException("Received RRQ from server while in upload")
+
+        elif isinstance(pkt, TftpPacketDAT):
+            self.sendError(TftpErrors.IllegalTftpOp)
+            raise TftpException("Received DAT from server while in upload")
+
+        else:
+            self.sendError(TftpErrors.IllegalTftpOp)
+            raise TftpException("Received unknown packet type from server: %s" % pkt)
+
+        # By default, no state change.
+        return self
+
+class TftpStateSentRRQ(TftpState):
+    """Just sent an RRQ packet."""
+    def handle(self, pkt, raddress, rport):
+        """Handle the packet in response to an RRQ to the server."""
+        if not self.context.tidport:
+            self.context.tidport = rport
+            log.info("Set remote port for session to %s" % rport)
+
+        # Now check the packet type and dispatch it properly.
+        if isinstance(pkt, TftpPacketOACK):
+            log.info("Received OACK from server")
+            try:
+                self.handleOACK(pkt)
+            except TftpException as err:
+                log.error("Failed to negotiate options: %s" % str(err))
+                self.sendError(TftpErrors.FailedNegotiation)
+                raise
+            else:
+                log.debug("Sending ACK to OACK")
+
+                self.sendACK(blocknumber=0)
+
+                log.debug("Changing state to TftpStateExpectDAT")
+                return TftpStateExpectDAT(self.context)
+
+        elif isinstance(pkt, TftpPacketDAT):
+            # If there are any options set, then the server didn't honour any
+            # of them.
+            log.info("Received DAT from server")
+            if self.context.options:
+                log.info("Server ignored options, falling back to defaults")
+                self.context.options = { 'blksize': DEF_BLKSIZE }
+            return self.handleDat(pkt)
+
+        # Every other packet type is a problem.
+        elif isinstance(pkt, TftpPacketACK):
+            # Umm, we ACK, the server doesn't.
+            self.sendError(TftpErrors.IllegalTftpOp)
+            raise TftpException("Received ACK from server while in download")
+
+        elif isinstance(pkt, TftpPacketWRQ):
+            self.sendError(TftpErrors.IllegalTftpOp)
+            raise TftpException("Received WRQ from server while in download")
+
+        elif isinstance(pkt, TftpPacketERR):
+            self.sendError(TftpErrors.IllegalTftpOp)
+            raise TftpException("Received ERR from server: %s" % pkt)
+
+        else:
+            self.sendError(TftpErrors.IllegalTftpOp)
+            raise TftpException("Received unknown packet type from server: %s" % pkt)
+
+        # By default, no state change.
+        return self
diff --git a/tester/rt/tftpy/__init__.py b/tester/rt/tftpy/__init__.py
new file mode 100644
index 0000000..33f988c
--- /dev/null
+++ b/tester/rt/tftpy/__init__.py
@@ -0,0 +1,26 @@
+"""
+This library implements the tftp protocol, based on rfc 1350.
+http://www.faqs.org/rfcs/rfc1350.html
+At the moment it implements only a client class, but will include a server,
+with support for variable block sizes.
+
+As a client of tftpy, this is the only module that you should need to import
+directly. The TftpClient and TftpServer classes can be reached through it.
+"""
+
+from __future__ import absolute_import, division, print_function, unicode_literals
+import sys
+
+# Make sure that this is at least Python 2.3
+required_version = (2, 3)
+if sys.version_info < required_version:
+    raise ImportError("Requires at least Python 2.3")
+
+from .TftpShared import *
+from .TftpPacketTypes import *
+from .TftpPacketFactory import *
+from .TftpClient import *
+from .TftpServer import *
+from .TftpContexts import *
+from .TftpStates import *
+
diff --git a/tester/rtems/testing/bsps/beagleboneblack.mc b/tester/rtems/testing/bsps/beagleboneblack.mc
new file mode 100644
index 0000000..03edc94
--- /dev/null
+++ b/tester/rtems/testing/bsps/beagleboneblack.mc
@@ -0,0 +1,57 @@
+#
+# RTEMS Tools Project (http://www.rtems.org/)
+# Copyright 2010-2017 Chris Johns (chrisj at rtems.org)
+# All rights reserved.
+#
+# This file is part of the RTEMS Tools package in 'rtems-tools'.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+
+#
+# All paths in defaults must be Unix format. Do not store any Windows format
+# paths in the defaults.
+#
+# Every entry must describe the type of checking a host must pass.
+#
+# Records:
+#  key: type, attribute, value
+#   type     : none, dir, exe, triplet
+#   attribute: none, required, optional
+#   value    : 'single line', '''multi line'''
+#
+
+#
+# The BeagleBone Black board connected via TFTP. The console is connected to a
+# telnet tty device.
+#
+[global]
+bsp:                      none,    none,   'beagleboneblack'
+jobs:                     none,    none,   '1'
+test_restarts:            none,    none,   '3'
+target_reset_command:     none,    none,   'wemo-reset CSEng1 5'
+
+[beagleboneblack]
+beagleboneblack:          none,    none,   '%{_rtscripts}/tftp.cfg'
+beagleboneblack_arch:     none,    none,   'arm'
+bsp_tty_dev:              none,    none,   'tuke:30007'
diff --git a/tester/rtems/testing/tftp.cfg b/tester/rtems/testing/tftp.cfg
new file mode 100644
index 0000000..7c81870
--- /dev/null
+++ b/tester/rtems/testing/tftp.cfg
@@ -0,0 +1,57 @@
+#
+# RTEMS Tools Project (http://www.rtems.org/)
+# Copyright 2010-2017 Chris Johns (chrisj at rtems.org)
+# All rights reserved.
+#
+# This file is part of the RTEMS Tools package in 'rtems-tools'.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+
+#
+# TFTP
+#
+# Get the boot loader to load the executable via TFTP and the tester will
+# deliver the correct executable.
+#
+
+%include %{_configdir}/base.cfg
+%include %{_configdir}/checks.cfg
+
+#
+# Console.
+#
+%include %{_configdir}/console.cfg
+
+#
+# RTEMS version
+#
+%include %{_rtdir}/rtems/version.cfg
+
+#
+# TFTP server.
+#
+%ifn %{defined tftp_port}
+ %define tftp_port 69
+%endif
+%tftp %{test_executable} %{tftp_port}



More information about the vc mailing list