diff --git a/omnibus/files/private-chef-cookbooks/private-chef/libraries/du.rb b/omnibus/files/private-chef-cookbooks/private-chef/libraries/du.rb new file mode 100644 index 0000000000..bb30c06625 --- /dev/null +++ b/omnibus/files/private-chef-cookbooks/private-chef/libraries/du.rb @@ -0,0 +1,25 @@ +require 'mixlib/shellout' +module Du + # Calculate the disk space used by the given path. Requires that + # `du` is in our PATH. + # + # @param path [String] Path to a directory on disk + # @return [Integer] KB used by directory on disk + # + def self.du(path) + # TODO(ssd) 2017-08-18: Do we need to worry about sparse files + # here? If so, can we expect the --apparent-size flag to exist on + # all of our platforms. + command = Mixlib::ShellOut.new("du -sk #{path}") + command.run_command + if command.status.success? + command.stdout.split("\t").first.to_i + else + Chef::Log.error("du -sk #{path} failed with exit status: #{command.exitstatus}") + Chef::Log.error("du stderr: #{command.stderr}") + raise "du failed" + end + rescue Errno::ENOENT + raise "The du utility is not available. Unable to check disk usage" + end +end diff --git a/omnibus/files/private-chef-cookbooks/private-chef/libraries/statfs.rb b/omnibus/files/private-chef-cookbooks/private-chef/libraries/statfs.rb new file mode 100644 index 0000000000..22da79a129 --- /dev/null +++ b/omnibus/files/private-chef-cookbooks/private-chef/libraries/statfs.rb @@ -0,0 +1,78 @@ +require 'ffi' + +class Statfs + # + # Statfs provides a simple interface to the statvfs system call. + # Since the statvfs struct varies a bit across platforms, this + # likely only works on Linux and OSX at the moment. + # + extend FFI::Library + ffi_lib FFI::Library::LIBC + + attach_function(:statvfs, [:string, :pointer], :int) + attach_function(:strerror, [:int], :string) + + FSBLKCNT_T = if RbConfig::CONFIG['host_os'] =~ /darwin|osx|mach/i + :uint + else + :ulong + end + + # See http://man7.org/linux/man-pages/man2/statvfs.2.html + class Statvfs < FFI::Struct + spec = [ + :f_bsize, :ulong, # Filesystem block size + :f_frsize, :ulong, # Fragement size + :f_blocks, FSBLKCNT_T, # Size of fs in f_frsize units + :f_bfree, FSBLKCNT_T, # Number of free blocks + :f_bavail, FSBLKCNT_T, # Number of free blocks for unpriviledged users + :f_files, FSBLKCNT_T, # Number of inodes + :f_ffree, FSBLKCNT_T, # Number of free inodes + :f_favail, FSBLKCNT_T, # Number of free inodes for unprivilged users + :f_fsid, :ulong, # Filesystem ID + :f_flag, :ulong, # Mount Flags + :f_namemax, :ulong # Max filename length + ] + + # Linux has this at the end of the struct and if we don't include + # it we end up getting a memory corruption error when th object + # gets GCd. + if RbConfig::CONFIG['host_os'] =~ /linux/i + spec << :f_spare + spec << [:int, 6] + end + + layout(*spec) + end + + def initialize(path) + @statvfs = stat(path) + end + + # + # @returns [Integer] Free inodes on the given filesystem + # + def free_inodes + @statvfs[:f_favail] + end + + # + # @returns [Integer] Free space in KB on the given filesystem + # + def free_space + # Since we are running as root we could report f_bfree but will + # stick with f_bavail since it will typically be more + # conservative. + (@statvfs[:f_frsize] * @statvfs[:f_bavail])/1024 + end + + private + + def stat(path) + statvfs = Statvfs.new + if statvfs(path, statvfs.to_ptr) != 0 + raise 'statvfs: ' + strerror(FFI.errno) + end + statvfs + end +end diff --git a/omnibus/files/private-chef-cookbooks/private-chef/providers/pg_upgrade.rb b/omnibus/files/private-chef-cookbooks/private-chef/providers/pg_upgrade.rb index 6b364a3af6..c1647cf05e 100644 --- a/omnibus/files/private-chef-cookbooks/private-chef/providers/pg_upgrade.rb +++ b/omnibus/files/private-chef-cookbooks/private-chef/providers/pg_upgrade.rb @@ -54,6 +54,7 @@ def whyrun_supported? action :upgrade do if upgrade_required? converge_by("Upgrading database cluster") do + check_required_disk_space unless ENV['CS_SKIP_PG_DISK_CHECK'] == "1" shutdown_postgres initialize_new_cluster update_to_latest_version @@ -123,6 +124,37 @@ def upgrade_required? end end +# +# Since we don't use the --link flag, we need to ensure the disk has +# enough space for another copy of the postgresql data. +# +def check_required_disk_space + old_data_dir_size = Du.du(old_data_dir) + # new_data_dir might not exist at the point of making this check. + # In that case check the first existing directory above it. + new_dir = dir_or_existing_parent(new_data_dir) + free_disk_space = Statfs.new(new_dir).free_space + + if old_data_dir_size < (free_disk_space * 0.90) + Chef::Log.debug("Old data dir size: #{old_data_dir_size}") + Chef::Log.debug(" Free disk space: #{free_disk_space}") + Chef::Log.debug("Free space is sufficient to start upgrade") + true + else + Chef::Log.fatal("Insufficient free space on disk to complete upgrade.") + Chef::Log.fatal("The current postgresql data directory contains #{old_data_dir_size} KB of data but only #{free_disk_space} KB is available on disk.") + Chef::Log.fatal("The upgrade process requires at least #{old_data_dir_size/0.90} KB.") + raise "Insufficient Disk Space to Upgrade" + end +end + +def dir_or_existing_parent(dir) + return dir if ::File.exist?(dir) + return dir if ::File.expand_path(dir) == "/" + + dir_or_existing_parent(::File.expand_path("#{dir}/..")) +end + # If a pre-existing postgres service exists it will need to be shut # down prior to running the upgrade step. def shutdown_postgres