6
require 'puppet/type/state'
7
require 'puppet/server/fileserver'
11
@doc = "Manages local files, including setting ownership and
12
permissions, creation of both files and directories, and
13
retrieving entire files from remote servers. As Puppet matures, it
14
expected that the ``file`` element will be used less and less to
15
manage content, and instead native elements will be used to do so.
17
If you find that you are often copying files in from a central
18
location, rather than using native elements, please contact
19
Reductive Labs and we can hopefully work with you to develop a
20
native element to support what you are doing."
23
desc "The path to the file to manage. Must be fully qualified."
27
unless value =~ /^#{File::SEPARATOR}/
28
raise Puppet::Error, "File paths must be fully qualified"
34
desc "Whether files should be backed up before
35
being replaced. If a filebucket is specified, files will be
36
backed up there; else, they will be backed up in the same directory
37
with a ``.puppet-bak`` extension,, and no backups
38
will be made if backup is ``false``.
40
To use filebuckets, you must first create a filebucket in your
47
The ``puppetmasterd`` daemon creates a filebucket by default,
48
so you can usually back up to your main server with this
49
configuration. Once you've described the bucket in your
50
configuration, you can use it in any file:
53
source => \"/path/in/nfs/or/something\",
57
This will back the file up to the central server.
59
At this point, the only benefits to doing so are that you do not
60
have backup files lying around on each of your machines, a given
61
version of a file is only backed up once, and you can restore
62
any given file manually, no matter how old. Eventually,
63
transactional support will be able to automatically restore
68
defaultto ".puppet-bak"
72
when false, "false", :false:
74
when true, "true", ".puppet-bak", :true:
77
# We can't depend on looking this up right now,
78
# we have to do it after all of the objects
79
# have been instantiated.
83
self.fail "Invalid backup type %s" %
88
# Provide a straight-through hook for setting the bucket.
95
newparam(:linkmaker) do
96
desc "An internal parameter used by the *symlink*
97
type to do recursive link creation."
100
newparam(:recurse) do
101
desc "Whether and how deeply to do recursive
104
newvalues(:true, :false, :inf, /^[0-9]+$/)
106
newval = super(value)
108
when :true, :inf: true
116
newparam(:replace) do
117
desc "Whether or not to replace a file that is
118
sourced but exists. This is useful for using file sources
119
purely for initialization."
120
newvalues(:true, :false)
125
desc "Force the file operation. Currently only used when replacing
126
directories with links."
127
newvalues(:true, :false)
132
desc "A parameter which omits action on files matching
133
specified patterns during recursion. Uses Ruby's builtin globbing
134
engine, so shell metacharacters are fully supported, e.g. ``[a-z]*``.
135
Matches that would descend into the directory structure are ignored,
141
unless value.is_a?(Array) or value.is_a?(String) or value == false
142
self.devfail "Ignore must be a string or an Array"
148
desc "How to handle links during file actions. During file copying,
149
``follow`` will copy the target file instead of the link, ``manage``
150
will copy the link itself, and ``ignore`` will just pass it by.
151
When not copying, ``manage`` and ``ignore`` behave equivalently
152
(because you cannot really ignore links entirely during local
153
recursion), and ``follow`` will manage the file to which the
156
newvalues(:follow, :manage, :ignore)
158
# :ignore and :manage behave equivalently on local files,
159
# but don't copy remote links
164
desc "Whether unmanaged files should be purged. If you have a filebucket
165
configured the purged files will be uploaded, but if you do not,
166
this will destroy data. Only use this option for generated
167
files unless you really know what you are doing. This option only
168
makes sense when recursively managing directories."
172
newvalues(:true, :false)
175
# Autorequire any parent directories.
176
autorequire(:file) do
178
pary = self[:path].split(File::SEPARATOR)
179
pary.shift # remove the initial nil
182
pary.inject([""]) do |ary, dir|
184
cur << ary.join(File::SEPARATOR)
191
# Autorequire the owner and group of the file.
192
{:user => :owner, :group => :group}.each do |type, state|
194
if @states.include?(state)
195
# The user/group states automatically converts to IDs
196
next unless should = @states[state].shouldorig
198
if val.is_a?(Integer) or val =~ /^\d+$/
208
if self[:content] and self[:source]
209
self.fail "You cannot specify both content and a source"
213
# List files, but only one level deep.
214
def self.list(base = "/")
215
unless FileTest.directory?(base)
220
Dir.entries(base).reject { |e|
221
e == "." or e == ".."
223
path = File.join(base, name)
228
files << self.create(
229
:name => path, :check => :all
240
@arghash.include?(arg)
243
# Determine the user to write files as.
245
if self.should(:owner) and ! self.should(:owner).is_a?(Symbol)
246
writeable = Puppet::SUIDManager.asuser(self.should(:owner)) {
247
FileTest.writable?(File.dirname(self[:path]))
250
# If the parent directory is writeable, then we execute
251
# as the user in question. Otherwise we'll rely on
252
# the 'owner' state to do things.
254
asuser = self.should(:owner)
261
# We have to do some extra finishing, to retrieve our bucket if
264
# Let's cache these values, since there should really only be
265
# a couple of these buckets
268
# Look up our bucket, if there is one
269
if @parameters.include?(:backup) and bucket = @parameters[:backup].bucket
272
if obj = @@filebuckets[bucket]
273
# This sets the @value on :backup, too
274
@parameters[:backup].bucket = obj
275
elsif obj = Puppet.type(:filebucket).bucket(bucket)
276
@@filebuckets[bucket] = obj
277
@parameters[:backup].bucket = obj
279
self.fail "Could not find filebucket %s" % bucket
281
when Puppet::Client::Dipper: # things are hunky-dorey
283
self.fail "Invalid bucket type %s" % bucket.class
290
def handlebackup(file = nil)
291
# let the path be specified
293
# if they specifically don't want a backup, then just say
295
unless FileTest.exists?(file)
303
case File.stat(file).ftype
306
# we don't need to backup directories when recurse is on
309
backup = self[:backup]
311
when Puppet::Client::Dipper:
312
notice "Recursively backing up to filebucket"
314
Find.find(self[:path]) do |f|
316
sum = backup.backup(f)
317
self.info "Filebucketed %s to %s with sum %s" %
318
[f, backup.name, sum]
324
newfile = file + backup
325
# Just move it, since it's a directory.
326
if FileTest.exists?(newfile)
327
remove_backup(newfile)
330
bfile = file + backup
332
# Ruby 1.8.1 requires the 'preserve' addition, but
333
# later versions do not appear to require it.
334
FileUtils.cp_r(file, bfile, :preserve => true)
337
# since they said they want a backup, let's error out
338
# if we couldn't make one
339
self.fail "Could not back %s up: %s" %
340
[file, detail.message]
343
self.err "Invalid backup type %s" % backup.inspect
348
backup = self[:backup]
350
when Puppet::Client::Dipper:
351
sum = backup.backup(file)
352
self.info "Filebucketed to %s with sum %s" %
356
newfile = file + backup
357
if FileTest.exists?(newfile)
358
remove_backup(newfile)
361
# FIXME Shouldn't this just use a Puppet object with
362
# 'source' specified?
363
bfile = file + backup
365
# Ruby 1.8.1 requires the 'preserve' addition, but
366
# later versions do not appear to require it.
367
FileUtils.cp(file, bfile, :preserve => true)
370
# since they said they want a backup, let's error out
371
# if we couldn't make one
372
self.fail "Could not back %s up: %s" %
373
[file, detail.message]
376
self.err "Invalid backup type %s" % backup.inspect
379
when "link": return true
381
self.notice "Cannot backup files of type %s" %
382
File.stat(file).ftype
387
def handleignore(children)
388
return children unless self[:ignore]
389
self[:ignore].each { |ignore|
391
Dir.glob(File.join(self[:path],ignore), File::FNM_DOTMATCH) {
392
|match| ignored.push(File.basename(match))
394
children = children - ignored
400
# Store a copy of the arguments for later.
401
tmphash = hash.to_hash
403
# Used for caching clients
408
# Get rid of any duplicate slashes, and remove any trailing slashes.
409
@title = @title.gsub(/\/+/, "/").sub(/\/$/, "")
411
# Clean out as many references to any file paths as possible.
412
# This was the source of many, many bugs.
414
@arghash.delete(self.class.namevar)
416
if @arghash.include?(:source)
417
@arghash.delete(:source)
420
if @arghash.include?(:parent)
421
@arghash.delete(:parent)
427
# Build a recursive map of a link source
428
def linkrecurse(recurse)
429
target = @states[:target].should
432
if self[:links] == :follow
437
unless FileTest.exist?(target)
438
#self.info "%s does not exist; not recursing" %
442
# Now stat our target
443
targetstat = File.send(method, target)
444
unless targetstat.ftype == "directory"
445
#self.info "%s is not a directory; not recursing" %
450
# Now that we know our corresponding target is a directory,
452
self[:ensure] = :directory
454
unless FileTest.readable? target
455
self.notice "Cannot manage %s: permission denied" % self.name
459
children = Dir.entries(target).reject { |d| d =~ /^\.+$/ }
461
#Get rid of ignored children
462
if @parameters.include?(:ignore)
463
children = handleignore(children)
467
children.each do |file|
469
longname = File.join(target, file)
471
# Files know to create directories when recursion
472
# is enabled and we're making links
478
if child = self.newchild(file, true, args)
479
unless @children.include?(child)
488
# Build up a recursive map of what's around right now
489
def localrecurse(recurse)
490
unless FileTest.exist?(self[:path]) and self.stat.directory?
491
#self.info "%s is not a directory; not recursing" %
496
unless FileTest.readable? self[:path]
497
self.notice "Cannot manage %s: permission denied" % self.name
501
children = Dir.entries(self[:path])
503
#Get rid of ignored children
504
if @parameters.include?(:ignore)
505
children = handleignore(children)
509
children.each { |file|
510
file = File.basename(file)
511
next if file =~ /^\.\.?$/ # skip . and ..
512
options = {:recurse => recurse}
514
if child = self.newchild(file, true, options)
515
# Mark any unmanaged files for removal if purge is set.
516
# Use the array rather than [] because tidy uses this method, too.
517
if @parameters.include?(:purge) and self[:purge] == :true and child.implicit?
518
child[:ensure] = :absent
521
unless @children.include?(child)
529
# Create a new file or directory object as a child to the current
531
def newchild(path, local, hash = {})
532
# make local copy of arguments
535
if path =~ %r{^#{File::SEPARATOR}}
537
"Must pass relative paths to PFile#newchild()"
540
path = File.join(self[:path], path)
545
unless hash.include?(:recurse)
546
if args.include?(:recurse)
547
if args[:recurse].is_a?(Integer)
548
args[:recurse] -= 1 # reduce the level of recursion
554
hash.each { |key,value|
561
# We specifically look in @parameters here, because 'linkmaker' isn't
562
# a valid attribute for subclasses, so using 'self[:linkmaker]' throws
564
if @parameters.include?(:linkmaker) and
565
args.include?(:source) and ! FileTest.directory?(args[:source])
566
klass = Puppet.type(:symlink)
568
# clean up the args a lot for links
571
:ensure => old[:source],
578
# The child might already exist because 'localrecurse' runs
579
# before 'sourcerecurse'. I could push the override stuff into
580
# a separate method or something, but the work is the same other
581
# than this last bit, so it doesn't really make sense.
582
if child = klass[path]
583
unless @children.include?(child)
584
self.debug "Not managing more explicit file %s" %
589
# This is only necessary for sourcerecurse, because we might have
590
# created the object with different 'should' values than are
593
args.each { |var,value|
596
# behave idempotently
597
unless child.should(var) == value
602
else # create it anew
603
#notice "Creating new file with args %s" % args.inspect
606
child = klass.implicitcreate(args)
608
# implicit creation can return nil
613
rescue Puppet::Error => detail
615
"Cannot manage: %s" %
618
self.debug args.inspect
622
"Cannot manage: %s" %
625
self.debug args.inspect
634
# We only need to behave specially when our parent is also
636
if @parent.is_a?(self.class)
637
# Remove the parent file name
638
ppath = @parent.path.sub(/\/?file=.+/, '')
640
if ppath != "/" and ppath != ""
643
tmp << self.class.name.to_s + "=" + self.name
649
# The top-level name is always puppet[top], so we don't
650
# bother with that. And we don't add the hostname
651
# here, it gets added in the log server thingy.
652
if self.name == "puppet[top]"
655
# We assume that if we don't have a parent that we
656
# should not cache the path
657
return [self.class.name.to_s + "=" + self.name]
662
# Recurse into the directory. This basically just calls 'localrecurse'
663
# and maybe 'sourcerecurse'.
665
recurse = self[:recurse]
666
# we might have a string, rather than a number
667
if recurse.is_a?(String)
668
if recurse =~ /^[0-9]+$/
669
recurse = Integer(recurse)
670
#elsif recurse =~ /^inf/ # infinite recursion
671
else # anything else is infinite recursion
676
# are we at the end of the recursion?
682
if recurse.is_a?(Integer)
686
self.localrecurse(recurse)
687
if @states.include? :target
688
self.linkrecurse(recurse)
690
if @states.include?(:source)
691
self.sourcerecurse(recurse)
696
return false unless @parameters.include?(:recurse)
698
val = @parameters[:recurse].value
700
if val and (val == true or val > 0)
707
# Remove the old backup.
708
def remove_backup(newfile)
709
if self.class.name == :file and self[:links] != :follow
714
old = File.send(method, newfile).ftype
716
if old == "directory"
718
"Will not remove directory backup %s; use a filebucket" %
722
info "Removing old backup of type %s" %
723
File.send(method, newfile).ftype
729
puts detail.backtrace
731
self.err "Could not remove old backup: %s" %
737
# Remove any existing data. This is only used when dealing with
738
# links or directories.
739
def remove_existing(should)
740
return unless s = stat(true)
743
self.fail "Could not back up; will not replace"
746
unless should.to_s == "link"
747
return if s.ftype.to_s == should.to_s
752
if self[:force] == :true
753
debug "Removing existing directory for replacement with %s" %
755
FileUtils.rmtree(self[:path])
757
notice "Not replacing directory; use 'force' to override"
760
debug "Removing existing %s for replacement with %s" %
762
File.unlink(self[:path])
764
self.fail "Could not back up files of type %s" % s.ftype
768
# a wrapper method to make sure the file exists before doing anything
770
if @states.include?(:source)
771
# This probably isn't the best place for it, but we need
772
# to make sure that we have a corresponding checksum state.
773
unless @states.include?(:checksum)
774
self[:checksum] = "md5"
777
# We have to retrieve the source info before the recursion happens,
778
# although I'm not exactly clear on why.
779
@states[:source].retrieve
782
if @parameters.include?(:recurse)
786
unless stat = self.stat(true)
787
self.debug "File does not exist"
788
@states.each { |name,state|
789
# We've already retrieved the source, and we don't
790
# want to overwrite whatever it did. This is a bit
791
# of a hack, but oh well, source is definitely special.
792
next if name == :source
799
states().each { |state|
800
# We don't want to call 'describe()' twice, so only do a local
801
# retrieve on the source.
802
if state.name == :source
803
state.retrieve(false)
810
# This recurses against the remote source and makes sure the local
811
# and remote structures match. It's run after 'localrecurse'.
812
def sourcerecurse(recurse)
813
# FIXME sourcerecurse should support purging non-remote files
814
source = @states[:source].source
816
unless ! source.nil? and source !~ /^\s*$/
817
self.notice "source %s does not exist" % @states[:source].should
821
sourceobj, path = uri2obj(source)
823
# we'll set this manually as necessary
824
if @arghash.include?(:ensure)
825
@arghash.delete(:ensure)
828
# okay, we've got our source object; now we need to
829
# build up a local file structure to match the remote
832
server = sourceobj.server
834
if state = self.state(:checksum)
844
ignore = self[:ignore]
846
desc = server.list(path, self[:links], r, ignore)
848
# Now create a new child for every file returned in the list.
849
desc.split("\n").each { |line|
850
file, type = line.split("\t")
851
next if file == "/" # skip the listing object
852
name = file.sub(/^\//, '')
853
args = {:source => source + file}
858
self.newchild(name, false, args)
862
# Set the checksum, from another state. There are multiple states that
863
# modify the contents of a file, and they need the ability to make sure
864
# that the checksum value is in sync.
865
def setchecksum(sum = nil)
866
if @states.include? :checksum
868
@states[:checksum].checksum = sum
870
# If they didn't pass in a sum, then tell checksum to
872
@states[:checksum].retrieve
873
@states[:checksum].checksum = @states[:checksum].is
878
# Stat our file. Depending on the value of the 'links' attribute, we use
879
# either 'stat' or 'lstat', and we expect the states to use the resulting
880
# stat object accordingly (mostly by testing the 'ftype' value).
881
def stat(refresh = false)
884
# Files are the only types that support links
885
if self.class.name == :file and self[:links] != :follow
889
# Just skip them when they don't exist at all.
890
unless FileTest.exists?(path) or FileTest.symlink?(path)
894
if @stat.nil? or refresh == true
896
@stat = File.send(method, self[:path])
897
rescue Errno::ENOENT => error
899
rescue Errno::EACCES => error
900
self.warning "Could not stat; permission denied"
909
sourceobj = FileSource.new
912
devfail "Got a nil source"
915
source = "file://localhost/%s" % URI.escape(source)
916
sourceobj.mount = "localhost"
917
sourceobj.local = true
920
uri = URI.parse(URI.escape(source))
922
self.fail "Could not understand source %s: %s" %
923
[source, detail.to_s]
928
unless defined? @@localfileserver
929
@@localfileserver = Puppet::Server::FileServer.new(
931
:Mount => { "/" => "localhost" },
934
#@@localfileserver.mount("/", "localhost")
936
sourceobj.server = @@localfileserver
937
path = "/localhost" + uri.path
939
args = { :Server => uri.host }
941
args[:Port] = uri.port
943
# FIXME We should cache a copy of this server
944
#sourceobj.server = Puppet::NetworkClient.new(args)
945
unless @clients.include?(source)
946
@clients[source] = Puppet::Client::FileClient.new(args)
948
sourceobj.server = @clients[source]
951
if tmp =~ %r{^/(\w+)}
954
#path = tmp.sub(%r{^/\w+},'') || "/"
956
self.fail "Invalid source path %s" % tmp
959
self.fail "Got other recursive file proto %s from %s" %
963
return [sourceobj, path.sub(/\/\//, '/')]
966
# Write out the file. We open the file correctly, with all of the
967
# uid and mode and such, and then yield the file handle for actual
969
def write(usetmp = true)
970
mode = self.should(:mode)
972
remove_existing(:file)
977
path = self[:path] + ".puppettmp"
982
# As the correct user and group
983
Puppet::SUIDManager.asuser(asuser(), self.should(:group)) do
985
# Open our file with the correct modes
987
Puppet::Util.withumask(000) do
989
File::CREAT|File::WRONLY|File::TRUNC, mode)
992
f = File.open(path, File::CREAT|File::WRONLY|File::TRUNC)
1002
# And put our new file in place
1005
File.rename(path, self[:path])
1007
self.err "Could not rename tmp %s for replacing: %s" %
1008
[self[:path], detail]
1010
# Make sure the created file gets removed
1011
if FileTest.exists?(path)
1017
# And then update our checksum, so the next run doesn't find it.
1018
# FIXME This is extra work, because it's going to read the whole
1019
# file back in again.
1022
end # Puppet.type(:pfile)
1024
# the filesource class can't include the path, because the path
1025
# changes for every file instance
1027
attr_accessor :mount, :root, :server, :local
1030
# We put all of the states in separate files, because there are so many
1031
# of them. The order these are loaded is important, because it determines
1032
# the order they are in the state list.
1033
require 'puppet/type/pfile/checksum'
1034
require 'puppet/type/pfile/content' # can create the file
1035
require 'puppet/type/pfile/source' # can create the file
1036
require 'puppet/type/pfile/target'
1037
require 'puppet/type/pfile/ensure' # can create the file
1038
require 'puppet/type/pfile/uid'
1039
require 'puppet/type/pfile/group'
1040
require 'puppet/type/pfile/mode'
1041
require 'puppet/type/pfile/type'
1043
# $Id: pfile.rb 1835 2006-11-08 05:22:24Z luke $