Puppet Function: change_risk

Defined in:
lib/puppet/functions/change_risk.rb
Function type:
Ruby 4.x API

Overview

Signatures:

  • change_risk(String $risk)Enum[op,noop,interface]

    Returns a string indicating which operative decision the function made.

    Examples:

    Calling the function.

    change_risk('medium')

    Parameters:

    • risk (String)

      The assessed risk to apply to the class.

    Returns:

    • (Enum[op,noop,interface])

      a string indicating which operative decision the function made.

  • change_risk(String $risk, Callable &$block)Enum[op,noop]

    Returns a string indicating which operative decision the function made.

    Examples:

    Calling the function.

    change_risk('medium') || { ... }

    Parameters:

    • risk (String)

      The assessed risk to apply to a block of code.

    • &block (Callable)

    Yields:

    • a block of code the assessed risk applies to.

    Returns:

    • (Enum[op,noop])

      a string indicating which operative decision the function made.



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/puppet/functions/change_risk.rb', line 7

Puppet::Functions.create_function(:change_risk, Puppet::Functions::InternalFunction) do
  # @param risk The assessed risk to apply to the class.
  # @return a string indicating which operative decision the function made.
  # @example Calling the function.
  #   change_risk('medium')
  dispatch :class_function do
    scope_param
    param 'String', :risk
    return_type 'Enum[op,noop,interface]'
  end

  # @param risk The assessed risk to apply to a block of code.
  # @yield a block of code the assessed risk applies to.
  # @return a string indicating which operative decision the function made.
  # @example Calling the function.
  #   change_risk('medium') || { ... }
  dispatch :with_block do
    scope_param
    param 'String', :risk
    block_param
    return_type 'Enum[op,noop]'
  end

  # Return whether or not a given change risk level is currently permitted.
  # This is common logic used by #class_function and #with_block to decide
  # whether or not to force their resources into noop mode.
  def change_permitted?(risk)
    # Ensure config is loaded
    call_function('include', 'change_risk')

    risk_not_found_action = closure_scope.lookupvar('change_risk::risk_not_found_action')
    Puppet.debug { "change_risk(#{risk}): risk_not_found_action=#{risk_not_found_action}" }

    permitted = closure_scope.lookupvar('change_risk::permitted_risk_normalized')[risk]
    Puppet.debug { "change_risk(#{risk}): permitted=#{permitted}" }

    # If we have a valid directive, we can just return it
    return permitted unless permitted.nil?

    # No valid directive means we need to figure out a return value ourselves
    if risk_not_found_action == 'none'
      true
    elsif risk_not_found_action == 'noop'
      false
    elsif risk_not_found_action == 'fail'
      call_function('fail', "Permitted risk data unavailable for risk '#{risk}'")
    else
      raise "Unexpected value for change_risk::risk_not_found_action: #{risk_not_found_action}"
    end
  end

  def ignore_permitted?(risk)
    if [true, 'true'].include?(closure_scope.lookupvar('change_risk::ignore_permitted_risk'))
      Puppet.debug { "change_risk(#{risk}): ignore_permitted_risk=true" }
      return true
    end

    disable_mechanism = closure_scope.lookupvar('change_risk::disable_mechanism')
    flag_set = closure_scope.lookupvar('facts')['noop_cli_value'] == false
    fact_set = [true, 'true'].include?(closure_scope.lookupvar('facts')['ignore_permitted_risk'])

    case disable_mechanism
    when 'flag'
      flag_set
    when 'fact'
      fact_set
    when 'both'
      flag_set || fact_set
    end
  end

  def previously_nooped?(scope)
    return false if scope.nil?
    return true if scope.respond_to?(:noop_default)
    previously_nooped?(scope.parent)
  end

  def eval_noop(scope, risk)
    Puppet.debug { "change_risk(#{risk}): #{scope.inspect}: evaluating..." }
    if change_permitted?(risk) || ignore_permitted?(risk)
      scope.call_function('noop', :undef) if previously_nooped?(scope)
      'op'
    else
      Puppet.debug { "change_risk(#{risk}): #{scope.inspect}: calling noop()" }
      scope.call_function('noop', true)
      'noop'
    end
  end

  def class_function(scope, risk)
    newtags = scope.resource.tags.delete_if { |t| t =~ %r{^change_risk:} }
    scope.resource.tags = newtags << "change_risk:#{risk}"

    # Check if we're implementing noop::class_interface()
    if scope.lookupvar('change_risk::respect_noop_class_interface') && !scope.get_local_variable('class_noop').nil?
      scope.call_function('noop::class_interface', [])
      'interface'
    else
      eval_noop(scope, risk)
    end
  end

  def with_block(scope, risk, &block)
    # Create a new scope to evalutate the block in. The new scope will be
    # used to contain the effects of a call to noop() inside that scope, if
    # needed, and also to cheat a little to add a tag to all resources
    # contained in the block, and any child-scope blocks that might get
    # included.
    #
    # To reset the change_risk tag from a parent scope and ensure the desired
    # change_risk tag is propogated, the new scope is spiked with a Delegator
    # resource which overrides the #tag and #merge_into methods of the source
    # resource, thus getting what we want and avoiding the need to insert a
    # whole new class to contain the resources.
    resource = ResourceDelegator.new(scope.resource, risk)
    newscope = scope.newscope(source: scope.source, resource: resource)

    # Ensure all variables from parent in newscope, then evaluate the block
    scope.to_hash(false, true).each_pair do |key, val|
      newscope[key] = val unless [Puppet::Parser::Scope::RESERVED_VARIABLE_NAMES,
                                  Puppet::Parser::Scope::VARNAME_SERVER_FACTS].flatten.include?(key)
    end
    action = eval_noop(newscope, risk)
    block.closure.call_by_name_with_scope(newscope, {}, false)

    # The block function return value is the determination of eval_noop()
    action
  end

  class ResourceDelegator < SimpleDelegator
    def initialize(obj, risk)
      super(obj)
      @risk = risk
    end

    def tags
      super.delete_if { |t| t =~ %r{^change_risk:} } << "change_risk:#{@risk}"
    end

    def merge_into(tag_set)
      tag_set.merge(tags)
    end
  end
end