items.py

Within each bundle, there may be a file called items.py. It defines any number of magic attributes that are automatically processed by BundleWrap. Each attribute is a dictionary mapping an item name (such as a file name) to a dictionary of attributes (e.g. file ownership information).

A typical items.py might look like this:

files = {
    '/etc/hosts': {
         'owner': "root",
         'group': "root",
         'mode': "0664",
         [...]
    },
}

users = {
    'janedoe': {
        'home': "/home/janedoe",
        'shell': "/bin/zsh",
        [...]
    },
    'johndoe': {
        'home': "/home/johndoe",
        'shell': "/bin/bash",
        [...]
    },
}

This bundle defines the attributes files and users. Within the users attribute, there are two user items. Each item maps its name to a dictionary that is understood by the specific kind of item. Below you will find a reference of all builtin item types and the attributes they understand. You can also define your own item types.


Item types

This table lists all item types included in BundleWrap along with the bundle attributes they understand.

TypeBundle attributeDescription
actionactionsActions allow you to run commands on every bw apply
directorydirectoriesManages permissions and ownership for directories
filefilesManages contents, permissions, and ownership for files
git_deploygit_deployDeploys the contents of a git repository
groupgroupsManages groups by wrapping groupadd, groupmod and groupdel
k8s_*k8s_*Manages resources in Kubernetes clusters by wrapping kubectl
pkg_aptpkg_aptInstalls and removes packages with APT
pkg_dnfpkg_dnfInstalls and removes packages with dnf
pkg_opkgpkg_opkgInstalls and removes packages with opkg
pkg_pacmanpkg_pacmanInstalls and removes packages with pacman
pkg_pamacpkg_pamacInstalls and removes packages with pamac
pkg_pippkg_pipInstalls and removes Python packages with pip
pkg_snappkg_snapInstalls and removes packages with snap
pkg_yumpkg_yumInstalls and removes packages with yum
pkg_zypperpkg_zypperInstalls and removes packages with zypper
postgres_dbpostgres_dbsManages Postgres databases
postgres_rolepostgres_rolesManages Postgres roles
pkg_pippkg_pipInstalls and removes Python packages with pip
pkg_freebsdpkg_freebsdInstalls and removes FreeBSD packages with pkg
pkg_openbsdpkg_openbsdInstalls and removes OpenBSD packages with pkg_add/pkg_delete
routerosrouterosManages RouterOS configuration
svc_freebsdsvc_freebsdStarts and stops services with FreeBSD's rc
svc_openbsdsvc_openbsdStarts and stops services with OpenBSD's rc
svc_systemdsvc_systemdStarts and stops services with systemd
svc_systemvsvc_systemvStarts and stops services with traditional System V init scripts
svc_upstartsvc_upstartStarts and stops services with Upstart
symlinksymlinksManages symbolic links and their ownership
userusersManages users by wrapping useradd, usermod and userdel
zfs_datasetzfs_datasetsManages ZFS datasets
zfs_poolzfs_poolsManages ZFS pools


Builtin item attributes

There are also attributes that can be applied to any kind of item.


after

This lets you control execution order of items. This is not something you will have to do very often, because there are already implicit dependencies between item types (e.g. all files automatically depend on the users owning them).

actions = {
    'a': {
        'command': 'true',
    },
    'b': {
        'command': 'true',
        'after': {'action:a'},
    },
}

When set up like this, action:b will only run after action:a has been completed. Note that it doesn't matter if action:a is successful or not, that what needs is for.

See Selectors for a complete overview of the ways to specify items here.


before

Just like after, but in the opposite direction.


comment

This is a string that will be displayed in interactive mode (bw apply -i) whenever the item is to be changed in any way. You can use it to warn users before they start disruptive actions.


error_on_missing_fault

This will simply skip an item instead of raising an error when a Fault used for an attribute on the item is unavailable. Faults are special objects used by repo.vault to handle secrets. A Fault being unavailable can mean you're missing the secret key required to decrypt a secret you're trying to use as an item attribute value.

Defaults to False.


needs

This allows for setting up dependencies between items. Here are two examples:

my_items = {
    'item1': {
        [...]
        'needs': [
            'file:/etc/foo.conf',
        ],
    },
    'item2': {
        [...]
        'needs': [
            'pkg_apt:',
            'bundle:foo',
        ],
    }
}

The first item (item1, specific attributes have been omitted) depends on a file called /etc/foo.conf, while item2 depends on all APT packages being installed and every item in the foo bundle.

Note that unlike after, with needs the depending item will be skipped if the item it depends on fails or is skipped (unless cascade_skip is set to False on that item).

See Selectors for a complete overview of the ways to specify items here.


needed_by

This attribute is an alternative way of defining dependencies. It works just like needs, but in the other direction. There are only three scenarios where you should use needed_by over needs:

  • if you need all items of a certain type to depend on something or
  • if you need all items in a bundle to depend on something or
  • if you need an item in a bundle you can't edit to depend on something in your bundles


tags

A list of strings to tag an item with. Tagging has no immediate effect in itself, but can be useful in a number of places. For example, you can add dependencies on all items with a given tag:

pkg_apt = {
    "mysql-server-{}".format(node.metadata.get('mysql_version', "5.5")): {
        'tags': ["provides-mysqld"],
    },
}

svc_systemd = {
    "myapp": {
        'needs': ["tag:provides-mysqld"],
    },
}

In this simplified example we save ourselves from duplicating the logic that gets the current MySQL version from metadata (which is probably overkill here, but you might encounter more complex situations).

Tags also allow for optional dependencies, since items can depend on tags that don't exist. So for example if you need to do something after items from another bundle have been completed, but that bundle might not always be there, you can depend on a tag given to the items of the other bundle.


triggers and triggered

In some scenarios, you may want to execute an action only when an item is fixed (e.g. restart a daemon after a config file has changed or run postmap after updating an alias file). To do this, BundleWrap has the builtin atttribute triggers. You can use it to point to any item that has its triggered attribute set to True. Such items will only be checked (or in the case of actions: run) if the triggering item is fixed (or a triggering action completes successfully).

files = {
    '/etc/daemon.conf': {
        [...]
        'triggers': [
            'action:restart_daemon',
        ],
    },
}

actions = {
    'restart_daemon': {
        'command': "service daemon restart",
        'triggered': True,
    },
}

The above example will run service daemon restart every time BundleWrap successfully applies a change to /etc/daemon.conf. If an action is triggered multiple times, it will only be run once.

Similar to needed_by, triggered_by can be used to define a triggers relationship from the opposite direction.

See Selectors for a complete overview of the ways to specify items here.


preceded_by

Operates like triggers, but will apply the triggered item before the triggering item. Let's look at an example:

files = {
    '/etc/example.conf': {
        [...]
        'preceded_by': [
            'action:backup_example',
        ],
    },
}

actions = {
    'backup_example': {
        'command': "cp /etc/example.conf /etc/example.conf.bak",
        'triggered': True,
    },
}

In this configuration, /etc/example.conf will always be copied before and only if it is changed. You would probably also want to set cascade_skip to False on the action so you can skip it in interactive mode when you're sure you don't need the backup copy.

Similar to needed_by, precedes can be used to define a preceded_by relationship from the opposite direction.

See Selectors for a complete overview of the ways to specify items here.


skip

Set this to True to always skip this item. This is useful if you just want to quickly disable this item to try something or if it's sitting somewhere in a dependency chain and it would be too cumbersome to remove entirely under certain conditions. Note that setting this to True will also change the default for cascade_skip to False.


unless

Another builtin item attribute is unless. For example, it can be used to construct a one-off file item where BundleWrap will only create the file once, but won't check or modify its contents once it exists.

files = {
    "/path/to/file": {
        [...]
        "unless": "test -x /path/to/file",
    },
}

This will run test -x /path/to/file before doing anything with the item. If the command returns 0, no action will be taken to "correct" the item.

Another common use for unless is with actions that perform some sort of install operation. In this case, the unless condition makes sure the install operation is only performed when it is needed instead of every time you run bw apply. In scenarios like this you will probably want to set cascade_skip to False so that skipping the installation (because the thing is already installed) will not cause every item that depends on the installed thing to be skipped. Example:

actions = {
    'download_thing': {
        'command': "wget http://example.com/thing.bin -O /opt/thing.bin && chmod +x /opt/thing.bin",
        'unless': "test -x /opt/thing.bin",
        'cascade_skip': False,
    },
    'run_thing': {
        'command': "/opt/thing.bin",
        'needs': ["action:download_thing"],
    },
}

If action:download_thing would not set cascade_skip to False, action:run_thing would only be executed once: directly after the thing has been downloaded. On subsequent runs, action:download_thing will fail the unless condition and be skipped. This would also cause all items that depend on it to be skipped, including action:run_thing.

The commands you choose for unless should not change the state of your node. Otherwise, running bw verify might unexpectedly interfere with your nodes.


cascade_skip

DEPRECATED: Use before and after instead.

There are some situations where you don't want to default behavior of skipping everything that depends on a skipped item. That's where cascade_skip comes in. Set it to False and skipping an item won't skip those that depend on it. Note that items can be skipped

  • interactively or
  • because of bw apply --only or bw apply --skip or
  • because a Fault was unavailable or
  • they were soft-locked on the node or
  • because they haven't been triggered or
  • because one of their dependencies was skipped or
  • because one of their dependencies failed or
  • they failed their unless condition or
  • the skip attribute was set or
  • because an action had its interactive attribute set to True during a non-interactive run

The following example will offer to run an apt-get update before installing a package, but continue to install the package even if the update is declined interactively.

actions = {
    'apt_update': {
        'cascade_skip': False,
        'command': "apt-get update",
    },
}

pkg_apt = {
    'somepkg': {
        'needs': ["action:apt_update"],
    },
}

cascade_skip defaults to True. However, if the item uses the unless or skip attributes or is triggered, the default changes to False. Most of the time, this is what you'll want.


when_creating

These attributes are only enforced during the creation of the item on the node (this means the first run of bw apply after adding this item to config). They are ignored in subsequent runs of bw apply, and when other (non-when_creating) attributes are changed.

Canned actions

Some item types have what we call "canned actions". Those are pre-defined actions attached directly to an item. Take a look at this example:

svc_upstart = {'mysql': {'running': True}}

files = {
    "/etc/mysql/my.cnf": {
        'source': "my.cnf",
        'triggers': [
            "svc_upstart:mysql:reload",  # this triggers the canned action
        ],
    },
}

Canned actions always have to be triggered in order to run. In the example above, a change in the file /etc/mysql/my.cnf will trigger the reload action defined by the svc_upstart item type for the mysql service.