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
summary
There 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.org
Once 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 || screen
set -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
fi
For 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 hostname
If 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 hostname
Warning: 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 hostname
Most 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 hostname
Or using the -a
flag which will create a prefix
automatically with all functions present in the group file:
r7 -a group example1 hostname
In 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 hostname
This 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=up
Inside 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 output
Will 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/OpenBSD
Note 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-config
Usage:
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:
nodename
Description: Returns
/tmp/r7/$(nodename)
Usage:
nodename
Description: Returns
/tmp/r7/$groupname.dir
Usage:
groupdir
Description: 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 destintaion
Description: Change directory to the
current group .dir
directory in /tmp/r7
then
performs like install
Usage:
groupinstall [-o owner] [-m mode] source destintaion
Description: Change directory to the
current node directory in /tmp/r7
then performs like
install
Usage:
nodeinstall [-o owner] [-m mode] source destintaion
Description: Install directory calling
install
for each file and checking for permissions
changes.
Usage:
installdir [-o fileowner] [-m filemode] [-O dirowner] [-O dirmode] source
destination
Description: Change directory to the current group directory then call installdir
Usage:
groupinstalldir [-o fileowner] [-m filemode] [-O dirowner] [-O dirmode] source
destination
Description: Change directory to the current group directory then call installdir
Usage:
nodeinstalldir [-o fileowner] [-m filemode] [-O dirowner] [-O dirmode] source
destination
Description: Source a POSIX shell file in the current execution
Usage:
fsource filename
Description: 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 filename
Description: 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 filename
Description: 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:
alias
Non documented functions are reserved for internal use.
Description: Default command showing active nodes and summary, ran when sourced
Usage:
info
Description: Shows the command line help
Usage:
usage
Description: Resets the SSH agent environment and issues ssh-add for SSH_IDENTITY_FILE
Usage:
unlock
Description: r7 openrsync, rsync, scp wrapper; selected in order of availability
Usage:
copy SRC DST
Description: r7 SSH wrapper; behaves like the standard OpenSSH command
Usage:
ssh OPTION HOST
Description: r7 SSH with debug connection mode for a specific HOST
Usage:
ssh-debug HOST
Description: Calls ssh-keygen to dump the SSH known_hosts file
Usage:
ssh-known-hosts
Description: Resets SSH authorized_keys on the specified HOST
Usage:
ssh-authorized-keys-reset HOST
Description: Adds a new host entry in the SSH known_hosts file
Usage:
ssh-accept-new HOST
Description: Cleans the SSH control master directory
Usage:
ssh-control-master-clean
Description: Searches nodes for authorized_keys_* and returns a list of users
Usage:
ssh-users
Description: Returns sorted SSH IDs
Usage:
ssh-ids
Description: Returns the list of authorized_keys from nodes
Usage:
ssh-authorized-keys
Description: Returns SSH IDs along with matching key file and host name
Usage:
ssh-ids-hosts
Description: Returns a list of available nodes
Usage:
nodes
Description: 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-active
Description: Executes
R7_SITE_CONFIG
in the loaded environment
Usage:
run-config
Description: Returns a list of active nodes matching a list of grep REGEXES
Usage:
members REGEXES
Description: Applies GROUPNAME functions (in prefix order) to the provided MEMBERS list
Usage:
group GROUPNAME MEMBERS
Description: Returns a list of functions found in the specified GROUPNAME
Usage:
group-functions GROUPNAME
Description: 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-all
Description: 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 PREFIX
Description: Dumps R7_RUNDIR to R7_OUTPUTDIR
Usage:
output-dump
Description: Returns a list of outputs from the latest run
Usage:
output-latest
Description: Returns a list of current dumped functions exit status codes
Usage:
output-status
Description: Returns a list of durations of executed groups
Usage:
output-duration
Description: Shows the dumped output matching HOST, GROUPNAME, and FUNCTION (or any if unspecified)
Usage:
output HOST GROUPNAME FUNCTION
Description: Returns the latest execution trace_id
Usage:
trace-id-last
Description: Returns the diffs from the latest execution
Usage:
output-diffs
Description: Returns the latest permission changes
Usage:
output-permissions
Description: Returns the latest errors detected
Usage:
output-errors
Description: Produces a summary of errors, diffs, and permission outputs
Usage:
summary
Description: Returns a directed graph (in DOT format) showing relations among groups, functions, and nodes
Usage:
digraph-host
Description: Returns a directed graph (in DOT format) of relations between SSH IDs and hosts
Usage:
digraph-ssh-id-hosts
Description: Retunrs a report page with the
content of README.md
in HTML format
Usage:
html
Description: 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.sh
unbound-adblock.sh
http-ban.sh
The 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.