# Encoding: utf-8
# Author:: Hugo Fichter
# Author:: Lamont Granquist (<lamont@getchef.com>)
# Author:: Joshua Timberman (<joshua@getchef.com>)
# Copyright:: Copyright (c) 2009-2014 Chef Software, Inc
# License:: Apache License, Version 2.0
#
# 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.
#

require 'chef/provider/mount'
require 'chef/log'
require 'forwardable'

class Chef
  class Provider
    class Mount
      # Mount Solaris File systems
      class Solaris < Chef::Provider::Mount
        extend Forwardable

        VFSTAB = '/etc/vfstab'.freeze

        def_delegator :@new_resource, :device, :device
        def_delegator :@new_resource, :device_type, :device_type
        def_delegator :@new_resource, :dump, :dump
        def_delegator :@new_resource, :fsck_device, :fsck_device
        def_delegator :@new_resource, :fstype, :fstype
        def_delegator :@new_resource, :mount_point, :mount_point
        def_delegator :@new_resource, :options, :options
        def_delegator :@new_resource, :pass, :pass

        def load_current_resource
          @current_resource = Chef::Resource::Mount.new(new_resource.name)
          current_resource.mount_point(mount_point)
          current_resource.device(device)
          current_resource.fsck_device(fsck_device)
          current_resource.device_type(device_type)
          update_current_resource_state
        end

        def define_resource_requirements
          requirements.assert(:mount, :remount) do |a|
            a.assertion { !device_should_exist? || ::File.exist?(device) }
            a.failure_message(Chef::Exceptions::Mount, "Device #{device} does not exist")
            a.whyrun("Assuming device #{device} would have been created")
          end

          unless fsck_device == '-'
            requirements.assert(:mount, :remount) do |a|
              a.assertion { ::File.exist?(fsck_device) }
              a.failure_message(Chef::Exceptions::Mount, "Device #{fsck_device} does not exist")
              a.whyrun("Assuming device #{fsck_device} would have been created")
            end
          end

          requirements.assert(:mount, :remount) do |a|
            a.assertion { ::File.exist?(mount_point) }
            a.failure_message(Chef::Exceptions::Mount, "Mount point #{mount_point} does not exist")
            a.whyrun("Assuming mount point #{mount_point} would have been created")
          end
        end

        def mount_fs
          actual_options = options || []
          actual_options.delete('noauto')
          command = "mount -F #{fstype}"
          command << " -o #{actual_options.join(',')}" unless actual_options.empty?
          command << " #{device} #{mount_point}"
          shell_out!(command)
        end

        def umount_fs
          shell_out!("umount #{mount_point}")
        end

        def remount_fs
          # FIXME: Should remount always do the remount or only if the options change?
          actual_options = options || []
          actual_options.delete('noauto')
          mount_options = actual_options.empty? ? '' : ",#{actual_options.join(',')}"
          shell_out!("mount -o remount#{mount_options} #{mount_point}")
        end

        def enable_fs
          unless mount_options_unchanged?
            # we are enabling because our options have changed, so disable first then re-enable.
            # XXX: this should be refactored to be the responsibility of the caller
            disable_fs if current_resource.enabled
          end

          vfstab_write(merge_vfstab_entry)
        end

        def disable_fs
          contents, found = delete_vfstab_entry

          if found
            vfstab_write(contents.reverse)
          else
            # this is likely some kind of internal error, since we should only call disable_fs when there
            # the filesystem we want to disable is enabled.
            Chef::Log.warn("#{new_resource} did not find the mountpoint to disable in the vfstab")
          end
        end

        def etc_tempfile
          yield Tempfile.open('vfstab', '/etc')
        end

        def mount_options_unchanged?
          new_options = options_remove_noauto(options)
          current_options = options_remove_noauto(current_resource.nil? ? nil : current_resource.options)

          current_resource.fsck_device == fsck_device &&
            current_resource.fstype == fstype &&
            current_options == new_options &&
            current_resource.dump == dump &&
            current_resource.pass == pass &&
            current_resource.options.include?('noauto') == !mount_at_boot?
        end

        def update_current_resource_state
          current_resource.mounted(mounted?)
          (enabled, fstype, options, pass) = read_vfstab_status
          current_resource.enabled(enabled)
          current_resource.fstype(fstype)
          current_resource.options(options)
          current_resource.pass(pass)
        end

        def enabled?
          read_vfstab_status[0]
        end

        # Check for the device in mounttab.
        # <device> on <mountpoint> type <fstype> <options> on <date>
        # /dev/dsk/c1t0d0s0 on / type ufs read/write/setuid/devices/intr/largefiles/logging/xattr/onerror=panic/dev=700040 on Tue May  1 11:33:55 2012
        def mounted?
          mounted = false
          shell_out!('mount -v').stdout.each_line do |line|
            case line
            when /^#{device_regex}\s+on\s+#{Regexp.escape(mount_point)}\s+/
              Chef::Log.debug("Special device #{device} is mounted as #{mount_point}")
              mounted = true
            when /^([\/\w]+)\son\s#{Regexp.escape(mount_point)}\s+/
              Chef::Log.debug("Special device #{Regexp.last_match[1]} is mounted as #{mount_point}")
              mounted = false
            end
          end
          mounted
        end

        private

        def read_vfstab_status
          # Check to see if there is an entry in /etc/vfstab. Last entry for a volume wins.
          enabled = false
          fstype = options = pass = nil
          ::File.foreach(VFSTAB) do |line|
            case line
            when /^[#\s]/
              next
              # solaris /etc/vfstab format:
              # device         device          mount           FS      fsck    mount   mount
              # to mount       to fsck         point           type    pass    at boot options
            when /^#{device_regex}\s+[-\/\w]+\s+#{Regexp.escape(mount_point)}\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/
              enabled = true
              fstype = Regexp.last_match[1]
              options = Regexp.last_match[4]
              # Store the 'mount at boot' column from vfstab as the 'noauto' option
              # in current_resource.options (linux style)
              if Regexp.last_match[3] == 'no'
                if options.nil? || options.empty?
                  options = 'noauto'
                else
                  options += ',noauto'
                end
              end
              pass = (Regexp.last_match[2] == '-') ? 0 : Regexp.last_match[2].to_i
              Chef::Log.debug("Found mount #{device} to #{mount_point} in #{VFSTAB}")
              next
            when /^[-\/\w]+\s+[-\/\w]+\s+#{Regexp.escape(mount_point)}\s+/
              # if we find a mountpoint on top of our mountpoint, then we are not enabled
              enabled = false
              Chef::Log.debug("Found conflicting mount point #{mount_point} in #{VFSTAB}")
            end
          end
          [enabled, fstype, options, pass]
        end

        def device_should_exist?
          !%w(tmpfs nfs ctfs proc mntfs objfs sharefs fd smbfs vxfs).include?(fstype)
        end

        def mount_at_boot?
          options.nil? || !options.include?('noauto')
        end

        def vfstab_write(contents)
          etc_tempfile do |f|
            f.write(contents.join(''))
            f.close
            # move, preserving modes of destination file
            mover = Chef::FileContentManagement::Deploy.strategy(true)
            mover.deploy(f.path, VFSTAB)
          end
        end

        def vfstab_entry
          actual_options = unless options.nil?
            tempops = options.dup
            tempops.delete('noauto')
            tempops
          end
          autostr = mount_at_boot? ? 'yes' : 'no'
          passstr = pass == 0 ? '-' : pass
          optstr = (actual_options.nil? || actual_options.empty?) ? '-' : actual_options.join(',')
          "\n#{device}\t#{fsck_device}\t#{mount_point}\t#{fstype}\t#{passstr}\t#{autostr}\t#{optstr}\n"
        end

        def delete_vfstab_entry
          contents = []
          found = false
          ::File.readlines(VFSTAB).reverse_each do |line|
            if !found && line =~ /^#{device_regex}\s+\S+\s+#{Regexp.escape(mount_point)}/
              found = true
              Chef::Log.debug("#{new_resource} is removed from vfstab")
              next
            end
            contents << line
          end
          [contents, found]
        end

        def merge_vfstab_entry
          contents = ::File.readlines(VFSTAB)
          contents[-1].chomp!
          contents << vfstab_entry
        end

        def options_remove_noauto(temp_options)
          new_options = []
          new_options += temp_options.nil? ? [] : temp_options
          new_options.delete('noauto')
          new_options
        end

        def device_regex
          if ::File.symlink?(device)
            "(?:#{Regexp.escape(device)}|#{Regexp.escape(::File.expand_path(::File.readlink(device), ::File.dirname(device)))})"
          else
            Regexp.escape(device)
          end
        end
      end
    end
  end
end
