cookbooks/fb_network_scripts/resources/redhat_interface.rb (322 lines of code) (raw):

# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 # # Copyright (c) 2012-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. # default_action :nothing property :interface, :kind_of => String, :name_attribute => true property :config, :kind_of => Hash # for internal use property :running, :kind_of => [TrueClass, FalseClass] property :path, :kind_of => String # This one is here for `load_current_value` class Helpers extend ::FB::NetworkScripts::RHInterfaceHelpers end # This one is here for all the actions action_class do class Helpers extend ::FB::NetworkScripts::RHInterfaceHelpers end end load_current_value do |new_resource| running Helpers.running?(new_resource.interface, node) end # This can shellout because it's called within `converge_by` def start(interface, best_effort) s = Mixlib::ShellOut.new("/sbin/ifup #{interface}").run_command unless best_effort s.error! end end # This can shellout because it's called within `converge_by` def stop(interface) # handle the case where we have an up interface without a config file if ::File.exist?("/etc/sysconfig/network-scripts/ifcfg-#{interface}") cmd = "/sbin/ifdown #{interface}" else cmd = "/sbin/ifconfig #{interface} down" end s = Mixlib::ShellOut.new(cmd).run_command s.error! end action :enable do # ~FC017 requires_full_restart = false to_converge = [] interface = new_resource.interface config = new_resource.config ifcfg_file = "/etc/sysconfig/network-scripts/ifcfg-#{interface}" hwaddr = Helpers.get_hwaddr(interface) config_hwaddr = config['hwaddr'] if config_hwaddr unless hwaddr fail "fb_network_scripts: no hwaddr found for #{interface} on the " + 'running system, cannot set explicity configured hwaddr ' + "(#{config_hwaddr})" end # MAC addresses, like IPv6 are hexadecimal based. Unlike IPv6 # there is no compression (removing zeros). There are a number # of formats: # (1) 01:23:45:67:89:ab (most common, in Linux) # (2) 01-23-45-67-89-ab (rfc7043 EUI48 RR) # (3) 0123:4567:89ab (Cisco, et al.) # We expect and only support format (1) # Differences in case (of hexadecimal characters) is ignored. if hwaddr.casecmp(config_hwaddr) != 0 fail 'fb_network_scripts: Explicitly configured hwaddr ' + "(#{config_hwaddr}) does not match running system (#{hwaddr})" end end primary_int = node['fb_network_scripts']['primary_interface'] routing_config = node['fb_network_scripts']['routing'].to_hash # Hack to prevent issues like #8540745 if ::File.exist?('/etc/sysconfig/network-scripts/ifcfg-br0') && primary_int != 'br0' fail 'fb_network_scripts: It looks like you\'re trying to go back from ' + 'a bridged primary interface. This will require a full networking ' + 'restart, and possibly a reboot to recover. If you want to ' + 'proceed, delete /etc/sysconfig/network-scripts/ifcfg-br0 and run ' + 'Chef again.' end # There is an ifconfig resource in chef, but it doesn't handle # ipv6, or ranges, so we don't use it tmp_ifcfg_file = ::File.join( Chef::Config[:file_cache_path], ::File.basename(ifcfg_file), ) # build resource gives you a resource that's not in the resource collection # this is important in this case as we don't want this resource to count # towards whether or not WE notify. t = build_resource(:template, tmp_ifcfg_file) do owner 'root' group 'root' mode '0644' source 'ifcfg.erb' variables({ 'interface' => interface, 'config' => config, 'hwaddr' => hwaddr, 'routing_config' => routing_config, }) action :nothing end t.run_action(:create) # this logic isn't conditional on tmp_ifcfg_file updating, because it's not # a source of truth... if someone fucks with the real ifcfg_file, we want to # catch that. updated_keys = ['all'] if ::File.exist?(ifcfg_file) current_file = Helpers.read_ifcfg(ifcfg_file) new_file = Helpers.read_ifcfg(tmp_ifcfg_file) updated_keys = Helpers.get_changed_keys(current_file, new_file) end unless updated_keys.empty? updated_keys.each do |key| case key when 'IPV6_SET_SYSCTLS' # We treat IPV6_SET_SYSCTL as a no-op while we roll it out, it only # affects the up/down and not running state next when 'IPV6ADDR_SECONDARIES' to_converge << :ips when 'MTU' to_converge << :mtu else Chef::Log.debug( "fb_network_scripts[#{interface}]: #{key} changed, will need " + 'interface restart', ) # not something we know how to change nicely requires_full_restart = true end end end # So in the event we entered the above conditional, we need to make sure # we update the ifcfg file. However, since `get_changed_keys` intelligently # checks for non-meaningful changes such as ip6 addr case changes, we *also* # need to see if the file itself changed, even if there is no meaninful change # needed, and update the file. if !updated_keys.empty? || current_file != new_file if node.interface_change_allowed?(interface) || !requires_full_restart # Options for updating the file: # * make a new resource for this. Updates will cause two resources to # fire but that's not too terrible # * move the file. Clean except the *next* run will update the temp # file which may confuse people # * copy the file. This isn't atomic, so we actually need to copy to # /tmp and them move it. Code is slightly uglier, but it's the # cleanest end-user experience # Tempfile expects the resource passed in to have a `path` method # that returns the file we eventually plan to make new_resource.path = ifcfg_file tempfile = Chef::FileContentManagement::Tempfile.new(new_resource).tempfile tpath = tempfile.path tempfile.close FileUtils.copy(tmp_ifcfg_file, tpath) ::File.rename(tpath, ifcfg_file) else Chef::Log.info( "fb_network_scripts[#{interface}]: not allowed to change " + ifcfg_file.to_s, ) Chef::Log.info( "fb_network_scripts[#{interface}]: requesting nw change permission", ) FB::Helpers._request_nw_changes_permission(run_context, new_resource) end end # This enforces ownership and permissions, not content so we can ensure that # other users are able to read the ifcfg-* files when needed. See T22854783 # we use build_resource because we don't want to trigger a restart just # because we fix permissions t = build_resource(:file, ifcfg_file) do only_if { node.interface_change_allowed?(interface) } owner 'root' group 'root' mode '0644' action :nothing end t.run_action(:create) t = template "#{ifcfg_file}-range" do owner 'root' group 'root' mode '0644' source 'ifcfg-range.erb' variables({ 'interface' => interface, 'config' => config, }) action :nothing end t.run_action(config['range'] ? :create : :delete) if interface == primary_int route_key = 'extra_routes' else route_key = "extra_routes_#{interface}" end route_config = node['fb_network_scripts']['routing'][route_key] if route_config extra_routes = route_config.to_hash else extra_routes = {} end unless extra_routes.empty? route_file = "/etc/sysconfig/network-scripts/route-#{interface}" route6_file = "/etc/sysconfig/network-scripts/route6-#{interface}" v4_routes = extra_routes.reject { |k, _v| k.include?(':') } v6_routes = extra_routes.select { |k, _v| k.include?(':') } fb_helpers_gated_template route_file do allow_changes node.nw_changes_allowed? owner 'root' group 'root' mode '0644' source 'route.erb' variables({ 'routes' => v4_routes, 'config' => config, }) gated_action v4_routes.empty? ? :delete : :create end fb_helpers_gated_template route6_file do allow_changes node.nw_changes_allowed? owner 'root' group 'root' mode '0644' source 'route.erb' variables({ 'routes' => v6_routes, 'config' => config, }) gated_action v6_routes.empty? ? :delete : :create end end # We enqueue all the actions here that are "gently massaging" things without # a full restart. That's OK, because even if we end up enqueueing a restart, # these actions all check for that first. to_converge.each do |key| case key when :ips Helpers.queue_update_ips(run_context, new_resource) when :mtu Helpers.queue_update_mtu(run_context, new_resource) end end # In the event that our magic was not good enough, ensure that things get # restarted if requires_full_restart if node.interface_change_allowed?(interface) Chef::Log.warn( "fb_network_scripts[#{interface}]: full network interface restart " + 'required', ) new_resource.updated_by_last_action true end end end action :update_ips do # ~FC017 interface = new_resource.interface if Helpers.will_restart_network?(run_context) Chef::Log.info("Ignoring #{interface} update_ips, network restart queued") else converge_by("update IPs on #{new_resource}") do config = new_resource.config Chef::Log.info("New resource #{config}") to_add, to_remove = Helpers.get_v6_changes(node, interface, config) Chef::Log.debug( "fb_network_scripts_redhat_interface[#{interface}] updating IPs: " + "#{to_add.size} to add and #{to_remove.size} to remove", ) unless to_add.empty? Helpers.add_v6addrs(interface, to_add) Chef::Log.info( "fb_network_scripts_redhat_interface[#{interface}] Added IPs: " + to_add.to_a.to_s, ) end unless to_remove.empty? Helpers.remove_v6addrs(interface, to_remove) Chef::Log.info( "fb_network_scripts_redhat_interface[#{interface}] Removed IPs: " + to_remove.to_a.to_s, ) end end end end action :update_mtu do interface = new_resource.interface if Helpers.will_restart_network?(run_context) Chef::Log.info("Ignoring #{interface} update_mtu, network restart queued") else converge_by("update MTU on #{new_resource}") do mtu = new_resource.config['mtu'] || 1500 Helpers.set_mtu(interface, mtu) Chef::Log.info( "fb_network_scripts_redhat_interface[#{interface}] set mtu to #{mtu}", ) end end end action :start do interface = new_resource.interface if Helpers.will_restart_network?(run_context) Chef::Log.info("Ignoring #{interface} start, network restart queued") elsif current_resource.running Chef::Log.debug("#{interface} already up") elsif node.interface_start_allowed?(interface) converge_by("start #{new_resource}") do start(interface, new_resource.config.fetch('best_effort', false)) Chef::Log.info( "fb_network_scripts_redhat_interface[#{interface}] started", ) end else Chef::Log.info( "fb_network_scripts[#{interface}]: not allowed to start #{interface}", ) Chef::Log.info( "fb_network_scripts[#{interface}]: requesting nw change permission", ) FB::Helpers._request_nw_changes_permission(run_context, new_resource) end end action :stop do interface = new_resource.interface if Helpers.will_restart_network?(run_context) Chef::Log.info("Ignoring #{interface} stop, network restart queued") elsif !current_resource.running Chef::Log.debug("#{interface} already down") elsif node.interface_change_allowed?(interface) converge_by("stop #{new_resource}") do stop(interface) Chef::Log.info("fb_network_scripts_redhat_interface[#{interface}] stop") end else Chef::Log.info("fb_network_scripts[#{interface}]: not allowed to stop " + interface.to_s) Chef::Log.info("fb_network_scripts[#{interface}]: requesting nw change " + 'permission') FB::Helpers._request_nw_changes_permission(run_context, new_resource) end end action :restart do interface = new_resource.interface if Helpers.will_restart_network?(run_context) Chef::Log.info("Ignoring #{interface} restart, network restart queued") else converge_by("restart #{new_resource}") do if current_resource.running stop(interface) end start(interface, new_resource.config.fetch('best_effort', false)) Chef::Log.info( "fb_network_scripts_redhat_interface[#{interface}] restarted", ) end end end action :disable do interface = new_resource.interface if node.interface_change_allowed?(interface) basefile = "/etc/sysconfig/network-scripts/ifcfg-#{interface}" [basefile, "#{basefile}-range"].each do |fname| file fname do action :delete end end else Chef::Log.info( "fb_network_scripts[#{interface}]: not allowed to disable #{interface}", ) Chef::Log.info( "fb_network_scripts[#{interface}]: requesting nw change permission", ) FB::Helpers._request_nw_changes_permission(run_context, new_resource) end end