Lots of false errors

27 views
Skip to first unread message

mt...@quoininc.com

unread,
May 4, 2018, 9:47:50 AM5/4/18
to help-cfengine
Hey guys,

Got a strange thing here.  I'm hoping there's a simple way to fix without a lot of refactoring.  But who knows?

Running CFengine 3.10.3.  Running on a redhat hub, and some of the clients bootstrapped will be Ubuntu or AIX.

I have a bundle called "context_shared" where shared promises will be run.  But there are some that are only shared among a particular OS.  So here's a basic example of what this file looks like:

body file control
{


  inputs
=> {
   
"context_shared/some_ubuntu_promises.cf",
   
"context_shared/some_aix_promises.cf
  }


bundle agent context_shared
{
  methods:
    ubuntu_16_04::
      "
any" usebundle => some_ubuntu_promises.cf;
    aix:
      "
any" usebundle => some_aix_promises.cf
}


Now, in the some_ubuntu_promises.cf file, I might use a command to call /bin/systemctl.  This file doesn't exist on aix (or on redhat, for that matter).  So it's only going to run if the ubuntu_16_04 class is set.  With me so far?

Here's the problem: The hub is redhat.  It tries to validate all the promises.  I guess part of validation is verifying that all files referenced in any promises are legit?  I'm getting tons of errors like this:

verbose: END    parsing file: /var/cfengine/inputs/context_shared/some_ubuntu_promises.cf
  error
: Proposed executable file '/bin/systemctl' doesn't exist.


Is there a way around this without having to have the hub be the same os as the clients?  Otherwise I'm not sure how to support multiple os's.  I'm wondering if I need to reorder something in the promises.cf file?  Seems like it should be able to know that it doesn't have the class ubuntu_16_04 set, and so can't validate any promises that are only reached when that class is set.

Nick Anderson

unread,
May 4, 2018, 10:03:20 AM5/4/18
to mt...@quoininc.com, help-cfengine
I believe you are seeing pre-eval. 
When the agent starts it resolves classes and vars as it's resolving inputs (because this can adjust what additional inputs you may load).

If you have platform specific policy files I would recommend only adding them to inputs on that platform. That way you don't waste time parsing policy that is not applicable.

I usually use body file control to extend inputs. You can setup a variable to include the proper policies for the platform ( I commonly use classic arrays with class guards and getindics and getvalues).

--
You received this message because you are subscribed to the Google Groups "help-cfengine" group.
To unsubscribe from this group and stop receiving emails from it, send an email to help-cfengin...@googlegroups.com.
To post to this group, send email to help-c...@googlegroups.com.
Visit this group at https://groups.google.com/group/help-cfengine.
For more options, visit https://groups.google.com/d/optout.

Nick Anderson

unread,
May 4, 2018, 10:23:32 AM5/4/18
to mt...@quoininc.com, help-cfengine
Alternatively you can guard all your promises. Just beware that guarding a use bundle does not guard classes and vars inside that bundle or children.

mt...@quoininc.com

unread,
May 4, 2018, 10:24:47 AM5/4/18
to help-cfengine


On Friday, May 4, 2018 at 10:03:20 AM UTC-4, Nick Anderson wrote:

If you have platform specific policy files I would recommend only adding them to inputs on that platform. That way you don't waste time parsing policy that is not applicable.

I usually use body file control to extend inputs. You can setup a variable to include the proper policies for the platform ( I commonly use classic arrays with class guards and getindics and getvalues).


This makes sense conceptually... can you provide an example of extending this way?  It doesn't look like I can put the "inputs" section inside of a class in the body file control. Or can I?  I seem to recall getting errors when trying something like this:

body file control
{
    ubuntu_16_04
::

      inputs
=> {
       
...
     
}
}


Nick Anderson

unread,
May 4, 2018, 10:32:49 AM5/4/18
to mt...@quoininc.com, help-cfengine
I can put together an example later today when I'm near my workstation again.


Nick Anderson

unread,
May 4, 2018, 10:38:59 AM5/4/18
to mt...@quoininc.com, help-cfengine
https://github.com/cfengine/masterfiles/blob/master/lib/autorun.cf

There is something you can look at in the mean time. Note that classes can be used in the bundle to affect the inputs variable. And you can use getindices and getvalues and then run the bundles dynamically (assuming none of them take parameters, or all take the same pattern of params).

I'll make something else later. But that reference might be enough to get you going for now.

Nick Anderson

unread,
May 4, 2018, 3:32:44 PM5/4/18
to mt...@quoininc.com, help-cfengine

mt...@quoininc.com writes:

Got a strange thing here. I'm hoping there's a simple way to fix without a lot
of refactoring. But who knows?

Running CFengine 3.10.3. Running on a redhat hub, and some of the clients
bootstrapped will be Ubuntu or AIX.

I have a bundle called "contextshared" where shared promises will be run. But

OK, you have several options.

1 You could instrument your policy with deep guards.

For example in your some_ubuntu_promises.cf use class expressions to restrict
the promises to the platforms you intend them to be used on. This will prevent

bundle agent some_ubuntu_promises::
{
    vars:

      ubuntu.!(ubuntu_14|ubuntu_15)::

        "units" string => execresult( "/bin/systemctl list-units", noshell);


  classes:

    ubuntu.systemd::

        "xxx_is_enabled" string => returnzero( "/bin/systemctl is-enabled xxx.service", noshell);


  commands:

      # Since commands are only run when a bundle is getting its 3 pass
      # convergence this technically doesn't need a guard. It's only subject to
      # run if it is part of the bundlesequence, or if its used by a methods
      # type promise.

   # ubuntu.!(ubuntu_14|ubuntu_15)::

        "/bin/systemctl daemon-reload"
          if => "systemd_unit_xxx_repaired.!systemd_unit_xxx_notkept";
}

This is considered a best practice. I however am guilty of rarely adhering to
it. I dislike the level of maintenance this can require, so when I do it I try
to use a single class per bundle that represents what is supported. But deep
guarding is the best way to prevent things from happening on platforms where
they should not.

bundle agent some_ubuntu_promises::
{
    classes:

      "$(this.bundle)_platform_supported"
        expression => "ubuntu.!(ubuntu_14|ubuntu_15).systemd";

    vars:

      "$(this.budle)_platform_supported"::

        "units" string => execresult( "/bin/systemctl list-units", noshell);

  classes:

      "$(this.budle)_platform_supported"::

        "xxx_is_enabled" string => returnzero( "/bin/systemctl is-enabled xxx.service", noshell);


  commands:

      # Since commands are only run when a bundle is getting its 3 pass
      # convergence this technically doesn't need a guard. It's only subject to
      # run if it is part of the bundlesequence, or if its used by a methods
      # type promise.

      "$(this.budle)_platform_supported"::

        "/bin/systemctl daemon-reload"
          if => "systemd_unit_xxx_repaired.!systemd_unit_xxx_notkept";
}

2 You could use classes in body file control to arrange different inputs for different contexts

If you are careful that a policy is only ever added to inputs on the host it
should run, then the agent will not have the opportunity to error because it
will not spend time parsing the file.

body file control
{

  ubuntu_16::  
    inputs => {
      "context_shared/some_ubuntu_promises.cf",
    };

  aix::
    inputs => {
      "context_shared/some_aix_promises.cf"
    };


bundle agent context_shared
{
  methods:

    ubuntu_16_04
::

      "any" usebundle => some_ubuntu_promises;

    aix:

      "any" usebundle => some_aix_promises;
}

The above may give you errors about undefined bundles. You can disable this
sanity check by setting ignore_missing_bundles in body common control to
true. See Components and Common Control in the Reference Manual.

Off topic: I am curious, what versions of AIX are you using?

3 You could use variables in a bundle to setup dynamic inputs

This has the added advantage of being able to leverage $(this.promise_dirname)
to load policy files based on relative location.

body file control
{
    inputs => { @(some_ubuntu_promises.inputs) };
}    

bundle common some_ubuntu_promises_file_control
{
  vars:

    any::
      "policy[my_always]" string => "$(this.promsie_dirname)/my_always.cf";

    ubutu::
      "policy[some_ubuntu_promises]" string => "$(this.promise_dirname)/some_ubuntu_promises.cf"; 

    aix::
     "policy[some_aix_promises]" string => "$(this.promise_dirname)/some_aix_promises.cf"; 

    any::
    "inputs" slist => getvalues( policy );
}

bundle agent context_shared
{
  methods:

    ubuntu_16_04
::

      "any" usebundle => some_ubuntu_promises;

    aix:

      "any" usebundle => some_aix_promises;
}

Again, this policy will error if your using a bundle that isn't part of inputs.
However, we can leverage the data structure, and the consistency in no bundle
params to run the bundles automatically.

body file control
{
    inputs => { @(some_ubuntu_promises.inputs) };
}    

bundle common some_ubuntu_promises_file_control
{
  vars:

    any::
      "policy[my_always]" string => "$(this.promsie_dirname)/my_always.cf";

   ubutu::
      "policy[some_ubuntu_promises]" string => "$(this.promise_dirname)/some_ubuntu_promises.cf"; 

   aix::
     "policy[some_aix_promises]" string => "$(this.promise_dirname)/some_aix_promises.cf"; 

    any::

    "inputs" slist => getvalues( policy );

    "bundles" slist => getvalues( policy ); # This assumes that we named the
                                            # key for each file the bundle we
                                            # want to call from that file.

}

bundle agent context_shared
{
  methods:

      "any" usebundle => $(some_ubuntu_promises_file_control.bundles);

}

The data structure for inputs can also make use of consistently naming. If you
are maintaining a file for each possible sys.flavor for example. In this
example we load platform and hostname specific policy if the policy files exist.

body file control
{
    inputs => { @(some_ubuntu_promises.inputs) };
}

bundle common some_ubuntu_promises_file_control
{
  vars:

    any::

      "policy[my_always]" string => "$(this.promsie_dirname)/my_always.cf";

        # Load platform specific policy if it exists
      "policy[$(sys.flavor)]"
        string => "$(this.promsie_dirname)/$(sys.flavor).cf",
        if => fileexists( "$(this.promsie_dirname)/$(sys.flavor).cf" );

        # Load host specific policy if it exists
       "policy[$(sys.fqhost)]"
        string => "$(this.promsie_dirname)/$(sys.flavor).cf",
        if => fileexists( "$(this.promsie_dirname)/$(sys.flavor).cf" );

    "inputs" slist => getvalues( policy );

    "bundles" slist => getvalues( policy ); # This assumes that we named the
                                            # key for each file the bundle we
                                            # want to call from that file.
}

bundle agent context_shared
{
  methods:

      "any" usebundle => $(some_ubuntu_promises_file_control.bundles);
}

These kinds of changes to enable dynamic policies can result in great speedups
and have benefits in policy maintenance. However, the platform split is not good
for all things, pushing everything into platform specific policies can
drastically increase and complicate your policy management. Additionally,
introducing dynamic inputs minimizes the hubs policy check protection. You
should be sure that your CI is testing these permutations before they are
released or you could distribute broken policy to a few hosts.

I hope these examples help!


Nick Anderson
Doer of things, CFEngine

mt...@quoininc.com

unread,
May 4, 2018, 3:54:20 PM5/4/18
to help-cfengine
Thanks for all of this, lots of options.  I've been working off the example you sent earlier, may have just gotten it to work.  I agree with the dislike of the extra maintenance involved in some of that deep guard stuff... I'm trying to avoid extra conditionals at every level.  I'll see how far I get with this and maybe either post my results (if they work) or try another one of your options.  Thanks again!

mt...@quoininc.com

unread,
May 7, 2018, 10:26:08 AM5/7/18
to help-cfengine
Just following up.  Here's what ended up working for me:


# ...

body common control
=> {
  bundlesequence
=> {
   
# ...
   
@(context_shared.bundles),
   
# ...
 
};

  inputs
=> {
   
# ...
   
"context_shared/context_shared.cf",
   
@(context_shared.inputs),
   
# ...
 
};
 
# ...
}
# ...

/context_shared/context_shared.cf:

bundle common context_shared
{
  vars
:
    any
::
     
"inputs" slist => { };
     
"bundles" slist => { };

    ubuntu_16_4
::
     
"inputs" slist => { "context_shared/ubuntu_16_4.cf" };
     
"bundles" slist => { "ubuntu_16_4" };

   
# ... (additional os-specific definitions)
}

Then I can define my os-specific stuff in the os-specific shared bundle, ex:

context_shared/ubuntu_16_4.cf:

body file control
{
  inputs
=> {

   
"context_shared/ubuntu_specific_stuff.cf"
 
}
}

bundle agent ubuntu_16_4
{
  methods
:
   
"any" usebundle => ubuntu_specific_stuff;
}



And lastly, an example of ubuntu-specific "stuff" that would fail validation on redhat if I didn't create this extra layer:

bundle agent ubuntu_specific_stuff
{
  vars
:
   
"audit1" string => execresult("/bin/systemctl is-enabled nfs-server", "noshell");

  classes
:
   
"audit1_failed" expression => strcmp("enabled", "$(audit1)");

  reports
:
    audit1_failed
::
     
"ERROR: $(this.bundle): FAILED.";
}


I based this solution on your recommendation for defining dynamic inputs and bundles and I used the inventory stuff as a model.  Thanks again!  

Nick Anderson

unread,
May 7, 2018, 11:33:00 AM5/7/18
to mt...@quoininc.com, help-cfengine

mt...@quoininc.com writes:

Just following up. Here's what ended up working for me:

Thanks for sharing what you converged on.

I have a couple things you might consider.

First, instead of modifying promises.cf directly to add your additional inputs,
consider using augments (def.json).

Instead of this in promises.cf:

body common control {
        bundlesequence => {
                    # ...
                    @(context_shared.bundles),
                    # ...
        };

        inputs => {
                    # ...
                    "context_shared/context_shared.cf",
                    @(context_shared.inputs),
        };
      # ...
}

You could extend the base inputs and append to the default bundlesequence from
def.json:

{
  "inputs": [ "context_shared/context_shared.cf" ],
  "vars": {
      "control_common_bundlesequence_end": [ "mtaft" ]
    }
}

context_shared/context_shared.cf could contain:

body file control
{
  inputs => { @(context_shared.inputs) );
}

bundle common context_shared
{
  vars:
    any::
      "inputs" slist => { };
      "bundles" slist => { };

    ubuntu_16_4::

      # "policy[usebundle]" string => "inputfile";

      "policy[ubuntu_16_4]" string => "context_shared/ubuntu_16_4.cf";

    any::

      # We sort this for predictibility which can aid debugging

      "inputs"  slist => sort( getvalues( policy ), lex);
      "bundles" slist => sort( getindices( policy ), lex);
}

bundle agent mtaft
{

  methods:

    # Activate all the context specific bundles.

    "$(context_shared.bundles)"
      usebundle => $(context_shared.bundles);

}

This bundle mtaft could be organized somewhere else if it makes more sense.

I think minimizing the number of changes you make to the vendored policy will
ease future policy framework upgrades. Also mapping bundle names to policy
inputs is useful so that you can maintain just the data structure instead of
multiple lists independently.

Reply all
Reply to author
Forward
0 new messages