bundle agent main
{
reports:
"${with}" with => concat(
"hello, ",
"world!");
}
Hi Jakub,
Welcome to CFEngine. I will jump around a bit in my response.
- Is there any "practical" guide for CFEngine which is reasonably recent, and can guide me through building small-scale stuff from the grounds up. I'd appreciate a link.
I don't think there is a single guide that works for everyone. If I had to provide but a single link it would have to be https://docs.cfengine.com.
Right now I have a couple of servers, on which I want to deploy some services (some in Docker, some as regular processes), I want a solution where I can describe that declaratively, and have a e.g. git repository with "current state of everything that's running on my servers", I also want to automate stuff like setting up backups, updating the system, etc. It's all Linux for now, and I expect the number of servers to grow in the future.
This sounds like stuff that CFEngine can help you with.
You might find promise-type-docker-compose
useful or a source of inspirational (https://build.cfengine.com/modules/promise-type-docker-compose/).
- And honestly, is CFEngine even the right tool for use cases like mine? I don't mind tinkering a bit and working in low-level stuff, but I hate fighting with the complexity of my tools. I want a simple low-level tool with which I build my own automation, not an "out-of-the-box" corporate solution for thousands of servers.
So, you have some servers running Linux and you want a tool that can help you manage configuration files, processes and help you adjust and maintain the state of the running systems over time. You want something simple and low level to build your own system with and not an out of the box solution.
I am bias, but I think CFEngine sounds like a natural fit.
I think that CFEngine is simple, but I guess it's subjective. I am at least quite familiar with how it operates. If you find yourself "fighting" with CFEngine maybe take a step back and see if you can approach from a different perspective. I remember when starting with CFEngine that it changed the way I thought about things.
- How to deal with the differences between paths in different distros? Should I just roll out CFEngine manually? I thought I can just install CFEngine in anyway, and then bootstrap and it will figure everything out from there, seems like that's not the case. I hope the "hub" can tell the agents where to look for the policies? Or is the files structure just coupled together everywhere?
I think it's important to remember the agency of the agents, no one tells an agent anything. Perhaps imagine yourself as the agent rather than hollering at the hub in the sky ask where you should look for your policy think what in my environment can I clue in on to figure out where to get policy.
Installing and bootstrapping to a hub and then figuring things out is totally possible. That's I think what most people do. By default the MPF doesn't really do much of anything.
I am not sure what you mean by "is the file structure just coupled together everywhere".
How to deal with the difference in paths between different distros? There are fundamental lower level answers to this but perhaps that leads well into your question if you need the MPF.
- Do I need MPF? If I have a git repo do I just copy MPF into it? What does MPF do for me? The very reason that I wanted to learn CFEngine is the simplicity and transparency of what it does, MPF seems to contradict that.
Categorically, I can tell you that you do not need the MPF. However, it does offer something to build off of and the policy is instrumented in the most common places so that you can tweak behavior without having to modify the vendored policy. Plus, there is often an assumption of use so if you are not using it it's good to highlight that fact. You can build a much leaner policy set, that covers just what you need. This goes back to dealing with differences in paths between repos and binaries.
The MPF didn't always exist. In the early versions of 3.x up to maybe 3.5 there wasn't much to graft on to (https://github.com/cfengine/core/tree/3.0.0/inputs). So, the questions from that period of time led to the structure of the Masterfiles Policy Framework MPF forming.
- Is there any "practical" guide for CFEngine which is reasonably recent, and can guide me through building small-scale stuff from the grounds up. I'd appreciate a link.
I think the all time best way of learning CFEngine is live in person training.
The best book is Diegos Learning CFEngine (https://cf-learn.info/). It's not current, but all the basics are in there and I recall when the book came out wishing I had that book when I started learning CFEngine. I referred to things in it for many years.
Aleksey Tslalokhin published the content that he used for CFEngine training. You can use it as a self guided tutorial. https://www.softcover.io/read/72be7d4f/cf3/_single-page
I think that modern practical guides focus on leveraging the newer higher level tools that are available (https://docs.cfengine.com/docs/3.24/getting-started.html).
If you do choose to write your own policy, I will say that you might find cf-locate
(https://github.com/cfengine/core/tree/master/contrib/cf-locate) useful. You could use it to lift out specific body and bundles from the standard library to keep your own policy set extremely minimal.
The follow-up from [2025-08-04 Mon] :
Got quick two questions: Can strings in CFEngine be split up into multiline without defining additional variables, and without adding unintended whitespaces? E.g. In C++ I'd do something like this: ``` const char* string = "This is my first line," " this is another line"; ``` Which evaluates to "This is my first line, this is another line"
Yep, you can.
bundle agent __main__ { vars: "s1" string => "Here is a multiline string \ where I \ have escaped some \ but not other newlines. "; # Sometimes there is preference to use concat() "s2" string => concat("Here we", " bring some strings", ' together and show', ` various quoting of`, ` strings which contain quoteing chars "'"'`, " handy tricks indeed to avoid escapes" ); reports: "$(s1)"; "$(s2)"; }
R: Here is a multiline string where I have escaped some but not other newlines. R: Here we bring some strings together and show various quoting of strings which contain quoteing chars "'"' handy tricks indeed to avoid escapes
Can I build my own bundles/functions which return values? I've seen that bundles can modify variables and define class in the parent scope, but that seems quite horrible. I mean something like this: ``` vars: "my_value" string => do_http_request("some arguments here"); ``` where "do_http_request" is some wrapper around Curl that I can write on my own, can't use url_get, because the binary on Alpine is compiled without curl support, and I would rather not bother with that, and instead do what I would do in other languages: abstract the mundane task of calling curl into something less repetitive.
Yes, it's possible to define your own bundles and bodies (it's quite expected that you will).
I recommend reading through the language concepts section of the reference manual (https://docs.cfengine.com/docs/3.24/reference-language-concepts.html). You can even define your own promise types (https://cfengine.com/blog/2020/introducing-cfengine-custom-promise-types/) but there is not currently any ability to define custom functions.
I think it's important to remember that bundles are not functions (https://docs.cfengine.com/docs/3.24/reference-language-concepts-bundles.html). Though you can use them in that way and they have the ability to return data into the calling bundle via reports promises using bundle_return_value_index
(https://docs.cfengine.com/docs/3.26/reference-promise-types-reports.html#bundle_return_value_index) and useresult
(https://docs.cfengine.com/docs/3.26/reference-promise-types-methods.html#useresult)
Here is an example:
bundle agent __main__ { methods: "Call my bundle with params and define a variable with returned data" usebundle => myBundle("KEY", "VALUE"), useresult => "returned_from_myBundle"; "More ...." usebundle => myBundle("CFEngine", "Awesome"), useresult => "returned_from_myBundle"; "Calculator" usebundle => add("5266", "42", "value"), useresult => "sum"; reports: "CFEngine version: $(sys.cf_version)"; "$(with)" with => storejson( returned_from_myBundle ); "Calculated $(sum[value])"; } bundle agent myBundle(key, value) { reports: "$(value)" bundle_return_value_index => "$(key)"; } bundle agent add(int1, int2, key) { reports: "$(with)" with => int( eval( "$(int1)+$(int2)", "math", "infix" ) ), bundle_return_value_index => "$(key)"; }
R: CFEngine version: 3.27.0a.5553c81fe R: { "CFEngine": "Awesome", "KEY": "VALUE" } R: Calculated 5308
Modules are another thing to know about (https://docs.cfengine.com/docs/3.26/reference-language-concepts-modules.html#top). There are a lot of different kinds of modules in CFEngine. There are the traditional variables and classes modules which allow you to define variables and classes from external programs via a simple line based protocol. You can use modules in different ways but primarily via usemodule()
(https://docs.cfengine.com/docs/3.26/reference-functions-usemodule.html#top) or via commands with module => true
(https://docs.cfengine.com/docs/3.26/reference-promise-types-commands.html#module). Then there are package modules which are external programs that communicate with CFEngine to implement packages promises, promise modules which are for defining custom promise types and CFEngine Build Modules (https://build.cfengine.com/) which are collections of policy or other CFEngine related content.
Hey Jakub,
I meant to ask but apparently forgot, when you say:
I've seen that bundles can modify variables and define class in the parent scope, but that seems quite horrible.
Can you point me to where you see this?
Modules have the power to define variables in any bundle scope of any namespace and can define namespace scoped classes in any namespace.
In policy there is a narrow case where you can define a variable in a remote bundle, but only if that bundle is not defined.
E.g:
bundle agent __main__ { vars: "remoteBundle.myvar" string => "Hello World"; reports: "$(remoteBundle.myvar)"; }
R: Hello World
But, it's protected when there is actual policy defining the bundle:
bundle agent __main__ { vars: "remoteBundle.myvar" string => "Hello World"; reports: "$(remoteBundle.myvar)"; } bundle agent remoteBundle { reports: "$(myvar)"; }
error: Ignoring remotely-injected variable 'myvar' error: Remote bundle variable injection detected! error: Variable identifier 'remoteBundle.myvar' is not legal error: Promise belongs to bundle 'main' in file '/tmp/example.cf' near line 5 error: Ignoring remotely-injected variable 'myvar' error: Remote bundle variable injection detected! error: Variable identifier 'remoteBundle.myvar' is not legal error: Promise belongs to bundle 'main' in file '/tmp/example.cf' near line 5 error: Remote bundle variable injection detected! error: Variable identifier 'remoteBundle.myvar' is not legal error: Promise belongs to bundle 'main' in file '/tmp/example.cf' near line 5 error: Remote bundle variable injection detected! error: Variable identifier 'remoteBundle.myvar' is not legal error: Promise belongs to bundle 'main' in file '/tmp/example.cf' near line 5 error: Remote bundle variable injection detected! error: Variable identifier 'remoteBundle.myvar' is not legal error: Promise belongs to bundle 'main' in file '/tmp/example.cf' near line 5 error: Remote bundle variable injection detected! error: Variable identifier 'remoteBundle.myvar' is not legal error: Promise belongs to bundle 'main' in file '/tmp/example.cf' near line 5 error: Remote bundle variable injection detected! error: Variable identifier 'remoteBundle.myvar' is not legal error: Promise belongs to bundle 'main' in file '/tmp/example.cf' near line 5 error: Remote bundle variable injection detected! error: Variable identifier 'remoteBundle.myvar' is not legal error: Promise belongs to bundle 'main' in file '/tmp/example.cf' near line 5 error: Remote bundle variable injection detected! error: Variable identifier 'remoteBundle.myvar' is not legal error: Promise belongs to bundle 'main' in file '/tmp/example.cf' near line 5 R: $(remoteBundle.myvar)
Thank you for the clarifications, I think I also understand CFEngine a bit better.
Great.
Yeah well, I find it quite puzzling that CFEngine doesn't have this kind of behavior out of the box, I feel like everything rests on this brittle assumption that CFEngine is installed in /var/cfengine .Installing and bootstrapping to a hub and then figuring things out is totally possible. That's I think what most people do. By default the MPF doesn't really do much of anything.
CFEngine being installed in /var/cfengine
has been the standard for CFEngine since it's inception (way back in 1993) as far as I know, it's that way in the oldest docs at least (https://docs.cfengine.com/docs/archive/manuals/cf2-Reference.html#Work-directory). Interestingly enough, the beginnings of developing the file system hierarchy standard (FHS) was around the same time (https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch07s02.html)
So from what I understand - I will have to modify the default MPF update scripts, so that, on the agent, it somehow "calculates" the paths?
It's still not clear to me what paths exactly you are talking about.
There are paths that are involved in building CFEngine, for example between packages built by the maintainers of CFEngine (where everything is installed in /var/cfengine
). Distributions will often build with FHS enabled. It's generally a-typical to run environments with a mix of FHS and non-FHS options. the MPF generally uses variables when referring to CFEngine's paths.
A more concrete example of what you mean might help me.
What I mean is that, with MPF by default, the installation path of CFEngine needs to be the same in every agent.I am not sure what you mean by "is the file structure just coupled together everywhere".
It should not have to need to be. We recommend keeping things the same for simplicity.
Since that is not a configuration we test, you might indeed run into some issues. What are the specific errors?
I think this is it: https://docs.cfengine.com/docs/3.26/reference-promise-types-methods.html#usebundleI meant to ask but apparently forgot, when you say:Can you point me to where you see this?I've seen that bundles can modify variables and define class in the parent scope, but that seems quite horrible.I've said it's "quite horrible" - that might be an exaggeration, it's just weird coming from a programming background.
OK, yeah. CFEngine is certainly different, there is no doubt about that.
I remember being annoyed because when I started with CFEngine I really wanted the promiser to store the returned data something like:
"DEFINE_THIS_VAR" usebundle => child;
Anyway, I got over it I guess. I don't want that anymore, but I still see how it would be a desire.