-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
(PUP-6675) Use pipes instead of temporary files for Puppet exec #5844
Conversation
I think this works, and it solves the problem I'm trying to fix. However, it's pretty fundamental and I'm still in the process of testing it. |
CLA signed by all contributors. |
Test build available at http://builds.puppetlabs.lan/puppet-agent/f4d9c43839580f89c3313601d316ab77ef89c733. |
Added some testing via pxp-agent and mcollective: puppetlabs/pxp-agent#583, https://github.com/puppetlabs/marionette-collective/pull/433 |
lib/puppet/util/execution.rb
Outdated
@@ -188,7 +188,8 @@ def self.execute(command, options = NoOptionsSpecified) | |||
|
|||
begin | |||
stdin = Puppet::FileSystem.open(options[:stdinfile] || null_file, nil, 'r') | |||
stdout = options[:squelch] ? Puppet::FileSystem.open(null_file, nil, 'w') : Puppet::FileSystem::Uniquefile.new('puppet') | |||
reader, writer = IO.pipe |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the child needs to close the reader and the parent needs to close the writer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Line 226/227 should be closing the writer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
64KB test fails, need to read output in a loop to clear the buffer before waiting on the process.
fd242cb
to
cec28d7
Compare
I believe this is ready. Kicked off a build at https://jenkins-master-prod-1.delivery.puppetlabs.net/view/puppet-agent/view/ad-hoc/view/vmpooler/job/platform_puppet-agent_pkg-van-ship_ad-hoc-vmpooler-ad-hoc/54/, which includes acceptance tests from this PR, the pxp-agent PR, and the mco PR. |
Manually ran mco tests on RedHat 7
|
pxp-agent and mcollective tests failed on a few platforms due to test setup. I've fixed them, but verifying they now where can be handled after merging this fix. |
pxp-agent's run_puppet_killed_puppet test seems to be a real issue caused by this change. Looking into it. |
Wow, looks like the original Windows / Posix split happened in cb53870 Though the original Tempfile usage stems from f8c1b08
|
The original Tempfile usage is actually 30ebbc9. Looks like they attempted to fix it, which led to https://projects.puppetlabs.com/issues/3025. |
Using pipes on Windows seems to cause problems when we test the lockfile after Puppet is killed. |
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint | ||
occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. | ||
EOF | ||
on(agent, "for i in {1..10}; do cat #{testfile} #{testfile} > #{testfile}.2 && mv #{testfile}.2 #{testfile}; done") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just generate 64k+ content on the Ruby side? It will send more over the wire, but will remove the Bash-ism above. There will be a QA undertaking shortly to prune / rewrite tests where possible. The less Bash shows up in tests, the better I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I can rewrite that.
elsif Puppet.features.posix? | ||
child_pid = nil | ||
begin | ||
child_pid = execute_posix(*exec_args) | ||
[stdin, stdout, stderr].each {|io| io.close rescue nil} | ||
unless options[:squelch] | ||
while !reader.eof? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe read
is normally performed within a Ruby Thread
since it's blocking.
You can look at the capture2
source for an example - https://github.com/ruby/ruby/blob/trunk/lib/open3.rb#L305-L322
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can also take a look at the PowerShell module which uses pipes - https://github.com/puppetlabs/puppetlabs-powershell/blob/master/lib/puppet_x/puppetlabs/powershell/powershell_manager.rb#L240-L307
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we're only using a single pipe, I think that's unnecessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Specifically, capture2 is using a thread for read while it also writes to stdin. We're not handling anything except reading from a single pipe, so only a single thread needed.
lib/puppet/util/execution.rb
Outdated
@@ -187,18 +187,30 @@ def self.execute(command, options = NoOptionsSpecified) | |||
null_file = Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null' | |||
|
|||
begin | |||
reader, writer = IO.pipe unless options[:squelch] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a pipe on Windows causes an issue in https://github.com/puppetlabs/pxp-agent/blob/master/acceptance/tests/pxp-module-puppet/run_puppet_killed_puppet.rb.
When that test kills the Puppet process, the child process is intentionally left running. However, the mechanism Puppet uses to determine whether a previous Puppet lockfile is stale reads the Puppet run as active. This appears to be because it's terminated, but still has an open handle to the pipe with the child process (perhaps specifically that it's waiting to read from that pipe).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what to do about that. It's not ideal to be unable to run Puppet again if a previous run was killed but left a long-running or hung process behind, but I'm not sure when that would really come up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Puppet uses Ruby's Process.kill to check whether the process is still running, and that states the process is running if OpenProcess succeeds. Which it appears to as long as the child process has an open handle to the shared pipe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Full explanation of closing a process in https://msdn.microsoft.com/en-us/library/windows/desktop/ms686714(v=vs.85).aspx.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is behavior people depend on in Puppet, so I modified the pxp-agent test to not expect it in puppetlabs/pxp-agent#583.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Iristyle have any concerns here?
Solaris machine died during Puppet acceptance tests, but everything else passed. |
"C:/#{@testfilename}" | ||
else | ||
"/tmp/#{@testfilename}" | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can use host.tmpfile
which IIRC will return a platform-specific path that you can use later in the test. Might also add a teardown for deleting the file.
spec/unit/util/execution_spec.rb
Outdated
Puppet::Util::Execution.execute('test command') | ||
|
||
expect(Puppet::FileSystem.exist?(path)).to be_falsey | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we add a test to make sure both ends of the pipe are closed (during normal run and if an exception is raised)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll try, that seems useful.
|
||
if execution_stub = Puppet::Util::ExecutionStub.current_value | ||
return execution_stub.call(*exec_args) | ||
child_pid = execution_stub.call(*exec_args) | ||
[stdin, stdout, stderr].each {|io| io.close rescue nil} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Puppetserver installs an execution stub, e.g. when executing an autosigning policy. Will it work correctly if we close these descriptors? /cc @camlow325
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should only be closing them on the Puppet side, the forked process inherits the handles. I'm actually not sure we need to close them earlier than we did before, since nothing else about forking changed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already do close them in Puppet Server, although it seems like it would have better if we didn't have to. See this code.
If we add this here, we might get exceptions when the second set of close calls are made. That should be okay with the swallowing rescue
, though, right? Not sure if anything would be written to the log when this happens?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rescue nil should handle that in both cases, and nothing should be logged. The execution doesn't change at all in this case, after execution_stub.call we still would have closed all the handles at old line 226.
lib/puppet/util/execution.rb
Outdated
unless options[:squelch] | ||
output = wait_for_output(stdout) | ||
Puppet.warning "Could not get output" unless output | ||
unless options[:squelch] || read_succeeded |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is read_succeeded
necessary? If options[:squelch]
is false
, then we always call (for posix and windows):
while !reader.eof?
output << reader.read
end
read_succeeded = true
which always sets read_succeeded
to true
or will raise. But if it's the latter, then we never reach this line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There doesn't seem to be a rescue, so I agree I don't see any circumstance where we'd reach this line but reading had failed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might make more sense outside the last ensure block. Update: nevermind, still no rescue that would cause it to trigger.
lib/puppet/util/execution.rb
Outdated
stdout.close! | ||
if !options[:squelch] && reader | ||
# if we opened a pipe, we need to clean it up. | ||
reader.close |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If an exception is raised between the time we open the pipe and when the execute_{posix,windows}
call returns, then the writer is never closed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're misreading what block this ensure relates to. It starts at line 189.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If an exception is raised between lines 190 and 201 (posix) or 207 (windows), then I don't think we close the writer
:
begin
reader, writer = IO.pipe unless options[:squelch]
...
process_info = execute_windows(*exec_args) # if this raises
begin
[stdin, stdout, stderr].each {|io| io.close rescue nil} # this line is never reached
...
end
ensure
if !options[:squelch] && reader
reader.close
end
end
A good test is to try to execute a non-existing program with squelch = false
. On Windows, I know execute_windows
will raise, and I think the posix impl behaves the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That does seem like a loose end. However, as far as I can tell if we never fork and then close one end, the other end also closes. So in that scenario the pipe would still be closed.
That still leaves dangling handles for the files. We could attempt to close all handles in the ensure block anyway.
Do we want this on stable or master @MikaelSmith ? |
I was hoping stable. |
@MikaelSmith In talking this over with the team, it feels too risky of a change to make in a .z without some assurances it won't cause regressions on other platforms. Can we target this for master, and if there are no downstream issues, backport to stable? |
Sure. I'll rebase and retarget. |
This test passes lots of data over pipes when performing an exec, to attempt to catch any issues with buffered reads over pipes. On *nix the standand buffer size is 64KB.
Under selinux, when Puppet is invoked by another process with reduced privileges, any sub-programs invoked by Puppet will not inherit Puppet's selinux priveleges. This specifically causes silent failures when invoking applications that don't normally have the ability to write files - such as iptables-save or hostname - because Puppet redirects their output to a temporary file. Use pipes instead of a temporary file to capture the output of subprocesses.
Done. I'll retarget the pxp-agent and mco acceptance tests to master as well. |
Not sure why it pulled in extra commits for commit checking, but everything else was passing. |
Yeah looks like it didn't update the job parameter for some reason... weird. |
I think the failure is because we're using double-dots in the commit check, so it's pulling in commits on the base branch that were made after you forked this branch, and it looks like the "Revert" commits are causing issues:
I thought we had updated travis and appveyor to use triple dots? |
Weirdly, it kind of looks like we're doing the opposite: https://github.com/puppetlabs/puppet/blob/master/Rakefile#L86. However, I think in this case it was just Github's PR re-targeting being kind of screwy with commit hooks. We've seen stuff like that a lot. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Waiting on appveyor
Reverted in #5868 |
Under selinux, when Puppet is invoked by another process with reduced
privileges, any sub-programs invoked by Puppet will not inherit Puppet's
selinux priveleges. This specifically causes silent failures when
invoking applications that don't normally have the ability to write
files - such as iptables-save or hostname - because Puppet redirects
their output to a temporary file.
Use pipes instead of a temporary file to capture the output of
subprocesses.