A Go implementation of a CDPI agent.
The Control-to-Data-Plane Interface (CDPI) works by exchanging Protocol Buffers, a language-agnostic format and toolchain for serializing messages, transmitted using gRPC, a performant RPC framework with many sophisticated features.
This directory provides two important pieces for interacting with the Spacetime CDPI:
-
The
agent
(//agent/cmd/agent
), a Go binary that handle the CDPI protocol and authentication details while delegating the actual implementation of enactments to a user-configured external process. While the details of the CDPI protocol are still subject to change, the command line interface for these binaries is intended to be significantly more stable and provide an easy to develop against abstraction that insulates platform integrators from the majority of those changes. -
The
agent
library (//agent
), a Go package that provides a growing set of abstractions for writing a new CDPI agent. This library is subject to change alongside the CDPI protocol, so platform integrators are encouraged to use theagent
binary until the underlying APIs reach a stable milestone.
This repo uses the bazel build system. Once you have a copy of bazel
in
your $PATH
, running bazel build //agent/cmd/agent
will build the Go binary. Similarly,
running bazel build //agent
will build the Go library.
For a full list of available build targets, you can use bazel query
:
bazel query //agent/...:all
The agent
binary accepts configuration in the form of a protobuf message, documented in
config.proto. The message can be encoded in
prototext format (human readable and
writable), json, or the
binary proto format. Most users will find the
prototext format the easiest to use, and as such it's the default.
More details for each aspect of the configuration are provided below, but a simple configuration file might look like this:
# each network_node is configured with a stanza like so:
network_nodes: {
id: "node-a"
enactment_driver: {
connection_params: {
# TODO: change to point to the domain of your spacetime instance
endpoint_uri: "scheduling.my_instance.spacetime.aalyria.com"
transport_security: {
system_cert_pool: {}
}
auth_strategy: {
jwt: {
# TODO: change to the domain of your spacetime instance
audience: "scheduling.my_instance.spacetime.aalyria.com"
# TODO: use the email your Aalyria representative will share with you
email: "[email protected]"
# TODO: use the private key ID your Aalyria representative will share with you
private_key_id: "BADDB0BACAFE"
signing_strategy: {
# TODO: change to the path of your PEM-encoded RSA private key
private_key_file: "/path/to/agent/private/key.pem"
}
}
}
}
external_command: {
# while each command invocation will receive a node ID as part of the
# enactment request, you can also pass additional arguments here to help
# integrate with your own systems
args: "/usr/local/bin/do_enactments"
args: "--node=a"
args: "--format=json"
# Encode enactment requests as JSON to the process's standard input and
# expect any new state messages to be written as JSON to the process's
# standard output (this is the default)
proto_format: JSON
}
}
}
network_nodes: {
id: "node-b"
enactment_driver: {
connection_params: {
# TODO: change to point to the domain of your spacetime instance
endpoint_uri: "scheduling.my_instance.spacetime.aalyria.com"
transport_security: {
system_cert_pool: {}
}
auth_strategy: {
jwt: {
# TODO: change to the domain of your spacetime instance
audience: "scheduling.my_instance.spacetime.aalyria.com"
# TODO: use the email your Aalyria representative will share with you
email: "[email protected]"
# TODO: use the private key ID your Aalyria representative will share with you
private_key_id: "BADDB0BACAFE"
signing_strategy: {
# TODO: change to the path of your PEM-encoded RSA private key
private_key_file: "/path/to/agent/private/key.pem"
}
}
}
}
external_command: {
args: "/usr/local/bin/some_other_enactment_cmd"
# Use the protobuf binary format for encoding both stdin and stdout messages.
proto_format: WIRE
}
}
}
See the documentation in config.proto for more details on the
available options. You can use the --dry-run
flag to check that your configuration is valid:
bazel run //agent/cmd/agent -- --log-level trace --config "$PWD/config.textproto" --dry-run
INFO: Invocation ID: d8a4b02f-1e01-47cb-bd26-5366704165af
INFO: Analyzed target //agent/cmd/agent:agent (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //agent/cmd/agent:agent up-to-date:
bazel-bin/agent/cmd/agent/agent_/agent
INFO: Elapsed time: 1.768s, Critical Path: 1.56s
INFO: 3 processes: 1 internal, 2 linux-sandbox.
INFO: Build completed successfully, 3 total actions
INFO: Running command line: bazel-bin/agent/cmd/agent/agent_/agent --log-level trace --config /path/to/config.textproto --format text --dry-run
2023-04-19 11:57:01AM INF config is valid
The agent uses signed JSON Web Tokens (JWTs) to authenticate with the CDPI service. The JWT needs to be signed using an RSA private key with a corresponding public key that's been shared - inside of a self-signed x509 certificate - with the Aalyria team.
For testing purposes, you can generate a valid key using the openssl
tool:
# generate a private key of size 4096 and save it to agent_priv_key.pem
openssl genrsa -out agent_priv_key.pem 4096
# extract the public key and save it to an x509 certificate named
# agent_pub_key.cer (with an expiration far into the future)
openssl req -new -x509 -key agent_priv_key.pem -out agent_pub_key.cer -days 36500
Assuming you've saved your configuration in a file called config.textproto
, you can use the
following command to start the agent:
bazel run //agent/cmd/agent -- --config "$PWD/my_config.textproto" --log-level debug
NOTE: bazel run
changes the working directory of the process, so you'll need
to use absolute paths to point to the config file.
If the agent was able to authenticate correctly, you should see something like this appear as output
(requires --log-level
be "debug" or "trace"):
2023-04-18 08:44:48PM INF starting agent
2023-04-18 08:44:48PM DBG node controller starting nodeID=Atlantis-groundstation
Writing a custom enactment backend using the agent
is relatively simple as the agent takes care of
the CDPI protocol details, including timing and error reporting. When the agent receives a scheduled
control update, it invokes the configured external process, writes the incoming
ScheduledControlUpdate
message in the encoding format of your choice to the process's stdin, and
optionally reads a new ControlPlaneState
message (using the same encoding format) from the
process's stdout.
-
If nothing is written to stdout and the process terminates with an exit code of 0, the enactment is considered successful and the node state is assumed to have been updated to match the change.
-
If anything goes wrong during the enactment (indicated by a non-zero exit code), the process's stderr and exit code are combined to form a gRPC status which is conveyed back to the CDPI endpoint as the (failing) result of the enactment.
Since the external process only needs to be able to encode and decode JSON, it's trivial to write the platform-specific logic in whatever language best suits the task. Included in this repo are some sample programs that demonstrate basic error handling and message parsing in different languages:
- examples/enact_flow_forward_updates.py: A python script that reads the input messages as ad-hoc JSON, implements some basic error handling, and demonstrates how one might go about enacting flow updates (the actual logic for forwarding packets is left as an exercise for the reader).
All agent implementations and instantiations MUST have some means to maintain time synchronization. Ideally an agent's time would be synchronzied to the same ultimate source(s) used by Spacetime itself, though this is not a strict requirement.
RECOMMENDED time synchronization mechanisms include:
- GPS or another Global Navigation Satellite System (GNSS) PNT service
- Network Time Protocol v4 (NTPv4), ideally with Network Time Security (NTS)
- IEEE Precision Time Protocol (IEEE 1588-2019)
Some experimental time synchronization mechanisms include:
An agent SHOULD maintain a sub-second difference from its chosen time source(s), and all RECOMMENDED time synchronizations can in principle achieve much better accuracies. The exact requirements and realistically achievable accuracies, however, are specific to a given deployment scenario.
If an agent instance or its underlying platform cannot maintain adequate time synchronization, for a mission-specific definition of "adequate", then it MUST do one of the following:
- include notification of the loss of adequate time synchronization with any reported telemetry, or
- not report telemetry (information is too untrustworthy to be useful).
An agent MAY continue to enact Scheduling API commands, especially if doing so might lead to restoring adequate time synchronization (e.g. restoration of a data plane connection over which a networked time protocol's messages are forwarded).