knife-stackbuilder 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,442 @@
1
+ # Copyright (c) 2014 Mevan Samaratunga
2
+
3
+ include StackBuilder::Common::Helpers
4
+
5
+ module StackBuilder::Chef
6
+
7
+ class RepoNotFoundError < StackBuilder::Common::StackBuilderError; end
8
+ class InvalidRepoError < StackBuilder::Common::StackBuilderError; end
9
+
10
+ class Repo
11
+
12
+ include ERB::Util
13
+
14
+ attr_reader :environments
15
+
16
+ REPO_DIRS = [
17
+ 'etc',
18
+ 'cookbooks',
19
+ 'environments',
20
+ 'secrets',
21
+ 'data_bags',
22
+ 'roles'
23
+ ]
24
+
25
+ def initialize(path, certificates = nil, environments = nil, cookbooks = nil)
26
+
27
+ raise StackBuilder::Common::StackBuilderError, "Repo path cannot be nil." if path.nil?
28
+
29
+ @logger = StackBuilder::Common::Config.logger
30
+ @repo_path = File.expand_path(path)
31
+
32
+ if Dir.exist?(@repo_path)
33
+
34
+ REPO_DIRS.each do |folder|
35
+
36
+ next if [ 'cookbooks' ].include?(folder)
37
+
38
+ repo_folder = "#{@repo_path}/#{folder}"
39
+ raise InvalidRepoError,
40
+ "Repo folder #{repo_folder} is missing" unless Dir.exist?(repo_folder)
41
+ end
42
+
43
+ @environments = [ ]
44
+ Dir["#{@repo_path}/environments/**/*.rb"].each do |envfile|
45
+ @environments << envfile[/\/(\w+).rb$/, 1]
46
+ end
47
+
48
+ @logger.debug("Found stack environments #{@environments}")
49
+ else
50
+ raise RepoNotFoundError,
51
+ "Unable to load repo @ #{@repo_path}. If you need to create a repo please " +
52
+ "provide the list of environments at a minimum" if environments.nil?
53
+
54
+ REPO_DIRS.each do |folder|
55
+ system("mkdir -p #{@repo_path}/#{folder}")
56
+ end
57
+
58
+ # Create Berksfile
59
+ @berks_cookbooks = cookbooks.nil? ? [] : cookbooks.split(',').map { |s| s.strip.split(':') }
60
+ berksfile_template = IO.read(File.expand_path('../../resources/Berksfile.erb', __FILE__))
61
+
62
+ berksfile = ERB.new(berksfile_template, nil, '-<>').result(binding)
63
+ File.open("#{@repo_path}/Berksfile", 'w+') { |f| f.write(berksfile) }
64
+
65
+ # Create Environments and Stacks
66
+ @environments = environments.split(',').map { |s| s.strip }
67
+ configfile_template = IO.read(File.expand_path('../../resources/Config.yml.erb', __FILE__))
68
+ envfile_template = IO.read(File.expand_path('../../resources/Environment.rb.erb', __FILE__))
69
+ stackfile_template = IO.read(File.expand_path('../../resources/Stack.yml.erb', __FILE__))
70
+
71
+ i = 1
72
+ @environments.each do |env_name|
73
+
74
+ @environment = env_name
75
+
76
+ configfile = ERB.new(configfile_template, nil, '-<>').result(binding)
77
+ File.open("#{@repo_path}/etc/#{env_name}.yml", 'w+') { |f| f.write(configfile) }
78
+
79
+ envfile = ERB.new(envfile_template, nil, '-<>').result(binding)
80
+ File.open("#{@repo_path}/environments/#{env_name}.rb", 'w+') { |f| f.write(envfile) }
81
+
82
+ stackfile = ERB.new(stackfile_template, nil, '-<>').result(binding)
83
+ File.open("#{@repo_path}/stack#{i}.yml", 'w+') { |f| f.write(stackfile) }
84
+ i += 1
85
+ end
86
+ @environment = nil
87
+
88
+ # Create or copy certs
89
+ create_certs(certificates) unless certificates.nil?
90
+ end
91
+ end
92
+
93
+ def upload_environments(environment = nil)
94
+
95
+ environments = (environment.nil? ? @environments : [ environment ])
96
+ knife_cmd = Chef::Knife::EnvironmentFromFile.new
97
+
98
+ environments.each do |env_name|
99
+
100
+ # TODO: Handle JSON environment files. JSON files should be processed similar to roles.
101
+
102
+ knife_cmd.name_args = [ "#{@repo_path}/environments/#{env_name}.rb" ]
103
+ run_knife(knife_cmd)
104
+ puts "Uploaded environment '#{env_name}' to '#{Chef::Config.chef_server_url}'."
105
+ end
106
+ end
107
+
108
+ def upload_certificates(environment = nil, server = nil)
109
+
110
+ create_certs(server) unless server.nil?
111
+
112
+ knife_cmd = Chef::Knife::DataBagList.new
113
+ data_bag_list = run_knife(knife_cmd).split
114
+
115
+ # Create environment specific data bags to hold certificates
116
+ @environments.each do |env_name|
117
+
118
+ data_bag_env = 'certificates-' + env_name
119
+ unless data_bag_list.include?(data_bag_env)
120
+ knife_cmd = Chef::Knife::DataBagCreate.new
121
+ knife_cmd.name_args = data_bag_env
122
+ run_knife(knife_cmd)
123
+ end
124
+ end
125
+
126
+ Dir["#{@repo_path}/.certs/*"].each do |server_cert_dir|
127
+
128
+ if File.directory?(server_cert_dir)
129
+
130
+ s = server_cert_dir.split('/').last
131
+
132
+ server_env_name = s[/.*_(\w+)$/, 1]
133
+ server_name = server_env_name.nil? ? s : s[/(.*)_\w+$/, 1]
134
+
135
+ if server.nil? || server==server_name
136
+
137
+ if server_env_name.nil?
138
+
139
+ environments = (environment.nil? ? @environments : [ environment ])
140
+ environments.each do |env_name|
141
+ upload_certificate(server_cert_dir, server_name, env_name)
142
+ end
143
+
144
+ elsif environment.nil? || environment==server_env_name
145
+ upload_certificate(server_cert_dir, server_name, server_env_name)
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ def upload_data_bags(environment = nil, data_bag = nil)
153
+
154
+ environments = (environment.nil? ? @environments : [ environment ])
155
+
156
+ knife_cmd = Chef::Knife::DataBagList.new
157
+ data_bag_list = run_knife(knife_cmd).split
158
+
159
+ Dir["#{@repo_path}/data_bags/*"].each do |data_bag_dir|
160
+
161
+ data_bag_name = data_bag_dir[/\/(\w+)$/, 1]
162
+ if data_bag.nil? || data_bag==data_bag_name
163
+
164
+ environments.each do |env_name|
165
+
166
+ data_bag_env = data_bag_name + '-' + env_name
167
+ unless data_bag_list.include?(data_bag_env)
168
+ knife_cmd = Chef::Knife::DataBagCreate.new
169
+ knife_cmd.name_args = data_bag_env
170
+ run_knife(knife_cmd)
171
+ end
172
+
173
+ env_file = "#{@repo_path}/etc/#{env_name}.yml"
174
+ env_vars = File.exist?(env_file) ?
175
+ StackBuilder::Common.load_yaml("#{@repo_path}/etc/#{env_name}.yml", ENV) : { }
176
+
177
+ secret = get_secret(env_name)
178
+
179
+ upload_data_bag_items(secret, data_bag_dir, data_bag_env, env_vars)
180
+
181
+ env_item_dir = data_bag_dir + '/' + env_name
182
+ upload_data_bag_items(secret, env_item_dir, data_bag_env, env_vars) if Dir.exist?(env_item_dir)
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ def upload_cookbooks(cookbook = nil, upload_options = '--no-freeze')
189
+
190
+ berksfile_path = "#{@repo_path}/Berksfile"
191
+ debug_flag = (@logger.debug? ? ' --debug' : '')
192
+
193
+ # Need to invoke Berkshelf from the shell as directly invoking it causes
194
+ # cookbook validation to throw an exception when 'Berksfile.upload' is
195
+ # called.
196
+ #
197
+ # TBD: More research needs to be done as direct invocation is preferable
198
+
199
+ cmd = ""
200
+
201
+ cmd += "export BERKSHELF_CHEF_CONFIG=#{ENV['BERKSHELF_CHEF_CONFIG']}; " \
202
+ if ENV.has_key?('BERKSHELF_CHEF_CONFIG')
203
+
204
+ if cookbook.nil?
205
+ cmd += "berks install#{debug_flag} --berksfile=#{berksfile_path}; "
206
+ cmd += "berks upload#{debug_flag} --berksfile=#{berksfile_path} #{upload_options}; "
207
+ else
208
+ cmd += "berks upload#{debug_flag} --berksfile=#{berksfile_path} #{upload_options} #{cookbook}; "
209
+ end
210
+
211
+ system(cmd)
212
+ end
213
+
214
+ def upload_roles(role = nil)
215
+
216
+ if role.nil?
217
+ Dir["#{@repo_path}/roles/*.json"].each do |role_file|
218
+ upload_role(role_file)
219
+ end
220
+ else
221
+ role_file = "#{@repo_path}/roles/#{role}.json"
222
+ upload_role(role_file) if File.exist?(role_file)
223
+ end
224
+ end
225
+
226
+ def get_secret(env_name)
227
+
228
+ secretfile = "#{@repo_path}/secrets/#{env_name}"
229
+ if File.exists?(secretfile)
230
+ key = IO.read(secretfile)
231
+ else
232
+ key = SecureRandom.uuid()
233
+ File.open("#{@repo_path}/secrets/#{env_name}", 'w+') { |f| f.write(key) }
234
+ end
235
+
236
+ key
237
+ end
238
+
239
+ private
240
+
241
+ def create_certs(certificates)
242
+
243
+ repo_cert_dir = @repo_path + '/.certs'
244
+ FileUtils.mkdir_p(repo_cert_dir)
245
+
246
+ if Dir.exist?(certificates)
247
+
248
+ raise CertificateError, "Unable to locate public CA certificate for server " +
249
+ "@#{certificates}/cacert.pem" unless File.exist?("#{certificates}/cacert.pem")
250
+
251
+ system("rsync -ru #{certificates}/* #{repo_cert_dir}")
252
+ else
253
+
254
+ cacert_file = "#{repo_cert_dir}/cacert.pem"
255
+ cakey_file = "#{repo_cert_dir}/cakey.pem"
256
+
257
+ if File.exist?(cacert_file) && File.exist?(cakey_file)
258
+
259
+ ca_cert = OpenSSL::X509::Certificate.new(IO.read(cacert_file))
260
+ ca_key = OpenSSL::PKey::RSA.new(IO.read(cakey_file))
261
+ else
262
+ ca_key = OpenSSL::PKey::RSA.new(2048)
263
+ File.open(cakey_file, 'w+') { |f| f.write(ca_key.to_pem) }
264
+
265
+ ca_subject = "/CN=ca/DC=stackbuilder.org"
266
+ ca_cert = create_ca_cert(ca_key, ca_subject)
267
+ File.open(cacert_file, 'w+') { |f| f.write(ca_cert.to_pem) }
268
+ end
269
+
270
+ servers = certificates.split(',')
271
+ servers.each do |server|
272
+
273
+ server_dir = "#{repo_cert_dir}/#{server}"
274
+ server_cert_file = "#{server_dir}/cert.pem"
275
+ server_key_file = "#{server_dir}/key.pem"
276
+
277
+ unless File.exist?(server_cert_file) && File.exist?(server_key_file)
278
+
279
+ server_key = OpenSSL::PKey::RSA.new(2048)
280
+ server_subject = "/C=US/O=#{server}/OU=Chef Community/CN=#{server}"
281
+ server_cert = create_server_cert(create_csr(server_key, server_subject), ca_key, ca_cert)
282
+
283
+ FileUtils.mkdir_p(server_dir)
284
+
285
+ File.open(server_cert_file, 'w+') { |f| f.write(server_cert.to_pem) }
286
+ File.open(server_key_file, 'w+') { |f| f.write(server_key.to_pem) }
287
+ end
288
+ end
289
+ end
290
+ end
291
+
292
+ def create_ca_cert(ca_key, ca_subject)
293
+
294
+ ca_cert = create_cert(ca_key, ca_subject)
295
+
296
+ extension_factory = OpenSSL::X509::ExtensionFactory.new
297
+ extension_factory.subject_certificate = ca_cert
298
+ extension_factory.issuer_certificate = ca_cert
299
+
300
+ ca_cert.add_extension extension_factory
301
+ .create_extension('subjectKeyIdentifier', 'hash')
302
+ ca_cert.add_extension extension_factory
303
+ .create_extension('basicConstraints', 'CA:TRUE', true)
304
+ ca_cert.add_extension extension_factory
305
+ .create_extension('keyUsage', 'cRLSign,keyCertSign', true)
306
+
307
+ ca_cert.sign ca_key, OpenSSL::Digest::SHA256.new
308
+
309
+ ca_cert
310
+ end
311
+
312
+ def create_csr(key, subject)
313
+
314
+ csr = OpenSSL::X509::Request.new
315
+ csr.version = 0
316
+ csr.subject = OpenSSL::X509::Name.parse(subject)
317
+ csr.public_key = key.public_key
318
+ csr.sign key, OpenSSL::Digest::SHA256.new
319
+
320
+ csr
321
+ end
322
+
323
+ def create_server_cert(csr, ca_key, ca_cert)
324
+
325
+ csr_cert = create_cert(csr.public_key, csr.subject, ca_cert.subject)
326
+
327
+ extension_factory = OpenSSL::X509::ExtensionFactory.new
328
+ extension_factory.subject_certificate = csr_cert
329
+ extension_factory.issuer_certificate = ca_cert
330
+
331
+ csr_cert.add_extension extension_factory
332
+ .create_extension('basicConstraints', 'CA:FALSE')
333
+ csr_cert.add_extension extension_factory
334
+ .create_extension('keyUsage', 'keyEncipherment,dataEncipherment,digitalSignature')
335
+ csr_cert.add_extension extension_factory
336
+ .create_extension('subjectKeyIdentifier', 'hash')
337
+
338
+ csr_cert.sign ca_key, OpenSSL::Digest::SHA256.new
339
+
340
+ csr_cert
341
+ end
342
+
343
+ def create_cert(key, subject, issuer = nil)
344
+
345
+ cert = OpenSSL::X509::Certificate.new
346
+ cert.serial = 0x0
347
+ cert.version = 2
348
+ cert.not_before = Time.now
349
+ cert.not_after = Time.now + (10 * 365 * 24 * 60 * 60) # 10 years
350
+
351
+ cert.public_key = key.public_key
352
+
353
+ cert.subject = subject.is_a?(OpenSSL::X509::Name) ?
354
+ subject : OpenSSL::X509::Name.parse(subject)
355
+
356
+ cert.issuer = issuer.is_a?(OpenSSL::X509::Name) ?
357
+ issuer : OpenSSL::X509::Name.parse(issuer.nil? ? subject : issuer)
358
+
359
+ cert
360
+ end
361
+
362
+ def upload_certificate(server_cert_dir, server_name, server_env_name)
363
+
364
+ data_bag_name = 'certificates-' + server_env_name
365
+
366
+ data_bag_item = {
367
+ 'id' => server_name,
368
+ 'cacert' => IO.read(server_cert_dir + "/../cacert.pem"),
369
+ 'cert' => IO.read(server_cert_dir + "/cert.pem"),
370
+ 'key' => IO.read(server_cert_dir + "/key.pem") }
371
+
372
+ tmpfile = "#{Dir.tmpdir}/#{server_name}.json"
373
+ File.open("#{tmpfile}", 'w+') { |f| f.write(data_bag_item.to_json) }
374
+
375
+ knife_cmd = Chef::Knife::DataBagFromFile.new
376
+ knife_cmd.name_args = [ data_bag_name, tmpfile ]
377
+ knife_cmd.config[:secret] = get_secret(server_env_name)
378
+ run_knife(knife_cmd)
379
+
380
+ puts "Uploaded '#{server_env_name}' certificate for server '#{server_name}' " +
381
+ "to data bag '#{data_bag_name}' at '#{Chef::Config.chef_server_url}'."
382
+
383
+ rescue Exception => msg
384
+ @logger.error(msg)
385
+ ensure
386
+ File.delete(tmpfile) unless tmpfile.nil? || !File.exist?(tmpfile)
387
+ end
388
+
389
+ def upload_data_bag_items(secret, path, data_bag_name, env_vars)
390
+
391
+ tmpfile = nil
392
+
393
+ Dir["#{path}/*.json"].each do |data_bag_file|
394
+
395
+ data_bag_item = eval_map_values(JSON.load(File.new(data_bag_file, 'r')), env_vars, data_bag_file)
396
+
397
+ data_bag_item_name = data_bag_item['id']
398
+ @logger.debug("Uploading data bag '#{data_bag_item_name}' with contents:\n#{data_bag_item.to_yaml}")
399
+
400
+ tmpfile = "#{Dir.tmpdir}/#{data_bag_item_name}.json"
401
+ File.open("#{tmpfile}", 'w+') { |f| f.write(data_bag_item.to_json) }
402
+
403
+ knife_cmd = Chef::Knife::DataBagFromFile.new
404
+ knife_cmd.name_args = [ data_bag_name, tmpfile ]
405
+ knife_cmd.config[:secret] = secret
406
+ run_knife(knife_cmd)
407
+
408
+ File.delete(tmpfile)
409
+
410
+ puts "Uploaded item '#{data_bag_item_name}' of data bag " +
411
+ "'#{data_bag_name}' to '#{Chef::Config.chef_server_url}'."
412
+ end
413
+
414
+ rescue Exception => msg
415
+ @logger.error(msg)
416
+ ensure
417
+ File.delete(tmpfile) unless tmpfile.nil? || !File.exist?(tmpfile)
418
+ end
419
+
420
+ def upload_role(role_file)
421
+
422
+ role_content = eval_map_values(JSON.load(File.new(role_file, 'r')), ENV)
423
+
424
+ role_name = role_content.is_a?(Chef::Role) ? role_content.name : role_content['name']
425
+ @logger.debug("Uploading role '#{role_name}' with contents:\n#{role_content.to_yaml}")
426
+
427
+ tmpfile = "#{Dir.tmpdir}/#{role_name}.json"
428
+ File.open("#{tmpfile}", 'w+') { |f| f.write(role_content.to_json) }
429
+
430
+ knife_cmd = Chef::Knife::RoleFromFile.new
431
+ knife_cmd.name_args = [ tmpfile ]
432
+ run_knife(knife_cmd)
433
+
434
+ puts "Uploaded role '#{role_name}' to '#{Chef::Config.chef_server_url}'."
435
+
436
+ rescue Exception => msg
437
+ @logger.error(msg)
438
+ ensure
439
+ File.delete(tmpfile) unless tmpfile.nil? || !File.exist?(tmpfile)
440
+ end
441
+ end
442
+ end
@@ -0,0 +1,67 @@
1
+ # Copyright (c) 2014 Mevan Samaratunga
2
+
3
+ include StackBuilder::Common::Helpers
4
+
5
+ module StackBuilder::Chef
6
+
7
+ class GenericNodeManager < StackBuilder::Chef::NodeManager
8
+
9
+ def create_vm(name, knife_config)
10
+
11
+ create_class_name = knife_config['create']['class']
12
+ raise ArgumentError, "Knife plugin's server 'create' class name not provided." \
13
+ if create_class_name.nil?
14
+
15
+ knife_cmd = eval(create_class_name + '.new')
16
+
17
+ if knife_config['create'].has_key?('name_key')
18
+ name_key = knife_config['create']['name_key']
19
+ knife_cmd.config[name_key.to_sym] = name
20
+ else
21
+ knife_cmd.name_args = [ name ]
22
+ end
23
+
24
+ config_knife(knife_cmd, knife_config['create']['options'] || { })
25
+ config_knife(knife_cmd, knife_config['options'] || { })
26
+
27
+ if knife_config['create']['synchronized']
28
+ @@sync ||= Mutex.new
29
+ @@sync.synchronize {
30
+ run_knife(knife_cmd, knife_config['create']['retries'] || 0)
31
+ }
32
+ else
33
+ run_knife(knife_cmd, knife_config['create']['retries'] || 0)
34
+ end
35
+ end
36
+
37
+ def delete_vm(name, knife_config)
38
+
39
+ return unless knife_config.has_key?('delete')
40
+
41
+ delete_class_name = knife_config['delete']['class']
42
+ raise ArgumentError, "Knife plugin's server 'delete' class name not provided." \
43
+ if delete_class_name.nil?
44
+
45
+ knife_cmd = eval(delete_class_name + '.new')
46
+
47
+ if knife_config['delete'].has_key?('name_key')
48
+ name_key = knife_config['create']['name_key']
49
+ knife_cmd.config[name_key.to_sym] = name
50
+ else
51
+ knife_cmd.name_args = [ name ]
52
+ end
53
+
54
+ config_knife(knife_cmd, knife_config['delete']['options'] || { })
55
+ config_knife(knife_cmd, knife_config['options'] || { })
56
+
57
+ if knife_config['delete']['synchronized']
58
+ @@sync ||= Mutex.new
59
+ @@sync.synchronize {
60
+ run_knife(knife_cmd, knife_config['delete']['retries'] || 0)
61
+ }
62
+ else
63
+ run_knife(knife_cmd, knife_config['delete']['retries'] || 0)
64
+ end
65
+ end
66
+ end
67
+ end