Skip to content

Commit 9ea2d74

Browse files
committed
Merge branch 'ticket/master/19447-windows-symlinks'
* ticket/master/19447-windows-symlinks: (#19447) Skip symlink test if provider doesn't support it (#19447) Rescue when symlink privilege does not exist (#19447) Fully qualify access to ruby `File` (#19447) Fall back to stat if we don't support symlinks (#19447) Check for symlink permission only if we try to create one Maint: Re-word doc string for file type's ensure property (#19447) Rewrite symlink tests for file type (#19447) check Puppet.features.manages_symlinks? (#19447) Windows symlink check process token (#19447) Puppet::FileSystem::File Windows symlink Closes GH-1997
2 parents ca377b7 + 75fea61 commit 9ea2d74

File tree

20 files changed

+1106
-86
lines changed

20 files changed

+1106
-86
lines changed
Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,37 @@
11
test_name "should create symlink"
2-
confine :except, :platform => 'windows'
32

43
message = 'hello world'
5-
target = "/tmp/test-#{Time.new.to_i}"
6-
source = "/tmp/test-#{Time.new.to_i}-source"
7-
84
agents.each do |agent|
5+
confine_block :to, :platform => 'windows' do
6+
# symlinks are supported only on Vista+ (version 6.0 and higher)
7+
on agents, facter('kernelmajversion') do
8+
skip_test "Test not supported on this plaform" if stdout.chomp.to_f < 6.0
9+
end
10+
end
11+
12+
link = agent.tmpfile("symlink-link")
13+
target = agent.tmpfile("symlink-target")
14+
915
step "clean up the system before we begin"
10-
on agent, "rm -rf #{target}"
11-
on agent, "echo '#{message}' > #{source}"
16+
on agent, "rm -rf #{target} #{link}"
17+
on agent, "echo '#{message}' > #{target}"
1218

1319
step "verify we can create a symlink"
14-
on(agent, puppet_resource("file", target, "ensure=#{source}"))
20+
on(agent, puppet_resource("file", link, "ensure=#{target}"))
1521

1622
step "verify the symlink was created"
17-
on agent, "test -L #{target} && test -f #{target}"
18-
step "verify source file"
19-
on agent, "test -f #{source}"
23+
on agent, "test -L #{link} && test -f #{link}"
24+
step "verify the symlink points to a file"
25+
on agent, "test -f #{target}"
2026

2127
step "verify the content is identical on both sides"
22-
on(agent, "cat #{source}") do
23-
fail_test "source missing content" unless stdout.include? message
28+
on(agent, "cat #{link}") do
29+
fail_test "link missing content" unless stdout.include? message
2430
end
2531
on(agent, "cat #{target}") do
2632
fail_test "target missing content" unless stdout.include? message
2733
end
2834

2935
step "clean up after the test run"
30-
on agent, "rm -rf #{target} #{source}"
36+
on agent, "rm -rf #{target} #{link}"
3137
end

acceptance/tests/resource/file/ticket_7680-follow-symlinks.rb

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,28 @@
11
test_name "#7680: 'links => follow' should use the file source content"
2-
confine :except, :platform => 'windows'
32

43
agents.each do |agent|
4+
confine_block :to, :platform => 'windows' do
5+
# symlinks are supported only on Vista+ (version 6.0 and higher)
6+
on agents, facter('kernelmajversion') do
7+
skip_test "Test not supported on this plaform" if stdout.chomp.to_f < 6.0
8+
end
9+
end
510

611
step "Create file content"
712
real_source = agent.tmpfile('follow_links_source')
813
dest = agent.tmpfile('follow_links_dest')
914
symlink = agent.tmpfile('follow_links_symlink')
1015

1116
on agent, "echo 'This is the real content' > #{real_source}"
12-
on agent, "ln -sf #{real_source} #{symlink}"
17+
if agent['platform'].include?('windows')
18+
# cygwin ln doesn't behave properly, fallback to mklink,
19+
# but that requires backslashes, that need to be escaped,
20+
# and the link cannot exist prior.
21+
on agent, "rm -f #{symlink}"
22+
on agent, "cmd /c mklink #{symlink.gsub('/', '\\\\\\\\')} #{real_source.gsub('/', '\\\\\\\\')}"
23+
else
24+
on agent, "ln -sf #{real_source} #{symlink}"
25+
end
1326

1427
manifest = <<-MANIFEST
1528
file { '#{dest}':

lib/puppet/feature/base.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,16 @@
7373

7474
# We can manage symlinks
7575
Puppet.features.add(:manages_symlinks) do
76-
! Puppet::Util::Platform.windows?
76+
if ! Puppet::Util::Platform.windows?
77+
true
78+
else
79+
begin
80+
require 'Win32API'
81+
Win32API.new('kernel32', 'CreateSymbolicLink', 'SSL', 'B')
82+
true
83+
rescue LoadError => err
84+
Puppet.debug("CreateSymbolicLink is not available")
85+
false
86+
end
87+
end
7788
end

lib/puppet/file_system/file.rb

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ class Puppet::FileSystem::File
1010
IMPL = if RUBY_VERSION =~ /^1\.8/
1111
require 'puppet/file_system/file18'
1212
Puppet::FileSystem::File18
13+
elsif Puppet::Util::Platform.windows?
14+
require 'puppet/file_system/file19windows'
15+
Puppet::FileSystem::File19Windows
1316
else
1417
require 'puppet/file_system/file19'
1518
Puppet::FileSystem::File19
@@ -104,6 +107,7 @@ def binread
104107
#
105108
# @return [Boolean] true if the named file exists.
106109
def self.exist?(path)
110+
return IMPL.exist?(path) if IMPL.method(:exist?) != self.method(:exist?)
107111
File.exist?(path)
108112
end
109113

@@ -131,18 +135,28 @@ def mkpath
131135
@path.mkpath
132136
end
133137

134-
# Creates a symbolic link dest which points to the current file. If dest
135-
# already exists and it is a directory, creates a symbolic link dest/the
136-
# current file. If dest already exists and it is not a directory,
137-
# raises Errno::EEXIST. But if :force option is set, overwrite dest.
138+
# Creates a symbolic link dest which points to the current file.
139+
# If dest already exists:
138140
#
139-
# @param dest [String] The mode to apply to the file if it is created
140-
# @param [Hash] options the options to create a message with.
141+
# * and is a file, will raise Errno::EEXIST
142+
# * and is a directory, will return 0 but perform no action
143+
# * and is a symlink referencing a file, will raise Errno::EEXIST
144+
# * and is a symlink referencing a directory, will return 0 but perform no action
145+
#
146+
# With the :force option set to true, when dest already exists:
147+
#
148+
# * and is a file, will replace the existing file with a symlink (DANGEROUS)
149+
# * and is a directory, will return 0 but perform no action
150+
# * and is a symlink referencing a file, will modify the existing symlink
151+
# * and is a symlink referencing a directory, will return 0 but perform no action
152+
#
153+
# @param dest [String] The path to create the new symlink at
154+
# @param [Hash] options the options to create the symlink with
141155
# @option options [Boolean] :force overwrite dest
142156
# @option options [Boolean] :noop do not perform the operation
143157
# @option options [Boolean] :verbose verbose output
144158
#
145-
# @raise [Errno::EEXIST] dest already exists and it is not a directory
159+
# @raise [Errno::EEXIST] dest already exists as a file and, :force is not set
146160
#
147161
# @return [Integer] 0
148162
def symlink(dest, options = {})
@@ -166,6 +180,7 @@ def readlink
166180
#
167181
# @return [Integer] the number of names passed as arguments
168182
def self.unlink(*file_names)
183+
return IMPL.unlink(*file_names) if IMPL.method(:unlink) != self.method(:unlink)
169184
File.unlink(*file_names)
170185
end
171186

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
require 'puppet/file_system/file19'
2+
require 'puppet/util/windows'
3+
4+
class Puppet::FileSystem::File19Windows < Puppet::FileSystem::File19
5+
6+
def self.exist?(path)
7+
if ! Puppet.features.manages_symlinks?
8+
return ::File.exist?(path)
9+
end
10+
11+
path = path.to_str if path.respond_to?(:to_str) # support WatchedFile
12+
path = path.to_s # support String and Pathname
13+
14+
begin
15+
if Puppet::Util::Windows::File.symlink?(path)
16+
path = Puppet::Util::Windows::File.readlink(path)
17+
end
18+
! Puppet::Util::Windows::File.stat(path).nil?
19+
rescue # generally INVALID_HANDLE_VALUE which means 'file not found'
20+
false
21+
end
22+
end
23+
24+
def exist?
25+
self.class.exist?(@path)
26+
end
27+
28+
def symlink(dest, options = {})
29+
raise_if_symlinks_unsupported
30+
31+
dest_exists = self.class.exist?(dest) # returns false on dangling symlink
32+
dest_stat = Puppet::Util::Windows::File.stat(dest) if dest_exists
33+
dest_symlink = Puppet::Util::Windows::File.symlink?(dest)
34+
35+
# silent fail to preserve semantics of original FileUtils
36+
return 0 if dest_exists && dest_stat.ftype == 'directory'
37+
38+
if dest_exists && dest_stat.ftype == 'file' && options[:force] != true
39+
raise(Errno::EEXIST, "#{dest} already exists and the :force option was not specified")
40+
end
41+
42+
if options[:noop] != true
43+
::File.delete(dest) if dest_exists # can only be file
44+
Puppet::Util::Windows::File.symlink(@path, dest)
45+
end
46+
47+
0
48+
end
49+
50+
def symlink?
51+
return false if ! Puppet.features.manages_symlinks?
52+
Puppet::Util::Windows::File.symlink?(@path)
53+
end
54+
55+
def readlink
56+
raise_if_symlinks_unsupported
57+
Puppet::Util::Windows::File.readlink(@path)
58+
end
59+
60+
def self.unlink(*file_names)
61+
if ! Puppet.features.manages_symlinks?
62+
return ::File.unlink(*file_names)
63+
end
64+
65+
file_names.each do |file_name|
66+
file_name = file_name.to_s # handle PathName
67+
stat = Puppet::Util::Windows::File.stat(file_name) rescue nil
68+
69+
# sigh, Ruby + Windows :(
70+
if stat && stat.ftype == 'directory'
71+
if Puppet::Util::Windows::File.symlink?(file_name)
72+
Dir.rmdir(file_name)
73+
else
74+
raise Errno::EPERM.new(file_name)
75+
end
76+
else
77+
::File.unlink(file_name)
78+
end
79+
end
80+
81+
file_names.length
82+
end
83+
84+
def unlink
85+
self.class.unlink(@path)
86+
end
87+
88+
def stat
89+
if ! Puppet.features.manages_symlinks?
90+
return super
91+
end
92+
Puppet::Util::Windows::File.stat(@path)
93+
end
94+
95+
def lstat
96+
if ! Puppet.features.manages_symlinks?
97+
return Puppet::Util::Windows::File.stat(@path)
98+
end
99+
Puppet::Util::Windows::File.lstat(@path)
100+
end
101+
102+
private
103+
def raise_if_symlinks_unsupported
104+
if ! Puppet.features.manages_symlinks?
105+
msg = "This version of Windows does not support symlinks. Windows Vista / 2008 or higher is required."
106+
raise Puppet::Util::Windows::Error.new(msg)
107+
end
108+
109+
if ! Puppet::Util::Windows::Process.process_privilege_symlink?
110+
Puppet.warning "The current user does not have the necessary permission to manage symlinks."
111+
end
112+
end
113+
end

lib/puppet/provider/file/windows.rb

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
desc "Uses Microsoft Windows functionality to manage file ownership and permissions."
33

44
confine :operatingsystem => :windows
5+
has_feature :manages_symlinks if Puppet.features.manages_symlinks?
56

67
include Puppet::Util::Warnings
78

@@ -35,33 +36,37 @@ def id2name(id)
3536
alias :name2uid :name2id
3637

3738
def owner
38-
return :absent unless resource.exist?
39+
return :absent unless resource.stat
3940
get_owner(resource[:path])
4041
end
4142

4243
def owner=(should)
4344
begin
44-
set_owner(should, resource[:path])
45+
path = resource[:links] == :manage ? file.path.to_s : file.readlink
46+
47+
set_owner(should, path)
4548
rescue => detail
4649
raise Puppet::Error, "Failed to set owner to '#{should}': #{detail}"
4750
end
4851
end
4952

5053
def group
51-
return :absent unless resource.exist?
54+
return :absent unless resource.stat
5255
get_group(resource[:path])
5356
end
5457

5558
def group=(should)
5659
begin
57-
set_group(should, resource[:path])
60+
path = resource[:links] == :manage ? file.path.to_s : file.readlink
61+
62+
set_group(should, path)
5863
rescue => detail
5964
raise Puppet::Error, "Failed to set group to '#{should}': #{detail}"
6065
end
6166
end
6267

6368
def mode
64-
if resource.exist?
69+
if resource.stat
6570
mode = get_mode(resource[:path])
6671
mode ? mode.to_s(8) : :absent
6772
else
@@ -85,4 +90,10 @@ def validate
8590
resource.fail("Can only manage owner, group, and mode on filesystems that support Windows ACLs, such as NTFS")
8691
end
8792
end
93+
94+
attr_reader :file
95+
private
96+
def file
97+
@file ||= Puppet::FileSystem::File.new(resource[:path])
98+
end
8899
end

lib/puppet/type/file/ensure.rb

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,35 @@ module Puppet
55
require 'puppet/util/symbolic_file_mode'
66
include Puppet::Util::SymbolicFileMode
77

8-
desc <<-'EOT'
9-
Whether to create files that don't currently exist.
10-
Possible values are `absent`, `present`, `file`, `directory`, and `link`.
11-
Specifying `present` will match any form of file existence, and
12-
if the file is missing will create an empty file. Specifying
13-
`absent` will delete the file (or directory, if `recurse => true` and
14-
`force => true`). Specifying `link` requires that you also set the `target`
15-
attribute; note that symlinks cannot be managed on Windows.
16-
17-
If you specify the path to another file as the ensure value, it is
18-
equivalent to specifying `link` and using that path as the `target`:
8+
desc <<-EOT
9+
Whether the file should exist, and if so what kind of file it should be.
10+
Possible values are `present`, `absent`, `file`, `directory`, and `link`.
11+
12+
* `present` will accept any form of file existence, and will create a
13+
normal file if the file is missing. (The file will have no content
14+
unless the `content` or `source` attribute is used.)
15+
* `absent` will make sure the file doesn't exist, deleting it
16+
if necessary.
17+
* `file` will make sure it's a normal file, and enables use of the
18+
`content` or `source` attribute.
19+
* `directory` will make sure it's a directory, and enables use of the
20+
`source`, `recurse`, `recurselimit`, `ignore`, and `purge` attributes.
21+
* `link` will make sure the file is a symlink, and **requires** that you
22+
also set the `target` attribute. Symlinks are supported on all Posix
23+
systems and on Windows Vista / 2008 and higher. On Windows, managing
24+
symlinks requires puppet agent's user account to have the "Create
25+
Symbolic Links" privilege; this can be configured in the "User Rights
26+
Assignment" section in the Windows policy editor. By default, puppet
27+
agent runs as the Administrator account, which does have this privilege.
28+
29+
Puppet avoids destroying directories unless the `force` attribute is set
30+
to `true`. This means that if a file is currently a directory, setting
31+
`ensure` to anything but `directory` or `present` will cause Puppet to
32+
skip managing the resource and log either a notice or an error.
33+
34+
There is one other non-standard value for `ensure`. If you specify the
35+
path to another file as the ensure value, it is equivalent to specifying
36+
`link` and using that path as the `target`:
1937
2038
# Equivalent resources:
2139

0 commit comments

Comments
 (0)