amigrind 0.1.1

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