# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2
# Copyright (c) 2016-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.
#

module FB
  # Module to be loaded into the provider namespace
  module FstabProvider
    def mount(mount_data, in_maint_disks, in_maint_mounts)
      if in_maint_mounts.include?(mount_data['mount_point'])
        Chef::Log.warn(
          "fb_fstab: Skipping mount of #{mount_data['mount_point']} because " +
          'the mount is marked as in-maintenance',
        )
        return true
      end

      relevant_disk = nil
      in_maint_disks.each do |disk|
        if mount_data['device'].start_with?(disk)
          relevant_disk = disk
          break
        end
      end
      if relevant_disk
        Chef::Log.warn(
          "fb_fstab: Skipping mount of #{mount_data['mount_point']} because " +
          " device #{relevant_disk} is marked as in-maintenance",
        )
        return true
      end

      # We don't use a 'directory' resource, because that would happen
      # later. Also, we don't want to conflict with resources that may
      # actually managed this directory (though they better check it's
      # not mounted :)). We just make sure there's a safe directory
      # to mount to before we call 'mount'
      #
      # Further, because we're called in a 'converge' block, this is whyrun
      # safe, just like the Mixlib::ShellOut below.
      Chef::Log.info("fb_fstab: Mounting #{mount_data['mount_point']}")
      if ::File.exist?(mount_data['mount_point'])
        if ::File.symlink?(mount_data['mount_point'])
          if mount_data['allow_mount_failure']
            msg = "fb_fstab: #{mount_data['mount_point']} is a symlink. " +
                  'This is probably not what you want and could cause SEVs.'
            Chef::Log.warn(msg)
            return false
          else
            fail "fb_fstab: #{mount_data['mount_point']} is a symlink, thus" +
              ' I will not mount over it.'
          end
        end
      else
        # we pass in a relatively sane perm which is subject to umask If they
        # sent in perms, we'll do a real chmod right afterward which isn't
        # subject to umask.
        # rubocop:disable Chef/Meta/DontUseFileUtils
        FileUtils.mkdir_p(mount_data['mount_point'], :mode => 0o755)
        if mount_data['mp_perms']
          FileUtils.chmod(mount_data['mp_perms'].to_i(8),
                          mount_data['mount_point'])
        end
        if mount_data['mp_owner'] || mount_data['mp_group']
          FileUtils.chown(mount_data['mp_owner'], mount_data['mp_group'],
                          mount_data['mount_point'])
        end
        # rubocop:enable Chef/Meta/DontUseFileUtils
        if mount_data['mp_immutable']
          readme = File.join(mount_data['mount_point'], 'README.txt')
          readme_body = 'This directory was created by chef to be an ' +
            'immutable mountpoint.  If you can see this, ' +
            "the mount is missing!\n"
          # rubocop:disable Chef/Meta/NoFileWrites
          File.open(readme, 'w') do |f| # rubocop:disable Chef/Meta/NoFileWrites
            f.write(readme_body)
          end
          # rubocop:enable Chef/Meta/NoFileWrites
          s = Mixlib::ShellOut.new(
            "/bin/chattr +i #{mount_data['mount_point']}",
          ).run_command
          s.error!
        end

      end
      if node.systemd?
        # If you use FUSE mounts, and you're running Chef from a systemd timer,
        # the FUSE helpers will be killed after a chef run, which unmounts your
        # filesystem, which is clearly not what you want. So we instead want to
        # ask systemd to do the mount for us. The cleanest way to do that is to
        # ask systemd what it's generated unit for that mount is and then
        # "start" that unit.
        #
        # HOWEVER!!!! NOTE WELL!!!!
        # If you don't do a `systemctl daemon-reload` after updating /etc/fstab,
        # then this won't be guaranteed to mount the right thing, so we have
        # that notify in the recipe
        s = Mixlib::ShellOut.new(
          "/bin/systemd-escape -p --suffix=mount #{mount_data['mount_point']}",
        ).run_command
        s.error!
        mountunit = s.stdout.chomp.shellescape
        s = Mixlib::ShellOut.new("/bin/systemctl start #{mountunit}")
      else
        # We cd into /dev/shm because otherwise mount will be dumb and
        # change the device of 'foo' to be '/foo' if /foo happens to exist.
        #
        # I COULD call --no-canonicalize except that when /bin/mount calls
        # /sbin/mount.tmpfs, it won't preserve that option, and then calls
        # /bin/mount -i without it and it's canonicalized anyway. Further on
        # other FS's --no-canonicalize may not be safe. THUS, we cd to a place
        # that should be emptyish.
        s = Mixlib::ShellOut.new(
          "cd /dev/shm && /bin/mount #{mount_data['mount_point']}",
        )
      end

      _run_command_flocked(s,
                           mount_data['lock_file'],
                           mount_data['mount_point'])
      if s.error? && mount_data['allow_mount_failure']
        Chef::Log.warn(
          "fb_fstab: Mounting #{mount_data['mount_point']} failed, but " +
          '"allow_mount_failure" was set, so moving on.',
        )
      else
        s.error!
      end
      true
    end

    def umount(mount_point, lock_file, remove_dir)
      Chef::Log.info("fb_fstab: Unmounting #{mount_point}")
      s = Mixlib::ShellOut.new("/bin/umount #{mount_point}")
      _run_command_flocked(s, lock_file, mount_point)
      if s.error? && node['fb_fstab']['allow_lazy_umount']
        Chef::Log.warn("fb_fstab: #{s.stderr.chomp}")
        Chef::Log.warn(
          "fb_fstab: Unmounting #{mount_point} failed, " +
          'trying lazy unmount.',
        )
        sl = Mixlib::ShellOut.new("/bin/umount -l #{mount_point}")
        _run_command_flocked(sl, lock_file, mount_point)
      else
        s.error!
      end
      if remove_dir == true
        Chef::Log.info("fb_fstab: Best effort removing #{mount_point}")
        s = Mixlib::ShellOut.new("/usr/bin/rmdir #{mount_point}")
        _run_command_flocked(s, lock_file, mount_point)
        if s.error?
          Chef::Log.warn("fb_fstab: #{s.stderr.chomp}")
          Chef::Log.warn(
            "fb_fstab: Removing #{mount_point} failed",
          )
        end
      end
      true
    end

    def remount(mount_data)
      mount_point = mount_data['mount_point']
      with_umount = mount_data['remount_with_umount']
      lock_file = mount_data['lock_file']
      Chef::Log.info("fb_fstab: Remounting #{mount_point}")
      if with_umount
        Chef::Log.debug("fb_fstab: umount and mounting #{mount_point}")
        cmd = "/bin/umount #{mount_point}; /bin/mount #{mount_point}"
      else
        Chef::Log.debug("fb_fstab: 'mount -o remount' on #{mount_point}")
        cmd = "/bin/mount -o remount #{mount_point}"
      end
      s = Mixlib::ShellOut.new(cmd)
      _run_command_flocked(s, lock_file, mount_point)
      if s.error? && mount_data['allow_remount_failure']
        Chef::Log.warn(
          "fb_fstab: Remounting #{mount_point} failed, but " +
          '"allow_remount_failure" was set, so moving on.',
        )
      else
        s.error!
      end
    end

    def get_unmasked_base_mounts(format)
      FB::Fstab.get_unmasked_base_mounts(format, node)
    end

    def canonicalize_device(device)
      FB::Fstab.canonicalize_device(device, node)
    end

    # Given a *mounted* device from node['filesystem'] in `mounted_data`, check
    # to see if we want to keep it. It looks in `desired_mounts` (an export of
    # node['fb_fstab']['mounts'] as well as `base_mounts` (a hash
    # representation of the saved OS mounts file).
    def should_keep(mounted_data, desired_mounts, base_mounts)
      Chef::Log.debug(
        "fb_fstab: Should we keep #{mounted_data}?",
      )
      # Does it look like something in desired mounts?
      desired_mounts.each_value do |desired_data|
        begin
          desired_device = canonicalize_device(desired_data['device'])
        rescue RuntimeError
          next if desired_data['allow_mount_failure']

          raise
        end
        Chef::Log.debug("fb_fstab: --> Lets see if it matches #{desired_data}")
        # if the devices are the same *and* are real devices, the
        # rest doesn't matter - we won't unmount a moved device. moves
        # option changes, etc. are all the work of the 'mount' step later.
        if mounted_data['device']&.start_with?('/dev/')
          if desired_device == mounted_data['device']
            Chef::Log.debug(
              "fb_fstab: Device #{mounted_data['device']} is supposed to be " +
              ' mounted, not considering for unmount',
            )
            return true
          end
        # If it's a virtual device, we just check the type and mount
        # point are the same
        elsif desired_data['mount_point'] == mounted_data['mount'] &&
              compare_fstype(desired_data['type'], mounted_data['fs_type'])
          Chef::Log.debug(
            "fb_fstab: Virtual fs of type #{mounted_data['fs_type']} is " +
            "desired at #{mounted_data['mount']}, not considering for unmount",
          )
          return true
        end
        Chef::Log.debug('fb_fstab: --> ... nope')
      end

      # If not, is it autofs controlled?
      if FB::Fstab.autofs_parent(mounted_data['mount'], node)
        Chef::Log.debug(
          "fb_fstab: #{mounted_data['device']} (#{mounted_data['mount']}) is" +
          ' autofs-controlled.',
        )
        return true
      end

      # If not, is it a base mount?
      # Note that if it's not in desired mounts, we can be more strict,
      # no one is trying to move things... it should be same device and point.
      Chef::Log.debug('fb_fstab: --> OK, well is it a base mount?')
      if base_mounts[mounted_data['device']] &&
         base_mounts[mounted_data['device']]['mount_point'] ==
         mounted_data['mount']
        Chef::Log.debug(
          "fb_fstab: #{mounted_data['device']} on #{mounted_data['mount']} is" +
          ' a base mount, not considering for unmount',
        )
        return true
      end
      false
    end

    # Walk all mounted filesystems and umount anything we don't know about
    def check_unwanted_filesystems
      # extra things to skip
      devs_to_skip = node['fb_fstab']['umount_ignores']['devices'].dup
      dev_prefixes_to_skip =
        node['fb_fstab']['umount_ignores']['device_prefixes'].dup
      mounts_to_skip =
        node['fb_fstab']['umount_ignores']['mount_points'].dup
      mount_prefixes_to_skip =
        node['fb_fstab']['umount_ignores']['mount_point_prefixes'].dup
      mount_regexes_to_skip =
        node['fb_fstab']['umount_ignores']['mount_point_regexes'].dup
      fstypes_to_skip = node['fb_fstab']['umount_ignores']['types'].dup

      base_mounts = get_unmasked_base_mounts(:hash)

      # we're going to iterate over specified mounts a lot, lets dump it
      desired_mounts = node['fb_fstab']['mounts'].to_hash

      fs_data = node.filesystem_data
      fs_data['by_pair'].to_hash.each_value do |mounted_data|
        # ohai uses many things to populate this structure, one of which
        # is 'blkid' which gives info on devices that are not currently
        # mounted. This skips those, plus swap, of course.
        unless mounted_data['mount']
          Chef::Log.debug(
            "fb_fstab: Skipping umount check for #{mounted_data['device']} " +
            "- it isn't mounted.",
          )
          next
        end
        # Work around chef 12 ohai bug
        if mounted_data.key?('inodes_used') && !mounted_data.key?('kb_used')
          Chef::Log.debug(
            'fb_fstab: Skipping mal-formed Chef 12 "df -i" entry ' +
            mounted_data.to_s,
          )
          next
        end
        # skip anything seemingly magical
        if devs_to_skip.include?(mounted_data['device'])
          Chef::Log.debug(
            "fb_fstab: Skipping umount check for #{mounted_data['device']} " +
            "(#{mounted_data['mount']}): exempted device",
          )
          next
        elsif mounts_to_skip.include?(mounted_data['mount'])
          Chef::Log.debug(
            "fb_fstab: Skipping umount check for #{mounted_data['device']} " +
            "(#{mounted_data['mount']}): exempted mountpoint",
          )
          next
        elsif fstypes_to_skip.include?(mounted_data['fs_type'])
          Chef::Log.debug(
            "fb_fstab: Skipping umount check for #{mounted_data['device']} " +
            "(#{mounted_data['mount']}): exempted fstype",
          )
          next
        elsif dev_prefixes_to_skip.any? do |i|
          mounted_data['device']&.start_with?(i)
        end
          Chef::Log.debug(
            "fb_fstab: Skipping umount check for #{mounted_data['device']} " +
            "(#{mounted_data['mount']}) - exempted device prefix",
          )
          next
        elsif mount_prefixes_to_skip.any? do |i|
          mounted_data['mount']&.start_with?(i)
        end
          Chef::Log.debug(
            "fb_fstab: Skipping umount check for #{mounted_data['device']} " +
            "(#{mounted_data['mount']}) - exempted mount_point prefix",
          )
          next
        elsif mount_regexes_to_skip.any? do |i|
          mounted_data['mount'] =~ /#{i}/
        end
          Chef::Log.debug(
            "fb_fstab: Skipping umount check for #{mounted_data['device']} " +
            "(#{mounted_data['mount']}) - exempted mount_point regex",
          )
          next
        end

        # Is this device in our list of desired mounts?
        next if should_keep(mounted_data, desired_mounts, base_mounts)

        if node['fb_fstab']['enable_unmount']
          converge_by "unmount #{mounted_data['mount']}" do
            umount(mounted_data['mount'], mounted_data['lock_file'], node['fb_fstab']['umount_delete_empty_mountdir'])
          end
        else
          Chef::Log.warn(
            "fb_fstab: Would umount #{mounted_data['device']} from " +
            "#{mounted_data['mount']}, but " +
            'node["fb_fstab"]["enable_unmount"] is false',
          )
          Chef::Log.debug("fb_fstab: #{mounted_data}")
        end
      end
    end

    def _normalize_type(type)
      if node['fb_fstab']['type_normalization_map'][type]
        return node['fb_fstab']['type_normalization_map'][type]
      end

      type
    end

    # We consider a filesystem type the "same" if they are identical or if
    # one is auto.
    def compare_fstype(type1, type2)
      type1 == type2 || _normalize_type(type1) == _normalize_type(type2)
    end

    def delete_ignored_opts!(tlist, skipped_opts)
      ignorable_opts_s = node['fb_fstab']['ignorable_opts'].select do |x|
        x.is_a?(::String)
      end
      ignorable_opts_r = node['fb_fstab']['ignorable_opts'].select do |x|
        x.is_a?(::Regexp)
      end
      tlist.delete_if do |x|
        skipped_opts.include?(x) || ignorable_opts_s.include?(x) ||
          ignorable_opts_r.any? do |regex|
            x =~ regex
          end
      end
    end

    # This translates human-readable size mount options into their
    # canonical bytes form. I.e. "size=4G" into "size=4194304"
    #
    # Assumes it's really a size opt - validate before calling!
    def translate_size_opt(opt)
      val = opt.split('=').last
      mag = val[-1].downcase
      mags = ['k', 'm', 'g', 't']
      return opt unless mags.include?(mag)

      num = val[0..-2].to_i
      mags.each do |d|
        num *= 1024
        return "size=#{num}" if mag == d
      end
      fail RangeError "fb_fstab: Failed to translate #{opt}"
    end

    def canonicalize_opts(opts, skipped_opts = [])
      # ensure both are arrays
      optsl = opts.is_a?(Array) ? opts.dup : opts.split(',')
      # 'rw' is implied, so if no readability is specified, add it to both,
      # so missing on one if them doesn't cause a false-negative
      optsl << 'rw' unless optsl.include?('ro') || optsl.include?('rw')
      delete_ignored_opts!(optsl, skipped_opts)
      optsl.map! do |x|
        x.start_with?('size=') ? translate_size_opt(x) : x
      end

      # sort
      optsl.sort
    end

    # Take opts in a variety of forms, and compare them intelligently
    def compare_opts(opts1, opts2, skipped_opts = [])
      c1 = canonicalize_opts(opts1, skipped_opts)
      c2 = canonicalize_opts(opts2, skipped_opts)

      # Check that they're the same
      c1 == c2
    end

    # on Kernel >= 5.9, tmpfs are mounted with inode64 opt active
    def _are_tmpfs_using_inode64?
      return node.linux? && (FB::Version.new(node['kernel']['release']) > FB::Version.new('5.9'))
    end

    # Given a tmpfs desired mount `desired` check to see what it's status is;
    # mounted (:same), needs remount (:remount), not mounted (:missing) or
    # something else is mounted in the way (:conflict)
    #
    # This is roughly the same as mount_status() below but we make many
    # exceptions for tmpfs filesystems.
    #
    # Unlike mount_status() we will never return :moved since there's no unique
    # device to move.
    def tmpfs_mount_status(desired)
      # Start with checking if it was mounted the way we would mount it
      # this is ALMOST the same as the 'is it identical' check for non-tmpfs
      # filesystems except that with tmpfs we don't treat 'auto' as equivalent and
      # that we skip inode64 option on current mount status - because it's activated
      # by default on Linux kernel >= 5.9
      fs_data = node.filesystem_data
      key = "#{desired['device']},#{desired['mount_point']}"
      if fs_data['by_pair'][key]
        mounted = fs_data['by_pair'][key].to_hash
        if mounted['fs_type'] == 'tmpfs'
          Chef::Log.debug(
            "fb_fstab: tmpfs #{desired['device']} on " +
            "#{desired['mount_point']} is currently mounted...",
          )

          skipped_opts = []
          if _are_tmpfs_using_inode64?

            # inode64 is active by default on tmpfs for Linux kernel > 5.9
            skipped_opts.push('inode64')
          end

          if compare_opts(desired['opts'], mounted['mount_options'], skipped_opts)
            Chef::Log.debug('fb_fstab: ... with identical options.')
            return :same
          else
            Chef::Log.debug(
              "fb_fstab: ... with different options #{desired['opts']} vs " +
              mounted['mount_options'].join(','),
            )
            Chef::Log.info(
              "fb_fstab: #{desired['mount_point']} is mounted with options " +
              "#{canonicalize_opts(mounted['mount_options'])} instead of " +
              canonicalize_opts(desired['opts']).to_s,
            )
            return :remount
          end
        end
      end
      # OK, if that's not the case, we don't have the same device, which
      # is OK. Find out if we have something mounted at the same spot, and
      # get its device name so we can find it's entry in node['filesystem']
      if fs_data['by_mountpoint'][desired['mount_point']]
        # If we are here the mountpoints are the same...
        mounted = fs_data['by_mountpoint'][desired['mount_point']].to_hash
        # OK, if it's tmpfs as well, we're diong good
        if mounted['fs_type'] == 'tmpfs'
          Chef::Log.warn(
            "fb_fstab: Treating #{mounted['devices']} on " +
            "#{desired['mount_point']} the same as #{desired['device']} on " +
            "#{desired['mount_point']} because they are both tmpfs.",
          )
          Chef::Log.debug(
            "fb_fstab: tmpfs #{desired['device']} on " +
            "#{desired['mount_point']} is currently mounted...",
          )
          Chef::Log.debug("fb_fstab: #{desired} vs #{mounted}")
          if compare_opts(desired['opts'], mounted['mount_options'])
            Chef::Log.debug('fb_fstab: ... with identical options.')
            return :same
          else
            Chef::Log.debug(
              "fb_fstab: ... with different options #{desired['opts']} vs " +
              mounted['mount_options'].join(','),
            )
            Chef::Log.info(
              "fb_fstab: #{desired['mount_point']} is mounted with options " +
              "#{canonicalize_opts(mounted['mount_options'])} instead of " +
              canonicalize_opts(desired['opts']).to_s,
            )
            return :remount
          end
        end
        Chef::Log.warn(
          "fb_fstab: tmpfs is desired on #{desired['mount_point']}, but " +
          "non-tmpfs #{mounted['devices']} (#{mounted['fs_type']}) currently " +
          'mounted there.',
        )
        return :conflict
      end
      :missing
    end

    # Given a desired mount `desired` check to see what it's status is;
    # mounted (:same), needs remount (:remount), not mounted (:missing),
    # moved (:moved), or something else is mounted in the way (:conflict)
    def mount_status(desired)
      # We treat tmpfs specially. While we don't want people to mount tmpfs with
      # a device of 'none' or 'tmpfs', we also don't want to make them remount
      # (and lose all their data) just to convert to fb_fstab. So we'll make
      # them use a new name in the config, but we will treat the pre-mounted
      # mounts as valid/the same. Besides, since the device is meaningless, we
      # can just ignore it for the purposes of this test anyway.
      if desired['type'] == 'tmpfs'
        return tmpfs_mount_status(desired)
      end

      key = "#{desired['device']},#{desired['mount_point']}"
      fs_data = node.filesystem_data
      mounted = nil
      if fs_data['by_pair'][key]
        mounted = fs_data['by_pair'][key].to_hash
      else
        key = "#{desired['device']}/,#{desired['mount_point']}"
        if fs_data['by_pair'][key]
          mounted = fs_data['by_pair'][key].to_hash
        end
      end

      if mounted
        Chef::Log.debug(
          "fb_fstab: There is an entry in node['filesystem'] for #{key}",
        )
        # If it's a virtual device, we require the fs type to be identical.
        # otherwise, we require them to be similar. This is because 'auto'
        # is meaningless without a physical device, so we don't want to allow
        # it to be the same.
        if compare_fstype(desired['type'], mounted['fs_type']) ||
           (desired['device'].start_with?('/') &&
            [desired['type'], mounted['fs_type']].include?('auto'))
          Chef::Log.debug(
            "fb_fstab: FS #{desired['device']} on #{desired['mount_point']}" +
            ' is currently mounted...',
          )
          if compare_opts(desired['opts'], mounted['mount_options'])
            Chef::Log.debug('fb_fstab: ... with identical options.')
            return :same
          else
            Chef::Log.debug(
              "fb_fstab: ... with different options #{desired['opts']} vs " +
              mounted['mount_options'].join(','),
            )
            Chef::Log.info(
              "fb_fstab: #{desired['mount_point']} is mounted with options " +
              "#{canonicalize_opts(mounted['mount_options'])} instead of " +
              canonicalize_opts(desired['opts']).to_s,
            )
            return :remount
          end
        else
          Chef::Log.warn(
            "fb_fstab: Device #{desired['device']} is mounted at " +
            "#{mounted['mount']} as desired, but with fstype " +
            "#{mounted['fs_type']} instead of #{desired['type']}",
          )
          return :conflict
        end
      end

      # In this case we don't have the device we expect at the mountpoint we
      # expect. Assuming it's not NFS/Gluster which can be mounted in more than
      # once place, we look up this device and see if it moved or just isn't
      # mounted
      unless ['nfs', 'nfs4', 'glusterfs'].include?(desired['type'])
        device = fs_data['by_device'][desired['device']]
        # Here we are checking if the device we want
        # has already a mount defined
        # We want to return :moved if it does except
        # in the case when it's a btrfs
        # disk and our desired and current options
        # are trying to mount different subvolumes
        if device && device['mounts'] && !device['mounts'].empty? &&
          !(
            FB::Fstab.btrfs_subvol?(
              device['fs_type'],
              device['mount_options'].join(','),
            ) &&
            FB::Fstab.btrfs_subvol?(
              desired['type'],
              desired['opts'],
            ) &&
            !FB::Fstab.same_subvol?(
              device['mounts'][0],
              device['mount_options'].join(','),
              desired['opts'],
            )
          )

          Chef::Log.warn(
            "fb_fstab: #{desired['device']} is at #{device['mounts']}, but" +
            " we want it at #{desired['mount_point']}",
          )
          return :moved
        end
      end

      # Ok, this device isn't mounted, but before we return we need to check
      # if anything else is mounted where we want to be.
      if fs_data['by_mountpoint'][desired['mount_point']]
        devices = fs_data['by_mountpoint'][
            desired['mount_point']]['devices']
        Chef::Log.warn(
          "fb_fstab: Device #{desired['device']} desired at " +
          "#{desired['mount_point']} but something #{devices} already " +
          'mounted there.',
        )
        return :conflict
      end
      :missing
    end

    def check_wanted_filesystems
      # before we do anything... node['filesystem'] is a mapping of devices
      # to mountpoint, but that won't work for things with a device of "none",
      # so build a reverse mapping too
      in_maint_disks = FB::Fstab.get_in_maint_disks
      in_maint_mounts = FB::Fstab.get_in_maint_mounts

      # walk desired mounts, see if it's mounted, and mount/update
      # as appropriate.
      node['fb_fstab']['mounts'].to_hash.each_value do |desired_data|
        # Using "none" as a device is deprecated. You can use descriptive
        # strings now. Doing so is not only the new hotness, but it also
        # prevents dupes in node['filesystem'] - so we require it.
        if desired_data['device'] == 'none'
          Chef::Log.warn('fb_fstab: We do not permit "none" devices, please ' +
                          'use a descriptive device name')
          next
        end

        if desired_data['type'] == 'swap'
          Chef::Log.debug('fb_fstab: We do not change swap from fb_fstab, ' +
                          'moving on...')
          next
        end

        if desired_data['opts']
          opt_list = desired_data['opts'].split(',')
          if opt_list.include?('noauto')
            Chef::Log.debug(
              "fb_fstab: '#{desired_data['device']}' is configured with " +
              "'noauto' option, we will not mount.",
            )
            next
          end
        end

        begin
          desired_data['device'] = canonicalize_device(desired_data['device'])
        rescue RuntimeError
          next if desired_data['allow_mount_failure']
        end

        status = mount_status(desired_data)

        case status
        when :same
          Chef::Log.debug(
            "fb_fstab: Skipping #{desired_data['mount_point']}; looks good!",
          )
          next
        when :missing
          converge_by "mount #{desired_data['mount_point']}" do
            mount(desired_data, in_maint_disks, in_maint_mounts)
          end
          # Device mounted, move on...
          next
        when :conflict
          # We already threw a warning, just note we're moving on
          Chef::Log.info(
            "fb_fstab: Skipping #{desired_data['mount_point']} due to conflict",
          )
          next
        when :moved
          Chef::Log.info(
            "fb_fstab: Skipping #{desired_data['mount_point']} since it " +
            'moved. Moving filesystems is scary.',
          )
          next
        when :remount
          base_msg = "fb_fstab: Mountpoint #{desired_data['mount_point']} " +
                     'options changed'
          if node['fb_fstab']['enable_remount'] &&
             desired_data['enable_remount']
            Chef::Log.debug("fb_fstab: #{base_msg} - remounting")
            converge_by "remount #{desired_data['mount_point']}" do
              remount(desired_data)
            end
            # There's nothing after us in the loop at this point, but I'm being
            # explicit with the 'next' here so that we never accidentally
            # remount an FS twice
            next
          else
            Chef::Log.warn("#{base_msg}, but remounts are not enabled")
          end
        end
      end
    end

    def _run_command_flocked(shellout, lock_file, mount_point)
      if lock_file.nil?
        return shellout.run_command
      else
        lock_fd = File.open(lock_file, 'a+') # rubocop:disable Chef/Meta/NoFileWrites
        unless lock_fd.flock(File::LOCK_EX | File::LOCK_NB)
          fail IOError, "Couldn't grab lock at #{lock_file} for #{mount_point}"
        end

        begin
          return shellout.run_command
        ensure
          lock_fd.flock(File::LOCK_UN)
        end
      end
    end
  end
end
