knife-stackbuilder 0.5.2

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