wombat-cli 0.2.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 (201) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/DESIGN.md +40 -0
  4. data/Gemfile +3 -0
  5. data/README.md +132 -0
  6. data/Rakefile +52 -0
  7. data/Vagrantfile +121 -0
  8. data/bin/wombat +24 -0
  9. data/cookbooks/automate/.gitignore +16 -0
  10. data/cookbooks/automate/.kitchen.ec2.yml +27 -0
  11. data/cookbooks/automate/.kitchen.yml +25 -0
  12. data/cookbooks/automate/Berksfile +6 -0
  13. data/cookbooks/automate/README.md +4 -0
  14. data/cookbooks/automate/chefignore +102 -0
  15. data/cookbooks/automate/libraries/_helper.rb +52 -0
  16. data/cookbooks/automate/libraries/delivery_api.rb +204 -0
  17. data/cookbooks/automate/libraries/delivery_project.rb +31 -0
  18. data/cookbooks/automate/libraries/dsl.rb +5 -0
  19. data/cookbooks/automate/metadata.rb +13 -0
  20. data/cookbooks/automate/recipes/default.rb +111 -0
  21. data/cookbooks/automate/recipes/update-users.rb +48 -0
  22. data/cookbooks/automate/templates/delivery.erb +5 -0
  23. data/cookbooks/automate/test/fixtures/cookbooks/mock_data/files/automate.crt +26 -0
  24. data/cookbooks/automate/test/fixtures/cookbooks/mock_data/files/automate.key +27 -0
  25. data/cookbooks/automate/test/fixtures/cookbooks/mock_data/files/chef.crt +25 -0
  26. data/cookbooks/automate/test/fixtures/cookbooks/mock_data/files/chef.key +27 -0
  27. data/cookbooks/automate/test/fixtures/cookbooks/mock_data/files/compliance.crt +26 -0
  28. data/cookbooks/automate/test/fixtures/cookbooks/mock_data/files/compliance.key +27 -0
  29. data/cookbooks/automate/test/fixtures/cookbooks/mock_data/files/private.pem +27 -0
  30. data/cookbooks/automate/test/fixtures/cookbooks/mock_data/files/public.pub +1 -0
  31. data/cookbooks/automate/test/fixtures/cookbooks/mock_data/metadata.rb +3 -0
  32. data/cookbooks/automate/test/fixtures/cookbooks/mock_data/recipes/default.rb +27 -0
  33. data/cookbooks/automate/test/integration/default/automate_spec.rb +55 -0
  34. data/cookbooks/build_node/.gitignore +16 -0
  35. data/cookbooks/build_node/.kitchen.ec2.yml +30 -0
  36. data/cookbooks/build_node/.kitchen.yml +23 -0
  37. data/cookbooks/build_node/Berksfile +8 -0
  38. data/cookbooks/build_node/README.md +4 -0
  39. data/cookbooks/build_node/chefignore +102 -0
  40. data/cookbooks/build_node/metadata.rb +15 -0
  41. data/cookbooks/build_node/recipes/default.rb +35 -0
  42. data/cookbooks/build_node/templates/client.erb +3 -0
  43. data/cookbooks/build_node/test/fixtures/cookbooks/mock_data/files/automate.crt +26 -0
  44. data/cookbooks/build_node/test/fixtures/cookbooks/mock_data/files/automate.key +27 -0
  45. data/cookbooks/build_node/test/fixtures/cookbooks/mock_data/files/chef.crt +25 -0
  46. data/cookbooks/build_node/test/fixtures/cookbooks/mock_data/files/chef.key +27 -0
  47. data/cookbooks/build_node/test/fixtures/cookbooks/mock_data/files/compliance.crt +26 -0
  48. data/cookbooks/build_node/test/fixtures/cookbooks/mock_data/files/compliance.key +27 -0
  49. data/cookbooks/build_node/test/fixtures/cookbooks/mock_data/files/private.pem +27 -0
  50. data/cookbooks/build_node/test/fixtures/cookbooks/mock_data/files/public.pub +1 -0
  51. data/cookbooks/build_node/test/fixtures/cookbooks/mock_data/metadata.rb +2 -0
  52. data/cookbooks/build_node/test/fixtures/cookbooks/mock_data/recipes/default.rb +18 -0
  53. data/cookbooks/build_node/test/integration/default/build-node_spec.rb +39 -0
  54. data/cookbooks/chef_server/.gitignore +16 -0
  55. data/cookbooks/chef_server/.kitchen.ec2.yml +26 -0
  56. data/cookbooks/chef_server/.kitchen.yml +25 -0
  57. data/cookbooks/chef_server/Berksfile +6 -0
  58. data/cookbooks/chef_server/README.md +4 -0
  59. data/cookbooks/chef_server/chefignore +102 -0
  60. data/cookbooks/chef_server/metadata.rb +13 -0
  61. data/cookbooks/chef_server/recipes/cheffish.rb +91 -0
  62. data/cookbooks/chef_server/recipes/default.rb +79 -0
  63. data/cookbooks/chef_server/test/fixtures/cookbooks/mock_data/files/automate.crt +26 -0
  64. data/cookbooks/chef_server/test/fixtures/cookbooks/mock_data/files/automate.key +27 -0
  65. data/cookbooks/chef_server/test/fixtures/cookbooks/mock_data/files/chef.crt +25 -0
  66. data/cookbooks/chef_server/test/fixtures/cookbooks/mock_data/files/chef.key +27 -0
  67. data/cookbooks/chef_server/test/fixtures/cookbooks/mock_data/files/compliance.crt +26 -0
  68. data/cookbooks/chef_server/test/fixtures/cookbooks/mock_data/files/compliance.key +27 -0
  69. data/cookbooks/chef_server/test/fixtures/cookbooks/mock_data/files/private.pem +27 -0
  70. data/cookbooks/chef_server/test/fixtures/cookbooks/mock_data/files/public.pub +1 -0
  71. data/cookbooks/chef_server/test/fixtures/cookbooks/mock_data/metadata.rb +2 -0
  72. data/cookbooks/chef_server/test/fixtures/cookbooks/mock_data/recipes/default.rb +23 -0
  73. data/cookbooks/chef_server/test/integration/default/chef_server_spec.rb +47 -0
  74. data/cookbooks/compliance/.gitignore +16 -0
  75. data/cookbooks/compliance/.kitchen.ec2.yml +26 -0
  76. data/cookbooks/compliance/.kitchen.yml +24 -0
  77. data/cookbooks/compliance/Berksfile +7 -0
  78. data/cookbooks/compliance/README.md +4 -0
  79. data/cookbooks/compliance/chefignore +102 -0
  80. data/cookbooks/compliance/metadata.rb +12 -0
  81. data/cookbooks/compliance/recipes/default.rb +59 -0
  82. data/cookbooks/compliance/spec/spec_helper.rb +2 -0
  83. data/cookbooks/compliance/spec/unit/recipes/default_spec.rb +20 -0
  84. data/cookbooks/compliance/templates/default/chef-compliance.rb.erb +1 -0
  85. data/cookbooks/compliance/test/fixtures/cookbooks/mock_data/files/automate.crt +26 -0
  86. data/cookbooks/compliance/test/fixtures/cookbooks/mock_data/files/automate.key +27 -0
  87. data/cookbooks/compliance/test/fixtures/cookbooks/mock_data/files/chef.crt +25 -0
  88. data/cookbooks/compliance/test/fixtures/cookbooks/mock_data/files/chef.key +27 -0
  89. data/cookbooks/compliance/test/fixtures/cookbooks/mock_data/files/compliance.crt +26 -0
  90. data/cookbooks/compliance/test/fixtures/cookbooks/mock_data/files/compliance.key +27 -0
  91. data/cookbooks/compliance/test/fixtures/cookbooks/mock_data/files/private.pem +27 -0
  92. data/cookbooks/compliance/test/fixtures/cookbooks/mock_data/files/public.pub +1 -0
  93. data/cookbooks/compliance/test/fixtures/cookbooks/mock_data/metadata.rb +4 -0
  94. data/cookbooks/compliance/test/fixtures/cookbooks/mock_data/recipes/default.rb +21 -0
  95. data/cookbooks/compliance/test/integration/default/compliance.rb +27 -0
  96. data/cookbooks/infranodes/.gitignore +16 -0
  97. data/cookbooks/infranodes/.kitchen.ec2.yml +27 -0
  98. data/cookbooks/infranodes/.kitchen.yml +21 -0
  99. data/cookbooks/infranodes/Berksfile +6 -0
  100. data/cookbooks/infranodes/README.md +4 -0
  101. data/cookbooks/infranodes/attributes/default.rb +3 -0
  102. data/cookbooks/infranodes/chefignore +102 -0
  103. data/cookbooks/infranodes/metadata.rb +13 -0
  104. data/cookbooks/infranodes/recipes/default.rb +41 -0
  105. data/cookbooks/infranodes/spec/spec_helper.rb +2 -0
  106. data/cookbooks/infranodes/spec/unit/recipes/default_spec.rb +20 -0
  107. data/cookbooks/infranodes/templates/default/client.rb.erb +5 -0
  108. data/cookbooks/infranodes/test/fixtures/cookbooks/mock_data/files/automate.crt +26 -0
  109. data/cookbooks/infranodes/test/fixtures/cookbooks/mock_data/files/automate.key +27 -0
  110. data/cookbooks/infranodes/test/fixtures/cookbooks/mock_data/files/chef.crt +25 -0
  111. data/cookbooks/infranodes/test/fixtures/cookbooks/mock_data/files/chef.key +27 -0
  112. data/cookbooks/infranodes/test/fixtures/cookbooks/mock_data/files/compliance.crt +26 -0
  113. data/cookbooks/infranodes/test/fixtures/cookbooks/mock_data/files/compliance.key +27 -0
  114. data/cookbooks/infranodes/test/fixtures/cookbooks/mock_data/files/private.pem +27 -0
  115. data/cookbooks/infranodes/test/fixtures/cookbooks/mock_data/files/public.pub +1 -0
  116. data/cookbooks/infranodes/test/fixtures/cookbooks/mock_data/metadata.rb +3 -0
  117. data/cookbooks/infranodes/test/fixtures/cookbooks/mock_data/recipes/default.rb +21 -0
  118. data/cookbooks/infranodes/test/integration/default/infranodes_spec.rb +20 -0
  119. data/cookbooks/infranodes/test/integration/helpers/serverspec/spec_helper.rb +8 -0
  120. data/cookbooks/wombat/.gitignore +16 -0
  121. data/cookbooks/wombat/.kitchen.yml +43 -0
  122. data/cookbooks/wombat/Berksfile +6 -0
  123. data/cookbooks/wombat/README.md +4 -0
  124. data/cookbooks/wombat/attributes/default.rb +71 -0
  125. data/cookbooks/wombat/attributes/packer.rb +18 -0
  126. data/cookbooks/wombat/chefignore +102 -0
  127. data/cookbooks/wombat/metadata.rb +11 -0
  128. data/cookbooks/wombat/recipes/authorized-keys.rb +10 -0
  129. data/cookbooks/wombat/recipes/default.rb +112 -0
  130. data/cookbooks/wombat/recipes/etc-hosts.rb +51 -0
  131. data/cookbooks/workstation/.gitignore +16 -0
  132. data/cookbooks/workstation/.kitchen.ec2.yml +29 -0
  133. data/cookbooks/workstation/.kitchen.yml +22 -0
  134. data/cookbooks/workstation/Berksfile +7 -0
  135. data/cookbooks/workstation/README.md +3 -0
  136. data/cookbooks/workstation/chefignore +102 -0
  137. data/cookbooks/workstation/files/atom.apm.list +7 -0
  138. data/cookbooks/workstation/files/atom.config.cson +3 -0
  139. data/cookbooks/workstation/files/cmder.xml +605 -0
  140. data/cookbooks/workstation/metadata.rb +14 -0
  141. data/cookbooks/workstation/recipes/browser.rb +45 -0
  142. data/cookbooks/workstation/recipes/certs-keys.rb +44 -0
  143. data/cookbooks/workstation/recipes/chef.rb +29 -0
  144. data/cookbooks/workstation/recipes/default.rb +16 -0
  145. data/cookbooks/workstation/recipes/dotnet.rb +17 -0
  146. data/cookbooks/workstation/recipes/editor.rb +19 -0
  147. data/cookbooks/workstation/recipes/profile.rb +42 -0
  148. data/cookbooks/workstation/recipes/terminal.rb +13 -0
  149. data/cookbooks/workstation/templates/default/bookmarks.html.erb +23 -0
  150. data/cookbooks/workstation/templates/default/data_collector.rb.erb +2 -0
  151. data/cookbooks/workstation/templates/default/ise_profile.ps1.erb +11 -0
  152. data/cookbooks/workstation/templates/default/knife.rb.erb +10 -0
  153. data/cookbooks/workstation/templates/default/master_preferences.json.erb +28 -0
  154. data/cookbooks/workstation/templates/default/ssh_config.erb +16 -0
  155. data/cookbooks/workstation/test/fixtures/cookbooks/mock_data/files/chef-server.crt +26 -0
  156. data/cookbooks/workstation/test/fixtures/cookbooks/mock_data/files/chef-server.key +27 -0
  157. data/cookbooks/workstation/test/fixtures/cookbooks/mock_data/files/compliance.crt +26 -0
  158. data/cookbooks/workstation/test/fixtures/cookbooks/mock_data/files/compliance.key +27 -0
  159. data/cookbooks/workstation/test/fixtures/cookbooks/mock_data/files/delivery.crt +26 -0
  160. data/cookbooks/workstation/test/fixtures/cookbooks/mock_data/files/delivery.key +27 -0
  161. data/cookbooks/workstation/test/fixtures/cookbooks/mock_data/files/private.pem +27 -0
  162. data/cookbooks/workstation/test/fixtures/cookbooks/mock_data/files/public.pub +1 -0
  163. data/cookbooks/workstation/test/fixtures/cookbooks/mock_data/metadata.rb +2 -0
  164. data/cookbooks/workstation/test/fixtures/cookbooks/mock_data/recipes/default.rb +21 -0
  165. data/cookbooks/workstation/test/integration/default/workstation_spec.rb +37 -0
  166. data/keys/.gitkeep +0 -0
  167. data/lib/wombat/build.rb +195 -0
  168. data/lib/wombat/cli.rb +169 -0
  169. data/lib/wombat/common.rb +163 -0
  170. data/lib/wombat/delete.rb +28 -0
  171. data/lib/wombat/deploy.rb +105 -0
  172. data/lib/wombat/output.rb +45 -0
  173. data/lib/wombat/version.rb +3 -0
  174. data/lib/wombat.rb +8 -0
  175. data/logs/.gitkeep +0 -0
  176. data/packer/automate.json +108 -0
  177. data/packer/build-node.json +114 -0
  178. data/packer/chef-server.json +106 -0
  179. data/packer/compliance.json +106 -0
  180. data/packer/files/.gitkeep +0 -0
  181. data/packer/infranodes.json +108 -0
  182. data/packer/mock-data/.gitignore +16 -0
  183. data/packer/mock-data/.kitchen.yml +21 -0
  184. data/packer/mock-data/Berksfile +3 -0
  185. data/packer/mock-data/README.md +4 -0
  186. data/packer/mock-data/chefignore +102 -0
  187. data/packer/mock-data/metadata.rb +7 -0
  188. data/packer/mock-data/recipes/default.rb +69 -0
  189. data/packer/mock-data/spec/spec_helper.rb +2 -0
  190. data/packer/mock-data/spec/unit/recipes/default_spec.rb +20 -0
  191. data/packer/mock-data/test/integration/default/serverspec/default_spec.rb +9 -0
  192. data/packer/mock-data/test/integration/helpers/serverspec/spec_helper.rb +8 -0
  193. data/packer/workstation.json +97 -0
  194. data/templates/bootstrap-aws.erb +36 -0
  195. data/templates/cfn.json.erb +661 -0
  196. data/terraform/README.md +13 -0
  197. data/terraform/templates/terraform.tfvars.erb +12 -0
  198. data/terraform/wombat.tf +328 -0
  199. data/wombat-cli.gemspec +32 -0
  200. data/wombat.example.yml +52 -0
  201. metadata +331 -0
@@ -0,0 +1,195 @@
1
+
2
+ require 'wombat/common'
3
+ require 'mixlib/shellout'
4
+ require 'parallel'
5
+
6
+ class BuildRunner
7
+
8
+ include Common
9
+
10
+ attr_reader :templates, :builder, :config, :parallel
11
+
12
+ def initialize(opts)
13
+ @templates = opts.templates
14
+ @builder = opts.builder.nil? ? "amazon-ebs" : opts.builder
15
+ @config = opts.config
16
+ @parallel = opts.parallel
17
+ end
18
+
19
+ def start
20
+ banner("Generating keys (if necessary)")
21
+ wombat['certs'].each do |hostname|
22
+ gen_x509_cert(hostname)
23
+ end
24
+
25
+ gen_ssh_key
26
+
27
+ if parallel
28
+ time = Benchmark.measure do
29
+ banner("Starting parallel build for templates: #{templates}")
30
+ parallel_pack(templates, builder)
31
+ end
32
+ else
33
+ time = Benchmark.measure do
34
+ banner("Starting sequential build for templates: #{templates}")
35
+ templates.each do |template|
36
+ options = {}
37
+ # TODO: this needs to be abstracted badly
38
+ case template
39
+ when 'build-node', 'build-nodes'
40
+ vendor_cookbooks('build-node')
41
+ build_nodes.each do |name, num|
42
+ template = 'build-node'
43
+ options = {'node-number' => num}
44
+ packer_cmd = Mixlib::ShellOut.new(packer_build(template, builder, options), :timeout => 3600, live_stream: STDOUT)
45
+ packer_cmd.run_command
46
+ end
47
+ when 'workstation', 'workstations'
48
+ bootstrap_aws
49
+ vendor_cookbooks('workstation')
50
+ workstations.each do |name, num|
51
+ template = 'workstation'
52
+ options = {'workstation-number' => num}
53
+ packer_cmd = Mixlib::ShellOut.new(packer_build(template, builder, options), :timeout => 3600, live_stream: STDOUT)
54
+ packer_cmd.run_command
55
+ end
56
+ when 'infranode', 'infranodes'
57
+ vendor_cookbooks('infranodes')
58
+ infranodes.each do |name, num|
59
+ template = 'infranodes'
60
+ options = {'node-name' => name}
61
+ packer_cmd = Mixlib::ShellOut.new(packer_build(template, builder, options), :timeout => 3600, live_stream: STDOUT)
62
+ packer_cmd.run_command
63
+ end
64
+ else
65
+ vendor_cookbooks(template)
66
+ packer_cmd = Mixlib::ShellOut.new(packer_build(template, builder, options), :timeout => 3600, live_stream: STDOUT)
67
+ packer_cmd.run_command
68
+ puts packer_cmd
69
+ puts packer_cmd.stdout
70
+ puts packer_cmd.stderr unless packer_cmd.stderr.empty?
71
+ end
72
+ end
73
+ end
74
+ end
75
+ banner("Build finished in #{duration(time.real)}.")
76
+ end
77
+
78
+ private
79
+
80
+ def vendor_cookbooks(template)
81
+ base = template.split('.json')[0].tr('-', '_')
82
+ rm = Mixlib::ShellOut.new("rm -rf vendored-cookbooks/#{base} cookbooks/#{base}/Berksfile.lock", live_stream: STDOUT)
83
+ rm.run_command
84
+ puts "Vendoring cookbooks for #{template}"
85
+ vendor = Mixlib::ShellOut.new("berks vendor -q -b cookbooks/#{base}/Berksfile vendored-cookbooks/#{base}", live_stream: STDOUT)
86
+ vendor.run_command
87
+ end
88
+
89
+ def packer_build(template, builder, options={})
90
+ # TODO: this is gross and feels gross so maybe we should do it more better
91
+ create_infranodes_json
92
+ case template
93
+ when 'build-node'
94
+ log_name = "build-node-#{options['node-number']}"
95
+ when 'workstation'
96
+ log_name = "workstation-#{options['workstation-number']}"
97
+ when 'infranodes'
98
+ log_name = "infranodes-#{options['node-name']}"
99
+ else
100
+ log_name = template
101
+ end
102
+
103
+ case builder
104
+ when 'amazon-ebs'
105
+ if template == 'workstation'
106
+ source_ami = wombat['aws']['source_ami']['windows']
107
+ else
108
+ source_ami = wombat['aws']['source_ami']['ubuntu']
109
+ end
110
+ if ENV['AWS_REGION']
111
+ puts "Region set by environment: #{ENV['AWS_REGION']}"
112
+ else
113
+ banner("$AWS_REGION not set, setting to #{wombat['aws']['region']}")
114
+ ENV['AWS_REGION'] = wombat['aws']['region']
115
+ end
116
+ log_prefix = "aws"
117
+ when 'googlecompute'
118
+ if template == 'workstation'
119
+ source_image = wombat['gce']['source_image']['windows']
120
+ else
121
+ source_image = wombat['gce']['source_image']['ubuntu']
122
+ end
123
+ log_prefix = "gce"
124
+ end
125
+ # TODO: fail if packer isn't found in a graceful way
126
+ cmd = %W(packer build #{packer_dir}/#{template}.json | tee #{log_dir}/#{log_prefix}-#{log_name}.log)
127
+ cmd.insert(2, "--only #{builder}")
128
+ cmd.insert(2, "--var org=#{wombat['org']}")
129
+ cmd.insert(2, "--var domain=#{wombat['domain']}")
130
+ cmd.insert(2, "--var domain_prefix=#{wombat['domain_prefix']}")
131
+ cmd.insert(2, "--var enterprise=#{wombat['enterprise']}")
132
+ cmd.insert(2, "--var chefdk=#{wombat['products']['chefdk']}")
133
+ cmd.insert(2, "--var chef_ver=#{wombat['products']['chef'].split('-')[1]}")
134
+ cmd.insert(2, "--var chef_channel=#{wombat['products']['chef'].split('-')[0]}")
135
+ cmd.insert(2, "--var automate=#{wombat['products']['automate']}")
136
+ cmd.insert(2, "--var compliance=#{wombat['products']['compliance']}")
137
+ cmd.insert(2, "--var chef-server=#{wombat['products']['chef-server']}")
138
+ cmd.insert(2, "--var node-name=#{options['node-name']}") if template =~ /infranodes/
139
+ cmd.insert(2, "--var node-number=#{options['node-number']}") if template =~ /build-node/
140
+ cmd.insert(2, "--var build-nodes=#{wombat['build-nodes']}")
141
+ cmd.insert(2, "--var winrm_password=#{wombat['workstation-passwd']}") if template =~ /workstation/
142
+ cmd.insert(2, "--var workstation-number=#{options['workstation-number']}") if template =~ /workstation/
143
+ cmd.insert(2, "--var workstations=#{wombat['workstations']}")
144
+ cmd.insert(2, "--var aws_source_ami=#{source_ami}")
145
+ cmd.insert(2, "--var gce_source_image=#{source_image}")
146
+ cmd.join(' ')
147
+ end
148
+
149
+ def parallel_pack(templates, builder)
150
+ proc_hash = {}
151
+ templates.each do |template_name|
152
+ if template_name == 'infranodes'
153
+ infranodes.each do |name, _rl|
154
+ next if name.empty?
155
+ proc_hash[name] = {
156
+ 'template' => 'infranodes',
157
+ 'options' => {
158
+ 'node-name' => name
159
+ }
160
+ }
161
+ end
162
+ elsif template_name == 'build-node'
163
+ build_nodes.each do |name, num|
164
+ proc_hash[name] = {
165
+ 'template' => 'build-node',
166
+ 'options' => {
167
+ 'node-number' => num
168
+ }
169
+ }
170
+ end
171
+ elsif template_name == 'workstation'
172
+ workstations.each do |name, num|
173
+ proc_hash[name] = {
174
+ 'template' => 'workstation',
175
+ 'options' => {
176
+ 'workstation-number' => num
177
+ }
178
+ }
179
+ end
180
+ else
181
+ proc_hash[template_name] = {
182
+ 'template' => template_name,
183
+ 'options' => {}
184
+ }
185
+ end
186
+ vendor_cookbooks(template_name)
187
+ end
188
+ puts proc_hash
189
+ Parallel.map(proc_hash.keys, in_threads: proc_hash.count) do |name|
190
+ cmd = packer_build(proc_hash[name]['template'], builder, proc_hash[name]['options'])
191
+ packer_cmd = Mixlib::ShellOut.new(cmd, :timeout => 3600, live_stream: STDOUT)
192
+ packer_cmd.run_command
193
+ end
194
+ end
195
+ end
data/lib/wombat/cli.rb ADDED
@@ -0,0 +1,169 @@
1
+ require 'optparse'
2
+ require 'ostruct'
3
+
4
+ require 'wombat/version'
5
+ require 'wombat/common'
6
+ require 'wombat/build'
7
+ require 'wombat/deploy'
8
+ require 'wombat/output'
9
+ require 'wombat/delete'
10
+
11
+ class Options
12
+
13
+ NAME = File.basename($0).freeze
14
+
15
+ def self.parse(args)
16
+ options = OpenStruct.new
17
+ options.templates = calculate_templates("*.json")
18
+
19
+ global = OptionParser.new do |opts|
20
+ opts.banner = "Usage: #{NAME} [SUBCOMMAND [options]]"
21
+ opts.separator ""
22
+ opts.version = Wombat::VERSION
23
+ opts.separator <<-COMMANDS.gsub(/^ {8}/, "")
24
+ build : build one or more templates
25
+ help : prints this help message
26
+ list : list all templates in project
27
+ deploy : deploy a stack
28
+ outputs : get outputs for a stack
29
+ delete : delete a stack
30
+ COMMANDS
31
+ end
32
+
33
+ templates_argv_proc = proc { |options|
34
+ options.templates = calculate_templates(args) unless args.empty?
35
+
36
+ options.templates.each do |t|
37
+ if !File.exists?("packer/#{t}.json")
38
+ $stderr.puts "File packer/#{t}.json does not exist for template '#{t}'"
39
+ exit(1)
40
+ end
41
+ end
42
+ }
43
+
44
+ box_version_argv_proc = proc { |options|
45
+ options.box = ARGV[0]
46
+ options.version = ARGV[1]
47
+ }
48
+
49
+ stack_argv_proc = proc { |options|
50
+ options.stack = ARGV[0]
51
+ }
52
+
53
+ subcommand = {
54
+ help: {
55
+ parser: OptionParser.new {},
56
+ argv: proc { |options|
57
+ puts global
58
+ exit(0)
59
+ }
60
+ },
61
+ build: {
62
+ class: BuildRunner,
63
+ parser: OptionParser.new { |opts|
64
+ opts.banner = "Usage: #{NAME} build [options] TEMPLATE[ TEMPLATE ...]"
65
+
66
+ opts.on("-c CONFIG", "--config CONFIG", "Use config file") do |opt|
67
+ options.config = opt
68
+ end
69
+
70
+ opts.on("-o BUILDER", "--only BUILDER", "Use config file") do |opt|
71
+ options.builder = opt
72
+ end
73
+
74
+ opts.on("--parallel", "Build in parallel") do |opt|
75
+ options.parallel = opt
76
+ end
77
+ },
78
+ argv: templates_argv_proc
79
+ },
80
+ list: {
81
+ class: ListRunner,
82
+ parser: OptionParser.new { |opts|
83
+ opts.banner = "Usage: #{NAME} list [TEMPLATE ...]"
84
+ },
85
+ argv: templates_argv_proc
86
+ },
87
+ outputs: {
88
+ class: OutputRunner,
89
+ parser: OptionParser.new { |opts|
90
+ opts.banner = "Usage: #{NAME} outputs [TEMPLATE ...]"
91
+ },
92
+ argv: stack_argv_proc
93
+ },
94
+ deploy: {
95
+ class: DeployRunner,
96
+ parser: OptionParser.new { |opts|
97
+ opts.banner = "Usage: #{NAME} deploy STACK"
98
+
99
+ opts.on("-c CLOUD", "--cloud CLOUD", "Select cloud") do |opt|
100
+ options.cloud = opt
101
+ end
102
+ },
103
+ argv: stack_argv_proc
104
+ },
105
+ delete: {
106
+ class: DeleteRunner,
107
+ parser: OptionParser.new { |opts|
108
+ opts.banner = "Usage: #{NAME} delete STACK"
109
+
110
+ opts.on("-c CLOUD", "--cloud CLOUD", "Select cloud") do |opt|
111
+ options.cloud = opt
112
+ end
113
+ },
114
+
115
+ argv: stack_argv_proc
116
+ }
117
+ }
118
+
119
+ global.order!
120
+
121
+ command = args.empty? ? :help : ARGV.shift.to_sym
122
+ subcommand.fetch(command).fetch(:parser).order!
123
+ subcommand.fetch(command).fetch(:argv).call(options)
124
+
125
+ options.command = command
126
+ options.klass = subcommand.fetch(command).fetch(:class)
127
+
128
+ options
129
+ end
130
+
131
+ def self.calculate_templates(globs)
132
+ Dir.chdir('packer') do
133
+ Array(globs).
134
+ map { |glob| result = Dir.glob("#{glob}"); result.empty? ? glob : result }.
135
+ flatten.
136
+ sort.
137
+ delete_if { |file| file =~ /\.variables\./ }.
138
+ map { |template| template.sub(/\.json$/, '') }
139
+ end
140
+ end
141
+ end
142
+
143
+ class ListRunner
144
+
145
+ include Common
146
+
147
+ attr_reader :templates
148
+
149
+ def initialize(opts)
150
+ @templates = opts.templates
151
+ end
152
+
153
+ def start
154
+ templates.each { |template| puts template }
155
+ end
156
+ end
157
+
158
+ class Runner
159
+
160
+ attr_reader :options
161
+
162
+ def initialize(options)
163
+ @options = options
164
+ end
165
+
166
+ def start
167
+ options.klass.new(options).start
168
+ end
169
+ end
@@ -0,0 +1,163 @@
1
+ require 'yaml'
2
+ require 'json'
3
+ require 'erb'
4
+ require 'openssl'
5
+ require 'net/ssh'
6
+ require 'benchmark'
7
+
8
+ module Common
9
+
10
+ def banner(msg)
11
+ puts "==> #{msg}"
12
+ end
13
+
14
+ def info(msg)
15
+ puts " #{msg}"
16
+ end
17
+
18
+ def warn(msg)
19
+ puts ">>> #{msg}"
20
+ end
21
+
22
+ def duration(total)
23
+ total = 0 if total.nil?
24
+ minutes = (total / 60).to_i
25
+ seconds = (total - (minutes * 60))
26
+ format("%dm%.2fs", minutes, seconds)
27
+ end
28
+
29
+ def wombat
30
+ if !File.exists?('wombat.yml')
31
+ File.open('wombat.yml', 'w') do |f|
32
+ f.puts File.read('wombat.example.yml')
33
+ end
34
+ end
35
+ YAML.load(File.read('wombat.yml'))
36
+ end
37
+
38
+ def lock
39
+ JSON.parse(File.read('wombat.lock'))
40
+ end
41
+
42
+ def bootstrap_aws
43
+ puts 'Generating bootstrap script from template'
44
+ @workstation_passwd = wombat['workstation-passwd']
45
+ rendered = ERB.new(File.read('templates/bootstrap-aws.erb'), nil, '-').result(binding)
46
+ File.open("#{packer_dir}/scripts/bootstrap-aws.txt", 'w') { |file| file.puts rendered }
47
+ puts "#{packer_dir}/scripts/bootstrap-aws.txt"
48
+ end
49
+
50
+ def gen_x509_cert(hostname)
51
+ rsa_key = OpenSSL::PKey::RSA.new(2048)
52
+ public_key = rsa_key.public_key
53
+
54
+ subject = "/C=AU/ST=New South Wales/L=Sydney/O=#{wombat['org']}/OU=wombats/CN=#{wombat['domain_prefix']}#{hostname}.#{wombat['domain']}"
55
+
56
+ cert = OpenSSL::X509::Certificate.new
57
+ cert.subject = cert.issuer = OpenSSL::X509::Name.parse(subject)
58
+ cert.not_before = Time.now
59
+ cert.not_after = Time.now + 365 * 24 * 60 * 60
60
+ cert.public_key = public_key
61
+ cert.serial = 0x0
62
+ cert.version = 2
63
+
64
+ ef = OpenSSL::X509::ExtensionFactory.new
65
+ ef.subject_certificate = cert
66
+ ef.issuer_certificate = cert
67
+ cert.extensions = [
68
+ ef.create_extension('basicConstraints', 'CA:TRUE', true),
69
+ ef.create_extension('subjectKeyIdentifier', 'hash'),
70
+ # ef.create_extension("keyUsage", "cRLSign,keyCertSign", true),
71
+ ]
72
+ cert.add_extension ef.create_extension('authorityKeyIdentifier',
73
+ 'keyid:always,issuer:always')
74
+
75
+ cert.sign(rsa_key, OpenSSL::Digest::SHA256.new)
76
+
77
+ if File.exist?("#{key_dir}/#{hostname}.crt") && File.exist?("#{key_dir}/#{hostname}.key")
78
+ puts "An x509 certificate already exists for #{hostname}"
79
+ else
80
+ File.open("#{key_dir}/#{hostname}.crt", 'w') { |file| file.puts cert.to_pem }
81
+ File.open("#{key_dir}/#{hostname}.key", 'w') { |file| file.puts rsa_key.to_pem }
82
+ puts "Certificate created for #{wombat['domain_prefix']}#{hostname}.#{wombat['domain']}"
83
+ end
84
+ end
85
+
86
+ def gen_ssh_key
87
+ rsa_key = OpenSSL::PKey::RSA.new 2048
88
+
89
+ type = rsa_key.ssh_type
90
+ data = [rsa_key.to_blob].pack('m0')
91
+
92
+ openssh_format = "#{type} #{data}"
93
+
94
+ if File.exist?("#{key_dir}/public.pub") && File.exist?("#{key_dir}/private.pem")
95
+ puts 'An SSH keypair already exists'
96
+ else
97
+ File.open("#{key_dir}/public.pub", 'w') { |file| file.puts openssh_format }
98
+ File.open("#{key_dir}/private.pem", 'w') { |file| file.puts rsa_key.to_pem }
99
+ puts 'SSH Keypair created'
100
+ end
101
+ end
102
+
103
+ def parse_log(instance, cloud)
104
+ case cloud
105
+ when "aws", "amazon", "jeffbezosband", "cfn"
106
+ File.read("#{log_dir}/aws-#{instance}.log").split("\n").grep(/#{wombat['aws']['region']}:/) {|x| x.split[1]}.last
107
+ when "gce", "gcp", "google", "gdm"
108
+ File.read("#{log_dir}/gce-#{instance}.log").split("\n").grep(/A disk image was created:/) {|x| x.split[1]}.last
109
+ end
110
+ end
111
+
112
+ def infranodes
113
+ unless wombat['infranodes'].nil?
114
+ wombat['infranodes'].sort
115
+ else
116
+ puts 'No infranodes listed in wombat.yml'
117
+ end
118
+ end
119
+
120
+ def build_nodes
121
+ build_nodes = {}
122
+ 1.upto(wombat['build-nodes'].to_i) do |i|
123
+ build_nodes["build-node-#{i}"] = i
124
+ end
125
+ build_nodes
126
+ end
127
+
128
+ def workstations
129
+ workstations = {}
130
+ 1.upto(wombat['workstations'].to_i) do |i|
131
+ workstations["workstation-#{i}"] = i
132
+ end
133
+ workstations
134
+ end
135
+
136
+ def create_infranodes_json
137
+ if File.exists?("#{packer_dir}/file/infranodes-info.json")
138
+ current_state = JSON(File.read('files/infranodes-info.json'))
139
+ else
140
+ current_state = nil
141
+ end
142
+ return if current_state == infranodes # yay idempotence
143
+ File.open("#{packer_dir}/files/infranodes-info.json", 'w') do |f|
144
+ f.puts JSON.pretty_generate(infranodes)
145
+ end
146
+ end
147
+
148
+ def key_dir
149
+ wombat['conf'].nil? ? 'keys' : wombat['conf']['key_dir']
150
+ end
151
+
152
+ def cookbook_dir
153
+ wombat['conf'].nil? ? 'cookbooks' : wombat['conf']['cookbook_dir']
154
+ end
155
+
156
+ def packer_dir
157
+ wombat['conf'].nil? ? 'packer' : wombat['conf']['packer_dir']
158
+ end
159
+
160
+ def log_dir
161
+ wombat['conf'].nil? ? 'logs' : wombat['conf']['log_dir']
162
+ end
163
+ end
@@ -0,0 +1,28 @@
1
+ require 'wombat/common'
2
+ require 'aws-sdk'
3
+
4
+ class DeleteRunner
5
+ include Common
6
+
7
+ attr_reader :stack, :cloud
8
+
9
+ def initialize(opts)
10
+ @stack = opts.stack
11
+ @cloud = opts.cloud.nil? ? "aws" : opts.cloud
12
+ end
13
+
14
+ def start
15
+ cfn_delete_stack(stack)
16
+ end
17
+
18
+ private
19
+
20
+ def cfn_delete_stack(stack)
21
+ cfn = Aws::CloudFormation::Client.new(region: lock['aws']['region'])
22
+
23
+ resp = cfn.delete_stack({
24
+ stack_name: stack,
25
+ })
26
+ banner("Deleted #{stack}")
27
+ end
28
+ end
@@ -0,0 +1,105 @@
1
+ require 'wombat/common'
2
+ require 'aws-sdk'
3
+
4
+ class DeployRunner
5
+ include Common
6
+
7
+ attr_reader :stack, :cloud
8
+
9
+ def initialize(opts)
10
+ @stack = opts.stack
11
+ @cloud = opts.cloud.nil? ? "aws" : opts.cloud
12
+ end
13
+
14
+ def start
15
+ case cloud
16
+ when 'aws'
17
+ update_lock(cloud)
18
+ create_template
19
+ create_stack(stack)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def create_stack(stack)
26
+ template_file = File.read("#{stack}.json")
27
+ cfn = Aws::CloudFormation::Client.new(region: lock['aws']['region'])
28
+
29
+ banner("Creating CloudFormation stack")
30
+ resp = cfn.create_stack({
31
+ stack_name: "#{stack}",
32
+ template_body: template_file,
33
+ capabilities: ["CAPABILITY_IAM"],
34
+ on_failure: "DELETE",
35
+ parameters: [
36
+ {
37
+ parameter_key: "KeyName",
38
+ parameter_value: lock['aws']['keypair'],
39
+ }
40
+ ]
41
+ })
42
+ puts "Created: #{resp.stack_id}"
43
+ end
44
+
45
+ def create_template
46
+ region = lock['aws']['region']
47
+ @chef_server_ami = lock['amis'][region]['chef-server']
48
+ @automate_ami = lock['amis'][region]['automate']
49
+ @compliance_ami = lock['amis'][region]['compliance']
50
+ @build_nodes = lock['build-nodes'].to_i
51
+ @build_node_ami = {}
52
+ 1.upto(@build_nodes) do |i|
53
+ @build_node_ami[i] = lock['amis'][region]['build-node'][i.to_s]
54
+ end
55
+ @infra = {}
56
+ infranodes.each do |name, _rl|
57
+ @infra[name] = lock['amis'][region]['infranodes'][name]
58
+ end
59
+ @workstations = lock['workstations'].to_i
60
+ @workstation_ami = {}
61
+ 1.upto(@workstations) do |i|
62
+ @workstation_ami[i] = lock['amis'][region]['workstation'][i.to_s]
63
+ end
64
+ @availability_zone = lock['aws']['az']
65
+ @demo = lock['name']
66
+ @version = lock['version']
67
+ @ttl = lock['ttl']
68
+ rendered_cfn = ERB.new(File.read('templates/cfn.json.erb'), nil, '-').result(binding)
69
+ File.open("#{@demo}.json", 'w') { |file| file.puts rendered_cfn }
70
+ banner("Generate CloudFormation JSON: #{@demo}.json")
71
+ end
72
+
73
+ def update_lock(cloud)
74
+ copy = {}
75
+ copy = wombat
76
+ region = copy[cloud]['region']
77
+ banner('Updating wombat.lock')
78
+ copy['amis'] = { region => {} }
79
+ Dir.glob("#{log_dir}/#{cloud}*.log") do |log|
80
+ instance = log.match('aws-(.*)\.log')[1]
81
+ if instance =~ /build-node/
82
+ copy['amis'][region].store('build-node', {})
83
+ 1.upto(wombat['build-nodes'].to_i) do |i|
84
+ copy['amis'][region]['build-node'].store(i.to_s, parse_log("build-node-#{i}", "aws"))
85
+ end
86
+ elsif instance =~ /workstation/
87
+ copy['amis'][region].store('workstation', {})
88
+ 1.upto(wombat['workstations'].to_i) do |i|
89
+ copy['amis'][region]['workstation'].store(i.to_s, parse_log("workstation-#{i}", "aws"))
90
+ end
91
+ elsif instance =~ /infranodes/
92
+ copy['amis'][region].store('infranodes', {})
93
+ infranodes.each do |name, _rl|
94
+ copy['amis'][region]['infranodes'].store(name, parse_log("infranodes-#{name}", "aws"))
95
+ end
96
+ else
97
+ copy['amis'][region].store(instance, parse_log(instance, "aws"))
98
+ end
99
+ end
100
+ copy['last_updated'] = Time.now.gmtime.strftime('%Y%m%d%H%M%S')
101
+ File.open('wombat.lock', 'w') do |f|
102
+ f.write(JSON.pretty_generate(copy))
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,45 @@
1
+ require 'wombat/common'
2
+ require 'aws-sdk'
3
+
4
+ class OutputRunner
5
+
6
+ include Common
7
+
8
+ attr_reader :stack
9
+
10
+ def initialize(opts)
11
+ @stack = opts.stack
12
+ end
13
+
14
+ def start
15
+ cfn_workstation_ips(stack)
16
+ end
17
+
18
+ private
19
+
20
+ def cfn_workstation_ips(stack)
21
+ ec2 = Aws::EC2::Resource.new
22
+ instances = cfn_stack_instances(stack)
23
+ instances.each do |name, id|
24
+ instance = ec2.instance(id)
25
+ if /Workstation/.match(name)
26
+ puts "#{name} (#{id}) => #{instance.public_ip_address}"
27
+ end
28
+ end
29
+ end
30
+
31
+ def cfn_stack_instances(stack)
32
+ cfn = Aws::CloudFormation::Client.new
33
+ resp = cfn.describe_stack_resources({
34
+ stack_name: stack,
35
+ })
36
+
37
+ instances = {}
38
+ resp.stack_resources.map do |resource|
39
+ if resource.resource_type == 'AWS::EC2::Instance'
40
+ instances[resource.logical_resource_id] = resource.physical_resource_id
41
+ end
42
+ end
43
+ instances
44
+ end
45
+ end