ey_enzyme 0.9.48 → 2.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/ey_enzyme/api/client.rb +133 -0
- data/lib/ey_enzyme/api.rb +29 -102
- data/lib/ey_enzyme/cli.rb +125 -68
- data/lib/ey_enzyme/cookbook_set.rb +121 -43
- data/lib/ey_enzyme/multi_logger.rb +19 -0
- data/lib/ey_enzyme/version.rb +1 -1
- data/lib/ey_enzyme.rb +2 -2
- metadata +117 -116
- data/bin/ey-recipes +0 -5
- data/lib/ey_enzyme/old_cli.rb +0 -35
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c71e1af54ddfa6f64786576598b0f828f1528bff
|
4
|
+
data.tar.gz: 8586d600ca185ccc67b491393f4b0bba56bd445e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c6e747c25787291bb9dffdf1077a645933885ad23bd5f97df67190ee3c22968dc08f11ad025fc7b84ff44fc8196ad0a0de5088e4c780ffb2db9f157ebbeb7f97
|
7
|
+
data.tar.gz: 7ff5be0a8cfc497cabf97c30ff01c0ea497210829dbb71fdaf48414d8ad917ce563610d0e43a2194188937a3e65d91fa877a3a0e14ce13f68ac817bfad801f40
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
require 'rest_client'
|
3
|
+
|
4
|
+
module EY
|
5
|
+
module Enzyme
|
6
|
+
module API
|
7
|
+
class Client
|
8
|
+
|
9
|
+
REST_TIMEOUT_SECONDS = 60
|
10
|
+
TOTAL_TIMEOUT_SECONDS = 600
|
11
|
+
RETRY_TIMEOUT_SECONDS = 30
|
12
|
+
|
13
|
+
attr_reader :api_url, :instance_id, :token, :logger
|
14
|
+
|
15
|
+
def initialize(api_url, instance_id, token, logger)
|
16
|
+
@api_url = api_url
|
17
|
+
@instance_id = instance_id
|
18
|
+
@token = token
|
19
|
+
@logger = logger
|
20
|
+
end
|
21
|
+
|
22
|
+
def rest
|
23
|
+
@rest ||= RestClient::Resource.new(
|
24
|
+
api_url,
|
25
|
+
:timeout => REST_TIMEOUT_SECONDS
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def report(message)
|
30
|
+
safe_api("report", :message => message)
|
31
|
+
end
|
32
|
+
|
33
|
+
def notify_success
|
34
|
+
retry_api("completed", :status => 'true')
|
35
|
+
end
|
36
|
+
|
37
|
+
def notify_error(type, error)
|
38
|
+
log :exception, "Notifying #{type} error", error
|
39
|
+
call_api("error", params_for(type, error))
|
40
|
+
rescue RestClient::Exception
|
41
|
+
log :exception, "Failed to notify of #{type} error", $!
|
42
|
+
raise
|
43
|
+
end
|
44
|
+
|
45
|
+
def dna
|
46
|
+
retry_api("dna")
|
47
|
+
end
|
48
|
+
|
49
|
+
def custom_recipe_url
|
50
|
+
if response = retry_api("custom-recipe")
|
51
|
+
response["url"]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
#upload, as in, upload the log file output of the chef run
|
56
|
+
def upload(type, file)
|
57
|
+
retry_api("store_gzip", :type => type, :file => file)
|
58
|
+
rescue => e
|
59
|
+
#blanket rescue feels dangerous, but we're at least logging it
|
60
|
+
#we don't have all the information we'd like to include in the exception message here anyway
|
61
|
+
#caller (CookbookSet#upload) should check return value and notify failure
|
62
|
+
api_error("store_gzip", e)
|
63
|
+
false
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def log(level, *msg)
|
69
|
+
@logger.send(level, *msg) if @logger
|
70
|
+
end
|
71
|
+
|
72
|
+
def keys
|
73
|
+
@keys ||= {:instance_id => instance_id, :token => token}
|
74
|
+
end
|
75
|
+
|
76
|
+
def params_for(type, error)
|
77
|
+
{ :class => error.class,
|
78
|
+
:message => error.message,
|
79
|
+
:backtrace => error.backtrace,
|
80
|
+
:where_failed => type }
|
81
|
+
end
|
82
|
+
|
83
|
+
def call_api(path, opts = {})
|
84
|
+
log :debug, "API call path: #{path.inspect}, data: #{opts.inspect}"
|
85
|
+
JSON.parse(
|
86
|
+
rest[path].post(
|
87
|
+
opts.merge(keys),
|
88
|
+
{
|
89
|
+
:content_type => "application/json",
|
90
|
+
:accept => "application/json"
|
91
|
+
}
|
92
|
+
)
|
93
|
+
)
|
94
|
+
end
|
95
|
+
|
96
|
+
def retry_api(path, opts={})
|
97
|
+
Timeout.timeout(TOTAL_TIMEOUT_SECONDS) do
|
98
|
+
loop do
|
99
|
+
if result = safe_api(path, opts)
|
100
|
+
return result
|
101
|
+
end
|
102
|
+
sleep RETRY_TIMEOUT_SECONDS
|
103
|
+
end
|
104
|
+
end
|
105
|
+
rescue Timeout::Error
|
106
|
+
false
|
107
|
+
end
|
108
|
+
|
109
|
+
def safe_api(path, opts = {})
|
110
|
+
begin
|
111
|
+
return call_api(path, opts)
|
112
|
+
rescue RestClient::BadGateway, RestClient::GatewayTimeout
|
113
|
+
api_error(path, $!)
|
114
|
+
return false
|
115
|
+
rescue RestClient::ServiceUnavailable => e
|
116
|
+
response = e.response
|
117
|
+
if response.headers[:content_type] =~ /json/
|
118
|
+
hash = JSON.parse(response)
|
119
|
+
log :error, "Failed to fetch DNA: #{hash.delete('message')}: #{hash.inspect}"
|
120
|
+
end
|
121
|
+
|
122
|
+
api_error(path, e)
|
123
|
+
return false
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def api_error(path, error)
|
128
|
+
log :exception, "API request failed", error
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/lib/ey_enzyme/api.rb
CHANGED
@@ -1,117 +1,44 @@
|
|
1
|
-
|
1
|
+
require 'ey_enzyme/api/client'
|
2
2
|
|
3
|
-
module EY
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
@instance_id = instance_id
|
8
|
-
@token = token
|
9
|
-
@logger = MultiLogger.new(log_file)
|
10
|
-
@rest = RestClient::Resource.new(api_url, :timeout => api_rest_timeout_seconds)
|
11
|
-
end
|
12
|
-
|
13
|
-
def report(message)
|
14
|
-
safe_api("report", :message => message)
|
15
|
-
end
|
16
|
-
|
17
|
-
def notify_success
|
18
|
-
retry_api("completed", :status => 'true')
|
19
|
-
end
|
20
|
-
|
21
|
-
def notify_error(type, error)
|
22
|
-
@logger.exception "Notifying #{type} error", error
|
23
|
-
call_api("error", params_for(type, error))
|
24
|
-
rescue RestClient::Exception
|
25
|
-
@logger.exception "Failed to notify of #{type} error", $!
|
26
|
-
raise
|
27
|
-
end
|
28
|
-
|
29
|
-
def dna
|
30
|
-
retry_api("dna")
|
31
|
-
end
|
32
|
-
|
33
|
-
def custom_recipe_url
|
34
|
-
if response = retry_api("custom-recipe")
|
35
|
-
response["url"]
|
3
|
+
module EY
|
4
|
+
module Enzyme
|
5
|
+
module API
|
6
|
+
class Unconfigured < StandardError
|
36
7
|
end
|
37
|
-
end
|
38
|
-
|
39
|
-
#upload, as in, upload the log file output of the chef run
|
40
|
-
def upload(type, content)
|
41
|
-
content = content.force_encoding('UTF-8') if content.respond_to?(:force_encoding)
|
42
|
-
retry_api("store", :type => type, :content => content)
|
43
|
-
rescue => e
|
44
|
-
#blanket rescue feels dangerous, but we're at least logging it
|
45
|
-
#we don't have all the information we'd like to include in the exception message here anyway
|
46
|
-
#caller (CookbookSet#upload) should check return value and notify failure
|
47
|
-
api_error("store", e)
|
48
|
-
false
|
49
|
-
end
|
50
|
-
|
51
|
-
private
|
52
|
-
|
53
|
-
def keys
|
54
|
-
@keys ||= {:instance_id => @instance_id, :token => @token}
|
55
|
-
end
|
56
|
-
|
57
|
-
def params_for(type, error)
|
58
|
-
{ :class => error.class,
|
59
|
-
:message => error.message,
|
60
|
-
:backtrace => error.backtrace,
|
61
|
-
:where_failed => type }
|
62
|
-
end
|
63
8
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
end
|
9
|
+
def self.configure(api_url, instance_id, token, logger)
|
10
|
+
@client = Client.new(api_url, instance_id, token, logger)
|
11
|
+
self
|
12
|
+
end
|
69
13
|
|
70
|
-
|
71
|
-
|
72
|
-
|
14
|
+
def self.client
|
15
|
+
raise Unconfigured unless @client
|
16
|
+
@client
|
17
|
+
end
|
73
18
|
|
74
|
-
|
75
|
-
|
76
|
-
|
19
|
+
def self.report(message)
|
20
|
+
client.report(message)
|
21
|
+
end
|
77
22
|
|
78
|
-
|
79
|
-
|
80
|
-
|
23
|
+
def self.notify_success
|
24
|
+
client.notify_success
|
25
|
+
end
|
81
26
|
|
82
|
-
|
83
|
-
|
84
|
-
loop do
|
85
|
-
if result = safe_api(path, opts)
|
86
|
-
return result
|
87
|
-
end
|
88
|
-
sleep api_sleep_before_retry_seconds
|
89
|
-
end
|
27
|
+
def self.notify_error(type, error)
|
28
|
+
client.notify_error(type, error)
|
90
29
|
end
|
91
|
-
rescue Timeout::Error
|
92
|
-
false
|
93
|
-
end
|
94
30
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
rescue RestClient::BadGateway, RestClient::GatewayTimeout
|
99
|
-
api_error(path, $!)
|
100
|
-
return false
|
101
|
-
rescue RestClient::ServiceUnavailable => e
|
102
|
-
response = e.response
|
103
|
-
if response.headers[:content_type] =~ /json/
|
104
|
-
hash = JSON.parse(response)
|
105
|
-
@logger.error "Failed to fetch DNA: #{hash.delete('message')}: #{hash.inspect}"
|
106
|
-
end
|
31
|
+
def self.dna
|
32
|
+
client.dna
|
33
|
+
end
|
107
34
|
|
108
|
-
|
109
|
-
|
35
|
+
def self.custom_recipe_url
|
36
|
+
client.custom_recipe_url
|
110
37
|
end
|
111
|
-
end
|
112
38
|
|
113
|
-
|
114
|
-
|
39
|
+
def self.upload(type, file)
|
40
|
+
client.upload(type, file)
|
41
|
+
end
|
115
42
|
end
|
116
43
|
end
|
117
44
|
end
|
data/lib/ey_enzyme/cli.rb
CHANGED
@@ -5,44 +5,66 @@ module EY::Enzyme
|
|
5
5
|
def self.run(args)
|
6
6
|
$stdout.sync = true
|
7
7
|
|
8
|
-
defaults = {
|
9
|
-
|
8
|
+
defaults = {
|
9
|
+
chef_bin: 'chef-client',
|
10
|
+
run_list: ['recipe[ey-init::main]'],
|
11
|
+
config: '/etc/engineyard/dracul.yml',
|
12
|
+
base_path: '/etc/chef/',
|
13
|
+
logfile: '/var/log/ey-enzyme.log',
|
14
|
+
log_level: :info,
|
15
|
+
keep: 5,
|
16
|
+
run_custom: true,
|
17
|
+
download: true,
|
18
|
+
mode: :deploy
|
19
|
+
}
|
10
20
|
options = {}
|
11
21
|
|
12
22
|
opts = OptionParser.new do |opts|
|
13
23
|
opts.version = EY::Enzyme::VERSION
|
14
|
-
opts.banner = "Usage: ey-enzyme
|
24
|
+
opts.banner = "Usage: ey-enzyme <options> [argument]"
|
15
25
|
opts.define_head "ey-enzyme: running recipes..."
|
16
26
|
opts.separator '*'*80
|
17
27
|
|
18
|
-
opts.on("--
|
19
|
-
options[:quick] = true
|
20
|
-
end
|
21
|
-
|
22
|
-
opts.on("--logfile", "Set the logfile (defaults to /var/log/enzyme.log)") do |path|
|
28
|
+
opts.on("--logfile", "Set the logfile (default: #{defaults[:logfile]})") do |path|
|
23
29
|
options[:logfile] = path
|
24
30
|
end
|
25
31
|
|
26
|
-
opts.on("--config CONFIG", "Use config file") do |config|
|
32
|
+
opts.on("--config CONFIG", "Use config file (default: #{defaults[:config]})") do |config|
|
27
33
|
options[:config] = config
|
28
34
|
end
|
29
35
|
|
30
|
-
opts.on("--
|
31
|
-
options[:
|
36
|
+
opts.on("--cookbooks PATH", "Use the given path for retrieving and using the cookbooks (default: #{defaults[:base_path]}") do |path|
|
37
|
+
options[:base_path] = path
|
32
38
|
end
|
33
39
|
|
34
|
-
opts.on("--
|
35
|
-
options[:
|
40
|
+
opts.on("--chef-bin PATH", "Use this chef-solo executable to run recipes") do |binary|
|
41
|
+
options[:chef_bin] = binary
|
36
42
|
end
|
37
43
|
|
38
|
-
opts.on("--
|
39
|
-
options[:
|
44
|
+
opts.on("--run-list RECIPES", "Comma-separated list of recipes to run") do |recipe_list|
|
45
|
+
options[:run_list] = recipe_list.split(',').collect {|r| "recipe[#{r}]" }
|
40
46
|
end
|
41
47
|
|
42
48
|
opts.on("--report message", "Report a status") do |message|
|
43
|
-
options[:
|
49
|
+
options[:mode] = :report
|
44
50
|
options[:message] = message
|
45
51
|
end
|
52
|
+
|
53
|
+
opts.on("--no-custom", "Do not overlay custom recipes") do
|
54
|
+
options[:run_custom] = false
|
55
|
+
end
|
56
|
+
|
57
|
+
opts.on("--cached", "Used the cached version of the cookbooks and dna.json") do
|
58
|
+
options[:download] = false
|
59
|
+
end
|
60
|
+
|
61
|
+
opts.on("--refresh-dna", "Write new dna even if --cached is on") do
|
62
|
+
options[:refresh_dna] = true
|
63
|
+
end
|
64
|
+
|
65
|
+
opts.on("--verbose", "Show debug logs as well") do
|
66
|
+
options[:log_level] = :debug
|
67
|
+
end
|
46
68
|
end
|
47
69
|
|
48
70
|
opts.parse!(args)
|
@@ -50,43 +72,57 @@ module EY::Enzyme
|
|
50
72
|
config = options[:config] || defaults[:config]
|
51
73
|
cli = new(defaults.merge(YAML.load_file(config)).merge(options))
|
52
74
|
|
53
|
-
if options[:report
|
75
|
+
if options[:mode] == :report
|
54
76
|
cli.report(options[:message])
|
55
|
-
elsif options[:type]
|
56
|
-
cli.deploy
|
57
77
|
else
|
58
|
-
|
78
|
+
cli.deploy
|
59
79
|
end
|
60
80
|
|
61
|
-
|
62
81
|
rescue OptionParser::InvalidOption
|
63
82
|
$stderr.puts "#{File.basename($0)} #{$!.message}"
|
64
83
|
abort opts.to_s
|
84
|
+
|
65
85
|
rescue *IGNORABLE_EXCEPTIONS
|
66
86
|
raise
|
87
|
+
|
67
88
|
rescue DeployError
|
68
89
|
if cli
|
90
|
+
cli.report $!.message
|
69
91
|
cli.notify_user_error($!)
|
70
92
|
exit 0
|
71
93
|
end
|
72
94
|
raise
|
95
|
+
|
96
|
+
rescue DNAError
|
97
|
+
if cli
|
98
|
+
cli.report $!.message
|
99
|
+
cli.notify_system_error($!)
|
100
|
+
exit 0
|
101
|
+
end
|
102
|
+
raise
|
103
|
+
|
73
104
|
rescue Exception
|
74
105
|
if cli
|
75
106
|
cli.notify_system_error($!)
|
76
107
|
exit 0
|
77
108
|
end
|
78
109
|
raise
|
110
|
+
|
79
111
|
end
|
80
112
|
|
81
113
|
def initialize(opts={})
|
82
114
|
@opts = opts
|
83
|
-
@
|
84
|
-
@
|
115
|
+
@logger = opts[:logger] = MultiLogger.new(opts[:logfile])
|
116
|
+
@api = API
|
117
|
+
@api.configure(opts[:api], opts[:instance_id], opts[:token], opts[:logger])
|
118
|
+
|
119
|
+
@logger.level = opts[:log_level]
|
85
120
|
|
86
121
|
@opts[:timestamp] = Time.now.strftime("%Y-%m-%dT%H-%M-%S")
|
87
122
|
end
|
88
123
|
|
89
124
|
def report(message)
|
125
|
+
@logger.debug "REPORT MESSAGE: #{message}"
|
90
126
|
@api.report(message)
|
91
127
|
end
|
92
128
|
|
@@ -98,74 +134,95 @@ module EY::Enzyme
|
|
98
134
|
@api.notify_error("user", error)
|
99
135
|
end
|
100
136
|
|
101
|
-
MAIN_RECIPE_PATH = '/etc/chef/recipes'
|
102
|
-
CUSTOM_RECIPE_PATH = '/etc/chef-custom/recipes'
|
103
|
-
|
104
137
|
def deploy
|
105
138
|
@logger.info "Starting configuration run"
|
106
139
|
|
107
|
-
|
108
|
-
|
109
|
-
case @opts[:type]
|
110
|
-
when :main
|
111
|
-
run_main
|
112
|
-
run_custom(json)
|
113
|
-
when :custom
|
114
|
-
run_custom(json)
|
115
|
-
else
|
116
|
-
raise "Unknown type: #{@opts[:type].inspect}"
|
117
|
-
end
|
140
|
+
ensure_resolv_conf_is_functional
|
141
|
+
update_dna
|
118
142
|
|
143
|
+
run_cookbooks
|
119
144
|
@api.notify_success
|
120
145
|
end
|
121
146
|
|
122
|
-
def
|
123
|
-
|
124
|
-
main_cookbooks.run
|
147
|
+
def cookbook_set
|
148
|
+
CookbookSet.new(@api, @opts)
|
125
149
|
end
|
126
150
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
151
|
+
private
|
152
|
+
|
153
|
+
def run_cookbooks
|
154
|
+
report "Initializing cookbook run"
|
155
|
+
cookbook_set.run
|
156
|
+
end
|
131
157
|
|
132
|
-
|
158
|
+
## Resovlve resolv.conf stuff
|
133
159
|
|
134
|
-
|
160
|
+
def ensure_resolv_conf_is_functional
|
161
|
+
@logger.info "Checking resolv.conf"
|
162
|
+
unless does_hostname_resolve?
|
163
|
+
modify_resolv_conf
|
164
|
+
end
|
135
165
|
end
|
136
166
|
|
137
|
-
def
|
138
|
-
|
167
|
+
def resolv_conf
|
168
|
+
File.read('/etc/resolv.conf')
|
139
169
|
end
|
140
170
|
|
141
|
-
def
|
142
|
-
|
171
|
+
def does_hostname_resolve?
|
172
|
+
hostname = `/bin/hostname --fqdn 2>/dev/null`
|
173
|
+
! hostname.empty?
|
174
|
+
end
|
143
175
|
|
144
|
-
|
145
|
-
|
176
|
+
def add_to_search(path, term)
|
177
|
+
data = File.read(path)
|
178
|
+
new_data = data.gsub(/^\s*(search\s+.*)$/) do |match|
|
179
|
+
if match =~ /\b#{term}\b/
|
180
|
+
match
|
181
|
+
else
|
182
|
+
# only actually append the term to the search line if it's not already there.
|
183
|
+
@logger.info "Adding #{term} to the #{path} 'search' line"
|
184
|
+
"#{match} #{term}"
|
185
|
+
end
|
146
186
|
end
|
147
|
-
|
148
|
-
@logger.debug "Writing json dna to file system"
|
149
|
-
update_main_dna(json)
|
150
|
-
json
|
187
|
+
File.open(path,'w') {|fh| fh.write new_data}
|
151
188
|
end
|
152
189
|
|
153
|
-
def
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
190
|
+
def modify_resolv_conf
|
191
|
+
original_resolv_conf = resolv_conf
|
192
|
+
|
193
|
+
add_to_search('/etc/resolv.conf','compute-1.internal')
|
194
|
+
|
195
|
+
if does_hostname_resolve?
|
196
|
+
# if it works, make it survive a dhcp lease renewal
|
197
|
+
add_to_search('/etc/resolv.conf.head', 'compute-1.internal')
|
198
|
+
else
|
199
|
+
@logger.info "Change failed; still can't resolve hostname. Reverting change."
|
200
|
+
File.open('/etc/resolv.conf','w') {|fh| fh.write original_resolv_conf}
|
201
|
+
end
|
158
202
|
end
|
159
203
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
204
|
+
### DNA.json manipulation
|
205
|
+
|
206
|
+
def update_dna
|
207
|
+
FileUtils.mkdir_p @opts[:base_path]
|
208
|
+
file = File.join(@opts[:base_path],'dna.json')
|
209
|
+
|
210
|
+
if File.exists?(file) and not @opts[:refresh_dna] and not @opts[:download]
|
211
|
+
@logger.info "Skipping download of fresh DNA - using existing #{@chef_dna} file."
|
212
|
+
return
|
213
|
+
end
|
214
|
+
|
215
|
+
report "Fetching instance DNA"
|
216
|
+
raise DNAError, "Failed to fetch DNA" unless json = @api.dna
|
217
|
+
|
218
|
+
main_json = {}
|
219
|
+
main_json['run_list'] = @opts[:run_list]
|
220
|
+
main_json['dna'] = json.dup
|
221
|
+
save_dna(file,main_json)
|
166
222
|
end
|
167
223
|
|
168
|
-
def
|
224
|
+
def save_dna(file, json)
|
225
|
+
@logger.debug "Writing json dna to #{file}"
|
169
226
|
File.open(file, 'w') do |f|
|
170
227
|
f.puts JSON.pretty_generate(json)
|
171
228
|
f.chmod(0600)
|
@@ -1,99 +1,175 @@
|
|
1
1
|
module EY::Enzyme
|
2
2
|
class CookbookSet
|
3
|
-
|
4
|
-
|
5
|
-
@
|
6
|
-
@
|
7
|
-
@
|
8
|
-
@
|
9
|
-
@
|
10
|
-
@
|
11
|
-
@
|
3
|
+
|
4
|
+
def initialize(api, opts)
|
5
|
+
@opts = opts
|
6
|
+
@api = api
|
7
|
+
@chef_config = File.join(opts[:base_path], "solo.rb")
|
8
|
+
@chef_dna = File.join(opts[:base_path], "dna.json")
|
9
|
+
@recipes_path = File.join(opts[:base_path], "recipes")
|
10
|
+
@main_recipes_url = opts[:recipes_url]
|
11
|
+
@user_recipes_url = opts.has_key?(:custom_recipes_url) ? opts[:custom_recipes_url] : api.custom_recipe_url
|
12
|
+
@logger = opts[:logger]
|
13
|
+
@timestamp = Time.now.strftime("%Y-%m-%dT%H-%M-%S")
|
14
|
+
@chef_log = "/var/log/chef.#{@timestamp}.log"
|
12
15
|
end
|
13
16
|
|
14
17
|
def run
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
+
if @main_recipes_url
|
19
|
+
setup
|
20
|
+
execute
|
21
|
+
else
|
22
|
+
@logger.info "No recipes for chef run. Skipping"
|
18
23
|
end
|
19
|
-
|
20
|
-
setup
|
21
|
-
execute
|
22
24
|
ensure
|
23
25
|
upload
|
24
26
|
end
|
25
27
|
|
28
|
+
private
|
29
|
+
|
26
30
|
def setup
|
27
|
-
@logger.info "Starting
|
31
|
+
@logger.info "Starting chef run"
|
28
32
|
|
29
33
|
abort("Instance ID mismatch") unless instance_id_valid?
|
30
34
|
FileUtils.rm(@chef_log) if File.exist?(@chef_log)
|
31
35
|
|
32
|
-
|
36
|
+
# Create the EC2 hint for ohai
|
37
|
+
# See: https://geekblood.wordpress.com/2015/03/13/enabling-ec2-metadata-in-ohai/
|
38
|
+
FileUtils.mkdir_p("#{@opts[:base_path]}/ohai/hints/")
|
39
|
+
FileUtils.touch("#{@opts[:base_path]}/ohai/hints/ec2.json")
|
33
40
|
|
34
|
-
File.open(chef_config, "w") do |f|
|
41
|
+
File.open(@chef_config, "w") do |f|
|
35
42
|
f.puts <<-EOS
|
36
43
|
cookbook_path "#{@recipes_path}/cookbooks"
|
37
44
|
file_store_path "#{@recipes_path}/"
|
38
45
|
file_cache_path "#{@recipes_path}/"
|
46
|
+
data_bag_path "#{@recipes_path}/data_bags"
|
39
47
|
log_location "#{@chef_log}"
|
40
48
|
log_level :info
|
41
49
|
node_name "#{@opts[:instance_id]}"
|
42
50
|
EOS
|
43
51
|
end
|
44
52
|
|
45
|
-
|
53
|
+
if @opts[:download]
|
54
|
+
@logger.info "Removing cookbooks"
|
55
|
+
FileUtils.rm_rf(@recipes_path)
|
56
|
+
FileUtils.mkdir_p(@recipes_path)
|
57
|
+
|
58
|
+
@logger.info "Downloading main cookbooks"
|
59
|
+
download_cookbooks
|
46
60
|
|
47
|
-
|
61
|
+
unless @user_recipes_url.nil?
|
62
|
+
if @opts[:run_custom]
|
63
|
+
@logger.info "Overlaying custom user cookbook recipes"
|
64
|
+
overlay_custom_cookbooks
|
65
|
+
else
|
66
|
+
@logger.info "Skipping overlay of custom cookbook recipes"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
else
|
70
|
+
@logger.info "Skipping download of new cookbooks - using existing cookbooks in #{@recipes_path}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def download_cookbooks
|
48
75
|
FileUtils.mkdir_p(@recipes_path)
|
76
|
+
result = %x[cd #{@recipes_path} && curl -fs '#{@main_recipes_url}' | tar -zxv]
|
77
|
+
if $?.exitstatus > 0
|
78
|
+
@logger.error "Failed to download and untar main cookbook."
|
79
|
+
@logger.error "Error output: #{result}"
|
80
|
+
raise DeployError, "Main cookbook download failed."
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def overlay_custom_cookbooks
|
85
|
+
Dir.mktmpdir do |tmpdir|
|
86
|
+
file=File.join(tmpdir,'user_recipes.tgz')
|
87
|
+
result = %x[curl -fs '#{@user_recipes_url}' -o #{file}]
|
88
|
+
if $?.exitstatus > 0
|
89
|
+
@logger.error "Failed to download user cookbook from #{@user_recipes_url}"
|
90
|
+
raise DeployError, "User cookbook download failed."
|
91
|
+
end
|
92
|
+
|
93
|
+
#Check COMPATIBILITY.txt file for release compatibilities
|
94
|
+
compat = %x[tar -azxf '#{file}' COMPATIBILITY.txt 2>/dev/null]
|
95
|
+
if $?.exitstatus == 0
|
96
|
+
# TODO: Determine how to compare against opts[:release_label]
|
97
|
+
# if not compatibible, log it, and raise DeployError, "Incompatible user cookbook found."
|
98
|
+
else
|
99
|
+
@api.report "WARNING: No COMPATIBILITY.txt file found in user cookbook."
|
100
|
+
end
|
101
|
+
|
102
|
+
result = %x[cd #{@recipes_path} && tar -zxvf #{file}]
|
103
|
+
if $?.exitstatus > 0
|
104
|
+
@logger.error "Failed to overlay user cookbook over main cookbook."
|
105
|
+
@logger.error "Error output: #{result}"
|
106
|
+
raise DeployError, "User cookbook overlay failed."
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
49
110
|
end
|
50
111
|
|
51
112
|
def execute
|
52
|
-
|
113
|
+
# Remove any stacktrace file from previous runs
|
114
|
+
stacktrace_file = File.join(@recipes_path,"chef-stacktrace.out")
|
115
|
+
FileUtils.rm_f stacktrace_file
|
116
|
+
tail = nil
|
117
|
+
if @logger.debug?
|
118
|
+
system("touch #{@chef_log}")
|
119
|
+
tail = Thread.new do
|
120
|
+
File.open(@chef_log, "r") do |fp|
|
121
|
+
while true do
|
122
|
+
select([fp])
|
123
|
+
$stderr << fp.read
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
command = "#{@opts[:chef_bin]} -j #{@chef_dna} -c #{@chef_config} > #{@chef_log} 2>&1"
|
53
129
|
@logger.debug "Running: #{command}"
|
54
130
|
if system(command)
|
55
131
|
@logger.info "Running telinit"
|
56
132
|
system("telinit q")
|
57
|
-
@logger.info "Finished
|
133
|
+
@logger.info "Finished chef run"
|
58
134
|
else
|
59
|
-
@logger.error("
|
60
|
-
@logger.error("
|
61
|
-
raise DeployError, "
|
135
|
+
@logger.error("Chef run failed. Reporting error")
|
136
|
+
@logger.error("Error output: #{chef_error_from_log}")
|
137
|
+
raise DeployError, "Chef run failed - see log file"
|
62
138
|
end
|
63
139
|
ensure
|
64
140
|
if File.exists?(@chef_log)
|
65
|
-
|
141
|
+
if File.exists?(stacktrace_file)
|
142
|
+
st_contents = File.read(stacktrace_file)
|
143
|
+
File.open(@chef_log, "a") do |l|
|
144
|
+
l.puts "\n-----------"
|
145
|
+
l.puts "Stack Trace (from #{stacktrace_file})"
|
146
|
+
l.puts "-----------\n\n#{st_contents}"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
FileUtils.ln_sf(@chef_log, "/var/log/chef.log")
|
66
150
|
end
|
151
|
+
tail && tail.kill
|
67
152
|
end
|
68
153
|
|
69
154
|
def upload
|
70
155
|
return unless File.exist?(@chef_log)
|
71
156
|
|
72
157
|
file = "#{@chef_log}.#{rand(1000)}.gz"
|
73
|
-
Zlib::GzipWriter.open(file)
|
158
|
+
Zlib::GzipWriter.open(file) do |io|
|
159
|
+
io << File.read(@chef_log)
|
160
|
+
end
|
74
161
|
|
75
|
-
unless @api.upload(
|
76
|
-
@logger.error "Failed to upload
|
77
|
-
raise UploadError, "
|
162
|
+
unless @api.upload('main', File.new(file))
|
163
|
+
@logger.error "Failed to upload chef log. Reporting error"
|
164
|
+
raise UploadError, "Failed to upload chef log"
|
78
165
|
end
|
79
166
|
rescue UploadError => error
|
167
|
+
@api.report error.message
|
80
168
|
@api.notify_error("logupload", error)
|
81
169
|
ensure
|
82
170
|
FileUtils.rm_f(file) if file
|
83
171
|
end
|
84
172
|
|
85
|
-
def main?
|
86
|
-
@name == "main"
|
87
|
-
end
|
88
|
-
|
89
|
-
def chef_config
|
90
|
-
main? ? "/etc/chef/solo.rb" : "/etc/chef-custom/solo.rb"
|
91
|
-
end
|
92
|
-
|
93
|
-
def chef_dna
|
94
|
-
main? ? '/etc/chef/dna.json' : '/etc/chef-custom/dna.json'
|
95
|
-
end
|
96
|
-
|
97
173
|
def chef_error_from_log
|
98
174
|
unless File.exist?(@chef_log)
|
99
175
|
return "(log doesn't exist)"
|
@@ -122,7 +198,9 @@ module EY::Enzyme
|
|
122
198
|
end
|
123
199
|
|
124
200
|
def local_instance_id
|
125
|
-
@local_instance_id ||= open(
|
201
|
+
@local_instance_id ||= URI.open("http://169.254.169.254/latest/meta-data/instance-id",
|
202
|
+
&:read)
|
126
203
|
end
|
204
|
+
|
127
205
|
end
|
128
206
|
end
|
@@ -1,11 +1,30 @@
|
|
1
1
|
module EY::Enzyme
|
2
2
|
class MultiLogger
|
3
3
|
|
4
|
+
SEVERITIES = {
|
5
|
+
:fatal => Logger::FATAL,
|
6
|
+
:error => Logger::ERROR,
|
7
|
+
:warn => Logger::WARN,
|
8
|
+
:info => Logger::INFO,
|
9
|
+
:debug => Logger::DEBUG,
|
10
|
+
}
|
11
|
+
|
4
12
|
def initialize(log_file)
|
5
13
|
@enzyme_log = Logger.new(log_file)
|
6
14
|
@stderr_log = Logger.new($stderr)
|
7
15
|
end
|
8
16
|
|
17
|
+
def level=(level)
|
18
|
+
@level = level
|
19
|
+
raise "Invalid log level: #{level}" unless SEVERITIES[level]
|
20
|
+
@enzyme_log.level = SEVERITIES[:debug]
|
21
|
+
@stderr_log.level = SEVERITIES[level]
|
22
|
+
end
|
23
|
+
|
24
|
+
def debug?
|
25
|
+
@level == :debug
|
26
|
+
end
|
27
|
+
|
9
28
|
def debug(message)
|
10
29
|
log(:debug, message)
|
11
30
|
end
|
data/lib/ey_enzyme/version.rb
CHANGED
data/lib/ey_enzyme.rb
CHANGED
@@ -3,16 +3,16 @@ require "open-uri"
|
|
3
3
|
require "zlib"
|
4
4
|
require "logger"
|
5
5
|
require "optparse"
|
6
|
+
require "fileutils"
|
6
7
|
|
7
8
|
require 'json'
|
8
9
|
require 'restclient'
|
9
10
|
|
10
11
|
module EY; end
|
12
|
+
require "ey_enzyme/api"
|
11
13
|
|
12
14
|
require "ey_enzyme/multi_logger"
|
13
|
-
require "ey_enzyme/api"
|
14
15
|
require "ey_enzyme/cli"
|
15
|
-
require "ey_enzyme/old_cli"
|
16
16
|
require "ey_enzyme/cookbook_set"
|
17
17
|
require "ey_enzyme/version"
|
18
18
|
|
metadata
CHANGED
@@ -1,160 +1,161 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: ey_enzyme
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
prerelease:
|
6
|
-
segments:
|
7
|
-
- 0
|
8
|
-
- 9
|
9
|
-
- 48
|
10
|
-
version: 0.9.48
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.0.7
|
11
5
|
platform: ruby
|
12
|
-
authors:
|
6
|
+
authors:
|
13
7
|
- Engine Yard Inc.
|
14
8
|
autorequire:
|
15
9
|
bindir: bin
|
16
10
|
cert_chain: []
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
- !ruby/object:Gem::Dependency
|
11
|
+
date: 2021-12-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
21
14
|
name: json
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.6.1
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 2.6.0
|
23
|
+
type: :runtime
|
22
24
|
prerelease: false
|
23
|
-
|
24
|
-
|
25
|
-
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.6.1
|
26
30
|
- - ">="
|
27
|
-
- !ruby/object:Gem::Version
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 2.6.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: mime-types
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 3.3.1
|
32
40
|
type: :runtime
|
33
|
-
version_requirements: *id001
|
34
|
-
- !ruby/object:Gem::Dependency
|
35
|
-
name: rest-client
|
36
41
|
prerelease: false
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 3.3.1
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rest-client
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.1'
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: 2.1.0
|
48
57
|
type: :runtime
|
49
|
-
version_requirements: *id002
|
50
|
-
- !ruby/object:Gem::Dependency
|
51
|
-
name: rake
|
52
58
|
prerelease: false
|
53
|
-
|
54
|
-
|
55
|
-
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - "~>"
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '2.1'
|
56
64
|
- - ">="
|
57
|
-
- !ruby/object:Gem::Version
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 2.1.0
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: rake
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
62
74
|
type: :development
|
63
|
-
version_requirements: *id003
|
64
|
-
- !ruby/object:Gem::Dependency
|
65
|
-
name: rspec
|
66
75
|
prerelease: false
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: rspec
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - "~>"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '2.14'
|
77
88
|
type: :development
|
78
|
-
version_requirements: *id004
|
79
|
-
- !ruby/object:Gem::Dependency
|
80
|
-
name: fakeweb
|
81
89
|
prerelease: false
|
82
|
-
|
83
|
-
|
84
|
-
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - "~>"
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '2.14'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: fakeweb
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
85
99
|
- - ">="
|
86
|
-
- !ruby/object:Gem::Version
|
87
|
-
|
88
|
-
segments:
|
89
|
-
- 0
|
90
|
-
version: "0"
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
91
102
|
type: :development
|
92
|
-
version_requirements: *id005
|
93
|
-
- !ruby/object:Gem::Dependency
|
94
|
-
name: fakeweb-matcher
|
95
103
|
prerelease: false
|
96
|
-
|
97
|
-
|
98
|
-
requirements:
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
99
106
|
- - ">="
|
100
|
-
- !ruby/object:Gem::Version
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
- !ruby/object:Gem::Dependency
|
110
|
+
name: fakeweb-matcher
|
111
|
+
requirement: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
105
116
|
type: :development
|
106
|
-
|
117
|
+
prerelease: false
|
118
|
+
version_requirements: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
107
123
|
description: Gem for kicking off chef recipes
|
108
124
|
email: ninja@engineyard.com
|
109
|
-
executables:
|
125
|
+
executables:
|
110
126
|
- ey-enzyme
|
111
|
-
- ey-recipes
|
112
127
|
extensions: []
|
113
|
-
|
114
128
|
extra_rdoc_files: []
|
115
|
-
|
116
|
-
|
129
|
+
files:
|
130
|
+
- bin/ey-enzyme
|
131
|
+
- lib/ey_enzyme.rb
|
117
132
|
- lib/ey_enzyme/api.rb
|
133
|
+
- lib/ey_enzyme/api/client.rb
|
118
134
|
- lib/ey_enzyme/cli.rb
|
119
135
|
- lib/ey_enzyme/cookbook_set.rb
|
120
136
|
- lib/ey_enzyme/multi_logger.rb
|
121
|
-
- lib/ey_enzyme/old_cli.rb
|
122
137
|
- lib/ey_enzyme/version.rb
|
123
|
-
|
124
|
-
- bin/ey-enzyme
|
125
|
-
- bin/ey-recipes
|
126
|
-
homepage: http://github.com/engineyard/ey_enzyme
|
138
|
+
homepage: https://github.com/engineyard/ey_enzyme
|
127
139
|
licenses: []
|
128
|
-
|
140
|
+
metadata: {}
|
129
141
|
post_install_message:
|
130
142
|
rdoc_options: []
|
131
|
-
|
132
|
-
require_paths:
|
143
|
+
require_paths:
|
133
144
|
- lib
|
134
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
135
|
-
|
136
|
-
requirements:
|
145
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
137
147
|
- - ">="
|
138
|
-
- !ruby/object:Gem::Version
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
version: "0"
|
143
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
144
|
-
none: false
|
145
|
-
requirements:
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
146
152
|
- - ">="
|
147
|
-
- !ruby/object:Gem::Version
|
148
|
-
|
149
|
-
segments:
|
150
|
-
- 0
|
151
|
-
version: "0"
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
152
155
|
requirements: []
|
153
|
-
|
154
156
|
rubyforge_project:
|
155
|
-
rubygems_version:
|
157
|
+
rubygems_version: 2.6.14
|
156
158
|
signing_key:
|
157
|
-
specification_version:
|
159
|
+
specification_version: 4
|
158
160
|
summary: Gem for kicking off chef recipes
|
159
161
|
test_files: []
|
160
|
-
|
data/bin/ey-recipes
DELETED
data/lib/ey_enzyme/old_cli.rb
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
module EY::Enzyme
|
2
|
-
class OldCLI
|
3
|
-
def self.run(args)
|
4
|
-
new_args = []
|
5
|
-
|
6
|
-
opts = OptionParser.new do |opts|
|
7
|
-
opts.version = "0.0.1"
|
8
|
-
|
9
|
-
opts.banner = "Usage: ey-recipes[-flag] [argument]"
|
10
|
-
opts.define_head "ey-recipes: running recipes..."
|
11
|
-
opts.separator '*'*80
|
12
|
-
|
13
|
-
opts.on("-c CONFIG", "--config CONFIG", "Use config file") do |config|
|
14
|
-
# NO-OP
|
15
|
-
end
|
16
|
-
|
17
|
-
opts.on("--deploy-main ENV", "Run main recipes") do |env_name|
|
18
|
-
new_args << "--main"
|
19
|
-
end
|
20
|
-
|
21
|
-
opts.on("-d ENV", "--deploy ENV", "Run custom recipes") do |env_name|
|
22
|
-
new_args << "--custom"
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
opts.parse!(args)
|
27
|
-
|
28
|
-
if new_args.any?
|
29
|
-
CLI.run(new_args)
|
30
|
-
else
|
31
|
-
$stderr.puts opts
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|