formatron 0.1.14 → 0.1.15

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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/formatron.gemspec +2 -1
  3. data/lib/formatron.rb +23 -10
  4. data/lib/formatron/aws.rb +26 -13
  5. data/lib/formatron/chef.rb +53 -9
  6. data/lib/formatron/chef/berkshelf.rb +15 -11
  7. data/lib/formatron/chef/keys.rb +5 -9
  8. data/lib/formatron/chef/knife.rb +39 -30
  9. data/lib/formatron/chef/ssh.rb +4 -1
  10. data/lib/formatron/chef/winrm.rb +37 -0
  11. data/lib/formatron/chef_clients.rb +6 -4
  12. data/lib/formatron/cli/generators/credentials.rb +1 -1
  13. data/lib/formatron/cloud_formation/resources/ec2.rb +58 -57
  14. data/lib/formatron/cloud_formation/scripts.rb +75 -3
  15. data/lib/formatron/cloud_formation/template.rb +6 -0
  16. data/lib/formatron/cloud_formation/template/vpc.rb +8 -0
  17. data/lib/formatron/cloud_formation/template/vpc/subnet.rb +8 -0
  18. data/lib/formatron/cloud_formation/template/vpc/subnet/bastion.rb +12 -0
  19. data/lib/formatron/cloud_formation/template/vpc/subnet/chef_server.rb +12 -0
  20. data/lib/formatron/cloud_formation/template/vpc/subnet/instance.rb +13 -2
  21. data/lib/formatron/cloud_formation/template/vpc/subnet/instance/security_group.rb +47 -2
  22. data/lib/formatron/cloud_formation/template/vpc/subnet/instance/setup.rb +92 -27
  23. data/lib/formatron/cloud_formation/template/vpc/subnet/nat.rb +12 -0
  24. data/lib/formatron/dsl/formatron/global.rb +2 -0
  25. data/lib/formatron/dsl/formatron/global/windows.rb +17 -0
  26. data/lib/formatron/dsl/formatron/vpc/subnet/instance.rb +1 -0
  27. data/lib/formatron/external/dsl.rb +19 -0
  28. data/lib/formatron/util/winrm.rb +40 -0
  29. data/lib/formatron/version.rb +1 -1
  30. metadata +33 -16
@@ -5,19 +5,18 @@ class Formatron
5
5
  # Download the Chef Server keys
6
6
  class Keys
7
7
  # rubocop:disable Metrics/ParameterLists
8
- def initialize(aws:, bucket:, name:, target:, guid:, ec2_key:)
8
+ def initialize(directory:, aws:, bucket:, name:, target:, guid:, ec2_key:)
9
9
  @aws = aws
10
10
  @bucket = bucket
11
11
  @name = name
12
12
  @target = target
13
13
  @guid = guid
14
14
  @ec2_key = ec2_key
15
+ @directory = directory
15
16
  end
16
17
  # rubocop:enable Metrics/ParameterLists
17
18
 
18
- # rubocop:disable Metrics/MethodLength
19
19
  def init
20
- @directory = Dir.mktmpdir 'formatron-chef-server-keys-'
21
20
  S3::ChefServerKeys.get(
22
21
  aws: @aws,
23
22
  bucket: @bucket,
@@ -29,23 +28,20 @@ class Formatron
29
28
  File.write ec2_key, @ec2_key
30
29
  File.chmod 0600, ec2_key
31
30
  end
32
- # rubocop:enable Metrics/MethodLength
33
31
 
34
32
  def user_key
35
33
  S3::ChefServerKeys.user_pem_path directory: @directory
36
34
  end
37
35
 
38
36
  def organization_key
39
- S3::ChefServerKeys.organization_pem_path directory: @directory
37
+ S3::ChefServerKeys.organization_pem_path(
38
+ directory: @directory
39
+ )
40
40
  end
41
41
 
42
42
  def ec2_key
43
43
  File.join @directory, 'ec2_key'
44
44
  end
45
-
46
- def unlink
47
- FileUtils.rm_rf @directory unless @directory.nil?
48
- end
49
45
  end
50
46
  end
51
47
  end
@@ -7,10 +7,18 @@ class Formatron
7
7
  # Wrapper for the knife cli
8
8
  # rubocop:disable Metrics/ClassLength
9
9
  class Knife
10
+ CONFIG_FILE = 'knife.rb'
11
+ DATABAG_SECRET_FILE = 'databag_secret'
12
+ DATABAG_DIRECTORY = 'databag'
13
+ DATABAG_ITEM_SUFFIX = '.json'
14
+
10
15
  # rubocop:disable Metrics/MethodLength
11
16
  # rubocop:disable Metrics/ParameterLists
12
17
  def initialize(
18
+ directory:,
13
19
  keys:,
20
+ administrator_name:,
21
+ administrator_password:,
14
22
  chef_server_url:,
15
23
  username:,
16
24
  organization:,
@@ -18,7 +26,12 @@ class Formatron
18
26
  databag_secret:,
19
27
  configuration:
20
28
  )
29
+ @knife_file = File.join directory, CONFIG_FILE
30
+ @databag_secret_file = File.join directory, DATABAG_SECRET_FILE
31
+ @databag_directory = File.join directory, DATABAG_DIRECTORY
21
32
  @keys = keys
33
+ @administrator_name = administrator_name
34
+ @administrator_password = administrator_password
22
35
  @chef_server_url = chef_server_url
23
36
  @username = username
24
37
  @organization = organization
@@ -31,8 +44,7 @@ class Formatron
31
44
 
32
45
  # rubocop:disable Metrics/MethodLength
33
46
  def init
34
- @knife_file = Tempfile.new 'formatron-knife-'
35
- @knife_file.write <<-EOH.gsub(/^ {10}/, '')
47
+ File.write @knife_file, <<-EOH.gsub(/^ {10}/, '')
36
48
  chef_server_url '#{@chef_server_url}'
37
49
  validation_client_name '#{@organization}-validator'
38
50
  validation_key '#{@keys.organization_key}'
@@ -41,28 +53,25 @@ class Formatron
41
53
  verify_api_cert #{@ssl_verify}
42
54
  ssl_verify_mode #{@ssl_verify ? ':verify_peer' : ':verify_none'}
43
55
  EOH
44
- @knife_file.close
45
- @databag_secret_file = Tempfile.new 'formatron-databag-secret-'
46
- @databag_secret_file.write @databag_secret
47
- @databag_secret_file.close
56
+ File.write @databag_secret_file, @databag_secret
57
+ FileUtils.mkdir_p @databag_directory
48
58
  end
49
59
  # rubocop:enable Metrics/MethodLength
50
60
 
51
61
  def deploy_databag(name:)
52
- databag_file = Tempfile.new ['formatron-databag-', '.json']
53
- databag_file.write @configuration.merge(id: name).to_json
54
- databag_file.close
62
+ databag_file = File.join(
63
+ @databag_directory, "#{name}#{DATABAG_ITEM_SUFFIX}"
64
+ )
65
+ File.write databag_file, @configuration.merge(id: name).to_json
55
66
  _attempt_to_create_databag unless _databag_exists
56
67
  _attempt_to_create_databag_item(
57
68
  name: name,
58
69
  databag_file: databag_file
59
70
  )
60
- ensure
61
- databag_file.unlink unless databag_file.nil?
62
71
  end
63
72
 
64
73
  def _databag_exists
65
- Util::Shell.exec "knife data bag show formatron -c #{@knife_file.path}"
74
+ Util::Shell.exec "knife data bag show formatron -c #{@knife_file}"
66
75
  end
67
76
 
68
77
  def _attempt_to_create_databag
@@ -70,9 +79,7 @@ class Formatron
70
79
  end
71
80
 
72
81
  def _create_databag
73
- # rubocop:disable Metrics/LineLength
74
- Util::Shell.exec "knife data bag create formatron -c #{@knife_file.path}"
75
- # rubocop:enable Metrics/LineLength
82
+ Util::Shell.exec "knife data bag create formatron -c #{@knife_file}"
76
83
  end
77
84
 
78
85
  def _attempt_to_create_databag_item(name:, databag_file:)
@@ -83,7 +90,7 @@ class Formatron
83
90
 
84
91
  def _create_databag_item(databag_file:)
85
92
  # rubocop:disable Metrics/LineLength
86
- Util::Shell.exec "knife data bag from file formatron #{databag_file.path} --secret-file #{@databag_secret_file.path} -c #{@knife_file.path}"
93
+ Util::Shell.exec "knife data bag from file formatron #{databag_file} --secret-file #{@databag_secret_file} -c #{@knife_file}"
87
94
  # rubocop:enable Metrics/LineLength
88
95
  end
89
96
 
@@ -95,7 +102,7 @@ class Formatron
95
102
 
96
103
  def _environment_exists(environment)
97
104
  # rubocop:disable Metrics/LineLength
98
- Util::Shell.exec "knife environment show #{environment} -c #{@knife_file.path}"
105
+ Util::Shell.exec "knife environment show #{environment} -c #{@knife_file}"
99
106
  # rubocop:enable Metrics/LineLength
100
107
  end
101
108
 
@@ -107,59 +114,61 @@ class Formatron
107
114
 
108
115
  def _create_environment(environment)
109
116
  # rubocop:disable Metrics/LineLength
110
- Util::Shell.exec "knife environment create #{environment} -c #{@knife_file.path} -d '#{environment} environment created by formatron'"
117
+ Util::Shell.exec "knife environment create #{environment} -c #{@knife_file} -d '#{environment} environment created by formatron'"
111
118
  # rubocop:enable Metrics/LineLength
112
119
  end
113
120
 
121
+ # rubocop:disable Metrics/MethodLength
114
122
  def bootstrap(
123
+ os:,
115
124
  guid:,
116
125
  bastion_hostname:,
117
126
  cookbook:,
118
127
  hostname:
119
128
  )
120
129
  # rubocop:disable Metrics/LineLength
121
- command = "knife bootstrap #{hostname} --sudo -x ubuntu -i #{@keys.ec2_key} -E #{guid} -r #{cookbook} -N #{guid} -c #{@knife_file.path}#{@ssl_verify ? '' : ' --node-ssl-verify-mode none'} --secret-file #{@databag_secret_file.path}"
122
- command = "#{command} -G ubuntu@#{bastion_hostname}" unless bastion_hostname.eql? hostname
130
+ if os.eql? 'windows'
131
+ command = "knife bootstrap windows winrm #{hostname} -x #{@administrator_name} -P '#{@administrator_password}' -E #{guid} -r #{cookbook} -N #{guid} -c #{@knife_file} --secret-file #{@databag_secret_file}"
132
+ else
133
+ command = "knife bootstrap #{hostname} --sudo -x ubuntu -i #{@keys.ec2_key} -E #{guid} -r #{cookbook} -N #{guid} -c #{@knife_file}#{@ssl_verify ? '' : ' --node-ssl-verify-mode none'} --secret-file #{@databag_secret_file}"
134
+ command = "#{command} -G ubuntu@#{bastion_hostname}" unless bastion_hostname.eql? hostname
135
+ end
123
136
  fail "failed to bootstrap instance: #{guid}" unless Util::Shell.exec command
124
137
  # rubocop:enable Metrics/LineLength
125
138
  end
139
+ # rubocop:enable Metrics/MethodLength
126
140
 
127
141
  def delete_databag(name:)
128
142
  # rubocop:disable Metrics/LineLength
129
- command = "knife data bag delete formatron #{name} -y -c #{@knife_file.path}"
143
+ command = "knife data bag delete formatron #{name} -y -c #{@knife_file}"
130
144
  fail "failed to delete data bag item: #{name}" unless Util::Shell.exec command
131
145
  # rubocop:enable Metrics/LineLength
132
146
  end
133
147
 
134
148
  def delete_node(node:)
135
- command = "knife node delete #{node} -y -c #{@knife_file.path}"
149
+ command = "knife node delete #{node} -y -c #{@knife_file}"
136
150
  fail "failed to delete node: #{node}" unless Util::Shell.exec command
137
151
  end
138
152
 
139
153
  def delete_client(client:)
140
154
  # rubocop:disable Metrics/LineLength
141
- command = "knife client delete #{client} -y -c #{@knife_file.path}"
155
+ command = "knife client delete #{client} -y -c #{@knife_file}"
142
156
  fail "failed to delete client: #{client}" unless Util::Shell.exec command
143
157
  # rubocop:enable Metrics/LineLength
144
158
  end
145
159
 
146
160
  def delete_environment(environment:)
147
161
  # rubocop:disable Metrics/LineLength
148
- command = "knife environment delete #{environment} -y -c #{@knife_file.path}"
162
+ command = "knife environment delete #{environment} -y -c #{@knife_file}"
149
163
  fail "failed to delete environment: #{environment}" unless Util::Shell.exec command
150
164
  # rubocop:enable Metrics/LineLength
151
165
  end
152
166
 
153
167
  def node_exists?(guid:)
154
- command = "knife node show #{guid} -c #{@knife_file.path}"
168
+ command = "knife node show #{guid} -c #{@knife_file}"
155
169
  Util::Shell.exec command
156
170
  end
157
171
 
158
- def unlink
159
- @knife_file.unlink unless @knife_file.nil?
160
- @databag_secret_file.unlink unless @databag_secret_file.nil?
161
- end
162
-
163
172
  private(
164
173
  :_create_databag,
165
174
  :_create_databag_item,
@@ -11,12 +11,15 @@ class Formatron
11
11
  end
12
12
 
13
13
  def run_chef_client(hostname:, bastion_hostname:)
14
+ # use the first-boot.json to ensure the runlist is correct
15
+ # if the node fails to converge the first time (in which case
16
+ # the server will show an empty run list for the node)
14
17
  Formatron::Util::SSH.exec(
15
18
  hostname: hostname,
16
19
  bastion_hostname: bastion_hostname,
17
20
  user: SSH_USER,
18
21
  key: @keys.ec2_key,
19
- command: 'sudo chef-client'
22
+ command: 'sudo chef-client -j /etc/chef/first-boot.json'
20
23
  )
21
24
  end
22
25
 
@@ -0,0 +1,37 @@
1
+ require 'formatron/util/winrm'
2
+
3
+ class Formatron
4
+ class Chef
5
+ # Perform commands on chef nodes over WinRM
6
+ class WinRM
7
+ def initialize(administrator_name:, administrator_password:)
8
+ @administrator_name = administrator_name
9
+ @administrator_password = administrator_password
10
+ end
11
+
12
+ def run_chef_client(hostname:)
13
+ # use the first-boot.json to ensure the runlist is correct
14
+ # if the node fails to converge the first time (in which case
15
+ # the server will show an empty run list for the node)
16
+ Formatron::Util::WinRM.exec(
17
+ hostname: hostname,
18
+ administrator_name: @administrator_name,
19
+ administrator_password: @administrator_password,
20
+ command: 'chef-client -j C:\chef\first-boot.json'
21
+ )
22
+ end
23
+
24
+ def bootstrapped?(hostname:)
25
+ Formatron::Util::WinRM.exec(
26
+ hostname: hostname,
27
+ administrator_name: @administrator_name,
28
+ administrator_password: @administrator_password,
29
+ command: 'if (-not (Test-Path C:\chef\client.pem)) { exit 1 }'
30
+ )
31
+ true
32
+ rescue
33
+ false
34
+ end
35
+ end
36
+ end
37
+ end
@@ -5,11 +5,14 @@ class Formatron
5
5
  # rubocop:disable Metrics/AbcSize
6
6
  # rubocop:disable Metrics/MethodLength
7
7
  def initialize(
8
+ directory:,
8
9
  aws:,
9
10
  bucket:,
10
11
  name:,
11
12
  target:,
12
13
  ec2_key:,
14
+ administrator_name:,
15
+ administrator_password:,
13
16
  hosted_zone_name:,
14
17
  vpc:,
15
18
  external:,
@@ -27,6 +30,7 @@ class Formatron
27
30
  bastions = Hash[bastions.map { |k, v| [k, v.sub_domain] }]
28
31
  chef_servers.each do |key, chef_server|
29
32
  @chef_clients[key] = Chef.new(
33
+ directory: directory,
30
34
  aws: aws,
31
35
  bucket: bucket,
32
36
  name: name,
@@ -36,6 +40,8 @@ class Formatron
36
40
  ssl_verify: chef_server.ssl_verify,
37
41
  chef_sub_domain: chef_server.sub_domain,
38
42
  ec2_key: ec2_key,
43
+ administrator_name: administrator_name,
44
+ administrator_password: administrator_password,
39
45
  bastions: bastions,
40
46
  hosted_zone_name: hosted_zone_name,
41
47
  server_stack: chef_server.stack || name,
@@ -57,9 +63,5 @@ class Formatron
57
63
  def init
58
64
  @chef_clients.values.each(&:init)
59
65
  end
60
-
61
- def unlink
62
- @chef_clients.values.each(&:unlink)
63
- end
64
66
  end
65
67
  end
@@ -7,7 +7,7 @@ class Formatron
7
7
  # CLI command for credentials generator
8
8
  module Credentials
9
9
  def self.dot_credentials
10
- File.join '.formatron', 'credentials.json'
10
+ File.join Formatron::WORKING_DIRECTORY, 'credentials.json'
11
11
  end
12
12
 
13
13
  def self.global_credentials
@@ -1,4 +1,5 @@
1
1
  require_relative '../template'
2
+ require_relative '../scripts'
2
3
 
3
4
  class Formatron
4
5
  module CloudFormation
@@ -274,49 +275,72 @@ class Formatron
274
275
  # rubocop:disable Metrics/ParameterLists
275
276
  # rubocop:disable Metrics/AbcSize
276
277
  def self.instance(
277
- scripts: nil,
278
- script_variables: nil,
279
- files: nil,
280
278
  instance_profile:,
281
279
  availability_zone:,
282
280
  instance_type:,
283
281
  key_name:,
282
+ administrator_name:,
283
+ administrator_password:,
284
284
  subnet:,
285
285
  name:,
286
286
  wait_condition_handle:,
287
287
  security_group:,
288
288
  logical_id:,
289
- source_dest_check:
289
+ source_dest_check:,
290
+ os:
290
291
  )
291
- files ||= {}
292
- scripts.each_index do |index|
293
- files["/tmp/formatron/script-#{index}.sh"] = {
294
- content: scripts[index],
295
- mode: '000755',
296
- owner: 'root',
297
- group: 'root'
298
- }
299
- end unless scripts.nil?
300
- script_variables_content =
301
- script_variables.reduce([]) do |content, (key, value)|
302
- content.concat(["#{key}=", value, "\n"])
303
- end unless script_variables.nil?
304
- files['/tmp/formatron/script-variables'] = {
305
- content: Template.join(*script_variables_content),
306
- mode: '000644',
307
- owner: 'root',
308
- group: 'root'
309
- } unless script_variables_content.nil?
292
+ if os.eql? 'windows'
293
+ user_data = Template.base_64(
294
+ Template.join(
295
+ # rubocop:disable Metrics/LineLength
296
+ "<powershell>\n",
297
+ "try\n",
298
+ "{\n",
299
+ Scripts.windows_administrator(
300
+ name: administrator_name,
301
+ password: administrator_password
302
+ ),
303
+ 'winrm quickconfig -q', "\n",
304
+ "winrm set winrm/config/winrs '@{MaxMemoryPerShellMB=\"1024\"}'", "\n",
305
+ "winrm set winrm/config '@{MaxTimeoutms=\"1800000\"}'", "\n",
306
+ "winrm set winrm/config/service '@{AllowUnencrypted=\"true\"}'", "\n",
307
+ "winrm set winrm/config/service/auth '@{Basic=\"true\"}'", "\n",
308
+ 'netsh advfirewall firewall add rule name="WinRM 5985" protocol=TCP dir=in localport=5985 action=allow', "\n",
309
+ 'netsh advfirewall firewall add rule name="WinRM 5986" protocol=TCP dir=in localport=5986 action=allow', "\n",
310
+ 'Stop-Service winrm', "\n",
311
+ 'Set-Service winrm -startuptype "automatic"', "\n",
312
+ 'Start-Service winrm', "\n",
313
+ 'cfn-init.exe -v -s ', Template.ref('AWS::StackName'),
314
+ " -r #{logical_id}",
315
+ ' --region ', Template.ref('AWS::Region'), "\n",
316
+ "}\n",
317
+ "catch\n",
318
+ "{\n",
319
+ 'cfn-signal.exe -e 1 ',
320
+ Template.base_64(Template.ref(wait_condition_handle)), "\n",
321
+ "}\n",
322
+ '</powershell>'
323
+ # rubocop:enable Metrics/LineLength
324
+ )
325
+ )
326
+ else
327
+ user_data = Template.base_64(
328
+ Template.join(
329
+ # rubocop:disable Metrics/LineLength
330
+ "#!/bin/bash -v\n",
331
+ "apt-get -y update\n",
332
+ "apt-get -y install python-setuptools\n",
333
+ "easy_install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz\n",
334
+ "export PATH=$PATH:/opt/aws/bin\n",
335
+ 'cfn-init --region ', Template.ref('AWS::Region'),
336
+ ' -v -s ', Template.ref('AWS::StackName'), " -r #{logical_id}\n",
337
+ "cfn-signal -e $? -r 'Formatron instance configuration complete' '", Template.ref(wait_condition_handle), "'\n"
338
+ # rubocop:enable Metrics/LineLength
339
+ )
340
+ )
341
+ end
310
342
  {
311
343
  Type: 'AWS::EC2::Instance',
312
- Metadata: {
313
- Comment1: 'Create setup scripts',
314
- 'AWS::CloudFormation::Init' => {
315
- config: {
316
- files: files
317
- }
318
- }
319
- },
320
344
  Properties: {
321
345
  IamInstanceProfile: Template.ref(instance_profile),
322
346
  AvailabilityZone: Template.join(
@@ -326,7 +350,7 @@ class Formatron
326
350
  ImageId: Template.find_in_map(
327
351
  Template::REGION_MAP,
328
352
  Template.ref('AWS::Region'),
329
- 'ami'
353
+ os
330
354
  ),
331
355
  SourceDestCheck: source_dest_check,
332
356
  InstanceType: instance_type,
@@ -337,36 +361,13 @@ class Formatron
337
361
  Key: 'Name',
338
362
  Value: name
339
363
  }],
340
- UserData: Template.base_64(
341
- Template.join(
342
- # rubocop:disable Metrics/LineLength
343
- "#!/bin/bash -v\n",
344
- "function error_exit\n",
345
- "{\n",
346
- " cfn-signal -e 1 -r \"$1\" '", Template.ref(wait_condition_handle), "'\n",
347
- " exit 1\n",
348
- "}\n",
349
- "apt-get -y update\n",
350
- "apt-get -y install python-setuptools\n",
351
- "easy_install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz\n",
352
- "export PATH=$PATH:/opt/aws/bin\n",
353
- 'cfn-init --region ', Template.ref('AWS::Region'),
354
- ' -v -s ', Template.ref('AWS::StackName'), " -r #{logical_id} ",
355
- " || error_exit 'Failed to run cfn-init'\n",
356
- "for file in /tmp/formatron/script-*.sh; do\n",
357
- " $file || error_exit \"failed to run Formatron setup script: $file\"\n",
358
- "done\n",
359
- "# If all went well, signal success\n",
360
- "cfn-signal -e $? -r 'Formatron instance configuration complete' '", Template.ref(wait_condition_handle), "'\n"
361
- # rubocop:enable Metrics/LineLength
362
- )
363
- )
364
+ UserData: user_data
364
365
  }
365
366
  }
366
367
  end
367
- # rubocop:enable Metrics/AbcSize
368
368
  # rubocop:enable Metrics/ParameterLists
369
369
  # rubocop:enable Metrics/MethodLength
370
+ # rubocop:enable Metrics/AbcSize
370
371
  end
371
372
  # rubocop:enable Metrics/ModuleLength
372
373
  end