# # Author:: Phil Dibowitz # Author:: Adam Jacob # Author:: Kurt Yoder (ktyopscode@yoderhome.com) # Author:: Deepali Jagtap () # Author:: Prabhu Das () # Author:: Isa Farnik () # Author:: James Gartrell () # Copyright:: Copyright (c) 2008-2020 Chef Software, Inc. # Copyright:: Copyright (c) 2015 Facebook, 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 "set" Ohai.plugin(:Filesystem) do provides "filesystem" def find_device(name) %w{/dev /dev/mapper}.each do |dir| path = File.join(dir, name) return path if File.exist?(path) end name end def parse_line(line, cmdtype) case cmdtype when "lsblk" regex = /NAME="(\S+).*?" UUID="(\S*)" LABEL="(\S*)" FSTYPE="(\S*)"/ if line =~ regex dev = $1 dev = find_device(dev) unless dev.start_with?("/") uuid = $2 label = $3 fs_type = $4 return { dev: dev, uuid: uuid, label: label, fs_type: fs_type } end when "blkid" bits = line.split dev = bits.shift.split(":")[0] f = { dev: dev } bits.each do |keyval| if keyval =~ /(\S+)="(\S+)"/ key = $1.downcase.to_sym key = :fs_type if key == :type f[key] = $2 end end return f end nil end def generate_device_view(fs) view = {} fs.each_value do |entry| view[entry[:device]] ||= Mash.new entry.each do |key, val| next if %w{device mount}.include?(key) view[entry[:device]][key] = val end view[entry[:device]][:mounts] ||= [] if entry[:mount] view[entry[:device]][:mounts] << entry[:mount] end end view end def generate_mountpoint_view(fs) view = {} fs.each_value do |entry| next unless entry[:mount] view[entry[:mount]] ||= Mash.new entry.each do |key, val| next if %w{mount device}.include?(key) view[entry[:mount]][key] = val end view[entry[:mount]][:devices] ||= [] if entry[:device] view[entry[:mount]][:devices] << entry[:device] end end view end def generate_deprecated_windows_view(fs) view = generate_mountpoint_view(fs) view.each do |mp, entry| view[mp].delete("devices") end view end def parse_common_df(out) fs = {} out.each_line do |line| case line when /^Filesystem\s+1024-blocks/ next when /^(.+?)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+\%)\s+(.+)$/ key = "#{$1},#{$6}" fs[key] = Mash.new fs[key][:device] = $1 fs[key][:kb_size] = $2 fs[key][:kb_used] = $3 fs[key][:kb_available] = $4 fs[key][:percent_used] = $5 fs[key][:mount] = $6 end end fs end def run_with_check(bin, &block) yield rescue Ohai::Exceptions::Exec => e unless Ohai.config[:plugin][:filesystem][:allow_partial_data] raise e end logger.warn("Plugin Filesystem: #{bin} binary is not available. Some data will not be available.") end ### Windows specific methods BEGINS # Drive types DRIVE_TYPE ||= %w{unknown no_root_dir removable local network cd ram}.freeze # Volume encryption or decryption status # # @see https://docs.microsoft.com/en-us/windows/desktop/SecProv/getconversionstatus-win32-encryptablevolume#parameters # CONVERSION_STATUS ||= %w{ FullyDecrypted FullyEncrypted EncryptionInProgress DecryptionInProgress EncryptionPaused DecryptionPaused }.freeze # Returns a Mash loaded with logical details # # Uses Win32_LogicalDisk and logical_properties to return general details of volumes. # # Returns an empty Mash in case of any WMI exception. # # @see https://docs.microsoft.com/en-us/windows/desktop/CIMWin32Prov/win32-logicaldisk # # @return [Mash] # def logical_info wmi = WmiLite::Wmi.new("Root\\CIMV2") # TODO: We should really be parsing Win32_Volume and Win32_MountPoint. disks = wmi.instances_of("Win32_LogicalDisk") logical_properties(disks) rescue WmiLite::WmiException Ohai::Log.debug("Unable to access Win32_LogicalDisk. Skipping logical details") Mash.new end # Returns a Mash loaded with encryption details # # Uses Win32_EncryptableVolume and encryption_properties to return encryption details of volumes. # # Returns an empty Mash in case of any WMI exception. # # @note We are fetching Encryption Status only as of now # # @see https://msdn.microsoft.com/en-us/library/windows/desktop/aa376483(v=vs.85).aspx # # @return [Mash] # def encryptable_info wmi = WmiLite::Wmi.new("Root\\CIMV2\\Security\\MicrosoftVolumeEncryption") disks = wmi.instances_of("Win32_EncryptableVolume") encryption_properties(disks) rescue WmiLite::WmiException Ohai::Log.debug("Unable to access Win32_EncryptableVolume. Skipping encryptable details") Mash.new end # Refines and calculates logical properties out of given instances. # # Note that :device here is the same as Volume name and there for compatibility with other OSes. # # @param [WmiLite::Wmi::Instance] disks # # @return [Mash] Each drive containing following properties: # # * :kb_size (Integer) # * :kb_available (Integer) # * :kb_used (Integer) # * :percent_used (Integer) # * :mount (String) # * :fs_type (String) # * :volume_name (String) # * :device (String) # def logical_properties(disks) properties = Mash.new disks.each do |disk| property = Mash.new # In windows the closest thing we have to a device is the volume name # and the "mountpoint" is the drive letter... device = disk["volumename"].to_s.downcase mount = disk["deviceid"] property[:kb_size] = disk["size"] ? disk["size"].to_i / 1000 : 0 property[:kb_available] = disk["freespace"].to_i / 1000 property[:kb_used] = property[:kb_size] - property[:kb_available] property[:percent_used] = (property[:kb_size] == 0 ? 0 : (property[:kb_used] * 100 / property[:kb_size])) property[:mount] = mount property[:fs_type] = disk["filesystem"].to_s.downcase property[:drive_type] = disk["drivetype"].to_i property[:drive_type_string] = DRIVE_TYPE[disk["drivetype"].to_i] property[:drive_type_human] = disk["description"].to_s property[:volume_name] = disk["volumename"].to_s property[:device] = device key = "#{device},#{mount}" properties[key] = property end properties end # Refines and calculates encryption properties out of given instances # # @param [WmiLite::Wmi::Instance] disks # # @return [Mash] Each drive containing following properties: # # * :encryption_status (String) # def encryption_properties(disks) properties = Mash.new disks.each do |disk| property = Mash.new property[:encryption_status] = disk["conversionstatus"] ? CONVERSION_STATUS[disk["conversionstatus"]] : "" key = disk["driveletter"] properties[key] = property end properties end # Merges all the various properties of filesystems # # @param [Array] disks_info # Array of the Mashes containing disk properties # # @return [Mash] # def merge_info(logical_info, encryption_info) fs = Mash.new encryption_keys_used = Set.new logical_info.each do |key, info| if encryption_info[info["mount"]] encryption_keys_used.add(info["mount"]) fs[key] = info.merge(encryption_info[info["mount"]]) else fs[key] = info.dup end end left_enc = encryption_info.reject { |x| encryption_keys_used.include?(x) } left_enc.each do |key, info| fs[",#{key}"] = info end fs end collect_data(:linux) do fs = Mash.new # Grab filesystem data from df run_with_check("df") do so = shell_out("df -P") fs.merge!(parse_common_df(so.stdout)) # Grab filesystem inode data from df so = shell_out("df -iP") so.stdout.each_line do |line| case line when /^Filesystem\s+Inodes/ next when /^(.+?)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+\%)\s+(.+)$/ key = "#{$1},#{$6}" fs[key] ||= Mash.new fs[key][:device] = $1 fs[key][:total_inodes] = $2 fs[key][:inodes_used] = $3 fs[key][:inodes_available] = $4 fs[key][:inodes_percent_used] = $5 fs[key][:mount] = $6 end end end # Grab mount information from /bin/mount run_with_check("mount") do so = shell_out("mount") so.stdout.each_line do |line| if line =~ /^(.+?) on (.+?) type (.+?) \((.+?)\)$/ key = "#{$1},#{$2}" fs[key] ||= Mash.new fs[key][:device] = $1 fs[key][:mount] = $2 fs[key][:fs_type] = $3 fs[key][:mount_options] = $4.split(",") end end end # We used to try to decide if we wanted to run lsblk or blkid # but they each have a variety of cases were they fail to report # data. For example, there are a variety of cases where lsblk won't # report unmounted filesystems, but blkid will. And vise-versa. Sweet. # So for reliability, we'll run both, if we have them. lsblk = which("lsblk") blkid = which("blkid") cmds = [] # These should be in order of preference... first writer wins. if lsblk cmds << "#{lsblk} -n -P -o NAME,UUID,LABEL,FSTYPE" end if blkid cmds << blkid end cmds.each do |cmd| cmdtype = File.basename(cmd.split.first) # setting the timeout here for `lsblk` and `blkid` commands to 60 # this is to allow machines with large amounts of attached LUNs # to respond back to the command successfully run_with_check(cmdtype) do so = shell_out(cmd, timeout: 60) so.stdout.each_line do |line| parsed = parse_line(line, cmdtype) next if parsed.nil? # lsblk lists each device once, so we need to update all entries # in the hash that are related to this device keys_to_update = [] fs.each_key do |key| keys_to_update << key if key.start_with?("#{parsed[:dev]},") end if keys_to_update.empty? key = "#{parsed[:dev]}," fs[key] = Mash.new fs[key][:device] = parsed[:dev] keys_to_update << key end keys_to_update.each do |k| %i{fs_type uuid label}.each do |subkey| if parsed[subkey] && !parsed[subkey].empty? fs[k][subkey] = parsed[subkey] end end end end end end # Grab any missing mount information from /proc/mounts if File.exist?("/proc/mounts") mounts = "" # Due to https://tickets.opscode.com/browse/OHAI-196 # we have to non-block read dev files. Ew. f = File.open("/proc/mounts") loop do data = f.read_nonblock(4096) mounts << data # We should just catch EOFError, but the kernel had a period of # bugginess with reading virtual files, so we're being extra # cautious here, catching all exceptions, and then we'll read # whatever data we might have rescue Exception break end f.close mounts.each_line do |line| if line =~ /^(\S+) (\S+) (\S+) (\S+) \S+ \S+$/ key = "#{$1},#{$2}" next if fs.key?(key) fs[key] = Mash.new fs[key][:device] = $1 fs[key][:mount] = $2 fs[key][:fs_type] = $3 fs[key][:mount_options] = $4.split(",") end end end by_pair = fs by_device = generate_device_view(fs) by_mountpoint = generate_mountpoint_view(fs) fs_data = Mash.new fs_data["by_device"] = by_device fs_data["by_mountpoint"] = by_mountpoint fs_data["by_pair"] = by_pair # Set the filesystem data filesystem fs_data end collect_data(:freebsd, :openbsd, :netbsd, :dragonflybsd) do fs = Mash.new # Grab filesystem data from df run_with_check("df") do so = shell_out("df") fs.merge!(parse_common_df(so.stdout)) so = shell_out("df -iP") so.stdout.lines do |line| case line when /^Filesystem/ # skip the header next when /^(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\%\s+(\d+)\s+(\d+)\s+(\d+)%\s+(\S+)$/ key = "#{$1},#{$9}" fs[key] ||= Mash.new fs[key][:device] = $1 fs[key][:inodes_used] = $6 fs[key][:inodes_available] = $7 fs[key][:total_inodes] = ($6.to_i + $7.to_i).to_s fs[key][:inodes_percent_used] = $8 fs[key][:mount] = $9 end end end # Grab mount information from mount run_with_check("mount") do so = shell_out("mount -l") so.stdout.lines do |line| if line =~ /^(.+?) on (.+?) \((.+?), (.+?)\)$/ key = "#{$1},#{$2}" fs[key] ||= Mash.new fs[key][:device] = $1 fs[key][:mount] = $2 fs[key][:fs_type] = $3 fs[key][:mount_options] = $4.split(/,\s*/) end end end # create views by_pair = fs by_device = generate_device_view(fs) by_mountpoint = generate_mountpoint_view(fs) fs_data = Mash.new fs_data["by_device"] = by_device fs_data["by_mountpoint"] = by_mountpoint fs_data["by_pair"] = by_pair # @todo in Chef 17 the filesystem2 part of this goes away filesystem fs_data filesystem2 fs_data end collect_data(:darwin) do fs = Mash.new block_size = 0 # on new versions of OSX, -i is default, on old versions it's not, so # specifying it gets consistent output run_with_check("df") do so = shell_out("df -i") so.stdout.each_line do |line| case line when /^Filesystem\s+(\d+)-/ block_size = $1.to_i next when /^(.+?)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+\%)\s+(\d+)\s+(\d+)\s+(\d+%)\s+(.+)$/ key = "#{$1},#{$9}" fs[key] = Mash.new fs[key][:block_size] = block_size fs[key][:device] = $1 fs[key][:kb_size] = ($2.to_i / (1024 / block_size)).to_s fs[key][:kb_used] = ($3.to_i / (1024 / block_size)).to_s fs[key][:kb_available] = ($4.to_i / (1024 / block_size)).to_s fs[key][:percent_used] = $5 fs[key][:inodes_used] = $6 fs[key][:inodes_available] = $7 fs[key][:total_inodes] = ($6.to_i + $7.to_i).to_s fs[key][:inodes_percent_used] = $8 fs[key][:mount] = $9 end end end run_with_check("mount") do so = shell_out("mount") so.stdout.lines do |line| if line =~ /^(.+?) on (.+?) \((.+?), (.+?)\)$/ key = "#{$1},#{$2}" fs[key] ||= Mash.new fs[key][:mount] = $2 fs[key][:fs_type] = $3 fs[key][:mount_options] = $4.split(/,\s*/) end end end by_pair = fs by_device = generate_device_view(fs) by_mountpoint = generate_mountpoint_view(fs) fs_data = Mash.new fs_data["by_device"] = by_device fs_data["by_mountpoint"] = by_mountpoint fs_data["by_pair"] = by_pair filesystem fs_data end collect_data(:solaris2) do fs = Mash.new # Grab filesystem data from df run_with_check("df") do so = shell_out("df -Pka") fs.merge!(parse_common_df(so.stdout)) # Grab file system type from df (must be done separately) so = shell_out("df -na") so.stdout.lines do |line| next unless line =~ /^(.+?)\s*: (\S+)\s*$/ mount = $1 fs.each do |key, fs_attributes| next unless fs_attributes[:mount] == mount fs[key][:fs_type] = $2 end end end # Grab mount information from /bin/mount run_with_check("mount") do so = shell_out("mount") so.stdout.lines do |line| next unless line =~ /^(.+?) on (.+?) (.+?) on (.+?)$/ key = "#{$2},#{$1}" fs[key] ||= Mash.new fs[key][:mount] = $1 fs[key][:mount_time] = $4 # $4 must come before "split", else it becomes nil fs[key][:mount_options] = $3.split("/") end end # Grab any zfs data from "zfs get" zfs = Mash.new zfs_get = "zfs get -p -H all" run_with_check("zfs") do so = shell_out(zfs_get) so.stdout.lines do |line| next unless line =~ /^([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)$/ filesystem = $1 property = $2 value = $3 source = $4.chomp zfs[filesystem] ||= Mash.new # if this fs doesn't exist, put in the bare minimum zfs[filesystem][property] = { value: value, source: source, } end end zfs.each do |fsname, attributes| mountpoint = attributes[:mountpoint][:value] if attributes[:mountpoint] key = "#{fsname},#{mountpoint}" fs[key] ||= Mash.new fs[key][:fs_type] = "zfs" fs[key][:mount] = mountpoint if mountpoint fs[key][:device] = fsname fs[key][:zfs_properties] = attributes # find all zfs parents parents = fsname.split("/") zfs_parents = [] (0..parents.length - 1).to_a.each do |parent_index| next_parent = parents[0..parent_index].join("/") zfs_parents.push(next_parent) end zfs_parents.pop fs[key][:zfs_parents] = zfs_parents fs[key][:zfs_zpool] = (zfs_parents.length == 0) end # create views by_pair = fs by_device = generate_device_view(fs) by_mountpoint = generate_mountpoint_view(fs) fs_data = Mash.new fs_data["by_device"] = by_device fs_data["by_mountpoint"] = by_mountpoint fs_data["by_pair"] = by_pair # @todo in Chef 17 the filesystem2 plugin goes away filesystem fs_data filesystem2 fs_data end collect_data(:aix) do def parse_df_or_mount(shell_out) oldie = Mash.new shell_out.lines.each do |line| fields = line.split case line # headers and horizontal rules to skip when /^\s*(node|---|^Filesystem\s+1024-blocks)/ next # strictly a df entry when /^(.+?)\s+([0-9-]+)\s+([0-9-]+)\s+([0-9-]+)\s+([0-9-]+\%*)\s+(.+)$/ if $1 == "Global" dev = "#{$1}:#{$6}" else dev = $1 end mountpoint = $6 key = "#{dev},#{mountpoint}" oldie[key] ||= Mash.new oldie[key][:kb_size] = $2 oldie[key][:kb_used] = $3 oldie[key][:kb_available] = $4 oldie[key][:percent_used] = $5 oldie[key][:mount] = mountpoint oldie[key][:device] = dev # an entry starting with 'G' or / (E.G. /tmp or /var) when %r{^\s*(G.*?|/\w)} if fields[0] == "Global" dev = fields[0] + ":" + fields[1] else dev = fields[0] end mountpoint = fields[1] key = "#{dev},#{mountpoint}" oldie[key] ||= Mash.new oldie[key][:mount] = mountpoint oldie[key][:fs_type] = fields[2] oldie[key][:mount_options] = fields[6].split(",") oldie[key][:device] = dev # entries occupying the 'Node' column parsed here else dev = fields[0] + ":" + fields[1] mountpoint = fields[2] key = "#{dev},#{mountpoint}" oldie[key] ||= Mash.new oldie[key][:mount] = mountpoint oldie[key][:device] = dev oldie[key][:fs_type] = fields[3] oldie[key][:mount_options] = fields[7].split(",") end end oldie end def collect_old_version(shell_outs) mount_hash = parse_df_or_mount shell_outs[:mount] df_hash = parse_df_or_mount shell_outs[:df_Pk] mount_hash.each do |key, hash| df_hash[key].merge!(hash) if df_hash.key?(key) end mount_hash.merge(df_hash) end # Cache the command output shell_outs = Mash.new run_with_check("mount") do shell_outs[:mount] = shell_out("mount").stdout end run_with_check("df") do shell_outs[:df_Pk] = shell_out("df -Pk").stdout end fs = collect_old_version(shell_outs) by_pair = fs by_device = generate_device_view(fs) by_mountpoint = generate_mountpoint_view(fs) fs_data = Mash.new fs_data["by_device"] = by_device fs_data["by_mountpoint"] = by_mountpoint fs_data["by_pair"] = by_pair # @todo in Chef 17 the filesystem2 plugin goes away here filesystem fs_data filesystem2 fs_data end collect_data(:windows) do require "wmi-lite/wmi" require_relative "../mash" fs = merge_info(logical_info, encryptable_info) by_pair = fs by_device = generate_device_view(fs) by_mountpoint = generate_mountpoint_view(fs) fs_data = Mash.new fs_data["by_device"] = by_device fs_data["by_mountpoint"] = by_mountpoint fs_data["by_pair"] = by_pair # Set the filesystem data - Windows didn't do the conversion when everyone # else did, so 15 will have both be the new API and 16 will drop the old API filesystem generate_deprecated_windows_view(fs) filesystem2 fs_data end end