From 514d5979534cee25f2d975e2b7c41af418fa9651 Mon Sep 17 00:00:00 2001 From: Ben Ford Date: Sat, 4 Jan 2020 14:10:34 -0800 Subject: [PATCH] Porting functions to the modern Puppet 4.x API --- .../functions/change_window/change_window.rb | 197 ++++++++++++++++++ .../change_window/merge_change_windows.rb | 53 +++++ .../change_window_change_window_spec.rb | 41 ++++ ...change_window_merge_change_windows_spec.rb | 41 ++++ 4 files changed, 332 insertions(+) create mode 100644 lib/puppet/functions/change_window/change_window.rb create mode 100644 lib/puppet/functions/change_window/merge_change_windows.rb create mode 100644 spec/functions/change_window_change_window_spec.rb create mode 100644 spec/functions/change_window_merge_change_windows_spec.rb diff --git a/lib/puppet/functions/change_window/change_window.rb b/lib/puppet/functions/change_window/change_window.rb new file mode 100644 index 0000000..34d5d48 --- /dev/null +++ b/lib/puppet/functions/change_window/change_window.rb @@ -0,0 +1,197 @@ +# This is an autogenerated function, ported from the original legacy version. +# It /should work/ as is, but will not have all the benefits of the modern +# function API. You should see the function docs to learn how to add function +# signatures for type safety and to document this function using puppet-strings. +# +# https://puppet.com/docs/puppet/latest/custom_functions_ruby.html +# +# ---- original file header ---- +require 'date' + +# ---- original file header ---- +# +# @summary +# Provides change_window function that allows you to check current time against change window +# +Puppet::Functions.create_function(:'change_window::change_window') do + # @param args + # The original array of arguments. Port this to individually managed params + # to get the full benefit of the modern function API. + # + # @return [Data type] + # Describe what the function returns here + # + dispatch :default_impl do + # Call the method named 'default_impl' when this is matched + # Port this to match individual params for better type safety + repeated_param 'Any', :args + end + + + def default_impl(*args) + + class << self + def change_window_time_is_within(s,e,c) + # Calculate by minutes of the week 0 ... 1439 + test = ((s[0].to_i*60+s[1].to_i)..(e[0].to_i*60+e[1])).cover?(c[0]*60+c[1]) + return test + end + end + + # args[0] TimeZone + # args[1] Window Type - per_day or window(default) + # args[2] Hash, window_wday + # args[3] Hash, window_time + # args[4] String, Point-in-time to test (optional) + + # Validate Arguments are all present + raise Puppet::ParseError, "Invalid argument count, got #{args.length} expected 4 or 5" unless (4..5).cover?(args.length) + + # Validate Args are correct type + raise Puppet::ParseError, "TimeZone must be a string!" unless args[0].is_a? String + raise Puppet::ParseError, "Window type must be a string!" unless args[1].is_a? String + raise Puppet::ParseError, "Window_wday must be a hash!" unless args[2].is_a? Hash + raise Puppet::ParseError, "Window_time must be a hash!" unless args[3].is_a? Hash + if( args.length == 5) + raise Puppet::ParseError, "Point-in-time must be an array!" unless args[4].is_a? Array + end + + # Set Timezone for other timestamp + timezone = args[0] + + # Evaluation Type + # per_day, or window + # If per_day, we'll expect that window exists between start and end for each included + # day. + # If window, we'll expect that the window is continuous - from start day, start time through end day, end time. + window_type = args[1].nil? ? 'window' : args[1] + + if !['window','per_day'].include?(window_type) + raise Puppet::ParseError, "Window type must be 'window' or 'per_day'" + end + + ## Setup the days of the week hash + # First, validate that we got the keys we expect + args[2].keys.each { |ww| + if not ['start','end'].include?(ww) + raise Puppet::ParseError, "The window_wday hash can include start and end only! Invalid value passed - #{ww}" + end + } + + # Now that we now we have valid keys, set local var equal to passed + window_wday = args[2] + + # Figure out what days have been passed & Convert non-int + # Create new hash for storing ONLY int based days + window_wday_int = Hash.new + + # For each day passed in, conver it from text to numeric + # if numeric wasn't used + for wdkey in window_wday.keys + if window_wday[wdkey].is_a? String + window_wday_int[wdkey] = Date::ABBR_DAYNAMES.map(&:downcase).find_index(window_wday[wdkey][0..2].downcase) + elsif window_wday[wdkey].is_a? Integer + window_wday_int[wdkey] = window_wday[wdkey] + else + raise Puppet::ParseError, "Invalid day found in supplied window days(its not a string or integer)" + end + end + + # One more validation - did we lookup valid values? + window_wday_int.keys.each { |wi| + if !window_wday_int[wi].is_a? Integer + raise Puppet::ParseError, "Invalid day found in supplied window days" + end + + if !(0..6).cover?(window_wday_int[wi]) + raise Puppet::ParseError, "Supplied weekday number is out of range (i.e. not 0-6) - value found #{window_wday_int[wi]}" + end + } + + ## Stores window start end time + #24Hour #Minute + #Example: {'start' => '17:00', 'end' => '23:00'} + + # Validate we got a usable time hash provided + args[3].keys.each { |ts| + if not ['start','end'].include?(ts) + raise Puppet::ParseError, "Invalid key provided for window_time. Only start/end supported - found #{ts}" + end + + if not args[3][ts] =~ /^\d(\d)?\:\d\d$/ + raise Puppet::ParseError, "Invalid time supplied for window_time #{ts} - found #{args[3][ts]}" + end + } + + # Now that we know its valid store it as a hash + start_time = args[3]['start'].split(':').map(&:to_i) + end_time = args[3]['end'].split(':').map(&:to_i) + window_time = {'start' => start_time, 'end' => end_time } + window_time_str = {'start' => sprintf('%02d:%02d', start_time[0], start_time[1]), + 'end' => sprintf('%02d:%02d',end_time[0],end_time[1])} + + # Get Time (this will use localtime) + if args.length != 5 + t = Time.now.getlocal(timezone) + else + raise Puppet::ParseError, "Invalid key for time expected 5 values and received #{args[4].length}" unless args[4].length == 5 + begin + t = Time.new( *args[4], 0, timezone) + rescue Exception + # Catch exception and rebrand as parse error + raise Puppet::ParseError, "Could not convert time array into valid Time.new object, received #{args[4].to_a}" + end + end + + # Build array of valid_days + if window_wday_int['start'] > window_wday_int['end'] + # EOW-Wrap 4,1 = 4,5,6,0,1 + valid_days = (window_wday_int['start']..6).to_a + (0..window_wday_int['end']).to_a.uniq + else + # Within-EOW 1,4 = 1,2,3,4 + valid_days = (window_wday_int['start']..window_wday_int['end']).to_a.uniq + end + + # Determine if today is within the valid days for the change window + if valid_days.include?(t.wday) + + # IF this is the first day of the window + if t.wday == window_wday_int['start'] and valid_days.length > 1 + # If window type or per_day with midnight wrap + if window_type == 'window' or (window_type == 'per_day' and window_time_str['start'] > window_time_str['end']) + # IF we are within and 23:59 return true + window_time['end'] = [23,59] + end + change_window_time_is_within(window_time['start'],window_time['end'],[t.hour,t.min]) == true ? true.to_s : false.to_s + + # If this is the last day of the window + elsif t.wday == window_wday_int['end'] and valid_days.length > 1 + + # If window type or per_day with midnight wrap + if window_type == 'window' or (window_type == 'per_day' and window_time_str['start'] > window_time_str['end']) + #If we are within 00:00 and end + window_time['start'] = [0, 0] + end + change_window_time_is_within(window_time['start'],window_time['end'],[t.hour,t.min]) == true ? true.to_s : false.to_s + + # If 1 day window type or per_day wo/ midnight wrap + elsif (valid_days.length == 1 and window_type == 'window') or window_type == 'per_day' + change_window_time_is_within(window_time['start'],window_time['end'],[t.hour,t.min]) == true ? true.to_s : false.to_s + + + # midweek per_day + elsif window_type == 'per_day' + change_window_time_is_within(window_time['start'],window_time['end'],[t.hour,t.min]) == true ? true.to_s : false.to_s + + # Fall through matches window type and between window start/end + else + true.to_s + end + + # Your not within the valid_days + else + false.to_s + end + + end +end diff --git a/lib/puppet/functions/change_window/merge_change_windows.rb b/lib/puppet/functions/change_window/merge_change_windows.rb new file mode 100644 index 0000000..28ab8d6 --- /dev/null +++ b/lib/puppet/functions/change_window/merge_change_windows.rb @@ -0,0 +1,53 @@ +# This is an autogenerated function, ported from the original legacy version. +# It /should work/ as is, but will not have all the benefits of the modern +# function API. You should see the function docs to learn how to add function +# signatures for type safety and to document this function using puppet-strings. +# +# https://puppet.com/docs/puppet/latest/custom_functions_ruby.html +# +# ---- original file header ---- +require 'date' + +# ---- original file header ---- +# +# @summary +# Creates complex change windows by merging a series of windows together +# +Puppet::Functions.create_function(:'change_window::merge_change_windows') do + # @param args + # The original array of arguments. Port this to individually managed params + # to get the full benefit of the modern function API. + # + # @return [Data type] + # Describe what the function returns here + # + dispatch :default_impl do + # Call the method named 'default_impl' when this is matched + # Port this to match individual params for better type safety + repeated_param 'Any', :args + end + + + def default_impl(*args) + + + # Validate arguments + raise Puppet::ParseError, "Invalid argument count, got #{args.length} and expected 1" unless args.length == 1 + raise Puppet::ParseError, "Change_windows must be an array!" unless args[0].is_a? Array + + in_cw = false + args[0].each { |cw| + raise Puppet::ParseError, "Expect an Array for change_window entry, received #{cw.class}" unless cw.is_a? Array + begin + if function_change_window( cw ) == 'true' + in_cw = true + end + rescue Exception => e + # Catch exception and rebrand it as ours + raise Puppet::ParseError, "change_window threw #{e.message}" + end + } + return in_cw.to_s + + end +end diff --git a/spec/functions/change_window_change_window_spec.rb b/spec/functions/change_window_change_window_spec.rb new file mode 100644 index 0000000..984ae1e --- /dev/null +++ b/spec/functions/change_window_change_window_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe 'change_window::change_window' do + # without knowing details about the implementation, this is the only test + # case that we can autogenerate. You should add more examples below! + it { is_expected.not_to eq(nil) } + +################################# +# Below are some example test cases. You may uncomment and modify them to match +# your needs. Notice that they all expect the base error class of `StandardError`. +# This is because the autogenerated function uses an untyped array for parameters +# and relies on your implementation to do the validation. As you convert your +# function to proper dispatches and typed signatures, you should change the +# expected error of the argument validation examples to `ArgumentError`. +# +# Other error types you might encounter include +# +# * StandardError +# * ArgumentError +# * Puppet::ParseError +# +# Read more about writing function unit tests at https://rspec-puppet.com/documentation/functions/ +# +# it 'raises an error if called with no argument' do +# is_expected.to run.with_params.and_raise_error(StandardError) +# end +# +# it 'raises an error if there is more than 1 arguments' do +# is_expected.to run.with_params({ 'foo' => 1 }, 'bar' => 2).and_raise_error(StandardError) +# end +# +# it 'raises an error if argument is not the proper type' do +# is_expected.to run.with_params('foo').and_raise_error(StandardError) +# end +# +# it 'returns the proper output' do +# is_expected.to run.with_params(123).and_return('the expected output') +# end +################################# + +end diff --git a/spec/functions/change_window_merge_change_windows_spec.rb b/spec/functions/change_window_merge_change_windows_spec.rb new file mode 100644 index 0000000..c3ceec6 --- /dev/null +++ b/spec/functions/change_window_merge_change_windows_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe 'change_window::merge_change_windows' do + # without knowing details about the implementation, this is the only test + # case that we can autogenerate. You should add more examples below! + it { is_expected.not_to eq(nil) } + +################################# +# Below are some example test cases. You may uncomment and modify them to match +# your needs. Notice that they all expect the base error class of `StandardError`. +# This is because the autogenerated function uses an untyped array for parameters +# and relies on your implementation to do the validation. As you convert your +# function to proper dispatches and typed signatures, you should change the +# expected error of the argument validation examples to `ArgumentError`. +# +# Other error types you might encounter include +# +# * StandardError +# * ArgumentError +# * Puppet::ParseError +# +# Read more about writing function unit tests at https://rspec-puppet.com/documentation/functions/ +# +# it 'raises an error if called with no argument' do +# is_expected.to run.with_params.and_raise_error(StandardError) +# end +# +# it 'raises an error if there is more than 1 arguments' do +# is_expected.to run.with_params({ 'foo' => 1 }, 'bar' => 2).and_raise_error(StandardError) +# end +# +# it 'raises an error if argument is not the proper type' do +# is_expected.to run.with_params('foo').and_raise_error(StandardError) +# end +# +# it 'returns the proper output' do +# is_expected.to run.with_params(123).and_return('the expected output') +# end +################################# + +end