opscode-pushy-client 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +88 -0
  3. data/Gemfile +20 -0
  4. data/Gemfile.lock +242 -0
  5. data/LICENSE +201 -0
  6. data/README.md +43 -0
  7. data/RELEASE_PROCESS.md +105 -0
  8. data/Rakefile +42 -0
  9. data/bin/print_execution_environment +18 -0
  10. data/bin/push-apply +47 -0
  11. data/bin/pushy-client +8 -0
  12. data/bin/pushy-service-manager +19 -0
  13. data/jenkins/jenkins_run_tests.sh +9 -0
  14. data/keys/client_private.pem +27 -0
  15. data/keys/server_public.pem +9 -0
  16. data/lib/pushy_client.rb +268 -0
  17. data/lib/pushy_client/cli.rb +168 -0
  18. data/lib/pushy_client/heartbeater.rb +153 -0
  19. data/lib/pushy_client/job_runner.rb +316 -0
  20. data/lib/pushy_client/periodic_reconfigurer.rb +62 -0
  21. data/lib/pushy_client/protocol_handler.rb +508 -0
  22. data/lib/pushy_client/version.rb +23 -0
  23. data/lib/pushy_client/whitelist.rb +66 -0
  24. data/lib/pushy_client/win32.rb +27 -0
  25. data/lib/pushy_client/windows_service.rb +253 -0
  26. data/omnibus/Berksfile +12 -0
  27. data/omnibus/Gemfile +15 -0
  28. data/omnibus/Gemfile.lock +232 -0
  29. data/omnibus/LICENSE +201 -0
  30. data/omnibus/README.md +141 -0
  31. data/omnibus/acceptance/Berksfile +6 -0
  32. data/omnibus/acceptance/Berksfile.lock +35 -0
  33. data/omnibus/acceptance/Makefile +13 -0
  34. data/omnibus/acceptance/README.md +29 -0
  35. data/omnibus/acceptance/metadata.rb +12 -0
  36. data/omnibus/acceptance/recipes/chef-server-user-org.rb +31 -0
  37. data/omnibus/config/projects/push-jobs-client.rb +83 -0
  38. data/omnibus/config/software/opscode-pushy-client.rb +78 -0
  39. data/omnibus/files/mapfiles/solaris +18 -0
  40. data/omnibus/files/openssl-customization/windows/ssl_env_hack.rb +34 -0
  41. data/omnibus/omnibus.rb +54 -0
  42. data/omnibus/package-scripts/push-jobs-client/postinst +55 -0
  43. data/omnibus/package-scripts/push-jobs-client/postrm +39 -0
  44. data/omnibus/resources/push-jobs-client/dmg/background.png +0 -0
  45. data/omnibus/resources/push-jobs-client/dmg/icon.png +0 -0
  46. data/omnibus/resources/push-jobs-client/msi/assets/LICENSE.rtf +197 -0
  47. data/omnibus/resources/push-jobs-client/msi/assets/banner_background.bmp +0 -0
  48. data/omnibus/resources/push-jobs-client/msi/assets/dialog_background.bmp +0 -0
  49. data/omnibus/resources/push-jobs-client/msi/assets/oc.ico +0 -0
  50. data/omnibus/resources/push-jobs-client/msi/assets/oc_16x16.ico +0 -0
  51. data/omnibus/resources/push-jobs-client/msi/assets/oc_32x32.ico +0 -0
  52. data/omnibus/resources/push-jobs-client/msi/localization-en-us.wxl.erb +26 -0
  53. data/omnibus/resources/push-jobs-client/msi/parameters.wxi.erb +9 -0
  54. data/omnibus/resources/push-jobs-client/msi/source.wxs.erb +138 -0
  55. data/omnibus/resources/push-jobs-client/pkg/background.png +0 -0
  56. data/omnibus/resources/push-jobs-client/pkg/license.html.erb +202 -0
  57. data/omnibus/resources/push-jobs-client/pkg/welcome.html.erb +5 -0
  58. data/opscode-pushy-client.gemspec +28 -0
  59. data/pkg/opscode-pushy-client-2.3.0.gem +0 -0
  60. data/spec/pushy_client/protocol_handler_spec.rb +48 -0
  61. data/spec/pushy_client/whitelist_spec.rb +70 -0
  62. data/spec/spec_helper.rb +12 -0
  63. metadata +235 -0
@@ -0,0 +1,168 @@
1
+ # @copyright Copyright 2014 Chef Software, Inc. All Rights Reserved.
2
+ #
3
+ # This file is provided to you under the Apache License,
4
+ # Version 2.0 (the "License"); you may not use this file
5
+ # except in compliance with the License. You may obtain
6
+ # a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing,
11
+ # software distributed under the License is distributed on an
12
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13
+ # KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations
15
+ # under the License.
16
+ #
17
+
18
+ require 'chef/application'
19
+ require 'chef/config'
20
+ # This is needed for compat with chef-client >= 11.8.0.
21
+ # To keep compat with older chef-client, rescue if not found
22
+ require 'chef/config_fetcher' rescue 'assuming chef-client < 11.8.0'
23
+ require 'chef/log'
24
+ require_relative '../pushy_client'
25
+ require_relative '../pushy_client/version'
26
+
27
+ class PushyClient
28
+ class CLI < Chef::Application
29
+
30
+ def self.find_default_config
31
+ configs = ['chef-push-client.rb', 'push-jobs-client.rb', 'client.rb']
32
+ base = "/etc/chef"
33
+ paths = configs.map {|c| Chef::Config.platform_specific_path(File.join(base, c)) }
34
+ path = paths.detect {|p| File.exist?(p) }
35
+ # todo make debug before commit
36
+ Chef::Log.info("Push Client using default config file path: '#{path}'")
37
+ path
38
+ end
39
+
40
+ option :config_file,
41
+ :short => "-c CONFIG",
42
+ :long => "--config CONFIG",
43
+ :default => find_default_config,
44
+ :description => "The configuration file to use"
45
+
46
+ option :log_level,
47
+ :short => "-l LEVEL",
48
+ :long => "--log_level LEVEL",
49
+ :description => "Set the log level (debug, info, warn, error, fatal)",
50
+ :proc => lambda { |l| l.to_sym }
51
+
52
+ option :log_location,
53
+ :short => "-L LOGLOCATION",
54
+ :long => "--logfile LOGLOCATION",
55
+ :description => "Set the log file location, defaults to STDOUT - recommended for daemonizing",
56
+ :proc => nil
57
+
58
+ option :help,
59
+ :short => "-h",
60
+ :long => "--help",
61
+ :description => "Show this message",
62
+ :on => :tail,
63
+ :boolean => true,
64
+ :show_options => true,
65
+ :exit => 0
66
+
67
+ option :node_name,
68
+ :short => "-N NODE_NAME",
69
+ :long => "--node-name NODE_NAME",
70
+ :description => "The node name for this client",
71
+ :proc => nil
72
+
73
+ option :chef_server_url,
74
+ :short => "-S CHEFSERVERURL",
75
+ :long => "--server CHEFSERVERURL",
76
+ :description => "The chef server URL",
77
+ :proc => nil
78
+
79
+ option :client_key,
80
+ :short => "-k KEY_FILE",
81
+ :long => "--client_key KEY_FILE",
82
+ :description => "Set the client key file location",
83
+ :proc => nil
84
+
85
+ option :version,
86
+ :short => "-v",
87
+ :long => "--version",
88
+ :description => "Show push client version",
89
+ :boolean => true,
90
+ :proc => lambda {|v| puts "Push Client: #{::PushyClient::VERSION}"},
91
+ :exit => 0
92
+
93
+ option :file_dir,
94
+ :short => "-d DIR",
95
+ :long => "--file_dir DIR",
96
+ :description => "Set the directory for temporary files",
97
+ :default => "/tmp/chef-push",
98
+ :proc => nil
99
+
100
+ option :allow_unencrypted,
101
+ :long => "--allow_unencrypted",
102
+ :boolean => true,
103
+ :description => "Allow unencrypted connections to 1.x servers"
104
+
105
+ def reconfigure
106
+ # We do not use Chef's formatters.
107
+ Chef::Config[:force_logger] = true
108
+ super
109
+ end
110
+
111
+ def setup_application
112
+ end
113
+
114
+ def shutdown(ret_code = 0)
115
+ @client.stop if @client
116
+ exit(ret_code)
117
+ end
118
+
119
+ def run_application
120
+ if Chef::Config[:version]
121
+ puts "Push Client version: #{::PushyClient::VERSION}"
122
+ end
123
+
124
+ ohai = Ohai::System.new
125
+ ohai.load_plugins
126
+ ohai.run_plugins(true, ['hostname'])
127
+
128
+ @client = PushyClient.new(
129
+ :chef_server_url => Chef::Config[:chef_server_url],
130
+ :client_key => Chef::Config[:client_key],
131
+ :node_name => Chef::Config[:node_name] || ohai[:fqdn] || ohai[:hostname],
132
+ :whitelist => Chef::Config[:whitelist] || { 'chef-client' => 'chef-client' },
133
+ :hostname => ohai[:hostname],
134
+ :filedir => Chef::Config[:file_dir],
135
+ :allow_unencrypted => Chef::Config[:allow_unencrypted]
136
+ )
137
+
138
+ @client.start
139
+
140
+ # install signal handlers
141
+ # Windows does not support QUIT and USR1 signals
142
+ exit_signals = if Chef::Platform.windows?
143
+ ["TERM", "INT"]
144
+ else
145
+ ["TERM", "QUIT", "INT"]
146
+ end
147
+
148
+ exit_signals.each do |sig|
149
+ Signal.trap(sig) do
150
+ puts "received #{sig}, shutting down"
151
+ shutdown(0)
152
+ end
153
+ end
154
+
155
+ unless Chef::Platform.windows?
156
+ Signal.trap("USR1") do
157
+ puts "received USR1, reconfiguring"
158
+ @client.trigger_reconfigure
159
+ end
160
+ end
161
+
162
+ # Block forever so that client threads can run
163
+ while true
164
+ sleep 3600
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,153 @@
1
+ # @copyright Copyright 2014 Chef Software, Inc. All Rights Reserved.
2
+ #
3
+ # This file is provided to you under the Apache License,
4
+ # Version 2.0 (the "License"); you may not use this file
5
+ # except in compliance with the License. You may obtain
6
+ # a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing,
11
+ # software distributed under the License is distributed on an
12
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13
+ # KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations
15
+ # under the License.
16
+ #
17
+
18
+ class PushyClient
19
+ class Heartbeater
20
+ NUM_HEARTBEATS_TO_LOG = 3
21
+
22
+ def initialize(client)
23
+ @client = client
24
+ @online_mutex = Mutex.new
25
+ @heartbeat_sequence = 1
26
+ @on_server_availability_change = []
27
+ end
28
+
29
+ attr_reader :client
30
+ attr_reader :incarnation_id
31
+ attr_reader :online_threshold
32
+ attr_reader :offline_threshold
33
+ attr_reader :interval
34
+
35
+ def node_name
36
+ client.node_name
37
+ end
38
+
39
+ def online?
40
+ @online
41
+ end
42
+
43
+ def on_server_availability_change(&block)
44
+ @on_server_availability_change << block
45
+ end
46
+
47
+ def start
48
+ @incarnation_id = client.config['incarnation_id']
49
+ @online_threshold = client.config['push_jobs']['heartbeat']['online_threshold']
50
+ @offline_threshold = client.config['push_jobs']['heartbeat']['offline_threshold']
51
+ @interval = client.config['push_jobs']['heartbeat']['interval']
52
+
53
+ @online_counter = 0
54
+ @offline_counter = 0
55
+ # We optimistically declare the server online since we just got a config blob via http from it
56
+ # however, if the server is reachable via http but not zmq we'll go down after a few heartbeats.
57
+ set_online(true)
58
+
59
+ @heartbeat_thread = Thread.new do
60
+ Chef::Log.info "[#{node_name}] Starting heartbeat / offline detection thread on interval #{interval} ..."
61
+
62
+ while true
63
+ begin
64
+ # When the server goes more than <offline_threshold> intervals
65
+ # without sending us a heartbeat, treat it as offline
66
+ @online_mutex.synchronize do
67
+ if @online
68
+ if @offline_counter > offline_threshold
69
+ Chef::Log.info "[#{node_name}] Server has missed #{@offline_counter} heartbeats in a row. Considering it offline, and stopping heartbeat."
70
+ set_online(false)
71
+ @online_counter = 0
72
+ else
73
+ @offline_counter += 1
74
+ end
75
+ end
76
+ end
77
+
78
+ # We only send heartbeats to online servers
79
+ if @online
80
+ client.send_heartbeat(@heartbeat_sequence)
81
+ if @heartbeat_sequence <= NUM_HEARTBEATS_TO_LOG
82
+ Chef::Log.info "[#{node_name}] Sending heartbeat #{@heartbeat_sequence} (logging first #{NUM_HEARTBEATS_TO_LOG})"
83
+ else
84
+ Chef::Log.debug "[#{node_name}] Sending heartbeat #{@heartbeat_sequence}"
85
+ end
86
+ @heartbeat_sequence += 1
87
+ end
88
+ sleep(interval)
89
+ rescue
90
+ client.log_exception("Error in heartbeat / offline detection thread", $!)
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ def stop
97
+ Chef::Log.info "[#{node_name}] Stopping heartbeat / offline detection thread ..."
98
+ @heartbeat_thread.kill
99
+ @heartbeat_thread.join
100
+ end
101
+
102
+ def reconfigure
103
+ stop
104
+ start # Start picks up new configuration
105
+ end
106
+
107
+ # TODO use the sequence for something?
108
+ def heartbeat_received(incarnation_id, sequence)
109
+ message = "[#{node_name}] Received server heartbeat (sequence ##{sequence})"
110
+ if @online_counter <= NUM_HEARTBEATS_TO_LOG
111
+ Chef::Log.info message + " logging #{@online_counter}/#{NUM_HEARTBEATS_TO_LOG}"
112
+ else
113
+ Chef::Log.debug message
114
+ end
115
+ # If the incarnation id has changed, we need to reconfigure.
116
+ if @incarnation_id != incarnation_id
117
+ if @incarnation_id.nil?
118
+ @incarnation_id = incarnation_id
119
+ Chef::Log.info "[#{node_name}] First heartbeat received. Server is at incarnation ID #{incarnation_id}."
120
+ else
121
+ # We need to set incarnation id before we reconfigure; this thread will
122
+ # be killed by the reconfigure :)
123
+ splay = Random.new.rand(interval.to_f)
124
+ Chef::Log.info "[#{node_name}] Server restart detected (incarnation ID changed from #{@incarnation_id} to #{incarnation_id}). Reconfiguring after a randomly chosen #{splay} second delay to avoid storming the server ..."
125
+ @incarnation_id = incarnation_id
126
+ sleep(splay)
127
+ client.trigger_reconfigure
128
+ end
129
+ end
130
+
131
+ @online_mutex.synchronize do
132
+ @offline_counter = 0
133
+
134
+ if !@online && @online_counter > online_threshold
135
+ Chef::Log.info "[#{node_name}] Server has heartbeated #{@online_counter} times without missing more than #{offline_threshold} heartbeats in a row."
136
+ set_online(true)
137
+ else
138
+ @online_counter += 1
139
+ end
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ def set_online(online)
146
+ @online = online
147
+ Chef::Log.info "[#{node_name}] Considering server online, and starting to heartbeat"
148
+ @on_server_availability_change.each do |block|
149
+ block.call(online)
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,316 @@
1
+ # @copyright Copyright 2014 Chef Software, Inc. All Rights Reserved.
2
+ #
3
+ # This file is provided to you under the Apache License,
4
+ # Version 2.0 (the "License"); you may not use this file
5
+ # except in compliance with the License. You may obtain
6
+ # a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing,
11
+ # software distributed under the License is distributed on an
12
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13
+ # KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations
15
+ # under the License.
16
+ #
17
+
18
+ # This is needed to fix an issue in win32-process v. 0.6.5
19
+ # where Process.wait blocks the entire Ruby interpreter
20
+ # for the duration of the process.
21
+ require 'chef/platform'
22
+ require 'mixlib/shellout'
23
+ if Chef::Platform.windows?
24
+ require 'pushy_client/win32'
25
+ end
26
+
27
+ class PushyClient
28
+ class JobRunner
29
+ def initialize(client)
30
+ @client = client
31
+ @on_job_state_change = []
32
+
33
+ set_job_state(:idle)
34
+ @pid = nil
35
+ @process_thread = nil
36
+
37
+ # Keep job state and process state in sync
38
+ @state_lock = Mutex.new
39
+ end
40
+
41
+ attr_reader :client
42
+ attr_reader :state
43
+ attr_reader :job_id
44
+ attr_reader :command
45
+ attr_reader :lockfile
46
+
47
+ def safe_to_reconfigure?
48
+ @state_lock.synchronize do
49
+ @state == :idle
50
+ end
51
+ end
52
+
53
+ def node_name
54
+ client.node_name
55
+ end
56
+
57
+ def start
58
+ end
59
+
60
+ def stop
61
+ if @state == :running
62
+ kill_process
63
+ end
64
+ set_job_state(:idle)
65
+ end
66
+
67
+ def reconfigure
68
+ # We have no configuration, and keep state between reconfigures
69
+ end
70
+
71
+ def commit(job_id, command, opts)
72
+ @opts = opts
73
+ @state_lock.synchronize do
74
+ if @state == :idle
75
+ # If we're being asked to lock
76
+ if client.whitelist[command] &&
77
+ client.whitelist[command].is_a?(Hash) &&
78
+ client.whitelist[command][:lock]
79
+ # If the command is chef-client
80
+ # We don't want to run if there is already another instance of chef-client going,
81
+ # so we check to see if there is a runlock on chef-client before committing. This
82
+ # currently only works in versions of chef where runlock has been implemented.
83
+
84
+ # The location of our lockfile
85
+ if client.whitelist[command][:lock] == true
86
+ lockfile_location = Chef::Config[:lockfile] || "#{Chef::Config[:file_cache_path]}/chef-client-running.pid"
87
+ else
88
+ lockfile_location = client.whitelist[command][:lock]
89
+ end
90
+ # Open the Lockfile
91
+ begin
92
+ @lockfile = File.open(lockfile_location, 'w')
93
+ locked = lockfile.flock(File::LOCK_EX|File::LOCK_NB)
94
+ unless locked
95
+ Chef::Log.info("[#{node_name}] Received commit #{job_id} but is already running '#{command}'")
96
+ client.send_command(:nack_commit, job_id)
97
+ return false
98
+ end
99
+ rescue Errno::ENOENT
100
+ end
101
+ elsif client.whitelist[command]
102
+ user_ok = check_user(job_id)
103
+ dir_ok = check_dir(job_id)
104
+ file_ok = check_file(job_id)
105
+ if user_ok && dir_ok && file_ok
106
+ Chef::Log.info("[#{node_name}] Received commit #{job_id}")
107
+ set_job_state(:committed, job_id, command)
108
+ client.send_command(:ack_commit, job_id)
109
+ true
110
+ else
111
+ client.send_command(:nack_commit, job_id)
112
+ end
113
+ else
114
+ Chef::Log.error("[#{node_name}] Received commit #{job_id}, but command '#{command}' is not in the whitelist!")
115
+ client.send_command(:nack_commit, job_id)
116
+ false
117
+ end
118
+ else
119
+ Chef::Log.warn("[#{node_name}] Received commit #{job_id} but current state is #{@state} #{@job_id}")
120
+ client.send_command(:nack_commit, job_id)
121
+ false
122
+ end
123
+ end
124
+ end
125
+
126
+ def run(job_id)
127
+ @state_lock.synchronize do
128
+ if @state == :committed && @job_id == job_id
129
+ Chef::Log.info("[#{node_name}] Received run #{job_id}")
130
+ pid, process_thread = start_process
131
+ set_job_state(:running, job_id, @command, pid, process_thread)
132
+ client.send_command(:ack_run, job_id)
133
+ true
134
+ else
135
+ Chef::Log.warn("[#{node_name}] Received run #{job_id} but current state is #{@state} #{@job_id}")
136
+ client.send_command(:nack_run, job_id)
137
+ false
138
+ end
139
+ end
140
+ end
141
+
142
+ def abort
143
+ Chef::Log.info("[#{node_name}] Received abort")
144
+ @state_lock.synchronize do
145
+ _job_id = job_id
146
+ stop
147
+ client.send_command(:aborted, _job_id)
148
+ end
149
+ end
150
+
151
+ def job_state
152
+ @state_lock.synchronize do
153
+ get_job_state
154
+ end
155
+ end
156
+
157
+ def on_job_state_change(&block)
158
+ @on_job_state_change << block
159
+ end
160
+
161
+ private
162
+
163
+ def get_job_state
164
+ {
165
+ :state => @state,
166
+ :job_id => @job_id,
167
+ :command => @command
168
+ }
169
+ end
170
+
171
+ def set_job_state(state, job_id = nil, command = nil, pid = nil, process_thread = nil)
172
+ if state == :idle || state == :running
173
+ if @lockfile
174
+ # If there is a lockfile Release the lock to allow chef-client to run
175
+ lockfile.flock(File::LOCK_UN)
176
+ lockfile.close
177
+ end
178
+ end
179
+ @state = state
180
+ @job_id = job_id
181
+ @command = command
182
+ @pid = pid
183
+ @process_thread = process_thread
184
+
185
+ Chef::Log.debug("[] Job #{job_id}: command '#{command}' state '#{state}'")
186
+
187
+ # Notify people of the change
188
+ @on_job_state_change.each { |block| block.call(get_job_state) }
189
+ end
190
+
191
+ def completed(job_id, exit_code, stdout, stderr)
192
+ Chef::Log.info("[#{node_name}] Job #{job_id} completed with exit code #{exit_code}")
193
+ @state_lock.synchronize do
194
+ if @state == :running && @job_id == job_id
195
+ set_job_state(:idle)
196
+ status = exit_code == 0 ? :succeeded : :failed
197
+ params = {}
198
+ params[:stdout] = stdout if stdout
199
+ params[:stderr] = stderr if stderr
200
+ client.send_command(status, job_id, params)
201
+ end
202
+ end
203
+ end
204
+
205
+ def start_process
206
+ # _pid and _job_id are local variables so that if @pid or @job_id change
207
+ # for any reason (for example, they become nil), the thread we create
208
+ # still tracks the correct pid.
209
+ if client.whitelist[command].is_a?(Hash)
210
+ command_line = client.whitelist[command][:command_line]
211
+ else
212
+ command_line = client.whitelist[command]
213
+ end
214
+ user = @opts['user']
215
+ dir = @opts['dir']
216
+ env = @opts['env'] || {}
217
+ capture = @opts['capture'] || false
218
+ path = extract_file
219
+ env.merge!({'CHEF_PUSH_JOB_FILE' => path}) if path
220
+ std_env = {'CHEF_PUSH_NODE_NAME' => node_name, 'CHEF_PUSH_JOB_ID' => @job_id}
221
+ env.merge!(std_env)
222
+ # XXX We set the timeout to 86400, because the time in ShellOut is
223
+ # 60 seconds, and that might be too slow. But we currently don't
224
+ # have the timeout from the pushy-server. Instead of changing it from
225
+ # a hard-coded value to a config option, we should expand the protocol
226
+ # to support sending the timeout.
227
+ command = Mixlib::ShellOut.new(command_line,
228
+ :user => user,
229
+ :cwd => dir,
230
+ :env => env,
231
+ :timeout => 86400)
232
+ _job_id = @job_id
233
+ # Can't get the _pid from the ShellOut command. So
234
+ # we can't kill it, either.
235
+ _pid = nil
236
+ Chef::Log.info("[#{node_name}] Job #{job_id}: started command '#{command_line}' with PID '#{_pid}'")
237
+
238
+ # Wait for the job to complete and close it out.
239
+ process_thread = Thread.new do
240
+ begin
241
+ command.run_command
242
+ stdout = command.stdout if capture
243
+ stderr = command.stderr if capture
244
+ completed(_job_id, command.status.exitstatus, stdout, stderr)
245
+ rescue
246
+ client.log_exception("Exception raised while waiting for job #{_job_id} to complete", $!)
247
+ abort
248
+ end
249
+ end
250
+
251
+ [ _pid, process_thread ]
252
+ end
253
+
254
+ def kill_process
255
+ Chef::Log.info("[#{node_name}] Killing process #{@pid}")
256
+ @process_thread.kill
257
+ @process_thread.join
258
+ begin
259
+ Process.kill(1, @pid) if @pid
260
+ rescue
261
+ client.log_exception("Exception in Process.kill(1, #{@pid})", $!)
262
+ end
263
+ end
264
+
265
+ def check_user(job_id)
266
+ user = @opts['user']
267
+ if user
268
+ begin
269
+ Etc.getpwnam(user)
270
+ true
271
+ rescue
272
+ Chef::Log.error("[#{node_name}] Received commit #{job_id}, but user '#{user}' does not exist!")
273
+ false
274
+ end
275
+ else
276
+ true
277
+ end
278
+ end
279
+
280
+ def check_dir(job_id)
281
+ # XX Perhaps should be stricted, e.g. forking a process to actually try to chdir
282
+ dir = @opts['dir']
283
+ dir_ok = !dir || Dir.exists?(dir)
284
+ Chef::Log.error("[#{node_name}] Received commit #{job_id}, but dir '#{dir}' does not exist!") unless dir_ok
285
+ dir_ok
286
+ end
287
+
288
+ def check_file(job_id)
289
+ file = @opts['file']
290
+ file_ok = !file || file.start_with?('base64:', 'raw:')
291
+ Chef::Log.error("[#{node_name}] Received commit #{job_id}, but file '#{file}' is a bad format!") unless file_ok
292
+ file_ok
293
+ end
294
+
295
+ def extract_file
296
+ file = @opts['file']
297
+ return nil unless file
298
+ require 'tmpdir'
299
+ dir = client.file_dir
300
+ Dir.mkdir(dir) unless Dir.exists?(dir)
301
+ path = Dir::Tmpname.create('pushy_file', dir){|p| p}
302
+ File.open(path, 'w') do |f|
303
+ type, filedata = file.split(/:/, 2)
304
+ case type
305
+ when "raw"
306
+ f.write(filedata)
307
+ when "base64"
308
+ f.write(Base64.decode64(filedata))
309
+ else
310
+ Chef::Log.error("[#{node_name}] Received commit #{job_id}, but file starting with '#{file.slice(0,80)}' has a bad format!")
311
+ end
312
+ end
313
+ path
314
+ end
315
+ end
316
+ end