formatron 0.1.0

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