chef_sous_vide 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.travis.yml +7 -0
- data/Berksfile +11 -0
- data/Berksfile.lock +45 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +297 -0
- data/LICENSE.txt +21 -0
- data/README.md +127 -0
- data/Rakefile +13 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/kitchen.yml +98 -0
- data/lib/sous_vide.rb +10 -0
- data/lib/sous_vide/event_methods.rb +176 -0
- data/lib/sous_vide/handler.rb +186 -0
- data/lib/sous_vide/outputs/json_file.rb +43 -0
- data/lib/sous_vide/outputs/json_http.rb +66 -0
- data/lib/sous_vide/outputs/logger.rb +48 -0
- data/lib/sous_vide/outputs/multi.rb +21 -0
- data/lib/sous_vide/tracked_resource.rb +75 -0
- data/lib/sous_vide/version.rb +3 -0
- data/sous_vide.gemspec +35 -0
- metadata +194 -0
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "cucumber/rake/task"
|
3
|
+
|
4
|
+
Cucumber::Rake::Task.new
|
5
|
+
|
6
|
+
task :default => [ "cucumber" ]
|
7
|
+
|
8
|
+
begin
|
9
|
+
require "kitchen/rake_tasks"
|
10
|
+
Kitchen::RakeTasks.new
|
11
|
+
rescue LoadError
|
12
|
+
puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV["CI"]
|
13
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "sous_vide"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/kitchen.yml
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
---
|
2
|
+
<% if (ARGV[0] == "converge") %>
|
3
|
+
transport:
|
4
|
+
name: rsync
|
5
|
+
<% end %>
|
6
|
+
|
7
|
+
driver:
|
8
|
+
name: docker
|
9
|
+
use_sudo: false
|
10
|
+
provision_command:
|
11
|
+
# prevent APT from deleting the APT folder
|
12
|
+
- rm /etc/apt/apt.conf.d/docker-clean
|
13
|
+
- apt-get install -y -q apt-transport-https rsync
|
14
|
+
# allow test-kitchen to use root user (connects via ssh)
|
15
|
+
- sed -i 's/prohibit-password/yes/' /etc/ssh/sshd_config
|
16
|
+
# disable systemd since it doesn't work in docker
|
17
|
+
- ln -sf /bin/true /bin/systemctl
|
18
|
+
# install chef-client as part of the image, save time in the runtime
|
19
|
+
- curl -LO https://omnitruck.chef.io/install.sh && bash ./install.sh -v 12.17.44 && rm install.sh
|
20
|
+
- ln /opt/chef/bin/chef-client /bin/chef-client
|
21
|
+
username: root
|
22
|
+
password: root
|
23
|
+
|
24
|
+
verifier:
|
25
|
+
sudo: false
|
26
|
+
|
27
|
+
provisioner:
|
28
|
+
install_strategy: 'skip'
|
29
|
+
|
30
|
+
platforms:
|
31
|
+
- name: ubuntu-16.04
|
32
|
+
- name: ubuntu-18.04
|
33
|
+
|
34
|
+
suites:
|
35
|
+
- name: default
|
36
|
+
includes:
|
37
|
+
- ubuntu-16.04
|
38
|
+
driver:
|
39
|
+
hostname: elasticsearch
|
40
|
+
instance_name: elasticsearch
|
41
|
+
forward:
|
42
|
+
- "5601:5601" # kibana
|
43
|
+
- "9200:9200" # es
|
44
|
+
run_list:
|
45
|
+
- sous_vide::install
|
46
|
+
- sous_vide::default
|
47
|
+
attributes:
|
48
|
+
kitchen:
|
49
|
+
roles:
|
50
|
+
- elasticsearch
|
51
|
+
java:
|
52
|
+
jdk_version: 8
|
53
|
+
elasticsearch:
|
54
|
+
install:
|
55
|
+
version: "6.6.2"
|
56
|
+
- name: tomcat
|
57
|
+
includes:
|
58
|
+
- ubuntu-18.04
|
59
|
+
- ubuntu-16.04
|
60
|
+
driver:
|
61
|
+
links:
|
62
|
+
- elasticsearch
|
63
|
+
run_list:
|
64
|
+
- sous_vide::install
|
65
|
+
- sous_vide::tomcat
|
66
|
+
attributes:
|
67
|
+
kitchen:
|
68
|
+
roles:
|
69
|
+
- tomcat-example
|
70
|
+
java:
|
71
|
+
jdk_version: 8
|
72
|
+
- name: nginx
|
73
|
+
includes:
|
74
|
+
- ubuntu-16.04
|
75
|
+
run_list:
|
76
|
+
- sous_vide::install
|
77
|
+
- sous_vide::nginx
|
78
|
+
attributes:
|
79
|
+
kitchen:
|
80
|
+
roles:
|
81
|
+
- nginx-example
|
82
|
+
nginx:
|
83
|
+
install_method: "source"
|
84
|
+
modules:
|
85
|
+
- nginx::headers_more_module
|
86
|
+
- nginx::http_auth_request_module
|
87
|
+
- nginx::http_echo_module
|
88
|
+
- nginx::http_geoip_module
|
89
|
+
- nginx::http_gzip_static_module
|
90
|
+
- nginx::http_realip_module
|
91
|
+
- nginx::http_v2_module
|
92
|
+
- nginx::http_ssl_module
|
93
|
+
- nginx::http_stub_status_module
|
94
|
+
- nginx::naxsi_module
|
95
|
+
- nginx::ngx_devel_module
|
96
|
+
- nginx::ngx_lua_module
|
97
|
+
- nginx::openssl_source
|
98
|
+
- nginx::upload_progress_module
|
data/lib/sous_vide.rb
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
module SousVide
|
2
|
+
# This module implements Chef event methods. It's based on Chef::EventDispatch::Base.
|
3
|
+
#
|
4
|
+
# Nested resources are explicitly ignored and the code flow will be as follows:
|
5
|
+
#
|
6
|
+
# 1. resource_action_start
|
7
|
+
# 2. resource_* events (possibly multiple)
|
8
|
+
# 3. resource_action_complete
|
9
|
+
#
|
10
|
+
# The code is intentionally procedural and explicit. If :resource_action_start assigned
|
11
|
+
# @processing_now only then other events will work. :resource_action_completed unassigns
|
12
|
+
# @processing_now so all events will be ignored until :resource_action_start is called
|
13
|
+
# again.
|
14
|
+
module EventMethods
|
15
|
+
# This hook will always fire whenever chef is about to converge a resource, including why_run
|
16
|
+
# mode, notifications or skipped resources.
|
17
|
+
def resource_action_start(new_resource, action, notification_type, notifying_resource)
|
18
|
+
if nested?(new_resource) # ignore nested resources
|
19
|
+
new_r_name = "#{new_resource.resource_name}[#{new_resource.name}]##{action}"
|
20
|
+
debug("Received :resource_action_start on #{new_r_name}.",
|
21
|
+
"It's a nested resource and will be ignored.")
|
22
|
+
return false
|
23
|
+
end
|
24
|
+
|
25
|
+
# This is a delayed notification. From now on we are in 'delayed' run phase.
|
26
|
+
if notification_type == :delayed && @run_phase != "delayed"
|
27
|
+
debug("Changed run phase to 'delayed'.")
|
28
|
+
@run_phase = "delayed"
|
29
|
+
end
|
30
|
+
|
31
|
+
@processing_now = create(chef_resource: new_resource, action: action)
|
32
|
+
debug("Received :resource_action_start on #{@processing_now}.")
|
33
|
+
|
34
|
+
@execution_order += 1
|
35
|
+
@processing_now.execution_order = @execution_order
|
36
|
+
@processing_now.execution_phase = @run_phase
|
37
|
+
@processing_now.started_at = Time.now.strftime("%F %T")
|
38
|
+
|
39
|
+
@processing_now.chef_resource_handle = new_resource
|
40
|
+
|
41
|
+
@processing_now.before_notifications = new_resource.before_notifications.size
|
42
|
+
@processing_now.immediate_notifications = new_resource.immediate_notifications.size
|
43
|
+
@processing_now.delayed_notifications = new_resource.delayed_notifications.size
|
44
|
+
|
45
|
+
# When notifying resource is present notification_type will also be present. It is nil for
|
46
|
+
# delayed notifications.
|
47
|
+
if notifying_resource
|
48
|
+
_name = "#{notifying_resource.resource_name}[#{notifying_resource.name}]"
|
49
|
+
debug("Notified from #{_name} (:#{notification_type})")
|
50
|
+
@processing_now.notifying_resource = _name
|
51
|
+
end
|
52
|
+
@processing_now.notification_type = notification_type
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
def resource_updated(new_resource, _action)
|
57
|
+
return false if @processing_now.nil? || nested?(new_resource) # ignore nested resources
|
58
|
+
|
59
|
+
debug("Received :resource_updated on #{@processing_now}")
|
60
|
+
@processing_now.status = "updated"
|
61
|
+
true
|
62
|
+
end
|
63
|
+
|
64
|
+
# Resource is skipped when a guard instruction stops the converge process or when
|
65
|
+
# `action :nothing` is used (it's a guard too).
|
66
|
+
def resource_skipped(new_resource, _action, conditional)
|
67
|
+
return false if @processing_now.nil? || nested?(new_resource) # ignore nested resources
|
68
|
+
|
69
|
+
debug("Received :resource_skipped on #{@processing_now}", "(#{conditional.to_text})")
|
70
|
+
@processing_now.guard_description = conditional.to_text
|
71
|
+
@processing_now.status = "skipped"
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
def resource_up_to_date(new_resource, _action)
|
76
|
+
return false if @processing_now.nil? || nested?(new_resource) # ignore nested resources
|
77
|
+
|
78
|
+
debug("Received :resource_up_to_date on #{@processing_now}")
|
79
|
+
@processing_now.status = "up-to-date"
|
80
|
+
true
|
81
|
+
end
|
82
|
+
|
83
|
+
def resource_completed(new_resource)
|
84
|
+
return false if @processing_now.nil? || nested?(new_resource) # ignore nested resources
|
85
|
+
|
86
|
+
debug("Received :resource_completed on #{@processing_now}")
|
87
|
+
@processing_now.duration_ms = (new_resource.elapsed_time.to_f * 1000).to_i
|
88
|
+
|
89
|
+
# If a resource has notifications Chef will converge it in a forced_why_run mode to
|
90
|
+
# determine if any update will happen and if the notifications should be called.
|
91
|
+
#
|
92
|
+
# When this event was fired in why_run mode we override it's status to `why-run`.
|
93
|
+
#
|
94
|
+
# This is also how Chef::Runner#focred_why_run is implemented so it should be reliable.
|
95
|
+
#
|
96
|
+
# TODO: what happens when :why_run for notification fails?
|
97
|
+
if ::Chef::Config[:why_run]
|
98
|
+
debug("Resource #{@processing_now.name}##{@processing_now.action} marked why-run",
|
99
|
+
"because Chef::Config[:why_run] is true.")
|
100
|
+
@processing_now.status = "why-run"
|
101
|
+
end
|
102
|
+
|
103
|
+
# This resource was not notified by another and is a subject of normal ordered converge
|
104
|
+
# process. @resource_collection_cursor is pointing to next resource according to the
|
105
|
+
# expanded resource collection.
|
106
|
+
#
|
107
|
+
# When chef-client fails we will take remaining entries and add to the report as
|
108
|
+
# 'unprocessed'. It works because resources are ordered and we can keep track where we are.
|
109
|
+
#
|
110
|
+
# why-run mode and notifications are not in natural order and must not move the cursor.
|
111
|
+
#
|
112
|
+
# Having it pointing ahead is relevant because current resource has just been converged
|
113
|
+
# (technically 'failed') and it is not 'unprocessed'.
|
114
|
+
if !@processing_now.notifying_resource && # not notified
|
115
|
+
!::Chef::Config[:why_run] && # not why-run
|
116
|
+
@run_phase == "converge" # only converge phase
|
117
|
+
|
118
|
+
@resource_collection_cursor += 1
|
119
|
+
end
|
120
|
+
|
121
|
+
@processing_now.completed_at = Time.now.strftime("%F %T")
|
122
|
+
@processed << @processing_now
|
123
|
+
@processing_now = nil
|
124
|
+
true
|
125
|
+
end
|
126
|
+
|
127
|
+
def resource_failed(new_resource, _action, exception)
|
128
|
+
return false if @processing_now.nil? || nested?(new_resource) # ignore nested resources
|
129
|
+
|
130
|
+
debug("Received :resource_failed on #{@processing_now}")
|
131
|
+
@processing_now.status = "failed"
|
132
|
+
@processing_now.error_source = new_resource.to_text
|
133
|
+
@processing_now.error_output = exception.message
|
134
|
+
true
|
135
|
+
end
|
136
|
+
|
137
|
+
# Resources with retries can succeed on subsequent attempts or ignore_failure option may be
|
138
|
+
# set and it's the only place we can capture intermittent errors.
|
139
|
+
#
|
140
|
+
# This event can fire multiple times, but we capture only the most recent error.
|
141
|
+
def resource_failed_retriable(new_resource, _action, _remaining_retries, exception)
|
142
|
+
return false if @processing_now.nil? || nested?(new_resource) # ignore nested resources
|
143
|
+
|
144
|
+
debug("Received :resource_failed_retriable on #{@processing_now}")
|
145
|
+
@processing_now.retries += 1
|
146
|
+
@processing_now.error_source = new_resource.to_text
|
147
|
+
@processing_now.error_output = exception.message
|
148
|
+
true
|
149
|
+
end
|
150
|
+
|
151
|
+
def converge_start
|
152
|
+
debug("Received :converge_start")
|
153
|
+
debug("Changed run phase to 'converge'.")
|
154
|
+
@run_phase = "converge"
|
155
|
+
@run_name ||= [@run_started_at, @chef_node_role, @chef_node_ipv4, @run_id].join(" ")
|
156
|
+
end
|
157
|
+
|
158
|
+
def converge_complete
|
159
|
+
debug("Received :converge_completed")
|
160
|
+
@run_success = true
|
161
|
+
@run_completed_at = Time.now.strftime("%F %T")
|
162
|
+
send_to_output!
|
163
|
+
end
|
164
|
+
|
165
|
+
def converge_failed(_exception)
|
166
|
+
debug("Received :converge_failed")
|
167
|
+
@run_success = false
|
168
|
+
@run_completed_at = Time.now.strftime("%F %T")
|
169
|
+
@run_phase = "post-converge"
|
170
|
+
debug("Changed run phase to 'post-converge'.")
|
171
|
+
|
172
|
+
consume_unprocessed_resources!
|
173
|
+
send_to_output!
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require "sous_vide/event_methods"
|
2
|
+
require "sous_vide/outputs/json_file"
|
3
|
+
require "sous_vide/outputs/json_http"
|
4
|
+
require "sous_vide/outputs/logger"
|
5
|
+
require "sous_vide/outputs/multi"
|
6
|
+
|
7
|
+
require "securerandom"
|
8
|
+
require "singleton"
|
9
|
+
|
10
|
+
module SousVide
|
11
|
+
# == SousVide::Handler
|
12
|
+
#
|
13
|
+
# The Handler receives event data from chef-client and keeps track of the converge process. It's
|
14
|
+
# essentially a stream parser hooked into Chef::EventDispatch.
|
15
|
+
#
|
16
|
+
# Event methods are all in SousVide::EventMethods module. This file contains logic that does not
|
17
|
+
# deal with events directly.
|
18
|
+
class Handler
|
19
|
+
include Singleton
|
20
|
+
include EventMethods
|
21
|
+
|
22
|
+
attr_accessor :chef_run_context,
|
23
|
+
:logger,
|
24
|
+
:sous_output,
|
25
|
+
:processing_now,
|
26
|
+
:processed,
|
27
|
+
:run_phase,
|
28
|
+
:run_id,
|
29
|
+
:run_name
|
30
|
+
|
31
|
+
# Enables the handler. Call it anywhere in the recipe, ideally as early as possible.
|
32
|
+
#
|
33
|
+
# SousVide::Handler.register(node.run_context)
|
34
|
+
#
|
35
|
+
# All converge-time resources will be included in the report regardless at what point
|
36
|
+
# registration happens.
|
37
|
+
#
|
38
|
+
# Compile-time resources defined before registration will not be included.
|
39
|
+
# TODO: see client.rb start_handlers as an option.
|
40
|
+
#
|
41
|
+
# `chef_handler` resource does not support subscribing to :events so we have give up DSL and
|
42
|
+
# use Chef API.
|
43
|
+
#
|
44
|
+
# The `Chef.event_handler` DSL could be used but dealing with returns and exceptions in procs
|
45
|
+
# is a pain.
|
46
|
+
def self.register(run_context)
|
47
|
+
::Chef::Log.info "Registering SousVide"
|
48
|
+
|
49
|
+
instance.chef_run_context = run_context
|
50
|
+
instance.post_initialize
|
51
|
+
|
52
|
+
run_context.events.register(instance)
|
53
|
+
end
|
54
|
+
|
55
|
+
def initialize
|
56
|
+
@execution_order = 0
|
57
|
+
@resource_collection_cursor = 0
|
58
|
+
|
59
|
+
@processed = []
|
60
|
+
@processing_now = nil
|
61
|
+
|
62
|
+
@run_started_at = Time.now.strftime("%F %T")
|
63
|
+
|
64
|
+
# Not related to RunStatus#run_id, it's our internal run id.
|
65
|
+
@run_id = SecureRandom.uuid.split("-").first # => 596e9d00
|
66
|
+
@run_phase = "compile"
|
67
|
+
|
68
|
+
# Default to Chef logger, but can be changed to anything that responds to #call
|
69
|
+
@logger = ::Chef::Log
|
70
|
+
@sous_output = Outputs::Logger.new(logger: @logger)
|
71
|
+
end
|
72
|
+
|
73
|
+
# This is called in #register, as soon as @chef_run_context is available.
|
74
|
+
def post_initialize
|
75
|
+
@chef_node_ipv4 = @chef_run_context.node["ipaddress"] || "<no ip>"
|
76
|
+
@chef_node_role = @chef_run_context.node["roles"].first || "<no role>"
|
77
|
+
@chef_node_instance_id = @chef_run_context.node.name
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
def run_data
|
82
|
+
{
|
83
|
+
chef_run_id: @run_id,
|
84
|
+
chef_run_name: @run_name,
|
85
|
+
chef_run_started_at: @run_started_at,
|
86
|
+
chef_run_completed_at: @run_completed_at,
|
87
|
+
chef_run_success: @run_success
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def node_data
|
92
|
+
{
|
93
|
+
chef_node_ipv4: @chef_node_ipv4,
|
94
|
+
chef_node_instance_id: @chef_node_instance_id,
|
95
|
+
chef_node_role: @chef_node_role
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
def create(chef_resource:, action:)
|
100
|
+
tracked = TrackedResource.new(action: action,
|
101
|
+
name: chef_resource.name,
|
102
|
+
type: chef_resource.resource_name)
|
103
|
+
|
104
|
+
tracked.cookbook_name = chef_resource.cookbook_name || "<Dynamically Defined Resource>"
|
105
|
+
tracked.cookbook_recipe = chef_resource.recipe_name || "<Dynamically Defined Resource>"
|
106
|
+
tracked.source_line = chef_resource.source_line || "<Dynamically Defined Resource>"
|
107
|
+
tracked.chef_resource_handle = chef_resource
|
108
|
+
tracked
|
109
|
+
end
|
110
|
+
|
111
|
+
# We will ignore nested resources. Once a top level resource triggered :resource_action
|
112
|
+
# started any events not related to it will be ignored.
|
113
|
+
def nested?(chef_resource)
|
114
|
+
@processing_now &&
|
115
|
+
@processing_now.chef_resource_handle != chef_resource
|
116
|
+
end
|
117
|
+
|
118
|
+
# When chef-client fails we haven't seen all resources and need to backfill the handler.
|
119
|
+
def consume_unprocessed_resources!
|
120
|
+
all_known_resources = expand_chef_resources!
|
121
|
+
|
122
|
+
# No unprocessed resources left. Failure likely occured on last resource or in a delayed
|
123
|
+
# notification.
|
124
|
+
# TODO: check delayed notification failure
|
125
|
+
return if @resource_collection_cursor >= all_known_resources.size
|
126
|
+
|
127
|
+
unprocessed = all_known_resources[@resource_collection_cursor..-1]
|
128
|
+
|
129
|
+
# We will pass unprocessed resources via :resource_action_start and :resource_completed so
|
130
|
+
# they will end up in @processed array, but with status set to 'unprocessed' and execution
|
131
|
+
# phase 'post-converge'.
|
132
|
+
#
|
133
|
+
# TODO: consider placing these resources before delayed notification, currently they are
|
134
|
+
# always at the very end. It _maybe_ makes sense.
|
135
|
+
unprocessed.each do |tracked|
|
136
|
+
resource_action_start(
|
137
|
+
tracked.chef_resource_handle, # new_resource
|
138
|
+
tracked.action, # action
|
139
|
+
nil, # notification_type
|
140
|
+
nil # notifying_resource
|
141
|
+
)
|
142
|
+
|
143
|
+
resource_completed(
|
144
|
+
tracked.chef_resource_handle # new_resource
|
145
|
+
)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Resources with multiple actions must be expanded, ie. given resource:
|
150
|
+
#
|
151
|
+
# service 'nginx' do
|
152
|
+
# action [:enable, :start]
|
153
|
+
# end
|
154
|
+
#
|
155
|
+
# After expansion we should have 2 resources:
|
156
|
+
#
|
157
|
+
# * service[nginx] with action :enable
|
158
|
+
# * service[nginx] with action :start
|
159
|
+
#
|
160
|
+
# We keep track of the progress and on failure we will pick up unprocessed resources from here
|
161
|
+
# to feed the handler in post-converge stage.
|
162
|
+
#
|
163
|
+
# On a successful chef-client run @processed will contain all resources and this method won't
|
164
|
+
# be called.
|
165
|
+
def expand_chef_resources!
|
166
|
+
chef_run_context.resource_collection.flat_map do |chef_resource|
|
167
|
+
Array(chef_resource.action).map do |action|
|
168
|
+
create(chef_resource: chef_resource, action: action)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def send_to_output!
|
174
|
+
@sous_output.call(run_data: run_data, node_data: node_data, resources_data: @processed)
|
175
|
+
end
|
176
|
+
|
177
|
+
def debug(*args)
|
178
|
+
message = args.compact.join(" ")
|
179
|
+
logger.debug(message)
|
180
|
+
end
|
181
|
+
|
182
|
+
def logger
|
183
|
+
@logger ||= ::Chef::Log
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|