potion

Potion is a single application to configure a system, like Puppet or Chef, but minimalist.

Usage

There are two ways to use potion:

  1. You can use potion to run a recipe. A recipe is a script which contains the description of the configuration that you want to apply.
  2. You can use potion to create a standalone autoexecutable recipe, so you don’t need potion to run a potion file.

Using potion to run a recipe

potion run Potionfile Otherrecipe.potion

The recipe is any bash script which can contain a number of specific funtions, listed below in API Reference.

Using potion to create a self-executable recipe

potion create Potionfile > recipe.sh

Using potion to run a local recipe in remote host

potion -q create Potionfile | ssh -tt hostname

Using potion with remote recipes

potion run https://miremotehost/remoterecipe.potion

Or more complex example with ssh and git:

potion run ssh+git://myremoterepohost/mirepo.git

When you point to a repo, a file called init.potion in the root dir of the repo is loaded automatically, as potion starting point.

Using default recipes

export POTION_DEFAULT_SOURCE="ssh+git://myremoterepohost/mirepo.git"
potion run

Using potion to run periodically

Potion does not support daemon mode, but you can use systemd timers or cron jobs to run potion every hour or when you prefer. Please note that potion requires some environment variables like HOME. Is a good practice tu run potion with sudo as unprivileged user, for example:

$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$ sudo -i potion run script.potion

Writing recipes

Potion recipes are, in fact, bash scripts, so any thing that you can create with a bash script could be used in recipe, nevertheless we recommend you to use potion API to more common situations, and create new modules over them. Recipes are evaluated in order, but you parallelize actions using context (read more below).

Contexts

A context is a new process running by potion to perform some task in parallel. You can configure context with the following syntax in the Potion file:

context @foo
{
  # do some actions
}

context @bar
{
  # do other actions
}

In this example we create two context which will be running in parallel in the host. Please note that context must be start with @ character.

Artifacts

An artifact is a set of files which should be included in the potion file and deployed by Potion application. You can think an artifact like a template for file. You can use artifacts like any other file, in the form:

file::present /etc/config.json \
  data="artifact://config.json"

This code will create (if doesn’t exist yet) a fil in /etc/config.json, which content is generated from artifact called config.json. The artifacts directory must be specified by command like when called potion, like the following example:

potion run -a ./artifacts_dir Potionfile

If you generate a self-executable potion file, the artifacts will be included in the script.

Instead of artifact:// url, you can use artifact+eval:// url, which allows you to expand variables inside the artifact file. You can use normal $variable form, or even run more complex like $(if $var; then echo value; fi).

You can also load artifacts programatically from the potion recipe, using artifact::load:

artifact::load ./path/to/artifactsdir

Secrets

Secrets are intented to keep some variables of your config in secret. Potion will add secret variables using module secret, documented in the API. The secret file is just another potion file which contains secret::add commands, which define secret values in plain.

There are three different ways to use secrets:

  1. Keep you secret file out of the recipes, and copy on each host individually. The run potion with -s (--secret) flag pointing to the secret file.

  2. Use symmetric encryption (recommended method). Potion will read a passphrase from /etc/potion.key (or where SECRET_MASTERKEY environment variable defines) and use that passphrase to decrypt the secret file (using AES256 via openssl).

  3. Using asymetric encyption via GnuPG, using different keyfiles for each host. This is the most secure way, but hard to maintain.

Recomended option is to set a plain secret.potion file outside your recipes repository and encrypt it with:

openssl enc -e -a -aes256 -in secret.potion -out secret.potion.aes

And keep secret.potion.aes (encrypted file) under version control system (The -a flag in openssl indicates to encode the file using base64, which is used by potion too). This methid provide a reasonable security and a goo way to manage the secrets from central repository.

Of course, you can load secrets programatically, for example:

secret::load "crypt+file://$PWD/secrets.potion.aes"

This example using openssl encrypt (crypt) method to decrupt secrets file. In API documentation, under module secret you can see different load formats, including plain and gpg ones.

Including other potion files

You can split your config in different modules, and you can load them from the starting potion file (init.potion if you are loading recipes from repository). See the include module to learn in which ways you can include other modules, but in general you will use the following:

include::path ./modules/\*.potion

Testing changes

Before to apply changes, you probably want to test the changes, to do that you can run potion with --pretend flag. If you are running and self-executable potion, you can use the environment variable $PRETEND=true to ensure that the potion just pretending instead on real running.

Bonus: How structure your recipes

There is no unique way to structure your recipes, depends of how complex your configuration is, but in general, this is a good approach:

  1. Put your artifacts under artifacts/ directory
  2. Create modules under modules/ to configure some software (i.e. module nginx). Module should have a ::present, ::exists and ::absent functions.
  3. Create modules under profiles/ to help you to group core actions. For example profile webserver will use module nginx and other to configure a webserver.
  4. Create modules under roles/ to group profiles. For example frontend role will use webserver and others to configure what a frontend server is.
  5. Finally modules under nodes/ and named like FQDN of the node, will include roles that defined those server.

Of course, you can follow any other approach that be convenient for you, this is just a recommendation.


External API Reference

dir

The dir module provides functions to ensure that specified directory is created or destroyed.

Usage example

dir::present "/mnt" mode=0755

dir::present [name=]<name> [options]

ensure that specified directory name passed as argument exists, and create it if not.

Parameters

name
the name of the directory to be created.
mode
the four digit octal mode of the new directory (0755) by default.
user
the user owner the directory (root by default).
group
the group owner the directory (root by default)

dir::absent [name=]<name>

ensure that directory named as arguement, does not exist, removing it if necessary.

Parameters

name
the name of the directory to be removed

dir::exists [name=<fname>]

return true if directory name passed as argument exists, or false otherwise.

file

The file module provides functions to ensure that specified file exists or not in the system and also has specified content. To creat content, use the content parameter, usually this parameter accepts a URL (in the usual curl accepted format or artifact:// to read from static artifact or artifact+eval:// to read from an evaluated artifact.

Usage example

file::present /etc/somefile \
  content="artifact://someartifact"
file::present /etc/otherfile \
  content="some string content"

file::exists [name=]<fname> [options]

return true if file exists, or none otherwise

Parameters

name
the filename to check for.
content
the content URL

file::present [name=]<name> [options]

ensure that file passed as argument exists.

Parameters

name
then file name to ensure
user
the user owner the file
group
the group owner the file
mode
the four digit octal mode of the file (i.e. 0644)
content
the content url

file::absent [name=]<name> [options]

ensure that file passed as argument does not exist.

Parameters

content
the content url (only removes the file if content is the same).

file::lines::present [name=]<name> [lines]

ensure that specified lines (one per argument) are present in the file.

Usage example

file::lines::present "/etc/sudoers" \
   "%wheel ALL=(ALL) NOPASSWD: ALL"
   "root ALL=(ALL) ALL"

file::lines::absent [name=]<name> [lines]

ensure that specifie lines are NOT present in the file.

Usage example

file::lines::absent "/etc/sudoers" \
  "%users ALL=(ALL) NOPASSWD: ALL"

format

The format module provides a way to format and mount specified block devices.

Usage example

format::present /dev/sda1 fstype=xfs

format::exists [name=]<device> [options]

return true if device exists and it’s mounted on mountpoint

Parameters

name
the device name

format::present [name=]<device> [options]

ensure that specified device has format

Parameters

name
the device to be initialized
fstype
the filesystem type to format the device

format::absent [name=]<device>

ensure that specific device is not formatted.

group

The group module offers a way to ensure that specified group of uses is created or not in the system.

Usage example

group::present mygroup

group::exists [name=]<groupname>

return true or false if the specific groupname exists or not, respectively.

group::present [name=]<groupname> system=<true|false> [gid=gid]

create group if not exists

Parameters

name
the name of the group to create.
system
if true create a system group.
gid
the GID of the new group

group::absent [name=]<groupname>

destroy groupname if exists

host

The host module provides functions to configure hostname of the machine.

Usage example

host::present myhost.mydomain.com

host::exists [name=]<hostname>

return true if the FQDN of the node is equal to specified one.

host::present [name=]<hostname>

ensure than the node has the specified hostname.

host::absent [name=]<hostname>

ensure than the node has not the specified hostname.

include

The include module allows you to include other potions from remote sites.

include::path <glob_expr>

include all files matches with glob expression

mount

The mount module provides functions to ensure that specific mountpoint is mounted.

Usage example

mount::present /mnt device=/dev/sda1 fstype=xfs

mount::exists [name=]<name> [opts]

return true if specific device is mounted with passed options

Parameters

device
the device to mount
fstype
the fstype of the device filesystem (default: auto)

mount::present [name=]<name> [opts]

ensure that specific device is mounted according with passed options.

Parameters

device
the device to mount
fstype
the fstype of the device filesystem (default: auto)

mount::absent [name=]<name>

ensure that specific mount point is not mounted.

pkg

The pkg module provides functions to ensure package installation on the system.

Usage example

pkg::present vim

pkg::exists [name=]<package>

return true if the specific package is present in the system.

pkg::present [name=]<package>

ensure that specified package is installed on the system.

pkg::absent [name=]<package>

ensure that specified package is not present on the system.

refresh

The refresh add functionality to refresh services or other resources.

refresh <resource>

refresh specified resource

secret

The secret module allows you to get/set passwords or secret config keys from diferent sources instead of save them in plain text in the potion file.

Environment variables

SECRET_MASTERKEY
Path to file where the master key for secrets lives. By default /etc/potion.key

secret::dump [name=]<name>

dump the encrypted file of a secret plain file.

secret::load [name=]<name>

load a secret file

secret::load_inline <format> <content>

load scripts from the inline string. Available formats are plain, crypt or gpg.

secret::add [name=]<name> content=<content>

add new secret with specified content associated with key passed as argument.

secret <name>

return the secret text associated with specified name

srv

The srv module provides high-level functions to manage package installations.

Usage example

srv::present sshd

srv::exists [name=]<service>

return true if the specific service is present in the system.

srv::present [name=]<service>

ensure that specified service is installed on the system.

srv::absent [name=]<service>

ensure that specified service is not present on the system.

swap

The swap module contains functions to create and configure swap space.

Usage example

swap::present \\
  /tmp/swapfile \\
  size=1G \\
  format=true

swap::exists

return true if swap is enabled

swap::present [name=<device|file>] [size=<size>] [format=<true|false>]

ensure that swap is enable in specified device or file and specified size.

Parameters

name
the device use as swap space.
size
the size required if swap file is created
format
if true, format swap space.

swap::absent [name=]<device|file>

ensure that specified device is not enabled for swap

Parameters

name
the device use as swap space.

tz

The tz module provides functions to configure timezone in the system.

Environment variables

TZ_DIR
path where zoneinfo live. By default /usr/share/zoneinfo.

Usage example

tz::present Europe/Madrid

tz::exists [name=]<zonename>

if timezone especified in argument is set return true, otherwise return false.

tz::present [name=]<zonename>

ensure that specified timezone is set.

tz::absent [name=]<zonename>

ensure that the specified timezone is not set.

user

The user module provides functions to manage users in the system.

Usage example

group::present mygroup
user::present someuser \
  groups=adm,mygroup \
  nohome=true \
  system=true \
  shel=/bin/false

user::present [name=<username>] [options]

create user if not exists

Parameters

basedir
set the base director to create home directories.
gecos
set the GECOS field in user record.
groups
a comma separated list of groups for the user.
nohome
if true do not create home directory (false by default).
system
if true create system user (false by default).
uid
if set, try to force that UID as user id for the new user.
shell
set the shell of the username.
password
the password for the user

user::absent [name=]<username> [keepdata=true]

destroy username if exists

Parameters

keepdata
if true do not remove user data.

Internal API Reference

artifact

The artifact module contains function to access to artifacts. An artifact is a file embedded in potion compiled object, usually you want to set some configuration files as artifacts.

artifact::hmac <artifact_name>

print to stdout the HMAC of the artifact passed as argument.

Environment variables

ALGO
set the algorithm to use for calculate HMAC. Valid values are: sha1, md5, sha256, sha512. The proper helper tool must be installed on the system.

artifact::extract

extract artifacts if artifact support is enabled.

artifact::exists <artifact_name>

return true if the artifact exists, false otherwise.

artifact::read <artifact_name>

read a specific artifact from potion.

artifact::eval <artifact_name>

read and eval specific artifact from potion.

artifact::build <directory>

scan the directory passed as argument and return a POTION_ARTIFACTS variable to stdout with the encoded version of the artifacts.

artifact::load <directory>

load in environment potion artifacts

context

This internal module provides functionality to run different context in paralell.

context::task::enqueue <fun>

enqueue a task to be done when context finished.

context::task::apply <context>

apply functions enqueued by context::task::enqueue.

context::execute <name>

Execute a context named as name.

Environment variables

CONTEXT
contains the context name, and this variable will be propagated to other functions in the same context.

context::run

run all context in paralell.

deprecated

This internal module provides a way to mark deprecated features or modules.

deprecated [name=]<name> [version=version] [date=date]

set a deprecation mark called name (could be a message), when potion version is lower to specified version a warning is raised, otherwise execution fails with an error. The same behaviour can be used with a date in the format %y-%m-%d, until that date the deprecation warning message will show, after that date execution breaks.

main

The main module contain the specified potion body.

main::init

print nice potion logo and some custom messages at the beginning.

Environment variables

POTION_DEFAULT_SOURCE
If set, always try to get potion files from this url. Note that if ther arguments are passed in the command line, potion will load all of them plus the defined in POTION_DEFAULT_SOURCE.
VERSION
a readonly variable which contains the potion version number.

meta

This internal module provides information about metadata. The metadata information is used internally by potion to store data discovered from the system, like num of CPUs, the hostname, etc. This is an internal module, so you might be not need to override metadata ever.

meta::get <key>

get the current value of key

meta::set <key> <value>

add new value to the metadata

meta::polulate

poplulate metadata values from the system

res

The res module provides a way to work with potion resources.

res::variables <var=value>+

pretty priting resources variables

res::created <resource>

print the properly message when resource is created

res::deleted <resource>

print the properly message when resource is deleted

res::refreshed <resource>

print the properly message when resource is refreshed

res::override <resource_name> <function_name>

override a resource with another function, the new function will receive the same parameters than the original resource. The original one can be invoked with \resource_name (prepending a backslash).