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