├── requirements.txt ├── iperf3 ├── __init__.py └── iperf3.py ├── MANIFEST.in ├── docs ├── source │ ├── _static │ │ └── iperf3-python.png │ ├── _templates │ │ ├── sidebarlogo.html │ │ └── sidebarintro.html │ ├── modules.rst │ ├── index.rst │ ├── installation.rst │ ├── examples.rst │ └── conf.py └── Makefile ├── tox.ini ├── LICENSE ├── examples ├── client.py ├── server.py └── udp_client.py ├── .gitignore ├── HISTORY.rst ├── setup.py ├── .travis.yml ├── README.rst └── tests ├── results.json └── test_iperf3.py /requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /iperf3/__init__.py: -------------------------------------------------------------------------------- 1 | from .iperf3 import Client, Server, TestResult, IPerf3 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS COPYING INSTALL LICENSE requirements.txt *.rst 2 | recursive-exclude *.pyc *.pyo 3 | -------------------------------------------------------------------------------- /docs/source/_static/iperf3-python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiezn/iperf3-python/HEAD/docs/source/_static/iperf3-python.png -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # content of: tox.ini , put in same dir as setup.py 2 | [tox] 3 | envlist = py27, py35 4 | [testenv] 5 | deps=pytest 6 | commands=py.test 7 | -------------------------------------------------------------------------------- /docs/source/_templates/sidebarlogo.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | Modules 2 | ======= 3 | 4 | iperf3 5 | ~~~~~~ 6 | .. automodule:: iperf3 7 | 8 | Client 9 | ++++++ 10 | 11 | .. autoclass:: Client 12 | :members: 13 | 14 | Server 15 | ++++++ 16 | 17 | .. autoclass:: Server 18 | :members: 19 | 20 | TestResult 21 | ++++++++++ 22 | 23 | .. autoclass:: TestResult 24 | :members: 25 | 26 | IPerf3 27 | ++++++ 28 | 29 | .. autoclass:: IPerf3 30 | :members: 31 | -------------------------------------------------------------------------------- /docs/source/_templates/sidebarintro.html: -------------------------------------------------------------------------------- 1 |

About iperf3 for Python

2 |

3 | iperf3 is a tool for active measurements of the maximum achievable bandwidth on IP networks. 4 | This is a python wrapper providing a nice and pythonic interface to the tool. 5 |

6 |

Useful Links

7 | 12 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. iperf3 documentation master file, created by 2 | sphinx-quickstart on Tue Aug 16 09:33:31 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | iperf3 python wrapper 7 | ===================== 8 | 9 | Release v\ |version|. 10 | 11 | iPerf3 is a tool for active measurements of the maximum achievable bandwidth on IP networks. More 12 | information on the iPerf3 utility can be found on their `official website `_ 13 | 14 | The python iperf3 module is a wrapper around the iperf3 utility. It utilises the API libiperf 15 | that comes with the default installation. It allows you to interact with the utility in a nice and 16 | pythonic way. 17 | 18 | **warning** This module is not compatible with the original iperf/iperf2 utility which is no longer under active development 19 | 20 | .. toctree:: 21 | :maxdepth: 3 22 | 23 | installation 24 | examples 25 | modules 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Mathijs Mortimer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import iperf3 4 | 5 | client = iperf3.Client() 6 | client.duration = 1 7 | client.server_hostname = '127.0.0.1' 8 | client.port = 5201 9 | 10 | print('Connecting to {0}:{1}'.format(client.server_hostname, client.port)) 11 | result = client.run() 12 | 13 | if result.error: 14 | print(result.error) 15 | else: 16 | print('') 17 | print('Test completed:') 18 | print(' started at {0}'.format(result.time)) 19 | print(' bytes transmitted {0}'.format(result.sent_bytes)) 20 | print(' retransmits {0}'.format(result.retransmits)) 21 | print(' avg cpu load {0}%\n'.format(result.local_cpu_total)) 22 | 23 | print('Average transmitted data in all sorts of networky formats:') 24 | print(' bits per second (bps) {0}'.format(result.sent_bps)) 25 | print(' Kilobits per second (kbps) {0}'.format(result.sent_kbps)) 26 | print(' Megabits per second (Mbps) {0}'.format(result.sent_Mbps)) 27 | print(' KiloBytes per second (kB/s) {0}'.format(result.sent_kB_s)) 28 | print(' MegaBytes per second (MB/s) {0}'.format(result.sent_MB_s)) 29 | -------------------------------------------------------------------------------- /examples/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import iperf3 4 | 5 | 6 | server = iperf3.Server() 7 | print('Running server: {0}:{1}'.format(server.bind_address, server.port)) 8 | 9 | while True: 10 | result = server.run() 11 | 12 | if result.error: 13 | print(result.error) 14 | else: 15 | print('') 16 | print('Test results from {0}:{1}'.format(result.remote_host, 17 | result.remote_port)) 18 | print(' started at {0}'.format(result.time)) 19 | print(' bytes received {0}'.format(result.received_bytes)) 20 | 21 | print('Average transmitted received in all sorts of networky formats:') 22 | print(' bits per second (bps) {0}'.format(result.received_bps)) 23 | print(' Kilobits per second (kbps) {0}'.format(result.received_kbps)) 24 | print(' Megabits per second (Mbps) {0}'.format(result.received_Mbps)) 25 | print(' KiloBytes per second (kB/s) {0}'.format(result.received_kB_s)) 26 | print(' MegaBytes per second (MB/s) {0}'.format(result.received_MB_s)) 27 | print('') 28 | -------------------------------------------------------------------------------- /examples/udp_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import iperf3 4 | 5 | client = iperf3.Client() 6 | client.duration = 1 7 | client.server_hostname = '127.0.0.1' 8 | client.port = 5201 9 | client.protocol = 'udp' 10 | 11 | 12 | print('Connecting to {0}:{1}'.format(client.server_hostname, client.port)) 13 | result = client.run() 14 | 15 | if result.error: 16 | print(result.error) 17 | else: 18 | print('') 19 | print('Test completed:') 20 | print(' started at {0}'.format(result.time)) 21 | print(' bytes transmitted {0}'.format(result.bytes)) 22 | print(' jitter (ms) {0}'.format(result.jitter_ms)) 23 | print(' avg cpu load {0}%\n'.format(result.local_cpu_total)) 24 | 25 | print('Average transmitted data in all sorts of networky formats:') 26 | print(' bits per second (bps) {0}'.format(result.bps)) 27 | print(' Kilobits per second (kbps) {0}'.format(result.kbps)) 28 | print(' Megabits per second (Mbps) {0}'.format(result.Mbps)) 29 | print(' KiloBytes per second (kB/s) {0}'.format(result.kB_s)) 30 | print(' MegaBytes per second (MB/s) {0}'.format(result.MB_s)) 31 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | Installation 4 | ============ 5 | 6 | To be able to utilise the python wrapper around iperf3 you will need to have the 7 | libiperf.so.0 shared library installed. Luckily this comes with the standard iperf3 8 | build. 9 | 10 | 11 | iperf3 utility 12 | ~~~~~~~~~~~~~~ 13 | 14 | Preferably get the latest build from the iperf3 `official website `__ 15 | 16 | Otherwise try your OS package manager: 17 | 18 | - Ubuntu: 19 | 20 | .. code-block:: bash 21 | 22 | sudo apt-get install iperf3 23 | 24 | - CentOS/RedHat 25 | 26 | .. code-block:: bash 27 | 28 | sudo yum install iperf3 29 | 30 | 31 | iperf3 python wrapper 32 | ~~~~~~~~~~~~~~~~~~~~~ 33 | 34 | The preferred installation method is through PyPi (aka pip install) 35 | 36 | .. code-block:: bash 37 | 38 | pip install iperf3 39 | 40 | If pip is unavailable for any reason you can also manually install from github: 41 | 42 | .. code-block:: bash 43 | 44 | git clone https://github.com/thiezn/iperf3-python.git 45 | cd iperf3-python 46 | python3 setup.py test # (optional) testing through py.test and/or tox 47 | python3 setup.py install 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editing 2 | *.swp 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # IPython Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | Release History 4 | --------------- 5 | 6 | 0.1.11 (2019-04-13) 7 | ++++++++++++++++++ 8 | - Fixed kB_s and MB_s in UDP test results (Thanks @gleichda) 9 | - Added omit option (Thanks @ChristofKaufmann) 10 | 11 | 0.1.10 (2018-03-28) 12 | +++++++++++++++++++ 13 | - Allow manual set of libiperf library path and name using lib_name kwarg on Iperf3 Class 14 | 15 | 0.1.9 (2018-02-22) 16 | ++++++++++++++++++ 17 | - Use find_library to load libiperf (Thanks to @Austinpayne). This should allow iperf3 lib to run on Mac OS X. 18 | 19 | 0.1.8 (2018-01-24) 20 | ++++++++++++++++++ 21 | - Fixed segmentation fault on several Linux distro's (Thanks to @illu89) 22 | - Added Travis testing against latest iperf3 releases (3.3) 23 | - Renamed bulksize argument to blksize to keep naming in line with iperf3 C library. bulksize argument still available for backwards compatibility 24 | 25 | 0.1.7 (2017-08-02) 26 | ++++++++++++++++++ 27 | - Fixed Mbps vs MB_s calculations (Thanks to @rustyhowell) 28 | 29 | 0.1.6 (2017-06-11) 30 | ++++++++++++++++++ 31 | - iperf3.__del__ now properly closing FD and pipes (Thanks to @p0intR) 32 | 33 | 0.1.5 (2017-05-22) 34 | ++++++++++++++++++ 35 | - iperf3.Client() now allows redirection of stdout (iperf3 version => 3.1 required) 36 | 37 | 0.1.4 (2017-05-15) 38 | ++++++++++++++++++ 39 | - Fixed server json_output=False feature 40 | 41 | 0.1.3 (2017-05-15) 42 | ++++++++++++++++++ 43 | - Added UDP support (thanks to @fciaccia) 44 | - Added bandwidth parameter 45 | - json_output = False will now print testresults to screen 46 | 47 | 0.1.2 (2016-09-22) 48 | ++++++++++++++++++ 49 | - Improved zerocopy setter validation 50 | - Fix for incorrect return of reverse flag (thanks to @fciaccia) 51 | 52 | 0.1.1 (2016-09-12) 53 | ++++++++++++++++++ 54 | 55 | - Added reverse test parameter (thanks to @cvicente) 56 | - Updated travis build to test against iperf3 versions 3.0.6 through 3.1.3 57 | 58 | 0.1.0 (2016-08-18) 59 | ++++++++++++++++++ 60 | 61 | **main functionality available** 62 | 63 | - introduced TestResult class providing easy access into test results 64 | - updated client and server examples 65 | - minor documentation tweaks 66 | 67 | 0.0.1 (2016-08-18) 68 | ++++++++++++++++++ 69 | 70 | **Initial Release** 71 | 72 | - Client and Server classes around iperf3's libiperf.so.0 API 73 | - Documentation on readthedocs 74 | - py.tests 75 | - blood 76 | - sweat 77 | - a lot of tears 78 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from setuptools import setup 3 | from setuptools.command.test import test as TestCommand 4 | import os 5 | import sys 6 | from codecs import open 7 | 8 | from iperf3 import iperf3 9 | 10 | here = os.path.abspath(os.path.dirname(__file__)) 11 | 12 | 13 | with open('README.rst', 'r', 'utf-8') as f: 14 | readme = f.read() 15 | 16 | with open('HISTORY.rst', 'r', 'utf-8') as f: 17 | history = f.read() 18 | 19 | 20 | class PyTest(TestCommand): 21 | def finalize_options(self): 22 | TestCommand.finalize_options(self) 23 | self.test_args = [] 24 | self.test_suite = True 25 | 26 | def run_tests(self): 27 | import pytest 28 | errcode = pytest.main(self.test_args) 29 | sys.exit(errcode) 30 | 31 | setup(name='iperf3', 32 | version=iperf3.__version__, 33 | url='https://github.com/thiezn/iperf3-python', 34 | author='Mathijs Mortimer', 35 | tests_require=['pytest'], 36 | install_requires=[], 37 | cmdclass={'test': PyTest}, 38 | description='Python wrapper around iperf3', 39 | keywords='iperf3 iperf', 40 | long_description=readme + '\n\n' + history, 41 | include_package_data=True, 42 | zip_safe=False, 43 | platforms='any', 44 | test_suite='iperf3.test.test_iperf3', 45 | classifiers=[ 46 | 'Programming Language :: Python', 47 | 'Programming Language :: Python :: 2', 48 | 'Programming Language :: Python :: 2.6', 49 | 'Programming Language :: Python :: 2.7', 50 | 'Programming Language :: Python :: 3', 51 | 'Programming Language :: Python :: 3.3', 52 | 'Programming Language :: Python :: 3.4', 53 | 'Programming Language :: Python :: 3.5', 54 | 'Development Status :: 4 - Beta', 55 | 'Natural Language :: English', 56 | 'Intended Audience :: Developers', 57 | 'Intended Audience :: System Administrators', 58 | 'Intended Audience :: Telecommunications Industry', 59 | 'License :: OSI Approved :: MIT License', 60 | 'Operating System :: Unix', 61 | 'Operating System :: MacOS', 62 | 'Topic :: Software Development :: Libraries :: Python Modules', 63 | 'Topic :: System :: Networking', 64 | ], 65 | extras_require={'testing': ['iperf3']}, 66 | author_email='mathijs@mortimer.nl', 67 | license='MIT', 68 | packages=['iperf3']) 69 | -------------------------------------------------------------------------------- /docs/source/examples.rst: -------------------------------------------------------------------------------- 1 | .. _examples: 2 | 3 | Examples 4 | ======== 5 | 6 | Check the examples/ folder for a few ready to go python scripts. 7 | 8 | Client 9 | ~~~~~~ 10 | 11 | **Example 1** 12 | 13 | This example sets up a client connection to a running server on 10.10.10.10:6969. 14 | When the test finalises the results are returned. This example shows all currently 15 | available options for a :class:`Client ` 16 | 17 | >>> import iperf3 18 | 19 | >>> client = iperf3.Client() 20 | >>> client.omit = 1 21 | >>> client.duration = 1 22 | >>> client.bind_address = '10.0.0.1' 23 | >>> client.server_hostname = '10.10.10.10' 24 | >>> client.port = 6969 25 | >>> client.blksize = 1234 26 | >>> client.num_streams = 10 27 | >>> client.zerocopy = True 28 | >>> client.verbose = False 29 | >>> client.reverse = True 30 | >>> client.run() 31 | {'start': {'test_start': {... 32 | 33 | **Example 2** 34 | 35 | This example shows how you can output the client test results to screen, just like 36 | the iperf3 application itself does. Note it does NOT return a :class:`TestResult ` 37 | instance. 38 | 39 | >>> import iperf3 40 | 41 | >>> client = iperf3.Client() 42 | >>> client.server_hostname = '10.10.10.10' 43 | >>> client.port = 6969 44 | >>> client.json_output = False 45 | >>> result = client.run() 46 | Time: Mon, 15 May 2017 18:20:01 GMT 47 | Connecting to host 10.10.10.10, port 6969 48 | [ 8] local 127.0.0.1 port 35670 connected to 127.0.0.1 port 5201 49 | Starting Test: protocol: TCP, 1 streams, 131072 byte blocks, omitting 0 seconds, 1 second test 50 | [ ID] Interval Transfer Bandwidth Retr Cwnd 51 | [ 8] 0.00-1.00 sec 3.96 GBytes 34.0 Gbits/sec 0 3.18 MByt... 52 | 53 | >>> result 54 | None 55 | 56 | **Example 3** 57 | 58 | Here is an example of running a UDP test. Please read the official documentation on 59 | UDP testing as there can be a few catches. 60 | 61 | .. literalinclude:: ../../examples/udp_client.py 62 | :language: python 63 | 64 | Server 65 | ~~~~~~ 66 | 67 | **Example 1** 68 | 69 | This example runs an iperf3 server on 10.10.10.10:6969 and prints out the test results. 70 | After each test ``server.run()`` finishes and produces the test results. This example 71 | shows all currently available options for a :class:`Server ` 72 | 73 | >>> import iperf3 74 | 75 | >>> server = iperf3.Server() 76 | >>> server.bind_address = '10.10.10.10' 77 | >>> server.port = 6969 78 | >>> server.verbose = False 79 | >>> while True: 80 | ... server.run() 81 | ... 82 | {'start': {'test_start': {... 83 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | os: linux 3 | sudo: required 4 | dist: trusty 5 | python: 6 | - "2.7" 7 | - "3.6" 8 | env: 9 | - IPERF_VERSION="3.0.6" 10 | - IPERF_VERSION="3.0.7" 11 | - IPERF_VERSION="3.0.8" 12 | - IPERF_VERSION="3.0.9" 13 | - IPERF_VERSION="3.0.10" 14 | - IPERF_VERSION="3.0.11" 15 | - IPERF_VERSION="3.0.12" 16 | - IPERF_VERSION="3.1" 17 | - IPERF_VERSION="3.1.1" 18 | - IPERF_VERSION="3.1.2" 19 | - IPERF_VERSION="3.1.3" 20 | - IPERF_VERSION="3.1.4" 21 | - IPERF_VERSION="3.1.5" 22 | - IPERF_VERSION="3.1.6" 23 | - IPERF_VERSION="3.1.7" 24 | - IPERF_VERSION="3.2" 25 | - IPERF_VERSION="3.3" 26 | - IPERF_VERSION="3.4" 27 | - IPERF_VERSION="3.5" 28 | - IPERF_VERSION="3.6" 29 | before_install: 30 | # Download the various available iperf3 sources 31 | - if [[ "$IPERF_VERSION" == "3.0.6" ]]; then wget https://github.com/esnet/iperf/archive/3.0.6.tar.gz ;fi 32 | - if [[ "$IPERF_VERSION" == "3.0.7" ]]; then wget https://github.com/esnet/iperf/archive/3.0.7.tar.gz ;fi 33 | - if [[ "$IPERF_VERSION" == "3.0.8" ]]; then wget https://github.com/esnet/iperf/archive/3.0.8.tar.gz ;fi 34 | - if [[ "$IPERF_VERSION" == "3.0.9" ]]; then wget https://github.com/esnet/iperf/archive/3.0.9.tar.gz ;fi 35 | - if [[ "$IPERF_VERSION" == "3.0.10" ]]; then wget https://github.com/esnet/iperf/archive/3.0.10.tar.gz ;fi 36 | - if [[ "$IPERF_VERSION" == "3.0.11" ]]; then wget https://github.com/esnet/iperf/archive/3.0.11.tar.gz ;fi 37 | - if [[ "$IPERF_VERSION" == "3.0.12" ]]; then wget https://github.com/esnet/iperf/archive/3.0.12.tar.gz ;fi 38 | - if [[ "$IPERF_VERSION" == "3.1" ]]; then wget https://github.com/esnet/iperf/archive/3.1.tar.gz ;fi 39 | - if [[ "$IPERF_VERSION" == "3.1.1" ]]; then wget https://github.com/esnet/iperf/archive/3.1.1.tar.gz ;fi 40 | - if [[ "$IPERF_VERSION" == "3.1.2" ]]; then wget https://github.com/esnet/iperf/archive/3.1.2.tar.gz ;fi 41 | - if [[ "$IPERF_VERSION" == "3.1.3" ]]; then wget https://github.com/esnet/iperf/archive/3.1.3.tar.gz ;fi 42 | - if [[ "$IPERF_VERSION" == "3.1.4" ]]; then wget https://github.com/esnet/iperf/archive/3.1.4.tar.gz ;fi 43 | - if [[ "$IPERF_VERSION" == "3.1.5" ]]; then wget https://github.com/esnet/iperf/archive/3.1.5.tar.gz ;fi 44 | - if [[ "$IPERF_VERSION" == "3.1.6" ]]; then wget https://github.com/esnet/iperf/archive/3.1.6.tar.gz ;fi 45 | - if [[ "$IPERF_VERSION" == "3.1.7" ]]; then wget https://github.com/esnet/iperf/archive/3.1.7.tar.gz ;fi 46 | - if [[ "$IPERF_VERSION" == "3.2" ]]; then wget https://github.com/esnet/iperf/archive/3.2.tar.gz ;fi 47 | - if [[ "$IPERF_VERSION" == "3.3" ]]; then wget https://github.com/esnet/iperf/archive/3.3.tar.gz ;fi 48 | - if [[ "$IPERF_VERSION" == "3.4" ]]; then wget https://github.com/esnet/iperf/archive/3.4.tar.gz ;fi 49 | - if [[ "$IPERF_VERSION" == "3.5" ]]; then wget https://github.com/esnet/iperf/archive/3.5.tar.gz ;fi 50 | - if [[ "$IPERF_VERSION" == "3.6" ]]; then wget https://github.com/esnet/iperf/archive/3.6.tar.gz ;fi 51 | 52 | # Install iperf version selected above 53 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then tar xvf *.tar.gz ;fi 54 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then cd iperf-*/ ;fi 55 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then ./bootstrap.sh ;fi 56 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then ./configure && make && sudo make install ;fi 57 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then cd .. ;fi 58 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export LD_LIBRARY_PATH=/usr/local/lib ;fi 59 | install: 60 | - "pip install -r requirements.txt" 61 | - "pip install coverage" 62 | - "pip install coveralls" 63 | script: 64 | - "coverage run --source=iperf3 setup.py test" 65 | after_success: 66 | coveralls 67 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | iperf3-python: Python wrapper around iperf3 2 | =========================================== 3 | 4 | |PyPi Status| |Build Status| |Coverage Status| |Documentation Status| 5 | 6 | Detailed documentation at 7 | `iperf3-python.readthedocs.org `__ 8 | 9 | iperf3 for python provides a wrapper around the excellent iperf3 10 | utility. iperf3 is a complete rewrite of the original iperf 11 | implementation. more information on the `official iperf3 12 | site `__ 13 | 14 | iperf3 introduced an API called libiperf that allows you to easily 15 | interact with iperf3 from other languages. This library provides a 16 | python wrapper around libiperf for easy integration into your own python 17 | scripts in a pythonic way 18 | 19 | Installation 20 | ------------ 21 | 22 | First you have to make sure the iperf3 utility is present on your system as the 23 | python module wraps around the libiperf API provided by it. 24 | 25 | The common linux distributions offer installations from their own repositories. These 26 | might be out of date so installation from the official `iperf3 website `__ 27 | is preferred. 28 | 29 | **note** The libiperf API was only introduced in 3.0.6 so make sure you have an updated version 30 | of iperf3 installation. 31 | 32 | **note** The libiperf API added a feature to programmatically retrieve the json output from the library. This 33 | enables us to retrieve the results without having to scrape the output from stdout. Effectively this means 34 | that if you want to redirect your script's stdout/stderr to something else you need at least iperf3 version 3.1 35 | 36 | - Install from source (preferred) 37 | 38 | .. code:: bash 39 | 40 | wget http://downloads.es.net/pub/iperf/iperf-3-current.tar.gz 41 | tar xvf iperf-3-current.tar.gz 42 | cd iperf-3.3/ # Or whatever the latest version is 43 | ./configure && make && sudo make install 44 | 45 | - Ubuntu: 46 | 47 | .. code:: bash 48 | 49 | sudo apt-get install iperf3 50 | 51 | - CenOS/RedHat 52 | 53 | .. code:: bash 54 | 55 | sudo yum install iperf3 56 | 57 | Once the iperf3 utility is installed the simplest way to install the python wrapper is through 58 | `PyPi `__ 59 | 60 | .. code:: bash 61 | 62 | pip install iperf3 63 | 64 | You can also install directly from the github repository: 65 | 66 | .. code:: bash 67 | 68 | git clone https://github.com/thiezn/iperf3-python.git 69 | cd iperf3-python 70 | python3 setup.py install 71 | 72 | Quickstart 73 | ---------- 74 | 75 | For detailed examples check out the `examples page `__ or 76 | the detailed documentation at `iperf3-python.readthedocs.org `__ 77 | 78 | 79 | **Server** 80 | 81 | .. code:: python 82 | 83 | >>> import iperf3 84 | 85 | >>> server = iperf3.Server() 86 | >>> result = server.run() 87 | >>> result.remote_host 88 | "10.10.10.10" 89 | 90 | **Client** 91 | 92 | .. code:: python 93 | 94 | >>> import iperf3 95 | 96 | >>> client = iperf3.Client() 97 | >>> client.duration = 1 98 | >>> client.server_hostname = '127.0.0.1' 99 | >>> client.port = 5201 100 | >>> result = client.run() 101 | >>> result.sent_Mbps 102 | 32583.293914794922 103 | 104 | 105 | External Dependencies 106 | --------------------- 107 | 108 | - iperf3 109 | - libiperf.so.0 (Comes with iperf3 >= 3.0.6) 110 | 111 | Testing 112 | ------- 113 | 114 | - Tested against the following iperf3 versions on Linux: 115 | 116 | - 3.0.6 117 | - 3.0.7 118 | - 3.0.8 119 | - 3.0.9 120 | - 3.0.10 121 | - 3.0.11 122 | - 3.0.12 123 | - 3.1 124 | - 3.1.1 125 | - 3.1.2 126 | - 3.1.3 127 | - 3.1.4 128 | - 3.1.5 129 | - 3.1.6 130 | - 3.1.7 131 | - 3.2 132 | - 3.3 133 | - 3.4 134 | - 3.5 135 | - 3.6 136 | 137 | - Test coverage reporting through `coveralls.io `__ 138 | - Tested against the following Python versions: 139 | 140 | - 2.7 141 | - 3.6 142 | 143 | .. |PyPi Status| image:: https://img.shields.io/pypi/v/iperf3.svg 144 | :target: https://pypi.python.org/pypi/iperf3 145 | .. |Build Status| image:: https://travis-ci.org/thiezn/iperf3-python.svg?branch=master 146 | :target: https://travis-ci.org/thiezn/iperf3-python 147 | .. |Coverage Status| image:: https://coveralls.io/repos/github/thiezn/iperf3-python/badge.svg?branch=master 148 | :target: https://coveralls.io/github/thiezn/iperf3-python?branch=master 149 | .. |Documentation Status| image:: https://readthedocs.org/projects/iperf3-python/badge/?version=latest 150 | :target: http://iperf3-python.readthedocs.io/en/latest/?badge=latest 151 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/iperf3.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/iperf3.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/iperf3" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/iperf3" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /tests/results.json: -------------------------------------------------------------------------------- 1 | { 2 | "start": { 3 | "connected": [{ 4 | "socket": 4, 5 | "local_host": "192.168.0.3", 6 | "local_port": 60166, 7 | "remote_host": "192.168.0.188", 8 | "remote_port": 9987 9 | }], 10 | "version": "iperf 3.0.11", 11 | "system_info": "Linux rhowelllx 4.4.0-79-generic #100-Ubuntu SMP Wed May 17 19:58:14 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux\n", 12 | "timestamp": { 13 | "time": "Wed, 28 Jun 2017 15:31:28 GMT", 14 | "timesecs": 1498663888 15 | }, 16 | "connecting_to": { 17 | "host": "192.168.0.188", 18 | "port": 9987 19 | }, 20 | "cookie": "rhowelllx.1498663888.027091.78beffbd", 21 | "tcp_mss_default": 1448, 22 | "test_start": { 23 | "protocol": "TCP", 24 | "num_streams": 1, 25 | "blksize": 131072, 26 | "omit": 0, 27 | "duration": 13, 28 | "bytes": 0, 29 | "blocks": 0, 30 | "reverse": 0 31 | } 32 | }, 33 | "intervals": [{ 34 | "streams": [{ 35 | "socket": 4, 36 | "start": 0, 37 | "end": 1.00007, 38 | "seconds": 1.00007, 39 | "bytes": 118766408, 40 | "bits_per_second": 9.50062e+08, 41 | "retransmits": 0, 42 | "snd_cwnd": 573408, 43 | "omitted": false 44 | }], 45 | "sum": { 46 | "start": 0, 47 | "end": 1.00007, 48 | "seconds": 1.00007, 49 | "bytes": 118766408, 50 | "bits_per_second": 9.50062e+08, 51 | "retransmits": 0, 52 | "omitted": false 53 | } 54 | }, { 55 | "streams": [{ 56 | "socket": 4, 57 | "start": 1.00007, 58 | "end": 2.00006, 59 | "seconds": 0.999984, 60 | "bytes": 117548640, 61 | "bits_per_second": 9.40404e+08, 62 | "retransmits": 0, 63 | "snd_cwnd": 573408, 64 | "omitted": false 65 | }], 66 | "sum": { 67 | "start": 1.00007, 68 | "end": 2.00006, 69 | "seconds": 0.999984, 70 | "bytes": 117548640, 71 | "bits_per_second": 9.40404e+08, 72 | "retransmits": 0, 73 | "omitted": false 74 | } 75 | }, { 76 | "streams": [{ 77 | "socket": 4, 78 | "start": 2.00006, 79 | "end": 3.00006, 80 | "seconds": 1, 81 | "bytes": 116375760, 82 | "bits_per_second": 9.31005e+08, 83 | "retransmits": 0, 84 | "snd_cwnd": 573408, 85 | "omitted": false 86 | }], 87 | "sum": { 88 | "start": 2.00006, 89 | "end": 3.00006, 90 | "seconds": 1, 91 | "bytes": 116375760, 92 | "bits_per_second": 9.31005e+08, 93 | "retransmits": 0, 94 | "omitted": false 95 | } 96 | }, { 97 | "streams": [{ 98 | "socket": 4, 99 | "start": 3.00006, 100 | "end": 4.00006, 101 | "seconds": 1, 102 | "bytes": 116310600, 103 | "bits_per_second": 930484800, 104 | "retransmits": 0, 105 | "snd_cwnd": 573408, 106 | "omitted": false 107 | }], 108 | "sum": { 109 | "start": 3.00006, 110 | "end": 4.00006, 111 | "seconds": 1, 112 | "bytes": 116310600, 113 | "bits_per_second": 930484800, 114 | "retransmits": 0, 115 | "omitted": false 116 | } 117 | }, { 118 | "streams": [{ 119 | "socket": 4, 120 | "start": 4.00006, 121 | "end": 5.00006, 122 | "seconds": 1, 123 | "bytes": 117678960, 124 | "bits_per_second": 941431680, 125 | "retransmits": 0, 126 | "snd_cwnd": 573408, 127 | "omitted": false 128 | }], 129 | "sum": { 130 | "start": 4.00006, 131 | "end": 5.00006, 132 | "seconds": 1, 133 | "bytes": 117678960, 134 | "bits_per_second": 941431680, 135 | "retransmits": 0, 136 | "omitted": false 137 | } 138 | }, { 139 | "streams": [{ 140 | "socket": 4, 141 | "start": 5.00006, 142 | "end": 6.00006, 143 | "seconds": 1, 144 | "bytes": 116831880, 145 | "bits_per_second": 934655040, 146 | "retransmits": 0, 147 | "snd_cwnd": 640016, 148 | "omitted": false 149 | }], 150 | "sum": { 151 | "start": 5.00006, 152 | "end": 6.00006, 153 | "seconds": 1, 154 | "bytes": 116831880, 155 | "bits_per_second": 934655040, 156 | "retransmits": 0, 157 | "omitted": false 158 | } 159 | }, { 160 | "streams": [{ 161 | "socket": 4, 162 | "start": 6.00006, 163 | "end": 7.00006, 164 | "seconds": 0.999999, 165 | "bytes": 117353160, 166 | "bits_per_second": 9.38826e+08, 167 | "retransmits": 0, 168 | "snd_cwnd": 640016, 169 | "omitted": false 170 | }], 171 | "sum": { 172 | "start": 6.00006, 173 | "end": 7.00006, 174 | "seconds": 0.999999, 175 | "bytes": 117353160, 176 | "bits_per_second": 9.38826e+08, 177 | "retransmits": 0, 178 | "omitted": false 179 | } 180 | }, { 181 | "streams": [{ 182 | "socket": 4, 183 | "start": 7.00006, 184 | "end": 8.00006, 185 | "seconds": 1, 186 | "bytes": 115984800, 187 | "bits_per_second": 9.27877e+08, 188 | "retransmits": 0, 189 | "snd_cwnd": 640016, 190 | "omitted": false 191 | }], 192 | "sum": { 193 | "start": 7.00006, 194 | "end": 8.00006, 195 | "seconds": 1, 196 | "bytes": 115984800, 197 | "bits_per_second": 9.27877e+08, 198 | "retransmits": 0, 199 | "omitted": false 200 | } 201 | }, { 202 | "streams": [{ 203 | "socket": 4, 204 | "start": 8.00006, 205 | "end": 9.00007, 206 | "seconds": 1.00001, 207 | "bytes": 117613800, 208 | "bits_per_second": 9.40903e+08, 209 | "retransmits": 0, 210 | "snd_cwnd": 640016, 211 | "omitted": false 212 | }], 213 | "sum": { 214 | "start": 8.00006, 215 | "end": 9.00007, 216 | "seconds": 1.00001, 217 | "bytes": 117613800, 218 | "bits_per_second": 9.40903e+08, 219 | "retransmits": 0, 220 | "omitted": false 221 | } 222 | }, { 223 | "streams": [{ 224 | "socket": 4, 225 | "start": 9.00007, 226 | "end": 10.0001, 227 | "seconds": 1.00001, 228 | "bytes": 115984800, 229 | "bits_per_second": 9.27871e+08, 230 | "retransmits": 0, 231 | "snd_cwnd": 640016, 232 | "omitted": false 233 | }], 234 | "sum": { 235 | "start": 9.00007, 236 | "end": 10.0001, 237 | "seconds": 1.00001, 238 | "bytes": 115984800, 239 | "bits_per_second": 9.27871e+08, 240 | "retransmits": 0, 241 | "omitted": false 242 | } 243 | }, { 244 | "streams": [{ 245 | "socket": 4, 246 | "start": 10.0001, 247 | "end": 11.0001, 248 | "seconds": 0.999983, 249 | "bytes": 117360680, 250 | "bits_per_second": 9.38902e+08, 251 | "retransmits": 0, 252 | "snd_cwnd": 640016, 253 | "omitted": false 254 | }], 255 | "sum": { 256 | "start": 10.0001, 257 | "end": 11.0001, 258 | "seconds": 0.999983, 259 | "bytes": 117360680, 260 | "bits_per_second": 9.38902e+08, 261 | "retransmits": 0, 262 | "omitted": false 263 | } 264 | }, { 265 | "streams": [{ 266 | "socket": 4, 267 | "start": 11.0001, 268 | "end": 12.0001, 269 | "seconds": 1, 270 | "bytes": 116531240, 271 | "bits_per_second": 9.32249e+08, 272 | "retransmits": 0, 273 | "snd_cwnd": 941200, 274 | "omitted": false 275 | }], 276 | "sum": { 277 | "start": 11.0001, 278 | "end": 12.0001, 279 | "seconds": 1, 280 | "bytes": 116531240, 281 | "bits_per_second": 9.32249e+08, 282 | "retransmits": 0, 283 | "omitted": false 284 | } 285 | }, { 286 | "streams": [{ 287 | "socket": 4, 288 | "start": 12.0001, 289 | "end": 13.0001, 290 | "seconds": 1.00001, 291 | "bytes": 116654080, 292 | "bits_per_second": 9.33228e+08, 293 | "retransmits": 0, 294 | "snd_cwnd": 941200, 295 | "omitted": false 296 | }], 297 | "sum": { 298 | "start": 12.0001, 299 | "end": 13.0001, 300 | "seconds": 1.00001, 301 | "bytes": 116654080, 302 | "bits_per_second": 9.33228e+08, 303 | "retransmits": 0, 304 | "omitted": false 305 | } 306 | }], 307 | "end": { 308 | "streams": [{ 309 | "sender": { 310 | "socket": 4, 311 | "start": 0, 312 | "end": 13.0001, 313 | "seconds": 13.0001, 314 | "bytes": 1520994808, 315 | "bits_per_second": 9.35992e+08, 316 | "retransmits": 0 317 | }, 318 | "receiver": { 319 | "socket": 4, 320 | "start": 0, 321 | "end": 13.0001, 322 | "seconds": 13.0001, 323 | "bytes": 1518193248, 324 | "bits_per_second": 9.34268e+08 325 | } 326 | }], 327 | "sum_sent": { 328 | "start": 0, 329 | "end": 13.0001, 330 | "seconds": 13.0001, 331 | "bytes": 1520994808, 332 | "bits_per_second": 9.35992e+08, 333 | "retransmits": 0 334 | }, 335 | "sum_received": { 336 | "start": 0, 337 | "end": 13.0001, 338 | "seconds": 13.0001, 339 | "bytes": 1518193248, 340 | "bits_per_second": 9.34268e+08 341 | }, 342 | "cpu_utilization_percent": { 343 | "host_total": 1.66812, 344 | "host_user": 0.0611844, 345 | "host_system": 1.59079, 346 | "remote_total": 4.15082, 347 | "remote_user": 0.156562, 348 | "remote_system": 3.99574 349 | } 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # iperf3 documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Aug 16 09:33:31 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | import os 24 | import sys 25 | sys.path.insert(0, os.path.abspath('../../iperf3')) 26 | 27 | # -- General configuration ------------------------------------------------ 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | # 31 | # needs_sphinx = '1.0' 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = [ 37 | 'sphinx.ext.autodoc', 38 | 'sphinx.ext.todo', 39 | 'sphinx.ext.coverage', 40 | 'sphinx.ext.viewcode', 41 | ] 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ['_templates'] 45 | 46 | # The suffix(es) of source filenames. 47 | # You can specify multiple suffix as a list of string: 48 | # 49 | # source_suffix = ['.rst', '.md'] 50 | source_suffix = '.rst' 51 | 52 | # The encoding of source files. 53 | # 54 | # source_encoding = 'utf-8-sig' 55 | 56 | # The master toctree document. 57 | master_doc = 'index' 58 | 59 | # General information about the project. 60 | project = 'iperf3' 61 | copyright = '2017, Mathijs Mortimer' 62 | author = 'Mathijs Mortimer' 63 | 64 | # The version info for the project you're documenting, acts as replacement for 65 | # |version| and |release|, also used in various other places throughout the 66 | # built documents. 67 | # 68 | # The short X.Y version. 69 | version = '0.1.11' 70 | # The full version, including alpha/beta/rc tags. 71 | release = '0.1.11' 72 | 73 | # The language for content autogenerated by Sphinx. Refer to documentation 74 | # for a list of supported languages. 75 | # 76 | # This is also used if you do content translation via gettext catalogs. 77 | # Usually you set "language" from the command line for these cases. 78 | language = None 79 | 80 | # There are two options for replacing |today|: either, you set today to some 81 | # non-false value, then it is used: 82 | # 83 | # today = '' 84 | # 85 | # Else, today_fmt is used as the format for a strftime call. 86 | # 87 | # today_fmt = '%B %d, %Y' 88 | 89 | # List of patterns, relative to source directory, that match files and 90 | # directories to ignore when looking for source files. 91 | # This patterns also effect to html_static_path and html_extra_path 92 | exclude_patterns = [] 93 | 94 | # The reST default role (used for this markup: `text`) to use for all 95 | # documents. 96 | # 97 | # default_role = None 98 | 99 | # If true, '()' will be appended to :func: etc. cross-reference text. 100 | # 101 | add_function_parentheses = True 102 | 103 | # If true, the current module name will be prepended to all description 104 | # unit titles (such as .. function::). 105 | # 106 | # add_module_names = True 107 | 108 | # If true, sectionauthor and moduleauthor directives will be shown in the 109 | # output. They are ignored by default. 110 | # 111 | # show_authors = False 112 | 113 | # The name of the Pygments (syntax highlighting) style to use. 114 | pygments_style = 'sphinx' 115 | 116 | # A list of ignored prefixes for module index sorting. 117 | # modindex_common_prefix = [] 118 | 119 | # If true, keep warnings as "system message" paragraphs in the built documents. 120 | # keep_warnings = False 121 | 122 | # If true, `todo` and `todoList` produce output, else they produce nothing. 123 | todo_include_todos = True 124 | 125 | 126 | # -- Options for HTML output ---------------------------------------------- 127 | 128 | # The theme to use for HTML and HTML Help pages. See the documentation for 129 | # a list of builtin themes. 130 | # 131 | html_theme = 'alabaster' 132 | 133 | # Theme options are theme-specific and customize the look and feel of a theme 134 | # further. For a list of options available for each theme, see the 135 | # documentation. 136 | # 137 | # html_theme_options = {} 138 | html_theme_options = { 139 | 'show_powered_by': False, 140 | 'github_user': 'thiezn', 141 | 'github_repo': 'iperf3-python', 142 | 'github_banner': True, 143 | 'show_related': False 144 | } 145 | 146 | # Add any paths that contain custom themes here, relative to this directory. 147 | # html_theme_path = [] 148 | 149 | # The name for this set of Sphinx documents. 150 | # " v documentation" by default. 151 | # 152 | # html_title = 'iperf3 v0.1' 153 | 154 | # A shorter title for the navigation bar. Default is the same as html_title. 155 | # 156 | # html_short_title = None 157 | 158 | # The name of an image file (relative to this directory) to place at the top 159 | # of the sidebar. 160 | # 161 | # html_logo = None 162 | 163 | # The name of an image file (relative to this directory) to use as a favicon of 164 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 165 | # pixels large. 166 | # 167 | # html_favicon = None 168 | 169 | # Add any paths that contain custom static files (such as style sheets) here, 170 | # relative to this directory. They are copied after the builtin static files, 171 | # so a file named "default.css" will overwrite the builtin "default.css". 172 | html_static_path = ['_static'] 173 | 174 | # Add any extra paths that contain custom files (such as robots.txt or 175 | # .htaccess) here, relative to this directory. These files are copied 176 | # directly to the root of the documentation. 177 | # 178 | # html_extra_path = [] 179 | 180 | # If not None, a 'Last updated on:' timestamp is inserted at every page 181 | # bottom, using the given strftime format. 182 | # The empty string is equivalent to '%b %d, %Y'. 183 | # 184 | # html_last_updated_fmt = None 185 | 186 | # If true, SmartyPants will be used to convert quotes and dashes to 187 | # typographically correct entities. 188 | # 189 | html_use_smartypants = True 190 | 191 | # Custom sidebar templates, maps document names to template names. 192 | # 193 | #html_sidebars = {} 194 | html_sidebars = { 195 | 'index': [ 196 | 'sidebarintro.html', 197 | 'sourcelink.html', 198 | 'searchbox.html' 199 | ], 200 | '**': [ 201 | 'sidebarlogo.html', 202 | 'localtoc.html', 203 | 'relations.html', 204 | 'sourcelink.html', 205 | 'searchbox.html' 206 | ] 207 | } 208 | 209 | # Additional templates that should be rendered to pages, maps page names to 210 | # template names. 211 | # 212 | # html_additional_pages = {} 213 | 214 | # If false, no module index is generated. 215 | # 216 | # html_domain_indices = True 217 | 218 | # If false, no index is generated. 219 | # 220 | # html_use_index = True 221 | 222 | # If true, the index is split into individual pages for each letter. 223 | # 224 | # html_split_index = False 225 | 226 | # If true, links to the reST sources are added to the pages. 227 | # 228 | html_show_sourcelink = False 229 | 230 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 231 | # 232 | html_show_sphinx = False 233 | 234 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 235 | # 236 | html_show_copyright = True 237 | 238 | # If true, an OpenSearch description file will be output, and all pages will 239 | # contain a tag referring to it. The value of this option must be the 240 | # base URL from which the finished HTML is served. 241 | # 242 | # html_use_opensearch = '' 243 | 244 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 245 | # html_file_suffix = None 246 | 247 | # Language to be used for generating the HTML full-text search index. 248 | # Sphinx supports the following languages: 249 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 250 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 251 | # 252 | # html_search_language = 'en' 253 | 254 | # A dictionary with options for the search language support, empty by default. 255 | # 'ja' uses this config value. 256 | # 'zh' user can custom change `jieba` dictionary path. 257 | # 258 | # html_search_options = {'type': 'default'} 259 | 260 | # The name of a javascript file (relative to the configuration directory) that 261 | # implements a search results scorer. If empty, the default will be used. 262 | # 263 | # html_search_scorer = 'scorer.js' 264 | 265 | # Output file base name for HTML help builder. 266 | htmlhelp_basename = 'iperf3doc' 267 | 268 | # -- Options for LaTeX output --------------------------------------------- 269 | 270 | latex_elements = { 271 | # The paper size ('letterpaper' or 'a4paper'). 272 | # 273 | # 'papersize': 'letterpaper', 274 | 275 | # The font size ('10pt', '11pt' or '12pt'). 276 | # 277 | # 'pointsize': '10pt', 278 | 279 | # Additional stuff for the LaTeX preamble. 280 | # 281 | # 'preamble': '', 282 | 283 | # Latex figure (float) alignment 284 | # 285 | # 'figure_align': 'htbp', 286 | } 287 | 288 | # Grouping the document tree into LaTeX files. List of tuples 289 | # (source start file, target name, title, 290 | # author, documentclass [howto, manual, or own class]). 291 | latex_documents = [ 292 | (master_doc, 'iperf3.tex', 'iperf3 Documentation', 293 | 'Mathijs Mortimer', 'manual'), 294 | ] 295 | 296 | # The name of an image file (relative to this directory) to place at the top of 297 | # the title page. 298 | # 299 | # latex_logo = None 300 | 301 | # For "manual" documents, if this is true, then toplevel headings are parts, 302 | # not chapters. 303 | # 304 | # latex_use_parts = False 305 | 306 | # If true, show page references after internal links. 307 | # 308 | # latex_show_pagerefs = False 309 | 310 | # If true, show URL addresses after external links. 311 | # 312 | # latex_show_urls = False 313 | 314 | # Documents to append as an appendix to all manuals. 315 | # 316 | # latex_appendices = [] 317 | 318 | # If false, no module index is generated. 319 | # 320 | # latex_domain_indices = True 321 | 322 | 323 | # -- Options for manual page output --------------------------------------- 324 | 325 | # One entry per manual page. List of tuples 326 | # (source start file, name, description, authors, manual section). 327 | man_pages = [ 328 | (master_doc, 'iperf3', 'iperf3 Documentation', 329 | [author], 1) 330 | ] 331 | 332 | # If true, show URL addresses after external links. 333 | # 334 | # man_show_urls = False 335 | 336 | 337 | # -- Options for Texinfo output ------------------------------------------- 338 | 339 | # Grouping the document tree into Texinfo files. List of tuples 340 | # (source start file, target name, title, author, 341 | # dir menu entry, description, category) 342 | texinfo_documents = [ 343 | (master_doc, 'iperf3', 'iperf3 Documentation', 344 | author, 'iperf3', 'One line description of project.', 345 | 'Miscellaneous'), 346 | ] 347 | 348 | # Documents to append as an appendix to all manuals. 349 | # 350 | # texinfo_appendices = [] 351 | 352 | # If false, no module index is generated. 353 | # 354 | # texinfo_domain_indices = True 355 | 356 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 357 | # 358 | # texinfo_show_urls = 'footnote' 359 | 360 | # If true, do not generate a @detailmenu in the "Top" node's menu. 361 | # 362 | # texinfo_no_detailmenu = False 363 | -------------------------------------------------------------------------------- /tests/test_iperf3.py: -------------------------------------------------------------------------------- 1 | import os 2 | import iperf3 3 | import pytest 4 | import subprocess 5 | from time import sleep 6 | 7 | def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): 8 | return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) 9 | 10 | class TestPyPerf: 11 | 12 | def test_init_client(self): 13 | client = iperf3.Client() 14 | assert client._test 15 | 16 | def test_init_server(self): 17 | server = iperf3.Server() 18 | assert server._test 19 | 20 | def test_lib_name(self): 21 | client = iperf3.Client(lib_name='libiperf.so.0') 22 | assert client._test 23 | 24 | def test_run_not_implemented(self): 25 | with pytest.raises(NotImplementedError): 26 | client = iperf3.IPerf3(role='c') 27 | client.run() 28 | 29 | def test_incorrect_role(self): 30 | with pytest.raises(ValueError): 31 | iperf3.IPerf3(role='bla') 32 | 33 | def test_role_property(self): 34 | client = iperf3.Client() 35 | assert client.role == 'c' 36 | 37 | def test_protocol_property(self): 38 | client = iperf3.Client() 39 | assert client.protocol == 'tcp' 40 | 41 | client.protocol = 'udp' 42 | assert client.protocol == 'udp' 43 | 44 | client.protocol = 'tcp' 45 | assert client.protocol == 'tcp' 46 | 47 | def test_udp_blksize_limit(self): 48 | """Ensure the blksize can't exceed MAX_UDP_BULKSIZE""" 49 | MAX_UDP_BULKSIZE = (65535 - 8 - 20) 50 | client = iperf3.Client() 51 | client.protocol = 'udp' 52 | 53 | client.blksize = MAX_UDP_BULKSIZE + 10 54 | assert client.blksize == MAX_UDP_BULKSIZE 55 | 56 | def test_bind_address_empty(self): 57 | """Test if we bind to any/all address when empty bind_address is 58 | passed""" 59 | client = iperf3.Client() 60 | client.bind_address = '' 61 | assert client.bind_address == '*' 62 | 63 | def test_bind_address(self): 64 | """Test setting of the bind address is properly passed 65 | on to the iperf3 API""" 66 | client = iperf3.Client() 67 | client.bind_address = '127.0.0.1' 68 | assert client.bind_address == '127.0.0.1' 69 | 70 | def test_port(self): 71 | server = iperf3.Server() 72 | server.port = 666 73 | assert server.port == 666 74 | 75 | def test_server_hostname_empty(self): 76 | client = iperf3.Client() 77 | client.server_hostname = '' 78 | assert client.server_hostname == None 79 | 80 | def test_server_hostname(self): 81 | client = iperf3.Server() 82 | client.server_hostname = '127.0.0.1' 83 | assert client.server_hostname == '127.0.0.1' 84 | 85 | def test_omit(self): 86 | client = iperf3.Client() 87 | client.omit = 666 88 | assert client.omit == 666 89 | 90 | def test_duration(self): 91 | client = iperf3.Client() 92 | client.duration = 666 93 | assert client.duration == 666 94 | 95 | def test_iperf3_version(self): 96 | client = iperf3.Client() 97 | assert 'iperf' in client.iperf_version 98 | 99 | def test_blksize(self): 100 | client = iperf3.Client() 101 | client.blksize = 666 102 | assert client.blksize == 666 103 | 104 | def test_bandwidth(self): 105 | client = iperf3.Client() 106 | client.bandwidth = 1 107 | assert client.bandwidth == 1 108 | 109 | def test_num_streams(self): 110 | client = iperf3.Client() 111 | client.num_streams = 666 112 | assert client.num_streams == 666 113 | 114 | def test_json_output_enabled(self): 115 | client = iperf3.Client() 116 | client.json_output = True 117 | assert client.json_output 118 | 119 | def test_json_output_disabled(self): 120 | client = iperf3.Client() 121 | client.json_output = False 122 | assert not client.json_output 123 | 124 | def test_verbose_enabled(self): 125 | client = iperf3.Client() 126 | client.verbose = True 127 | assert client.verbose 128 | 129 | def test_verbose_disabled(self): 130 | client = iperf3.Client() 131 | client.verbose = False 132 | assert not client.verbose 133 | 134 | def test_zerocopy_enabled(self): 135 | client = iperf3.Client() 136 | client.zerocopy = True 137 | assert client.zerocopy 138 | 139 | def test_zerocopy_disabled(self): 140 | client = iperf3.Client() 141 | client.zerocopy = False 142 | assert not client.zerocopy 143 | 144 | def test_reverse_enabled(self): 145 | client = iperf3.Client() 146 | client.reverse = True 147 | assert client.reverse 148 | 149 | def test_reverse_disabled(self): 150 | client = iperf3.Client() 151 | client.reverse = False 152 | assert not client.reverse 153 | 154 | def test_get_last_error(self): 155 | client = iperf3.Client() 156 | print(client._error_to_string(client._errno)) 157 | assert client._errno == 111 158 | 159 | def test_error_to_string(self): 160 | client = iperf3.Client() 161 | assert client._error_to_string(1) == 'cannot be both server and client' 162 | 163 | def test_client_failed_run(self): 164 | client = iperf3.Client() 165 | client.server_hostname = '127.0.0.1' 166 | client.port = 5201 167 | client.duration = 1 168 | response = client.run() 169 | assert "unable to connect to server" in response.error 170 | 171 | def test_client_succesful_run(self): 172 | client = iperf3.Client() 173 | client.server_hostname = '127.0.0.1' 174 | client.port = 5202 175 | client.duration = 1 176 | 177 | server = subprocess.Popen(["iperf3", "-s", "-p", "5202"]) 178 | sleep(.3) # give the server some time to start 179 | response = client.run() 180 | server.kill() 181 | 182 | assert response.remote_host == '127.0.0.1' 183 | assert response.remote_port == 5202 184 | 185 | # These are added to check some of the TestResult variables 186 | assert not response.reverse 187 | assert response.type == 'client' 188 | assert response.__repr__() 189 | 190 | def test_client_succesful_run_reverse(self): 191 | client = iperf3.Client() 192 | client.server_hostname = '127.0.0.1' 193 | client.port = 5203 194 | client.duration = 1 195 | client.reverse = True 196 | 197 | server = subprocess.Popen(["iperf3", "-s", "-p", "5203"]) 198 | sleep(.3) # give the server some time to start 199 | response = client.run() 200 | server.kill() 201 | 202 | assert response.remote_host == '127.0.0.1' 203 | assert response.remote_port == 5203 204 | 205 | # These are added to check some of the TestResult variables 206 | assert response.reverse 207 | assert response.type == 'client' 208 | assert response.__repr__() 209 | 210 | def test_client_succesful_run_udp(self): 211 | client = iperf3.Client() 212 | client.protocol = 'udp' 213 | client.server_hostname = '127.0.0.1' 214 | client.port = 5204 215 | client.duration = 1 216 | 217 | server = subprocess.Popen(["iperf3", "-s", "-p", "5204"]) 218 | sleep(.3) # give the server some time to start 219 | response = client.run() 220 | server.kill() 221 | 222 | assert response.remote_host == '127.0.0.1' 223 | assert response.remote_port == 5204 224 | 225 | # These are added to check some of the TestResult variables 226 | assert not response.reverse 227 | assert response.type == 'client' 228 | assert response.__repr__() 229 | 230 | def test_server_failed_run(self): 231 | """This test will launch two server instances on the same ip:port 232 | to generate an error""" 233 | server = iperf3.Server() 234 | server.bind_address = '127.0.0.1' 235 | server.port = 5201 236 | 237 | server2 = subprocess.Popen(["iperf3", "-s"]) 238 | sleep(.3) # give the server some time to start 239 | 240 | response = server.run() 241 | server2.kill() 242 | 243 | assert "unable to start listener for connections: " in response.error 244 | 245 | def test_server_run(self): 246 | server = iperf3.Server() 247 | server.bind_address = '127.0.0.1' 248 | server.port = 5205 249 | 250 | # Launching the client with a sleep timer to give our server some time to start 251 | client = subprocess.Popen( 252 | 'sleep .5 && iperf3 -c 127.0.0.1 -p 5205 -t 1', 253 | shell=True, 254 | stdout=subprocess.PIPE, 255 | stderr=subprocess.PIPE 256 | ) 257 | response = server.run() 258 | 259 | if server.iperf_version.startswith('iperf 3.0') or server.iperf_version.startswith('iperf 3.1'): 260 | assert not response.error 261 | assert response.local_host == '127.0.0.1' 262 | assert response.local_port == 5205 263 | assert response.type == 'server' 264 | else: 265 | assert response.error == 'the client has unexpectedly closed the connection' 266 | 267 | def test_server_run_output_to_screen(self): 268 | server = iperf3.Server() 269 | server.bind_address = '127.0.0.1' 270 | server.port = 5206 271 | server.json_output = False 272 | 273 | # Launching the client with a sleep timer to give our server some time to start 274 | client = subprocess.Popen( 275 | 'sleep .5 && iperf3 -c 127.0.0.1 -p 5206 -t 1', 276 | shell=True, 277 | stdout=subprocess.PIPE, 278 | stderr=subprocess.PIPE 279 | ) 280 | response = server.run() 281 | client.kill() 282 | 283 | assert not response 284 | 285 | def test_client_succesful_run_output_to_screen(self): 286 | """Test if we print iperf3 test output to screen when json_output = False.""" 287 | client = iperf3.Client() 288 | client.server_hostname = '127.0.0.1' 289 | client.port = 5207 290 | client.duration = 1 291 | client.json_output = False 292 | 293 | server = subprocess.Popen(["iperf3", "-s", "-p", "5207"]) 294 | sleep(.3) # give the server some time to start 295 | response = client.run() 296 | server.kill() 297 | 298 | assert response == None 299 | 300 | def test_client_succesful_run_udp_output_to_screen(self): 301 | """Test if we print iperf3 test output to screen when json_output = False.""" 302 | client = iperf3.Client() 303 | client.protocol = 'udp' 304 | client.server_hostname = '127.0.0.1' 305 | client.port = 5208 306 | client.duration = 1 307 | client.json_output = False 308 | 309 | server = subprocess.Popen(["iperf3", "-s", "-p", "5208"]) 310 | sleep(.3) # give the server some time to start 311 | response = client.run() 312 | server.kill() 313 | 314 | assert response == None 315 | 316 | def test_result(self): 317 | dirname = os.path.dirname(os.path.abspath(__file__)) 318 | with open(os.path.join(dirname, 'results.json')) as f: 319 | json = f.read() 320 | 321 | result = iperf3.TestResult(json) 322 | assert result.sent_bps == 935992000 323 | assert result.sent_kbps == 935992 324 | assert result.sent_Mbps == 935.992 325 | assert isclose(result.sent_kB_s, 114256.836, rel_tol=0.01) 326 | assert isclose(result.sent_MB_s, 111.579, rel_tol=0.01) 327 | 328 | assert result.received_bps == 934268000 329 | assert result.received_kbps == 934268 330 | assert result.received_Mbps == 934.268 331 | 332 | assert isclose(result.received_kB_s, 114046.387, rel_tol=0.01) 333 | assert isclose(result.received_MB_s, 111.373, rel_tol=0.01) 334 | 335 | -------------------------------------------------------------------------------- /iperf3/iperf3.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python wrapper for the iperf3 libiperf.so.0 library. The module consists of two 4 | classes, :class:`Client` and :class:`Server`, that inherit from the base class 5 | :class:`IPerf3`. They provide a nice (if i say so myself) and pythonic way to 6 | interact with the iperf3 utility. 7 | 8 | At the moment the module redirects stdout and stderr to a pipe and returns the 9 | received data back after each ``client.run()`` or ``server.run()`` call. In 10 | later releases there will be an option to toggle this on or off. 11 | 12 | A user should never have to utilise the :class:`IPerf3` class directly, this 13 | class provides common settings for the :class:`Client` and :class:`Server` 14 | classes. 15 | 16 | To get started quickly see the :ref:`examples` page. 17 | 18 | .. moduleauthor:: Mathijs Mortimer 19 | """ 20 | 21 | from ctypes import util, cdll, c_char_p, c_int, c_char, c_void_p, c_uint64 22 | import os 23 | import select 24 | import json 25 | import threading 26 | from socket import SOCK_DGRAM, SOCK_STREAM 27 | 28 | try: 29 | from queue import Queue 30 | except ImportError: 31 | from Queue import Queue # Python2 compatibility 32 | 33 | 34 | __version__ = '0.1.11' 35 | 36 | 37 | MAX_UDP_BULKSIZE = (65535 - 8 - 20) 38 | 39 | 40 | def more_data(pipe_out): 41 | """Check if there is more data left on the pipe 42 | 43 | :param pipe_out: The os pipe_out 44 | :rtype: bool 45 | """ 46 | r, _, _ = select.select([pipe_out], [], [], 0) 47 | return bool(r) 48 | 49 | 50 | def read_pipe(pipe_out): 51 | """Read data on a pipe 52 | 53 | Used to capture stdout data produced by libiperf 54 | 55 | :param pipe_out: The os pipe_out 56 | :rtype: unicode string 57 | """ 58 | out = b'' 59 | while more_data(pipe_out): 60 | out += os.read(pipe_out, 1024) 61 | 62 | return out.decode('utf-8') 63 | 64 | 65 | def output_to_pipe(pipe_in): 66 | """Redirects stdout and stderr to a pipe 67 | 68 | :param pipe_out: The pipe to redirect stdout and stderr to 69 | """ 70 | os.dup2(pipe_in, 1) # stdout 71 | # os.dup2(pipe_in, 2) # stderr 72 | 73 | 74 | def output_to_screen(stdout_fd, stderr_fd): 75 | """Redirects stdout and stderr to a pipe 76 | 77 | :param stdout_fd: The stdout file descriptor 78 | :param stderr_fd: The stderr file descriptor 79 | """ 80 | os.dup2(stdout_fd, 1) 81 | # os.dup2(stderr_fd, 2) 82 | 83 | 84 | class IPerf3(object): 85 | """The base class used by both the iperf3 :class:`Server` and :class:`Client` 86 | 87 | .. note:: You should not use this class directly 88 | """ 89 | def __init__(self, 90 | role, 91 | verbose=True, 92 | lib_name=None): 93 | """Initialise the iperf shared library 94 | 95 | :param role: 'c' = client; 's' = server 96 | :param verbose: enable verbose output 97 | :param lib_name: optional name and path for libiperf.so.0 library 98 | """ 99 | if lib_name is None: 100 | lib_name = util.find_library('libiperf') 101 | if lib_name is None: 102 | # If we still couldn't find it lets try the manual approach 103 | lib_name = 'libiperf.so.0' 104 | 105 | try: 106 | self.lib = cdll.LoadLibrary(lib_name) 107 | except OSError: 108 | raise OSError( 109 | "Couldn't find shared library {}, is iperf3 installed?".format( 110 | lib_name 111 | ) 112 | ) 113 | 114 | # Set the appropriate C types. 115 | self.lib.iperf_client_end.restype = c_int 116 | self.lib.iperf_client_end.argtypes = (c_void_p,) 117 | self.lib.iperf_free_test.restxpe = None 118 | self.lib.iperf_free_test.argtypes = (c_void_p,) 119 | self.lib.iperf_new_test.restype = c_void_p 120 | self.lib.iperf_new_test.argtypes = None 121 | self.lib.iperf_defaults.restype = c_int 122 | self.lib.iperf_defaults.argtypes = (c_void_p,) 123 | self.lib.iperf_get_test_role.restype = c_char 124 | self.lib.iperf_get_test_role.argtypes = (c_void_p,) 125 | self.lib.iperf_set_test_role.restype = None 126 | self.lib.iperf_set_test_role.argtypes = (c_void_p, c_char,) 127 | self.lib.iperf_get_test_bind_address.restype = c_char_p 128 | self.lib.iperf_get_test_bind_address.argtypes = (c_void_p,) 129 | self.lib.iperf_set_test_bind_address.restype = None 130 | self.lib.iperf_set_test_bind_address.argtypes = (c_void_p, c_char_p,) 131 | self.lib.iperf_get_test_server_port.restype = c_int 132 | self.lib.iperf_get_test_server_port.argtypes = (c_void_p,) 133 | self.lib.iperf_set_test_server_port.restype = None 134 | self.lib.iperf_set_test_server_port.argtypes = (c_void_p, c_int,) 135 | self.lib.iperf_get_test_json_output.restype = c_int 136 | self.lib.iperf_get_test_json_output.argtypes = (c_void_p,) 137 | self.lib.iperf_set_test_json_output.restype = None 138 | self.lib.iperf_set_test_json_output.argtypes = (c_void_p, c_int,) 139 | self.lib.iperf_get_verbose.restype = c_int 140 | self.lib.iperf_get_verbose.argtypes = (c_void_p,) 141 | self.lib.iperf_set_verbose.restype = None 142 | self.lib.iperf_set_verbose.argtypes = (c_void_p, c_int) 143 | self.lib.iperf_strerror.restype = c_char_p 144 | self.lib.iperf_strerror.argtypes = (c_int,) 145 | self.lib.iperf_get_test_server_hostname.restype = c_char_p 146 | self.lib.iperf_get_test_server_hostname.argtypes = (c_void_p,) 147 | self.lib.iperf_set_test_server_hostname.restype = None 148 | self.lib.iperf_set_test_server_hostname.argtypes = ( 149 | c_void_p, c_char_p, 150 | ) 151 | self.lib.iperf_get_test_protocol_id.restype = c_int 152 | self.lib.iperf_get_test_protocol_id.argtypes = (c_void_p,) 153 | self.lib.set_protocol.restype = c_int 154 | self.lib.set_protocol.argtypes = (c_void_p, c_int,) 155 | self.lib.iperf_get_test_omit.restype = c_int 156 | self.lib.iperf_get_test_omit.argtypes = (c_void_p,) 157 | self.lib.iperf_set_test_omit.restype = None 158 | self.lib.iperf_set_test_omit.argtypes = (c_void_p, c_int,) 159 | self.lib.iperf_get_test_duration.restype = c_int 160 | self.lib.iperf_get_test_duration.argtypes = (c_void_p,) 161 | self.lib.iperf_set_test_duration.restype = None 162 | self.lib.iperf_set_test_duration.argtypes = (c_void_p, c_int,) 163 | self.lib.iperf_get_test_rate.restype = c_uint64 164 | self.lib.iperf_get_test_rate.argtypes = (c_void_p,) 165 | self.lib.iperf_set_test_rate.restype = None 166 | self.lib.iperf_set_test_rate.argtypes = (c_void_p, c_uint64,) 167 | self.lib.iperf_get_test_blksize.restype = c_int 168 | self.lib.iperf_get_test_blksize.argtypes = (c_void_p,) 169 | self.lib.iperf_set_test_blksize.restype = None 170 | self.lib.iperf_set_test_blksize.argtypes = (c_void_p, c_int,) 171 | self.lib.iperf_get_test_num_streams.restype = c_int 172 | self.lib.iperf_get_test_num_streams.argtypes = (c_void_p,) 173 | self.lib.iperf_set_test_num_streams.restype = None 174 | self.lib.iperf_set_test_num_streams.argtypes = (c_void_p, c_int,) 175 | self.lib.iperf_has_zerocopy.restype = c_int 176 | self.lib.iperf_has_zerocopy.argtypes = None 177 | self.lib.iperf_set_test_zerocopy.restype = None 178 | self.lib.iperf_set_test_zerocopy.argtypes = (c_void_p, c_int,) 179 | self.lib.iperf_get_test_reverse.restype = c_int 180 | self.lib.iperf_get_test_reverse.argtypes = (c_void_p,) 181 | self.lib.iperf_set_test_reverse.restype = None 182 | self.lib.iperf_set_test_reverse.argtypes = (c_void_p, c_int,) 183 | self.lib.iperf_run_client.restype = c_int 184 | self.lib.iperf_run_client.argtypes = (c_void_p,) 185 | self.lib.iperf_run_server.restype = c_int 186 | self.lib.iperf_run_server.argtypes = (c_void_p,) 187 | self.lib.iperf_reset_test.restype = None 188 | self.lib.iperf_reset_test.argtypes = (c_void_p,) 189 | 190 | try: 191 | # Only available from iperf v3.1 and onwards 192 | self.lib.iperf_get_test_json_output_string.restype = c_char_p 193 | self.lib.iperf_get_test_json_output_string.argtypes = (c_void_p,) 194 | except AttributeError: 195 | pass 196 | 197 | # The test C struct iperf_test 198 | self._test = self._new() 199 | self.defaults() 200 | 201 | # stdout/strerr redirection variables 202 | self._stdout_fd = os.dup(1) 203 | self._stderr_fd = os.dup(2) 204 | self._pipe_out, self._pipe_in = os.pipe() # no need for pipe write 205 | 206 | # Generic test settings 207 | self.role = role 208 | self.json_output = True 209 | self.verbose = verbose 210 | 211 | def __del__(self): 212 | """Cleanup the test after the :class:`IPerf3` class is terminated""" 213 | os.close(self._stdout_fd) 214 | os.close(self._stderr_fd) 215 | os.close(self._pipe_out) 216 | os.close(self._pipe_in) 217 | 218 | try: 219 | # In the current version of libiperf, the control socket isn't 220 | # closed on iperf_client_end(), see proposed pull request: 221 | # https://github.com/esnet/iperf/pull/597 222 | # Workaround for testing, don't ever do this..: 223 | # 224 | # sck=self.lib.iperf_get_control_socket(self._test) 225 | # os.close(sck) 226 | 227 | self.lib.iperf_client_end(self._test) 228 | self.lib.iperf_free_test(self._test) 229 | except AttributeError: 230 | # self.lib doesn't exist, likely because iperf3 wasn't installed or 231 | # the shared library libiperf.so.0 could not be found 232 | pass 233 | 234 | def _new(self): 235 | """Initialise a new iperf test 236 | 237 | struct iperf_test *iperf_new_test() 238 | """ 239 | return self.lib.iperf_new_test() 240 | 241 | def defaults(self): 242 | """Set/reset iperf test defaults.""" 243 | self.lib.iperf_defaults(self._test) 244 | 245 | @property 246 | def role(self): 247 | """The iperf3 instance role 248 | 249 | valid roles are 'c'=client and 's'=server 250 | 251 | :rtype: 'c' or 's' 252 | """ 253 | try: 254 | self._role = c_char( 255 | self.lib.iperf_get_test_role(self._test) 256 | ).value.decode('utf-8') 257 | except TypeError: 258 | self._role = c_char( 259 | chr(self.lib.iperf_get_test_role(self._test)) 260 | ).value.decode('utf-8') 261 | return self._role 262 | 263 | @role.setter 264 | def role(self, role): 265 | if role.lower() in ['c', 's']: 266 | self.lib.iperf_set_test_role( 267 | self._test, 268 | c_char(role.lower().encode('utf-8')) 269 | ) 270 | self._role = role 271 | else: 272 | raise ValueError("Unknown role, accepted values are 'c' and 's'") 273 | 274 | @property 275 | def bind_address(self): 276 | """The bind address the iperf3 instance will listen on 277 | 278 | use * to listen on all available IPs 279 | :rtype: string 280 | """ 281 | result = c_char_p( 282 | self.lib.iperf_get_test_bind_address(self._test) 283 | ).value 284 | if result: 285 | self._bind_address = result.decode('utf-8') 286 | else: 287 | self._bind_address = '*' 288 | 289 | return self._bind_address 290 | 291 | @bind_address.setter 292 | def bind_address(self, address): 293 | self.lib.iperf_set_test_bind_address( 294 | self._test, 295 | c_char_p(address.encode('utf-8')) 296 | ) 297 | self._bind_address = address 298 | 299 | @property 300 | def port(self): 301 | """The port the iperf3 server is listening on""" 302 | self._port = self.lib.iperf_get_test_server_port(self._test) 303 | return self._port 304 | 305 | @port.setter 306 | def port(self, port): 307 | self.lib.iperf_set_test_server_port(self._test, int(port)) 308 | self._port = port 309 | 310 | @property 311 | def json_output(self): 312 | """Toggles json output of libiperf 313 | 314 | Turning this off will output the iperf3 instance results to 315 | stdout/stderr 316 | 317 | :rtype: bool 318 | """ 319 | enabled = self.lib.iperf_get_test_json_output(self._test) 320 | 321 | if enabled: 322 | self._json_output = True 323 | else: 324 | self._json_output = False 325 | 326 | return self._json_output 327 | 328 | @json_output.setter 329 | def json_output(self, enabled): 330 | if enabled: 331 | self.lib.iperf_set_test_json_output(self._test, 1) 332 | else: 333 | self.lib.iperf_set_test_json_output(self._test, 0) 334 | 335 | self._json_output = enabled 336 | 337 | @property 338 | def verbose(self): 339 | """Toggles verbose output for the iperf3 instance 340 | 341 | :rtype: bool 342 | """ 343 | enabled = self.lib.iperf_get_verbose(self._test) 344 | 345 | if enabled: 346 | self._verbose = True 347 | else: 348 | self._verbose = False 349 | 350 | return self._verbose 351 | 352 | @verbose.setter 353 | def verbose(self, enabled): 354 | if enabled: 355 | self.lib.iperf_set_verbose(self._test, 1) 356 | else: 357 | self.lib.iperf_set_verbose(self._test, 0) 358 | self._verbose = enabled 359 | 360 | @property 361 | def _errno(self): 362 | """Returns the last error ID 363 | 364 | :rtype: int 365 | """ 366 | return c_int.in_dll(self.lib, "i_errno").value 367 | 368 | @property 369 | def iperf_version(self): 370 | """Returns the version of the libiperf library 371 | 372 | :rtype: string 373 | """ 374 | # TODO: Is there a better way to get the const char than allocating 30? 375 | VersionType = c_char * 30 376 | return VersionType.in_dll(self.lib, "version").value.decode('utf-8') 377 | 378 | def _error_to_string(self, error_id): 379 | """Returns an error string from libiperf 380 | 381 | :param error_id: The error_id produced by libiperf 382 | :rtype: string 383 | """ 384 | strerror = self.lib.iperf_strerror 385 | strerror.restype = c_char_p 386 | return strerror(error_id).decode('utf-8') 387 | 388 | def run(self): 389 | """Runs the iperf3 instance. 390 | 391 | This function has to be instantiated by the Client and Server 392 | instances 393 | 394 | :rtype: NotImplementedError 395 | """ 396 | raise NotImplementedError 397 | 398 | 399 | class Client(IPerf3): 400 | """An iperf3 client connection. 401 | 402 | This opens up a connection to a running iperf3 server 403 | 404 | Basic Usage:: 405 | 406 | >>> import iperf3 407 | 408 | >>> client = iperf3.Client() 409 | >>> client.duration = 1 410 | >>> client.server_hostname = '127.0.0.1' 411 | >>> client.port = 5201 412 | >>> client.run() 413 | {'intervals': [{'sum': {... 414 | """ 415 | 416 | def __init__(self, *args, **kwargs): 417 | """Initialise the iperf shared library""" 418 | super(Client, self).__init__(role='c', *args, **kwargs) 419 | 420 | # Internal variables 421 | self._blksize = None 422 | self._server_hostname = None 423 | self._port = None 424 | self._num_streams = None 425 | self._zerocopy = False 426 | self._omit = None 427 | self._duration = None 428 | self._bandwidth = None 429 | self._protocol = None 430 | 431 | @property 432 | def server_hostname(self): 433 | """The server hostname to connect to. 434 | 435 | Accepts DNS entries or IP addresses. 436 | 437 | :rtype: string 438 | """ 439 | result = c_char_p( 440 | self.lib.iperf_get_test_server_hostname(self._test) 441 | ).value 442 | if result: 443 | self._server_hostname = result.decode('utf-8') 444 | else: 445 | self._server_hostname = None 446 | return self._server_hostname 447 | 448 | @server_hostname.setter 449 | def server_hostname(self, hostname): 450 | self.lib.iperf_set_test_server_hostname( 451 | self._test, 452 | c_char_p(hostname.encode('utf-8')) 453 | ) 454 | self._server_hostname = hostname 455 | 456 | @property 457 | def protocol(self): 458 | """The iperf3 instance protocol 459 | 460 | valid protocols are 'tcp' and 'udp' 461 | 462 | :rtype: str 463 | """ 464 | proto_id = self.lib.iperf_get_test_protocol_id(self._test) 465 | 466 | if proto_id == SOCK_STREAM: 467 | self._protocol = 'tcp' 468 | elif proto_id == SOCK_DGRAM: 469 | self._protocol = 'udp' 470 | 471 | return self._protocol 472 | 473 | @protocol.setter 474 | def protocol(self, protocol): 475 | if protocol == 'tcp': 476 | self.lib.set_protocol(self._test, int(SOCK_STREAM)) 477 | elif protocol == 'udp': 478 | self.lib.set_protocol(self._test, int(SOCK_DGRAM)) 479 | 480 | if self.blksize > MAX_UDP_BULKSIZE: 481 | self.blksize = MAX_UDP_BULKSIZE 482 | 483 | self._protocol = protocol 484 | 485 | @property 486 | def omit(self): 487 | """The test startup duration to omit in seconds.""" 488 | self._omit = self.lib.iperf_get_test_omit(self._test) 489 | return self._omit 490 | 491 | @omit.setter 492 | def omit(self, omit): 493 | self.lib.iperf_set_test_omit(self._test, omit) 494 | self._omit = omit 495 | 496 | @property 497 | def duration(self): 498 | """The test duration in seconds.""" 499 | self._duration = self.lib.iperf_get_test_duration(self._test) 500 | return self._duration 501 | 502 | @duration.setter 503 | def duration(self, duration): 504 | self.lib.iperf_set_test_duration(self._test, duration) 505 | self._duration = duration 506 | 507 | @property 508 | def bandwidth(self): 509 | """Target bandwidth in bits/sec""" 510 | self._bandwidth = self.lib.iperf_get_test_rate(self._test) 511 | return self._bandwidth 512 | 513 | @bandwidth.setter 514 | def bandwidth(self, bandwidth): 515 | self.lib.iperf_set_test_rate(self._test, bandwidth) 516 | self._bandwidth = bandwidth 517 | 518 | @property 519 | def blksize(self): 520 | """The test blksize.""" 521 | self._blksize = self.lib.iperf_get_test_blksize(self._test) 522 | return self._blksize 523 | 524 | @blksize.setter 525 | def blksize(self, bulksize): 526 | # iperf version < 3.1.3 has some weird bugs when bulksize is 527 | # larger than MAX_UDP_BULKSIZE 528 | if self.protocol == 'udp' and bulksize > MAX_UDP_BULKSIZE: 529 | bulksize = MAX_UDP_BULKSIZE 530 | 531 | self.lib.iperf_set_test_blksize(self._test, bulksize) 532 | self._blksize = bulksize 533 | 534 | @property 535 | def bulksize(self): 536 | """The test bulksize. 537 | 538 | Deprecated argument, use blksize instead to ensure consistency 539 | with iperf3 C libary 540 | """ 541 | # Keeping bulksize argument for backwards compatibility with 542 | # iperf3-python < 0.1.7 543 | return self.blksize 544 | 545 | @bulksize.setter 546 | def bulksize(self, bulksize): 547 | # Keeping bulksize argument for backwards compatibility with 548 | # iperf3-python < 0.1.7 549 | self.blksize = bulksize 550 | 551 | @property 552 | def num_streams(self): 553 | """The number of streams to use.""" 554 | self._num_streams = self.lib.iperf_get_test_num_streams(self._test) 555 | return self._num_streams 556 | 557 | @num_streams.setter 558 | def num_streams(self, number): 559 | self.lib.iperf_set_test_num_streams(self._test, number) 560 | self._num_streams = number 561 | 562 | @property 563 | def zerocopy(self): 564 | """Toggle zerocopy. 565 | 566 | Use the sendfile() system call for "Zero Copy" mode. This uses much 567 | less CPU. This is not supported on all systems. 568 | 569 | **Note** there isn't a hook in the libiperf library for getting the 570 | current configured value. Relying on zerocopy.setter function 571 | 572 | :rtype: bool 573 | """ 574 | return self._zerocopy 575 | 576 | @zerocopy.setter 577 | def zerocopy(self, enabled): 578 | if enabled and self.lib.iperf_has_zerocopy(): 579 | self.lib.iperf_set_test_zerocopy(self._test, 1) 580 | self._zerocopy = True 581 | else: 582 | self.lib.iperf_set_test_zerocopy(self._test, 0) 583 | self._zerocopy = False 584 | 585 | @property 586 | def reverse(self): 587 | """Toggles direction of test 588 | 589 | :rtype: bool 590 | """ 591 | enabled = self.lib.iperf_get_test_reverse(self._test) 592 | 593 | if enabled: 594 | self._reverse = True 595 | else: 596 | self._reverse = False 597 | 598 | return self._reverse 599 | 600 | @reverse.setter 601 | def reverse(self, enabled): 602 | if enabled: 603 | self.lib.iperf_set_test_reverse(self._test, 1) 604 | else: 605 | self.lib.iperf_set_test_reverse(self._test, 0) 606 | 607 | self._reverse = enabled 608 | 609 | def run(self): 610 | """Run the current test client. 611 | 612 | :rtype: instance of :class:`TestResult` 613 | """ 614 | if self.json_output: 615 | output_to_pipe(self._pipe_in) # Disable stdout 616 | error = self.lib.iperf_run_client(self._test) 617 | 618 | if not self.iperf_version.startswith('iperf 3.1'): 619 | data = read_pipe(self._pipe_out) 620 | if data.startswith('Control connection'): 621 | data = '{' + data.split('{', 1)[1] 622 | else: 623 | data = c_char_p( 624 | self.lib.iperf_get_test_json_output_string(self._test) 625 | ).value 626 | if data: 627 | data = data.decode('utf-8') 628 | 629 | output_to_screen(self._stdout_fd, self._stderr_fd) # enable stdout 630 | 631 | if not data or error: 632 | data = '{"error": "%s"}' % self._error_to_string(self._errno) 633 | 634 | return TestResult(data) 635 | 636 | 637 | class Server(IPerf3): 638 | """An iperf3 server connection. 639 | 640 | This starts an iperf3 server session. The server terminates after each 641 | succesful client connection so it might be useful to run Server.run() 642 | in a loop. 643 | 644 | The C function iperf_run_server is called in a seperate thread to make 645 | sure KeyboardInterrupt(aka ctrl+c) can still be captured 646 | 647 | Basic Usage:: 648 | 649 | >>> import iperf3 650 | 651 | >>> server = iperf3.Server() 652 | >>> server.run() 653 | {'start': {... 654 | """ 655 | 656 | def __init__(self, *args, **kwargs): 657 | """Initialise the iperf3 server instance""" 658 | super(Server, self).__init__(role='s', *args, **kwargs) 659 | 660 | def run(self): 661 | """Run the iperf3 server instance. 662 | 663 | :rtype: instance of :class:`TestResult` 664 | """ 665 | 666 | def _run_in_thread(self, data_queue): 667 | """Runs the iperf_run_server 668 | 669 | :param data_queue: thread-safe queue 670 | """ 671 | output_to_pipe(self._pipe_in) # disable stdout 672 | error = self.lib.iperf_run_server(self._test) 673 | output_to_screen(self._stdout_fd, self._stderr_fd) # enable stdout 674 | 675 | # TODO json_output_string not available on earlier iperf3 builds 676 | # have to build in a version check using self.iperf_version 677 | # The following line should work on later versions: 678 | # data = c_char_p( 679 | # self.lib.iperf_get_test_json_output_string(self._test) 680 | # ).value 681 | data = read_pipe(self._pipe_out) 682 | 683 | if not data or error: 684 | data = '{"error": "%s"}' % self._error_to_string(self._errno) 685 | 686 | self.lib.iperf_reset_test(self._test) 687 | data_queue.put(data) 688 | 689 | if self.json_output: 690 | data_queue = Queue() 691 | 692 | t = threading.Thread( 693 | target=_run_in_thread, args=[self, data_queue] 694 | ) 695 | t.daemon = True 696 | 697 | t.start() 698 | while t.is_alive(): 699 | t.join(.1) 700 | 701 | return TestResult(data_queue.get()) 702 | else: 703 | # setting json_output to False will output test to screen only 704 | self.lib.iperf_run_server(self._test) 705 | self.lib.iperf_reset_test(self._test) 706 | 707 | return None 708 | 709 | 710 | class TestResult(object): 711 | """Class containing iperf3 test results. 712 | 713 | :param text: The raw result from libiperf as text 714 | :param json: The raw result from libiperf asjson/dict 715 | :param error: Error captured during test, None if all ok 716 | 717 | :param time: Start time 718 | :param timesecs: Start time in seconds 719 | 720 | :param system_info: System info 721 | :param version: Iperf Version 722 | 723 | :param local_host: Local host ip 724 | :param local_port: Local port number 725 | :param remote_host: Remote host ip 726 | :param remote_port: Remote port number 727 | 728 | :param reverse: Test ran in reverse direction 729 | :param protocol: 'TCP' or 'UDP' 730 | :param num_streams: Number of test streams 731 | :param blksize: 732 | :param omit: Test duration to omit in the beginning in seconds 733 | :param duration: Test duration (following omit duration) in seconds 734 | 735 | :param local_cpu_total: The local total CPU load 736 | :param local_cpu_user: The local user CPU load 737 | :param local_cpu_system: The local system CPU load 738 | :param remote_cpu_total: The remote total CPU load 739 | :param remote_cpu_user: The remote user CPU load 740 | :param remote_cpu_system: The remote system CPU load 741 | 742 | 743 | TCP test specific 744 | 745 | :param tcp_mss_default: 746 | :param retransmits: amount of retransmits (Only returned from client) 747 | 748 | :param sent_bytes: Sent bytes 749 | :param sent_bps: Sent bits per second 750 | :param sent_kbps: sent kilobits per second 751 | :param sent_Mbps: Sent Megabits per second 752 | :param sent_kB_s: Sent kiloBytes per second 753 | :param sent_MB_s: Sent MegaBytes per second 754 | 755 | :param received_bytes: Received bytes 756 | :param received_bps: Received bits per second 757 | :param received_kbps: Received kilobits per second 758 | :param received_Mbps: Received Megabits per second 759 | :param received_kB_s: Received kiloBytes per second 760 | :param received_MB_s: Received MegaBytes per second 761 | 762 | 763 | UDP test specific 764 | 765 | :param bytes: 766 | :param bps: 767 | :param jitter_ms: 768 | :param kbps: 769 | :param Mbps: 770 | :param kB_s: 771 | :param MB_s: 772 | :param packets: 773 | :param lost_packets: 774 | :param lost_percent: 775 | :param seconds: 776 | """ 777 | 778 | def __init__(self, result): 779 | """Initialise TestResult 780 | 781 | :param result: raw json output from :class:`Client` and :class:`Server` 782 | """ 783 | # The full result data 784 | self.text = result 785 | self.json = json.loads(result) 786 | 787 | if 'error' in self.json: 788 | self.error = self.json['error'] 789 | else: 790 | self.error = None 791 | 792 | # start time 793 | self.time = self.json['start']['timestamp']['time'] 794 | self.timesecs = self.json['start']['timestamp']['timesecs'] 795 | 796 | # generic info 797 | self.system_info = self.json['start']['system_info'] 798 | self.version = self.json['start']['version'] 799 | 800 | # connection details 801 | connection_details = self.json['start']['connected'][0] 802 | self.local_host = connection_details['local_host'] 803 | self.local_port = connection_details['local_port'] 804 | self.remote_host = connection_details['remote_host'] 805 | self.remote_port = connection_details['remote_port'] 806 | 807 | # test setup 808 | self.tcp_mss_default = self.json['start'].get('tcp_mss_default') 809 | self.protocol = self.json['start']['test_start']['protocol'] 810 | self.num_streams = self.json['start']['test_start']['num_streams'] 811 | self.blksize = self.json['start']['test_start']['blksize'] 812 | self.omit = self.json['start']['test_start']['omit'] 813 | self.duration = self.json['start']['test_start']['duration'] 814 | 815 | # system performance 816 | cpu_utilization_perc = self.json['end']['cpu_utilization_percent'] 817 | self.local_cpu_total = cpu_utilization_perc['host_total'] 818 | self.local_cpu_user = cpu_utilization_perc['host_user'] 819 | self.local_cpu_system = cpu_utilization_perc['host_system'] 820 | self.remote_cpu_total = cpu_utilization_perc['remote_total'] 821 | self.remote_cpu_user = cpu_utilization_perc['remote_user'] 822 | self.remote_cpu_system = cpu_utilization_perc['remote_system'] 823 | 824 | # TCP specific test results 825 | if self.protocol == 'TCP': 826 | sent_json = self.json['end']['sum_sent'] 827 | self.sent_bytes = sent_json['bytes'] 828 | self.sent_bps = sent_json['bits_per_second'] 829 | 830 | recv_json = self.json['end']['sum_received'] 831 | self.received_bytes = recv_json['bytes'] 832 | self.received_bps = recv_json['bits_per_second'] 833 | 834 | # Bits are measured in 10**3 terms 835 | # Bytes are measured in 2**10 terms 836 | # kbps = Kilobits per second 837 | # Mbps = Megabits per second 838 | # kB_s = kiloBytes per second 839 | # MB_s = MegaBytes per second 840 | 841 | self.sent_kbps = self.sent_bps / 1000 842 | self.sent_Mbps = self.sent_kbps / 1000 843 | self.sent_kB_s = self.sent_bps / (8 * 1024) 844 | self.sent_MB_s = self.sent_kB_s / 1024 845 | 846 | self.received_kbps = self.received_bps / 1000 847 | self.received_Mbps = self.received_kbps / 1000 848 | self.received_kB_s = self.received_bps / (8 * 1024) 849 | self.received_MB_s = self.received_kB_s / 1024 850 | 851 | # retransmits only returned from client 852 | self.retransmits = sent_json.get('retransmits') 853 | 854 | # UDP specific test results 855 | elif self.protocol == 'UDP': 856 | self.bytes = self.json['end']['sum']['bytes'] 857 | self.bps = self.json['end']['sum']['bits_per_second'] 858 | self.jitter_ms = self.json['end']['sum']['jitter_ms'] 859 | self.kbps = self.bps / 1000 860 | self.Mbps = self.kbps / 1000 861 | self.kB_s = self.bps / (8 * 1024) 862 | self.MB_s = self.kB_s / 1024 863 | self.packets = self.json['end']['sum']['packets'] 864 | self.lost_packets = self.json['end']['sum']['lost_packets'] 865 | self.lost_percent = self.json['end']['sum']['lost_percent'] 866 | self.seconds = self.json['end']['sum']['seconds'] 867 | 868 | @property 869 | def reverse(self): 870 | if self.json['start']['test_start']['reverse']: 871 | return True 872 | else: 873 | return False 874 | 875 | @property 876 | def type(self): 877 | if 'connecting_to' in self.json['start']: 878 | return 'client' 879 | else: 880 | return 'server' 881 | 882 | def __repr__(self): 883 | """Print the result as received from iperf3""" 884 | return self.text 885 | --------------------------------------------------------------------------------