amigrind 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +72 -0
  5. data/.travis.yml +4 -0
  6. data/CODE_OF_CONDUCT.md +49 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +201 -0
  9. data/README.md +112 -0
  10. data/Rakefile +6 -0
  11. data/amigrind.gemspec +36 -0
  12. data/bin/amigrind +8 -0
  13. data/lib/amigrind.rb +29 -0
  14. data/lib/amigrind/blueprints/aws_config.rb +56 -0
  15. data/lib/amigrind/blueprints/base_ami_source.rb +15 -0
  16. data/lib/amigrind/blueprints/blueprint.rb +22 -0
  17. data/lib/amigrind/blueprints/evaluator.rb +269 -0
  18. data/lib/amigrind/blueprints/parent_blueprint_source.rb +10 -0
  19. data/lib/amigrind/blueprints/provisioner.rb +20 -0
  20. data/lib/amigrind/blueprints/provisioners/file_upload.rb +29 -0
  21. data/lib/amigrind/blueprints/provisioners/local_shell.rb +28 -0
  22. data/lib/amigrind/blueprints/provisioners/remote_shell.rb +57 -0
  23. data/lib/amigrind/build/packer_runner.rb +106 -0
  24. data/lib/amigrind/build/rackerizer.rb +134 -0
  25. data/lib/amigrind/builder.rb +46 -0
  26. data/lib/amigrind/cli.rb +12 -0
  27. data/lib/amigrind/cli/_helpers.rb +49 -0
  28. data/lib/amigrind/cli/_root.rb +43 -0
  29. data/lib/amigrind/cli/blueprints/_category.rb +15 -0
  30. data/lib/amigrind/cli/blueprints/list.rb +21 -0
  31. data/lib/amigrind/cli/blueprints/show.rb +22 -0
  32. data/lib/amigrind/cli/build/_category.rb +15 -0
  33. data/lib/amigrind/cli/build/execute.rb +32 -0
  34. data/lib/amigrind/cli/build/print_packer.rb +28 -0
  35. data/lib/amigrind/cli/environments/_category.rb +15 -0
  36. data/lib/amigrind/cli/environments/list.rb +23 -0
  37. data/lib/amigrind/cli/environments/show.rb +22 -0
  38. data/lib/amigrind/cli/inventory/_category.rb +15 -0
  39. data/lib/amigrind/cli/inventory/add_to_channel.rb +28 -0
  40. data/lib/amigrind/cli/inventory/get-image.rb +34 -0
  41. data/lib/amigrind/cli/inventory/list.rb +14 -0
  42. data/lib/amigrind/cli/inventory/remove_from_channel.rb +28 -0
  43. data/lib/amigrind/cli/repo/_category.rb +15 -0
  44. data/lib/amigrind/cli/repo/init.rb +22 -0
  45. data/lib/amigrind/config.rb +89 -0
  46. data/lib/amigrind/environments/channel.rb +9 -0
  47. data/lib/amigrind/environments/environment.rb +66 -0
  48. data/lib/amigrind/environments/rb_evaluator.rb +7 -0
  49. data/lib/amigrind/repo.rb +217 -0
  50. data/lib/amigrind/version.rb +3 -0
  51. data/sample_config/config.yaml +9 -0
  52. data/sample_repo/.amigrind_root +0 -0
  53. data/sample_repo/blueprints/dependent_ubuntu.rb +21 -0
  54. data/sample_repo/blueprints/simple_ubuntu.rb +37 -0
  55. data/sample_repo/environments/development.yaml.example +15 -0
  56. metadata +288 -0
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/amigrind.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'amigrind/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "amigrind"
8
+ spec.version = Amigrind::VERSION
9
+ spec.authors = ["Ed Ropple"]
10
+ spec.email = ["ed@edropple.com"]
11
+
12
+ spec.summary = "An easy, convention-over-configuration builder for Packer images."
13
+ spec.homepage = "https://github.com/eropple/amigrind"
14
+ spec.license = "Apache 2.0"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "bin"
18
+ spec.executables = [ 'amigrind' ]
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.11"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "rspec", "~> 3.0"
24
+
25
+ spec.add_runtime_dependency 'cri', '~> 2.7.0'
26
+ spec.add_runtime_dependency 'os', '~> 0.9.6'
27
+ spec.add_runtime_dependency 'racker', '~> 0.2.0'
28
+ spec.add_runtime_dependency 'virtus', '~> 1.0.5'
29
+ spec.add_runtime_dependency 'activesupport', '~> 4.2.6'
30
+ spec.add_runtime_dependency 'activemodel', '~> 4.2.6'
31
+ spec.add_runtime_dependency 'erubis', '~> 2.7.0'
32
+ spec.add_runtime_dependency 'ptools', '~> 1.3', '>= 1.3.3'
33
+ spec.add_runtime_dependency 'settingslogic', '~> 2.0.9'
34
+
35
+ spec.add_runtime_dependency 'amigrind-core', '~> 0.1.0'
36
+ end
data/bin/amigrind ADDED
@@ -0,0 +1,8 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
5
+
6
+ require 'amigrind/cli'
7
+
8
+ Amigrind::CLI.run(ARGV)
data/lib/amigrind.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'amigrind/version'
2
+ require 'amigrind/core'
3
+ require 'amigrind/config'
4
+ require 'amigrind/repo'
5
+
6
+ require 'ptools'
7
+ require 'open3'
8
+ require 'fileutils'
9
+ require 'aws-sdk'
10
+ require 'racker'
11
+ require 'json'
12
+ require 'yaml'
13
+ require 'ice_nine'
14
+ require 'set'
15
+ require 'erubis'
16
+ require 'virtus'
17
+ require 'active_support/core_ext/string/inflections'
18
+ require 'active_support/core_ext/hash'
19
+ require 'active_support/core_ext/numeric/time'
20
+
21
+ # Pre-includes
22
+ module Amigrind
23
+ end
24
+
25
+ Dir["#{__dir__}/**/*.rb"].reject { |f| f.include?('/cli') }.each { |f| require_relative f }
26
+
27
+ # Post-includes
28
+ module Amigrind
29
+ end
@@ -0,0 +1,56 @@
1
+ module Amigrind
2
+ module Blueprints
3
+ class AWSConfig
4
+ include Virtus.model(constructor: false, mass_assignment: false)
5
+
6
+ class BlockDeviceMapping
7
+ end
8
+
9
+ attribute :instance_type, String
10
+ # TODO: ssh_username is currently required for all builds, but should
11
+ # instead inherit from the parent blueprint if one exists. This
12
+ # is annoying and recursive right now, and a small pain point,
13
+ # so I've punted it.
14
+ attribute :ssh_username, String
15
+
16
+ attribute :region, String
17
+ attribute :copy_regions, Array[String]
18
+ attribute :associate_public_ip_address, Boolean
19
+ attribute :ebs_optimized, Boolean
20
+ attribute :enhanced_networking, Boolean
21
+ attribute :iam_instance_profile, String
22
+ attribute :ssh_keypair_name, String
23
+ attribute :ssh_private_ip, Boolean
24
+ attribute :user_data, String
25
+ attribute :windows_password_timeout, ActiveSupport::Duration
26
+
27
+ attribute :run_tags, Hash[String => String]
28
+ attribute :run_volume_tags, Hash[String => String]
29
+ attribute :security_group_ids, Array[String]
30
+
31
+ attribute :vpc_id, String
32
+ attribute :subnet_ids, Array[String]
33
+
34
+ # TODO: object-ize the ami block device mappings
35
+ attribute :ami_block_device_mappings, Array[BlockDeviceMapping]
36
+ attribute :launch_block_device_mappings, Array[BlockDeviceMapping]
37
+
38
+ attr_reader :custom
39
+
40
+ def initialize
41
+ @copy_regions = []
42
+
43
+ @run_tags = {}
44
+ @run_volume_tags = {}
45
+ @security_group_ids = []
46
+
47
+ @subnet_ids = []
48
+
49
+ @ami_block_device_mappings = []
50
+ @launch_block_device_mappings = []
51
+
52
+ @custom = {}
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,15 @@
1
+ module Amigrind
2
+ module Blueprints
3
+ class BaseAMISource
4
+ include Virtus.model(constructor: false, mass_assignment: false)
5
+
6
+ attribute :family, Symbol
7
+ attribute :version, String
8
+ attribute :ids, Hash[String => String]
9
+
10
+ def initialize
11
+ @ids = {}
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ require 'amigrind/blueprints/provisioner'
2
+
3
+ module Amigrind
4
+ module Blueprints
5
+ class Blueprint
6
+ include Virtus.model(constructor: false, mass_assignment: false)
7
+
8
+ attribute :name, String
9
+ attribute :build_channel, Symbol
10
+ attribute :description, String, default: ''
11
+ attr_accessor :source
12
+ attribute :aws, Amigrind::Blueprints::AWSConfig
13
+
14
+ attribute :provisioners, Array[Amigrind::Blueprints::Provisioner], default: []
15
+
16
+ def initialize
17
+ @aws = AWSConfig.new
18
+ @provisioners = []
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,269 @@
1
+ module Amigrind
2
+ module Blueprints
3
+ class Evaluator
4
+ include Amigrind::Core::Logging::Mixin
5
+ include Amigrind::Blueprints::Provisioners
6
+
7
+ attr_reader :blueprint
8
+
9
+ def initialize(filename, environment = nil)
10
+ @blueprint = Blueprint.new
11
+ @blueprint.name = File.basename(filename, ".rb")
12
+
13
+ regex = Amigrind::Core::BLUEPRINT_NAME_REGEX
14
+ raise "blueprint name (#{@blueprint.name}) must match #{regex.source}" \
15
+ unless regex.match(@blueprint.name)
16
+
17
+ @properties =
18
+ if environment.nil?
19
+ debug_log "no environment found to use with blueprint"
20
+ {}
21
+ else
22
+ debug_log "using environment '#{environment.name}' with blueprint"
23
+ environment.properties.merge(environment_name: environment.name)
24
+ end
25
+
26
+ unless environment.nil?
27
+ @blueprint.aws.vpc_id = environment.aws.vpc
28
+ @blueprint.aws.subnet_ids = environment.aws.subnets
29
+
30
+ @blueprint.aws.region = environment.aws.region
31
+ @blueprint.aws.copy_regions = environment.aws.copy_regions
32
+ @blueprint.aws.ssh_keypair_name = environment.aws.ssh_keypair_name
33
+ end
34
+
35
+ instance_eval(IO.read(filename), File.expand_path(filename), 0)
36
+ end
37
+
38
+ def self.evaluate(filename, environment = nil)
39
+ Evaluator.new(filename, environment).blueprint
40
+ end
41
+
42
+ def properties
43
+ @properties
44
+ end
45
+
46
+ private
47
+
48
+ def build_channel(b)
49
+ raise "'build_channel' must be a String or Symbol." \
50
+ unless b.is_a?(String) || b.is_a?(Symbol)
51
+
52
+ @blueprint.build_channel = b
53
+ end
54
+
55
+ def description(d)
56
+ raise "'description' must be a String." unless d.is_a?(String)
57
+ @blueprint.description = d
58
+ end
59
+
60
+ def parent_blueprint(p)
61
+ raise "'parent_blueprint' must be a String." unless p.is_a?(String)
62
+ @blueprint.source = p
63
+ end
64
+
65
+ def source(type, &block)
66
+ case type
67
+ when :ami
68
+ BaseAMIEvaluator.new(@blueprint, self, &block)
69
+ when :parent
70
+ ParentBlueprintEvaluator.new(@blueprint, self, &block)
71
+ else
72
+ raise "Invalid source type: #{type} (must be :ami, :parent)"
73
+ end
74
+ end
75
+
76
+ def aws(&block)
77
+ AWSConfigEvaluator.new(@blueprint, self, &block)
78
+ end
79
+
80
+ def provisioner(name, provisioner_class, weight: nil, &block)
81
+ weight ||= (@blueprint.provisioners.max_by(&:weight) || 0) + 1
82
+
83
+ raise "'name' must be a String or Symbol." \
84
+ unless name.is_a?(String) || name.is_a?(Symbol)
85
+ raise "'provisioner_class' must inherit from Amigrind::Blueprints::Provisioner" \
86
+ unless provisioner_class.ancestors.include?(Amigrind::Blueprints::Provisioner)
87
+ raise "'weight' must be a Fixnum." unless weight.is_a?(Fixnum)
88
+
89
+ @blueprint.provisioners <<
90
+ ProvisionerEvaluator.new(name, self, weight, provisioner_class, &block).provisioner
91
+ end
92
+
93
+ class BaseAMIEvaluator
94
+ def initialize(bp, evaluator, &block)
95
+ @bp = bp
96
+ @bp.source = Amigrind::Blueprints::BaseAMISource.new
97
+
98
+ @evaluator = evaluator
99
+
100
+ instance_eval(&block)
101
+ end
102
+
103
+ private
104
+
105
+ def properties
106
+ @evaluator.properties
107
+ end
108
+
109
+ def family(f)
110
+ raise "'family' must implement #to_sym." unless f.respond_to?(:to_sym)
111
+
112
+ @bp.source.family = f.to_sym.to_s.strip.to_sym
113
+ end
114
+
115
+ def version(v)
116
+ raise "'version' must be a String." unless v.is_a?(String)
117
+
118
+ @bp.source.version = v.strip
119
+ end
120
+
121
+ def id(region, image_id)
122
+ regex = Amigrind::Core::AMI_REGEX
123
+
124
+ raise "'region' must be stringable." unless region.respond_to?(:to_s)
125
+ raise "'image_id' must be in AMI format (/#{regex.source}/)" \
126
+ unless regex.match(image_id)
127
+
128
+ @bp.source.ids[region.to_s.strip] = image_id
129
+ end
130
+
131
+ def ids(ami_ids)
132
+ raise "'ami_ids' must be a Hash-alike." unless ami_ids.respond_to?(:each_pair)
133
+ ami_ids.each_pair { |region, image_id| id(region, image_id) }
134
+ end
135
+ end
136
+
137
+ class ParentBlueprintEvaluator
138
+ def initialize(bp, evaluator, &block)
139
+ @bp = bp
140
+ @bp.source = Amigrind::Blueprints::ParentBlueprintSource.new
141
+
142
+ @evaluator = evaluator
143
+
144
+ instance_eval(&block)
145
+ end
146
+
147
+ private
148
+
149
+ def properties
150
+ @evaluator.properties
151
+ end
152
+
153
+ def name(n)
154
+ raise "'name' must be a String." unless n.is_a?(String)
155
+
156
+ @bp.source.name = n
157
+ end
158
+
159
+ def channel(c)
160
+ raise "'channel' must implement #to_sym." unless c.respond_to?(:to_sym)
161
+
162
+ @bp.source.channel = c
163
+ end
164
+ end
165
+
166
+ class AWSConfigEvaluator
167
+ def initialize(bp, evaluator, &block)
168
+ @bp = bp
169
+
170
+ @evaluator = evaluator
171
+
172
+ instance_eval(&block)
173
+ end
174
+
175
+ private
176
+
177
+ def properties
178
+ @evaluator.properties
179
+ end
180
+
181
+ def run_tag(key, value)
182
+ raise "'key' must be a String." unless key.is_a?(String)
183
+ raise "'value' must be stringable." unless value.respond_to?(:to_s)
184
+
185
+ @bp.aws.run_tags[key] = value.to_s
186
+ end
187
+
188
+ def run_tags(tags)
189
+ raise "'tags' must be a Hash." unless tags.respond_to?(:each_pair)
190
+ tags.each_pair { |key, value| run_tag(key, value) }
191
+ end
192
+
193
+ def run_volume_tag(key, value)
194
+ raise "'key' must be a String." unless key.is_a?(String)
195
+ raise "'value' must be stringable." unless value.respond_to?(:to_s)
196
+
197
+ @bp.aws.run_volume_tags[key] = value.to_s
198
+ end
199
+
200
+ def run_volume_tags(tags)
201
+ raise "'tags' must be a Hash." unless tags.respond_to?(:each_pair)
202
+ tags.each_pair { |key, value| run_volume_tag(key, value) }
203
+ end
204
+
205
+ def vpc(vpc_id)
206
+ regex = Amigrind::Core::VPC_REGEX
207
+ raise "'vpc_id' must be a resource (#{regex.source})" unless regex.match(vpc_id)
208
+
209
+ @bp.aws.vpc_id = vpc_id
210
+ end
211
+
212
+ def subnet(subnet_id)
213
+ regex = Amigrind::Core::SUBNET_REGEX
214
+ raise "'subnet_id' must be a resource (#{regex.source})" unless regex.match(subnet_id)
215
+
216
+ @bp.aws.subnet_ids << subnet_id
217
+ end
218
+
219
+ def security_group(sg_id)
220
+ regex = Amigrind::Core::SG_REGEX
221
+ raise "'sg_id' must be a resource (#{regex.source})." unless regex.match(sg_id)
222
+
223
+ @bp.aws.security_group_ids << sg_id
224
+ end
225
+
226
+ def custom(key, value)
227
+ raise "custom 'key' must be a Symbol." unless key.is_a?(Symbol)
228
+ raise "custom 'value' must be non-nil." if value.nil?
229
+
230
+ @bp.aws.custom[key] = value
231
+ end
232
+
233
+ def method_missing(m, *args)
234
+ @bp.aws.send(:"#{m}=", args[0])
235
+ end
236
+ end
237
+ end
238
+
239
+ class ProvisionerEvaluator
240
+ attr_reader :provisioner
241
+
242
+ def initialize(name, evaluator, weight, provisioner_class, &block)
243
+ @provisioner = provisioner_class.new
244
+ @provisioner.name = name.to_s
245
+ @provisioner.weight = weight
246
+
247
+ @evaluator = evaluator
248
+
249
+ instance_eval(&block)
250
+ end
251
+
252
+ def method_missing(m, *args)
253
+ eq_msg = :"#{m}="
254
+
255
+ if @provisioner.respond_to?(eq_msg)
256
+ @provisioner.send(eq_msg, args[0])
257
+ else
258
+ @provisioner.send(m, *args)
259
+ end
260
+ end
261
+
262
+ private
263
+
264
+ def properties
265
+ @evaluator.properties
266
+ end
267
+ end
268
+ end
269
+ end