diff --git a/pscheduler-test-throughput/throughput/cli-to-spec b/pscheduler-test-throughput/throughput/cli-to-spec index 44bfe40566..c9041be768 100755 --- a/pscheduler-test-throughput/throughput/cli-to-spec +++ b/pscheduler-test-throughput/throughput/cli-to-spec @@ -2,7 +2,6 @@ # # Convert comamnd-line options to a test specification -import optparse import pscheduler import sys import datetime @@ -205,6 +204,11 @@ opt_parser.add_option("--loopback", action="store_true", dest="loopback") +opt_parser.add_option('--qperf-test', + help='Qperf test to run.', + action='store', type='string', + dest='qperf_test') + (options, remaining_args) = opt_parser.parse_args(args) if len(remaining_args) != 0: @@ -365,6 +369,10 @@ if options.reverse_connections: if options.loopback: result["loopback"] = True schema.set(5) + +if options.qperf_test: + result['qperf-test'] = options.qperf_test + schema.set(7) result["schema"] = schema.value() diff --git a/pscheduler-test-throughput/throughput/validate.py b/pscheduler-test-throughput/throughput/validate.py index f75b1824f1..926b1f5b25 100644 --- a/pscheduler-test-throughput/throughput/validate.py +++ b/pscheduler-test-throughput/throughput/validate.py @@ -3,7 +3,6 @@ # from pscheduler import json_validate -import json MAX_SCHEMA = 7 @@ -288,7 +287,8 @@ "single-ended-port": { "$ref": "#/pScheduler/Integer" }, "reverse": { "$ref": "#/pScheduler/Boolean" }, "reverse-connections": { "$ref": "#/pScheduler/Boolean" }, - "loopback": { "$ref": "#/pScheduler/Boolean" } + "loopback": { "$ref": "#/pScheduler/Boolean" }, + "qperf-test": { "type": "string", "enum": [ "rds_bw", "sctp_bw", "sdp_bw", "tcp_bw", "udp_bw", "rc_bi_bw", "rc_bw", "uc_bi_bw", "uc_bw", "ud_bi_bw", "ud_bw", "rc_rdma_read_bw", "rc_rdma_write_bw", "uc_rdma_write_bw" ] } }, "additionalProperties": False, "required": [ @@ -296,7 +296,6 @@ "dest" ] } - } } diff --git a/pscheduler-tool-qperf/.gitattributes b/pscheduler-tool-qperf/.gitattributes new file mode 100644 index 0000000000..d4a8ae2751 --- /dev/null +++ b/pscheduler-tool-qperf/.gitattributes @@ -0,0 +1,2 @@ +# Ignore those files when creating a tarball with `git archive` +debian/ export-ignore diff --git a/pscheduler-tool-qperf/Makefile b/pscheduler-tool-qperf/Makefile new file mode 100644 index 0000000000..413b497973 --- /dev/null +++ b/pscheduler-tool-qperf/Makefile @@ -0,0 +1,7 @@ +# +# Makefile for Any Package +# + +AUTO_TARBALL := 1 + +include unibuild/unibuild.make diff --git a/pscheduler-tool-qperf/qperf/LICENSE b/pscheduler-tool-qperf/qperf/LICENSE new file mode 100644 index 0000000000..67db858821 --- /dev/null +++ b/pscheduler-tool-qperf/qperf/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/pscheduler-tool-qperf/qperf/Makefile b/pscheduler-tool-qperf/qperf/Makefile new file mode 100644 index 0000000000..579d8d2156 --- /dev/null +++ b/pscheduler-tool-qperf/qperf/Makefile @@ -0,0 +1,57 @@ +# +# Makefile for any tool class +# + +NAME=qperf + +FILES=\ + can-run \ + duration \ + enumerate \ + merged-results \ + participant-data \ + run \ + +MODULES=\ + qperf_parser \ + qperf_defaults \ + qperf_utils \ + +PYS=$(MODULES:%=%.py) +PYCS=$(MODULES:%=__pycache__/%.pyc) + +$(PYCS): +ifndef DESTDIR + @echo No PYTHON specified for build + @false +endif + $(PYTHON) -m compileall . +TO_CLEAN += $(PYCS) + + +CONFS=\ + $(NAME).conf + +install: $(FILES) $(CONFS) $(PYS) $(PYCS) +ifndef DESTDIR + @echo No DESTDIR specified for installation + @false +endif +ifdef CONFS +ifndef CONFDIR + @echo No CONFDIR specified for installation + @false +endif +endif + mkdir -p $(DESTDIR) + install -m 555 $(FILES) $(DESTDIR) + install -m 444 $(PYS) $(DESTDIR) + cp -r __pycache__ $(DESTDIR) +ifdef CONFS + mkdir -p $(CONFDIR) + install -m 644 $(CONFS) $(CONFDIR) +endif + + +clean: + rm -f $(TO_CLEAN) *~ diff --git a/pscheduler-tool-qperf/qperf/can-run b/pscheduler-tool-qperf/qperf/can-run new file mode 100755 index 0000000000..a76c4a8be3 --- /dev/null +++ b/pscheduler-tool-qperf/qperf/can-run @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +# +# Determine if this tool can run a test based on a test spec. +# + +import pscheduler + +logger = pscheduler.Log(prefix='tool-qperf', quiet=True) + +json = pscheduler.json_load(exit_on_error=True) + +logger.debug(f'can-run for {json}') + +try: + if json['type'] not in ['throughput']: + pscheduler.succeed_json({ + 'can-run': False, + 'reasons': [ 'Unsupported test type' ] + }) +except KeyError: + pscheduler.succeed_json({ + 'can-run': False, + 'reasons': [ 'Missing test type' ] + }) + + +try: + spec = json['spec'] + pscheduler.json_check_schema(spec, 7) +except KeyError: + pscheduler.succeed_json({ + 'can-run': False, + 'reasons': [ 'Missing test specification' ] + }) +except ValueError as ex: + pscheduler.succeed_json({ + 'can-run': False, + 'reasons': [ str(ex) ] + }) + + +throughput_tests = [ + 'rds_bw', # RDS streaming one way bandwidth + 'sctp_bw', # SCTP streaming one way bandwidth + 'sdp_bw', # SDP streaming one way bandwidth + 'tcp_bw', # TCP streaming one way bandwidth + 'udp_bw', # UDP streaming one way bandwidth + 'rc_bi_bw', # RC streaming two way bandwidth + 'rc_bw', # RC streaming one way bandwidth + 'uc_bi_bw', # UC streaming two way bandwidth + 'uc_bw', # UC streaming one way bandwidth + 'ud_bi_bw', # UD streaming two way bandwidth + 'ud_bw', # UD streaming one way bandwidth + 'rc_rdma_read_bw', # RC RDMA read streaming one way bandwidth + 'rc_rdma_write_bw', # RC RDMA write streaming one way bandwidth + 'uc_rdma_write_bw' # UC RDMA write streaming one way bandwidth +] + +try: + if spec['qperf-test'].lower() not in throughput_tests: + pscheduler.succeed_json({ + 'can-run': False, + 'reasons': [ 'Unsupported qperf test type' ] + }) +except KeyError: + pscheduler.succeed_json({ + 'can-run': False, + 'reasons': [ 'Missing qperf test type' ] + }) +except ValueError as ex: + pscheduler.succeed_json({ + 'can-run': False, + 'reasons': [ str(ex) ] + }) + +errors = [] + +throughput_options = [ + 'schema', + 'source', + 'source-node', + 'dest', + 'dest-node', + 'duration', + 'link-rtt', + 'client-cpu-affinity', + 'server-cpu-affinity', + 'qperf-test', +] + +supported_options = throughput_options + +for option in spec: + if option not in supported_options: + logger.debug(f'qperf unsupported option {option}') + errors.append(f'qperf unsupported option {option}') + +logger.debug('can-run succeeded') + +result = { + 'can-run': len(errors) == 0 +} + +if len(errors) > 0: + result['reasons'] = errors + +pscheduler.succeed_json(result) diff --git a/pscheduler-tool-qperf/qperf/duration b/pscheduler-tool-qperf/qperf/duration new file mode 100755 index 0000000000..9d77f31150 --- /dev/null +++ b/pscheduler-tool-qperf/qperf/duration @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# +# Determine the duration of the qperf + +import pscheduler + +import qperf_defaults +import qperf_utils + +logger = pscheduler.Log(prefix='tool-qperf', quiet=True) + +json = pscheduler.json_load(exit_on_error=True)['spec'] + + +# Setup: How long it takes to connect and set up the test +setup = qperf_utils.setup_time(json.get('link-rtt')) + + +# Duration: How long the test should run + +duration = json.get('duration', qperf_defaults.DEFAULT_DURATION) +delta = pscheduler.iso8601_as_timedelta(duration) +duration = int(pscheduler.timedelta_as_seconds(delta)) + + + +full_duration = setup + duration + qperf_defaults.DEFAULT_WAIT_SLEEP + qperf_defaults.DEFAULT_SERVER_SHUTDOWN +logger.debug(f'final duration = {full_duration}s') + + +pscheduler.succeed_json({ + 'duration': f'PT{full_duration}S' +}) diff --git a/pscheduler-tool-qperf/qperf/enumerate b/pscheduler-tool-qperf/qperf/enumerate new file mode 100755 index 0000000000..2ed2d86180 --- /dev/null +++ b/pscheduler-tool-qperf/qperf/enumerate @@ -0,0 +1,17 @@ +#!/bin/sed -e 1d +{ + "schema": 1, + + "name": "qperf", + "description": "Measure network throughput with qperf", + "version": "1.0", + "tests": [ "throughput" ], + + "preference": 0, + + "maintainer": { + "name": "perfSONAR Development Team", + "email": "perfsonar-developer@internet2.edu", + "href": "http://www.perfsonar.net" + } +} diff --git a/pscheduler-tool-qperf/qperf/merged-results b/pscheduler-tool-qperf/qperf/merged-results new file mode 100755 index 0000000000..4c097416d2 --- /dev/null +++ b/pscheduler-tool-qperf/qperf/merged-results @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +import pscheduler + +logger = pscheduler.Log(quiet=True) + +input = pscheduler.json_load(exit_on_error=True) + +try: + result_list = input['results'] +except KeyError as ex: + logger.error(f'merged-result error {ex}') + pscheduler.fail(f'Missing required key in merged-result input: {ex}') + +result_len = len(result_list) +if not(result_len == 2): + pscheduler.fail(f'Expected 2 results in merged-results, got {result_len}') + +source_results = result_list[0] +dest_results = result_list[1] +final_results = source_results + + +# it's possible one could come back as null, ensure we have the right type +if not final_results: + final_results = {'succeeded': False} + +final_diag = '' + +if source_results and source_results.get('diags'): + final_diag += f'Participant 0:\n{source_results.get("diags")}\n' + +if dest_results and dest_results.get('diags'): + final_diag += f'Participant 1:\n{dest_results.get("diags")}\n' + +final_results['diags'] = final_diag + +pscheduler.succeed_json(final_results) diff --git a/pscheduler-tool-qperf/qperf/participant-data b/pscheduler-tool-qperf/qperf/participant-data new file mode 100755 index 0000000000..91c1213a58 --- /dev/null +++ b/pscheduler-tool-qperf/qperf/participant-data @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# +# Return participant-specific data for a run +# + +import packaging.version +import pscheduler +import qperf_utils + +json = pscheduler.json_load(exit_on_error=True) +result = { + 'schema': 3 +} + + +# Provide the local qperf version + +status, out, err = pscheduler.run_program(['qperf', '--version'], timeout=2) +if status != 0: + pscheduler.fail(f'Unable to run qperf: {err}') + +try: + version_text = out.split()[1] + version = packaging.version.Version(version_text) +except (IndexError, packaging.version.InvalidVersion) as ex: + pscheduler.fail(str(ex)) + +result['qperf-version'] = str(version) + + +try: + participant = json['participant'] +except KeyError: + pscheduler.fail('Missing participant') + +config = qperf_utils.get_config() + +if participant in [0, 1]: + result['port'] = config['port'] + result['data_port'] = config['data_port'] +else: + pscheduler.fail('Invalid participant number for this test') + +pscheduler.succeed_json(result) diff --git a/pscheduler-tool-qperf/qperf/qperf.conf b/pscheduler-tool-qperf/qperf/qperf.conf new file mode 100644 index 0000000000..2115edb906 --- /dev/null +++ b/pscheduler-tool-qperf/qperf/qperf.conf @@ -0,0 +1,33 @@ +### +# Configuration file for pScheduler qperf tool throughput tests +# +[qperf] +## +# Path to the qperf command. Default is /usr/bin/qperf +# +# qperf_cmd = /usr/bin/qperf + +# The port where qperf server will listen when this host is the receiving endpoint. Default is 20000 +# +# port = 20000 + +# The port where qperf server will listen when this host is the sending endpoint. Default is 20001 +# +# data_port = 20001 + +# The test duration. Default is 10 seconds +# +# duration = 'PT10S' + +# The link RTT in seconds. This is a rough approximation of RTT +# for the longest possible terrestrial path. Default is 0.2 seconds +# +# link_rtt = 'PT0.200S' + +# The number of seconds before client will start to allow server time to boot. Default is 5 +# +# wait_sleep = 5 + +# The number of seconds to let the server spin down. Default is 2 +# +# server_shutdown = 2 diff --git a/pscheduler-tool-qperf/qperf/qperf_defaults.py b/pscheduler-tool-qperf/qperf/qperf_defaults.py new file mode 100644 index 0000000000..c10f45a378 --- /dev/null +++ b/pscheduler-tool-qperf/qperf/qperf_defaults.py @@ -0,0 +1,25 @@ +########################################################## +# Contains variables shared across qperf tool components +########################################################## + +# Default test time +DEFAULT_DURATION = 'PT10S' + +# Default link RTT. +DEFAULT_LINK_RTT = 'PT0.200S' + +# Default number of seconds before client will start to allow server time to boot +DEFAULT_WAIT_SLEEP = 5 + +# Default number of seconds to let the server spin down +DEFAULT_SERVER_SHUTDOWN = 2 + +# The default qperf control port +DEFAULT_PORT = 20000 +DEFAULT_DATA_PORT = 20001 + +# The default location of the config file +CONFIG_FILE = '/etc/pscheduler/tool/qperf.conf' + +# Default install location of qperf +DEFAULT_QPERF_PATH = '/usr/bin/qperf' diff --git a/pscheduler-tool-qperf/qperf/qperf_parser.py b/pscheduler-tool-qperf/qperf/qperf_parser.py new file mode 100644 index 0000000000..70c88184d4 --- /dev/null +++ b/pscheduler-tool-qperf/qperf/qperf_parser.py @@ -0,0 +1,116 @@ +import re +import pscheduler + +logger = pscheduler.Log(quiet=True) + +def to_bits(value_str): + unit_map = { + 'Kb': 1e3, + 'Mb': 1e6, + 'Gb': 1e9, + 'Tb': 1e12, + 'KB': 8e3, + 'MB': 8e6, + 'GB': 8e9, + 'TB': 8e12, + 'B': 8, + } + + pattern = re.compile(r'([\d.]+)\s*(K|M|G|T)?(b|B)(/sec)?') + match = pattern.search(value_str.strip()) + + if not match: + raise ValueError(f'Cannot parse bits from "{value_str}"') + + value, prefix, btype, _ = match.groups() + unit = (prefix or '') + btype + + if unit not in unit_map: + raise ValueError(f'Unknown unit "{unit}" in "{value_str}"') + + bits = int(float(value) * unit_map[unit]) + + return bits + + +def to_count(value_str): + multipliers = { + 'thousand': 1e3, + 'million': 1e6, + 'billion': 1e9, + 'trillion': 1e12 + } + + value_str = value_str.strip().replace(',', '') + + pattern = re.compile(r'([\d.]+)\s*(thousand|million|billion|trillion)?', re.IGNORECASE) + match = pattern.search(value_str) + + if not match: + raise ValueError(f'Cannot parse count from "{value_str}"') + + value, word = match.groups() + multiplier = multipliers.get(word.lower(), 1) if word else 1 + return int(float(value) * multiplier) + + +def translate_line(key, value): + if key == 'bw': + return { + 'throughput-bits': to_bits(value), + 'throughput-bytes': to_bits(value) / 8, + 'receiver-throughput-bits': to_bits(value) + } + if key == 'send_bw': + return { + 'throughput-bits': to_bits(value), + 'throughput-bytes': to_bits(value) / 8, + } + if key == 'recv_bw': + return { + 'receiver-throughput-bits': to_bits(value) + } + if key == 'loc_send_msgs': + return { + 'sent': to_count(value) + } + if key == 'rem_recv_msgs': + return { + 'received': to_count(value) + } + return {} + + +def parse_output(lines, duration=0): + results = { + 'succeeded': True, + } + summary = { + 'omitted': False, + 'start': 0, + 'end': duration, + 'stream-id': 0, + } + for line in lines: + # some tests send fail messages to stdout, not stderr + if 'failed' in line: + return { + 'succeeded': False, + 'error': f'qperf test failed: {line}' + } + if '=' in line: + key, value = line.split('=', 1) + summary.update(translate_line(key.strip(), value.strip())) + if summary.get('sent') and summary.get('received'): + summary['lost'] = summary['sent'] - summary['received'] + _ = summary.pop('received', None) + + results['summary'] = { + 'summary': summary, + 'streams': [summary] + } + results['intervals'] = [{ + 'summary': summary, + 'streams': [summary] + }] + return results diff --git a/pscheduler-tool-qperf/qperf/qperf_utils.py b/pscheduler-tool-qperf/qperf/qperf_utils.py new file mode 100644 index 0000000000..0289569949 --- /dev/null +++ b/pscheduler-tool-qperf/qperf/qperf_utils.py @@ -0,0 +1,60 @@ +### +# utility functions for the qperf tool +# + +import pscheduler +import configparser +import qperf_defaults + +logger = pscheduler.Log(quiet=True) + +## +# Read and return config file (or nothing if unable to) +def get_config(): + obj = { + 'port': qperf_defaults.DEFAULT_PORT, + 'data_port': qperf_defaults.DEFAULT_DATA_PORT, + 'qperf_cmd': qperf_defaults.DEFAULT_QPERF_PATH, + 'duration': qperf_defaults.DEFAULT_DURATION, + 'link_rtt': qperf_defaults.DEFAULT_LINK_RTT, + 'wait_sleep': qperf_defaults.DEFAULT_WAIT_SLEEP, + 'server_shutdown': qperf_defaults.DEFAULT_SERVER_SHUTDOWN, + } + + try: + config = configparser.ConfigParser() + config.read(qperf_defaults.CONFIG_FILE) + except: + logger.warning(f'Unable to read configuration file {qperf_defaults.CONFIG_FILE}. Proceeding with defaults.') + return obj + + options = [ + 'qperf_cmd', + 'port', + 'data_port', + 'duration', + 'link_rtt', + 'wait_sleep', + 'server_shutdown' + ] + + for option in options: + if config.has_option('qperf', option): + obj[option] = config.get('qperf', option) + + return obj + + + +def setup_time(rtt): + delta = pscheduler.iso8601_as_timedelta(rtt or qperf_defaults.DEFAULT_LINK_RTT) + rtt = pscheduler.timedelta_as_seconds(delta) + + return max(((rtt * 1000.0) / 50.0), 1.0) + + + +if __name__ == "__main__": + + config = get_config() + print(config) diff --git a/pscheduler-tool-qperf/qperf/run b/pscheduler-tool-qperf/qperf/run new file mode 100755 index 0000000000..51f95b34c5 --- /dev/null +++ b/pscheduler-tool-qperf/qperf/run @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +# +# Run an qperf test +# + +import datetime +import pscheduler +import sys +import qperf_utils +import time +import qperf_parser + +# track when this run starts +start_time = datetime.datetime.now() + +logger = pscheduler.Log(prefix='tool-qperf', quiet=True) +logger.debug('starting qperf tool') + +# parse JSON input +input = pscheduler.json_load(exit_on_error=True) + +logger.debug(f'Input is {input}') + +try: + participant = input['participant'] + participant_data = input['participant-data'] + test_spec = input['test']['spec'] +except KeyError as ex: + pscheduler.fail(f'Missing required key in run input: {ex}') +except Exception: + pscheduler.fail(f'Error parsing run input: {sys.exc_info()[0]}') + +participants = len(participant_data) +if not(participants == 2): + pscheduler.fail(f'qperf requires exactly 2 participants, got {len(participant_data)}') + +config = qperf_utils.get_config() + +# look up our local qperf command path +qperf_cmd = config['qperf_cmd'] + + +# convert from ISO to seconds for test duration +test_duration = test_spec.get('duration', config['duration']) +delta = pscheduler.iso8601_as_timedelta(test_duration) +test_duration = int(pscheduler.timedelta_as_seconds(delta)) + + +qperf_first_args = [ qperf_cmd ] + +def run_client(): + diags = [] + global qperf_first_args + qperf_args = qperf_first_args.copy() + + # destination + dest = test_spec['dest'] + qperf_args.append('--host') + qperf_args.append(dest) + + #duration + qperf_args.append('--time') + qperf_args.append(test_duration) + + # cpu affinity + client_cpu_affinity = test_spec.get('client-cpu-affinity', None) + if client_cpu_affinity is not None: + qperf_args.append('--loc_cpu_affinity') + qperf_args.append(client_cpu_affinity) + server_cpu_affinity = test_spec.get('server-cpu-affinity', None) + if server_cpu_affinity is not None: + qperf_args.append('--rem_cpu_affinity') + qperf_args.append(server_cpu_affinity) + + # set server port. Must be same as server port + qperf_args.append('--listen_port') + qperf_args.append(config['port']) + + qperf_test = test_spec['qperf-test'].lower() + + # set data port + if qperf_test in ['tcp_bw', 'udp_bw', 'sdp_bw', 'rds_bw', 'sctp_bw']: + qperf_args.append('--ip_port') + qperf_args.append(config['data_port']) + + # options to unify output for test types + qperf_args.append('--unify_nodes') # data from server as rem, from client as loc + + # we want all the data + qperf_args.append('-vv') + + # set test type + qperf_args.append(qperf_test) + + # tell the server to stop after the test completes + qperf_args.append('quit') + + + # join and run_program want these all to be string types, so + # just to be safe cast everything in the list to a string + qperf_args = [str(x) for x in qperf_args] + + command_line = ' '.join(qperf_args) + logger.debug(f'Client: Running command: {command_line}') + + diags.append(command_line) + + try: + start_at = input['schedule']['start'] + logger.debug(f'Client: Sleeping until {start_at}') + pscheduler.sleep_until(start_at) + logger.debug('Client: Starting') + except KeyError: + pscheduler.fail('Unable to find start time in input') + + logger.debug(f'Client: Waiting {config["wait_sleep"]} sec for server on other side to start') + time.sleep(config['wait_sleep']) # wait for server to start on other side + + qperf_timeout = test_duration + qperf_timeout += qperf_utils.setup_time(test_spec.get('link-rtt')) + logger.debug(f'Client: timeout for client is {qperf_timeout}') + + try: + status, stdout, stderr = pscheduler.run_program(qperf_args, timeout=qperf_timeout) + except Exception as ex: + logger.error(f'qperf failed to complete execution: {ex}') + return {'succeeded': False, + 'diags': '\n'.join(diags), + 'error': 'The qperf command failed during execution. See server logs for more details.'} + + return _make_result('\n'.join(diags), status, stdout, stderr, test_duration) + + + +def run_server(): + + diags = [] + + global qperf_first_args + qperf_args = qperf_first_args.copy() + + # set server port. Must be same as server port + qperf_args.append('--listen_port') + qperf_args.append(config['port']) + + qperf_args = [str(x) for x in qperf_args] + logger.debug(f'Server: Running command: {" ".join(qperf_args)}') + + stdout = '' + stderr = '' + status = 0 + + diags.append(' '.join(qperf_args)) + + try: + start_at = input['schedule']['start'] + logger.debug(f'Server: Sleeping until {start_at}') + pscheduler.sleep_until(start_at) + logger.debug('Server: Starting') + except KeyError: + pscheduler.fail('Unable to find start time in input') + + qperf_timeout = test_duration + qperf_timeout += qperf_utils.setup_time(test_spec.get('link-rtt')) + qperf_timeout += config['wait_sleep'] + logger.debug(f'Server: Timeout for server is {qperf_timeout}') + status, stdout, stderr = pscheduler.run_program(qperf_args, timeout=qperf_timeout) + return _make_result('\n'.join(diags), status, stdout, stderr) + + + +def _make_result(diags, status, stdout, stderr, duration=0): + if status: + error_text = f'[STDOUT]\n{stdout}\n\n[STDERR]\n{stderr}' + return {'succeeded': False, + 'diags': diags, + 'error': f'qperf returned an error: {error_text}'} + + lines = stdout.split('\n') + + results = qperf_parser.parse_output(lines, duration) + results['diags'] = f'[DIAGS]\n{diags}\n\n[STDOUT]\n{stdout}' + + return results + + +results = {} +try: + if participant == 0: + logger.debug('Running client') + results = run_client() + elif participant == 1: + # Non-loopback server + logger.debug('Running server') + results = run_server() + else: + pscheduler.fail('Invalid participant.') +except Exception as ex: + logger.exception() + +logger.debug(f'Results: {results}') + +pscheduler.succeed_json(results) diff --git a/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/changelog b/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/changelog new file mode 100644 index 0000000000..f4b8ae656e --- /dev/null +++ b/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/changelog @@ -0,0 +1,5 @@ +pscheduler-tool-qperf (0.1) unstable; urgency=low + + * Initial release + + -- perfSONAR developers Thu, 23 Oct 2025 09:00:00 +0200 diff --git a/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/compat b/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/compat new file mode 100644 index 0000000000..f599e28b8a --- /dev/null +++ b/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/compat @@ -0,0 +1 @@ +10 diff --git a/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/control b/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/control new file mode 100644 index 0000000000..b50e3c534f --- /dev/null +++ b/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/control @@ -0,0 +1,16 @@ +Source: pscheduler-tool-qperf +Section: net +Priority: optional +Maintainer: perfSONAR developers +Build-Depends: debhelper (>= 10), python3 +Standards-Version: 3.9.8 +Homepage: https://github.com/perfsonar/pscheduler +Vcs-Git: git://github.com/perfsonar/pscheduler +Vcs-Browser: https://github.com/perfsonar/pscheduler + +Package: pscheduler-tool-qperf +Architecture: all +Depends: ${misc:Depends}, python3, python3-pscheduler (>= 4.4.0~), python3-packaging, + pscheduler-server (>= 4.4.0~), pscheduler-test-throughput, qperf +Description: pScheduler qperf tool + qperf tool class for pScheduler diff --git a/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/copyright b/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/copyright new file mode 100644 index 0000000000..fcad4c27ef --- /dev/null +++ b/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/copyright @@ -0,0 +1,23 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: pscheduler-tool-qperf +Source: https://github.com/perfsonar/pscheduler + +Files: * +Copyright: 2016-2023 perfSONAR project +License: Apache-2.0 + +License: Apache-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + . + On Debian systems, the complete text of the Apache version 2.0 license + can be found in "/usr/share/common-licenses/Apache-2.0". diff --git a/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/rules b/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/rules new file mode 100755 index 0000000000..855665f55d --- /dev/null +++ b/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/rules @@ -0,0 +1,37 @@ +#!/usr/bin/make -f +# See debhelper(7) (uncomment to enable) +# output every command that modifies files on the build system. +#DH_VERBOSE = 1 + +# see EXAMPLES in dpkg-buildflags(1) and read /usr/share/dpkg/* +DPKG_EXPORT_BUILDFLAGS = 1 +include /usr/share/dpkg/default.mk + +# main packaging script based on dh7 syntax +%: + dh $@ + +DEB_SOURCE_PACKAGE ?= $(strip $(shell egrep '^Source: ' debian/control | cut -f 2 -d ':')) +CLASS ?= $(shell echo $(DEB_SOURCE_PACKAGE) | sed 's/^pscheduler-//; s/-.*//') +NAME ?= $(shell echo $(DEB_SOURCE_PACKAGE) | sed 's/^[^-]*-[^-]*-//') +ROOT ?= $(CURDIR)/debian/$(DEB_SOURCE_PACKAGE) +PYTHON := $(shell which python3) + +override_dh_auto_build: + +override_dh_auto_test: + +override_dh_auto_install: + make install \ + PYTHON=$(PYTHON) \ + DOCDIR=$(ROOT)/usr/share/doc/pscheduler/$(CLASS) \ + DESTDIR=$(ROOT)/usr/lib/pscheduler/classes/$(CLASS)/$(NAME) \ + CONFDIR=$(ROOT)/etc/pscheduler/$(CLASS)/$(NAME) + + if [ -f $(CURDIR)/debian/sudoers ]; then \ + install -D -m 0440 $(CURDIR)/debian/sudoers \ + $(ROOT)/etc/sudoers.d/$(DEB_SOURCE_PACKAGE); \ + fi + +override_dh_auto_clean: + make clean diff --git a/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/source/format b/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/source/format new file mode 100644 index 0000000000..163aaf8d82 --- /dev/null +++ b/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/triggers b/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/triggers new file mode 100644 index 0000000000..b44d62bb88 --- /dev/null +++ b/pscheduler-tool-qperf/qperf/unibuild-packaging/deb/triggers @@ -0,0 +1 @@ +activate-noawait pscheduler-warmboot diff --git a/pscheduler-tool-qperf/qperf/unibuild-packaging/rpm/pscheduler-tool-qperf.spec b/pscheduler-tool-qperf/qperf/unibuild-packaging/rpm/pscheduler-tool-qperf.spec new file mode 100644 index 0000000000..8ba99adad6 --- /dev/null +++ b/pscheduler-tool-qperf/qperf/unibuild-packaging/rpm/pscheduler-tool-qperf.spec @@ -0,0 +1,67 @@ +# +# RPM Spec for pScheduler qperf Tool +# + +%define short qperf +%define perfsonar_auto_version 5.3.0 +%define perfsonar_auto_relnum 0.a1.0 + +Name: pscheduler-tool-%{short} +Version: %{perfsonar_auto_version} +Release: %{perfsonar_auto_relnum}%{?dist} + +Summary: qperf tool class for pScheduler +BuildArch: noarch +License: ASL 2.0 +Vendor: perfSONAR +Group: Unspecified + +Source0: %{short}-%{version}.tar.gz + +Provides: %{name} = %{version}-%{release} + +Requires: pscheduler-server >= 4.4.0 +Requires: %{_pscheduler_python}-pscheduler >= 4.4.1 +Requires: %{_pscheduler_python}-cryptography +Requires: %{_pscheduler_python}-packaging +Requires: pscheduler-test-throughput +Requires: qperf +# For additrional TCP congestion control modules +Requires: kernel-modules-extra +Requires: rpm-post-wrapper + +BuildRequires: pscheduler-rpm + + +%description +qperf tool class for pScheduler + + +%prep +%setup -q -n %{short}-%{version} + + +%define dest %{_pscheduler_tool_libexec}/%{short} + +%build +make \ + PYTHON=%{_pscheduler_python} \ + DESTDIR=$RPM_BUILD_ROOT/%{dest} \ + CONFDIR=$RPM_BUILD_ROOT/%{_pscheduler_tool_confdir}\ + install + +%post +rpm-post-wrapper '%{name}' "$@" <<'POST-WRAPPER-EOF' +pscheduler internal warmboot +POST-WRAPPER-EOF + + +%postun +pscheduler internal warmboot + + +%files +%defattr(-,root,root,-) +%license LICENSE +%config(noreplace) %{_pscheduler_tool_confdir}/* +%{dest} diff --git a/unibuild-order b/unibuild-order index 317f9963b3..8316339b8f 100755 --- a/unibuild-order +++ b/unibuild-order @@ -219,6 +219,8 @@ ifelse(DISTRO/eval(MAJOR >= 11),Debian/1,iperf3, # We only want iperf3 on iperf3) pscheduler-tool-iperf3 +pscheduler-tool-qperf + pscheduler-tool-net-snmp-set --bundle snmp pscheduler-tool-nmapreach --bundle extras # TODO: This has problems. See #1223.