ey_enzyme 0.9.48 → 2.0.7

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 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
- # encoding: utf-8
1
+ require 'ey_enzyme/api/client'
2
2
 
3
- module EY::Enzyme
4
- class API
5
- def initialize(api_url, instance_id, token, log_file = "/var/log/enzyme.log")
6
- @api_url = api_url
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
- def call_api(path, opts={})
65
- @logger.debug "API call path: #{path.inspect}, data: #{opts.inspect}"
66
- JSON.parse(@rest[path].post(opts.merge(keys).to_json,
67
- {"Content-Type" => "application/json", "Accept" => "application/json"}))
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
- def api_rest_timeout_seconds
71
- 60
72
- end
14
+ def self.client
15
+ raise Unconfigured unless @client
16
+ @client
17
+ end
73
18
 
74
- def api_max_total_seconds
75
- 10 * 60
76
- end
19
+ def self.report(message)
20
+ client.report(message)
21
+ end
77
22
 
78
- def api_sleep_before_retry_seconds
79
- 30
80
- end
23
+ def self.notify_success
24
+ client.notify_success
25
+ end
81
26
 
82
- def retry_api(path, opts={})
83
- Timeout.timeout(api_max_total_seconds) do
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
- def safe_api(path, opts={})
96
- begin
97
- return call_api(path, opts)
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
- api_error(path, e)
109
- return false
35
+ def self.custom_recipe_url
36
+ client.custom_recipe_url
110
37
  end
111
- end
112
38
 
113
- def api_error(path, error)
114
- @logger.exception("API request failed", error)
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 = {:config => '/etc/engineyard/dracul.yml', :keep => 5, :chef_bin => 'chef-solo',
9
- :logfile => "/var/log/enzyme.log"}
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 [-flag] [argument]"
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("--quick", "Set the recipes to run in quick mode") do
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("--chef-bin PATH", "Use this chef-solo executable to run recipes") do |binary|
31
- options[:chef_bin] = binary
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("--main", "Run main recipes") do
35
- options[:type] = :main
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("--custom", "Run custom recipes") do
39
- options[:type] = :custom
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[:report] = true
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
- abort opts.to_s
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
- @api = API.new(opts[:api], opts[:instance_id], opts[:token], opts[:logfile])
84
- @logger = MultiLogger.new(opts[:logfile])
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
- json = update_dna
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 run_main
123
- report 'running main recipes'
124
- main_cookbooks.run
147
+ def cookbook_set
148
+ CookbookSet.new(@api, @opts)
125
149
  end
126
150
 
127
- def run_custom(json)
128
- report 'running custom recipes if they exist'
129
- url = @opts.has_key?(:custom_recipes_url) ? @opts[:custom_recipes_url] : @api.custom_recipe_url
130
- update_custom_dna(json) if url
151
+ private
152
+
153
+ def run_cookbooks
154
+ report "Initializing cookbook run"
155
+ cookbook_set.run
156
+ end
131
157
 
132
- @custom_cookbooks ||= CookbookSet.new(@opts, "custom", CUSTOM_RECIPE_PATH, url, @api)
158
+ ## Resovlve resolv.conf stuff
133
159
 
134
- @custom_cookbooks.run
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 main_cookbooks
138
- @main_cookbooks ||= CookbookSet.new(@opts, "main", MAIN_RECIPE_PATH, @opts[:recipes_url], @api)
167
+ def resolv_conf
168
+ File.read('/etc/resolv.conf')
139
169
  end
140
170
 
141
- def update_dna
142
- @logger.debug "Getting instance's DNA"
171
+ def does_hostname_resolve?
172
+ hostname = `/bin/hostname --fqdn 2>/dev/null`
173
+ ! hostname.empty?
174
+ end
143
175
 
144
- unless json = @api.dna
145
- raise DNAError, "failed to fetch DNA"
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 update_main_dna(json)
154
- main_json = json.dup
155
- main_json['run_list'] = 'recipe[ey-base]'
156
- main_json["quick"] = true if @opts[:quick]
157
- update_chef_dna("/etc/chef/dna.json", main_json)
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
- def update_custom_dna(json)
161
- custom_json = json.dup
162
- custom_json['run_list'] = 'recipe[main]'
163
- # Make sure the path exist, it's soooo wrong to write this file before the cookbooks have been downloaded nor the config written
164
- FileUtils.mkdir_p('/etc/chef-custom/') unless File.exists?('/etc/chef-custom/')
165
- update_chef_dna("/etc/chef-custom/dna.json", custom_json)
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 update_chef_dna(file, json)
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
- def initialize(opts, name, recipes_path, recipes_url, api)
4
- @opts = opts
5
- @name = name
6
- @recipes_path = recipes_path
7
- @logger = MultiLogger.new(opts[:logfile])
8
- @timestamp = Time.now.strftime("%Y-%m-%dT%H-%M-%S")
9
- @chef_log = "/var/log/chef.#{name}.#{@timestamp}.log"
10
- @recipes_url = recipes_url
11
- @api = api
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
- unless @recipes_url
16
- @logger.info "No recipes for #{@name} chef run. Skipping"
17
- return
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 #{@name} chef run"
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
- FileUtils.mkdir_p('/etc/chef-custom/') unless File.exists?('/etc/chef-custom/')
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
- @logger.info "Removing #{@name} cookbooks"
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
- FileUtils.rm_rf(@recipes_path)
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
- command = "#{@opts[:chef_bin]} -j #{chef_dna} -c #{chef_config} -r \"#{@recipes_url}\" > #{@chef_log} 2>&1"
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 #{@name} chef run"
133
+ @logger.info "Finished chef run"
58
134
  else
59
- @logger.error("#{@name} chef run failed. Reporting error")
60
- @logger.error("#{@name} error output: #{chef_error_from_log}")
61
- raise DeployError, "#{@name} chef run failed"
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
- FileUtils.ln_sf(@chef_log, "/var/log/chef.#{@name}.log")
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) { |io| io << File.read(@chef_log) }
158
+ Zlib::GzipWriter.open(file) do |io|
159
+ io << File.read(@chef_log)
160
+ end
74
161
 
75
- unless @api.upload(@name, File.read(file))
76
- @logger.error "Failed to upload #{@name} log. Reporting error"
77
- raise UploadError, "failed to upload #{@name} log"
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('http://169.254.169.254/latest/meta-data/instance-id').read
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
@@ -1,5 +1,5 @@
1
1
  module EY
2
2
  module Enzyme
3
- VERSION = "0.9.48"
3
+ VERSION = "2.0.7"
4
4
  end
5
5
  end
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
- hash: 91
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
- date: 2010-02-25 00:00:00 Z
19
- dependencies:
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
- requirement: &id001 !ruby/object:Gem::Requirement
24
- none: false
25
- requirements:
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
- hash: 3
29
- segments:
30
- - 0
31
- version: "0"
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
- requirement: &id002 !ruby/object:Gem::Requirement
38
- none: false
39
- requirements:
40
- - - ~>
41
- - !ruby/object:Gem::Version
42
- hash: 13
43
- segments:
44
- - 1
45
- - 6
46
- - 1
47
- version: 1.6.1
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
- requirement: &id003 !ruby/object:Gem::Requirement
54
- none: false
55
- requirements:
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
- hash: 3
59
- segments:
60
- - 0
61
- version: "0"
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
- requirement: &id004 !ruby/object:Gem::Requirement
68
- none: false
69
- requirements:
70
- - - ~>
71
- - !ruby/object:Gem::Version
72
- hash: 11
73
- segments:
74
- - 1
75
- - 2
76
- version: "1.2"
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
- requirement: &id005 !ruby/object:Gem::Requirement
83
- none: false
84
- requirements:
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
- hash: 3
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
- requirement: &id006 !ruby/object:Gem::Requirement
97
- none: false
98
- requirements:
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
99
106
  - - ">="
100
- - !ruby/object:Gem::Version
101
- hash: 3
102
- segments:
103
- - 0
104
- version: "0"
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
- version_requirements: *id006
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
- files:
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
- - lib/ey_enzyme.rb
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
- none: false
136
- requirements:
145
+ required_ruby_version: !ruby/object:Gem::Requirement
146
+ requirements:
137
147
  - - ">="
138
- - !ruby/object:Gem::Version
139
- hash: 3
140
- segments:
141
- - 0
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
- hash: 3
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: 1.8.6
157
+ rubygems_version: 2.6.14
156
158
  signing_key:
157
- specification_version: 3
159
+ specification_version: 4
158
160
  summary: Gem for kicking off chef recipes
159
161
  test_files: []
160
-
data/bin/ey-recipes DELETED
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'ey_enzyme'
4
-
5
- EY::Enzyme::OldCLI.run(ARGV)
@@ -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