opscode-pushy-client 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +88 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +242 -0
- data/LICENSE +201 -0
- data/README.md +43 -0
- data/RELEASE_PROCESS.md +105 -0
- data/Rakefile +42 -0
- data/bin/print_execution_environment +18 -0
- data/bin/push-apply +47 -0
- data/bin/pushy-client +8 -0
- data/bin/pushy-service-manager +19 -0
- data/jenkins/jenkins_run_tests.sh +9 -0
- data/keys/client_private.pem +27 -0
- data/keys/server_public.pem +9 -0
- data/lib/pushy_client.rb +268 -0
- data/lib/pushy_client/cli.rb +168 -0
- data/lib/pushy_client/heartbeater.rb +153 -0
- data/lib/pushy_client/job_runner.rb +316 -0
- data/lib/pushy_client/periodic_reconfigurer.rb +62 -0
- data/lib/pushy_client/protocol_handler.rb +508 -0
- data/lib/pushy_client/version.rb +23 -0
- data/lib/pushy_client/whitelist.rb +66 -0
- data/lib/pushy_client/win32.rb +27 -0
- data/lib/pushy_client/windows_service.rb +253 -0
- data/omnibus/Berksfile +12 -0
- data/omnibus/Gemfile +15 -0
- data/omnibus/Gemfile.lock +232 -0
- data/omnibus/LICENSE +201 -0
- data/omnibus/README.md +141 -0
- data/omnibus/acceptance/Berksfile +6 -0
- data/omnibus/acceptance/Berksfile.lock +35 -0
- data/omnibus/acceptance/Makefile +13 -0
- data/omnibus/acceptance/README.md +29 -0
- data/omnibus/acceptance/metadata.rb +12 -0
- data/omnibus/acceptance/recipes/chef-server-user-org.rb +31 -0
- data/omnibus/config/projects/push-jobs-client.rb +83 -0
- data/omnibus/config/software/opscode-pushy-client.rb +78 -0
- data/omnibus/files/mapfiles/solaris +18 -0
- data/omnibus/files/openssl-customization/windows/ssl_env_hack.rb +34 -0
- data/omnibus/omnibus.rb +54 -0
- data/omnibus/package-scripts/push-jobs-client/postinst +55 -0
- data/omnibus/package-scripts/push-jobs-client/postrm +39 -0
- data/omnibus/resources/push-jobs-client/dmg/background.png +0 -0
- data/omnibus/resources/push-jobs-client/dmg/icon.png +0 -0
- data/omnibus/resources/push-jobs-client/msi/assets/LICENSE.rtf +197 -0
- data/omnibus/resources/push-jobs-client/msi/assets/banner_background.bmp +0 -0
- data/omnibus/resources/push-jobs-client/msi/assets/dialog_background.bmp +0 -0
- data/omnibus/resources/push-jobs-client/msi/assets/oc.ico +0 -0
- data/omnibus/resources/push-jobs-client/msi/assets/oc_16x16.ico +0 -0
- data/omnibus/resources/push-jobs-client/msi/assets/oc_32x32.ico +0 -0
- data/omnibus/resources/push-jobs-client/msi/localization-en-us.wxl.erb +26 -0
- data/omnibus/resources/push-jobs-client/msi/parameters.wxi.erb +9 -0
- data/omnibus/resources/push-jobs-client/msi/source.wxs.erb +138 -0
- data/omnibus/resources/push-jobs-client/pkg/background.png +0 -0
- data/omnibus/resources/push-jobs-client/pkg/license.html.erb +202 -0
- data/omnibus/resources/push-jobs-client/pkg/welcome.html.erb +5 -0
- data/opscode-pushy-client.gemspec +28 -0
- data/pkg/opscode-pushy-client-2.3.0.gem +0 -0
- data/spec/pushy_client/protocol_handler_spec.rb +48 -0
- data/spec/pushy_client/whitelist_spec.rb +70 -0
- data/spec/spec_helper.rb +12 -0
- 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
|