diff --git a/tests/acceptance/20_cfe_basics/flag_lib.cf b/tests/acceptance/20_cfe_basics/flag_lib.cf new file mode 100644 index 000000000..e04c63274 --- /dev/null +++ b/tests/acceptance/20_cfe_basics/flag_lib.cf @@ -0,0 +1,84 @@ +####################################################### +# +# Test flag_lib +# +####################################################### + +bundle common acc_path +{ + vars: + "root" string => getenv("NCF_TESTS_ACCEPTANCE", 1024); +} + +body common control +{ + inputs => { "${acc_path.root}/default.cf.sub", "${acc_path.root}/default_ncf.cf.sub", "@{ncf_inputs.default_files}" }; + bundlesequence => { configuration, initialization, default("${this.promise_filename}") }; + version => "1.0"; +} + +####################################################### + +bundle agent init { + vars: + "existing" string => '{ "already": { "created": "2018-10-15T16:52:20+0200" } }'; + + methods: + "clean" usebundle => file_absent("${configuration.flag_file}"); +} + +####################################################### + +bundle agent test +{ + + methods: + "read before" + usebundle => get_flag("test"), + useresult => "get1"; + "reserve" + usebundle => reserve_flag("test"), + useresult => "reserve2"; + "read after" + usebundle => get_flag("test"), + useresult => "get3"; + "reserve twice" + usebundle => reserve_flag("test"), + useresult => "reserve4"; + "set flag" + usebundle => set_flag("test"), + useresult => "set5"; + "read after set" + usebundle => get_flag("test"), + useresult => "get6"; + "set another flag without reservation" + usebundle => set_flag("test2"), + useresult => "set7"; + + "clean" usebundle => file_content("${configuration.flag_file}", "${init.existing}", "true"); + "read an existing condition" + usebundle => get_flag("already"), + useresult => "get8"; +} + +bundle agent check +{ + classes: + "ok_1" expression => strcmp("${test.get1[1]}", "free"); + "ok_2" expression => strcmp("${test.reserve2[1]}", "ok"); + "ok_3" expression => strcmp("${test.get3[1]}", "set"); + "ok_4" expression => strcmp("${test.reserve4[1]}", "failed"); + "ok_5" expression => strcmp("${test.set5[1]}", "ok"); + "ok_6" expression => strcmp("${test.get6[1]}", "set"); + "ok_7" expression => strcmp("${test.set7[1]}", "notreserved"); + "ok_8" expression => strcmp("${test.get8[1]}", "set"); + + "ok" expression => "ok_1.ok_2.ok_3.ok_4.ok_5.ok_6.ok_7.ok_8"; + + reports: + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; +} + diff --git a/tests/acceptance/default_ncf.cf.sub b/tests/acceptance/default_ncf.cf.sub index 2d2bcabc5..be5b5c165 100644 --- a/tests/acceptance/default_ncf.cf.sub +++ b/tests/acceptance/default_ncf.cf.sub @@ -16,6 +16,13 @@ bundle common ncf_inputs } +# Load a custom ncf.conf for tests +bundle common ncf_configuration { + vars: + "ncf_configuration_basedir" string => dirname("${this.promise_filename}"); + "ncf_configuration_file" string => "${ncf_configuration_basedir}/ncf.conf"; +} + bundle common test_utils { vars: diff --git a/tests/acceptance/ncf.conf b/tests/acceptance/ncf.conf new file mode 100644 index 000000000..0c0516169 --- /dev/null +++ b/tests/acceptance/ncf.conf @@ -0,0 +1,19 @@ +# This file can be used to adjust some elements in ncf. +# +# This is a Mustache template file, and allows variation +# of configuration based on CFEngine classes +# see http://mustache.github.io/ for the full mustache specification +# and https://docs.cfengine.com/docs/3.6/reference-promise-types-files.html for +# CFEngine implementation +# +# Which logger(s) should be used in ncf? +# Comma separated list, default is '_log_default', use '' to disable all loggers +loggers=_log_default +# Which abort handler(s) should be used in ncf? +# Comma separated list, default is '_abort_default', use '' to disable all abort handlers +abort_handlers=_abort_default +# Which port should be for CFEngine connections/data transfers +# default is 5308 +cfengine_port=5308 +# Where to store persistent flags, default is '/var/rudder/agent-data/flags.json' +flag_file=/tmp/flags.json \ No newline at end of file diff --git a/tree/10_ncf_internals/configuration.cf b/tree/10_ncf_internals/configuration.cf index 7da1a1abf..55b464a8b 100644 --- a/tree/10_ncf_internals/configuration.cf +++ b/tree/10_ncf_internals/configuration.cf @@ -24,6 +24,10 @@ bundle agent configuration { vars: + # We expand the template in the agent folder, so that it is not + # overwritten by the policy server expanded version, and if there are multiple + # agents on the node, each conf file does not compete + "ncf_configuration_file" string => "${sys.workdir}/ncf.conf.cache"; pass1.!ncf_override_conf_file_defined:: "ncf_configuration_basedir" string => dirname("${this.promise_filename}"); @@ -31,13 +35,7 @@ bundle agent configuration "ncf_configuration_file_source" string => "${ncf_configuration_basedir}/../ncf.conf"; pass1.ncf_override_conf_file_defined:: - "ncf_configuration_file_source" string => "${ncf_configuration.ncf_configuration_file}"; - - pass1:: - # We expand the template in the agent folder, so that it is not - # overwritten by the policy server expanded version, and if there are multiple - # agents on the node, each conf file does not compete - "ncf_configuration_file" string => "${sys.workdir}/ncf.conf.cache"; + "ncf_configuration_file_source" string => "${ncf_configuration.ncf_configuration_file}"; pass2:: # Read all entries from the configuration file @@ -47,18 +45,21 @@ bundle agent configuration "enabled_loggers" slist => splitstring("${ncf_config[loggers][1]}",",","10"); "enabled_abort_handlers" slist => splitstring("${ncf_config[abort_handlers][1]}",",","10"); "cfengine_port" string => "${ncf_config[cfengine_port][1]}"; + "flag_file" string => "${ncf_config[flag_file][1]}"; # Add a default empty list to allow skipping those handlers - pass3.enabled_loggers_empty:: + pass2.enabled_loggers_empty:: "enabled_loggers" slist => {}; - pass3.enabled_abort_handlers_empty:: + pass2.enabled_abort_handlers_empty:: "enabled_abort_handlers" slist => {}; - pass3.!enabled_loggers_defined:: + pass2.!enabled_loggers_defined:: "enabled_loggers" slist => { "_log_default" }; - pass3.!enabled_abort_handlers_defined:: + pass2.!enabled_abort_handlers_defined:: "enabled_abort_handlers" slist => { "_abort_default" }; - pass3.!cfengine_port_defined:: + pass2.!cfengine_port_defined:: "cfengine_port" string => "5308"; + pass2.!flag_file_defined:: + "flag_file" string => "/var/rudder/agent-data/flags.json"; any:: # Constants @@ -69,12 +70,12 @@ bundle agent configuration "trace" string => "[TRACE]"; classes: - "ncf_override_conf_file_defined" expression => fileexists("${ncf_configuration.ncf_configuration_file}"); - - "pass3" expression => "pass1"; + "pass3" expression => "pass2"; "pass2" expression => "pass1"; "pass1" expression => "any"; + "ncf_override_conf_file_defined" expression => fileexists("${ncf_configuration.ncf_configuration_file}"); + # Define ncf verbosity classes according to classes # defined by the agent after verbosity options (-I, -v, -d) "info" expression => "inform_mode", @@ -90,7 +91,7 @@ bundle agent configuration "info" expression => "debug", scope => "namespace"; - pass3:: + pass2:: # this line will not be evaluated by cfengine if destination_prefix contains a variable that does not exist # and it will always be evaluated to true otherwise # Using the source variable for empty check to allow trailing comma @@ -99,6 +100,7 @@ bundle agent configuration "enabled_abort_handlers_defined" expression => strcmp("${enabled_abort_handlers}", "${enabled_abort_handlers}"); "enabled_abort_handlers_empty" expression => strcmp("${ncf_config[abort_handlers][1]}", ""); "cfengine_port_defined" not => strcmp("${cfengine_port}", "${ncf_config[cfengine_port][1]}"); + "flag_file_defined" not => strcmp("${flag_file}", "${ncf_config[flag_file][1]}"); # We cannot use the generic method to expand template, as it relies on the logger, # which is not yet defined diff --git a/tree/10_ncf_internals/initialization.cf b/tree/10_ncf_internals/initialization.cf index b680f02b0..bcb4d9821 100644 --- a/tree/10_ncf_internals/initialization.cf +++ b/tree/10_ncf_internals/initialization.cf @@ -40,6 +40,7 @@ bundle agent initialization # the ncf_init_* classes can be used to get the agregated result of ncf intialization "copy classes for reporting" usebundle => _classes_copy("ncf_internals_modules_update", "ncf_init"); "init dry_run context" usebundle => dry_run_initialization; + "init flag context" usebundle => flag_initialization; reports: info:: diff --git a/tree/20_cfe_basics/flag_lib.cf b/tree/20_cfe_basics/flag_lib.cf new file mode 100644 index 000000000..c7f15cc1b --- /dev/null +++ b/tree/20_cfe_basics/flag_lib.cf @@ -0,0 +1,243 @@ +##################################################################################### +# Copyright 2018 Normation SAS +##################################################################################### +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +##################################################################################### + +# # Flag library +# +# ## Concept +# +# It allows storing a persisting database of flags that will be usable in the agents. +# Flags are somehow like persistent conditions but: +# +# * They have no expiration date +# * They are kept when resetting agent state +# +# This allows use cases like executing a command only once on a system. +# +# The storage format is a json file containing: +# +# ```json +# { +# "flag_name_1": { "created": "20181008-...." }, +# "flag_name_2": { "created": "20181008-...." } +# } +# ``` +# +# (dates are ISO 8601 formatted) +# +# The flag file is by default in `/var/rudder/agent-data/flags.json`, and configurable in `ncf.conf`. +# +# ## Usage +# +# This library provides several methods: +# +# * `reserve_flag` to reserve a flag for 24 hours, this will prevent anyone from reserving it during this period. It can give two possible values: +# * `ok`: if the flag was correctly reserved +# * `failed`: if the flag could not be reserved. I can happen if it was reserved by another agent. +# +# * `set_flag` to actually set a permanent flag, requires preceding reservation. +# If the flag already existed, its date is updated. +# It can give three possible values: +# * `ok`: if the flag was correctly set or updated +# * `failed`: if the flag could not be written +# * `notreserved`: if the flag was not reserved, in this case it is not set +# +# * `get_flag` to query the current state of a flag. Should not be used unless +# you really need the state of a flag without reserving it after. It can give two possible values: +# * `set`: if the flag is set or reserved +# * `free`: if the flag is free + +# A typical use case is: +# +# * At the beginning of the action, try to reserve the flag with `reserve_flag`. If it fails, +# it means the flag is already there or reserved +# * If it succeeds, start your task if any +# * Once the task if done, actually set the flag you reserved using `set_flag` +# +# ## Note about safety +# +# This library is though to be resilient to race conditions when several +# agents are executing at the same time. +# This is helped by always using reserve_flag to set an "almost atomic" lock on the flag. +# +# Race conditions can then only happen when when running the agent without +# locks (between get_flag and the actual persistent class definition, in reserve_flag). +# + +# This is called in 10_ncf_internals/initialization.cf +bundle agent flag_initialization { + vars: + # designed to be incremented when a global unique ID is needed + "flag_unique.value" string => "1"; +} + +bundle agent reserve_flag(name) { + vars: + "cname" string => canonify("${name}"); + "flag_unique.value" string => eval("${flag_unique.value}+1", "math", "infix"); + + classes: + pass1:: + "free" expression => strcmp("${current[1]}", "free"); + "flag_reserved_${cname}" expression => "any", + # 1 day + persistence => "1440", + scope => "namespace", + if => "free"; + + any:: + "pass3" expression => "pass2"; + "pass2" expression => "pass1"; + "pass1" expression => "any"; + + methods: + "${flag_unique.value}" usebundle => get_flag("${name}"), + useresult => "current"; + + reports: + pass3:: + "ok" + bundle_return_value_index => "1", + comment => "${flag_unique.value}", + if => "free.flag_reserved_${cname}"; + "failed" + bundle_return_value_index => "1", + comment => "${flag_unique.value}", + if => "!free"; + + pass3.info:: + "Reserved flag '${cname}'" + comment => "${flag_unique.value}", + if => "free"; + + pass3:: + "Failed, flag '${cname}' was not free" + comment => "${flag_unique.value}", + if => "!free"; +} + +bundle agent set_flag(name) { + vars: + "cname" string => canonify("${name}"); + "time" string => strftime("localtime", "%FT%T%z", "${sys.systime}"); + "flag" data => '{ "${cname}": { "created": "${time}" } }'; + pass2:: + "output" data => mergedata("ncf_flags.data", "flag"); + + classes: + "pass3" expression => "pass2"; + "pass2" expression => "pass1"; + "pass1" expression => "any"; + + files: + pass3:: + "${configuration.flag_file}" + create => "true", + template_method => "mustache", + edit_template => "${sys.workdir}/modules/templates/datastate.json.tpl", + template_data => "@{set_flag.output}", + edit_defaults => no_backup, + classes => classes_generic("write_ncf_flag_${cname}"), + if => "flag_reserved_${cname}"; + + methods: + "${flag_unique.value}" usebundle => _read_flags; + + reports: + "ok" + bundle_return_value_index => "1", + comment => "${flag_unique.value}", + if => "write_ncf_flag_${cname}_ok"; + "failed" + bundle_return_value_index => "1", + comment => "${flag_unique.value}", + if => "write_ncf_flag_${cname}_failed"; + "notreserved" + bundle_return_value_index => "1", + comment => "${flag_unique.value}", + if => "!flag_reserved_${cname}"; + + "Flag '${cname}' (created ${time}) could not be set" + comment => "${flag_unique.value}", + if => "write_ncf_flag_${cname}_failed"; + "Flag '${cname}' (created ${time}) could not be set as it was not reserved" + comment => "${flag_unique.value}", + if => "!flag_reserved_${cname}"; + + info:: + "Flag '${cname}' (created ${time}) was set" + comment => "${flag_unique.value}", + if => "write_ncf_flag_${cname}_ok"; +} + +# Should probably not be necessary, unless you need to know the state of the flag +# without reserving it +bundle agent get_flag(name) { + vars: + "cname" string => canonify("${name}"); + "flag_unique.value" string => eval("${flag_unique.value}+1", "math", "infix"); + + classes: + "is_set" expression => isvariable("ncf_flags.data[${cname}]"); + "pass1" expression => "any"; + + methods: + "${flag_unique.value}" usebundle => _read_flags; + + reports: + "set" + bundle_return_value_index => "1", + comment => "${flag_unique.value}", + if => "is_set|flag_reserved_${cname}"; + "free" + bundle_return_value_index => "1", + comment => "${flag_unique.value}", + if => "!is_set.!flag_reserved_${cname}"; + info:: + "Flag '${cname}' is set" + comment => "${flag_unique.value}", + if => "is_set"; + + "Flag '${cname}' is reserved" + comment => "${flag_unique.value}", + if => "!is_set.flag_reserved_${cname}"; + + "Flag '${cname}' is free" + comment => "${flag_unique.value}", + if => "!is_set.!flag_reserved_${cname}"; +} + +# This updates the value of the in-memory flags database. Should not be called from outside. +bundle agent _read_flags { + vars: + "flag_unique.value" string => eval("${flag_unique.value}+1", "math", "infix"); + + # Pass3 to avoid reading the json at each pass + pass3.file_exists:: + "ncf_flags.data" data => readjson("${configuration.flag_file}"), + comment => "${flag_unique.value}"; + pass3.!file_exists:: + "ncf_flags.data" data => '{}'; + + classes: + "file_exists" expression => fileexists("${configuration.flag_file}"); + + "pass3" expression => "pass2"; + "pass2" expression => "pass1"; + "pass1" expression => "any"; +} + diff --git a/tree/ncf.conf b/tree/ncf.conf index 963a82062..509e83d2b 100644 --- a/tree/ncf.conf +++ b/tree/ncf.conf @@ -6,7 +6,6 @@ # and https://docs.cfengine.com/docs/3.6/reference-promise-types-files.html for # CFEngine implementation # -{{#classes.any}} # Which logger(s) should be used in ncf? # Comma separated list, default is '_log_default', use '' to disable all loggers loggers=_log_default @@ -16,4 +15,5 @@ abort_handlers=_abort_default # Which port should be for CFEngine connections/data transfers # default is 5308 cfengine_port=5308 -{{/classes.any}} +# Where to store persistent flags, default is '/var/rudder/agent-data/flags.json' +flag_file=/var/cfengine-community/state/flags.json \ No newline at end of file