cookbooks/fb_fstab/libraries/default.rb (255 lines of code) (raw):

# 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 # Fstab utility functions unless defined?(FB::Fstab) class Fstab BASE_FILENAME = '/etc/.fstab.chef'.freeze IN_MAINT_DISKS_FILENAME = '/var/chef/in_maintenance_disks'.freeze IN_MAINT_MOUNTS_FILENAME = '/var/chef/in_maintenance_mounts'.freeze BTRFS_ROOTPARENT = '5'.freeze def self.determine_base_fstab_entries(full_fstab) core_fs_line_matching = [ '^LABEL=(\/|\/boot|SWAP.*|\/mnt\/d\d+)\s', '^\S+\s/\s', '^UUID=', '^devpts', '^sysfs', '^proc', '^tmpfs\s+\/dev\/shm.*', '^/dev/sda', '^/dev/fioa', '^/dev/mapper', ] base = '' full_fstab.split("\n").each do |line| iscore = core_fs_line_matching.any? do |thing| line =~ /#{thing}/ end # These messages are technically debug lines, but since it only # happens once and we'll want to know this for debugging, it's going # in as info. unless iscore Chef::Log.info("FB::Fstab.generate_base_fstab: Skipping #{line}") next end Chef::Log.info("FB::Fstab.generate_base_fstab: Keeping #{line}") base << "#{line}\n" end base end def self.generate_base_fstab unless File.exist?(BASE_FILENAME) && File.size?(BASE_FILENAME) FileUtils.cp('/etc/fstab', '/root/fstab.before_fb_fstab') FileUtils.chmod(0400, '/root/fstab.before_fb_fstab') full_fstab = File.read('/etc/fstab') base_fstab = determine_base_fstab_entries(full_fstab) File.write(BASE_FILENAME, base_fstab) end end # Returns the content of the file def self.base_fstab_contents(node) unless node['fb_fstab']['_basefilecontents'] node.default['fb_fstab']['_basefilecontents'] = File.read(BASE_FILENAME) end node['fb_fstab']['_basefilecontents'] end # Returns an array of strings def self.parse_in_maint_file(path) return [] unless File.exist?(path) age = ( Time.now - File.stat(path).mtime ).to_i if age > 60 * 60 * 24 * 7 Chef::Log.warn( "fb_fstab: Removing stale #{path} - it is more than 1 week old.", ) File.unlink(path) return [] end entries = [] File.read(path).each_line do |line| next if line.start_with?('#') next if line.strip.empty? entries << line.strip end entries end # Returns an array of disks def self.get_in_maint_disks disks = self.parse_in_maint_file(IN_MAINT_DISKS_FILENAME) unless disks.empty? Chef::Log.warn( "fb_fstab: Will skip in-maintenance disks: #{disks.join(' ')}", ) end disks end # Returns an array of mounts def self.get_in_maint_mounts mounts = self.parse_in_maint_file(IN_MAINT_MOUNTS_FILENAME) # Canonicalize mount paths (e.g. removing trailing slashes) mounts.map! { |m| Pathname.new(m).cleanpath.to_s } unless mounts.empty? Chef::Log.warn( "fb_fstab: Will skip in-maintenance mounts: #{mounts.join(' ')}", ) end mounts end def self.get_autofs_points(node) autofs_points = [] node.filesystem_data['by_pair'].to_hash.each_value do |data| autofs_points << data['mount'] if data['fs_type'] == 'autofs' end autofs_points end def self.autofs_parent(dir, node) get_autofs_points(node).each do |pt| Chef::Log.debug( "fb_fstab: Checking if #{dir} is within autofs tree at #{pt}", ) if dir.start_with?(pt) Chef::Log.debug('fb_fstab: it is!') return pt end end false end def self.label_to_device(label, node) d = node.filesystem_data['by_device'].select do |x, y| y['label'] && y['label'] == label && !x.start_with?('/dev/block') end fail "Requested disk label #{label} doesn't exist" if d.empty? Chef::Log.debug("fb_fstab: label #{label} is device #{d.keys[0]}") d.keys[0] end def self.uuid_to_device(uuid, node) d = node.filesystem_data['by_device'].to_hash.select do |x, y| y['uuid'] && y['uuid'] == uuid && !x.start_with?('/dev/block') end fail "Requested disk UUID #{uuid} doesn't exist" if d.empty? Chef::Log.debug("fb_fstab: uuid #{uuid} is device #{d.keys[0]}") d.keys[0] end def self.canonicalize_device(device, node) Chef::Log.debug("fb_fstab: Canonicalizing #{device}") if device.start_with?('LABEL=') label = device.sub('LABEL=', '') device = label_to_device(label, node) elsif device.start_with?('UUID=') uuid = device.sub('UUID=', '') device = uuid_to_device(uuid, node) end device end # This will always return the id of the subvolume specified in the option def self._canonicalize_subvol_opt(mount, opts) type = '' value = '' opts.split(',').each do |option| if option.include?('subvol=') || option.include?('subvolid=') data = option.split('=') type = data[0] value = data[1] break end end if type == 'subvolid' && !value.empty? return value elsif type != 'subvol' fail "fb_fstab: Cannot canonicalize subvolume from options: #{opts}" end cmd = "/usr/sbin/btrfs subvol list #{mount}" subvolume_data = Mixlib::ShellOut.new(cmd).run_command subvolume_data.error! subvolume_data.stdout.each_line do |line| # eg. ID 260 gen 49 top level 5 path cache fields = line.split if fields[8] == value return fields[1] end end fail "fb_fstab: Cannot canonicalize subvolume: #{opts}" end def self.same_subvol?(mount, opts1, opts2) a = self._canonicalize_subvol_opt(mount, opts1) b = self._canonicalize_subvol_opt(mount, opts2) a == b end def self.btrfs_subvol?(fs_type, mount_options) fs_type == 'btrfs' && ( mount_options.include?('subvol=') || mount_options.include?('subvolid=') ) end def self.get_base_mount_opts(node, mountpoint) FB::Fstab.base_fstab_contents(node).each_line do |line| next if line.strip.empty? line_parts = line.strip.split if line_parts[1] == mountpoint return line_parts[3] end end fail "Could not retrieve mount opts for '#{mountpoint}'" end def self.get_unmasked_base_mounts(format, node, hash_by = 'device') res = case format when :hash {} when :lines [] end hash_by_values = Set['device', 'mount_point'] unless hash_by_values.include?(hash_by) fail "fb_fstab: Invalid hash_by value, allowed are: #{hash_by_values}" end desired_mounts = node['fb_fstab']['mounts'].to_hash FB::Fstab.base_fstab_contents(node).each_line do |line| next if line.strip.empty? # do not add swap if swap is managed elsewhere, e.g. fb_swap next if line.include?('swap') && node['fb_fstab']['exclude_base_swap'] line_parts = line.strip.split line_dev_spec = line_parts[0] # if someone specifies the same device in a mount that is in the # mounts we got from provisioning, then they are trying to override # that. We only look at the `device` part here because we shouldn't # have things like NFS or other such things specified in provisioning # anyway. next if desired_mounts.any? do |_name, data| line_dev_spec == data['device'] end # If that failed, we canonicalize (if possible) and try again against # canonicalized versions of what's in the user's config begin fs_spec = canonicalize_device(line_dev_spec, node) rescue StandardError => e # Special handing for UUIDs. I hate UUIDs. Really, did I mention I # hate UUIDs? Who thought those were a good idea. Anyway in the # event we got UUIDs from provisioning and the user wants to use # labels, let that happen. raise e unless line_dev_spec.start_with?('UUID=') next if desired_mounts.any? do |_name, data| data['mount_point'] == line_parts[1] && data['device'].start_with?('LABEL=') end raise e end # If someone has a more specific mount, don't use the original next if desired_mounts.any? do |_name, data| begin cdev = canonicalize_device(data['device'], node) rescue RuntimeError => e # If the entry in node['fstab']['mounts] failed to resolve, # that's an error, orthogonal to what we're doing here, # unless they set `allow_mount_failure`. So if it failed, # raise an error, otherwise don't. # # HOWEVER, this `next` is not `next true`, because we're # not skipping the mount - there was no valid comparison # done. We're just moving to the next iteration of any?` next if data['allow_mount_failure'] raise e end # We want to skip btrfs subvolumes as # it's valid to specifiy the same device multiple times [data['device'], cdev].include?(fs_spec) && !self.btrfs_subvol?(data['type'], data['opts']) end case format when :hash mount_point = line_parts[1] case hash_by when 'device' res[fs_spec] = { 'mount_point' => mount_point, 'type' => line_parts[2], 'opts' => line_parts[3], 'dump' => line_parts[4], 'pass' => line_parts[5], } when 'mount_point' res[mount_point] = { 'device' => fs_spec, 'type' => line_parts[2], 'opts' => line_parts[3], 'dump' => line_parts[4], 'pass' => line_parts[5], } end when :lines res << line.strip end end Chef::Log.debug("fb_fstab: base mounts: #{res}") res end end end # https://github.com/jamesmartin/chefspec/pull/1/files # https://github.com/sethvargo/chefspec/issues/562#issuecomment-74120922 # http://jtimberman.housepub.org/blog/2015/05/30/quick-tip-stubbing-library-helpers-in-chefspec/ end