tori
tori is a tool to track system configurations and replicate them.
For an overview, see the discover page. If you’d like a more detailed description of what tori is, its purpose, origins and goals, see the announcement blog post.
For updates and access to source code, visit the project website .
Installation
First of all, check if your operating system is supported.
tori is still in very early development and not yet packaged. To try it, clone its repository to your system:
git clone https://brew.bsd.cafe/jutty/tori.git /path/to/repository
Next, create ~/.config/tori/tori.conf
with the following content:
tori_root = /path/to/repository
If you clone it to the default location, ~/.local/share/tori
, the above step is not necessary.
Finally, you need to symlink the tori
file at the repository root to somewhere on your $PATH
:
ln -s /path/to/repository/tori $HOME/.local/bin/tori
Usage
You are now ready to start creating your configuration in ~/.config/tori
. You could start by creating a packages
file there with the packages you expect to have installed.
When you want to compare the configuration with your actual system, you can use tori check
.
If you did that at this point, with only a few packages in your ~/.config/tori/packages
file, tori would likely give you a long list of all the other packages you have manually installed but have not added to this package list. Among the options on how to proceed, you could now “Add all to configuration”.
Then, if you run tori check
again it should exit immediately, as all the packages match.
To learn more about all the ways you can declare your system using the configuration directory, see the documentation page on configuration.
Currently, the following commands are implemented:
check
: check for divergences between the configuration and the systemcache
: force an update of the local package cachehelp
: show a usage summary with supported optionsversion
: print the current version and its release date
To issue a command, use tori <command>
, as in tori check
.
Configuration
tori looks for configuration in ~/.config/tori
.
In this directory, you can create the following files and directories:
tori.conf
: configures options that will determine how tori itself behavespackages
: a list of packages containing one package name per linebase
: a directory containing any number of files that tori will copy to specific locations
Configuration options
The tori.conf
file must use the following format:
tori_root = ~/.local/apps/my-tori-installation
For a complete reference on this file, including its syntax and all available options, see the tori.conf
documentation page.
Package lists
tori will read the packages
file and check if it matches the list of currently installed packages. If it does not match, it by default will ask you how to proceed.
The application only concerns itself with manually installed packages. Any dependencies pulled automatically should not be picked up by tori unless you have manually installed that dependency or marked it as such.
This is ensured by specifically querying your package manager for a list of manually installed packages, so tori should actually not even be aware of which automatically-installed packages you have on your system.
In your packages
file, empty lines and lines beginning with a #
are ignored.
Base files
tori will go through the contents of the ~/.config/tori/base
directory and take different actions depending on how they are laid out.
file_strategy = spare
The default value is tree
. Currently there is just one other valid value, spare
.
Tree strategy
The default way tori compares files is called the tree strategy. It expects you to have top-level directories inside base
that match the directories starting from your system’s root (for example, a base/home
directory).
tori will recursively inspect this directory for files and compare them to the contents of the matching directories in the system.
For example, if you have the following configuration:
~/.config/tori
└── base
└── home
└── alice
└── .shrc
Upon running tori check
, tori will compare your configuration’s .shrc
file at ~/.config/tori/base/home/alice/.shrc
to /home/alice/.shrc
.
If any of the files in your configuration do not exist on the corresponding location, tori will copy them to it. If they do exist and match, it won’t do anything. And if they differ from their counterparts, by default tori will ask you how to proceed.
Differs: ~/.shrc
[1] Overwrite system
[2] Overwrite configuration
[3] Show difference
[0] Exit
Choose an option number:
In the example above, ~/.config/tori/base/home/alice/.shrc
and /home/alice/.shrc
have differed, so the user is presented with choices on how to conciliate them.
If you decide to do so, bear in mind important files may get overwritten without warning.
If you would rather not replicate the system directory hierarchy in your base
directory (for instance, if you don’t like having to navigate complicated directory structures), you can also place spare files in the base
directory in any other structure you desire.
In this case, you will have to create a files
file on the root of your configuration directory to manually specify what are the files you want them to be matched against.
To accomplish the same as the previous example with the spare strategy, you could have the following file structure:
~/.config/tori
├── base
│ └── .shrc
└── files
In the files
file, you then would specify what system file the .shrc
file in base should match using a <base file> <system file>
format.
.shrc ~/.shrc
If either of these paths contain spaces, make sure to double quote them.
tori.conf
The tori.conf
file is found at the root of the configuration directory, which defaults to ~/.config/tori
.
Each line must use the following format:
tori_root = ~/.local/share/tori
file_strategy = tree
The values in the example above are the default ones, meaning if you do not specify these options they are what will be considered their values.
Options
The following configuration options can be used to specify how you want tori to behave.
Application behavior
tori_root
: The directory where you installed tori. This is useful when you installed tori by simply cloning its repository. It should point to the directory where theREADME.md
and thetori
files are.~/.local/share/tori
(default)<path>
: Any valid filesystem path. See Syntax for possible home directory substitutions.
File management
file_strategy
: What file structure should be used when merging the files from your~/.config/tori/base
directories. For a more detailed explanation of what each strategy does, see the configuration page.tree
(default): mirror the structure of the filesystem, withbase
being the equivalent of its root (/
)
Syntax
Configuration options are case insensitive. The spaces around the =
character are optional. Blank lines and lines beginning with a #
are ignored.
You can use ~
and $HOME
to represent your home directory. $HOME
will be replaced in every occasion with your home directory, while ~
will only be replaced for the first occurrence in the configuration value.
Values containing spaces must be wrapped in double quotes.
Configuration values must not contain the character *
.
check
When you run tori check
, tori will look into your configuration files and perform two tasks:
- Compare the files inside the
base
directory to the corresponding files on the system - Compare the package list in the
packages
file to the installed packages
If any divergence is found, it will by default prompt you on what action to take.
cache
To know if package names are valid, tori will ask your package manager for a list of all available packages and store it in ~/.cache/tori
.
By default, this cache will be refreshed daily. This happens before the first operation that uses the cache is triggered in a day.
If you would like to manually refresh this cache, you can use the tori cache
command.
When this happens, you might see a password prompt from your authentication tool (such as sudo
or doas
) with the message “Updating package cache”.
Package conflict resolution
When running tori check
, tori will generate two package lists: one with your installed packages and one from the packages
file in your configuration directory.
In case it finds differences between the two lists, its default behavior is to ask you what to do. This approach is described in the following section.
Interactive resolution
If it finds a difference between the two package lists, tori will present you with a list of packages and a menu with several conflict resolution strategies:
[tori] 16:38:19: System and configuration packages differ
Packages on configuration but not installed: bastille fend ksh xh
[1] Install all
[2] Enter packages to install
[3] Remove all from configuration
[4] Enter packages to remove from configuration
[5] Decide on editor
[6] Cancel
Choose an option [1-6]:
tori will perform two separate checks that may produce two separate dialogues like the one above. It checks for packages found in the configuration that are missing from the system and for packages found in the system but missing from the configuration.
If it finds packages in your configuration that are not installed on the system, it will present you with the following choices:
- Install all: The exact list of packages just displayed is passed to the package manager’s install option. The package manager may ask for additional confirmation.
- Enter packages to install: Allows you to provide a space-separated list of packages to install. These packages will be checked for invalid characters and invalid package names. If there is an issue, you will be prompted again.
- Remove all from configuration: Removes all packages from your configuration file.
- Enter packages to remove from configuration: Allows you to provide a space-separated list of packages to remove from the configuration file.
- Decide on editor: Opens your editor and allows you to choose individually what actions to take for each package.
- Cancel: Do nothing and close the dialog
If it finds packages installed on the system but not listed in your configuration, it will present you with the following choices:
- Uninstall all: The exact list of packages just displayed is passed to the package manager’s uninstall option. The package manager may ask for additional confirmation.
- Enter packages to uninstall: Allows you to provide a space-separated list of packages to uninstall. These packages will be checked for invalid characters and invalid package names. If there is an issue, you will be prompted again.
- Add all to configuration: Adds all packages to your configuration file.
- Enter packages to add to configuration: Allows you to provide a space-separated list of packages to add to the configuration file.
- Decide on editor: Opens your editor and allows you to choose individually what actions to take for each package.
- Cancel: Do nothing and close the dialog
Decide on editor
If you choose to decide what to do in your editor, the editor set on your $EDITOR
environment variable will be launched with the following text:
# Options:
# [i]nstall Install package to system
# [r]emove Remove from configuration
# [s]kip Do not take any action
# Providing just the value between brackets is sufficient
# Replace 'skip' below with the desired option
skip bastille
skip fend
skip ksh
skip xh
This example shows packages on configuration but not installed. The inverse set of operations are displayed on top if you have installed packages that are not in your configuration:
# Options:
# [u]ninstall Uninstall package from system
# [a]dd Add to configuration
# [s]kip Do not take any action
# Providing just the value between brackets is sufficient
# Replace 'skip' below with the desired option
skip bastille
skip fend
skip ksh
skip xh
Edit the skip
option on each package to decide what action to take on them:
# Options:
# [u]ninstall Uninstall package from system
# [a]dd Add to configuration
# [s]kip Do not take any action
# Providing just the value between brackets is sufficient
# Replace 'skip' below with the desired option
uninstall bastille
u fend
a ksh
skip xh
In the example above, bastille
and fend
would be uninstalled, ksh
would be added to the configuration and no action would be taken for xh
.
Backups
tori will create backup copies of files before overwriting them.
This includes tori’s own configuration directory, meaning that when you overwrite your own configuration files through tori’s dialogs or non-interactive options, it will also backup your configuration files before doing so.
There are two types of backups: canonical and ephemeral. Each lives inside a separate directory in the backup directory, which defaults to ~/.local/state/tori/backup
.
Both directories, as well as the backup
directory, may be deleted at will. They will be automatically recreated whenever an overwrite operation takes place.
Canonical backups
Canonical backups are created the first time tori is told to modify a given file.
These are meant to represent the state of the file system prior to tori’s intervention. If tori is running on a recently-installed system, the canonical backups should mirror the original state of the system as of its installation.
If there is already a canonical backup, it instead proceeds to the creation of an ephemeral one.
Ephemeral backups
Ephemeral backups are timestamped backups created every time tori has to modify a file. This is mainly meant as a safety net against undesired consequences when using non-interactive options.
If an ephemeral backup is being requested when there is already a matching timestamp, meaning they were requested within the same second, the older one will be overwritten.
Directory structure
tori creates a backup directory in ~/.local/state/tori/backup
that contains two other directories:
canonical
ephemeral
Both of them will mirror the directory structure from the root of the file system, as shown in the example below.
~/.local/state/tori
└── backup
├── canonical
│ ├── etc
│ │ ├── pf.conf
│ │ └── rc.conf
│ └── home
│ └── alice
│ └── .shrc
└── ephemeral
├── etc
│ ├── pf.conf_2024-09-03T08-54-51
│ └── pf.conf_2024-09-03T09-02-05
└── home
└── alice
└── .shrc_2024-09-03T08-46-54
In this case, the rc.conf
file was overwritten once, creating a canonical backup corresponding to the first time tori touched it. pf.conf
, on the other hand, was overwritten three times and .shrc
twice, which is why there is a canonical copy for the first change and other timestamped ephemeral copies for subsequent ones.
Backlog
This document is meant as a list of planned changes. While their inclusion here means their implementation is desirable, the timing will depend on other priorities that could be unlisted.
Configuration
- Allow imperative configuration commands through a parsed file containing instructions
Package management
- When deciding to install/uninstall packages, there should be the option to also add/remove them from the configuration, both interactively and non-interactively
-
Add install, uninstall and query commands to check the status of packages and simultaneously add/remove to/from system and configuration
- Once install/uninstall and add/remove to/from configuration conflict resolution strategies have been implemented, the same functions can be reused to implement this
File management
-
Allow merging spare files on
~/.config/tori/base
by specifying where they should be copied to
Metaconfiguration
-
Make configurable:
- authorizer command (sudo/doas/su)
- package cache update frequency (currently defaults to daily)
-
Configurable hostname discernment:
-
extension, as in
packages.hostname
,base.hostname
base/file.hostname
,hostname.conf
-
directory, as in
hostname/packages
,hostname/base
,hostname.conf
-
extension, as in
Utilities
-
Add a
--break
flag to thelog
function to print a newline before any output-
Add this flag to the
log debug "user:\n$user_packages"
call onscan_packages()
inconfiguration.sh
-
Add this flag to the
-
set_opts()
would be more ergonomic if it tookon
andoff
instead of-
and+
Portability
tori
’s role is to manage configuration files and installed packages, allowing the transfer of this configuration between different versions of an operating system or even between different Unix and Unix-like operating systems, provided they are supported.
Given this purpose, it ideally should not require the user to install even a single package in order to run. It must be a portable application with minimal dependencies so it can perform its functions on brand new systems where very few packages have been installed and little to no configuration has been done.
To achieve this portability and independence, it is meant to run on a POSIX-compatible shell where POSIX utilities are available. If a system does not provide this, it is very unlikely tori
can function.
Note that while tori
expects a POSIX shell, it is not meant as a universal tool able to run on any POSIX-compliant system. A POSIX shell is required because it is the interpreter for tori’s source code and it was chosen because it comes pre-installed on most Unix-like systems.
For some of its functionalities, tori
needs to be running in a supported operating system. It has specific package management and service management features that work by abstracting the actual operating system’s interfaces behind a function that detects what system is running and then runs the appropriate commands.
The currently supported operating systems are tracked in the Support Matrix page.
While it strives to do so, in some situations, tori may perform tasks by relying on resources or features not specified by POSIX, such as when there is no option or the available option presents readability or usability issues.
In these situations, tori tends to rely on specific functions that will switch their behavior depending on the operating system’s support for the operation. Regardless, all of its features are expected to work under supported operating systems.
Below is a list of assumptions made about what is supported in the running environment:
- shell
local
read
withread -r -p <prompt> <variable_with_user_input>
syntaxmkdir
with-p
find
grep
sed
xargs
uname
date
- with nanoseconds as
%N
- While nanoseconds in
date
is not specified by POSIX, it is available on the currently supported systems and is used only when$DEBUG
is set in the environment
- While nanoseconds in
- with
-r
for getting a modification date- This feature is not specified by POSIX. So far it was tested on FreeBSD and Void Linux
- with nanoseconds as
env
at/usr/bin/env
- While this may be an issue from a portability standpoint, hardcoding the path where
sh
is also poses another portability issue. A more robust way to find it would be desirable
- While this may be an issue from a portability standpoint, hardcoding the path where
The POSIX specification used as reference is the IEEE Std 1003.1-2024 Edition.
tori is developed and tested on the sh
Almquist shell as shipped by FreeBSD and the dash
Almquist shell as shipped by Void Linux.
Utilities
The utility functions available at src/utility.sh
provide functionality that is very simple and sometimes generally used across the whole application.
log
log <level> <message>
The log
function takes a log level between 1 and 5 as its first argument and a message as its second argument. The log message must be wrapped in double quotes, otherwise only the first word will be considered part of the message and the rest will be discarded.
If the environment variable DEBUG
is set to a number corresponding to the provided log level or greater, the message is printed.
The current log levels are:
user
: Always displays, with[tori]
at the very left followed by a second-precision timestamp, a colon and the message. This is meant for user messages that are always relevant. This level is not affected by the value of theDEBUG
environment variablefatal
: For critical, unrecoverable or unsupported situations. The application must immediately exit after printing the message. Displays when the log level is 1 or higher with a second-precision timestamp.error
: For unexpected conditions where potential undesired behavior, loss of user data or other unintended state may happen or has already happened. The application may exit immediately after printing the message. Displays when the log level is 2 or higher with a second-precision timestamp.warn
: For expected conditions already handled by the application that may potentially be unintended behavior, depending on the user’s intentions. A warning should not precede the application exiting. Displays when the log level is 3 or higher with a second-precision timestamp. This is the default whenDEBUG
is not set or is set to a non-numerical value.info
: For messages that do not represent errors or unintended situations, but provide more insight into the current state or behavior. This level is for development purposes. Displays when the log level is 4 or higher with a nanosecond-precision timestamp.debug
For messages that express any change in state or behavior, typically to help understand the application’s flow of execution. This level is for development purposes. Displays when the log level is 5 or higher with a nanosecond-precision timestamp. Levels higher than 5 are not valid, but are brought down to 5.
The log level defaults to 3 (warn). To completely disable log messages (except for the user
level), set DEBUG=0
.
If DEBUG
is set to a value greater than 5, it will be reset to 5. If it is set to any non-numerical value, it will be set to the default, 3.
All log messages are printed to STDERR
so as not to shadow function return values.
set_opts
set_opts <on|off>
The set_opts
function checks if the shell options are supported and sets them. If they are not supported, it considers the shell unsupported. This causes it to print a fatal error message and return exit code 1.
This function is called with the on
argument in the very early stages after the application just started in order to enable these options for the remainder of execution. It may then be used again to perform operations that require these options to be unset.
For example:
set_opts off # disable options
echo "$potentially_unset_variable"
set_opts on # re-enable options
It modifies the following options:
errexit
: Exit after any non-zero return1nounset
: Exit when trying to expand an unset variable
To enable them, use set_opts on
. To disable them, use set_opts off
.
print_help
print_help
The print_help
function is responsible for printing help text when the user runs tori help
, tori -h
or tori --help
. It takes no arguments.
juno@akuma:~/tori $ tori -h
tori: configuration managent and system replication tool
Options:
check compare configuration to system state
cache refresh the local package cache
version print current version with release date
help show this help text
See 'man tori' or https://tori.jutty.dev/docs for more help
The help text contains a brief description of what tori is, what commands are available and where to find more help via the man page or this documentation website.
prepare_directories
prepare_directories
The prepare_directories
function runs at the very start of execution in order to verify that critical directories exist. It takes no arguments.
The directories are:
CONFIG_ROOT
, default~/.config/tori
: application exits with a fatal error and exit code 1 if not foundTMP_ROOT
, default/tmp/tori
: created if not foundCACHE_ROOT
, default~/.cache/tori
: created if not foundBACKUP_ROOT
, default~/.local/state/tori/backup
: created if not found- with subdirectories
canonical
andephemeral
- with subdirectories
ask
ask <prompt> <option>[,option2,option3[, ...]]
The ask
function will prompt the user for one choice among the comma-separated list of alternatives.
It will also include an “Exit” option that does not need to be provided in the list of alternatives.
This function will print a number to standard output corresponding to the chosen option starting from 1, and 0 if the “Exit” option is chosen.
tildify
tildify <path>
The tildify
function takes a file path and replaces the first occurrence of the user’s home directory with a ~
(tilde) character. The home directory string to replace is determined by reading the value of the presently set $HOME
environment variable.
While this description is generally accurate, the shell manuals tend to describe it more specifically as “if any untested command fails”. What is meant by “untested” may vary between different shell implementations. Therefore, it should be tested whether a given instruction’s failure is interrupting execution or not whenever this is intended.
tori determines what is the running operating system through the /etc/os-release
file and the output of uname -s
.
If an /etc/os-release
file is present, it takes precedence over the output of uname
. Both the NAME
and ID
values will be looked at. This is aimed at helping to disambiguate between different variants of the same operating system.
The NAME
value may be the only queried value if for the given supported operating system it is enough to disambiguate between the variants tori needs to be aware of.
In case there is no /etc/os-release
file found, the output of uname
is the next value considered.
If a supported operating system is not detected on neither of these, tori will exit with an error.
check
The check
option performs two tasks through the configuration processing functions available at src/configuration.sh
- Traversing the configuration’s
base
directory to assemble a file list containing the equivalent full paths starting from the actual filesystem’s root (/
), checking if these paths correspond to actual files and if they differ from their counterparts in thebase
directory - Comparing the installed packages with the
packages
file at the root of the configuration directory
The first task is currently accomplished by resorting to find
. While this allows for cleaner code, it relies on a utility with variable behavior across operating systems. Given the simplicity of the query, a more portable yet less readable option might be using a POSIX-compliant wildcard such as .[!.]* ..?* *
to match the files directly (e.g., in a for loop). Another option that may provide both readability and portability is repeating the match, once for hidden file and once for non-hidden files.
The second task is accomplished by resorting to the package management functions available at src/package.sh
. The package_manager
function abstracts the actual package manager in the underlying system and provides an OS-independent way to query manually installed packages.
By parsing the packages
configuration file at the root of the configuration directory (~/.config/tori/packages
by default), a final package list is obtained. The package manager is also queried for its list of manually installed packages. Both lists are then sorted and deduplicated before they can be filtered by each other using grep
inverted matching.
This allows obtaining two-way differences and displaying them to the user. If no conflict resolution strategy has been configured or passed through the command line interface, several options are given:
- Install/uninstall all
- Enter packages to install/uninstall
- Add all to/remove all from configuration
- Enter packages to add to/remove from configuration
- Decide in editor
cache
To determine if a package really exists, there must be a quick way to query a list of all available packages that does not mean individually making requests with each package name.
To solve this, tori will ask the package manager for a list of all available packages and cache it. It then uses this file to determine if package names are valid.
By default, the cache is updated when the current date (day, month and year) differs from the date of modification of the cache file. Note that this is not the same as 24 hours after the last refresh.
The user may configure a different frequency and can also force a cache refresh through the command line interface using tori cache
.
User data
User data is organized in two main categories: configuration
and backups
.
Configuration
The configuration directory defaults to ~/.config/tori
. See the usage documentation for what are its contents.
tori
will look only for regular files inside the configuration directory and currently will ignore symbolic link and any other filetype when scanning it.
Backups
The backups directory defaults to ~/.local/state/tori/backup
. It contains copies of files made prior to tori modifying them. Its structure is also documented in the usage documentation, on the backups page.
This directory is not essential to the functioning of tori
. It is provided as a safety net for the user and may be deleted at any time. It will be automatically recreated.
User files
tori
can track if a given file in the configuration directory is present and matches the content of the corresponding file on the system level.
However, if a file changed on the system level is not in the configuration directory, tori
can only alert users of that if the operating system provides some way to compare the present state of the system to the original one prior to user intervention.
One example would be FreeBSD’s intrusion detection system, which provides a way to know which files have been changed.
Another way would be by relying on a file system that provides the ability to compare differences between snapshots, such as ZFS.
A third way would be if the operating system’s package manager provides a command line interface to read the contents of packages and has packages that correspond to the core system, such as Void Linux’s package manager, xbps
.
The resource-intensiveness of each of these methods will vary greatly and therefore checking the whole system for changes can be time-consuming or provide overwhelming output.
For this reason, tori
by default operates in a more minimal fashion where users take responsibility for adding the files they would like to track to their configuration and only get warnings of untracked files when explicitly asked.
User packages
In order to take advantage of all the features offered by tori
for package management, the operating system’s package manager must provide a way to:
- install and uninstall packages from a command-line interface
- query manually installed packages
For information on how the application determines differences between the configuration package list and installed packages, see check
.
validate_input_packages
validate_input_packages <package>[ <package>[ ...]]
This function takes a list of package names separated by spaces and verifies that:
- It only contains valid characters
- Each value is a valid package name
Character validation
An OS-specific pattern will be matched against the package name to decide which characters are valid or invalid by the naming standards its package repository uses.
For example, if the OS is FreeBSD, it will set the following pattern:
invalid_characters_pattern='[^A-Za-z0-9\+_\.-]'
If no OS-specific documentation is found on what are the allowed characters in package names, the obtained package list containing all available packages in the operating system’s main repository should be analyzed to determine what is the current range of characters it uses.
The following allowed character groups have been determined so far:
- FreeBSD (
pkg
)- uppercase and lowercase letters
- numbers
- dashes
- underscores
- dots
- plus signs
Package name validation
Package name validation is performed by checking if each package name is found in the package cache. This list is retrieved by the package manager and should represent the full range of valid packages available in the queried repository.
Outcome
If there is any error in validation, the function will return 1
. In the check
command, for instance, this will cause the dialog to prompt the user again for valid package names.
If all package names are found in the cache, the function will have a clean exit with code 0
, allowing the calling function to continue and perform any actual operations on these packages.
backup_paths
This function takes a newline-separated list of absolute paths and creates canonical or ephemeral backups as needed.
Paths are assembled by taking the value of $BACKUP_ROOT
and concatenating to it /canonical
or /ephemeral
followed by the absolute path. For ephemeral backups, a timestamp is also appended to the end of the filename.
Checks are performed for permission to read the files being copied. If permission is not granted to the user running tori, the command set in $AUTHORIZE_COMMAND
will be used to escalate privilege.
Release process
This document specifies the process leading to a version being ready for release.
Overall steps
- Update changelog
- Update documentation and README
- Update the
--version
output - Update the
--help
output - Create an update on the website if appropriate
- Tag the last commit of main and web repositories
- Push tags
- Create a pull request
- Merge
- Update git mirrors
- Create release
- Push web changes
- Announce on relevant channels