diff --git a/pscheduler-test-dns64/Makefile b/pscheduler-test-dns64/Makefile
new file mode 100644
index 000000000..413b49797
--- /dev/null
+++ b/pscheduler-test-dns64/Makefile
@@ -0,0 +1,7 @@
+#
+# Makefile for Any Package
+#
+
+AUTO_TARBALL := 1
+
+include unibuild/unibuild.make
diff --git a/pscheduler-test-dns64/README.md b/pscheduler-test-dns64/README.md
new file mode 100644
index 000000000..cd6bb53c3
--- /dev/null
+++ b/pscheduler-test-dns64/README.md
@@ -0,0 +1 @@
+Documentation on how to write a test for pScheduler can be found in the main directory of the PDK in the file test.md (https://github.com/perfsonar/pscheduler/blob/pdk-docs/scripts/PDK/test.md)
diff --git a/pscheduler-test-dns64/dns64/Makefile b/pscheduler-test-dns64/dns64/Makefile
new file mode 100644
index 000000000..47cfddf6c
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/Makefile
@@ -0,0 +1,47 @@
+#
+# Makefile for any test class
+#
+
+NAME=dns64
+
+# TODO: Everything below this should be made into a template that can
+# be included.
+
+FILES=\
+ cli-to-spec \
+ enumerate \
+ participants \
+ result-format \
+ spec-format \
+ spec-is-valid \
+ spec-to-cli
+
+MODULES=\
+ validate \
+
+
+PYS=$(MODULES:%=%.py)
+PYCS=$(MODULES:%=__pycache__/%.pyc)
+
+$(PYCS):
+ifndef DESTDIR
+ @echo No PYTHON specified for build
+ @false
+endif
+ $(PYTHON) -m compileall .
+TO_CLEAN += $(PYCS)
+
+
+install: $(FILES) $(PYS) $(PYCS)
+ifndef DESTDIR
+ @echo No DESTDIR specified for installation
+ @false
+endif
+ mkdir -p $(DESTDIR)
+ install -m 555 $(FILES) $(DESTDIR)
+ install -m 444 $(PYS) $(DESTDIR)
+ cp -r __pycache__ $(DESTDIR)
+
+
+clean:
+ rm -f $(TO_CLEAN) *~
diff --git a/pscheduler-test-dns64/dns64/cli-to-spec b/pscheduler-test-dns64/dns64/cli-to-spec
new file mode 100755
index 000000000..b0b63f224
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/cli-to-spec
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+
+#
+# Development Order #4:
+#
+# This file encodes CLI arguments as JSON data in a test spec,
+# as defined by the datatypes in validate.py
+#
+# This can be tested directly using the following syntax:
+# ./cli-to-spec --option argument
+#
+
+import re
+import argparse
+import pscheduler
+import sys
+
+if len(sys.argv) > 1:
+
+ # Args are on the command line
+ args = sys.argv[1:]
+
+else:
+
+ # Args are in a JSON array on stdin
+ json_args = pscheduler.json_load(exit_on_error=True)
+ args = []
+
+ if not isinstance(json_args,list):
+ pscheduler.fail("Invalid JSON for this operation")
+ for arg in json_args:
+ if not ( isinstance(arg, str)
+ or isinstance(arg, int)
+ or isinstance(arg, float) ):
+ pscheduler.fail("Invalid JSON for this operation")
+ args = [ str(arg) for arg in json_args ]
+
+
+
+# Gargle the arguments
+
+arg_parser = argparse.ArgumentParser(epilog=
+"""
+ This test validates DNS64 IPv4 and IPv6 conversion
+"""
+)
+
+# Add all potential command line options here
+
+
+arg_parser.add_argument("--query",
+ help="Hostname to look up.")
+
+arg_parser.add_argument("--nameserver",
+ help="Nameserver to query.")
+
+arg_parser.add_argument("--host",
+ help="Host to run the test.")
+
+arg_parser.add_argument("--host-node",
+ help="Hostname to run the test.", dest="host_node")
+
+arg_parser.add_argument("--translation-prefix",
+ help="Translation prefix to expect for converted results.",
+ dest="translation_prefix")
+
+arg_parser.add_argument("--timeout",
+ help="Timeout for each query attempt",
+ dest="timeout")
+
+
+arguments = arg_parser.parse_args(args)
+
+
+# Call .set(n) on this object to indicate that a parameter requires
+# schema level n or higher. The object will return the highest-set
+# value when .value() is called.
+#
+# For example, if the 'foo' parameter was introduced in schema level 2:
+#
+# if options.foo is not None:
+# result['foo'] = options.foo
+# schema.set(2)
+#
+# If this is a brand-new test, you won't need to call .set() since the
+# default is 1.
+
+schema = pscheduler.HighInteger(1)
+
+
+# Build the test specification. All we do here is build and set
+# schema levels. Validation happens elsewhere.
+
+result = { }
+
+if arguments.query is not None:
+ result['query'] = arguments.query
+
+if arguments.nameserver is not None:
+ result['nameserver'] = arguments.nameserver
+
+if arguments.host is not None:
+ result['host'] = arguments.host
+
+if arguments.host_node is not None:
+ result['host-node'] = arguments.host_node
+
+if arguments.translation_prefix is not None:
+ result['translation-prefix'] = arguments.translation_prefix
+
+if arguments.timeout is not None:
+ result['timeout'] = arguments.timeout
+
+result['schema'] = schema.value()
+
+
+pscheduler.succeed_json(result)
diff --git a/pscheduler-test-dns64/dns64/enumerate b/pscheduler-test-dns64/dns64/enumerate
new file mode 100644
index 000000000..f1fdea1fb
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/enumerate
@@ -0,0 +1,21 @@
+#!/bin/sed -e 1d;/^#/d
+
+#
+# Development Order #2:
+#
+# This JSON data describes the test, and will be shown when 'pscheduler
+# plugins tests' is run. This is the first file which should be edited.
+#
+
+{
+ "schema": 1,
+ "name": "dns64",
+ "description": "Test that checks for the correct functioning of a DNS64 server",
+ "version": "1.0",
+ "maintainer": {
+ "name": "perfSONAR Development Team",
+ "email": "perfsonar-developer@internet2.edu",
+ "href": "http://www.perfsonar.net"
+ },
+ "scheduling-class": "background"
+}
diff --git a/pscheduler-test-dns64/dns64/inputs/result-format b/pscheduler-test-dns64/dns64/inputs/result-format
new file mode 100644
index 000000000..7202fa726
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/inputs/result-format
@@ -0,0 +1,13 @@
+{
+ "succeeded": true,
+ "result": {
+ "schema": 1,
+ "time": "PT0.071234S",
+ "succeeded": true,
+ "translated": true,
+ "ipv4": ["27.173.202.254", "190.173.250.206", "222.173.190.239"],
+ "ipv6": ["64:ff9b::1bad:cafe", "64:ff9b::bead:face", "64:ff9b::dead:beef"]
+ },
+ "error": "",
+ "diags": ""
+}
diff --git a/pscheduler-test-dns64/dns64/inputs/spec-format b/pscheduler-test-dns64/dns64/inputs/spec-format
new file mode 100644
index 000000000..cc2e104b7
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/inputs/spec-format
@@ -0,0 +1,7 @@
+{
+ "query": "ipv4.me",
+ "nameserver": "2001:4860:4860::64",
+ "host": "foo.example.com",
+ "translation-prefix": "64:ff9b::/96",
+ "timeout": "PT15S"
+}
diff --git a/pscheduler-test-dns64/dns64/participants b/pscheduler-test-dns64/dns64/participants
new file mode 100644
index 000000000..d3940e756
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/participants
@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+
+#
+# Development Order #7:
+#
+# Participant list generator for 'dns64' task spec
+#
+# Input is an unvalidated dns64 test specification.
+#
+
+# Output is a JSON object:
+#
+# {
+# "participants": [ "host-1", ..., "host-n" ]
+# "null-reason": "Optional reason why participants[0] is null"
+# }
+#
+# The first element of "participants" array may be null to
+# signify that local host is the first participant.
+#
+
+import pscheduler
+import sys
+
+from validate import spec_is_valid
+
+# Validate the input
+
+json = pscheduler.json_load(exit_on_error=True)
+
+valid, message = spec_is_valid(json)
+if not valid:
+ pscheduler.fail(message)
+
+
+# Determine the list of participants
+
+# This test only has a single participant, which can be determined by
+# looking up the 'host-node' or 'host' item in the specification.
+host = json.get('host-node', json.get('host', None))
+
+participants = [ host ]
+
+result = { "participants": participants }
+
+
+# Explain why the first participant is null
+
+if host is None:
+ result["null-reason"] = "No host specified"
+
+
+pscheduler.succeed_json(result)
diff --git a/pscheduler-test-dns64/dns64/result-format b/pscheduler-test-dns64/dns64/result-format
new file mode 100755
index 000000000..856937084
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/result-format
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+
+#
+# Development Order #8:
+#
+# This will format a test result into something that is human readable.
+#
+# To test this file, a spec is needed. You can generate one by pulling
+# a result out of the pScheduler API or modifying inputs/result-format
+# to suit your format.
+#
+# Invoke this program as follows:
+#
+#
+# ./result-format text/plain < inputs/result-format
+# ./result-format text/html < inputs/result-format
+#
+
+import pscheduler
+
+from validate import result_is_valid
+from validate import MAX_SCHEMA
+
+# This is a Jinja2 template with the contents of the test
+# specification provided as variables.
+#
+# Input provided to the template will be the original test spec in
+# spec.* and the result to bef formatted in result.*.
+#
+# See the documentation for spec_result_method() in
+# python-pscheduler/pscheduler/pscheduler/text.py for a list of
+# variables and functions provided.
+
+TEMPLATE = '''
+{% if _mime_type == 'text/plain' %}
+
+Elapsed Time ... {{ unspec(result.time) }}
+{% for record in result.ipv4 -%}
+IPv4 address ... {{ record }}
+{% endfor -%}
+
+{%- for record in result.ipv6 -%}
+IPv6 address ... {{ record }}
+{% endfor -%}
+
+Translated ..... {{ result.translated }}
+
+{% elif _mime_type == 'text/html' %}
+
+
+Elapsed Time | {{ unspec(result.time) }} |
+IPv4 | - {{ result.ipv4 | join('
- ') }}
|
+IPv6 | - {{ result.ipv6 | join('
- ') }}
|
+Translated | {{ result.translated }} |
+
+
+{% else %}
+
+{{ error('Unsupported MIME type "' + _mime_type + '"') }}
+
+{% endif %}
+'''
+
+pscheduler.result_format_method(TEMPLATE, max_schema=MAX_SCHEMA, validator=result_is_valid)
diff --git a/pscheduler-test-dns64/dns64/spec-format b/pscheduler-test-dns64/dns64/spec-format
new file mode 100755
index 000000000..7c1956653
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/spec-format
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+#
+# Development Order #9:
+#
+# This will format a test spec into something that is human readable.
+#
+# To test this file, a spec is needed. You can generate one with
+# cli-to-spec or modify inputs/spec-format after you've written
+# it.
+#
+# Invoke this program as follows:
+#
+#
+# ./spec-format text/plain < inputs/spec-format
+# ./spec-format text/html < inputs/spec-format
+#
+
+import pscheduler
+
+from validate import spec_is_valid
+from validate import MAX_SCHEMA
+
+# This is a Jinja2 template with the contents of the test
+# specification provided as variables.
+#
+# See the documentation for spec_result_method() in
+# python-pscheduler/pscheduler/pscheduler/text.py for a list of
+# variables and functions provided.
+
+TEMPLATE='''
+{# If dealing with a spec that has multiple schemas, do this:
+ {%- set schema = 1 if schema is undefined else schema -%}
+#}
+
+{% if _mime_type == 'text/plain' %}
+
+Query ............. {{ unspec(query) }}
+Nameserver ........ {{ unspec(nameserver) }}
+Host ............. {{ unspec(host) }}
+Host node ............. {{ unspec(hostnode) }}
+Translation prefix ......... {{ unspec(translationprefix) }}
+Timeout .......... {{ unspec(timeout) }}
+
+
+{% elif _mime_type == 'text/html' %}
+
+
+Query | {{ unspec(query) }} |
+Nameserver | {{ unspec(nameserver) }} |
+Host | {{ unspec(host) }} |
+Host node | {{ unspec(hostnode) }} |
+Translation prefix | {{ unspec(translationprefix) }} |
+Timeout | {{ unspec(timeout) }} |
+
+
+
+{% else %}
+
+{{ error('Unsupported MIME type "' + _mime_type + '"') }}
+
+{% endif %}
+'''
+
+pscheduler.spec_format_method(TEMPLATE, max_schema=MAX_SCHEMA, validator=spec_is_valid)
diff --git a/pscheduler-test-dns64/dns64/spec-is-valid b/pscheduler-test-dns64/dns64/spec-is-valid
new file mode 100755
index 000000000..0656e8705
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/spec-is-valid
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+
+#
+# Development Order #6:
+#
+# This file accepts a test spec through stdin and outputs whether
+# or not it was validated through stdout.
+#
+# This can be tested directly using the following syntax:
+# ./cli-to-spec --option argument | ./spec-is-valid
+#
+# NOTE: In most cases, there should be no need to modify this file.
+#
+
+import pscheduler
+
+from validate import spec_is_valid
+
+try:
+ json = pscheduler.json_load()
+except ValueError as ex:
+ pscheduler.succeed_json({
+ "valid": False,
+ "error": str(ex)
+ })
+
+valid, message = spec_is_valid(json)
+
+result = {
+ "valid": valid
+}
+
+if not valid:
+ result["error"] = message
+
+pscheduler.succeed_json(result)
diff --git a/pscheduler-test-dns64/dns64/spec-to-cli b/pscheduler-test-dns64/dns64/spec-to-cli
new file mode 100755
index 000000000..7a68433fc
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/spec-to-cli
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+
+#
+# Development Order #5:
+#
+# This file will convert a test specification to command-line options.
+#
+# This can be tested directly using the following syntax:
+# ./cli-to-spec --option argument | ./spec-to-cli
+
+import pscheduler
+
+from validate import spec_is_valid
+
+spec = pscheduler.json_load(exit_on_error=True)
+
+# First, validate the spec
+valid, message = spec_is_valid(spec)
+
+if not valid:
+ pscheduler.fail(message)
+
+result = pscheduler.speccli_build_args(spec,
+ strings=[
+
+ # Add all argument strings here, as tuples
+
+ ( 'query', 'query' ),
+ ( 'nameserver', 'nameserver' ),
+ ( 'host', 'host' ),
+ ( 'host-node', 'host_node' ),
+ ( 'translation-prefix', 'translation_prefix'), #should this be _ or -?
+ ( 'timeout', 'timeout' )
+ ])
+
+pscheduler.succeed_json(result)
diff --git a/pscheduler-test-dns64/dns64/unibuild-packaging/deb/changelog b/pscheduler-test-dns64/dns64/unibuild-packaging/deb/changelog
new file mode 100644
index 000000000..b8a37a9ed
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/unibuild-packaging/deb/changelog
@@ -0,0 +1,12 @@
+pscheduler-test-dns64 (5.2.0~a1.0-1) perfsonar-5.2-snapshot; urgency=low
+
+ * New upstream version.
+
+ -- perfSONAR developers Tue, 11 Jun 2024 18:29:36 +0200
+
+pscheduler-test-dns64 (5.1.0~a1.0-1) perfsonar-5.1-snapshot; urgency=low
+
+ * Initial release
+
+ -- perfSONAR developers Tue, 13 Oct 2020 17:48:12 +0000
+
diff --git a/pscheduler-test-dns64/dns64/unibuild-packaging/deb/compat b/pscheduler-test-dns64/dns64/unibuild-packaging/deb/compat
new file mode 100644
index 000000000..f599e28b8
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/unibuild-packaging/deb/compat
@@ -0,0 +1 @@
+10
diff --git a/pscheduler-test-dns64/dns64/unibuild-packaging/deb/control b/pscheduler-test-dns64/dns64/unibuild-packaging/deb/control
new file mode 100644
index 000000000..ab4da06e7
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/unibuild-packaging/deb/control
@@ -0,0 +1,16 @@
+Source: pscheduler-test-dns64
+Section: net
+Priority: optional
+Maintainer: perfSONAR developers
+Build-Depends: debhelper (>= 10)
+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/tree/master
+
+Package: pscheduler-test-dns64
+Architecture: all
+Depends: ${misc:Depends}, python3, python3-pscheduler,
+ pscheduler-server
+Description: pScheduler dns64 test
+ dns64 test class for pScheduler
diff --git a/pscheduler-test-dns64/dns64/unibuild-packaging/deb/copyright b/pscheduler-test-dns64/dns64/unibuild-packaging/deb/copyright
new file mode 100644
index 000000000..fc651d8d1
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/unibuild-packaging/deb/copyright
@@ -0,0 +1,23 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: pscheduler-test-dns64
+Source: https://github.com/perfsonar/pscheduler
+
+Files: *
+Copyright: 2020-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-test-dns64/dns64/unibuild-packaging/deb/rules b/pscheduler-test-dns64/dns64/unibuild-packaging/deb/rules
new file mode 100755
index 000000000..db75d9a7e
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/unibuild-packaging/deb/rules
@@ -0,0 +1,33 @@
+#!/usr/bin/make -f
+# See debhelper(7) (uncomment to enable)
+# output every command that modifies files on the build system.
+#DH_VERBOSE = 1
+
+# 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 -C $(NAME) 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 -C $(NAME) clean
diff --git a/pscheduler-test-dns64/dns64/unibuild-packaging/deb/source/format b/pscheduler-test-dns64/dns64/unibuild-packaging/deb/source/format
new file mode 100644
index 000000000..163aaf8d8
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/unibuild-packaging/deb/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/pscheduler-test-dns64/dns64/unibuild-packaging/deb/triggers b/pscheduler-test-dns64/dns64/unibuild-packaging/deb/triggers
new file mode 100644
index 000000000..b44d62bb8
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/unibuild-packaging/deb/triggers
@@ -0,0 +1 @@
+activate-noawait pscheduler-warmboot
diff --git a/pscheduler-test-dns64/dns64/unibuild-packaging/rpm/pscheduler-test-dns64.spec b/pscheduler-test-dns64/dns64/unibuild-packaging/rpm/pscheduler-test-dns64.spec
new file mode 100644
index 000000000..89f369bc6
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/unibuild-packaging/rpm/pscheduler-test-dns64.spec
@@ -0,0 +1,68 @@
+#
+# RPM Spec for pScheduler dns64 Test
+#
+
+#
+# Development Order #1:
+#
+# This file is significant for building the test into pScheduler.
+# If additional libraries or parts of pScheduler are required,
+# they should be added here after line 25.
+#
+
+%define short dns64
+%define perfsonar_auto_version 5.2.0
+%define perfsonar_auto_relnum 0.a1.0
+
+Name: pscheduler-test-%{short}
+Version: %{perfsonar_auto_version}
+Release: %{perfsonar_auto_relnum}%{?dist}
+
+Summary: dns64 test for pScheduler
+BuildArch: noarch
+License: Apache 2.0
+Group: Unspecified
+
+Source0: %{short}-%{version}.tar.gz
+
+Provides: %{name} = %{version}-%{release}
+
+# Include all required libraries here
+Requires: pscheduler-server
+Requires: %{_pscheduler_python}-pscheduler >= 1.3
+Requires: rpm-post-wrapper
+
+BuildRequires: pscheduler-rpm
+
+
+%description
+dns64 test class for pScheduler
+
+
+%prep
+%setup -q -n %{short}-%{version}
+
+
+%define dest %{_pscheduler_test_libexec}/%{short}
+
+%build
+make \
+ PYTHON=%{_pscheduler_python} \
+ DESTDIR=$RPM_BUILD_ROOT/%{dest} \
+ install
+
+
+
+%post
+rpm-post-wrapper '%{name}' "$@" <<'POST-WRAPPER-EOF'
+pscheduler internal warmboot
+POST-WRAPPER-EOF
+
+
+%postun
+pscheduler internal warmboot
+
+
+%files
+%defattr(-,root,root,-)
+%{dest}
diff --git a/pscheduler-test-dns64/dns64/validate.py b/pscheduler-test-dns64/dns64/validate.py
new file mode 100644
index 000000000..6b243a309
--- /dev/null
+++ b/pscheduler-test-dns64/dns64/validate.py
@@ -0,0 +1,137 @@
+#
+# Validator for a pScheduler test and its result.
+#
+
+# IMPORTANT:
+#
+# When making changes to the JSON schemas in this file, corresponding
+# changes MUST be made in 'spec-format' and 'result-format' to make
+# them capable of formatting the new specifications and results.
+
+from pscheduler import json_validate_from_standard_template
+from pscheduler import json_validate
+import pscheduler
+
+MAX_SCHEMA = 1
+
+log = pscheduler.Log(prefix='test-dns64')
+
+#
+# Test Specification
+#
+
+# NOTE: A large dictionary of existing, commonly-used datatypes used
+# throughout pScheduler is defined in
+# pscheduler/python-pscheduler/pscheduler/pscheduler/jsonval.py.
+# Please use those where possible.
+
+SPEC_SCHEMA = {
+
+ "local": {
+
+ # Define any local types used in the spec here
+
+ },
+
+ "versions": {
+
+ # Initial version of the specification
+ "1": {
+ "type": "object",
+ # schema, host, host-node, and timeout are standard and
+ # should be included in most single-participant tests.
+ "properties": {
+ "schema": { "$ref": "#/pScheduler/Cardinal", "enum": [ 1 ] },
+ "query": { "$ref": "#/pScheduler/URL" },
+ "nameserver": { "$ref": "#/pScheduler/Host" },
+ "host": { "$ref": "#/pScheduler/Host" },
+ "host-node": { "$ref": "#/pScheduler/Host" },
+ "translation-prefix": { "$ref": "#/pScheduler/String" },
+ "timeout": { "$ref": "#/pScheduler/Duration" },
+ },
+ # If listed here, these parameters MUST be in the test spec.
+ "required": [
+ "query"
+ ],
+ # Treat other properties as acceptable. This should
+ # ALWAYS be false.
+ "additionalProperties": False
+ },
+
+ # Second and later versions of the specification
+ # "2": {
+ # "type": "object",
+ # "properties": {
+ # "schema": { "type": "integer", "enum": [ 2 ] },
+ # ...
+ # },
+ # "required": [
+ # "schema",
+ # ...
+ # ],
+ # "additionalProperties": False
+ #},
+
+ }
+
+}
+
+
+
+def spec_is_valid(json):
+
+ (valid, errors) = json_validate_from_standard_template(json, SPEC_SCHEMA)
+ #return json_validate(json, SPEC_SCHEMA, max_schema=MAX_SCHEMA)
+ log.debug(json)
+
+ if not valid:
+ return (valid, errors)
+
+ return (valid, errors)
+
+
+
+#
+# Test Result
+#
+
+RESULT_SCHEMA = {
+
+ "local": {
+ # Define any local types here.
+ },
+
+ "versions": {
+
+ "1": {
+ "type": "object",
+ "properties": {
+ "schema": { "type": "integer", "enum": [ 1 ] },
+ "succeeded": { "$ref": "#/pScheduler/Boolean" },
+ "time": { "$ref": "#/pScheduler/Duration" },
+ "ipv4": {
+ "type": "array",
+ "items": { "$ref": "#/pScheduler/IPv4" },
+ "minItems" : 0
+ },
+ "ipv6": {
+ "type": "array",
+ "items": {"$ref": "#/pScheduler/IPv6"},
+ "minItems": 0
+ },
+ "translated": { "$ref": "#/pScheduler/Boolean" },
+ },
+ "required": [
+ "succeeded",
+ "time",
+ ],
+ "additionalProperties": False
+ }
+
+ }
+
+}
+
+
+def result_is_valid(json):
+ return json_validate_from_standard_template(json, RESULT_SCHEMA)
diff --git a/pscheduler-tool-pydns64/Makefile b/pscheduler-tool-pydns64/Makefile
new file mode 100644
index 000000000..413b49797
--- /dev/null
+++ b/pscheduler-tool-pydns64/Makefile
@@ -0,0 +1,7 @@
+#
+# Makefile for Any Package
+#
+
+AUTO_TARBALL := 1
+
+include unibuild/unibuild.make
diff --git a/pscheduler-tool-pydns64/README.md b/pscheduler-tool-pydns64/README.md
new file mode 100644
index 000000000..6a4417fb0
--- /dev/null
+++ b/pscheduler-tool-pydns64/README.md
@@ -0,0 +1 @@
+Documentation for writing a tool for pScheduler can be found in the main directory of the PDK in the file tool.md (https://github.com/perfsonar/pscheduler/blob/pdk-docs/scripts/PDK/tool.md)
diff --git a/pscheduler-tool-pydns64/pydns64/Makefile b/pscheduler-tool-pydns64/pydns64/Makefile
new file mode 100644
index 000000000..ca806235d
--- /dev/null
+++ b/pscheduler-tool-pydns64/pydns64/Makefile
@@ -0,0 +1,28 @@
+#
+# Makefile for any tool class
+#
+
+NAME=pydns64
+
+FILES=\
+ can-run \
+ duration \
+ enumerate \
+ participant-data \
+ run \
+ merged-results \
+
+
+
+install: $(FILES)
+ifndef DESTDIR
+ @echo No DESTDIR specified for installation
+ @false
+endif
+ mkdir -p $(DESTDIR)
+ install -m 555 $(FILES) $(DESTDIR)
+
+
+
+clean:
+ rm -f $(TO_CLEAN) *~
diff --git a/pscheduler-tool-pydns64/pydns64/can-run b/pscheduler-tool-pydns64/pydns64/can-run
new file mode 100644
index 000000000..a769020f0
--- /dev/null
+++ b/pscheduler-tool-pydns64/pydns64/can-run
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+
+#
+# Development Order #3:
+#
+# This file will determine if this tool can run a test based on a test spec.
+#
+# Be sure to edit line 19, inserting the names of the tests the tool
+# should be compatible with.
+#
+
+# exit statuses should be different based on error
+
+import pscheduler
+
+json = pscheduler.json_load(exit_on_error=True);
+
+try:
+ if json['type'] != 'dns64':
+ pscheduler.succeed_json({
+ "can-run": False,
+ "reasons": [ "Unsupported test type" ]
+ })
+except KeyError:
+ pscheduler.succeed_json({
+ "can-run": False,
+ "reasons": [ "Missing test type" ]
+ })
+
+
+
+pscheduler.succeed_json({ "can-run": True })
diff --git a/pscheduler-tool-pydns64/pydns64/duration b/pscheduler-tool-pydns64/pydns64/duration
new file mode 100644
index 000000000..105053ebf
--- /dev/null
+++ b/pscheduler-tool-pydns64/pydns64/duration
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+
+#
+# Development Order #4:
+#
+# Determine the duration of a specified test.
+#
+
+#
+# TODO: This is a bare-bones, unreliable implementation that should be
+# used only for testing.
+#
+
+import datetime
+
+import pscheduler
+
+json = pscheduler.json_load(exit_on_error=True);
+
+try:
+ timeout_iso = json['spec']['timeout']
+except KeyError:
+ timeout_iso = 'PT10S'
+timeout = pscheduler.iso8601_as_timedelta(timeout_iso)
+
+pscheduler.succeed_json({
+ "duration": pscheduler.timedelta_as_iso8601( timeout )
+ })
diff --git a/pscheduler-tool-pydns64/pydns64/enumerate b/pscheduler-tool-pydns64/pydns64/enumerate
new file mode 100644
index 000000000..147a725e6
--- /dev/null
+++ b/pscheduler-tool-pydns64/pydns64/enumerate
@@ -0,0 +1,28 @@
+#!/bin/sed -e 1d;/^#/d
+
+#
+# Development Order #2:
+#
+# This JSON data describes the tool, and will be shown when 'pscheduler
+# plugins tools' is run. This is the first file which should be edited.
+#
+# Be sure to edit line 19, as this determines what tests the tool is
+# compatible with.
+#
+
+{
+ "schema": 1,
+
+ "name": "pydns64",
+ "description": "Tool to validate DNS64 queries",
+ "version": "1.0",
+ "tests": [ "dns64" ],
+
+ "preference": 0,
+
+ "maintainer": {
+ "name": "perfSONAR Development Team",
+ "email": "perfsonar-developer@internet2.edu",
+ "href": "http://www.perfsonar.net"
+ }
+}
diff --git a/pscheduler-tool-pydns64/pydns64/error-example-spec.json b/pscheduler-tool-pydns64/pydns64/error-example-spec.json
new file mode 100644
index 000000000..cd18e6878
--- /dev/null
+++ b/pscheduler-tool-pydns64/pydns64/error-example-spec.json
@@ -0,0 +1,17 @@
+{
+ "test": {
+ "type": "dns64",
+ "spec": {
+ "query": "ip4.me",
+ "timeout": "PT10S",
+ "schema": 1
+ }
+ },
+ "schema": 1,
+ "tool": "pydns64",
+ "href": "https://localhost.localdomain/pscheduler/tasks/d0035107-6ef0-4cc7-83b4-19367b502bfc",
+ "schedule": {
+ "slip": "PT5M"
+ }
+}
+
diff --git a/pscheduler-tool-pydns64/pydns64/example-test-spec.json b/pscheduler-tool-pydns64/pydns64/example-test-spec.json
new file mode 100644
index 000000000..7675309c3
--- /dev/null
+++ b/pscheduler-tool-pydns64/pydns64/example-test-spec.json
@@ -0,0 +1,19 @@
+{
+ "test": {
+ "type": "dns64",
+ "spec": {
+ "query": "v4onlytest.notonthe.net",
+ "nameserver": "2001:4860:4860::64",
+ "translation-prefix": "64:ff9b::/96",
+ "timeout": "PT15S",
+ "schema": 1
+ }
+ },
+ "schema": 1,
+ "tool": "pydns64",
+ "href": "https://localhost.localdomain/pscheduler/tasks/d0035107-6ef0-4cc7-83b4-19367b502bfc",
+ "schedule": {
+ "slip": "PT5M"
+ }
+}
+
diff --git a/pscheduler-tool-pydns64/pydns64/merged-results b/pscheduler-tool-pydns64/pydns64/merged-results
new file mode 100644
index 000000000..9397ecea0
--- /dev/null
+++ b/pscheduler-tool-pydns64/pydns64/merged-results
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+#
+# Merge the participant results of a run by this tool into a
+# test-standard result.
+# Note that this method will be given results for all participants
+# whether their runs succeeded or failed and should return a result
+# that indicates whether or not the run as a whole failed. This
+# allows multi-participant tools to return a successful result even if
+# one of the participants failed but there's enough data to do so.
+#
+
+import pscheduler
+
+input = pscheduler.json_load(exit_on_error=True);
+
+try:
+ # Single-participant tests usually use the result generated by the
+ # tool's 'run' method. Multiple-participant tests would go over
+ # the result from all participants and produce a suitable result.
+ result = input['results'][0]['result']
+except (IndexError, KeyError) as ex:
+ result = {
+ 'succeeded': False,
+ 'error': "Error in participant data: {}".format(str(ex))
+ }
+
+pscheduler.succeed_json(result)
diff --git a/pscheduler-tool-pydns64/pydns64/participant-data b/pscheduler-tool-pydns64/pydns64/participant-data
new file mode 100644
index 000000000..521bf47d1
--- /dev/null
+++ b/pscheduler-tool-pydns64/pydns64/participant-data
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+#
+# Return participant-specific data for a run
+#
+
+import pscheduler
+
+json = pscheduler.json_load(exit_on_error=True)
+
+# In this case, nothing of interest.
+print("{}")
+
+pscheduler.succeed()
diff --git a/pscheduler-tool-pydns64/pydns64/run b/pscheduler-tool-pydns64/pydns64/run
new file mode 100755
index 000000000..58112eb04
--- /dev/null
+++ b/pscheduler-tool-pydns64/pydns64/run
@@ -0,0 +1,120 @@
+#!/usr/bin/env python3
+
+#
+# Development Order #5:
+#
+# This is the meat and bones of the tool, where the actual desired
+# commands or operation will be run. The results are then recorded
+# and added to the 'results' JSON data, which will then be sent
+# back to the test. Both system and api are able to be used here.
+#
+
+import datetime
+import subprocess
+import json
+import sys
+import time
+import ipaddress
+from ipaddress import IPv6Address, IPv4Address
+from ipaddress import IPv6Network
+
+import pscheduler
+import dns.resolver
+
+
+log = pscheduler.Log(prefix='tool-pydns64', quiet=True)
+
+# from stdin
+input = pscheduler.json_load(exit_on_error=True)
+resolver = dns.resolver.Resolver()
+
+# Take input from test spec
+try:
+ query = input['test']['spec']['query']
+except KeyError:
+ pscheduler.fail('missing data in input')
+
+try:
+ resolver.nameservers = [input['test']['spec']['nameserver']]
+except KeyError:
+ pass # Not there? Don't care.
+
+timeout_iso = input['test']['spec'].get("timeout", "PT10S")
+timeout = pscheduler.timedelta_as_seconds( pscheduler.iso8601_as_timedelta(timeout_iso) )
+
+prefix = input['test']['spec'].get("translation-prefix", "64:ff9b::/96")
+
+# Run the actual task here:
+
+start_time = datetime.datetime.now()
+succeeded = False
+error = ''
+diags = ''
+ip4answers = None
+ip6answers = None
+ip4result_list = None
+ip6result_list = None
+translated = True
+
+# do test here
+
+try:
+ ip4answers = resolver.resolve(query, 'A')
+ ip6answers = resolver.resolve(query, 'AAAA' )
+ succeeded = True
+except dns.exception.Timeout:
+ error = 'Timeout'
+except dns.resolver.NoAnswer:
+ if ip4answers:
+ error = 'No IPv6 answer'
+ else:
+ error = 'No IPv4 answer, aborting'
+except dns.resolver.NXDOMAIN:
+ error = 'Domain does not exist'
+
+end_time = datetime.datetime.now()
+
+if ip4answers is None:
+ ip4result_list = []
+else:
+ ip4result_list = sorted(list(ip4answers))
+
+if ip6answers is None:
+ ip6result_list = []
+else:
+ ip6result_list = sorted(list(ip6answers))
+
+# Check to see if results are correctly translated
+if len(ip4result_list) == 0 or len(ip6result_list) == 0:
+ translated = False
+elif len(ip4result_list) != len(ip6result_list):
+ translated = False
+else:
+ for index, ip in enumerate(ip6result_list):
+ ipaddr = IPv6Address(ip)
+ if ipaddr not in IPv6Network(prefix):
+ translated = False
+ break
+ else:
+ if IPv4Address(ip4result_list[index]).packed != ipaddr.packed[-4:]:
+ translated = False
+ break
+
+
+# Organize results into json data
+results = {
+ 'succeeded': succeeded,
+ 'result': {
+ 'schema': 1,
+ 'time': pscheduler.timedelta_as_iso8601( end_time - start_time),
+ 'succeeded': succeeded,
+ 'translated': translated
+ },
+ 'error': error,
+ 'diags': diags }
+
+results [ 'result' ][ 'ipv4' ] = [str(ip) for ip in ip4result_list]
+results [ 'result' ][ 'ipv6' ] = [str(ip) for ip in ip6result_list]
+
+pscheduler.succeed_json(results)
+
diff --git a/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/changelog b/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/changelog
new file mode 100644
index 000000000..cdf7dcf25
--- /dev/null
+++ b/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/changelog
@@ -0,0 +1,12 @@
+pscheduler-tool-pydns64 (5.2.0~a1.0-1) perfsonar-5.2-snapshot; urgency=low
+
+ * New upstream version.
+
+ -- perfSONAR developers Tue, 11 Jun 2024 18:29:36 +0200
+
+pscheduler-tool-pydns64 (5.1.0~a1.0-1) perfsonar-5.1-snapshot; urgency=low
+
+ * Initial release
+
+ -- perfSONAR developers Tue, 13 Oct 2020 17:50:44 +0000
+
diff --git a/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/compat b/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/compat
new file mode 100644
index 000000000..f599e28b8
--- /dev/null
+++ b/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/compat
@@ -0,0 +1 @@
+10
diff --git a/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/control b/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/control
new file mode 100644
index 000000000..32d306fb8
--- /dev/null
+++ b/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/control
@@ -0,0 +1,16 @@
+Source: pscheduler-tool-pydns64
+Section: net
+Priority: optional
+Maintainer: perfSONAR developers
+Build-Depends: debhelper (>= 10)
+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/tree/master
+
+Package: pscheduler-tool-pydns64
+Architecture: all
+Depends: ${misc:Depends}, python3, python3-pscheduler,
+ pscheduler-server, pscheduler-test-pydns64
+Description: pScheduler pydns64 tool
+ pydns64 tool class for pScheduler
diff --git a/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/copyright b/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/copyright
new file mode 100644
index 000000000..5f5add890
--- /dev/null
+++ b/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/copyright
@@ -0,0 +1,23 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: pscheduler-tool-pydns64
+Source: https://github.com/perfsonar/pscheduler
+
+Files: *
+Copyright: 2020-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-pydns64/pydns64/unibuild-packaging/deb/rules b/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/rules
new file mode 100755
index 000000000..db75d9a7e
--- /dev/null
+++ b/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/rules
@@ -0,0 +1,33 @@
+#!/usr/bin/make -f
+# See debhelper(7) (uncomment to enable)
+# output every command that modifies files on the build system.
+#DH_VERBOSE = 1
+
+# 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 -C $(NAME) 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 -C $(NAME) clean
diff --git a/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/source/format b/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/source/format
new file mode 100644
index 000000000..163aaf8d8
--- /dev/null
+++ b/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/triggers b/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/triggers
new file mode 100644
index 000000000..b44d62bb8
--- /dev/null
+++ b/pscheduler-tool-pydns64/pydns64/unibuild-packaging/deb/triggers
@@ -0,0 +1 @@
+activate-noawait pscheduler-warmboot
diff --git a/pscheduler-tool-pydns64/pydns64/unibuild-packaging/rpm/pscheduler-tool-pydns64.spec b/pscheduler-tool-pydns64/pydns64/unibuild-packaging/rpm/pscheduler-tool-pydns64.spec
new file mode 100644
index 000000000..dfd879bbc
--- /dev/null
+++ b/pscheduler-tool-pydns64/pydns64/unibuild-packaging/rpm/pscheduler-tool-pydns64.spec
@@ -0,0 +1,58 @@
+#
+# RPM Spec for pScheduler pydns64 Tool
+#
+
+#
+# Development Order #1:
+#
+# This file is significant for buildling the tool into pScheduler.
+# If additional libraries or parts of pScheduler are required,
+# they should be added here (line 25).
+%define short pydns64
+%define perfsonar_auto_version 5.2.0
+%define perfsonar_auto_relnum 0.a1.0
+
+Name: pscheduler-tool-%{short}
+Version: %{perfsonar_auto_version}
+Release: %{perfsonar_auto_relnum}%{?dist}
+
+Summary: pydns64 tool class for pScheduler
+BuildArch: noarch
+License: Apache 2.0
+Group: Unspecified
+
+Source0: %{short}-%{version}.tar.gz
+
+Provides: %{name} = %{version}-%{release}
+
+# Include all required libraries here
+Requires: pscheduler-server
+Requires: %{_pscheduler_python}-pscheduler
+Requires: rpm-post-wrapper
+
+BuildRequires: pscheduler-rpm
+
+%description
+pydns64 tool class for pScheduler
+
+%prep
+%setup -q -n %{short}-%{version}
+
+%define dest %{_pscheduler_tool_libexec}/%{short}
+
+%build
+make \
+ DESTDIR=$RPM_BUILD_ROOT/%{dest} \
+ install
+
+%post
+rpm-post-wrapper '%{name}' "$@" <<'POST-WRAPPER-EOF'
+pscheduler internal warmboot
+POST-WRAPPER-EOF
+
+%postun
+pscheduler internal warmboot
+
+%files
+%defattr(-,root,root,-)
+%{dest}
diff --git a/unibuild-order b/unibuild-order
index 5a2e6f7a0..d6ab9f788 100755
--- a/unibuild-order
+++ b/unibuild-order
@@ -157,6 +157,7 @@ pscheduler-test-clock
pscheduler-test-dhcp --bundle extras
pscheduler-test-disk-to-disk --bundle extras
pscheduler-test-dns
+pscheduler-test-dns64
pscheduler-test-dot1x --bundle extras
pscheduler-test-http
pscheduler-test-latency
@@ -234,6 +235,7 @@ pscheduler-tool-ping
pscheduler-tool-psclock
pscheduler-tool-pstimer
pscheduler-tool-psurl --bundle obsolete
+pscheduler-tool-pydns64
pscheduler-tool-pysnmp --bundle snmp
define(HAVE_S3_BENCHMARK,ifelse(HAVE_GOLANG,0,0,