cookbooks/fb_swap/libraries/default.rb (202 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
module FbSwap
def self._validate(node)
device = self._device(node)
file = self._file(node)
# Let's look at /proc/swaps for actual size. We can't examine swap
# partitions formatted size unless they're "mounted" here. This also
# produces a list of swap files mounted.
swaps_enabled = self._get_swaps_enabled
if device
max_device_size_bytes = self._get_max_device_size_bytes(device)
# there is a swap device, we'll default it to not mounted. A negative
# size will ensure we format it (again) before mounting it due to
# inequality
device_current_size_bytes = -1
else
max_device_size_bytes = 0
# no swap device, so it has no concept of size.
device_current_size_bytes = nil
end
# If there is an umounted swap file, we don't know the 'formatted' size
file_current_size_bytes = -1
# ensure we only have things enabled that we understand
swaps_enabled.each do |swap|
if device && swap['file'] == device && swap['mode'] == 'partition'
# found swap partition
device_current_size_bytes = swap['size_bytes']
elsif swap['file'] == file && swap['mode'] == 'file'
# found the swap file
file_current_size_bytes = swap['size_bytes']
else
fail "fb_swap: Found an unmanaged swap: #{swap}"
end
end
if node['fb_swap']['enabled']
size = node['fb_swap']['size']
else
size = 0
end
if size.nil?
# size nil means use full, existing swap device
if max_device_size_bytes.zero?
msg = 'fb_swap: default swap is requested, but there\'s no swap ' +
'partition so no implicit size. Set node.default[\'fb_swap\']' +
'[\'size\'] to something specific if you want to use a swap file'
if node['fb_swap']['strict']
fail msg
else
Chef::Log.warn(msg)
device_size_bytes = 0
file_size_bytes = 0
end
else
# default is to use all of the swap device, no swap file
device_size_bytes = max_device_size_bytes
file_size_bytes = 0
end
else
# API is in KB, convert to bytes for lowest common denominator
size_bytes = size * 1024
if size_bytes % 4096 != 0
fail "fb_swap::default: #{size_bytes} bytes is not an even number " +
'of 4KiB pages'
elsif size_bytes <= 1048576 && node['fb_swap']['enabled']
fail "fb_swap::default: #{size_bytes} is less than 1MiB. Use " +
'enabled = false instead'
elsif node['fb_swap']['filesystem'] != '/'
device_size_bytes = 0
file_size_bytes = size_bytes
elsif size_bytes <= max_device_size_bytes
device_size_bytes = size_bytes
file_size_bytes = 0
else
device_size_bytes = max_device_size_bytes
file_size_bytes = size_bytes - max_device_size_bytes
end
end
if file_size_bytes.positive? && !self.swap_file_possible?(node)
fail "fb_swap: swap file of #{file_size_bytes} requested, but " +
'system does not support it. See previous log lines for ' +
'warnings that explain why'
end
if device
swapoff_needed, device_size_bytes = self._validate_resize(
node, 'device', device_size_bytes, device_current_size_bytes
)
else
swapoff_needed = false
end
file_swapoff_needed, file_size_bytes = self._validate_resize(
node, 'file', file_size_bytes, file_current_size_bytes
)
swapoff_needed ||= file_swapoff_needed
node.default['fb_swap']['_calculated'] = {
'device_size_bytes' => device_size_bytes,
'device_current_size_bytes' => device_current_size_bytes,
'file_size_bytes' => file_size_bytes,
'file_current_size_bytes' => file_current_size_bytes,
'swapoff_needed' => swapoff_needed,
}
end
def self._validate_resize(node, type, size_bytes, current_size_bytes)
# returns a pair:
# swapoff needed: boolean
# size_bytes: size to use
if current_size_bytes == -1
# current size is not-enabled. We might be leaving disabled or enabling
# both of which are safe and involve no swapoff.
return false, size_bytes
elsif size_bytes != current_size_bytes
Chef::Log.debug(
"fb_swap: #{type} size changed from #{current_size_bytes} to " +
"#{size_bytes} delta = #{size_bytes - current_size_bytes} bytes",
)
reason = node['fb_swap']['swapoff_allowed_because']
if reason
Chef::Log.debug("fb_swap: resizing #{type} allowed because #{reason}")
return true, size_bytes
else
# Failing a chef run for the inability to resize swap is
# overzealous. We chose not to, to allow the recipe to define
# a second way to set swapoff_allowed_because via a flag file.
Chef::Log.error(
"fb_swap: swap #{type} size change requested requires a " +
'\'swapoff\'. This is not safe. You can whitelisted with ' +
'\'swapoff_allowed_because\' API. Size change reverted.',
)
# a number of resources look at whether swap is enabled or not. If
# we get here, then swap must stay enabled, so change it back.
node.default['fb_swap']['enabled'] = true
# we are not doing a swapoff, and we are going to reset the
# size calculated to the current size instead of what was
# requested.
return false, current_size_bytes
end
end
# default case is we are not using swapoff
[false, size_bytes]
end
def self._device(node)
swap_mounts = node.filesystem_data['by_device'].to_hash.select do |_k, v|
v['fs_type'] == 'swap'
end
case swap_mounts.count
when 0
return nil
when 1
return swap_mounts.keys[0]
else
fail 'More than one swap mount found, this is not right.'
end
end
def self._file(node)
filesystem = node['fb_swap']['filesystem']
filesystem += '/' unless filesystem.end_with?('/')
"#{filesystem}swapvol/swapfile"
end
def self._path(node, type)
case type
when 'device'
self._device(node)
when 'file'
self._file(node)
end
end
def self._swap_unit(node, type)
FB::Systemd.path_to_unit(self._path(node, type), 'swap')
end
def self._get_max_device_size_bytes(device)
cmd = Mixlib::ShellOut.new([
'/usr/sbin/blockdev',
'--getsize64',
device,
]).run_command
cmd.error!
size = cmd.stdout.to_i
if size < 4096
fail 'fb_swap: swap device is too small to contain swap header'
end
size
end
def self._get_swaps_enabled
cmd = Mixlib::ShellOut.new([
'/usr/sbin/swapon',
'--show=NAME,TYPE,SIZE,USED',
'--raw',
'--bytes',
'--noheadings',
]).run_command
cmd.error!
cmd.stdout.each_line.collect do |line|
file, mode, size_bytes, used_bytes = line.chomp.split
{
'file' => file,
'mode' => mode,
# add 4k to the size to add the header back in
'size_bytes' => size_bytes.to_i + 4096,
'used_bytes' => used_bytes.to_i,
}
end
end
def self._filesystem_map_for_fs(node)
node.filesystem_data['by_mountpoint'][node['fb_swap']['filesystem']]
end
def self.swap_file_possible?(node)
if _filesystem_map_for_fs(node)['fs_type'] == 'btrfs'
# The historical take on btrfs is that swap files are not supported.
# This is changing soon (4.16+?). There's no feature test for this
# (yet?) so we'll be pessimistic and say no.
Chef::Log.warn('fb_swap: Swap file not generally possible on btrfs')
return false
end
return !self._on_rotational?(node)
end
def self._on_rotational?(node)
_filesystem_map_for_fs(node)['devices'].each do |dev|
lsblk = Mixlib::ShellOut.new("lsblk --json --output ROTA #{dev}")
lsblk.run_command.error!
block_device = JSON.parse(lsblk.stdout)['blockdevices'][0]
if block_device['rota'] == '1'
Chef::Log.debug(
"fb_swap: Swap file not possible due to rotational device #{dev}",
)
return true
end
end
Chef::Log.debug(
'fb_swap: Swap file possible (no rotational device members on ' +
'filesytem)',
)
return false
end
end
end