Tue Jun 3 2025
R7 is a configuration management framework made to configure nodes
with configuration files and related operations versioned in a central
repository. To remain code versioning system agnostic, manual or
automated versioning is left to the user, via makefiles or triggered
from r7 site-config configuration file (See
below).
The idea is to store hosts configurations in
nodes/hostname directories that maps each host’s
/etc and to assign those hosts to configuration groups
containing the directives to install the configuration and potential
requirements.
Nodes sub-directories are host names understood by the ssh client and configuration.
Currently, r7 requires valid ssh configuration and key files in
the repository .ssh/ directory.
Repositories can be initialized with rset init or
rset -w path/to/new-repository init, the user will then
have to provide valid configurations and keys for .ssh/ in
the newly created directory.
See Technical Notes for dependencies and supported platforms.
There is an example of r7 repository:
./nodes/
./nodes/hostA
./nodes/hostB
./nodes/hostC
./groups/
./groups/common
./groups/test0
./groups/common.dir
./groups/test0.dir
./.ssh/
./.ssh/config
./.ssh/id_ed25519_r7
./.ssh/id_ed25519_r7.pub
./site-config
There is an example of r7 site configuration
(site-config file):
prefix install state doc
group common '.*'
group test0 hostB hostC
output-dump
summaryThere is an example of the deployment of that configuration within the r7 repository:
$ r7 run-config
- (ok) hostA:common::install_nothing
- (ok) hostA:common::state_uname
- (ok) hostA:common::doc_tutorial
- (ok) hostB:common::install_nothing
- (ok) hostB:common::state_uname
- (ok) hostB:common::doc_tutorial
- (ok) hostC:common::install_nothing
- (ok) hostC:common::state_uname
- (ok) hostC:common::doc_tutorial
- (ok) hostB:test0::state_test
- (ok) hostC:test0::state_test
In this example, everything goes fine, groups are executed on each
related nodes, note the '.*' matching all hosts present in
./nodes. Groups are POSIX shell scripts that are a
collection of functions.
There is the common group:
install_nothing() {
echo Nothing installed
}
state_uname() {
uname -a
}
doc_tutorial() {
echo "## Welcome to the R7 tutorial!"
}And there is the test0 group:
state_test() {
test 1 -eq 1
}Any function managed by r7 needs to be declared in the
format ^PREFIX_NAME().
Group files names can’t contain spaces, tabs or newlines, as well for
nodes dirnames. The ‘:’ character is not recommended as
well as it may reduce readability of the interactive output.
Called groups and node directories are copied to related nodes in
hostname:/tmp/r7/:
/tmp/r7/hostA/ # Contents of test.org/nodes/hostA/
/tmp/r7/common.dir # Contents of test.org/groups/common.dir/
/tmp/r7/test.dir # Contents of test.org/groups/test.dir
Groups may have no .dir directory, in this case, nothing
is copied.
Once the group copy is done, functions are executed in the order of
the defined prefix (part of the site-config file shown
before):
prefix install state doc
This means the functions matching ^install.*(),
^state.*() and ^doc.*() (the default
R7_PREFIX) in the respective declaration order they’re
found in the related group file will be executed.
Site configurations are shell scripts sourced by r7 after it’s
environment is initialized, so prefix, group,
output-dump, summary are the functions or
aliases provided by r7 itself. This allow for scripting from the
site-config file. Here output-dump, and
summary are called to output potential errors, diffs and
permissions changes at the end of the interactive output obtained by
r7 run-config. If nothing happened, nothing is added to the
output.
In our previous example, we modify state_test so the
test fails.
state_test() {
test 1 -eq 2
}And we re-run the configuration to see what’s happening.
$ r7 run-config
- (ok) hostA:common::install_nothing
- (ok) hostA:common::state_uname
- (ok) hostA:common::doc_tutorial
- (ok) hostB:common::install_nothing
- (ok) hostB:common::state_uname
- (ok) hostB:common::doc_tutorial
- (ok) hostC:common::install_nothing
- (ok) hostC:common::state_uname
- (ok) hostC:common::doc_tutorial
- (error 1) hostB:test0::state_test
- (error 1) hostC:test0::state_test
- (error 1) - in hostB test0 state_test (1748622980)
- (error 1) - in hostC test0 state_test (1748622980)
The last two lines are part of the summary output.
Let’s modify the state_test function again to add some
verbosity to it’s output.
Note that we also change the return value.
state_test() {
echo something
false || {
echo >&2 failed
return 22
}
}This will produce the following summary:
- (error 22) - in hostB test0 state_test (1748622980)
something
failed
- (error 22) - in hostC test0 state_test (1748622980)
something
failed
Diffs and permissions changes are also displayed by the
summary command. When they’re are detected, the captured
related output is shown.
Using the init command, we can specify the current or a
specified work directory to be created and act as an r7 repository.
Automated versioning of outputs are currently let to the user, a recommended place would be the end of the site configuration file.
This command initializes a new directory called
test.org:
r7 -w test.org init
cd test.orgOnce this step is done, it is required to:
Copy or initialize the ssh key to the .ssh folder in
the repository created in .ssh/id_ed25519-r7 and
./ssh/id_ed25519-r7.pub
Adjust the SSH client configuration file in the repository
.ssh/config
Note that it is possible to import additional SSH configurations, like the user one from the SSH configuration, but it is advised to keep them separate.
The function unlock can be used to set the SSH agent
environment, this will be required for any automated operation later
done by r7:
cd test.org
. $(command -v r7) # Sourcing r7
unlock
tmux || screenset -g update-environment -r
This is useful for handling multiple tmux sessions.
# Add this to the shell's rc
if [ -z "$TMUX" ]; then
if [ -n "$SSH_TTY" ]; then
if [ -z "$SSH_AUTH_SOCK" ]; then
export SSH_AUTH_SOCK="$HOME/.ssh/.auth_socket"
fi
if [ ! -S "$SSH_AUTH_SOCK" ]; then
eval $(ssh-agent -a $SSH_AUTH_SOCK) >/dev/null 2>&1
echo $SSH_AGENT_PID >$HOME/.ssh/.auth_pid
fi
if [ -z $SSH_AGENT_PID ]; then
export SSH_AGENT_PID=$(cat $HOME/.ssh/.auth_pid)
fi
ssh-add "$SSH_ID" 2>/dev/null
fi
fiFor adding a new host, the simplest way is to have pre-configured
authorized_keys for the target user, it is then required to
add it to the known_hosts file. For that, it is possible to
use r7 SSH functions:
. $(command -v r7) # Sourcing r7
ssh-accept-new hostnameIf resetting the target’s user authorized_keys file is
needed, the following function can be used:
. $(command -v r7) # Sourcing r7
ssh-authorized-keys-reset hostnameWarning: This will resets the SSH
authorized_keys file for the target user (root, by default) and put the
.ssh/id_ed25519-r7.pub in place.
Once the host is accepted in the known_hosts file, it’s
possible to import it automatically.
Avoiding to import the configuration manually is possible with the
import command:
r7 import hostnameMost common configuration files will be copied in a newly created
nodes/hostname sub-directory.
Group files collects functions related to the configuration of the operating system, networking, services, global and user configurations packages installations and application deployments.
By specifying the execution prefix, the user determines the order of execution of the functions declared in the group file.
service_A() {
service A enable
service A restart
}
service_B() {
service B enable
service B restart
}
state_A() {
service A status
}
doc_A() {
echo "## Service A"
echo
echo "- [url](http://test.org/)"
echo
}file: groups/example1
With the default prefix, install, state,
doc, the function that will be executed are
state_A and doc_A. To test that group file
with all the functions inside we can call r7 the following way:
r7 -P 'service state doc' group example1 hostnameOr using the -a flag which will create a prefix
automatically with all functions present in the group file:
r7 -a group example1 hostnameIn a real situation, this example will have failing functions because
the service command may not be found or the services named
A or B does not exist.
! (error 127) tools0:example1::service_A
! (error 127) tools0:example1::service_B
! (error 127) tools0:example1::state_A
- (ok) tools0:example1::doc_A
We could force the group execution to stop using a combination of the
-e flag and the error group function.
service_A() {
service A enable &&
service A restart ||
error unable to setup service A
}
service_B() {
service B enable &&
service B restart ||
error unable to setup service B
}
state_A() {
service A status
}
doc_A() {
echo "## Service A"
echo
echo "- [url](http://test.org/)"
echo
}file: groups/example2
Let’s run the updated example with the -s flag to
trigger the summary output.
rm -fr _output
r7 -sea group example2 hostnameThis will produce something like the following:
! (error) tools0:example2::service_A
! (exit) tools0:example2::service_A
- (error 127) - in tools0 example2 service_A (1748791986)
/bin/sh: <stdin>[155]: service: not found
:: 1748791986 tools0 ERROR unable to setup service A
To remove a function from a group, we need to ensure that this function returned 0 for the last execution.
state_function_to_remove() {
# old code
# not used anymore
:
}This would prevent for seeing potential past errors caused by this function in the summary output.
The group library comes with the following functions that will allow
installation of files and triggering actions if necessary:
install, groupinstall,
nodeinstall. They are all shortcuts to an internal
r7_install function, it’s just the wrappers changes
directory to respectively /tmp/r7,
/tmp/r7/groupname.dir/ and
/tmp/r7/hostname/.
If we want to install files from the group directory we will use something like:
install_program() {
groupinstall -m 755 -o root:bin my_program /usr/local/bin
}We are supposing to have the file
groups/example3.dir/my_program
And if it’s something from node, we should use:
install_program_config() {
nodeinstall -m 644 -o root:wheel my_config /etc
}We can now chain the install functions to configuration
checking and initialization of programs, services or network features
like in the following example, in which we install ntp.conf
from the deployed node directory to /etc:
service_ntpd() {
nodeinstall -m 644 -o root:wheel ntpd.conf /etc &&
ntpd -n &&
rcctl enable ntpd &&
rcctl restart ntpd
}We may want to avoid executing a function or parts of it if the node
has no related files before executing the installation and actions
handling parts. This can be done with the help of the node
function.
service_ntpd() {
node ntpd.conf &&
nodeinstall -m 644 -o root:wheel ntpd.conf /etc &&
ntpd -n &&
rcctl enable ntpd &&
rcctl restart ntpd
}Here, node will return 31 if there is no
file or directory called ntpd.conf inside the deployed node
directory (/tmp/r7/hostname).
0, 30 and 31 exit codes are
ignored when r7 will search for execution errors. This means
30 and 31 exit code are reserved to r7.
In other terms, this will update the NTP daemon configuration only if
ntpd.conf is present in the node directory, otherwise it is
kept by default, which is the system provided default
ntpd.conf.
Here’s an example how to source additional shell from the deployed node directory, or elsewhere:
service_sysrc() {
sysrc dumpdev=NO
sysrc kld_list="vmm if_tuntap if_bridge nmdm"
sysrc microcode_update_enable=YES
sysrc mountd_enable=YES
sysrc nfs_server_enable=YES
sysrc ntpdate_enable=YES
sysrc powerd_enable=YES
sysrc rcshutdown_timeout=900
sysrc rpcbind_enable=YES
sysrc sshd_enable=YES
sysrc zfs_enable=YES
nodesource sysrc.local
}In this FreeBSD example, we configure the system RC globally with
service_sysrc which apply, if sysrc.local is
found in the root of the node directory, it’s sourcing in the current
scope via the help of nodesource sysrc.local.
The file nodes/hostname/sysrcl.local contains the
following:
sysrc ifconfig_igb0=DHCP
sysrc ifconfig_igb1=upInside the r7 release, a small example groups library can be found in
the groups directory, that can also be browsed in the repository.
Here we install the profile file provided in the
groupname.dir directory or the profile provided by the node
directory:
setup_profile() {
groupinstall -m 644 -o root:wheel profile /etc
node profile &&
nodeinstall -m 644 -o root:wheel profile /etc
}Here we add new users:
setup_users() {
useradd user1 2>/dev/null || :
useradd user2 2>/dev/null || :
}In the following example, we install a script and call it from a dedicated user crontab.
mail_admins=admins@test.org
install_myscript() {
groupinstall -o root:bin -m 755 myscript /usr/local/bin &&
useradd -d /var/empty -s /sbin/nologin _myscript
crontab -u _myscript - <<-/
MAILTO=$mail_admins
*/5 * * * * myscript
/
}After the execution is done, the result are saved in the
_run directory, output-dump needs to be called
to produce the current _output structure.
The r7 output command can then be used to show specific
output groups or functions:
r7 outputWill show the whole output of the latest groups deployments.
This format can be used to match specific host, group or function:
r7 output [hostname [groupname [funcname]]]For example:
$ r7 output tools0 OpenBSD state_packages\*
- tools0 OpenBSD state_packages_upgrades
quirks-7.103 signed on 2025-05-30T01:12:03Z
- tools0 OpenBSD state_packages_repository
https://cdn.openbsd.org/pub/OpenBSDNote that the output function arguments are using
shell’s case glob patterns.
A R7_WORKDIR/.active_nodes file is automatically created
when r7 tries to connect to a list of hosts via the group
command or function. This serves as a cache of available hosts reachable
via SSH login (the username is specified in the SSH configuration
file).
To add a previously non-available host, simply remove the file and
use group (here via run-config):
rm .active_nodes
r7 run-configUsage:
r7 [options] [command [arguments...]]-h Show usage and exit
-d Enable debug mode
-i Specify the SSH identity file
-F Specify the SSH client configuration file
-w Set the work directory where
./nodes and ./groups can be found
-r Set the run directory (default:
./_run)
-o Set the output directory (default:
./_output)
-c Specify the site configuration file (default:
./site-config)
-n Enable dry run mode; prevents any modification on hosts
-e Exit the group execution on the error function
-p Run each host in parallel per group call
-O Show output interactively
-P Specify a prefix, can contain whole function names
-a Select all available prefixes for a group command
-s Apply output-dump and summary after a group command
| Name | Description |
|---|---|
| run-config | Run the site configuration |
| group | Deploy specified group to hosts |
| output-dump | Dump the run directory to the output directory |
| output | Consult the output |
| summary | Produce a summary of errors and changes |
| digraph-host | Produce a dot digraph for hosts |
| digraph-ssh | Produce a dot digraph of SSH ID’s, users and nodes relations |
| import | Import TARGET host in the current nodes directory |
| info | Default command showing active nodes and summary |
| init | Initialize a new work directory |
| Name | Default value | Description |
|---|---|---|
| R7_DEBUG | no | Enable debug mode |
| R7_DRYRUN | no | Enable dry-run mode |
| R7_GROUP_ALLPREFIX | no | Group selects all available prefix |
| R7_GROUP_SUMMARY | no | Enable output-dump and summary after group execution |
| R7_GROUP_EXITONERROR | no | Enable exit on error function |
| R7_PARALLEL | no | Enable group nodes parallel execution |
| R7_PREFIX | install state doc | Set the r7 group execution prefix |
| R7_SHOWOUTPUT | no | Show interactive output |
| R7_WORKDIR | PWD | Set the r7 repository path |
| R7_SITE_CONFIG | R7_WORKDIR/site-config | Set the r7 site-config path |
| R7_OUTPUTDIR | R7_WORKDIR/_output | Set the r7 output directory path |
| SSH_CONFIG_DIR | R7_WORKDIR/.ssh | Set the SSH configuration directory path |
| SSH_CONFIG_FILE | R7_WORKDIR/.ssh/config | Set the SSH configuration file path |
| SSH_CONTROL_DIR | SSH_CONFIG_DIR/control | Set the SSH control dir path |
| SSH_IDENTITY_FILE | R7_WORKDIR/.ssh/id_ed25519_r7 | Set the SSH identity file |
| SSH_KNOWN_HOSTS | R7_WORKDIR/.ssh/known_hosts | Set the SSH known hosts file |
| SSH_CONNECT_TIMEOUT | 3 | Set the SSH connect timeout |
Those variables are available during the execution of th group file’s functions.
| Name | Description |
|---|---|
| trace_id | Current run trace ID |
| groupname | Current group name being executed |
| exitonerror | Cause the error function to exit if equals yes |
| nodename | Contains the output of nodename |
| nodedir | Contains the output of nodedir |
| groupdir | Contains the output of groupdir |
Those functions are accessible during the execution of the group file’s functions. Non documented functions are reserved for internal use.
Description: Append a trace containing the
current node name and trace_id
Usage:
trace [string...]Description: Trace an error message that
will inform the user of an event in the summary output, exit the group
execution if -e is set
Usage:
error [string...]Description: Returns true if the argument
is an existing file or directory inside
/tmp/r7/$(nodename), else, returns 31
Usage:
node [filename|dirname]Description: Returns the small host name of the current host
Usage:
nodenameDescription: Returns
/tmp/r7/$(nodename)
Usage:
nodenameDescription: Returns
/tmp/r7/$groupname.dir
Usage:
groupdirDescription: Change directory to
/tmp/r7 and install files if changed and sets mode and
owner, returns true if the file changed, otherwise returns 30
Usage:
install [-o owner] [-m mode] source destintaionDescription: Change directory to the
current group .dir directory in /tmp/r7 then
performs like install
Usage:
groupinstall [-o owner] [-m mode] source destintaionDescription: Change directory to the
current node directory in /tmp/r7 then performs like
install
Usage:
nodeinstall [-o owner] [-m mode] source destintaionDescription: Install directory calling
install for each file and checking for permissions
changes.
Usage:
installdir [-o fileowner] [-m filemode] [-O dirowner] [-O dirmode] source
destinationDescription: Change directory to the current group directory then call installdir
Usage:
groupinstalldir [-o fileowner] [-m filemode] [-O dirowner] [-O dirmode] source
destinationDescription: Change directory to the current group directory then call installdir
Usage:
nodeinstalldir [-o fileowner] [-m filemode] [-O dirowner] [-O dirmode] source
destinationDescription: Source a POSIX shell file in the current execution
Usage:
fsource filenameDescription: Change directory to the current group directory then call fsource with the file name as argument, produces an error if filename is not found from the group directory
Usage:
groupsource filenameDescription: Change directory to the current group directory then call node and fsource with the file name as argument without producing an error if the filename is not found
Usage:
nodesource filenameDescription: Template the files passed as arguments with the current environment or by sourcing a specified file
Usage:
template [-s sourcefile] [file...]Description: Change directory to the current group directory before calling template
Usage:
grouptemplate [-s sourcefile] [file...]Description: Change directory to the current node directory before calling template
Usage:
nodetemplate [-s sourcefile] [file...]Usable from the r7 site-config file or sourced in the
current shell using:
. $(command -v r7)Each functions are declared with underscore names and aliased with dashes. Shorter aliases are also available, list all declared aliases with:
aliasNon documented functions are reserved for internal use.
Description: Default command showing active nodes and summary, ran when sourced
Usage:
infoDescription: Shows the command line help
Usage:
usageDescription: Resets the SSH agent environment and issues ssh-add for SSH_IDENTITY_FILE
Usage:
unlockDescription: r7 openrsync, rsync, scp wrapper; selected in order of availability
Usage:
copy SRC DSTDescription: r7 SSH wrapper; behaves like the standard OpenSSH command
Usage:
ssh OPTION HOSTDescription: r7 SSH with debug connection mode for a specific HOST
Usage:
ssh-debug HOSTDescription: Calls ssh-keygen to dump the SSH known_hosts file
Usage:
ssh-known-hostsDescription: Resets SSH authorized_keys on the specified HOST
Usage:
ssh-authorized-keys-reset HOSTDescription: Adds a new host entry in the SSH known_hosts file
Usage:
ssh-accept-new HOSTDescription: Cleans the SSH control master directory
Usage:
ssh-control-master-cleanDescription: Searches nodes for authorized_keys_* and returns a list of users
Usage:
ssh-usersDescription: Returns sorted SSH IDs
Usage:
ssh-idsDescription: Returns the list of authorized_keys from nodes
Usage:
ssh-authorized-keysDescription: Returns SSH IDs along with matching key file and host name
Usage:
ssh-ids-hostsDescription: Returns a list of available nodes
Usage:
nodesDescription: Returns a list of active nodes
by detecting SSH availability (a login is done), writes
R7_WORKDIR/.active_nodes containing the available hosts
Usage:
nodes-activeDescription: Executes
R7_SITE_CONFIG in the loaded environment
Usage:
run-configDescription: Returns a list of active nodes matching a list of grep REGEXES
Usage:
members REGEXESDescription: Applies GROUPNAME functions (in prefix order) to the provided MEMBERS list
Usage:
group GROUPNAME MEMBERSDescription: Returns a list of functions found in the specified GROUPNAME
Usage:
group-functions GROUPNAMEDescription: Returns a list of all group
functions found in the current R7_WORKDIR/groups directory
with the group_name::function_name format
Usage:
group-functions-allDescription: Sets the current PREFIX for matching function names, can match for the whole unique name in the group, for executing one specified function only
Usage:
prefix PREFIXDescription: Dumps R7_RUNDIR to R7_OUTPUTDIR
Usage:
output-dumpDescription: Returns a list of outputs from the latest run
Usage:
output-latestDescription: Returns a list of current dumped functions exit status codes
Usage:
output-statusDescription: Returns a list of durations of executed groups
Usage:
output-durationDescription: Shows the dumped output matching HOST, GROUPNAME, and FUNCTION (or any if unspecified)
Usage:
output HOST GROUPNAME FUNCTIONDescription: Returns the latest execution trace_id
Usage:
trace-id-lastDescription: Returns the diffs from the latest execution
Usage:
output-diffsDescription: Returns the latest permission changes
Usage:
output-permissionsDescription: Returns the latest errors detected
Usage:
output-errorsDescription: Produces a summary of errors, diffs, and permission outputs
Usage:
summaryDescription: Returns a directed graph (in DOT format) showing relations among groups, functions, and nodes
Usage:
digraph-hostDescription: Returns a directed graph (in DOT format) of relations between SSH IDs and hosts
Usage:
digraph-ssh-id-hostsDescription: Retunrs a report page with the
content of README.md in HTML format
Usage:
htmlDescription: Import configuration files from a target host
Usage:
import HOST/bin/sh or compatible must be set for the user’s shell
(which is usually the case by default)/tmp must be writable by the userThe provided group library is shared as additional examples WITH NO WARRANTIES of the documentation and contain the following third-party files under the ISC License. Copyright information are indicated in the LICENSE section of related files.
pf-badhost.shunbound-adblock.shhttp-ban.shThe project is shared under the terms of the ISC license. Any version
previous 0.1.0 needs to be considered obsolete and should
not be redistributed. History in the GIT repository is kept for
transparency purpose only.
Below is a copy of the applicable license:
ISC License
Copyright 2025 xs <xs@inda.re>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.