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.
- 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
|