Three high level patterns in CFEngine

Posted by:

19 Nov 2018

This post was syndicated with permission from the original source.

How do you deal with config files that need different settings based on various services that are running on a host and cooperate with other teams? It’s a common question, and it came up on in #cfengine on irc.freenode.net recently.

The issue is that team A might be working on package A, which requires some environment variables set. But team B might be working on a totally different thing — and want to achieve the same thing. I hoped to give them a bit of ‘library’ code to take care of it, rather than have them touch a centralized environment-setting policy file.

Here, I look at three high level patterns I have seen.

Competitive management
Each bundle that needs a specific config manages it by itself, typically by using a parameterized bundle.
Centralized management
All config definition is in a single place, anyone needing something edits the global config.
Cooperative management
Each bundle that needs a specific config, publishes the config it needs, and another policy handles consolidating the configs and edits the config as necessary.

Let’s take a look at an example of each of these patterns. I will start from an example recently shared in #cfengine on irc.freenode.net that manages =/etc/environment, /etc/sysstemd.conf, and /etc/profile. I’ll first fix it, so that it works, then show several different patterns that achieves the same goal.

The original example

Hello everyone. I am trying to use set_line_based to set some “global”
environment variables. I’ve created a bundle that I thought might make it
easier. However, using this bundle more than once only applies the first
‘invocation’. Code here: https://pastebin.com/mbU1Bb6i

Example usage:

  bundle agent vagrant_vm
  {
    meta:
        "tags" slist => {
                          "autorun"
        };
    methods:
        "unifi";
        "any" usebundle => global_env("PAPERTRAIL_HOST",  "xxx.papertrailapp.com");
        "any" usebundle => global_env("PAPERTRAIL_PORT",  "12345");
        "any" usebundle => global_env("APPOPTICS_USER",   "ops@whatever.com");
        "any" usebundle => global_env("APPOPTICS_APIKEY", "cafebabedeadbeef");
        "any" usebundle => global_env("CORE_JDBC_URL",    "jdbc:postgresql://localhost/xxx");
        "any" usebundle => global_env("CORE_JDBC_USER",   "vagrant");
        "any" usebundle => global_env("SMS_ENABLED",      "false");
        "any" usebundle => global_env("SMS_AWS_REGION",   "eu-west-1");
  }

Implementation:

  bundle agent global_env(name, value)
  {
    vars:
        "etcenv[$(name)]" string => "$(value)";
        "systemconf[DefaultEnvironment=$(name)]" string => "$(value)";
        "etcprofile[export $(name)]" string => "$(value)";

    files:
        "/etc/environment"
          edit_line => set_line_based("global_env.etcenv", "=", "\s*=\s*", ".*", "\s*#\s*");
        "/etc/systemd/system.conf"
          edit_line => set_line_based("global_env.systemconf", "=", "\s*=\s*", ".*", "\s*#\s*"),
          classes   => if_repaired("system_conf_changed");
        "/etc/profile"
          edit_line => set_line_based("global_env.etcprofile", "=", "\s*=\s*", ".*", "\s*#\s*");

    commands:
      system_conf_changed::
        "/sbin/systemctl daemon-reload"
          contain => exec_owner("root");
  }

The reason that only the first actuation of global_env alters the file is because the promise does not differ. Once a promise has been kept or repaired, it is not actuated again within the same agent run. The promise can be made unique by including the parameters which are different between actuation’s in the promise. Here we add a handle using both parameters, so that the promise is unique across actuation’s of the bundle for different values of name and value.

  "/etc/environment"
    edit_line => set_line_based("global_env.etcenv", "=", "\s*=\s*", ".*", "\s*#\s*"),
    handle => "etc_environment_$(name)_$(value)";

I will combine them together into the same policy file and make some minor changes.

  • I added handles using the parameters to make sure that the promises are unique across executions with different parameter values. For more on this, see the language concepts for bundles, it discusses how bundles are not functions.
  • I added bundle agent __main__ so that the vagrant_vm bundle will be actuated when this policy file is run directly.
  • I removed the unifi methods promise because it was not provided in the original example.
  • I added reports that show the complete content of the files for easy demonstration.
  • I include the stdlib for my executions, you don’t see it, but know that it’s in use.
  • I guard the systemd daemon-reload with a check that the executing user is root because I prototype policy as my own user, inside org-mode using ob-cfengine3 (shameless plug).
  • I add an init bundle to reset as if running from a clean state with no pre-existing configuration.
  • I added body contain exec_owner() because it isn’t in the stdlib.
  • I added create => "true" to the files promises.
  • I prefixed the files promises with /tmp.

Competitive management

This is a common pattern. I think of this pattern as competitive because each service manages what it needs, multiple services may compete for control over the same resources like configuration files. Each service only worries about itself, this pattern allows for extra settings to exist in the configuration files which is beneficial in situations where control must be shared between multiple agents. This pattern results in files being edited multiple times within a single agent run which may be useful in ordering, but results in some overhead. Additionally, depending how the service restart on config change is managed, may lead to the same service re-starting multiple times in the same agent run.

Policy:

  bundle agent global_env(name, value)
  {
    vars:
        "etcenv[$(name)]" string => "$(value)";
        "systemconf[DefaultEnvironment=$(name)]" string => "$(value)";
        "etcprofile[export $(name)]" string => "$(value)";

    files:

        "/tmp/etc/environment"
          edit_line => set_line_based("global_env.etcenv", "=", "\s*=\s*", ".*", "\s*#\s*"),
          create => "true",
          handle => "_etc_environment_$(name)_$(value)";

        "/tmp/etc/systemd/system.conf"
          edit_line => set_line_based("global_env.systemconf", "=", "\s*=\s*", ".*", "\s*#\s*"),
          classes   => if_repaired("system_conf_changed"),
          create => "true",
          handle => "_etc_systemd_system_conf_$(name)_$(value)";

        "/tmp/etc/profile"
          edit_line => set_line_based("global_env.etcprofile", "=", "\s*=\s*", ".*", "\s*#\s*"),
          create => "true",
          handle => "_etc_profile_$(name)_$(value)";

    commands:
      system_conf_changed::
        "/sbin/systemctl daemon-reload"
          contain => exec_owner("root"),
          if => strcmp( "$(sys.user_data[username])", "root" ),
          handle => "_sbin_systemctl_daemon_reload_$(name)_$(value)";
  }

  body contain exec_owner(user)
  {
          exec_owner => "$(user)";
  }

  bundle agent vagrant_vm
  {
    meta:
        "tags" slist => {
                          "autorun"
        };
    methods:
        "any" usebundle => global_env("PAPERTRAIL_HOST",  "xxx.papertrailapp.com");
        "any" usebundle => global_env("PAPERTRAIL_PORT",  "12345");
        "any" usebundle => global_env("APPOPTICS_USER",   "ops@whatever.com");
        "any" usebundle => global_env("APPOPTICS_APIKEY", "cafebabedeadbeef");
        "any" usebundle => global_env("CORE_JDBC_URL",    "jdbc:postgresql://localhost/xxx");
        "any" usebundle => global_env("CORE_JDBC_USER",   "vagrant");
        "any" usebundle => global_env("SMS_ENABLED",      "false");
        "any" usebundle => global_env("SMS_AWS_REGION",   "eu-west-1");
  }
  bundle agent init 
  {
    files:
        "/tmp/etc/environment" delete => tidy;
        "/tmp/etc/systemd/system.conf" delete => tidy;
        "/tmp/etc/profile" delete => tidy;
  }
  bundle agent __main__
  {
    methods:
        "init";
        "vagrant_vm";

    reports:
        "CFEngine $(sys.cf_version)";
        "/tmp/etc/environment"         printfile => cat( "$(this.promiser)" );
        "/tmp/etc/systemd/system.conf" printfile => cat( "$(this.promiser)" );
        "/tmp/etc/profile"             printfile => cat( "$(this.promiser)" );
  }

Output:

R: CFEngine 3.12.0
R: /tmp/etc/environment
R: PAPERTRAIL_HOST=xxx.papertrailapp.com
R: PAPERTRAIL_PORT=12345
R: APPOPTICS_USER=ops@whatever.com
R: APPOPTICS_APIKEY=cafebabedeadbeef
R: CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: CORE_JDBC_USER=vagrant
R: SMS_ENABLED=false
R: SMS_AWS_REGION=eu-west-1
R: /tmp/etc/systemd/system.conf
R: DefaultEnvironment=PAPERTRAIL_HOST=xxx.papertrailapp.com
R: DefaultEnvironment=PAPERTRAIL_PORT=12345
R: DefaultEnvironment=APPOPTICS_USER=ops@whatever.com
R: DefaultEnvironment=APPOPTICS_APIKEY=cafebabedeadbeef
R: DefaultEnvironment=CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: DefaultEnvironment=CORE_JDBC_USER=vagrant
R: DefaultEnvironment=SMS_ENABLED=false
R: DefaultEnvironment=SMS_AWS_REGION=eu-west-1
R: /tmp/etc/profile
R: export PAPERTRAIL_HOST=xxx.papertrailapp.com
R: export PAPERTRAIL_PORT=12345
R: export APPOPTICS_USER=ops@whatever.com
R: export APPOPTICS_APIKEY=cafebabedeadbeef
R: export CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: export CORE_JDBC_USER=vagrant
R: export SMS_ENABLED=false
R: export SMS_AWS_REGION=eu-west-1

A common evolution for this policy would be to condense the write operations by altering the parameter to pass a map of key value pairs.

Condense the write operations by changing the parameters

A quick change to the parameters allows multiple key values to be set at a single time, reducing IO.

Policy:

  bundle agent global_env(map)
  {
    vars:
        # map is KEY=VAL
        "name" slist => getindices( map );
        "etcenv[$(name)]" string => "$(map[$(name)])";
        "systemconf[DefaultEnvironment=$(name)]" string => "$(map[$(name)])";
        "etcprofile[export $(name)]" string => "$(map[$(name)])";

    files:

        "/tmp/etc/environment"
          edit_line => set_line_based("global_env.etcenv", "=", "\s*=\s*", ".*", "\s*#\s*"),
          create => "true",
          handle => "_etc_environment_$(name)_$(map[$(name)])";

        "/tmp/etc/systemd/system.conf"
          edit_line => set_line_based("global_env.systemconf", "=", "\s*=\s*", ".*", "\s*#\s*"),
          classes   => if_repaired("system_conf_changed"),
          create => "true",
          handle => "_etc_systemd_system_conf_$(name)_$(map[$(name)])";

        "/tmp/etc/profile"
          edit_line => set_line_based("global_env.etcprofile", "=", "\s*=\s*", ".*", "\s*#\s*"),
          create => "true",
          handle => "_etc_profile_$(name)_$(map[$(name)])";

    commands:
      system_conf_changed::
        "/sbin/systemctl daemon-reload"
          contain => exec_owner("root"),
          if => strcmp( "$(sys.user_data[username])", "root" ),
          handle => "_sbin_systemctl_daemon_reload_$(name)_$(map[$(name)])";
  }

  body contain exec_owner(user)
  {
          exec_owner => "$(user)";
  }

  bundle agent vagrant_vm
  {
    meta:
        "tags" slist => {
                          "autorun"
        };

    vars:

              "d" data => '{
    "PAPERTRAIL_HOST": "xxx.papertrailapp.com",
    "PAPERTRAIL_PORT": "12345",
    "APPOPTICS_USER": "ops@whatever.com",
    "APPOPTICS_APIKEY": "cafebabedeadbeef",
    "CORE_JDBC_URL": "jdbc:postgresql://localhost/xxx",
    "CORE_JDBC_USER": "vagrant",
    "SMS_ENABLED": "false",
    "SMS_AWS_REGION": "eu-west-1"
  }';

    methods:
        "any" usebundle => global_env( @(d) );

  }
  bundle agent init
  {
    files:
        "/tmp/etc/environment" delete => tidy;
        "/tmp/etc/systemd/system.conf" delete => tidy;
        "/tmp/etc/profile" delete => tidy;
  }
  bundle agent __main__
  {
    methods:
        "init";
        "vagrant_vm";

    reports:
        "CFEngine $(sys.cf_version)";
        "/tmp/etc/environment"         printfile => cat( "$(this.promiser)" );
        "/tmp/etc/systemd/system.conf" printfile => cat( "$(this.promiser)" );
        "/tmp/etc/profile"             printfile => cat( "$(this.promiser)" );
  }

Output:

R: CFEngine 3.12.0
R: /tmp/etc/environment
R: APPOPTICS_USER=ops@whatever.com
R: PAPERTRAIL_PORT=12345
R: CORE_JDBC_USER=vagrant
R: APPOPTICS_APIKEY=cafebabedeadbeef
R: SMS_ENABLED=false
R: PAPERTRAIL_HOST=xxx.papertrailapp.com
R: SMS_AWS_REGION=eu-west-1
R: CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: /tmp/etc/systemd/system.conf
R: DefaultEnvironment=APPOPTICS_USER=ops@whatever.com
R: DefaultEnvironment=CORE_JDBC_USER=vagrant
R: DefaultEnvironment=SMS_AWS_REGION=eu-west-1
R: DefaultEnvironment=SMS_ENABLED=false
R: DefaultEnvironment=APPOPTICS_APIKEY=cafebabedeadbeef
R: DefaultEnvironment=CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: DefaultEnvironment=PAPERTRAIL_HOST=xxx.papertrailapp.com
R: DefaultEnvironment=PAPERTRAIL_PORT=12345
R: /tmp/etc/profile
R: export SMS_ENABLED=false
R: export CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: export APPOPTICS_USER=ops@whatever.com
R: export CORE_JDBC_USER=vagrant
R: export SMS_AWS_REGION=eu-west-1
R: export PAPERTRAIL_PORT=12345
R: export APPOPTICS_APIKEY=cafebabedeadbeef
R: export PAPERTRAIL_HOST=xxx.papertrailapp.com

Centralized management

Let’s take a look at what a centralized management pattern might look like. This is the simplest pattern, and many times how policies begin as a prototype, before evolving into another pattern.

We removed the vagrant_vm bundle, and the uniqness from the promise handles. Now, all the configuration data is right in the global_env bundle. We use a classic array as a generator to produce copies of the key values tailored for each config file. An alternative to creating different variables using a classic array generator we could copy set_line_based and customize it to allow a prefix to be specified.

Policy:

  bundle agent global_env
  {
    vars:

      vagrant_vm::

        "env[PAPERTRAIL_HOST]"            string => "xxx.papertrailapp.com";
        "env[PAPERTRAIL_PORT]"            string => "12345";
        "env[APPOPTICS_USER]"            string => "ops@whatever.com";
        "env[APPOPTICS_APIKEY]"            string => "cafebabedeadbeef";
        "env[CORE_JDBC_URL]"            string => "jdbc:postgresql://localhost/xxx";
        "env[CORE_JDBC_USER]"            string => "vagrant";
        "env[SMS_ENABLED]"            string => "false";
        "env[SMS_AWS_REGION]"            string => "eu-west-1";

      application_2::

        "env[PAPERTRAIL_HOST]"            string => "xxx.trailpaperapp.com";
        "env[PAPERTRAIL_PORT]"            string => "54321";
        "env[APPOPTICS_USER]"            string => "ops@whatever.com";
        "env[APPOPTICS_APIKEY]"            string => "xxxxiiiiiiixxixxix";
        "env[CORE_JDBC_URL]"            string => "jdbc:postgresql://localhost/xxx";
        "env[CORE_JDBC_USER]"            string => "vagrant";
        "env[SMS_ENABLED]"            string => "false";
        "env[SMS_AWS_REGION]"            string => "eu-east-2";


      any::

        "env_i" slist => getindices( env );
        # Here we take the original simple key value pair and add different
        # prefixes for the differnt config file types using a classic array as a
        # generator. Alternatively, we could copy set_line_based and customize it
        # allowing for a prefix parameter.
        "systemconf[DefaultEnvironment=$(env_i)]" string => "$(env[$(env_i)])";
        "etcprofile[export $(env_i)]" string => "$(env[$(env_i)])";

    files:

        "/tmp/etc/environment"
          edit_line => set_line_based("global_env.env", "=", "\s*=\s*", ".*", "\s*#\s*"),
          create => "true",
          handle => "_etc_environment";

        "/tmp/etc/systemd/system.conf"
          edit_line => set_line_based("global_env.systemconf", "=", "\s*=\s*", ".*", "\s*#\s*"),
          classes   => if_repaired("system_conf_changed"),
          create => "true",
          handle => "_etc_systemd_system_conf";

        "/tmp/etc/profile"
          edit_line => set_line_based("global_env.etcprofile", "=", "\s*=\s*", ".*", "\s*#\s*"),
          create => "true",
          handle => "_etc_profile";

    commands:
      system_conf_changed::
        "/sbin/systemctl daemon-reload"
          contain => exec_owner("root"),
          if => strcmp( "$(sys.user_data[username])", "root" ),
          handle => "_sbin_systemctl_daemon_reload";
  }

  body contain exec_owner(user)
  {
          exec_owner => "$(user)";
  }

  bundle agent init
  {
    files:
        "/tmp/etc/environment" delete => tidy;
        "/tmp/etc/systemd/system.conf" delete => tidy;
        "/tmp/etc/profile" delete => tidy;
  }
  bundle agent __main__
  {
    classes:
        "application_2" scope => "namespace";

    methods:
        "init";
        "global_env";

    reports:

        "CFEngine $(sys.cf_version)";
        "/tmp/etc/environment"         printfile => cat( "$(this.promiser)" );
        "/tmp/etc/systemd/system.conf" printfile => cat( "$(this.promiser)" );
        "/tmp/etc/profile"             printfile => cat( "$(this.promiser)" );
  }

Output:

R: CFEngine 3.12.0
R: /tmp/etc/environment
R: SMS_AWS_REGION=eu-east-2
R: PAPERTRAIL_HOST=xxx.trailpaperapp.com
R: CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: SMS_ENABLED=false
R: APPOPTICS_APIKEY=xxxxiiiiiiixxixxix
R: APPOPTICS_USER=ops@whatever.com
R: PAPERTRAIL_PORT=54321
R: CORE_JDBC_USER=vagrant
R: /tmp/etc/systemd/system.conf
R: DefaultEnvironment=APPOPTICS_USER=ops@whatever.com
R: DefaultEnvironment=CORE_JDBC_USER=vagrant
R: DefaultEnvironment=SMS_AWS_REGION=eu-east-2
R: DefaultEnvironment=SMS_ENABLED=false
R: DefaultEnvironment=APPOPTICS_APIKEY=xxxxiiiiiiixxixxix
R: DefaultEnvironment=CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: DefaultEnvironment=PAPERTRAIL_HOST=xxx.trailpaperapp.com
R: DefaultEnvironment=PAPERTRAIL_PORT=54321
R: /tmp/etc/profile
R: export SMS_ENABLED=false
R: export CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: export APPOPTICS_USER=ops@whatever.com
R: export CORE_JDBC_USER=vagrant
R: export SMS_AWS_REGION=eu-east-2
R: export PAPERTRAIL_PORT=54321
R: export APPOPTICS_APIKEY=xxxxiiiiiiixxixxix
R: export PAPERTRAIL_HOST=xxx.trailpaperapp.com

A common evolution’s on this policy include moving variable definition into it’s own bundle, separating the data into external files, and since all data is known at one time, full file management is an option.

Centralized management separate data bundle

Here I am sure to have the data bundle converge before the bundle that will use the data defined there. This separation enables more complex data convergence, delegation of control, and especially with more data can improve the readability of the policy.

Policy:

  bundle agent global_env
  {
    vars:

        "env_i" slist => getindices( "app_data.env" );
        # Here we take the original simple key value pair and add different prefixes
        # for the differnt config file types using a classic array as a generator.
        "systemconf[DefaultEnvironment=$(env_i)]" string => "$(app_data.env[$(env_i)])";
        "etcprofile[export $(env_i)]" string => "$(app_data.env[$(env_i)])";

    files:

        "/tmp/etc/environment"
          edit_line => set_line_based("app_data.env", "=", "\s*=\s*", ".*", "\s*#\s*"),
          create => "true",
          handle => "_etc_environment";

        "/tmp/etc/systemd/system.conf"
          edit_line => set_line_based("$(this.bundle).systemconf", "=", "\s*=\s*", ".*", "\s*#\s*"),
          classes   => if_repaired("system_conf_changed"),
          create => "true",
          handle => "_etc_systemd_system_conf";

        "/tmp/etc/profile"
          edit_line => set_line_based("$(this.bundle).etcprofile", "=", "\s*=\s*", ".*", "\s*#\s*"),
          create => "true",
          handle => "_etc_profile";

    commands:
      system_conf_changed::
        "/sbin/systemctl daemon-reload"
          contain => exec_owner("root"),
          if => strcmp( "$(sys.user_data[username])", "root" ),
          handle => "_sbin_systemctl_daemon_reload";
  }
  bundle agent app_data
  {
    vars:
          vars:

      vagrant_vm::

        "env[PAPERTRAIL_HOST]"            string => "xxx.papertrailapp.com";
        "env[PAPERTRAIL_PORT]"            string => "12345";
        "env[APPOPTICS_USER]"            string => "ops@whatever.com";
        "env[APPOPTICS_APIKEY]"            string => "cafebabedeadbeef";
        "env[CORE_JDBC_URL]"            string => "jdbc:postgresql://localhost/xxx";
        "env[CORE_JDBC_USER]"            string => "vagrant";
        "env[SMS_ENABLED]"            string => "false";
        "env[SMS_AWS_REGION]"            string => "eu-west-1";

      application_2::

        "env[PAPERTRAIL_HOST]"            string => "xxx.trailpaperapp.com";
        "env[PAPERTRAIL_PORT]"            string => "54321";
        "env[APPOPTICS_USER]"            string => "ops@whatever.com";
        "env[APPOPTICS_APIKEY]"            string => "xxxxiiiiiiixxixxix";
        "env[CORE_JDBC_URL]"            string => "jdbc:postgresql://localhost/xxx";
        "env[CORE_JDBC_USER]"            string => "vagrant";
        "env[SMS_ENABLED]"            string => "false";
        "env[SMS_AWS_REGION]"            string => "eu-east-2";

  }

  body contain exec_owner(user)
  {
          exec_owner => "$(user)";
  }

  bundle agent init
  {
    files:
        "/tmp/etc/environment" delete => tidy;
        "/tmp/etc/systemd/system.conf" delete => tidy;
        "/tmp/etc/profile" delete => tidy;
  }
  bundle agent __main__
  {
    classes:
        "vagrant_vm" expression => "any", scope => "namespace";

    methods:
        "init";
        "app_data";
        "global_env";

    reports:
        "CFEngine $(sys.cf_version)";
        "/tmp/etc/environment"         printfile => cat( "$(this.promiser)" );
        "/tmp/etc/systemd/system.conf" printfile => cat( "$(this.promiser)" );
        "/tmp/etc/profile"             printfile => cat( "$(this.promiser)" );
  }

Output:

R: CFEngine 3.12.0
R: /tmp/etc/environment
R: CORE_JDBC_USER=vagrant
R: PAPERTRAIL_PORT=12345
R: CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: SMS_ENABLED=false
R: SMS_AWS_REGION=eu-west-1
R: APPOPTICS_APIKEY=cafebabedeadbeef
R: PAPERTRAIL_HOST=xxx.papertrailapp.com
R: APPOPTICS_USER=ops@whatever.com
R: /tmp/etc/systemd/system.conf
R: DefaultEnvironment=APPOPTICS_USER=ops@whatever.com
R: DefaultEnvironment=CORE_JDBC_USER=vagrant
R: DefaultEnvironment=SMS_AWS_REGION=eu-west-1
R: DefaultEnvironment=SMS_ENABLED=false
R: DefaultEnvironment=APPOPTICS_APIKEY=cafebabedeadbeef
R: DefaultEnvironment=CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: DefaultEnvironment=PAPERTRAIL_HOST=xxx.papertrailapp.com
R: DefaultEnvironment=PAPERTRAIL_PORT=12345
R: /tmp/etc/profile
R: export SMS_ENABLED=false
R: export CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: export APPOPTICS_USER=ops@whatever.com
R: export CORE_JDBC_USER=vagrant
R: export SMS_AWS_REGION=eu-west-1
R: export PAPERTRAIL_PORT=12345
R: export APPOPTICS_APIKEY=cafebabedeadbeef
R: export PAPERTRAIL_HOST=xxx.papertrailapp.com

Centralized management separate data bundle with full file management

Here we remove the init to reset the environment and add full file content management by attaching edit_defaults empty to the files promises. This ensures no unknown content in the configuration file.

Policy:

  bundle agent global_env
  {
      vars:

        "env_i" slist => getindices( "app_data.env" );
      # Here we take the original simple key value pair and add different prefixes
      # for the differnt config file types using a classic array as a generator.
        "systemconf[DefaultEnvironment=$(env_i)]" string => "$(app_data.env[$(env_i)])";
        "etcprofile[export $(env_i)]" string => "$(app_data.env[$(env_i)])";

    files:

        "/tmp/etc/environment"
          edit_line => set_line_based("app_data.etcenv", "=", "\s*=\s*", ".*", "\s*#\s*"),
          edit_defaults => empty,
          create => "true",
          handle => "_etc_environment";

        "/tmp/etc/systemd/system.conf"
          edit_line => set_line_based("$(this.bundle).systemconf", "=", "\s*=\s*", ".*", "\s*#\s*"),
          edit_defaults => empty,
          classes   => if_repaired("system_conf_changed"),
          create => "true",
          handle => "_etc_systemd_system_conf";

        "/tmp/etc/profile"
          edit_line => set_line_based("$(this.bundle).etcprofile", "=", "\s*=\s*", ".*", "\s*#\s*"),
          edit_defaults => empty,
          create => "true",
          handle => "_etc_profile";

    commands:
      system_conf_changed::
        "/sbin/systemctl daemon-reload"
          contain => exec_owner("root"),
          if => strcmp( "$(sys.user_data[username])", "root" ),
          handle => "_sbin_systemctl_daemon_reload";
  }
  bundle agent app_data
  {
    vars:
      vagrant_vm::
        "env[PAPERTRAIL_HOST]"            string => "xxx.papertrailapp.com";
        "env[PAPERTRAIL_PORT]"            string => "12345";
        "env[APPOPTICS_USER]"            string => "ops@whatever.com";
        "env[APPOPTICS_APIKEY]"            string => "cafebabedeadbeef";
        "env[CORE_JDBC_URL]"            string => "jdbc:postgresql://localhost/xxx";
        "env[CORE_JDBC_USER]"            string => "vagrant";
        "env[SMS_ENABLED]"            string => "false";
        "env[SMS_AWS_REGION]"            string => "eu-west-1";

      application_2::
        "env[PAPERTRAIL_HOST]"            string => "xxx.trailpaperapp.com";
        "env[PAPERTRAIL_PORT]"            string => "54321";
        "env[APPOPTICS_USER]"            string => "ops@whatever.com";
        "env[APPOPTICS_APIKEY]"            string => "xxxxiiiiiiixxixxix";
        "env[CORE_JDBC_URL]"            string => "jdbc:postgresql://localhost/xxx";
        "env[CORE_JDBC_USER]"            string => "vagrant";
        "env[SMS_ENABLED]"            string => "false";
        "env[SMS_AWS_REGION]"            string => "eu-east-2";

  }

  body contain exec_owner(user)
  {
          exec_owner => "$(user)";
  }

  bundle agent init
  {
    files:
        "/tmp/etc/environment" delete => tidy;
        "/tmp/etc/systemd/system.conf" delete => tidy;
        "/tmp/etc/profile" delete => tidy;
  }
  bundle agent __main__
  {
    classes:
        "vagrant_vm" expression => "any", scope => "namespace";

    methods:
        #"init";
        "app_data";
        "global_env";

    reports:
        "CFEngine $(sys.cf_version)";
        "/tmp/etc/environment"         printfile => cat( "$(this.promiser)" );
        "/tmp/etc/systemd/system.conf" printfile => cat( "$(this.promiser)" );
        "/tmp/etc/profile"             printfile => cat( "$(this.promiser)" );
  }

Output:

R: CFEngine 3.12.0
R: /tmp/etc/environment
R: PAPERTRAIL_HOST=xxx.trailpaperapp.com
R: PAPERTRAIL_PORT=22222
R: APPOPTICS_USER=ops@cfengine.com
R: APPOPTICS_APIKEY=toodledum
R: CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: CORE_JDBC_USER=vagrant
R: SMS_ENABLED=false
R: SMS_AWS_REGION=eu-west-1
R: /tmp/etc/systemd/system.conf
R: DefaultEnvironment=PAPERTRAIL_PORT=22222
R: DefaultEnvironment=APPOPTICS_APIKEY=toodledum
R: DefaultEnvironment=SMS_ENABLED=false
R: DefaultEnvironment=CORE_JDBC_USER=vagrant
R: DefaultEnvironment=CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: DefaultEnvironment=APPOPTICS_USER=ops@cfengine.com
R: DefaultEnvironment=PAPERTRAIL_HOST=xxx.trailpaperapp.com
R: DefaultEnvironment=SMS_AWS_REGION=eu-west-1
R: /tmp/etc/profile
R: export SMS_ENABLED=false
R: export CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: export APPOPTICS_USER=ops@whatever.com
R: export CORE_JDBC_USER=vagrant
R: export SMS_AWS_REGION=eu-west-1
R: export PAPERTRAIL_PORT=12345
R: export APPOPTICS_APIKEY=cafebabedeadbeef
R: export PAPERTRAIL_HOST=xxx.papertrailapp.com

Centralized management separate data bundle with external data files

In addition to the benefits derived from separating data bundles external data files can further enable delegation of control working much more easily with external systems, reduces the size of the policy and improve policy readability, and eases testing of policy. We can use the json, yaml or env file formats interchangeably. CSV is also possible, but different parsing would be required.

/tmp/env.env:

  PAPERTRAIL_HOST="xxx.papertrailapp.com"
  PAPERTRAIL_PORT="11111"
  APPOPTICS_USER="ops@whatever.com"
  APPOPTICS_APIKEY="cafebabedeadbeef"
  CORE_JDBC_URL="jdbc:postgresql://localhost/xxx"
  CORE_JDBC_USER="vagrant"
  SMS_ENABLED="false"
  SMS_AWS_REGION="eu-west-1"

/tmp/env.json:

  {
    "PAPERTRAIL_HOST": "xxx.papertrailapp.com",
    "PAPERTRAIL_PORT": "11111",
    "APPOPTICS_USER": "ops@whatever.com",
    "APPOPTICS_APIKEY": "cafebabedeadbeef",
    "CORE_JDBC_URL": "jdbc:postgresql://localhost/xxx",
    "CORE_JDBC_USER": "vagrant",
    "SMS_ENABLED": "false",
    "SMS_AWS_REGION": "eu-west-1"
  }

/tmp/env.yaml:

  ---
  PAPERTRAIL_HOST: xxx.papertrailapp.com
  PAPERTRAIL_PORT: '11111'
  APPOPTICS_USER: ops@whatever.com
  APPOPTICS_APIKEY: cafebabedeadbeef
  CORE_JDBC_URL: jdbc:postgresql://localhost/xxx
  CORE_JDBC_USER: vagrant
  SMS_ENABLED: 'false'
  SMS_AWS_REGION: eu-west-1

Policy:

  bundle agent global_env
  {
    vars:
        "env_i" slist => getindices( "app_data.env" );
        # Here we take the original simple key value pair and add different prefixes
        # for the differnt config file types using a classic array as a generator.
        "etc_systemd_conf[DefaultEnvironment=$(env_i)]" string => "$(app_data.env[$(env_i)])";
        "etc_profile[export $(env_i)]" string => "$(app_data.env[$(env_i)])";

    files:

        "/tmp/etc/environment"
          edit_line => set_line_based("app_data.env", "=", "\s*=\s*", ".*", "\s*#\s*"),
          edit_defaults => empty,
          create => "true",
          handle => "_etc_environment";

        "/tmp/etc/systemd/system.conf"
          edit_line => set_line_based("$(this.bundle).etc_systemd_conf", "=", "\s*=\s*", ".*", "\s*#\s*"),
          edit_defaults => empty,
          classes   => if_repaired("system_conf_changed"),
          create => "true",
          handle => "_etc_systemd_system_conf";

        "/tmp/etc/profile"
          edit_line => set_line_based("$(this.bundle).etc_profile", "export ", "\s*=\s*", ".*", "\s*#\s*"),
          edit_defaults => empty,
          create => "true",
          handle => "_etc_profile";

    commands:
      system_conf_changed::
        "/sbin/systemctl daemon-reload"
          contain => exec_owner("root"),
          if => strcmp( "$(sys.user_data[username])", "root" ),
          handle => "_sbin_systemctl_daemon_reload";
  }
  bundle agent app_data
  {
    vars:
      vagrant_vm::

        "env" data => readdata( "/tmp/env.yaml", auto );

  }

  body contain exec_owner(user)
  {
          exec_owner => "$(user)";
  }

  bundle agent init
  {
    files:
        "/tmp/etc/environment" delete => tidy;
        "/tmp/etc/systemd/system.conf" delete => tidy;
        "/tmp/etc/profile" delete => tidy;
  }
  bundle agent __main__
  {
    classes:
        "vagrant_vm" expression => "any", scope => "namespace";

    methods:
        #"init";
        "app_data";
        "global_env";

    reports:
        "CFEngine $(sys.cf_version)";
        "/tmp/etc/environment"         printfile => cat( "$(this.promiser)" );
        "/tmp/etc/systemd/system.conf" printfile => cat( "$(this.promiser)" );
        "/tmp/etc/profile"             printfile => cat( "$(this.promiser)" );
  }

Output:

R: CFEngine 3.12.0
R: /tmp/etc/environment
R: PAPERTRAIL_HOST=xxx.papertrailapp.com
R: PAPERTRAIL_PORT=11111
R: APPOPTICS_USER=ops@whatever.com
R: APPOPTICS_APIKEY=cafebabedeadbeef
R: CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: CORE_JDBC_USER=vagrant
R: SMS_ENABLED=false
R: SMS_AWS_REGION=eu-west-1
R: /tmp/etc/systemd/system.conf
R: DefaultEnvironment=PAPERTRAIL_PORT=11111
R: DefaultEnvironment=APPOPTICS_APIKEY=cafebabedeadbeef
R: DefaultEnvironment=SMS_ENABLED=false
R: DefaultEnvironment=CORE_JDBC_USER=vagrant
R: DefaultEnvironment=CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: DefaultEnvironment=APPOPTICS_USER=ops@whatever.com
R: DefaultEnvironment=PAPERTRAIL_HOST=xxx.papertrailapp.com
R: DefaultEnvironment=SMS_AWS_REGION=eu-west-1
R: /tmp/etc/profile
R: export PAPERTRAIL_HOSTexport xxx.papertrailapp.com
R: export SMS_AWS_REGIONexport eu-west-1
R: export PAPERTRAIL_PORTexport 11111
R: export APPOPTICS_USERexport ops@whatever.com
R: export SMS_ENABLEDexport false
R: export CORE_JDBC_USERexport vagrant
R: export CORE_JDBC_URLexport jdbc:postgresql://localhost/xxx
R: export APPOPTICS_APIKEYexport cafebabedeadbeef

Cooperative management

Here we allow each service to define the key values they need. They advertise/ or /publish their data by tagging it. In this iteration, since we have full knowledge of the desired state at one time we switched the file promises to be templates so that the full content is managed. This helps to avoid unknown settings.

Policy:

  bundle agent global_env
  {
    vars:
        # Collect a datastrucutre from variables tagged as used_by this bundle
        "env"
          data => variablesmatching_as_data( ".*", "used_by=global_env" );

        # Get the index of the collected data (the variable names) in lexically
        # sorted order so that we can iterate over them
        "i" slist => sort( getindices( env ), lex );

        # Here we merge all the objects together by picking out each objects data
        # and merging on top. NOTE In event of conflict the last definition
        # alphabetically wins

        "c" data => '[]';
        "c" data => mergedata( c, mergedata( "env[$(i)]"));

    files:

        "/tmp/etc/environment"
          create => "true",
          template_method => "inline_mustache",
          template_data => @(c),
          edit_template_string => "{{#-top-}}{{{@}}}={{{.}}}$(const.n){{/-top-}}";

        "/tmp/etc/systemd/system.conf"
          create => "true",
          template_method => "inline_mustache",
          template_data => @(c),
          edit_template_string => "{{#-top-}}DefaultEnvironment={{{@}}}={{{.}}}$(const.n){{/-top-}}";

        "/tmp/etc/profile"
          create => "true",
          template_method => "inline_mustache",
          template_data => @(c),
          edit_template_string => "{{#-top-}}export {{{@}}}={{{.}}}$(const.n){{/-top-}}";

    commands:
      system_conf_changed::
        "/sbin/systemctl daemon-reload"
          contain => exec_owner("root"),
          if => strcmp( "$(sys.user_data[username])", "root" ),
          handle => "_sbin_systemctl_daemon_reload_$(name)_$(value)";
  }
  bundle agent vagrant_vm
  {
    vars:

        "v" data =>  '{
                        "PAPERTRAIL_HOST": "xxx.papertrailapp.com",
                        "PAPERTRAIL_PORT": "11111",
                        "APPOPTICS_USER": "ops@whatever.com",
                        "APPOPTICS_APIKEY": "cafebabedeadbeef",
                        "CORE_JDBC_URL": "jdbc:postgresql://localhost/xxx",
                        "CORE_JDBC_USER": "vagrant",
                        "SMS_ENABLED": "false",
                        "SMS_AWS_REGION": "eu-west-1"
        }',
          meta => { "used_by=global_env" };
  }

  bundle agent application_2
  {
    vars:

        "v" data =>  '{
                        "PAPERTRAIL_HOST": "xxx.trailpaperapp.com",
                        "PAPERTRAIL_PORT": "22222",
                        "APPOPTICS_USER": "ops@cfengine.com",
                        "APPOPTICS_APIKEY": "toodledum",
                        "CORE_JDBC_URL": "jdbc:postgresql://localhost/xxx",
                        "CORE_JDBC_USER": "vagrant",
                        "SMS_ENABLED": "false",
                        "SMS_AWS_REGION": "eu-west-1"
        }',
          meta => { "used_by=global_env" };
  }


  body contain exec_owner(user)
  {
          exec_owner => "$(user)";
  }

  bundle agent init
  {
    files:
        "/tmp/etc/environment" delete => tidy;
        "/tmp/etc/systemd/system.conf" delete => tidy;
        "/tmp/etc/profile" delete => tidy;
  }
  bundle agent __main__
  {
    classes:
        "vagrant_vm" scope => "namespace";

    methods:
        #"init";
        "global_env";

    reports:
        "/tmp/etc/environment"         printfile => cat( "$(this.promiser)" );
        "/tmp/etc/systemd/system.conf" printfile => cat( "$(this.promiser)" );
        "/tmp/etc/profile"             printfile => cat( "$(this.promiser)" );
  }

Output:

R: /etc/environment
R: PAPERTRAIL_HOST=xxx.papertrailapp.com
R: PAPERTRAIL_PORT=11111
R: APPOPTICS_USER=ops@whatever.com
R: APPOPTICS_APIKEY=cafebabedeadbeef
R: CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: CORE_JDBC_USER=vagrant
R: SMS_ENABLED=false
R: SMS_AWS_REGION=eu-west-1
R: /etc/systemd/system.conf
R: DefaultEnvironment=PAPERTRAIL_HOST=xxx.papertrailapp.com
R: DefaultEnvironment=PAPERTRAIL_PORT=11111
R: DefaultEnvironment=APPOPTICS_USER=ops@whatever.com
R: DefaultEnvironment=APPOPTICS_APIKEY=cafebabedeadbeef
R: DefaultEnvironment=CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: DefaultEnvironment=CORE_JDBC_USER=vagrant
R: DefaultEnvironment=SMS_ENABLED=false
R: DefaultEnvironment=SMS_AWS_REGION=eu-west-1
R: /etc/profile
R: export PAPERTRAIL_HOST=xxx.papertrailapp.com
R: export PAPERTRAIL_PORT=11111
R: export APPOPTICS_USER=ops@whatever.com
R: export APPOPTICS_APIKEY=cafebabedeadbeef
R: export CORE_JDBC_URL=jdbc:postgresql://localhost/xxx
R: export CORE_JDBC_USER=vagrant
R: export SMS_ENABLED=false
R: export SMS_AWS_REGION=eu-west-1

As with other patterns moving the data into external files would improve delegation of control and tighter integration with other systems.

Nick Anderson