Primer is a flexible OS initialisation automator in pure POSIX shell. The main goal is to bring up barebone minimum OS installations to a minimal set of features for running containerised applications. Primer automates a number of installation steps, each of which aiming at the installation of an OS level feature with minimum dependencies. New steps can easily be written if necessary, as they interact with primer using a well-defined interface. As such, primer can also be used in other domains than cloud first applications. Being written in POSIX compatible shell makes it suitable within the embedded space, for example.
The main target OSes of primer are minimal linux OSes such as Alpine Linux, Ubuntu cloud or server images or Clear Linux*. Primer is probably best fitted for initialising "on the metal", rather than VMs where cloud-init often fills such a role.
Primer will prep your system automatically with configuration descriptions that can be put under revision control. However, primer has no support for dependencies between the various steps, nor has a DSL: it rather express machine configuration through a set of environment variables. Primer tries to fill the gap between home-made initialisation scripts and larger solutions such as Ansible, puppet or cloud-init.
Note: Primer is still under development and a little bit of a moving target. But it has reached enough stability to start being used on real (or virtual) hosts, rather that test containers. Please raise issues, or even better, fix bugs through PRs.
Primer is perhaps best explained through an example, scroll past this section for documentation. As you probably do not want to pollute your host system just for testing primer, the example is a little bit more convoluted than necessary to confine primer to a Docker container and not harm the main host. Let's simulate the installation of the Docker daemon and client on an Alpine host by running primer in an Alpine container. To do this, run the following command from the main directory of the project:
docker run \
-it \
--rm \
-v $(pwd):/primer:ro \
--entrypoint /primer/primer \
alpine \
-s "docker"
This works by creating a transient alpine
Docker container, in which the main
directory of the project is bind-mounted onto the /primer
directory in the
container (the -v
option to docker). In that container, we make primer
the
entrypoint (given the bind-mount, primer
is located at /primer/primer
in the
container). All options following the name of the image alpine
are then passed
to primer
. In this example, we specify a single step, called docker
. Its
implementation is part of the default set of
steps.
The command should output something similar to the following. In your terminal, this is most likely going to be pretty printed with a few colours instead:
[20200428-200338] [primer] [notice] Installing steps: docker
[20200428-200338] [primer] [ info ] Loading docker implementation from /primer/libexec/steps/docker.sh
[20200428-200338] [primer] [ info ] Primer step install>>> docker
[20200428-200338] [primer] [ info ] Installing packages: docker
[20200428-200338] [primer] [ info ] Updating OS package indices (if relevant)
[20200428-200354] [primer] [ info ] Starting Docker daemon
[20200428-200354] [primer] [notice] Service docker start is not relevant in a container
[20200428-200354] [primer] [ info ] Enabling docker daemon at start
[20200428-200354] [primer] [notice] Service docker enable is not relevant in a container
Primer finds out the implementation of the step called docker
at
/primer/libexec/steps/docker.sh
and start requesting the implementation to
install. The implementation at docker.sh
is loaded into the main process on
demand. It needs to have a function called with the same name, prefixed with
primer_step_
, i.e. primer_step_docker
. The function might be called a few
times under the installation procedure. By default, the current implementation
installs the Docker daemon and client, and arranges for the daemon to start
right now and autostart with the OS. This is the only feature that actually do
not work when running in a container. Finally, the implementation makes sure
that the user calling the script, if not root
is made part of the group
docker
so that it will be able to call the client with elevating privileges.
All other regular users in the system can also be made part of that group if
necessary. This is a security risk, but is optional and relevant when operating
on unmanned servers.
As the container automatically dies and disappear once primer
has finished, it
is hard to verify what happened. You can however start the installation of
another step called forever
after the step called docker
.
forever
is a dummy step that will sleep forever when requested to
be installed. It is implemented here. Modify the
command example above by giving the following option to primer
:
-s "docker forever"
primer
will executes both steps in sequence and forever
will have the side
effect that the container remains up and running, doing mostly nothing. You
should then be able to docker exec -it
into it from another terminal and poke
around.
The docker
installation step can do much more. It is for example able to
register at a Docker registry for one (the caller) or some seleted users, and
would install Docker directly using the official installation
script on other distributions. The script, as it is downloaded
from the Internet, will be checked for integrity and on Debian derivatives the
GPG signature of the official Docker repository that it adds to the system will
be validated. Provided you have an account at gitlab.com, the following would
install Docker in an ubuntu container instead, and login the main user (again
root
in the case of a container) at gitlab (you will have to provide your
credentials!). Note the default is to verify the commit signature of the Docker
installation script against the one hard-coded into the docker.sh
implementation. If the official Docker script changes, the docker
step in
primer
will fail.
docker run \
-it \
--rm \
-v $(pwd):/primer:ro \
--entrypoint /primer/primer \
ubuntu \
-s "docker" \
--docker:registry youruser:[email protected]
In the command above, note the specially formatted option --docker:registry
.
This option is parsed by primer so that what appears before the separating :
(colon sign) is the name of a step to be looked up (in our case, the same step
as above, i.e. docker
). The remaining forms an option that will be blindly
passed to the primer_step_docker
function in the implementation. So, in this
case, the function called primer_step_docker
, implemented as part of
docker.sh
will be called as follows. Note that registry
automatically became
a double-dashed option --registry
.
primer_step_docker option --registry youruser:[email protected]
Most steps can be communicated with this way. Modifying their behaviour can also
be done through setting environment variables that start with the same name as
the step, but in uppercase, i.e. the docker
step can be controlled by a series
of environment variables that are called PRIMER_STEP_DOCKER_
. The value of
command line options have precedence over these variables.
To finish your exploration of the possibilities of primer, run the following
command instead. It creates a few users on the system, they will be made part of
the docker
group automatically as part of the docker
step. In this example,
primer takes the list of steps to perform from the PRIMER_STEPS
environment
variable instead.
PRIMER_STEPS="users docker" \
docker run \
-it \
--rm \
-e PRIMER_STEPS \
-v $(pwd):/primer:ro \
--entrypoint /primer/primer \
ubuntu \
--users:db /primer/spec/support/data/users.db \
--docker:access ".*"
Primer sports a number of options to control its behaviour, but also recognises
environment variables constructed the same way, i.e. all starting with
PRIMER_
. Of interest might be the option called --config
which should point
to a .env
formatted file. By default, and if it exists, primer will read the
file called primer.env
from the current directory when it is called. As this
file can contain any number of environment variables, including PRIMER_STEPS
to control which steps to execute and in which order, the file forms a
"contract" describing what to install on a particular host. Placing relevant
.env
formatted files under revision control, combined with the ability for
primer not only to install, but also clean a system, makes it possible to
operate on existing OS installations in a reproducible way.
At the time of writing, primer already have the following steps implemented. This list will grow with personal needs, or with the help of the community. PRs are welcome! Some inspiration can be taken from the config implementation in cloud-init.
packages
upgrades the system to the latest and installs additional packages.users
takes a/etc/passwd
inspired colon separated file to create a number of users on the host system. Relevant groups will be created and all users can be made members of additional groups (e.g.sudo
?). The module is able to generate strong passwords for all these users if necessary. This module is also able to fetch public keys from GitHub handles to automatically allow for SSH access.git
installs git on the system, with LFS support.dynufw
installs ufw and a simplification wrapper on top of ufw. The wrapper is able to track host names that would change their pointed IP with time (dynamic DNS).timezone
places the host at a given location.sshkeys
automatically generates strong SSH keys for the calling user. The target of this module are deploy keys when interacting with automated CI systems.docker
installs the Docker daemon and client, it has been described above.compose
installs Docker compose at the latest or a specific version, together with bash completion. The binary integrity can be verified before installation.machine
installs Docker machine at the latest or a specific version, together with bash completion. The binary integrity can be verified before installation.machinery
installs machinery from a given branch (defaults tomaster
).lazydocker
installs lazydocker at the latest or a specific version. Lazydocker is a CUI for Docker. Binary integrity is verified before installation.micro
installs micro at the latest or a specific version. micro is a modern editor for the terminal.github
installs binaries from project releases (assets) at github.disk
formats and mounts attached disks.install
installs (recursively) remote or local files onto the host.install
benefits from the--curl
option for authenticated access to remote resources, when necessary.freeform
looks up and executes binaries in alphabetical order to perform free-form installation/removal operations.
Primer recognises both single-dash led short options and double-dash led long options. Long options can be separated from their values using both a space or an equal sign. After all options come a command instructing Primer what to do, the default being to perform installation of all steps, no questions asked. As an example of setting options, all styles, consider the following invocations that are all equivalent and set verbosity to debug before printing the help:
./primer -v debug help
./primer --verbose debug help
./primer --verbose=debug help
Colon separated list of directories where to look for the implementation of
steps. The default is the steps
directory in the libexec
directory. Note
that when amalgamated, the default is empty as all standard steps
are already part of the amalgamation.
Space separated list of paths to configuration files to read in order, in .env
format. These files should contain a number of environment variables to specify
the steps (and their order) that primer will perform, together with variables to
change the options of these steps. In those files, all empty lines or lines
starting with a #
(hash mark) will be ignored. Variables should be separated
from their values by an equal sign =
and values should not be quoted. The
default is to substitute the value of existing (or set through the
file) variables by their value. Substitution uses an almost eval-safe
implementation for security and supports all POSIX expansion formats.
The default for this option is to read:
- The file
primer.env
in the current working directory, if present. - The file
primer_<mac>.env
in the current working directory, if present. It will be read on "top" of the previous file. In that filename, at run time,<mac>
will be the MAC address of the main Ethernet interface of the host, in lower-case and without any field separators.
Primer is not only able to read files, but also remote resources. To access
protected resources, you can combine with using the --curl
option.
In path or resource specifications passed to --config
, a number of tokens will
automatically be replaced by their value at run time. These are:
%mac%
will be replaced with the MAC address of the host, in lower case, without any separator signs.%host%
and%hostname%
will be replaced with the hostname of the host, as reported by the commandhostname
.
These defaults and tokens allow to use common primer settings across several
(similar) host installations and in networked settings. Generic options for all
hosts would be placed in the main primer.env
file, while host-specific options
would be present in the resources using the %mac%
token. To discover the MAC
address that primer believes is the one of the main Ethernet interface, you can
run the following command:
./primer -v notice env|grep -E '\s+mac:'|awk '{print $2}'|tr -d ':'
When this flag is present, references to environment variables in .env
files
will not be expanded to their value at run time.
The value of this option should be the space separated list of steps to
consider. When installing or cleaning, steps will be run in that order, and the
value of this option would override any value that would come from an
environment variable, e.g. read as a .env
file.
The value of this option is the path to a file matching URLs to their curl
options. In this file, blank lines and lines starting with a #
(hash mark)
will be ignored. Otherwise, the first item of the line should be an extended
regular expression to match against a URL, and the remaining options that will
be blindly passed to curl
when it is used to download URLs matching the
expression. The expression itself can be URL encoded to arrange for all special
characters, including possible spaces. This feature can be used, for example,
for debugging purposes, or to provide authentication details to curl
to access
URLs.
Any long option that looks like the above, where <step>
is the name of an
existing step, and <opt>
is one of the options supported by the implementation
of the step will be passed further as --<opt>
to the main function in the
implementation of the step. These values have precedence over any value that
would have been set through environment variables, e.g. read through an
.env
file.
Change verbosity. Available levels are, from most to least verbose, and as of the yu.sh implementation:
trace
debug
info
(default)notice
warn
error
Print help and exit.
Apart from the options, Primer takes a trailing command. The trailing command
blindly defaults to install
, meaning that the default behaviour will be to
install as specified per the --steps
command-line option or PRIMER_STEPS
environment variable. Primer accepts a number of aliases to those commands:
install
,up
: This is the default command, it will perform all the installation steps specified through thePRIMER_STEPS
environment variable or the--steps
option.clean
,remove
,down
: Uninstall the steps specified as for theinstall
command.env
,environment
: List all known and accessible steps, together with the options and environment variables that can be used to modify their behaviour. This also prints out host information, e.g. hostname, MAC address (of the primary ethernet interface), name of distribution, version, etc.info
: List all options and environment variables that can be used to modify the behaviour of the steps to be run. The list of steps can be specified as for theinstall
commandhelp
: Print help and exit.
Primer recognises a number of environment variables, some of which associated to
its command-line options. Command-line options always have
precedence over the value coming from environment variables. All variables start
with PRIMER_
. They are:
PRIMER_PATH
is the same as the--path
option.PRIMER_EXTS
is a space-separated list of dot-led extensions to add to the name of the step when looking for their implementations withinPRIMER_PATH
. It defaults to.sh
.PRIMER_STEPS
is the same as the--steps
option.PRIMER_CONFIG
is the same as the--config
option.PRIMER_LOCAL
is the root directory to use as/usr/local
when manuall installing contrib packages of various sorts. It defaults to/usr/local
.PRIMER_BINDIR
is thebin
directory underPRIMER_LOCAL
.PRIMER_LIBDIR
is thelib
directory underPRIMER_LOCAL
.PRIMER_OPTDIR
is theopt
directory underPRIMER_LOCAL
.PRIMER_CURL_OPTIONS
is the same as the--curl
option.
In addition, steps will also recognise environment variables, these all start
with PRIMER_STEP_
. More details are available in the conventions document.
The current implementation depends on a number of internal modules, on the utility library yu.sh and a predefined set of steps, even though it is possible to create and integrate more steps. To make it easier to ship the script to "raw" servers, primer supports amalgamation. To create a single binary that can easily be copied to a target machine, run the following commands from the root directory of the project:
./libexec/yu.sh/bin/amalgamation.sh primer > primer.sh
chmod a+x ./primer.sh
Primer comes with a test suite, based on shellspec. Provided you have
installed shellspec and it is in your path, run the following command from the
main directory of the project to run the test suite. The test suite requires
that you have docker installed on your machine and that your user is able to
call the docker
client without elevated privileges. Running inside docker
allows to perform OS-wide modifications in the context of containers so as to
not pollute your main system.
shellspec
You are more than welcome to contribute through PRs. New steps will not be accepted without minimal test rules or documentation. There are a few coding conventions to follow for all contributors.
The original idea for this utility comes from a number of projects at Yanzi, including some of the code. The code has since then, undergone large modifications up to a point where little of the original is left.
Most of the design, refactoring and rewriting has been done on my free-time. A few enhancements have been sponsored by Lindborg Systems AB.