opscode-pushy-client 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
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