1# Files bodies
2
3bundle common files_common
4# @brief Enumerate policy files used by this policy file for inclusion to inputs
5{
6  vars:
7      "inputs" slist => { "$(this.promise_dirname)/common.cf" };
8}
9
10body file control
11# @brief Include policy files used by this policy file as part of inputs
12{
13      inputs => { @(files_common.inputs) };
14}
15
16###################################################
17# edit_line bundles
18###################################################
19
20bundle edit_line insert_before_if_no_line(before, string)
21# @brief Insert `string` before `before` if `string` is not found in the file
22# @param before The regular expression matching the line which `string` will be
23# inserted before
24# @param string The string to be prepended
25#
26{
27  insert_lines:
28      "$(string)"
29        location => before($(before)),
30        comment => "Prepend a line to the file if it doesn't already exist";
31}
32
33##
34
35bundle edit_line insert_file(templatefile)
36# @brief Reads the lines from `templatefile` and inserts those into the
37# file being edited.
38# @param templatefile The name of the file from which to import lines.
39{
40  insert_lines:
41
42      "$(templatefile)"
43      comment => "Insert the template file into the file being edited",
44      insert_type => "file";
45}
46
47##
48
49bundle edit_line lines_present(lines)
50# @brief Ensure `lines` are present in the file. Lines that do not exist are appended to the file
51# @param lines List or string that should be present in the file
52#
53# **Example:**
54#
55# ```cf3
56# bundle agent example
57# {
58#  vars:
59#    "nameservers" slist => { "8.8.8.8", "8.8.4.4" };
60#
61#  files:
62#      "/etc/resolv.conf" edit_line => lines_present( @(nameservers) );
63#      "/etc/ssh/sshd_config" edit_line => lines_present( "PermitRootLogin no" );
64# }
65# ```
66{
67  insert_lines:
68
69      "$(lines)"
70        comment => "Append lines if they don't exist";
71}
72
73bundle edit_line insert_lines(lines)
74# @brief Alias for `lines_present`
75# @param lines List or string that should be present in the file
76{
77  insert_lines:
78
79      "$(lines)"
80        comment => "Append lines if they don't exist";
81}
82
83bundle edit_line append_if_no_line(lines)
84# @brief Alias for `lines_present`
85# @param lines List or string that should be present in the file
86{
87  insert_lines:
88
89      "$(lines)"
90        comment => "Append lines if they don't exist";
91}
92
93bundle edit_line append_if_no_lines(lines)
94# @brief Alias for `lines_present`
95# @param lines List or string that should be present in the file
96{
97  insert_lines:
98
99      "$(lines)"
100        comment => "Append lines if they don't exist";
101}
102
103##
104
105bundle edit_line comment_lines_matching(regex,comment)
106# @brief Comment lines in the file that matching an [anchored] regex
107# @param regex Anchored regex that the entire line needs to match
108# @param comment A string that is prepended to matching lines
109{
110  replace_patterns:
111
112      "^($(regex))$"
113
114      replace_with => comment("$(comment)"),
115      comment => "Search and replace string";
116}
117
118##
119
120bundle edit_line contains_literal_string(string)
121# @brief Ensure the literal string is present in the promised file
122# @description If the string is not found in the file it is inserted according
123# to CFEngine defaults.
124# @param string The string (potentially multiline) to ensure exists in the
125# promised file.
126{
127
128   insert_lines:
129     "$(string)"
130       insert_type => "preserve_block",
131       expand_scalars => "false",
132       whitespace_policy => { "exact_match" };
133}
134
135##
136
137bundle edit_line uncomment_lines_matching(regex,comment)
138# @brief Uncomment lines of the file where the regex matches
139# the entire text after the comment string
140# @param regex The regex that lines need to match after `comment`
141# @param comment The prefix of the line that is removed
142{
143  replace_patterns:
144
145      "^$(comment)\s?($(regex))$"
146
147      replace_with => uncomment,
148      comment => "Uncomment lines matching a regular expression";
149}
150
151##
152
153bundle edit_line comment_lines_containing(regex,comment)
154# @brief Comment lines of the file matching a regex
155# @param regex A regex that a part of the line needs to match
156# @param comment A string that is prepended to matching lines
157{
158  replace_patterns:
159
160      "^((?!$(comment)).*$(regex).*)$"
161
162      replace_with => comment("$(comment)"),
163      comment => "Comment out lines in a file";
164}
165
166##
167
168bundle edit_line uncomment_lines_containing(regex,comment)
169# @brief Uncomment lines of the file where the regex matches
170# parts of the text after the comment string
171# @param regex The regex that lines need to match after `comment`
172# @param comment The prefix of the line that is removed
173{
174  replace_patterns:
175
176      "^$(comment)\s?(.*$(regex).*)$"
177
178      replace_with => uncomment,
179      comment => "Uncomment a line containing a fragment";
180}
181
182##
183
184bundle edit_line delete_lines_matching(regex)
185# @brief Delete lines matching a regular expression
186# @param regex The regular expression that the lines need to match
187{
188  delete_lines:
189
190      "$(regex)"
191
192      comment => "Delete lines matching regular expressions";
193}
194
195##
196
197bundle edit_line warn_lines_matching(regex)
198# @brief Warn about lines matching a regular expression
199# @param regex The regular expression that the lines need to match
200{
201  delete_lines:
202
203      "$(regex)"
204
205      comment => "Warn about lines in a file",
206      action => warn_only;
207}
208
209##
210
211bundle edit_line prepend_if_no_line(string)
212# @brief Prepend `string` if it doesn't exist in the file
213# @param string The string to be prepended
214#
215# **See also:** [`insert_lines`][insert_lines] in
216# [`edit_line`][bundle edit_line]
217{
218  insert_lines:
219      "$(string)"
220      location => start,
221      comment => "Prepend a line to the file if it doesn't already exist";
222}
223
224##
225
226bundle edit_line replace_line_end(start,end)
227# @brief Give lines starting with `start` the ending given in `end`
228#
229# Whitespaces will be left unmodified. For example,
230# `replace_line_end("ftp", "2121/tcp")` would replace
231#
232# `"ftp             21/tcp"`
233#
234# with
235#
236# `"ftp             2121/tcp"`
237#
238# @param start The string lines have to start with
239# @param end The string lines should end with
240{
241  field_edits:
242
243      "\s*$(start)\s.*"
244      comment => "Replace lines with $(this.start) and $(this.end)",
245      edit_field => line("(^|\s)$(start)\s*", "2", "$(end)","set");
246}
247
248bundle edit_line replace_uncommented_substrings( _comment, _find, _replace )
249# @brief Replace all occurrences of `_find` with `_replace` on lines that do not follow a `_comment`
250# @param _comment Sequence of characters, each indicating the start of a comment.
251# @param _find String matching substring to replace
252# @param _replace String to substitute `_find` with
253#
254# **Example:**
255#
256# ```cf3
257# bundle agent example_replace_uncommented_substrings
258# {
259#   files:
260#     "/tmp/file.txt"
261#       edit_line => replace_uncommented_substrings( "#", "ME", "YOU");
262# }
263# ```
264#
265# **Notes:**
266#
267# * Only single character comments are supported as `_comment` is used in the PCRE character group (`[^...]`).
268# * `-` in `_comment` is interpreted as a range unless it's used as the first or last character. For example, setting `_comment` to `0-9` means any digit starts a comment.
269#
270# **History:**
271#
272# * Introduced 3.17.0, 3.15.3
273{
274  vars:
275      "_reg_match_uncommented_lines_containing_find"
276        string => "^([^$(_comment)]*)\Q$(_find)\E(.*$)";
277
278  replace_patterns:
279    "$(_reg_match_uncommented_lines_containing_find)"
280      replace_with => text_between_match1_and_match2( $(_replace) );
281}
282
283##
284
285bundle edit_line append_to_line_end(start,end)
286# @brief Append `end` to any lines beginning with `start`
287#
288# `end` will be appended to all lines starting with `start` and not
289# already ending with `end`.  Whitespaces will be left unmodified.
290#
291# For example, `append_to_line_end("kernel", "vga=791")` would replace
292# `kernel /boot/vmlinuz root=/dev/sda7`
293#
294# with
295#
296# `kernel /boot/vmlinuz root=/dev/sda7 vga=791`
297#
298# **WARNING**: Be careful not to have multiple promises matching the same line, which would result in the line growing indefinitely.
299#
300# @param start pattern to match lines of interest
301# @param end string to append to matched lines
302#
303# **Example:**
304#
305# ```cf3
306#  files:
307#      "/tmp/boot-options" edit_line => append_to_line_end("kernel", "vga=791");
308# ```
309#
310{
311  field_edits:
312
313      "\s*$(start)\s.*"
314      comment => "Append lines with $(this.start) and $(this.end)",
315      edit_field => line("(^|\s)$(start)\s*", "2", "$(end)","append");
316}
317
318##
319
320bundle edit_line regex_replace(find,replace)
321# @brief Find exactly a regular expression and replace exactly the match with a string.
322# You can think of this like a PCRE powered sed.
323# @param find The regular expression
324# @param replace The replacement string
325{
326  replace_patterns:
327
328      "$(find)"
329      replace_with => value("$(replace)"),
330      comment => "Search and replace string";
331}
332
333##
334
335bundle edit_line resolvconf(search,list)
336# @brief Adds search domains and name servers to the system
337# resolver configuration.
338#
339# Use this bundle to modify `resolv.conf`. Existing entries for
340# `search` and `nameserver` are replaced.
341#
342# @param search The search domains with space
343# @param list An slist of nameserver addresses
344{
345  delete_lines:
346
347      "search.*"     comment => "Reset search lines from resolver";
348      "nameserver.*" comment => "Reset nameservers in resolver";
349
350  insert_lines:
351
352      "search $(search)"    comment => "Add search domains to resolver";
353      "nameserver $(list)"  comment => "Add name servers to resolver";
354}
355
356##
357
358bundle edit_line resolvconf_o(search,list,options)
359# @brief Adds search domains, name servers and options to the system
360# resolver configuration.
361#
362# Use this bundle to modify `resolv.conf`. Existing entries for
363# `search`, `nameserver` and `options` are replaced.
364#
365# @param search The search domains with space
366# @param list An slist of nameserver addresses
367# @param options is an slist of variables to modify the resolver
368
369{
370  delete_lines:
371
372      "search.*"     comment => "Reset search lines from resolver";
373      "nameserver.*" comment => "Reset nameservers in resolver";
374      "options.*"    comment => "Reset options in resolver";
375
376  insert_lines:
377
378      "search $(search)"    comment => "Add search domains to resolver";
379      "nameserver $(list)"  comment => "Add name servers to resolver";
380      "options $(options)"  comment => "Add options to resolver";
381}
382
383##
384
385bundle edit_line manage_variable_values_ini(tab, sectionName)
386# @brief Sets the RHS of configuration items in the file of the form
387# `LHS=RHS`
388#
389# If the line is commented out with `#`, it gets uncommented first.
390# Adds a new line if none exists.
391# Removes any variable value pairs not defined for the ini section.
392#
393# @param tab An associative array containing `tab[sectionName][LHS]="RHS"`.
394# The value is not changed when the `RHS` is "dontchange"
395# @param sectionName The section in the file within which values should be
396# modified
397#
398# **See also:** `set_variable_values_ini()`
399{
400  vars:
401      "index" slist => getindices("$(tab)[$(sectionName)]");
402
403  delete_lines:
404      ".*"
405      select_region => INI_section(escape("$(sectionName)")),
406      comment       => "Remove all entries in the region so there are no extra entries";
407
408  insert_lines:
409      "[$(sectionName)]"
410      location => start,
411      comment => "Insert lines";
412
413      "$(index)=$($(tab)[$(sectionName)][$(index)])"
414      select_region => INI_section(escape("$(sectionName)"));
415}
416
417##
418
419bundle edit_line set_variable_values_ini(tab, sectionName)
420# @brief Sets the RHS of configuration items in the file of the form
421# `LHS=RHS`
422#
423# If the line is commented out with `#`, it gets uncommented first.
424# Adds a new line if none exists.
425#
426# @param tab An associative array containing `tab[sectionName][LHS]="RHS"`.
427# The value is not changed when the `RHS` is "dontchange"
428# @param sectionName The section in the file within which values should be
429# modified
430#
431# **See also:** `manage_variable_values_ini()`
432{
433  vars:
434      "index" slist => getindices("$(tab)[$(sectionName)]");
435
436      # Be careful if the index string contains funny chars
437      "cindex[$(index)]" string => canonify("$(index)");
438
439  classes:
440      "edit_$(cindex[$(index)])"     not => strcmp("$($(tab)[$(sectionName)][$(index)])","dontchange"),
441      comment => "Create conditions to make changes";
442
443  field_edits:
444
445      # If the line is there, but commented out, first uncomment it
446      "#+\s*$(index)\s*=.*"
447      select_region => INI_section(escape("$(sectionName)")),
448      edit_field => col("\s*=\s*","1","$(index)","set"),
449      if => "edit_$(cindex[$(index)])";
450
451      # match a line starting like the key something
452      "\s*$(index)\s*=.*"
453      edit_field => col("\s*=\s*","2","$($(tab)[$(sectionName)][$(index)])","set"),
454      select_region => INI_section(escape("$(sectionName)")),
455      classes => results("bundle", "set_variable_values_ini_not_$(cindex[$(index)])"),
456      if => "edit_$(cindex[$(index)])";
457
458  insert_lines:
459      "[$(sectionName)]"
460      location => start,
461      comment => "Insert lines";
462
463      "$(index)=$($(tab)[$(sectionName)][$(index)])"
464      select_region => INI_section(escape("$(sectionName)")),
465        if => "!(set_variable_values_ini_not_$(cindex[$(index)])_kept|set_variable_values_ini_not_$(cindex[$(index)])_repaired).edit_$(cindex[$(index)])";
466
467}
468
469bundle edit_line insert_ini_section(name, config)
470# @brief Inserts a INI section with content
471#
472# ```
473# # given an array "barray"
474# files:
475#     "myfile.ini" edit_line => insert_ini_section("foo", "barray");
476# ```
477#
478# Inserts a section in an INI file with the given configuration
479# key-values from the array `config`.
480#
481# @param name the name of the INI section
482# @param config The fully-qualified name of an associative array containing `v[LHS]="rhs"`
483{
484  vars:
485      # TODO: refactor once 3.7.x is EOL
486      "indices" slist => getindices($(config));
487      "k" slist => sort("indices", lex);
488
489  insert_lines:
490      "[$(name)]"
491      location => start,
492      comment => "Insert an ini section with values if not present";
493
494      "$(k)=$($(config)[$(k)])"
495      location => after("[$(name)]");
496}
497
498
499bundle edit_line set_quoted_values(v)
500# @brief Sets the RHS of variables in shell-like files of the form:
501#
502# ```
503#      LHS="RHS"
504# ```
505#
506# Adds a new line if no LHS exists, and replaces RHS values if one does exist.
507# If the line is commented out with #, it gets uncommented first.
508#
509# @param v The fully-qualified name of an associative array containing `v[LHS]="rhs"`
510#
511# **Example:**
512#
513# ```cf3
514#     vars:
515#        "stuff[lhs-1]" string => "rhs1";
516#        "stuff[lhs-2]" string => "rhs2";
517#
518#     files:
519#        "myfile"
520#          edit_line => set_quoted_values(stuff)
521# ```
522#
523# **See also:** `set_variable_values()`
524{
525  meta:
526      "tags"
527      slist =>
528      {
529        "deprecated=3.6.0",
530        "deprecation-reason=Generic reimplementation",
531        "replaced-by=set_line_based"
532      };
533
534  vars:
535      "index" slist => getindices("$(v)");
536      # Be careful if the index string contains funny chars
537
538      "cindex[$(index)]" string => canonify("$(index)");
539
540  field_edits:
541      # If the line is there, but commented out, first uncomment it
542      "#+\s*$(index)\s*=.*"
543      edit_field => col("=","1","$(index)","set");
544
545      # match a line starting like the key = something
546      "\s*$(index)\s*=.*"
547      edit_field => col("=","2",'"$($(v)[$(index)])"',"set"),
548      classes    => results("bundle", "$(cindex[$(index)])_in_file"),
549      comment    => "Match a line starting like key = something";
550
551  insert_lines:
552      '$(index)="$($(v)[$(index)])"'
553      comment    => "Insert a variable definition",
554        if => "!($(cindex[$(index)])_in_file_kept|$(cindex[$(index)])_in_file_repaired)";
555}
556
557##
558
559bundle edit_line set_variable_values(v)
560# @brief Sets the RHS of variables in files of the form:
561#
562# ```
563#      LHS=RHS
564# ```
565#
566# Adds a new line if no LHS exists, and replaces RHS values if one does exist.
567# If the line is commented out with #, it gets uncommented first.
568#
569# @param v The fully-qualified name of an associative array containing `v[LHS]="rhs"`
570#
571# **Example:**
572#
573# ```cf3
574#     vars:
575#        "stuff[lhs-1]" string => "rhs1";
576#        "stuff[lhs-2]" string => "rhs2";
577#
578#     files:
579#        "myfile"
580#          edit_line => set_variable_values(stuff)
581# ```
582#
583# **See also:** `set_quoted_values()`
584{
585  meta:
586      "tags"
587      slist =>
588      {
589        "deprecated=3.6.0",
590        "deprecation-reason=Generic reimplementation",
591        "replaced-by=set_line_based"
592      };
593
594  vars:
595
596      "index" slist => getindices("$(v)");
597
598      # Be careful if the index string contains funny chars
599
600      "cindex[$(index)]" string => canonify("$(index)");
601      "cv"               string => canonify("$(v)");
602
603  field_edits:
604
605      # match a line starting like the key = something
606
607      "\s*$(index)\s*=.*"
608
609      edit_field => col("\s*$(index)\s*=","2","$($(v)[$(index)])","set"),
610      classes => results("bundle", "$(cv)_$(cindex[$(index)])_in_file"),
611      comment => "Match a line starting like key = something";
612
613  insert_lines:
614
615      "$(index)=$($(v)[$(index)])"
616
617      comment => "Insert a variable definition",
618        if => "!($(cv)_$(cindex[$(index)])_in_file_kept|$(cv)_$(cindex[$(index)])_in_file_repaired)";
619}
620
621bundle edit_line set_config_values(v)
622# @brief Sets the RHS of configuration items in the file of the form:
623#
624# ```
625#   LHS RHS
626# ```
627#
628# If the line is commented out with `#`, it gets uncommented first.
629#
630# Adds a new line if none exists.
631#
632# @param v The fully-qualified name of an associative array containing `v[LHS]="rhs"`
633{
634  meta:
635      "tags"
636      slist =>
637      {
638        "deprecated=3.6.0",
639        "deprecation-reason=Generic reimplementation",
640        "replaced-by=set_line_based"
641      };
642
643  vars:
644      "index" slist => getindices("$(v)");
645
646      # Be careful if the index string contains funny chars
647      "cindex[$(index)]" string => canonify("$(index)");
648
649      # Escape the value (had a problem with special characters and regex's)
650      "ev[$(index)]" string => escape("$($(v)[$(index)])");
651
652      # Do we have more than one line commented out?
653      "index_comment_matches_$(cindex[$(index)])"
654        int => countlinesmatching("^\s*#\s*($(index)\s+.*|$(index))$","$(edit.filename)");
655
656
657  classes:
658      # Check to see if this line exists
659      "line_exists_$(cindex[$(index)])"
660        expression => regline("^\s*($(index)\s.*|$(index))$","$(edit.filename)"),
661        scope => "bundle";
662
663      # if there's more than one comment, just add new (don't know who to use)
664      "multiple_comments_$(cindex[$(index)])"
665        expression => isgreaterthan("$(index_comment_matches_$(cindex[$(index)]))","1"),
666        scope => "bundle";
667
668  replace_patterns:
669      # If the line is commented out, uncomment and replace with
670      # the correct value
671      "^\s*#\s*($(index)\s+.*|$(index))$"
672        comment => "If we find a single commented entry we can uncomment it to
673                    keep the settings near any inline documentation. If there
674                    are multiple comments, then we don't try to replace them and
675                    instead will later append the new value after the first
676                    commented occurrence of $(index).",
677        handle => "set_config_values_replace_commented_line",
678        replace_with => value("$(index) $($(v)[$(index)])"),
679                  if => "!line_exists_$(cindex[$(index)]).!replace_attempted_$(cindex[$(index)])_reached.!multiple_comments_$(cindex[$(index)])",
680             classes => results("bundle", "uncommented_$(cindex[$(index)])");
681
682      # If the line is there with the wrong value, replace with
683      # the correct value
684      "^\s*($(index)\s+(?!$(ev[$(index)])$).*|$(index))$"
685           comment => "Correct the value $(index)",
686      replace_with => value("$(index) $($(v)[$(index)])"),
687           classes => results("bundle", "replace_attempted_$(cindex[$(index)])");
688
689  insert_lines:
690      # If the line doesn't exist, or there is more than one occurrence
691      # of the LHS commented out, insert a new line and try to place it
692      # after the commented LHS (keep new line with old comments)
693      "$(index) $($(v)[$(index)])"
694         comment => "Insert the value, marker exists $(index)",
695        location => after("^\s*#\s*($(index)\s+.*|$(index))$"),
696              if => "replace_attempted_$(cindex[$(index)])_reached.multiple_comments_$(cindex[$(index)])";
697
698      # If the line doesn't exist and there are no occurrences
699      # of the LHS commented out, insert a new line at the eof
700      "$(index) $($(v)[$(index)])"
701         comment => "Insert the value, marker doesn't exist $(index)",
702              if => "replace_attempted_$(cindex[$(index)])_reached.!multiple_comments_$(cindex[$(index)])";
703
704}
705
706bundle edit_line set_line_based(v, sep, bp, kp, cp)
707# @brief Sets the RHS of configuration items in the file of the form:
708#
709# ```
710#   LHS$(sep)RHS
711# ```
712#
713# Example usage for `x=y` lines (e.g. rsyncd.conf):
714#
715# ```cf3
716# "myfile"
717# edit_line => set_line_based("test.config", "=", "\s*=\s*", ".*", "\s*#\s*");
718# ```
719#
720# Example usage for `x y` lines (e.g. sshd_config):
721#
722# ```cf3
723# "myfile"
724# edit_line => set_line_based("test.config", " ", "\s+", ".*", "\s*#\s*");
725# ```
726#
727# If the line is commented out with `$(cp)`, it gets uncommented first.
728#
729# Adds a new line if none exists or if more than one commented-out
730# possible matches exist.
731#
732#
733# **Note:** If the data structure being used for the first parameter is in the current bundle, you can use `$(this.bundle).variable`.
734#
735# Originally `set_config_values` by Ed King.
736#
737# @param v The fully-qualified name (`bundlename.variable`) of an associative array containing `v[LHS]="rhs"`
738# @param sep The separator to insert, e.g. ` ` for space-separated
739# @param bp The key-value separation regex, e.g. `\s+` for space-separated
740# @param kp The keys to select from v, use `.*` for all
741# @param cp The comment pattern from line-start, e.g. `\s*#\s*`
742{
743  meta:
744      "tags"
745      slist =>
746      {
747        "replaces=set_config_values",
748        "replaces=set_config_values_matching",
749        "replaces=set_variable_values",
750        "replaces=set_quoted_values",
751        "replaces=maintain_key_values",
752      };
753
754  vars:
755      "vkeys" slist => getindices("$(v)");
756      "i" slist => grep($(kp), vkeys);
757
758      # Be careful if the index string contains funny chars
759      "ci[$(i)]" string => canonify("$(i)");
760
761      # Escape the value (had a problem with special characters and regex's)
762      "ev[$(i)]" string => escape("$($(v)[$(i)])");
763
764      # Do we have more than one line commented out?
765      "comment_matches_$(ci[$(i)])"
766      int => countlinesmatching("^$(cp)($(i)$(bp).*|$(i))$",
767                                $(edit.filename));
768
769
770  classes:
771      # Check to see if this line exists
772      "exists_$(ci[$(i)])"
773      expression => regline("^\s*($(i)$(bp).*|$(i))$",
774                            $(edit.filename));
775
776      # if there's more than one comment, just add new (don't know who to use)
777      "multiple_comments_$(ci[$(i)])"
778      expression => isgreaterthan("$(comment_matches_$(ci[$(i)]))",
779                                  "1");
780
781
782  replace_patterns:
783      # If the line is commented out, uncomment and replace with
784      # the correct value
785      "^$(cp)($(i)$(bp).*|$(i))$"
786             comment => "Uncommented the value '$(i)'",
787        replace_with => value("$(i)$(sep)$($(v)[$(i)])"),
788                  if => "!exists_$(ci[$(i)]).!replace_attempted_$(ci[$(i)])_reached.!multiple_comments_$(ci[$(i)])",
789             classes => results("bundle", "uncommented_$(ci[$(i)])");
790
791      # If the line is there with the wrong value, replace with
792      # the correct value
793      "^\s*($(i)$(bp)(?!$(ev[$(i)])$).*|$(i))$"
794           comment => "Correct the value '$(i)'",
795      replace_with => value("$(i)$(sep)$($(v)[$(i)])"),
796           classes => results("bundle", "replace_attempted_$(ci[$(i)])");
797
798  insert_lines:
799      # If the line doesn't exist, or there is more than one occurrence
800      # of the LHS commented out, insert a new line and try to place it
801      # after the commented LHS (keep new line with old comments)
802      "$(i)$(sep)$($(v)[$(i)])"
803         comment => "Insert the value, marker '$(i)' exists",
804        location => after("^$(cp)($(i)$(bp).*|$(i))$"),
805              if => "replace_attempted_$(ci[$(i)])_reached.multiple_comments_$(ci[$(i)])";
806
807      # If the line doesn't exist and there are no occurrences
808      # of the LHS commented out, insert a new line at the eof
809      "$(i)$(sep)$($(v)[$(i)])"
810         comment => "Insert the value, marker '$(i)' doesn't exist",
811              if => "replace_attempted_$(ci[$(i)])_reached.!multiple_comments_$(ci[$(i)]).!exists_$(ci[$(i)])";
812
813  reports:
814    verbose_mode|EXTRA::
815      "$(this.bundle): Line for '$(i)' exists" if => "exists_$(ci[$(i)])";
816      "$(this.bundle): Line for '$(i)' does not exist" if => "!exists_$(ci[$(i)])";
817}
818
819bundle edit_line set_config_values_matching(v,pat)
820# @brief Sets the RHS of configuration items in the file of the form
821#
822# ```
823#   LHS RHS
824# ```
825#
826# If the line is commented out with `#`, it gets uncommented first.
827# Adds a new line if none exists.
828#
829# @param v the fully-qualified name of an associative array containing v[LHS]="rhs"
830# @param pat Only elements of `v` that match the regex `pat` are use
831{
832  meta:
833      "tags"
834      slist =>
835      {
836        "deprecated=3.6.0",
837        "deprecation-reason=Generic reimplementation",
838        "replaced-by=set_line_based"
839      };
840
841  vars:
842      "allparams" slist => getindices("$(v)");
843      "index"     slist => grep("$(pat)", "allparams");
844
845      # Be careful if the index string contains funny chars
846      "cindex[$(index)]" string => canonify("$(index)");
847
848  replace_patterns:
849      # If the line is there, maybe commented out, uncomment and replace with
850      # the correct value
851      "^\s*($(index)\s+(?!$($(v)[$(index)])).*|# ?$(index)\s+.*)$"
852      comment => "Correct the value",
853      replace_with => value("$(index) $($(v)[$(index)])"),
854      classes => results("bundle", "replace_attempted_$(cindex[$(index)])");
855
856  insert_lines:
857      "$(index) $($(v)[$(index)])"
858      if => "replace_attempted_$(cindex[$(index)])_reached";
859
860}
861
862##
863
864bundle edit_line maintain_key_values(v,sep)
865# @brief Sets the RHS of configuration items with an giving separator
866#
867# Contributed by David Lee
868{
869  meta:
870      "tags"
871      slist =>
872      {
873        "deprecated=3.6.0",
874        "deprecation-reason=Generic reimplementation",
875        "replaced-by=set_line_based"
876      };
877
878  vars:
879      "index" slist => getindices("$(v)");
880      # Be careful if the index string contains funny chars
881      "cindex[$(index)]" string => canonify("$(index)");
882      # Matching pattern for line (basically key-and-separator)
883      "keypat[$(index)]" string => "\s*$(index)\s*$(sep)\s*";
884
885      # Values may contain regexps. Escape them for replace_pattern matching.
886      "ve[$(index)]" string => escape("$($(v)[$(index)])");
887
888  classes:
889      "$(cindex[$(index)])_key_in_file"
890      comment => "Dynamic Class created if patterns matching",
891      expression => regline("^$(keypat[$(index)]).*", "$(edit.filename)");
892
893  replace_patterns:
894      # For convergence need to use negative lookahead on value:
895      # "key sep (?!value).*"
896      "^($(keypat[$(index)]))(?!$(ve[$(index)])$).*"
897      comment => "Replace definition of $(index)",
898      replace_with => value("$(match.1)$($(v)[$(index)])");
899
900  insert_lines:
901      "$(index)$(sep)$($(v)[$(index)])"
902      comment => "Insert definition of $(index)",
903      if => "!$(cindex[$(index)])_key_in_file";
904}
905
906##
907
908bundle edit_line append_users_starting(v)
909# @brief For adding to `/etc/passwd` or `etc/shadow`
910# @param v An array `v[username] string => "line..."`
911#
912# **Note:** To manage local users with CFEngine 3.6 and later,
913# consider making `users` promises instead of modifying system files.
914{
915  vars:
916
917      "index"        slist => getindices("$(v)");
918
919  classes:
920
921      "add_$(index)"     not => userexists("$(index)"),
922      comment => "Class created if user does not exist";
923
924  insert_lines:
925
926      "$($(v)[$(index)])"
927
928      comment => "Append users into a password file format",
929      if => "add_$(index)";
930}
931
932##
933
934bundle edit_line append_groups_starting(v)
935# @brief For adding groups to `/etc/group`
936# @param v An array `v[groupname] string => "line..."`
937#
938# **Note:** To manage local users with CFEngine 3.6 and later,
939# consider making `users` promises instead of modifying system files.
940{
941  vars:
942
943      "index"        slist => getindices("$(v)");
944
945  classes:
946
947      "add_$(index)"     not => groupexists("$(index)"),
948      comment => "Class created if group does not exist";
949
950  insert_lines:
951
952      "$($(v)[$(index)])"
953
954      comment => "Append users into a group file format",
955      if => "add_$(index)";
956
957}
958
959##
960
961bundle edit_line set_colon_field(key,field,val)
962# @brief Set the value of field number `field` of the line whose
963# first field is `key` to the value `val`, in a colon-separated file.
964# @param key The value the first field has to match
965# @param field The field to be modified
966# @param val The new value of `field`
967{
968  field_edits:
969
970      "$(key):.*"
971
972      comment => "Edit a colon-separated file, using the first field as a key",
973      edit_field => col(":","$(field)","$(val)","set");
974}
975
976##
977
978bundle edit_line set_user_field(user,field,val)
979# @brief Set the value of field number "field" in a `:-field`
980# formatted file like `/etc/passwd`
981# @param user The user to be modified
982# @param field The field that should be modified
983# @param val The value for `field`
984#
985# **Note:** To manage local users with CFEngine 3.6 and later,
986# consider making `users` promises instead of modifying system files.
987{
988  field_edits:
989
990      "$(user):.*"
991
992      comment => "Edit a user attribute in the password file",
993      edit_field => col(":","$(field)","$(val)","set");
994}
995
996##
997
998bundle edit_line append_user_field(group,field,allusers)
999# @brief For adding users to to a file like `/etc/group`
1000# at field position `field`, comma separated subfields
1001# @param group The group to be modified
1002# @param field The field where users should be added
1003# @param allusers The list of users to add to `field`
1004#
1005# **Note:** To manage local users with CFEngine 3.6 and later,
1006# consider making `users` promises instead of modifying system files.
1007{
1008  field_edits:
1009
1010      "$(group):.*"
1011
1012      comment => "Append users into a password file format",
1013      edit_field => col(":","$(field)","$(allusers)","alphanum");
1014}
1015
1016##
1017
1018bundle edit_line expand_template(templatefile)
1019# @brief Read in the named text file and expand `$(var)` inside the file
1020# @param templatefile The name of the file
1021{
1022  insert_lines:
1023
1024      "$(templatefile)"
1025
1026      insert_type => "file",
1027      comment => "Expand variables in the template file",
1028      expand_scalars => "true";
1029}
1030
1031bundle edit_line replace_or_add(pattern,line)
1032# @brief Replace a pattern in a file with a single line.
1033#
1034# If the pattern is not found, add the line to the file.
1035#
1036# @param pattern The pattern that should be replaced
1037# The pattern must match the whole line (it is automatically
1038# anchored to the start and end of the line) to avoid
1039# ambiguity.
1040# @param line The line with which to replace matches of `pattern`
1041{
1042  vars:
1043      "cline" string => canonify("$(line)");
1044      "eline" string => escape("$(line)");
1045
1046  replace_patterns:
1047      "^(?!$(eline)$)$(pattern)$"
1048      comment => "Replace a pattern here",
1049      replace_with => value("$(line)"),
1050      classes => results("bundle", "replace_$(cline)");
1051
1052  insert_lines:
1053      "$(line)"
1054      if => "replace_$(cline)_reached";
1055}
1056
1057bundle edit_line converge(marker, lines)
1058# @brief Converge `lines` marked with `marker`
1059#
1060# Any content marked with `marker` is removed, then `lines` are
1061# inserted.  Every `line` should contain `marker`.
1062#
1063# @param marker The marker (not a regular expression; will be escaped)
1064# @param lines The lines to insert; all must contain `marker`
1065#
1066# **Example:**
1067#
1068# ```cf3
1069# bundle agent pam_d_su_include
1070# #@brief Ensure /etc/pam.d/su has includes configured properly
1071# {
1072#   files:
1073#     ubuntu::
1074#       "/etc/pam.d/su"
1075#         edit_line => converge( "@include", "@include common-auth
1076# @include common-account
1077# @include common-session");
1078# }
1079# ```
1080#
1081# **History:**
1082#
1083# * Introduced in 3.6.0
1084{
1085  vars:
1086      "regex" string => escape($(marker));
1087
1088  delete_lines:
1089      ".*$(regex).*" comment => "Delete lines matching the marker";
1090  insert_lines:
1091      "$(lines)" comment => "Insert the given lines";
1092}
1093
1094bundle edit_line converge_prepend(marker, lines)
1095# @brief Converge `lines` marked with `marker` to start of content
1096#
1097# Any content marked with `marker` is removed, then `lines` are
1098# inserted at *start* of content.  Every `line` should contain `marker`.
1099#
1100# @param marker The marker (not a regular expression; will be escaped)
1101# @param lines The lines to insert; all must contain `marker`
1102#
1103# **Example:**
1104#
1105# ```cf3
1106# bundle agent pam_d_su_session
1107# #@brief Ensure /etc/pam.d/su has session configured properly
1108# {
1109#   files:
1110#     ubuntu::
1111#       "/etc/pam.d/su"
1112#         edit_line => converge_prepend( "session", "session       required   pam_env.so readenv=1 envfile=/etc/default/locale
1113# session    optional   pam_mail.so nopen
1114# session    required   pam_limits.so" );
1115# }
1116# ```
1117#
1118# **History:**
1119#
1120# * Introduced in 3.17.0, 3.15.3, 3.12.6
1121{
1122  vars:
1123      "regex" string => escape($(marker));
1124
1125  delete_lines:
1126      ".*$(regex).*" comment => "Delete lines matching the marker";
1127  insert_lines:
1128      "$(lines)" location => start, comment => "Insert the given lines";
1129}
1130
1131
1132bundle edit_line fstab_option_editor(method, mount, option)
1133# @brief Add or remove `/etc/fstab` options for a mount
1134#
1135# This bundle edits the options field of a mount.  The `method` is a
1136# `field_operation` which can be `append`, `prepend`, `set`, `delete`,
1137# or `alphanum`.  The option is OS-specific.
1138#
1139# @param method `field_operation` to apply
1140# @param mount the mount point
1141# @param option the option to add or remove
1142#
1143# **Example:**
1144#
1145# ```cf3
1146#  files:
1147#      "/etc/fstab" edit_line => fstab_option_editor("delete", "/", "acl");
1148#      "/etc/fstab" edit_line => fstab_option_editor("append", "/", "acl");
1149# ```
1150{
1151   field_edits:
1152      "(?!#)\S+\s+$(mount)\s.+"
1153      edit_field => fstab_options($(option), $(method));
1154}
1155
1156##-------------------------------------------------------
1157## editing bodies
1158##-------------------------------------------------------
1159
1160body edit_field fstab_options(newval, method)
1161# @brief Edit the options field in a fstab format
1162# @param newval the new option
1163# @param method `field_operation` to apply
1164#
1165# This body edits the options field in the fstab file format.  The
1166# `method` is a `field_operation` which can be `append`, `prepend`,
1167# `set`, `delete`, or `alphanum`.  The `newval` option is OS-specific.
1168#
1169# **Example:**
1170#
1171# ```cf3
1172#   # from the `fstab_options_editor`
1173#   field_edits:
1174#      "(?!#)\S+\s+$(mount)\s.+"
1175#      edit_field => fstab_options($(option), $(method));
1176# ```
1177{
1178      field_separator => "\s+";
1179      select_field    => "4";
1180      value_separator  => ",";
1181      field_value     => "$(newval)";
1182      field_operation => "$(method)";
1183}
1184
1185body edit_field quoted_var(newval,method)
1186# @brief Edit the quoted value of the matching line
1187# @param newval The new value
1188# @param method The method by which to edit the field
1189{
1190      field_separator => "\"";
1191      select_field    => "2";
1192      value_separator  => " ";
1193      field_value     => "$(newval)";
1194      field_operation => "$(method)";
1195      extend_fields => "false";
1196      allow_blank_fields => "true";
1197}
1198
1199##
1200
1201body edit_field col(split,col,newval,method)
1202# @brief Edit tabluar data with comma-separated sub-values
1203# @param split The separator that defines columns
1204# @param col The (1-based) index of the value to change
1205# @param newval The new value
1206# @param method The method by which to edit the field
1207{
1208      field_separator    => "$(split)";
1209      select_field       => "$(col)";
1210      value_separator    => ",";
1211      field_value        => "$(newval)";
1212      field_operation    => "$(method)";
1213      extend_fields      => "true";
1214      allow_blank_fields => "true";
1215}
1216
1217##
1218
1219body edit_field line(split,col,newval,method)
1220# @brief Edit tabular data with space-separated sub-values
1221# @param split The separator that defines columns
1222# @param col The (1-based) index of the value to change
1223# @param newval The new value
1224# @param method The method by which to edit the field
1225{
1226      field_separator    => "$(split)";
1227      select_field       => "$(col)";
1228      value_separator    => " ";
1229      field_value        => "$(newval)";
1230      field_operation    => "$(method)";
1231      extend_fields      => "true";
1232      allow_blank_fields => "true";
1233}
1234
1235##
1236
1237body replace_with text_between_match1_and_match2( _text )
1238# @brief Replace matched line with substituted string
1239# @param _text String to substitute between first and second match
1240{
1241        replace_value => "$(match.1)$(_text)$(match.2)";
1242        occurrences => "all";
1243}
1244
1245body replace_with value(x)
1246# @brief Replace matching lines
1247# @param x The replacement string
1248{
1249      replace_value => "$(x)";
1250      occurrences => "all";
1251}
1252
1253##
1254
1255body select_region INI_section(x)
1256# @brief Restrict the `edit_line` promise to the lines in section `[x]`
1257# @param x The name of the section in an INI-like configuration file
1258{
1259      select_start => "\[$(x)\]\s*";
1260      select_end => "\[.*\]\s*";
1261
1262@if minimum_version(3.10)
1263      select_end_match_eof => "true";
1264@endif
1265}
1266
1267##-------------------------------------------------------
1268## edit_defaults
1269##-------------------------------------------------------
1270
1271body edit_defaults std_defs
1272# @brief Standard definitions for `edit_defaults`
1273# Don't empty the file before editing starts and don't make a backup.
1274{
1275      empty_file_before_editing => "false";
1276      edit_backup => "false";
1277      #max_file_size => "300000";
1278}
1279
1280##
1281
1282body edit_defaults empty
1283# @brief Empty the file before editing
1284#
1285# No backup is made
1286{
1287      empty_file_before_editing => "true";
1288      edit_backup => "false";
1289      #max_file_size => "300000";
1290}
1291
1292##
1293
1294body edit_defaults no_backup
1295# @brief Don't make a backup of the file before editing
1296{
1297      edit_backup => "false";
1298}
1299
1300##
1301
1302body edit_defaults backup_timestamp
1303# @brief Make a timestamped backup of the file before editing
1304{
1305      empty_file_before_editing => "false";
1306      edit_backup => "timestamp";
1307      #max_file_size => "300000";
1308}
1309
1310##-------------------------------------------------------
1311## location
1312##-------------------------------------------------------
1313
1314body location start
1315# @brief Editing occurs before the matched line
1316{
1317      before_after => "before";
1318}
1319
1320##
1321
1322body location after(str)
1323# @brief Editing occurs after the line matching `str`
1324# @param str Regular expression matching the file line location
1325{
1326      before_after => "after";
1327      select_line_matching => "$(str)";
1328}
1329
1330##
1331
1332body location before(str)
1333# @brief Editing occurs before the line matching `str`
1334# @param str Regular expression matching the file line location
1335{
1336      before_after => "before";
1337      select_line_matching => "$(str)";
1338}
1339
1340##-------------------------------------------------------
1341## replace_with
1342##-------------------------------------------------------
1343
1344##
1345
1346body replace_with comment(c)
1347# @brief Comment all lines matching the pattern by preprending `c`
1348# @param c The prefix that comments out lines
1349{
1350      replace_value => "$(c) $(match.1)";
1351      occurrences => "all";
1352}
1353
1354##
1355
1356body replace_with uncomment
1357# @brief Uncomment all lines matching the pattern by removing
1358# anything outside the matching string
1359{
1360      replace_value => "$(match.1)";
1361      occurrences => "all";
1362}
1363
1364##-------------------------------------------------------
1365## copy_from
1366##-------------------------------------------------------
1367
1368body copy_from secure_cp(from,server)
1369# @brief Download a file from a remote server over an encrypted channel
1370#
1371# Only copy the file if it is different from the local copy, and verify
1372# that the copy is correct.
1373#
1374# @param from The location of the file on the remote server
1375# @param server The hostname or IP of the server from which to download
1376{
1377      source      => "$(from)";
1378      servers     => { "$(server)" };
1379      compare     => "digest";
1380      encrypt     => "true";
1381      verify      => "true";
1382}
1383
1384##
1385
1386body copy_from remote_cp(from,server)
1387# @brief Download a file from a remote server.
1388#
1389# @param from The location of the file on the remote server
1390# @param server The hostname or IP of the server from which to download
1391{
1392      servers     => { "$(server)" };
1393      source      => "$(from)";
1394      compare     => "mtime";
1395}
1396
1397##
1398
1399body copy_from remote_dcp(from,server)
1400# @brief Download a file from a remote server if it is different from the local copy.
1401#
1402# @param from The location of the file on the remote server
1403# @param server The hostname or IP of the server from which to download
1404#
1405# **See Also:** `local_dcp()`
1406{
1407      servers     => { "$(server)" };
1408      source      => "$(from)";
1409      compare     => "digest";
1410}
1411
1412##
1413
1414body copy_from local_cp(from)
1415# @brief Copy a file if the modification time or creation time of the source
1416# file is newer (the default comparison mechanism).
1417# @param from The path to the source file.
1418#
1419# **Example:**
1420#
1421# ```cf3
1422# bundle agent example
1423# {
1424#   files:
1425#       "/tmp/file.bak"
1426#         copy_from => local_cp("/tmp/file");
1427# }
1428# ```
1429#
1430# **See Also:** `local_dcp()`
1431{
1432      source      => "$(from)";
1433}
1434
1435##
1436
1437body copy_from local_dcp(from)
1438# @brief Copy a local file if the hash on the source file differs.
1439# @param from The path to the source file.
1440#
1441# **Example:**
1442#
1443# ```cf3
1444# bundle agent example
1445# {
1446#   files:
1447#       "/tmp/file.bak"
1448#       copy_from => local_dcp("/tmp/file");
1449# }
1450# ```
1451#
1452# **See Also:** `local_cp()`, `remote_dcp()`
1453{
1454      source      => "$(from)";
1455      compare     => "digest";
1456}
1457
1458##
1459
1460body copy_from perms_cp(from)
1461# @brief Copy a local file and preserve file permissions on the local copy.
1462#
1463# @param from The path to the source file.
1464{
1465      source      => "$(from)";
1466      preserve    => "true";
1467}
1468
1469body copy_from perms_dcp(from)
1470# @brief Copy a local file if it is different from the existing copy and
1471# preserve file permissions on the local copy.
1472#
1473# @param from The path to the source file.
1474{
1475      source      => "$(from)";
1476      preserve    => "true";
1477      compare     => "digest";
1478}
1479
1480body copy_from backup_local_cp(from)
1481# @brief Copy a local file and  keep a backup of old versions.
1482#
1483# @param from The path to the source file.
1484{
1485      source      => "$(from)";
1486      copy_backup => "timestamp";
1487}
1488
1489##
1490
1491body copy_from seed_cp(from)
1492# @brief Copy a local file if the file does not already exist, i.e. seed the
1493# placement
1494# @param from The path to the source file.
1495#
1496# **Example:**
1497#
1498# ```cf3
1499# bundle agent home_dir_init
1500# {
1501#   files:
1502#       "/home/mark.burgess/."
1503#       copy_from => seed_cp("/etc/skel"),
1504#       depth_search => recurse(inf),
1505#       file_select => all,
1506#       comment => "We want to be sure that the home directory has files that are
1507#                   present in the skeleton.";
1508# }
1509# ```
1510{
1511      source      => "$(from)";
1512      compare     => "exists";
1513}
1514
1515##
1516
1517body copy_from sync_cp(from,server)
1518# @brief Synchronize a file with a remote server.
1519#
1520# * If the file does not exist on the remote server then it should be purged.
1521# * Allow types to change (directories to files and vice versa).
1522# * The mode of the remote file should be preserved.
1523# * Files are compared using the default comparison (mtime or ctime).
1524#
1525# @param from The location of the file on the remote server
1526# @param server The hostname or IP of the server from which to download
1527#
1528# **Example**:
1529#
1530# ```cf3
1531# files:
1532#   "/tmp/masterfiles/."
1533#     copy_from => sync_cp( "/var/cfengine/masterfiles", $(sys.policy_server) ),
1534#     depth_search => recurse(inf),
1535#     file_select => all,
1536#     comment => "Mirror masterfiles from the hub to a temporary directory";
1537# ```
1538#
1539# **See Also:** `dir_sync()`, `copyfrom_sync()`
1540{
1541      servers     => { "$(server)" };
1542      source      => "$(from)";
1543      purge       => "true";
1544      preserve    => "true";
1545      type_check  => "false";
1546}
1547
1548##
1549
1550body copy_from no_backup_cp(from)
1551# @brief Copy a local file and don't make any backup of the previous version
1552#
1553# @param from The path to the source file.
1554{
1555      source      => "$(from)";
1556      copy_backup => "false";
1557}
1558
1559##
1560
1561body copy_from no_backup_dcp(from)
1562# @brief Copy a local file if contents have changed, and don't make any backup
1563# of the previous version
1564#
1565# @param from The path to the source file.
1566{
1567      source      => "$(from)";
1568      copy_backup => "false";
1569      compare     => "digest";
1570}
1571
1572##
1573
1574body copy_from no_backup_rcp(from,server)
1575# @brief Download a file if it's newer than the local copy, and don't make any
1576# backup of the previous version
1577#
1578# @param from The location of the file on the remote server
1579# @param server The hostname or IP of the server from which to download
1580{
1581      servers     => { "$(server)" };
1582      source      => "$(from)";
1583      compare     => "mtime";
1584      copy_backup => "false";
1585}
1586
1587##-------------------------------------------------------
1588## link_from
1589##-------------------------------------------------------
1590
1591body link_from ln_s(x)
1592# @brief Create a symbolink link to `x`
1593# The link is created even if the source of the link does not exist.
1594# @param x The source of the link
1595{
1596      link_type => "symlink";
1597      source => "$(x)";
1598      when_no_source => "force";
1599}
1600
1601##
1602
1603body link_from linkchildren(tofile)
1604# @brief Create a symbolink link to `tofile`
1605# If the promiser is a directory, children are linked to the source, unless
1606# entries with identical names already exist.
1607# The link is created even if the source of the link does not exist.
1608#
1609# @param tofile The source of the link
1610{
1611      source        => "$(tofile)";
1612      link_type     => "symlink";
1613      when_no_source  => "force";
1614      link_children => "true";
1615      when_linking_children => "if_no_such_file"; # "override_file";
1616}
1617
1618body link_from linkfrom(source, type)
1619# @brief Make any kind of link to a file
1620# @param source link to this
1621# @param type the link's type (`symlink` or `hardlink`)
1622{
1623      source => $(source);
1624      link_type => $(type);
1625}
1626
1627##-------------------------------------------------------
1628## perms
1629##-------------------------------------------------------
1630
1631body perms m(mode)
1632# @brief Set the file mode
1633# @param mode The new mode
1634{
1635      mode   => "$(mode)";
1636}
1637
1638##
1639
1640body perms mo(mode,user)
1641# @brief Set the file's mode and owners
1642# @param mode The new mode
1643# @param user The username of the new owner
1644{
1645      owners => { "$(user)" };
1646      mode   => "$(mode)";
1647}
1648
1649##
1650
1651body perms mog(mode,user,group)
1652# @brief Set the file's mode, owner and group
1653# @param mode The new mode
1654# @param user The username of the new owner
1655# @param group The group name
1656{
1657      owners => { "$(user)" };
1658      groups => { "$(group)" };
1659      mode   => "$(mode)";
1660}
1661
1662##
1663
1664body perms og(u,g)
1665# @brief Set the file's owner and group
1666# @param u The username of the new owner
1667# @param g The group name
1668{
1669      owners => { "$(u)" };
1670      groups => { "$(g)" };
1671}
1672
1673##
1674
1675body perms owner(user)
1676# @brief Set the file's owner
1677# @param user The username of the new owner
1678{
1679      owners => { "$(user)" };
1680}
1681
1682body perms system_owned(mode)
1683# @brief Set the file owner and group to the system default
1684# @param mode the access permission in octal format
1685#
1686# **Example:**
1687#
1688# ```cf3
1689# files:
1690#     "/etc/passwd" perms => system_owned("0644");
1691# ```
1692{
1693      mode   => "$(mode)";
1694      owners => { "root" };
1695
1696    freebsd|openbsd|netbsd|darwin::
1697      groups => { "wheel" };
1698
1699    linux::
1700      groups => { "root" };
1701
1702    solaris::
1703      groups => { "sys" };
1704
1705    aix::
1706      groups => { "system" };
1707}
1708
1709##-------------------------------------------------------
1710## ACLS (extended Unix perms)
1711##-------------------------------------------------------
1712
1713body acl access_generic(acl)
1714# @brief Set the `aces` of the access control as specified
1715#
1716# Default/inherited ACLs are left unchanged. This body is
1717# applicable for both files and directories on all platforms.
1718#
1719# @param acl The aces to be set
1720{
1721      acl_method => "overwrite";
1722      aces => { "@(acl)" };
1723
1724    windows::
1725      acl_type => "ntfs";
1726
1727    !windows::
1728      acl_type => "posix";
1729}
1730
1731##
1732
1733body acl ntfs(acl)
1734# @brief Set the `aces` on NTFS file systems, and overwrite
1735# existing ACLs.
1736#
1737# This body requires CFEngine Enterprise.
1738#
1739# @param acl The aces to be set
1740{
1741      acl_type => "ntfs";
1742      acl_method => "overwrite";
1743      aces => { "@(acl)" };
1744}
1745
1746##
1747
1748body acl strict
1749# @brief Limit file access via ACLs to users with administrator privileges,
1750# overwriting existing ACLs.
1751#
1752# **Note:** May need to take ownership of file/dir to be sure no-one else is
1753# allowed access.
1754{
1755      acl_method => "overwrite";
1756
1757    windows::
1758      aces => { "user:Administrator:rwx" };
1759    !windows::
1760      aces => { "user:root:rwx" };
1761}
1762
1763##-------------------------------------------------------
1764## depth_search
1765##-------------------------------------------------------
1766
1767body depth_search recurse(d)
1768# @brief Search files and direcories recursively, up to the specified depth
1769# Directories on different devices are excluded.
1770#
1771# @param d The maximum search depth
1772{
1773      depth => "$(d)";
1774      xdev  => "true";
1775}
1776
1777##
1778
1779body depth_search recurse_ignore(d,list)
1780# @brief Search files and directories recursively,
1781# but don't recurse into the specified directories
1782#
1783# @param d The maximum search depth
1784# @param list The list of directories to be excluded
1785{
1786      depth => "$(d)";
1787      exclude_dirs => { @(list) };
1788}
1789
1790##
1791
1792body depth_search include_base
1793# @brief Search files and directories recursively,
1794# starting from the base directory.
1795{
1796      include_basedir => "true";
1797}
1798
1799body depth_search recurse_with_base(d)
1800# @brief Search files and directories recursively up to the specified
1801# depth, starting from the base directory excluding directories on
1802# other devices.
1803#
1804# @param d The maximum search depth
1805{
1806      depth => "$(d)";
1807      xdev  => "true";
1808      include_basedir => "true";
1809}
1810
1811##-------------------------------------------------------
1812## delete
1813##-------------------------------------------------------
1814
1815body delete tidy
1816# @brief Delete the file and remove empty directories
1817# and links to directories
1818{
1819      dirlinks => "delete";
1820      rmdirs   => "true";
1821}
1822
1823##-------------------------------------------------------
1824## rename
1825##-------------------------------------------------------
1826
1827body rename disable
1828# @brief Disable the file
1829{
1830      disable => "true";
1831}
1832
1833##
1834
1835body rename rotate(level)
1836# @brief Rotate and store up to `level` backups of the file
1837# @param level The number of backups to store
1838{
1839      rotate => "$(level)";
1840}
1841
1842##
1843
1844body rename to(file)
1845# @brief Rename the file to `file`
1846# @param file The new name of the file
1847{
1848      newname => "$(file)";
1849}
1850
1851##-------------------------------------------------------
1852## file_select
1853##-------------------------------------------------------
1854
1855body file_select name_age(name,days)
1856# @brief Select files that have a matching `name` and have not been modified for at least `days`
1857# @param name A regex that matches the file name
1858# @param days Number of days
1859{
1860      leaf_name   => { "$(name)" };
1861      mtime       => irange(0,ago(0,0,"$(days)",0,0,0));
1862      file_result => "mtime.leaf_name";
1863}
1864
1865##
1866
1867body file_select days_old(days)
1868# @brief Select files that have not been modified for at least `days`
1869# @param days Number of days
1870{
1871      mtime       => irange(0,ago(0,0,"$(days)",0,0,0));
1872      file_result => "mtime";
1873}
1874
1875##
1876
1877body file_select size_range(from,to)
1878# @brief Select files that have a size within the specified range
1879# @param from The lower bound of the allowed file size
1880# @param to The upper bound of the allowed file size
1881{
1882      search_size => irange("$(from)","$(to)");
1883      file_result => "size";
1884}
1885
1886##
1887
1888body file_select bigger_than(size)
1889# @brief Select files that are above a given size
1890# @param size The number of bytes files have
1891{
1892      search_size => irange("0","$(size)");
1893      file_result => "!size";
1894}
1895
1896##
1897
1898body file_select exclude(name)
1899# @brief Select all files except those that match `name`
1900# @param name A regular expression
1901{
1902      leaf_name  => { "$(name)"};
1903      file_result => "!leaf_name";
1904}
1905
1906##
1907
1908body file_select plain
1909# @brief Select plain, regular files
1910{
1911      file_types  => { "plain" };
1912      file_result => "file_types";
1913}
1914
1915body file_select dirs
1916# @brief Select directories
1917{
1918      file_types  => { "dir" };
1919      file_result => "file_types";
1920}
1921
1922##
1923
1924body file_select by_name(names)
1925# @brief Select files that match `names`
1926# @param names A regular expression
1927{
1928      leaf_name  => { @(names)};
1929      file_result => "leaf_name";
1930}
1931
1932##
1933
1934body file_select ex_list(names)
1935# @brief Select all files except those that match `names`
1936# @param names A list of regular expressions
1937{
1938      leaf_name  => { @(names)};
1939      file_result => "!leaf_name";
1940}
1941
1942##
1943
1944body file_select all
1945# @brief Select all file system entries
1946{
1947      leaf_name => { ".*" };
1948      file_result => "leaf_name";
1949}
1950
1951##
1952
1953body file_select older_than(years, months, days, hours, minutes, seconds)
1954# @brief Select files older than the date-time specified
1955# @param years Number of years
1956# @param months Number of months
1957# @param days Number of days
1958# @param hours Number of hours
1959# @param minutes Number of minutes
1960# @param seconds Number of seconds
1961#
1962# Generic older_than selection body, aimed to have a common definition handy
1963# for every case possible.
1964{
1965      mtime       => irange(0,ago("$(years)","$(months)","$(days)","$(hours)","$(minutes)","$(seconds)"));
1966      file_result => "mtime";
1967}
1968
1969##
1970
1971body file_select filetype_older_than(filetype, days)
1972# @brief Select files of specified type older than specified number of days
1973#
1974# @param filetype File type to select
1975# @param days Number of days
1976#
1977# This body only takes a single filetype, see `filetypes_older_than()`
1978# if you want to select more than one type of file.
1979{
1980      file_types => { "$(filetype)" };
1981      mtime      => irange(0,ago(0,0,"$(days)",0,0,0));
1982      file_result => "file_types.mtime";
1983}
1984
1985##
1986
1987body file_select filetypes_older_than(filetypes, days)
1988# @brief Select files of specified types older than specified number of days
1989#
1990# This body only takes a list of filetypes
1991#
1992# @param filetypes A list of file types
1993# @param days Number of days
1994#
1995# **See also:** `filetype_older_than()`
1996{
1997      file_types => { @(filetypes) };
1998      mtime      => irange(0,ago(0,0,"$(days)",0,0,0));
1999      file_result => "file_types.mtime";
2000}
2001
2002body file_select symlinked_to(target)
2003# @brief Select symlinks that point to $(target)
2004# @param target The file the symlink should point to in order to be selected
2005{
2006  file_types  => { "symlink" };
2007  issymlinkto => { "$(target)" };
2008  file_result => "issymlinkto";
2009}
2010
2011##-------------------------------------------------------
2012## changes
2013##-------------------------------------------------------
2014
2015body changes detect_all_change
2016# @brief Detect all file changes using the best hash method
2017#
2018# This is fierce, and will cost disk cycles
2019#
2020{
2021      hash           => "best";
2022      report_changes => "all";
2023      update_hashes  => "yes";
2024}
2025
2026##
2027
2028body changes detect_all_change_using(hash)
2029# @brief Detect all file changes using a given hash method
2030#
2031# Detect all changes using a configurable hashing algorithm
2032# for times when you care about both content and file stats e.g. mtime
2033#
2034# @param hash supported hashing algorithm (md5, sha1, sha224, sha256, sha384, sha512, best)
2035{
2036      hash           => "$(hash)";
2037      report_changes => "all";
2038      update_hashes  => "yes";
2039}
2040
2041##
2042
2043body changes detect_content
2044# @brief Detect file content changes using md5
2045#
2046# This is a cheaper alternative
2047{
2048      hash           => "md5";
2049      report_changes => "content";
2050      update_hashes  => "yes";
2051}
2052
2053##
2054
2055body changes detect_content_using(hash)
2056# @brief Detect file content changes using a given hash algorithm.
2057#
2058# For times when you only care about content, not file stats e.g. mtime
2059# @param hash - supported hashing algorithm (md5, sha1, sha224, sha256, sha384,
2060#   sha512, best)
2061{
2062      hash           => "$(hash)";
2063      report_changes => "content";
2064      update_hashes  => "yes";
2065}
2066
2067##
2068
2069body changes noupdate
2070# @brief Detect content changes in (small) files that should never change
2071{
2072      hash           => "sha256";
2073      report_changes => "content";
2074      update_hashes  => "no";
2075}
2076
2077##
2078
2079body changes diff
2080# @brief Detect file content changes using sha256
2081# and report the diff to CFEngine Enterprise
2082{
2083      hash           => "sha256";
2084      report_changes => "content";
2085      report_diffs   => "true";
2086      update_hashes  => "yes";
2087}
2088
2089##
2090
2091body changes all_changes
2092# @brief Detect all file changes using sha256
2093# and report the diff to CFEngine Enterprise
2094{
2095      hash           => "sha256";
2096      report_changes => "all";
2097      report_diffs   => "true";
2098      update_hashes  => "yes";
2099}
2100
2101##
2102
2103body changes diff_noupdate
2104# @brief Detect content changes in (small) files
2105# and report the diff to CFEngine Enterprise
2106{
2107      hash           => "sha256";
2108      report_changes => "content";
2109      report_diffs   => "true";
2110      update_hashes  => "no";
2111}
2112
2113# template bundles
2114
2115bundle agent file_mustache(mustache_file, json_file, target_file)
2116# @brief Make a file from a Mustache template and a JSON file
2117# @param mustache_file the file with the Mustache template
2118# @param json_file a file with JSON data
2119# @param target_file the target file to write
2120#
2121# **Example:**
2122#
2123# ```cf3
2124# methods:
2125#      "m" usebundle => file_mustache("x.mustache", "y.json", "z.txt");
2126# ```
2127{
2128  files:
2129      "$(target_file)"
2130      create => "true",
2131      edit_template => $(mustache_file),
2132      template_data => readjson($(json_file), "100k"),
2133      template_method => "mustache";
2134}
2135
2136bundle agent file_mustache_jsonstring(mustache_file, json_string, target_file)
2137# @brief Make a file from a Mustache template and a JSON string
2138# @param mustache_file the file with the Mustache template
2139# @param json_string a string with JSON data
2140# @param target_file the target file to write
2141#
2142# **Example:**
2143#
2144# ```cf3
2145# methods:
2146#      "m" usebundle => file_mustache_jsonstring("x.mustache", '{ "x": "y" }', "z.txt");
2147# ```
2148{
2149  files:
2150      "$(target_file)"
2151      create => "true",
2152      edit_template => $(mustache_file),
2153      template_data => parsejson($(json_string)),
2154      template_method => "mustache";
2155}
2156
2157bundle agent file_tidy(file)
2158# @brief Remove a file
2159# @param file to remove
2160#
2161# **Example:**
2162#
2163# ```cf3
2164# methods:
2165#      "" usebundle => file_tidy("/tmp/z.txt");
2166# ```
2167{
2168  files:
2169      "$(file)" delete => tidy;
2170
2171  reports:
2172    "DEBUG|DEBUG_$(this.bundle)"::
2173      "DEBUG $(this.bundle): deleting $(file) with delete => tidy";
2174}
2175
2176bundle agent dir_sync(from, to)
2177# @brief Synchronize a directory entire, deleting unknown files
2178# @param from source directory
2179# @param to destination directory
2180#
2181# **Example:**
2182#
2183# ```cf3
2184# methods:
2185#      "" usebundle => dir_sync("/tmp", "/var/tmp");
2186# ```
2187{
2188  files:
2189      "$(to)/."
2190      create => "true",
2191      depth_search => recurse("inf"),
2192      copy_from => copyfrom_sync($(from));
2193
2194  reports:
2195    "DEBUG|DEBUG_$(this.bundle)"::
2196      "DEBUG $(this.bundle): copying directory $(from) to $(to)";
2197}
2198
2199bundle agent file_copy(from, to)
2200# @brief Copy a file
2201# @param from source file
2202# @param to destination file
2203#
2204# **Example:**
2205#
2206# ```cf3
2207# methods:
2208#      "" usebundle => file_copy("/tmp/z.txt", "/var/tmp/y.txt");
2209# ```
2210{
2211  files:
2212      "$(to)"
2213      copy_from => copyfrom_sync($(from));
2214
2215  reports:
2216    "DEBUG|DEBUG_$(this.bundle)"::
2217      "DEBUG $(this.bundle): copying file $(from) to $(to)";
2218}
2219
2220body copy_from copyfrom_sync(f)
2221# @brief Copy a directory or file with digest checksums, preserving attributes and purging leftovers
2222# @param f the file or directory
2223{
2224      source => "$(f)";
2225      purge => "true";
2226      preserve => "true";
2227      type_check => "false";
2228      compare => "digest";
2229}
2230
2231bundle agent file_make(file, str)
2232# @brief Make a file from a string
2233# @param file target
2234# @param str the string data
2235#
2236# **Example:**
2237#
2238# ```cf3
2239# methods:
2240#      "" usebundle => file_make("/tmp/z.txt", "Some text
2241# and some more text here");
2242# ```
2243{
2244  vars:
2245      "len" int => string_length($(str));
2246    summarize::
2247      "summary" string => format("%s...%s",
2248                                string_head($(str), 18),
2249                                string_tail($(str), 18));
2250  classes:
2251      "summarize" expression => isgreaterthan($(len), 40);
2252
2253  files:
2254      "$(file)"
2255      create => "true",
2256      edit_line => insert_lines($(str)),
2257      edit_defaults => empty;
2258
2259  reports:
2260    "DEBUG|DEBUG_$(this.bundle)"::
2261      "DEBUG $(this.bundle): creating $(file) with contents '$(str)'"
2262      if => "!summarize";
2263
2264      "DEBUG $(this.bundle): creating $(file) with contents '$(summary)'"
2265      if => "summarize";
2266}
2267
2268bundle agent file_make_mog(file, str, mode, owner, group)
2269# @brief Make a file from a string with mode, owner, group
2270# @param file target
2271# @param str the string data
2272# @param mode the file permissions in octal
2273# @param owner the file owner as a name or UID
2274# @param group the file group as a name or GID
2275#
2276# **Example:**
2277#
2278# ```cf3
2279# methods:
2280#      "" usebundle => file_make_mog("/tmp/z.txt", "Some text
2281# and some more text here", "0644", "root", "root");
2282# ```
2283{
2284  vars:
2285      "len" int => string_length($(str));
2286    summarize::
2287      "summary" string => format("%s...%s",
2288                                string_head($(str), 18),
2289                                string_tail($(str), 18));
2290  classes:
2291      "summarize" expression => isgreaterthan($(len), 40);
2292
2293  files:
2294      "$(file)"
2295      create => "true",
2296      edit_line => insert_lines($(str)),
2297      perms => mog($(mode), $(owner), $(group)),
2298      edit_defaults => empty;
2299
2300  reports:
2301    "DEBUG|DEBUG_$(this.bundle)"::
2302      "DEBUG $(this.bundle): creating $(file) with contents '$(str)', mode '$(mode)', owner '$(owner)' and group '$(group)'"
2303      if => "!summarize";
2304
2305      "DEBUG $(this.bundle): creating $(file) with contents '$(summary)', mode '$(mode)', owner '$(owner)' and group '$(group)'"
2306      if => "summarize";
2307}
2308
2309bundle agent file_make_mustache(file, template, data)
2310# @brief Make a file from a mustache template
2311# @param file Target file to render
2312# @param template Path to mustache template
2313# @param data Data container to use
2314#
2315# **Example:**
2316#
2317# ```cf3
2318# vars:
2319#   "state" data => datastate();
2320#
2321# methods:
2322#      "" usebundle => file_make_mustache( "/tmp/z.txt", "/tmp/z.mustache", @(state) );
2323# ```
2324{
2325  files:
2326      "$(file)"
2327        create => "true",
2328        edit_template => "$(template)",
2329        template_method => "mustache",
2330        template_data => @(data);
2331
2332  reports:
2333      "DEBUG|DEBUG_$(this.bundle)"::
2334      "DEBUG $(this.bundle): rendering $(file) with template '$(template)'";
2335}
2336
2337bundle agent file_make_mustache_with_perms(file, template, data, mode, owner, group)
2338# @brief Make a file from a mustache template
2339# @param file Target file to render
2340# @param template Path to mustache template
2341# @param data Data container to use
2342# @param mode File permissions
2343# @param owner Target file owner
2344# @param group Target file group
2345#
2346# **Example:**
2347#
2348# ```cf3
2349# vars:
2350#   "state" data => datastate();
2351#
2352# methods:
2353#      "" usebundle => file_make_mustache( "/tmp/z.txt", "/tmp/z.mustache", @(state),
2354#                                          600, "root", "root" );
2355# ```
2356{
2357  files:
2358      "$(file)"
2359        create => "true",
2360        edit_template => "$(template)",
2361        template_method => "mustache",
2362        perms => mog( $(mode), $(owner), $(group) ),
2363        template_data => @(data);
2364
2365  reports:
2366      "DEBUG|DEBUG_$(this.bundle)"::
2367      "DEBUG $(this.bundle): rendering $(file) with template '$(template)'";
2368}
2369
2370bundle agent file_empty(file)
2371# @brief Make an empty file
2372# @param file target
2373#
2374# **Example:**
2375#
2376# ```cf3
2377# methods:
2378#      "" usebundle => file_empty("/tmp/z.txt");
2379# ```
2380{
2381  files:
2382      "$(file)"
2383      create => "true",
2384      edit_defaults => empty;
2385
2386  reports:
2387    "DEBUG|DEBUG_$(this.bundle)"::
2388      "DEBUG $(this.bundle): creating empty $(file) with 0 size";
2389}
2390
2391bundle agent file_hardlink(target, link)
2392# @brief Make a hard link to a file
2393# @param target of link
2394# @param link the hard link's location
2395#
2396# **Example:**
2397#
2398# ```cf3
2399# methods:
2400#      "" usebundle => file_hardlink("/tmp/z.txt", "/tmp/z.link");
2401# ```
2402{
2403  files:
2404      "$(link)"
2405      move_obstructions => "true",
2406      link_from => linkfrom($(target), "hardlink");
2407
2408  reports:
2409    "DEBUG|DEBUG_$(this.bundle)"::
2410      "DEBUG $(this.bundle): $(link) will be a hard link to $(target)";
2411}
2412
2413bundle agent file_link(target, link)
2414# @brief Make a symlink to a file
2415# @param target of symlink
2416# @param link the symlink's location
2417#
2418# **Example:**
2419#
2420# ```cf3
2421# methods:
2422#      "" usebundle => file_link("/tmp/z.txt", "/tmp/z.link");
2423# ```
2424{
2425  files:
2426      "$(link)"
2427      move_obstructions => "true",
2428      link_from => linkfrom($(target), "symlink");
2429
2430  reports:
2431    "DEBUG|DEBUG_$(this.bundle)"::
2432      "DEBUG $(this.bundle): $(link) will be a symlink to $(target)";
2433}
2434