summaryrefslogtreecommitdiff
path: root/lib/chef/resource/swap_file.rb
blob: 942424ab6fddc77138edec3f0c4fd4b4e6450909 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
#
# Copyright:: 2012-2018, Seth Vargo
# Copyright:: 2017-2020, Chef Software, Inc.
#
# 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_relative "../resource"

class Chef
  class Resource
    class SwapFile < Chef::Resource
      resource_name :swap_file
      provides(:swap_file) { true }

      description "Use the swap_file resource to create or delete swap files on Linux systems, and optionally to manage the swappiness configuration for a host."
      introduced "14.0"

      property :path, String,
        description: "The path where the swap file will be created on the system if it differs from the resource block's name.",
        name_property: true

      property :size, Integer,
        description: "The size (in MBs) of the swap file."

      property :persist, [TrueClass, FalseClass],
        description: "Persist the swapon.",
        default: false

      property :timeout, Integer,
        description: "Timeout for 'dd' / 'fallocate' commands.",
        default: 600

      property :swappiness, Integer,
        description: "The swappiness value to set on the system."

      action :create do
        description "Create a swapfile."

        if swap_enabled?
          Chef::Log.debug("#{new_resource} already created - nothing to do")
        else
          begin
            Chef::Log.info "starting first create: #{node["virtualization"]["system"]}"
            do_create(swap_creation_command)
          rescue Mixlib::ShellOut::ShellCommandFailed => e
            Chef::Log.warn("#{new_resource} Rescuing failed swapfile creation for #{new_resource.path}")
            Chef::Log.debug("#{new_resource} Exception when creating swapfile #{new_resource.path}: #{e}")
            do_create(dd_command)
          end
        end
        if new_resource.swappiness
          declare_resource(:sysctl, "vm.swappiness") do
            value new_resource.swappiness
          end
        end
      end

      action :remove do
        description "Remove a swapfile and disable swap."

        swapoff if swap_enabled?
        remove_swapfile if ::File.exist?(new_resource.path)
      end

      action_class do
        def do_create(command)
          create_swapfile(command)
          set_permissions
          mkswap
          swapon
          persist if persist?
        end

        def create_swapfile(command)
          converge_by "create empty swapfile at #{new_resource.path}" do # ~FC054
            shell_out!(command, timeout: new_resource.timeout)
          end
        end

        def set_permissions
          permissions = "600"
          converge_by "set permissions on #{new_resource.path} to #{permissions}" do
            shell_out!("chmod #{permissions} #{new_resource.path}")
          end
        end

        def mkswap
          converge_by "make #{new_resource.path} swappable" do
            shell_out!("mkswap -f #{new_resource.path}")
          end
        end

        def swapon
          converge_by "enable swap for #{new_resource.path}" do
            shell_out!("swapon #{new_resource.path}")
          end
        end

        def swapoff
          converge_by "turn off swap for #{new_resource.path}" do
            shell_out!("swapoff #{new_resource.path}")
          end
        end

        def remove_swapfile
          converge_by "remove swap file #{new_resource.path}" do
            ::FileUtils.rm(new_resource.path)
          end
        end

        def swap_enabled?
          enabled_swapfiles = shell_out("swapon --summary").stdout
          # Regex for our resource path and only our resource path
          # It will terminate on whitespace after the path it match
          # /testswapfile would match
          # /testswapfiledir/someotherfile will not
          swapfile_regex = Regexp.new("^#{new_resource.path}[\\s\\t\\n\\f]+")
          !swapfile_regex.match(enabled_swapfiles).nil?
        end

        def swap_creation_command
          command = if compatible_filesystem? && compatible_kernel && !docker?
                      fallocate_command
                    else
                      dd_command
                    end
          Chef::Log.debug("#{new_resource} swap creation command is '#{command}'")
          command
        end

        def fallback_swap_creation_command
          command = dd_command
          Chef::Log.debug("#{new_resource} fallback swap creation command is '#{command}'")
          command
        end

        # The block size (1MB)
        def block_size
          1_048_576
        end

        def fallocate_size
          size = block_size * new_resource.size
          Chef::Log.debug("#{new_resource} fallocate size is #{size}")
          size
        end

        def fallocate_command
          size = fallocate_size
          command = "fallocate -l #{size} #{new_resource.path}"
          Chef::Log.debug("#{new_resource} fallocate command is '#{command}'")
          command
        end

        def dd_command
          command = "dd if=/dev/zero of=#{new_resource.path} bs=#{block_size} count=#{new_resource.size}"
          Chef::Log.debug("#{new_resource} dd command is '#{command}'")
          command
        end

        def compatible_kernel
          fallocate_location = shell_out("which fallocate").stdout
          Chef::Log.debug("#{new_resource} fallocate location is '#{fallocate_location}'")
          ::File.exist?(fallocate_location.chomp)
        end

        def compatible_filesystem?
          compatible_filesystems = %w{xfs ext4}
          parent_directory = ::File.dirname(new_resource.path)
          # Get FS info, get second line as first is column headings
          command = "df -PT #{parent_directory} | awk 'NR==2 {print $2}'"
          result = shell_out(command).stdout
          Chef::Log.debug("#{new_resource} filesystem listing is '#{result}'")
          compatible_filesystems.any? { |fs| result.include? fs }
        end

        def persist?
          !!new_resource.persist
        end

        def persist
          fstab = "/etc/fstab"
          contents = ::File.readlines(fstab)
          addition = "#{new_resource.path} swap swap defaults 0 0"

          if contents.any? { |line| line.strip == addition }
            Chef::Log.debug("#{new_resource} already added to /etc/fstab - skipping")
          else
            Chef::Log.info("#{new_resource} adding entry to #{fstab} for #{new_resource.path}")

            contents << "#{addition}\n"
            ::File.open(fstab, "w") { |f| f.write(contents.join("")) }
          end
        end
      end
    end
  end
end