formatron 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +3 -0
  6. data/.simplecov +7 -0
  7. data/.travis.yml +17 -0
  8. data/CODE_OF_CONDUCT.md +13 -0
  9. data/Gemfile +6 -0
  10. data/Guardfile +16 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +93 -0
  13. data/Rakefile +16 -0
  14. data/bin/console +14 -0
  15. data/bin/setup +7 -0
  16. data/exe/formatron +20 -0
  17. data/formatron.gemspec +52 -0
  18. data/lib/formatron.rb +357 -0
  19. data/lib/formatron/aws.rb +197 -0
  20. data/lib/formatron/chef.rb +156 -0
  21. data/lib/formatron/chef/berkshelf.rb +55 -0
  22. data/lib/formatron/chef/keys.rb +48 -0
  23. data/lib/formatron/chef/knife.rb +169 -0
  24. data/lib/formatron/chef_clients.rb +73 -0
  25. data/lib/formatron/cli.rb +33 -0
  26. data/lib/formatron/cli/completion.rb +26 -0
  27. data/lib/formatron/cli/deploy.rb +57 -0
  28. data/lib/formatron/cli/destroy.rb +57 -0
  29. data/lib/formatron/cli/generators/bootstrap.rb +250 -0
  30. data/lib/formatron/cli/generators/credentials.rb +100 -0
  31. data/lib/formatron/cli/generators/instance.rb +118 -0
  32. data/lib/formatron/cli/provision.rb +59 -0
  33. data/lib/formatron/cloud_formation.rb +54 -0
  34. data/lib/formatron/cloud_formation/resources/cloud_formation.rb +27 -0
  35. data/lib/formatron/cloud_formation/resources/ec2.rb +336 -0
  36. data/lib/formatron/cloud_formation/resources/iam.rb +94 -0
  37. data/lib/formatron/cloud_formation/resources/route53.rb +54 -0
  38. data/lib/formatron/cloud_formation/scripts.rb +128 -0
  39. data/lib/formatron/cloud_formation/template.rb +114 -0
  40. data/lib/formatron/cloud_formation/template/parameters.rb +20 -0
  41. data/lib/formatron/cloud_formation/template/vpc.rb +181 -0
  42. data/lib/formatron/cloud_formation/template/vpc/subnet.rb +187 -0
  43. data/lib/formatron/cloud_formation/template/vpc/subnet/acl.rb +147 -0
  44. data/lib/formatron/cloud_formation/template/vpc/subnet/bastion.rb +66 -0
  45. data/lib/formatron/cloud_formation/template/vpc/subnet/chef_server.rb +205 -0
  46. data/lib/formatron/cloud_formation/template/vpc/subnet/instance.rb +162 -0
  47. data/lib/formatron/cloud_formation/template/vpc/subnet/instance/policy.rb +74 -0
  48. data/lib/formatron/cloud_formation/template/vpc/subnet/instance/security_group.rb +117 -0
  49. data/lib/formatron/cloud_formation/template/vpc/subnet/instance/setup.rb +68 -0
  50. data/lib/formatron/cloud_formation/template/vpc/subnet/nat.rb +94 -0
  51. data/lib/formatron/completion.rb +26 -0
  52. data/lib/formatron/completion/completion.sh.erb +35 -0
  53. data/lib/formatron/config.rb +31 -0
  54. data/lib/formatron/config/reader.rb +29 -0
  55. data/lib/formatron/dsl.rb +15 -0
  56. data/lib/formatron/dsl/formatron.rb +25 -0
  57. data/lib/formatron/dsl/formatron/global.rb +19 -0
  58. data/lib/formatron/dsl/formatron/global/ec2.rb +17 -0
  59. data/lib/formatron/dsl/formatron/vpc.rb +17 -0
  60. data/lib/formatron/dsl/formatron/vpc/subnet.rb +27 -0
  61. data/lib/formatron/dsl/formatron/vpc/subnet/acl.rb +18 -0
  62. data/lib/formatron/dsl/formatron/vpc/subnet/chef_server.rb +32 -0
  63. data/lib/formatron/dsl/formatron/vpc/subnet/chef_server/organization.rb +22 -0
  64. data/lib/formatron/dsl/formatron/vpc/subnet/instance.rb +29 -0
  65. data/lib/formatron/dsl/formatron/vpc/subnet/instance/chef.rb +22 -0
  66. data/lib/formatron/dsl/formatron/vpc/subnet/instance/policy.rb +21 -0
  67. data/lib/formatron/dsl/formatron/vpc/subnet/instance/policy/statement.rb +23 -0
  68. data/lib/formatron/dsl/formatron/vpc/subnet/instance/security_group.rb +21 -0
  69. data/lib/formatron/dsl/formatron/vpc/subnet/instance/setup.rb +22 -0
  70. data/lib/formatron/dsl/formatron/vpc/subnet/instance/setup/variable.rb +23 -0
  71. data/lib/formatron/external.rb +61 -0
  72. data/lib/formatron/external/dsl.rb +171 -0
  73. data/lib/formatron/external/outputs.rb +25 -0
  74. data/lib/formatron/generators/bootstrap.rb +90 -0
  75. data/lib/formatron/generators/bootstrap/config.rb +62 -0
  76. data/lib/formatron/generators/bootstrap/ec2.rb +17 -0
  77. data/lib/formatron/generators/bootstrap/formatronfile.rb +52 -0
  78. data/lib/formatron/generators/bootstrap/formatronfile/Formatronfile.erb +79 -0
  79. data/lib/formatron/generators/bootstrap/ssl.rb +35 -0
  80. data/lib/formatron/generators/credentials.rb +17 -0
  81. data/lib/formatron/generators/instance.rb +64 -0
  82. data/lib/formatron/generators/instance/config.rb +47 -0
  83. data/lib/formatron/generators/instance/formatronfile.rb +47 -0
  84. data/lib/formatron/generators/instance/formatronfile/Formatronfile.erb +16 -0
  85. data/lib/formatron/generators/util.rb +14 -0
  86. data/lib/formatron/generators/util/cookbook.rb +65 -0
  87. data/lib/formatron/generators/util/gitignore.rb +16 -0
  88. data/lib/formatron/generators/util/readme.rb +18 -0
  89. data/lib/formatron/logger.rb +8 -0
  90. data/lib/formatron/s3/chef_server_cert.rb +85 -0
  91. data/lib/formatron/s3/chef_server_keys.rb +103 -0
  92. data/lib/formatron/s3/cloud_formation_template.rb +61 -0
  93. data/lib/formatron/s3/configuration.rb +58 -0
  94. data/lib/formatron/s3/path.rb +30 -0
  95. data/lib/formatron/util/dsl.rb +107 -0
  96. data/lib/formatron/util/shell.rb +20 -0
  97. data/lib/formatron/util/vpc.rb +15 -0
  98. data/lib/formatron/version.rb +4 -0
  99. data/support/cloudformation_describe_stacks_response.rb +36 -0
  100. data/support/dsl_test.rb +123 -0
  101. data/support/route53_get_hosted_zone_response.rb +21 -0
  102. data/support/s3_get_object_response.rb +21 -0
  103. data/support/template_test.rb +41 -0
  104. metadata +414 -0
@@ -0,0 +1,197 @@
1
+ require 'aws-sdk'
2
+
3
+ class Formatron
4
+ # shared AWS clients
5
+ # rubocop:disable Metrics/ClassLength
6
+ class AWS
7
+ attr_reader :region
8
+
9
+ REGIONS = {
10
+ 'us-east-1' => {
11
+ ami: 'ami-ff02509a'
12
+ },
13
+ 'us-west-2' => {
14
+ ami: 'ami-8ee605bd'
15
+ },
16
+ 'us-west-1' => {
17
+ ami: 'ami-198a495d'
18
+ },
19
+ 'eu-west-1' => {
20
+ ami: 'ami-37360a40'
21
+ },
22
+ 'eu-central-1' => {
23
+ ami: 'ami-46272b5b'
24
+ },
25
+ 'ap-southeast-1' => {
26
+ ami: 'ami-42170410'
27
+ },
28
+ 'ap-southeast-2' => {
29
+ ami: 'ami-6d6c2657'
30
+ },
31
+ 'ap-northeast-1' => {
32
+ ami: 'ami-402e4c40'
33
+ },
34
+ 'sa-east-1' => {
35
+ ami: 'ami-1f4bda02'
36
+ }
37
+ }
38
+
39
+ CAPABILITIES = %w(CAPABILITY_IAM)
40
+
41
+ STACK_READY_STATES = %w(
42
+ CREATE_COMPLETE
43
+ UPDATE_COMPLETE
44
+ UPDATE_ROLLBACK_COMPLETE
45
+ ROLLBACK_COMPLETE
46
+ )
47
+
48
+ def initialize(credentials:)
49
+ @credentials = JSON.parse(File.read(credentials))
50
+ @region = @credentials['region']
51
+ _create_aws_credentials
52
+ _create_s3_client
53
+ _create_cloudformation_client
54
+ _create_route53_client
55
+ end
56
+
57
+ def upload_file(kms_key:, bucket:, key:, content:)
58
+ @s3_client.put_object(
59
+ bucket: bucket,
60
+ key: key,
61
+ body: content,
62
+ server_side_encryption: 'aws:kms',
63
+ ssekms_key_id: kms_key
64
+ )
65
+ end
66
+
67
+ def delete_file(bucket:, key:)
68
+ @s3_client.delete_object(
69
+ bucket: bucket,
70
+ key: key
71
+ )
72
+ end
73
+
74
+ def download_file(bucket:, key:, path:)
75
+ @s3_client.get_object(
76
+ bucket: bucket,
77
+ key: key,
78
+ response_target: path
79
+ )
80
+ end
81
+
82
+ def get_file(bucket:, key:)
83
+ @s3_client.get_object(
84
+ bucket: bucket,
85
+ key: key
86
+ ).body.read
87
+ end
88
+
89
+ # rubocop:disable Metrics/MethodLength
90
+ def deploy_stack(stack_name:, template_url:, parameters:)
91
+ aws_parameters = parameters.map do |key, value|
92
+ {
93
+ parameter_key: key,
94
+ parameter_value: value,
95
+ use_previous_value: false
96
+ }
97
+ end
98
+ @cloudformation_client.create_stack(
99
+ stack_name: stack_name,
100
+ template_url: template_url,
101
+ capabilities: CAPABILITIES,
102
+ on_failure: 'DO_NOTHING',
103
+ parameters: aws_parameters
104
+ )
105
+ rescue Aws::CloudFormation::Errors::AlreadyExistsException
106
+ _update_stack(
107
+ stack_name: stack_name,
108
+ template_url: template_url,
109
+ parameters: aws_parameters
110
+ )
111
+ end
112
+ # rubocop:enable Metrics/MethodLength
113
+
114
+ def hosted_zone_name(hosted_zone_id)
115
+ @route53_client.get_hosted_zone(
116
+ id: hosted_zone_id
117
+ ).hosted_zone.name.chomp '.'
118
+ end
119
+
120
+ def _update_stack(stack_name:, template_url:, parameters:)
121
+ @cloudformation_client.update_stack(
122
+ stack_name: stack_name,
123
+ template_url: template_url,
124
+ capabilities: CAPABILITIES,
125
+ parameters: parameters
126
+ )
127
+ rescue Aws::CloudFormation::Errors::ValidationError => error
128
+ raise error unless error.message.eql?(
129
+ 'No updates are to be performed.'
130
+ )
131
+ end
132
+
133
+ def delete_stack(stack_name:)
134
+ @cloudformation_client.delete_stack(
135
+ stack_name: stack_name
136
+ )
137
+ end
138
+
139
+ def stack_outputs(stack_name:)
140
+ description = @cloudformation_client.describe_stacks(
141
+ stack_name: stack_name
142
+ ).stacks[0]
143
+ status = description.stack_status
144
+ fail "CloudFormation stack, #{stack_name}, " \
145
+ "is not ready: #{status}" unless STACK_READY_STATES.include? status
146
+ description.outputs.each_with_object({}) do |output, outputs|
147
+ outputs[output.output_key] = output.output_value
148
+ end
149
+ end
150
+
151
+ def stack_ready!(stack_name:)
152
+ status = @cloudformation_client.describe_stacks(
153
+ stack_name: stack_name
154
+ ).stacks[0].stack_status
155
+ fail "CloudFormation stack, #{stack_name}, " \
156
+ "is not ready: #{status}" unless STACK_READY_STATES.include? status
157
+ end
158
+
159
+ def _create_aws_credentials
160
+ @aws_credentials = Aws::Credentials.new(
161
+ @credentials['access_key_id'],
162
+ @credentials['secret_access_key']
163
+ )
164
+ end
165
+
166
+ def _create_s3_client
167
+ @s3_client = ::Aws::S3::Client.new(
168
+ region: @region,
169
+ signature_version: 'v4',
170
+ credentials: @aws_credentials
171
+ )
172
+ end
173
+
174
+ def _create_cloudformation_client
175
+ @cloudformation_client = ::Aws::CloudFormation::Client.new(
176
+ region: @region,
177
+ credentials: @aws_credentials
178
+ )
179
+ end
180
+
181
+ def _create_route53_client
182
+ @route53_client = ::Aws::Route53::Client.new(
183
+ region: @region,
184
+ credentials: @aws_credentials
185
+ )
186
+ end
187
+
188
+ private(
189
+ :_create_aws_credentials,
190
+ :_create_s3_client,
191
+ :_create_cloudformation_client,
192
+ :_create_route53_client,
193
+ :_update_stack
194
+ )
195
+ end
196
+ # rubocop:enable Metrics/ClassLength
197
+ end
@@ -0,0 +1,156 @@
1
+ require 'formatron/cloud_formation'
2
+ require 'formatron/logger'
3
+ require_relative 'chef/keys'
4
+ require_relative 'chef/berkshelf'
5
+ require_relative 'chef/knife'
6
+
7
+ class Formatron
8
+ # manage the instance provisioning with Chef
9
+ # rubocop:disable Metrics/ClassLength
10
+ class Chef
11
+ # rubocop:disable Metrics/MethodLength
12
+ # rubocop:disable Metrics/ParameterLists
13
+ def initialize(
14
+ aws:,
15
+ bucket:,
16
+ name:,
17
+ target:,
18
+ ec2_key:,
19
+ username:,
20
+ organization:,
21
+ ssl_verify:,
22
+ chef_sub_domain:,
23
+ bastions:,
24
+ hosted_zone_name:,
25
+ server_stack:,
26
+ guid:,
27
+ configuration:,
28
+ databag_secret:
29
+ )
30
+ @aws = aws
31
+ @name = name
32
+ @target = target
33
+ @chef_sub_domain = chef_sub_domain
34
+ @hosted_zone_name = hosted_zone_name
35
+ @organization = organization
36
+ @server_stack = server_stack
37
+ @bastions = bastions
38
+ chef_server_url = _chef_server_url
39
+ @keys = Keys.new(
40
+ aws: @aws,
41
+ bucket: bucket,
42
+ name: server_stack,
43
+ target: @target,
44
+ guid: guid,
45
+ ec2_key: ec2_key
46
+ )
47
+ @knife = Knife.new(
48
+ keys: @keys,
49
+ chef_server_url: chef_server_url,
50
+ username: username,
51
+ organization: organization,
52
+ ssl_verify: ssl_verify,
53
+ name: @name,
54
+ databag_secret: databag_secret,
55
+ configuration: configuration
56
+ )
57
+ @berkshelf = Berkshelf.new(
58
+ keys: @keys,
59
+ chef_server_url: chef_server_url,
60
+ username: username,
61
+ ssl_verify: ssl_verify
62
+ )
63
+ end
64
+ # rubocop:enable Metrics/ParameterLists
65
+ # rubocop:enable Metrics/MethodLength
66
+
67
+ def init
68
+ CloudFormation.stack_ready!(
69
+ aws: @aws,
70
+ name: @server_stack,
71
+ target: @target
72
+ )
73
+ @keys.init
74
+ @knife.init
75
+ @berkshelf.init
76
+ end
77
+
78
+ def deploy_databag
79
+ Formatron::LOG.info do
80
+ "Deploying data bag to chef server: #{@chef_sub_domain}"
81
+ end
82
+ @knife.deploy_databag
83
+ end
84
+
85
+ def delete_databag
86
+ Formatron::LOG.info do
87
+ "Deleting data bag from chef server: #{@chef_sub_domain}"
88
+ end
89
+ @knife.delete_databag
90
+ end
91
+
92
+ # rubocop:disable Metrics/MethodLength
93
+ def provision(
94
+ sub_domain:,
95
+ cookbook:,
96
+ bastion:
97
+ )
98
+ Formatron::LOG.info do
99
+ "Provision #{sub_domain} with Chef cookbook: #{cookbook}"
100
+ end
101
+ bastion ||= @bastions.keys[0]
102
+ bastion_hostname = _hostname(
103
+ sub_domain: @bastions[bastion]
104
+ )
105
+ CloudFormation.stack_ready!(
106
+ aws: @aws,
107
+ name: @name,
108
+ target: @target
109
+ )
110
+ cookbook_name = File.basename cookbook
111
+ hostname = _hostname(
112
+ sub_domain: sub_domain
113
+ )
114
+ @knife.create_environment environment: sub_domain
115
+ @berkshelf.upload environment: sub_domain, cookbook: cookbook
116
+ @knife.bootstrap(
117
+ bastion_hostname: bastion_hostname,
118
+ environment: sub_domain,
119
+ cookbook: cookbook_name,
120
+ hostname: hostname
121
+ )
122
+ end
123
+ # rubocop:enable Metrics/ParameterLists
124
+ # rubocop:enable Metrics/MethodLength
125
+
126
+ def destroy(sub_domain:)
127
+ Formatron::LOG.info do
128
+ "Delete Chef configuration for node: #{sub_domain}"
129
+ end
130
+ @knife.delete_node node: sub_domain
131
+ @knife.delete_client client: sub_domain
132
+ @knife.delete_environment environment: sub_domain
133
+ end
134
+
135
+ def unlink
136
+ @keys.unlink
137
+ @knife.unlink
138
+ @berkshelf.unlink
139
+ end
140
+
141
+ def _chef_server_url
142
+ "https://#{@chef_sub_domain}.#{@hosted_zone_name}" \
143
+ "/organizations/#{@organization}"
144
+ end
145
+
146
+ def _hostname(sub_domain:)
147
+ "#{sub_domain}.#{@hosted_zone_name}"
148
+ end
149
+
150
+ private(
151
+ :_chef_server_url,
152
+ :_hostname
153
+ )
154
+ end
155
+ # rubocop:enable Metrics/ClassLength
156
+ end
@@ -0,0 +1,55 @@
1
+ require 'formatron/util/shell'
2
+ require 'English'
3
+
4
+ class Formatron
5
+ class Chef
6
+ # Wrapper for the berkshelf cli
7
+ class Berkshelf
8
+ CONFIG_FILE_CONTENTS = <<-EOH.gsub(/^ {8}/, '')
9
+ {
10
+ "chef": {
11
+ "chef_server_url": "%{server_url}",
12
+ "node_name": "%{user}",
13
+ "client_key": "%{key_file}"
14
+ },
15
+ "ssl": {
16
+ "verify": %{ssl_verify}
17
+ }
18
+ }
19
+ EOH
20
+
21
+ def initialize(keys:, chef_server_url:, username:, ssl_verify:)
22
+ @keys = keys
23
+ @chef_server_url = chef_server_url
24
+ @username = username
25
+ @ssl_verify = ssl_verify
26
+ end
27
+
28
+ def init
29
+ @config_file = Tempfile.new 'formatron-berkshelf-'
30
+ @config_file.write CONFIG_FILE_CONTENTS % {
31
+ server_url: @chef_server_url,
32
+ user: @username,
33
+ key_file: @keys.user_key,
34
+ ssl_verify: @ssl_verify
35
+ }
36
+ @config_file.close
37
+ end
38
+
39
+ def upload(cookbook:, environment:)
40
+ # rubocop:disable Metrics/LineLength
41
+ command = "berks install -b #{File.join(cookbook, 'Berksfile')}"
42
+ fail "failed to download cookbooks for opscode environment: #{environment}" unless Util::Shell.exec command
43
+ command = "berks upload -c #{@config_file.path} -b #{File.join(cookbook, 'Berksfile')}"
44
+ fail "failed to upload cookbooks for opscode environment: #{environment}" unless Util::Shell.exec command
45
+ command = "berks apply #{environment} -c #{@config_file.path} -b #{File.join(cookbook, 'Berksfile.lock')}"
46
+ fail "failed to apply cookbooks to opscode environment: #{environment}" unless Util::Shell.exec command
47
+ # rubocop:enable Metrics/LineLength
48
+ end
49
+
50
+ def unlink
51
+ @config_file.unlink unless @config_file.nil?
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,48 @@
1
+ require 'formatron/s3/chef_server_keys'
2
+
3
+ class Formatron
4
+ class Chef
5
+ # Download the Chef Server keys
6
+ class Keys
7
+ # rubocop:disable Metrics/ParameterLists
8
+ def initialize(aws:, bucket:, name:, target:, guid:, ec2_key:)
9
+ @aws = aws
10
+ @bucket = bucket
11
+ @name = name
12
+ @target = target
13
+ @guid = guid
14
+ @ec2_key = ec2_key
15
+ end
16
+ # rubocop:enable Metrics/ParameterLists
17
+
18
+ def init
19
+ @directory = Dir.mktmpdir 'formatron-chef-server-keys-'
20
+ S3::ChefServerKeys.get(
21
+ aws: @aws,
22
+ bucket: @bucket,
23
+ name: @name,
24
+ target: @target,
25
+ guid: @guid,
26
+ directory: @directory
27
+ )
28
+ File.write ec2_key, @ec2_key
29
+ end
30
+
31
+ def user_key
32
+ S3::ChefServerKeys.user_pem_path directory: @directory
33
+ end
34
+
35
+ def organization_key
36
+ S3::ChefServerKeys.organization_pem_path directory: @directory
37
+ end
38
+
39
+ def ec2_key
40
+ File.join @directory, 'ec2_key'
41
+ end
42
+
43
+ def unlink
44
+ FileUtils.rm_rf @directory unless @directory.nil?
45
+ end
46
+ end
47
+ end
48
+ end