File: parsed.rb

package info (click to toggle)
puppet-agent 8.10.0-5
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 27,392 kB
  • sloc: ruby: 286,820; sh: 492; xml: 116; makefile: 88; cs: 68
file content (310 lines) | stat: -rw-r--r-- 11,162 bytes parent folder | download
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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
require 'puppet/provider/parsedfile'
require_relative '../mount'

fstab = case Facter.value('os.family')
        when 'Solaris' then '/etc/vfstab'
        when 'AIX' then '/etc/filesystems'
        else
          '/etc/fstab'
        end

Puppet::Type.type(:mount).provide(
  :parsed,
  parent: Puppet::Provider::ParsedFile,
  default_target: fstab,
  filetype: :flat,
) do
  include Puppet::Provider::Mount

  @doc = 'Manages filesystem mounts'

  commands mountcmd: 'mount', umount: 'umount'

  @fields = case Facter.value('os.family')
            when 'Solaris'
              [:device, :blockdevice, :name, :fstype, :pass, :atboot, :options]
            else
              [:device, :name, :fstype, :options, :dump, :pass]
            end

  if Facter.value('os.family') == 'AIX'
    # * is the comment character on AIX /etc/filesystems
    text_line :comment, match: %r{^\s*\*}
  else
    text_line :comment, match: %r{^\s*#}
  end
  text_line :blank, match: %r{^\s*$}

  optional_fields  = @fields - [:device, :name, :blockdevice]
  mandatory_fields = @fields - optional_fields

  # fstab will ignore lines that have fewer than the mandatory number of columns,
  # so we should, too.
  field_pattern = '(\s*(?>\S+))'
  text_line :incomplete, match: %r{^(?!#{field_pattern}{#{mandatory_fields.length}})}

  case Facter.value('os.family')
  when 'AIX'
    # The only field that is actually ordered is :name. See `man filesystems` on AIX
    @fields = [:name, :account, :boot, :check, :dev, :free, :mount, :nodename,
               :options, :quota, :size, :type, :vfs, :vol, :log]
    self.line_separator = "\n"
    # Override lines and use scan instead of split, because we DON'T want to
    # remove the separators
    def self.lines(text)
      lines = text.split("\n")
      filesystem_stanza = false
      filesystem_index = 0
      ret = []
      lines.each_with_index do |line, i|
        if %r{^\S+:}.match?(line)
          # Begin new filesystem stanza and save the index
          ret[filesystem_index] = filesystem_stanza.join("\n") if filesystem_stanza
          filesystem_stanza = Array(line)
          filesystem_index = i
          # Eat the preceding blank line
          ret[i - 1] = nil if i > 0 && ret[i - 1] && ret[i - 1].match(%r{^\s*$})
          nil
        elsif %r{^(\s*\*.*|\s*)$}.match?(line)
          # Just a comment or blank line; add in place
          ret[i] = line
        else
          # Non-comments or blank lines must be part of a stanza
          filesystem_stanza << line
        end
      end
      # Add the final stanza to the return
      ret[filesystem_index] = filesystem_stanza.join("\n") if filesystem_stanza
      ret = ret.compact.flatten
      ret.reject { |line| line.match(%r{^\* HEADER}) }
    end

    def self.header
      super.gsub(%r{^#}, '*')
    end

    record_line name,
                fields: @fields,
                separator: %r{\n},
                block_eval: :instance do
      def post_parse(result)
        property_map = {
          dev: :device,
          nodename: :nodename,
          options: :options,
          vfs: :fstype,
        }
        # Result is modified in-place instead of being returned; icky!
        memo = result.dup
        result.clear
        # Save the line for later, just in case it is unparsable
        result[:line] = @fields.map { |field|
          memo[field] if memo[field] != :absent
        }.compact.join("\n")
        result[:record_type] = memo[:record_type]
        special_options = []
        result[:name] = memo[:name].sub(%r{:\s*$}, '').strip
        memo.each do |_, k_v|
          next unless k_v&.is_a?(String) && k_v.match('=')
          attr_name, attr_value = k_v.split('=', 2).map(&:strip)
          attr_map_name = property_map[attr_name.to_sym]
          if attr_map_name
            # These are normal "options" options (see `man filesystems`)
            result[attr_map_name] = attr_value
          else
            # These /etc/filesystem attributes have no mount resource simile,
            # so are added to the "options" property for puppet's sake
            special_options << "#{attr_name}=#{attr_value}"
          end
          if result[:nodename]
            result[:device] = "#{result[:nodename]}:#{result[:device]}"
            result.delete(:nodename)
          end
        end
        result[:options] = [result[:options], special_options.sort].flatten.compact.join(',')
        unless result[:device]
          result[:device] = :absent
          # TRANSLATORS "prefetch" is a program name and should not be translated
          Puppet.err _("Prefetch: Mount[%{name}]: Field 'device' is missing") % { name: result[:name] }
        end
        unless result[:fstype]
          result[:fstype] = :absent
          # TRANSLATORS "prefetch" is a program name and should not be translated
          Puppet.err _("Prefetch: Mount[%{name}]: Field 'fstype' is missing") % { name: result[:name] }
        end
      end

      def to_line(result)
        output = []
        output << "#{result[:name]}:"
        if result[:device]&.match(%r{^/})
          output << "\tdev\t\t= #{result[:device]}"
        elsif result[:device] && result[:device] != :absent
          unless %r{^.+:/}.match?(result[:device])
            # Just skip this entry; it was malformed to begin with
            Puppet.err _("Mount[%{name}]: Field 'device' must be in the format of <absolute path> or <host>:<absolute path>") % { name: result[:name] }
            return result[:line]
          end
          nodename, path = result[:device].split(':')
          output << "\tdev\t\t= #{path}"
          output << "\tnodename\t= #{nodename}"
        else
          # Just skip this entry; it was malformed to begin with
          Puppet.err _("Mount[%{name}]: Field 'device' is required") % { name: result[:name] }
          return result[:line]
        end
        if result[:fstype] && result[:fstype] != :absent
          output << "\tvfs\t\t= #{result[:fstype]}"
        else
          # Just skip this entry; it was malformed to begin with
          Puppet.err _("Mount[%{name}]: Field 'device' is required") % { name: result[:name] }
          return result[:line]
        end
        if result[:options]
          options = result[:options].split(',')
          special_options = options.select do |x|
            x.match('=') &&
              ['account', 'boot', 'check', 'free', 'mount', 'size', 'type',
               'vol', 'log', 'quota'].include?(x.split('=').first)
          end
          options -= special_options
          special_options.sort.each do |x|
            k, v = x.split('=')
            output << "\t#{k}\t\t= #{v}"
          end
          output << "\toptions\t\t= #{options.join(',')}" unless options.empty?
        end
        if result[:line] && result[:line].split("\n").sort == output.sort
          "\n#{result[:line]}"
        else
          "\n#{output.join("\n")}"
        end
      end
    end
  else
    record_line name, fields: @fields, separator: %r{\s+}, joiner: "\t", optional: optional_fields, block_eval: :instance do
      def to_line(record)
        # convert whitespace to ASCII before writing to fstab
        # duplicate the record since we don't want our resource to have ASCII whitespaces
        result = record.dup
        [:device, :name].each do |param|
          if record[param].is_a?(String)
            result[param] = result[param].gsub(' ', '\\\040') if result[param].include?(' ')
          end
        end
        join(result)
      end

      def post_parse(record)
        # handle ASCII-encoded whitespaces in fstab
        [:device, :name].each do |param|
          if record[param].is_a?(String)
            record[param].gsub!('\040', ' ') if record[param].include?('\040')
          end
        end
        record
      end

      def pre_gen(record)
        if !record[:options] || record[:options].empty?
          if Facter.value(:kernel) == 'Linux'
            record[:options] = 'defaults'
          else
            raise Puppet::Error, _("Mount[%{name}]: Field 'options' is required") % { name: record[:name] }
          end
        end
        if !record[:fstype] || record[:fstype].empty?
          raise Puppet::Error, _("Mount[%{name}]: Field 'fstype' is required") % { name: record[:name] }
        end
        record
      end
    end
  end

  # Every entry in fstab is :unmounted until we can prove different
  def self.prefetch_hook(target_records)
    target_records.map do |record|
      # Eat the trailing slash(es) of mountpoints in fstab
      # This mimics the behavior of munging the resource title
      record[:name]&.gsub!(%r{^(.+?)/*$}, '\1')
      record[:ensure] = :unmounted if record[:record_type] == :parsed
      record
    end
  end

  def self.instances
    providers = super
    mounts = mountinstances.dup

    # Update fstab entries that are mounted
    providers.each do |prov|
      if mounts.delete(name: prov.get(:name), mounted: :yes)
        prov.set(ensure: :mounted)
      end
    end

    # Add mounts that are not in fstab but mounted
    mounts.each do |mount|
      providers << new(ensure: :ghost, name: mount[:name])
    end
    providers
  end

  def self.prefetch(resources = nil)
    # Get providers for all resources the user defined and that match
    # a record in /etc/fstab.
    super
    # We need to do two things now:
    # - Update ensure from :unmounted to :mounted if the resource is mounted
    # - Check for mounted devices that are not in fstab and
    #   set ensure to :ghost (if the user wants to add an entry
    #   to fstab we need to know if the device was mounted before)
    mountinstances.each do |hash|
      mount = resources[hash[:name]]
      next unless mount
      case mount.provider.get(:ensure)
      when :absent # Mount not in fstab
        mount.provider.set(ensure: :ghost)
      when :unmounted # Mount in fstab
        mount.provider.set(ensure: :mounted)
      end
    end
  end

  def self.mountinstances
    regex = case Facter.value('os.family')
            when 'Darwin'
              %r{ on (?:/private/var/automount)?(\S*)}
            when 'Solaris', 'HP-UX'
              %r{^(\S*) on }
            when 'AIX'
              %r{^(?:\S*\s+\S+\s+)(\S+)}
            when %r{FreeBSD|NetBSD}i
              %r{ on (.*) \(}
            else
              %r{ on (.*) type }
            end
    instances = []
    mount_output = mountcmd.split("\n")
    if mount_output.length >= 2 && mount_output[1] =~ %r{^[- \t]*$}
      # On some OSes (e.g. AIX) mount output begins with a header line
      # followed by a line consisting of dashes and whitespace.
      # Discard these two lines.
      mount_output[0..1] = []
    end
    mount_output.each do |line|
      if (match = regex.match(line)) && (name = match.captures.first)
        instances << { name: name, mounted: :yes } # Only :name is important here
      else
        raise Puppet::Error, _('Could not understand line %{line} from mount output') % { line: line }
      end
    end
    instances
  end

  def flush
    needs_mount = @property_hash.delete(:needs_mount)
    super
    mount if needs_mount
  end
end