summaryrefslogtreecommitdiffstats
path: root/lib/puppet/network/handler/fileserver.rb
blob: 5da4cedef792baa984139ab4de0ae60fd8af3ab1 (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
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
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
require 'puppet'
require 'puppet/network/authstore'
require 'webrick/httpstatus'
require 'cgi'
require 'delegate'
require 'sync'
require 'xmlrpc/server'

require 'puppet/network/handler'
require 'puppet/network/xmlrpc/server'
require 'puppet/file_serving'
require 'puppet/file_serving/metadata'
require 'puppet/network/handler'

class Puppet::Network::Handler
  AuthStoreError = Puppet::AuthStoreError
  class FileServerError < Puppet::Error; end
  class FileServer < Handler
    desc "The interface to Puppet's fileserving abilities."

    attr_accessor :local

    CHECKPARAMS = [:mode, :type, :owner, :group, :checksum]

    # Special filserver module for puppet's module system
    MODULES = "modules"
    PLUGINS = "plugins"

    @interface = XMLRPC::Service::Interface.new("fileserver") { |iface|
      iface.add_method("string describe(string, string)")
      iface.add_method("string list(string, string, boolean, array)")
      iface.add_method("string retrieve(string, string)")
    }

    def self.params
      CHECKPARAMS.dup
    end

    # If the configuration file exists, then create (if necessary) a LoadedFile
    # object to manage it; else, return nil.
    def configuration
      # Short-circuit the default case.
      return @configuration if defined?(@configuration)

      config_path = @passed_configuration_path || Puppet[:fileserverconfig]
      return nil unless FileTest.exist?(config_path)

      # The file exists but we don't have a LoadedFile instance for it.
      @configuration = Puppet::Util::LoadedFile.new(config_path)
    end

    # Create our default mounts for modules and plugins.  This is duplicated code,
    # but I'm not really worried about that.
    def create_default_mounts
      @mounts = {}
      Puppet.debug "No file server configuration file; autocreating #{MODULES} mount with default permissions"
      mount = Mount.new(MODULES)
      mount.allow("*")
      @mounts[MODULES] = mount

      Puppet.debug "No file server configuration file; autocreating #{PLUGINS} mount with default permissions"
      mount = PluginMount.new(PLUGINS)
      mount.allow("*")
      @mounts[PLUGINS] = mount
    end

    # Describe a given file.  This returns all of the manageable aspects
    # of that file.
    def describe(url, links = :follow, client = nil, clientip = nil)
      links = links.intern if links.is_a? String

      mount, path = convert(url, client, clientip)

      mount.debug("Describing #{url} for #{client}") if client

      # use the mount to resolve the path for us.
      return "" unless full_path = mount.file_path(path, client)

      metadata = Puppet::FileServing::Metadata.new(url, :path => full_path, :links => links)

      return "" unless metadata.exist?

      begin
        metadata.collect
      rescue => detail
        puts detail.backtrace if Puppet[:trace]
        Puppet.err detail
        return ""
      end

      metadata.attributes_with_tabs
    end

    # Create a new fileserving module.
    def initialize(hash = {})
      @mounts = {}
      @files = {}

      @local = hash[:Local]

      @noreadconfig = true if hash[:Config] == false

      @passed_configuration_path = hash[:Config]

      if hash.include?(:Mount)
        @passedconfig = true
        raise Puppet::DevError, "Invalid mount hash #{hash[:Mount].inspect}" unless hash[:Mount].is_a?(Hash)

        hash[:Mount].each { |dir, name|
          self.mount(dir, name) if FileTest.exists?(dir)
        }
        self.mount(nil, MODULES)
        self.mount(nil, PLUGINS)
      else
        @passedconfig = false
        if configuration
          readconfig(false) # don't check the file the first time.
        else
          create_default_mounts
        end
      end
    end

    # List a specific directory's contents.
    def list(url, links = :ignore, recurse = false, ignore = false, client = nil, clientip = nil)
      mount, path = convert(url, client, clientip)

      mount.debug "Listing #{url} for #{client}" if client

      return "" unless mount.path_exists?(path, client)

      desc = mount.list(path, recurse, ignore, client)

      if desc.length == 0
        mount.notice "Got no information on //#{mount}/#{path}"
        return ""
      end

      desc.collect { |sub| sub.join("\t") }.join("\n")
    end

    def local?
      self.local
    end

    # Is a given mount available?
    def mounted?(name)
      @mounts.include?(name)
    end

    # Mount a new directory with a name.
    def mount(path, name)
      if @mounts.include?(name)
        if @mounts[name] != path
          raise FileServerError, "#{@mounts[name].path} is already mounted at #{name}"
        else
          # it's already mounted; no problem
          return
        end
      end

      # Let the mounts do their own error-checking.
      @mounts[name] = Mount.new(name, path)
      @mounts[name].info "Mounted #{path}"

      @mounts[name]
    end

    # Retrieve a file from the local disk and pass it to the remote
    # client.
    def retrieve(url, links = :ignore, client = nil, clientip = nil)
      links = links.intern if links.is_a? String

      mount, path = convert(url, client, clientip)

      mount.info "Sending #{url} to #{client}" if client

      unless mount.path_exists?(path, client)
        mount.debug "#{mount} reported that #{path} does not exist"
        return ""
      end

      links = links.intern if links.is_a? String

      if links == :ignore and FileTest.symlink?(path)
        mount.debug "I think that #{path} is a symlink and we're ignoring them"
        return ""
      end

      str = mount.read_file(path, client)

      if @local
        return str
      else
        return CGI.escape(str)
      end
    end

    def umount(name)
      @mounts.delete(name) if @mounts.include? name
    end

    private

    def authcheck(file, mount, client, clientip)
      # If we're local, don't bother passing in information.
      if local?
        client = nil
        clientip = nil
      end
      unless mount.allowed?(client, clientip)
        mount.warning "#{client} cannot access #{file}"
        raise Puppet::AuthorizationError, "Cannot access #{mount}"
      end
    end

    # Take a URL and some client info and return a mount and relative
    # path pair.
    #
    def convert(url, client, clientip)
      readconfig

      url = URI.unescape(url)

      mount, stub = splitpath(url, client)

      authcheck(url, mount, client, clientip)

      return mount, stub
    end

    # Return the mount for the Puppet modules; allows file copying from
    # the modules.
    def modules_mount(module_name, client)
      # Find our environment, if we have one.
      unless hostname = (client || Facter.value("hostname"))
        raise ArgumentError, "Could not find hostname"
      end
      env = (node = Puppet::Node.indirection.find(hostname)) ? node.environment : nil

      # And use the environment to look up the module.
      (mod = Puppet::Node::Environment.new(env).module(module_name) and mod.files?) ? @mounts[MODULES].copy(mod.name, mod.file_directory) : nil
    end

    # Read the configuration file.
    def readconfig(check = true)
      return if @noreadconfig

      return unless configuration

      return if check and ! @configuration.changed?

      newmounts = {}
      begin
        File.open(@configuration.file) { |f|
          mount = nil
          count = 1
          f.each { |line|
            case line
            when /^\s*#/; next # skip comments
            when /^\s*$/; next # skip blank lines
            when /\[([-\w]+)\]/
              name = $1
              raise FileServerError, "#{newmounts[name]} is already mounted as #{name} in #{@configuration.file}" if newmounts.include?(name)
              mount = Mount.new(name)
              newmounts[name] = mount
            when /^\s*(\w+)\s+(.+)$/
              var = $1
              value = $2
              case var
              when "path"
                raise FileServerError.new("No mount specified for argument #{var} #{value}") unless mount
                if mount.name == MODULES
                  Puppet.warning "The '#{mount.name}' module can not have a path. Ignoring attempt to set it"
                else
                  begin
                    mount.path = value
                  rescue FileServerError => detail
                    Puppet.err "Removing mount #{mount.name}: #{detail}"
                    newmounts.delete(mount.name)
                  end
                end
              when "allow"
                raise FileServerError.new("No mount specified for argument #{var} #{value}") unless mount
                value.split(/\s*,\s*/).each { |val|
                  begin
                    mount.info "allowing #{val} access"
                    mount.allow(val)
                  rescue AuthStoreError => detail
                    puts detail.backtrace if Puppet[:trace]

                      raise FileServerError.new(
                        detail.to_s,

                      count, @configuration.file)
                  end
                }
              when "deny"
                raise FileServerError.new("No mount specified for argument #{var} #{value}") unless mount
                value.split(/\s*,\s*/).each { |val|
                  begin
                    mount.info "denying #{val} access"
                    mount.deny(val)
                  rescue AuthStoreError => detail

                    raise FileServerError.new(
                      detail.to_s,

                      count, @configuration.file)
                  end
                }
              else
                raise FileServerError.new("Invalid argument '#{var}'", count, @configuration.file)
              end
            else
              raise FileServerError.new("Invalid line '#{line.chomp}'", count, @configuration.file)
            end
            count += 1
          }
        }
      rescue Errno::EACCES => detail
        Puppet.err "FileServer error: Cannot read #{@configuration}; cannot serve"
        #raise Puppet::Error, "Cannot read #{@configuration}"
      rescue Errno::ENOENT => detail
        Puppet.err "FileServer error: '#{@configuration}' does not exist; cannot serve"
      end

      unless newmounts[MODULES]
        Puppet.debug "No #{MODULES} mount given; autocreating with default permissions"
        mount = Mount.new(MODULES)
        mount.allow("*")
        newmounts[MODULES] = mount
      end

      unless newmounts[PLUGINS]
        Puppet.debug "No #{PLUGINS} mount given; autocreating with default permissions"
        mount = PluginMount.new(PLUGINS)
        mount.allow("*")
        newmounts[PLUGINS] = mount
      end

      unless newmounts[PLUGINS].valid?
        Puppet.debug "No path given for #{PLUGINS} mount; creating a special PluginMount"
        # We end up here if the user has specified access rules for
        # the plugins mount, without specifying a path (which means
        # they want to have the default behaviour for the mount, but
        # special access control).  So we need to move all the
        # user-specified access controls into the new PluginMount
        # object...
        mount = PluginMount.new(PLUGINS)
        # Yes, you're allowed to hate me for this.

          mount.instance_variable_set(
            :@declarations,

              newmounts[PLUGINS].instance_variable_get(:@declarations)
              )
        newmounts[PLUGINS] = mount
      end

      # Verify each of the mounts are valid.
      # We let the check raise an error, so that it can raise an error
      # pointing to the specific problem.
      newmounts.each { |name, mount|
        raise FileServerError, "Invalid mount #{name}" unless mount.valid?
      }
      @mounts = newmounts
    end

    # Split the path into the separate mount point and path.
    def splitpath(dir, client)
      # the dir is based on one of the mounts
      # so first retrieve the mount path
      mount = nil
      path = nil
      if dir =~ %r{/([-\w]+)}
        # Strip off the mount name.
        mount_name, path = dir.sub(%r{^/}, '').split(File::Separator, 2)

        unless mount = modules_mount(mount_name, client)
          unless mount = @mounts[mount_name]
            raise FileServerError, "Fileserver module '#{mount_name}' not mounted"
          end
        end
      else
        raise FileServerError, "Fileserver error: Invalid path '#{dir}'"
      end

      if path.nil? or path == ''
        path = '/'
      elsif path
        # Remove any double slashes that might have occurred
        path = URI.unescape(path.gsub(/\/\//, "/"))
      end

      return mount, path
    end

    def to_s
      "fileserver"
    end

    # A simple class for wrapping mount points.  Instances of this class
    # don't know about the enclosing object; they're mainly just used for
    # authorization.
    class Mount < Puppet::Network::AuthStore
      attr_reader :name

      @@syncs = {}

      @@files = {}

      Puppet::Util.logmethods(self, true)

      # Create a map for a specific client.
      def clientmap(client)
        {
          "h" => client.sub(/\..*$/, ""),
          "H" => client,
          "d" => client.sub(/[^.]+\./, "") # domain name
        }
      end

      # Replace % patterns as appropriate.
      def expand(path, client = nil)
        # This map should probably be moved into a method.
        map = nil

        if client
          map = clientmap(client)
        else
          Puppet.notice "No client; expanding '#{path}' with local host"
          # Else, use the local information
          map = localmap
        end
        path.gsub(/%(.)/) do |v|
          key = $1
          if key == "%"
            "%"
          else
            map[key] || v
          end
        end
      end

      # Do we have any patterns in our path, yo?
      def expandable?
        if defined?(@expandable)
          @expandable
        else
          false
        end
      end

      # Return a fully qualified path, given a short path and
      # possibly a client name.
      def file_path(relative_path, node = nil)
        full_path = path(node)

        unless full_path
          p self
          raise ArgumentError.new("Mounts without paths are not usable") unless full_path
        end

        # If there's no relative path name, then we're serving the mount itself.
        return full_path unless relative_path and relative_path != "/"

        File.join(full_path, relative_path)
      end

      # Create out object.  It must have a name.
      def initialize(name, path = nil)
        unless name =~ %r{^[-\w]+$}
          raise FileServerError, "Invalid name format '#{name}'"
        end
        @name = name

        if path
          self.path = path
        else
          @path = nil
        end

        @files = {}

        super()
      end

      def fileobj(path, links, client)
        obj = nil
        if obj = @files[file_path(path, client)]
          # This can only happen in local fileserving, but it's an
          # important one.  It'd be nice if we didn't just set
          # the check params every time, but I'm not sure it's worth
          # the effort.
          obj[:audit] = CHECKPARAMS
        else

          obj = Puppet::Type.type(:file).new(

            :name => file_path(path, client),

            :audit => CHECKPARAMS
          )
          @files[file_path(path, client)] = obj
        end

        if links == :manage
          links = :follow
        end

        # This, ah, might be completely redundant
        obj[:links] = links unless obj[:links] == links

        obj
      end

      # Read the contents of the file at the relative path given.
      def read_file(relpath, client)
        File.read(file_path(relpath, client))
      end

      # Cache this manufactured map, since if it's used it's likely
      # to get used a lot.
      def localmap
        unless defined?(@@localmap)
          @@localmap = {
            "h" =>  Facter.value("hostname"),
            "H" => [Facter.value("hostname"),
              Facter.value("domain")].join("."),
            "d" =>  Facter.value("domain")
          }
        end
        @@localmap
      end

      # Return the path as appropriate, expanding as necessary.
      def path(client = nil)
        if expandable?
          return expand(@path, client)
        else
          return @path
        end
      end

      # Set the path.
      def path=(path)
        # FIXME: For now, just don't validate paths with replacement
        # patterns in them.
        if path =~ /%./
          # Mark that we're expandable.
          @expandable = true
        else
          raise FileServerError, "#{path} does not exist" unless FileTest.exists?(path)
          raise FileServerError, "#{path} is not a directory" unless FileTest.directory?(path)
          raise FileServerError, "#{path} is not readable" unless FileTest.readable?(path)
          @expandable = false
        end
        @path = path
      end

      # Verify that the path given exists within this mount's subtree.
      #
      def path_exists?(relpath, client = nil)
        File.exists?(file_path(relpath, client))
      end

      # Return the current values for the object.
      def properties(obj)
        obj.retrieve.inject({}) { |props, ary| props[ary[0].name] = ary[1]; props }
      end

      # Retrieve a specific directory relative to a mount point.
      # If they pass in a client, then expand as necessary.
      def subdir(dir = nil, client = nil)
        basedir = self.path(client)

        dirname = if dir
          File.join(basedir, *dir.split("/"))
        else
          basedir
        end

        dirname
      end

      def sync(path)
        @@syncs[path] ||= Sync.new
        @@syncs[path]
      end

      def to_s
        "mount[#{@name}]"
      end

      # Verify our configuration is valid.  This should really check to
      # make sure at least someone will be allowed, but, eh.
      def valid?
        if name == MODULES
          return @path.nil?
        else
          return ! @path.nil?
        end
      end

      # Return a new mount with the same properties as +self+, except
      # with a different name and path.
      def copy(name, path)
        result = self.clone
        result.path = path
        result.instance_variable_set(:@name, name)
        result
      end

      # List the contents of the relative path +relpath+ of this mount.
      #
      # +recurse+ is the number of levels to recurse into the tree,
      # or false to provide no recursion or true if you just want to
      # go for broke.
      #
      # +ignore+ is an array of filenames to ignore when traversing
      # the list.
      #
      # The return value of this method is a complex nest of arrays,
      # which describes a directory tree.  Each file or directory is
      # represented by an array, where the first element is the path
      # of the file (relative to the root of the mount), and the
      # second element is the type.  A directory is represented by an
      # array as well, where the first element is a "directory" array,
      # while the remaining elements are other file or directory
      # arrays.  Confusing?  Hell yes.  As an added bonus, all names
      # must start with a slash, because... well, I'm fairly certain
      # a complete explanation would involve the words "crack pipe"
      # and "bad batch".
      #
      def list(relpath, recurse, ignore, client = nil)
        abspath = file_path(relpath, client)
        if FileTest.exists?(abspath)
          if FileTest.directory?(abspath) and recurse
            return reclist(abspath, recurse, ignore)
          else
            return [["/", File.stat(abspath).ftype]]
          end
        end
        nil
      end

      def reclist(abspath, recurse, ignore)
        require 'puppet/file_serving'
        require 'puppet/file_serving/fileset'
        if recurse.is_a?(Fixnum)
          args = { :recurse => true, :recurselimit => recurse, :links => :follow }
        else
          args = { :recurse => recurse, :links => :follow }
        end
        args[:ignore] = ignore if ignore
        fs = Puppet::FileServing::Fileset.new(abspath, args)
        ary = fs.files.collect do |file|
          if file == "."
            file = "/"
          else
            file = File.join("/", file )
          end
          stat = fs.stat(File.join(abspath, file))
          next if stat.nil?
          [ file, stat.ftype ]
        end

        ary.compact
      end

    end

    # A special mount class specifically for the plugins mount -- just
    # has some magic to effectively do a union mount of the 'plugins'
    # directory of all modules.
    #
    class PluginMount < Mount
      def path(client)
        ''
      end

      def mod_path_exists?(mod, relpath, client = nil)
        ! mod.plugin(relpath).nil?
      end

      def path_exists?(relpath, client = nil)
        !valid_modules(client).find { |mod| mod.plugin(relpath) }.nil?
      end

      def valid?
        true
      end

      def mod_file_path(mod, relpath, client = nil)
        File.join(mod, PLUGINS, relpath)
      end

      def file_path(relpath, client = nil)
        return nil unless mod = valid_modules(client).find { |m| m.plugin(relpath) }
        mod.plugin(relpath)
      end

      # create a list of files by merging all modules
      def list(relpath, recurse, ignore, client = nil)
        result = []
        valid_modules(client).each do |mod|
          if modpath = mod.plugin(relpath)
            if FileTest.directory?(modpath) and recurse
              ary = reclist(modpath, recurse, ignore)
              ary ||= []
              result += ary
            else
              result += [["/", File.stat(modpath).ftype]]
            end
          end
        end
        result
      end

      private
      def valid_modules(client)
        Puppet::Node::Environment.new.modules.find_all { |mod| mod.exist? }
      end

      def add_to_filetree(f, filetree)
        first, rest = f.split(File::SEPARATOR, 2)
      end
    end
  end
end