cookbooks/fb_ipset/resources/default.rb (75 lines of code) (raw):

# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 # # Copyright (c) 2017-present, Facebook, Inc. # All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. property :state_file, String default_action :update action_class do def ipset_save(state_file) ipset_save_output = Mixlib::ShellOut.new('ipset save') ipset_save_output.run_command.error! file state_file do owner 'root' group 'root' mode '0600' content ipset_save_output.stdout end end end action :update do existing_ipsets = FB::IPset.get_existing_ipsets expected_ipsets = node['fb_ipset']['sets'].to_hash # all ipsets should contain those keys for the comparison to be correct required_keys = ['type', 'family', 'hashsize', 'maxelem', 'members'].to_set # rubocop:disable Style/CombinableLoops # We do not want to combine this loop with the identical loop below because # we want to check all ipsets before acting on any of them expected_ipsets.each do |name, attributes| if attributes.keys.to_set != required_keys fail "Set #{name} should contain #{required_keys.to_a} keys" end end expected_ipsets.each do |setname, expected_set| unless existing_ipsets[setname] Chef::Log.info("fb_ipset[#{setname}]: Creating set") converge_by "Creating set #{setname}" do FB::IPset.ipset_to_cmds(setname, expected_set).each do |cmd| shell_out(cmd).error! end end next end existing_set = existing_ipsets.delete(setname) existing_set['members'] = existing_set['members'].sort # you give ipset addresses with a netmask, but if it's a /32 # then when it gives it back to you, it does not include that... # which means that if someone specifies it, we'll never be idempotent, # so strip that from what they gave us so that they match expected_set['members'] = expected_set['members'].map do |addr| if addr.end_with?('/32') addr.gsub('/32', '') else addr end end.sort # Further, people will likely set 'maxelem' to a number, since # it's a number, but we'll get it as a string, so stringify it expected_set['maxelem'] = expected_set['maxelem'].to_s if existing_set == expected_set next end Chef::Log.info("fb_ipset[#{setname}]: Updating set") Chef::Log.debug("fb_ipset[#{setname}]: old: #{existing_set.to_a.sort}") Chef::Log.debug("fb_ipset[#{setname}]: new: #{expected_set.to_a.sort}") # create the replacement ipset new_name = "#{setname}NEW" cmds = FB::IPset.ipset_to_cmds(new_name, expected_set) # swap and cleanup cmds << "ipset swap #{new_name} #{setname}" cmds << "ipset flush #{new_name}" cmds << "ipset destroy #{new_name}" # run the actual commands converge_by "Create and swap #{setname}" do cmds.each do |cmd| shell_out(cmd).error! end end end # rubocop:enable Style/CombinableLoops ipset_save new_resource.state_file || node['fb_ipset']['state_file'] end action :cleanup do existing_ipsets = FB::IPset.get_existing_ipsets.keys.to_set expected_ipsets = node['fb_ipset']['sets'].keys.to_set (existing_ipsets - expected_ipsets).each do |setname| Chef::Log.info("fb_ipset[#{setname}]: Set not defined. Removing") cmd = "ipset destroy #{setname}" converge_by "Delete #{setname}" do r = shell_out(cmd) r.error! end end ipset_save new_resource.state_file || node['fb_ipset']['state_file'] end