# 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.
#

unified_mode(false) if Chef::VERSION >= 18 # TODO(T144966423)
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
  requires_full_restart = false
  to_converge = []
  interface = new_resource.interface
  config = new_resource.config

  ifcfg_file = "/etc/sysconfig/network-scripts/ifcfg-#{interface}"
  hwaddr = FB::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, "Would have enabled #{interface}"
      )
    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 node.root_user
    group node.root_group
    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'
    rule_key = 'extra_rules'
  else
    route_key = "extra_routes_#{interface}"
    rule_key = "extra_rules_#{interface}"
  end
  route_config = node['fb_network_scripts']['routing'][route_key]
  rule_config = node['fb_network_scripts']['rules'][rule_key]
  if route_config
    extra_routes = route_config.to_hash
  else
    extra_routes = {}
  end
  if rule_config
    extra_rules = rule_config.to_hash
  else
    extra_rules = {}
  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
  # Check for the enabled rules and add them to networking scripts
  unless extra_rules.empty?
    rule_file = "/etc/sysconfig/network-scripts/rule-#{interface}"
    rule6_file = "/etc/sysconfig/network-scripts/rule6-#{interface}"
    v4_rules =
      extra_rules.select { |_pref, route| route['protocol'].include?('v4') }
    v6_rules =
      extra_rules.select { |_pref, route| route['protocol'].include?('v6') }
    fb_helpers_gated_template rule_file do
      allow_changes node.nw_changes_allowed?
      owner 'root'
      group 'root'
      mode '0644'
      source 'rule.erb'
      variables({
                  'rules' => v4_rules,
                })
      gated_action v4_rules.empty? ? :delete : :create
    end
    fb_helpers_gated_template rule6_file do
      allow_changes node.nw_changes_allowed?
      owner 'root'
      group 'root'
      mode '0644'
      source 'rule.erb'
      variables({
                  'rules' => v6_rules,
                })
      gated_action v6_rules.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
  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, "Would have started #{interface}"
    )
  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, "Would have stopped #{interface}"
    )
  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, "Would have disabled #{interface}"
    )
  end
end
