amigrind 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.rubocop.yml +72 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +201 -0
- data/README.md +112 -0
- data/Rakefile +6 -0
- data/amigrind.gemspec +36 -0
- data/bin/amigrind +8 -0
- data/lib/amigrind.rb +29 -0
- data/lib/amigrind/blueprints/aws_config.rb +56 -0
- data/lib/amigrind/blueprints/base_ami_source.rb +15 -0
- data/lib/amigrind/blueprints/blueprint.rb +22 -0
- data/lib/amigrind/blueprints/evaluator.rb +269 -0
- data/lib/amigrind/blueprints/parent_blueprint_source.rb +10 -0
- data/lib/amigrind/blueprints/provisioner.rb +20 -0
- data/lib/amigrind/blueprints/provisioners/file_upload.rb +29 -0
- data/lib/amigrind/blueprints/provisioners/local_shell.rb +28 -0
- data/lib/amigrind/blueprints/provisioners/remote_shell.rb +57 -0
- data/lib/amigrind/build/packer_runner.rb +106 -0
- data/lib/amigrind/build/rackerizer.rb +134 -0
- data/lib/amigrind/builder.rb +46 -0
- data/lib/amigrind/cli.rb +12 -0
- data/lib/amigrind/cli/_helpers.rb +49 -0
- data/lib/amigrind/cli/_root.rb +43 -0
- data/lib/amigrind/cli/blueprints/_category.rb +15 -0
- data/lib/amigrind/cli/blueprints/list.rb +21 -0
- data/lib/amigrind/cli/blueprints/show.rb +22 -0
- data/lib/amigrind/cli/build/_category.rb +15 -0
- data/lib/amigrind/cli/build/execute.rb +32 -0
- data/lib/amigrind/cli/build/print_packer.rb +28 -0
- data/lib/amigrind/cli/environments/_category.rb +15 -0
- data/lib/amigrind/cli/environments/list.rb +23 -0
- data/lib/amigrind/cli/environments/show.rb +22 -0
- data/lib/amigrind/cli/inventory/_category.rb +15 -0
- data/lib/amigrind/cli/inventory/add_to_channel.rb +28 -0
- data/lib/amigrind/cli/inventory/get-image.rb +34 -0
- data/lib/amigrind/cli/inventory/list.rb +14 -0
- data/lib/amigrind/cli/inventory/remove_from_channel.rb +28 -0
- data/lib/amigrind/cli/repo/_category.rb +15 -0
- data/lib/amigrind/cli/repo/init.rb +22 -0
- data/lib/amigrind/config.rb +89 -0
- data/lib/amigrind/environments/channel.rb +9 -0
- data/lib/amigrind/environments/environment.rb +66 -0
- data/lib/amigrind/environments/rb_evaluator.rb +7 -0
- data/lib/amigrind/repo.rb +217 -0
- data/lib/amigrind/version.rb +3 -0
- data/sample_config/config.yaml +9 -0
- data/sample_repo/.amigrind_root +0 -0
- data/sample_repo/blueprints/dependent_ubuntu.rb +21 -0
- data/sample_repo/blueprints/simple_ubuntu.rb +37 -0
- data/sample_repo/environments/development.yaml.example +15 -0
- metadata +288 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
module Amigrind
|
2
|
+
module Blueprints
|
3
|
+
class Provisioner
|
4
|
+
include Virtus.model(constructor: false, mass_assignment: false)
|
5
|
+
|
6
|
+
attribute :name, String
|
7
|
+
attribute :weight, Fixnum
|
8
|
+
|
9
|
+
def racker_name
|
10
|
+
"#{name}-#{weight}-#{self.class.name.demodulize}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_racker_hash
|
14
|
+
raise "#{self.class.name}#to_racker_hash must be implemented."
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Dir["#{__dir__}/provisioners/**.rb"].each { |f| require_relative f }
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Amigrind
|
2
|
+
module Blueprints
|
3
|
+
module Provisioners
|
4
|
+
class FileUpload < Amigrind::Blueprints::Provisioner
|
5
|
+
attribute :source, String
|
6
|
+
attribute :destination, String
|
7
|
+
attribute :direction_method, String
|
8
|
+
|
9
|
+
def direction=(dir)
|
10
|
+
case dir
|
11
|
+
when :download, :upload
|
12
|
+
@direction_method = dir.to_s
|
13
|
+
else
|
14
|
+
raise "unrecognized 'direction': #{dir}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_racker_hash
|
19
|
+
{
|
20
|
+
type: 'file',
|
21
|
+
source: @source,
|
22
|
+
destination: @destination,
|
23
|
+
direction: @direction_method
|
24
|
+
}.delete_if { |_, v| v.nil? }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Amigrind
|
2
|
+
module Blueprints
|
3
|
+
module Provisioners
|
4
|
+
class LocalShell < Amigrind::Blueprints::Provisioner
|
5
|
+
attribute :inline, Array[String]
|
6
|
+
|
7
|
+
def command=(cmd)
|
8
|
+
raise "'command' must be a String or an array of String." \
|
9
|
+
unless cmd.is_a?(String) ||
|
10
|
+
(cmd.respond_to?(:all?) && cmd.all? { |l| l.is_a?(String) })
|
11
|
+
|
12
|
+
if cmd.is_a?(String)
|
13
|
+
@inline = cmd.split("\n")
|
14
|
+
else
|
15
|
+
@inline = cmd
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_racker_hash
|
20
|
+
{
|
21
|
+
type: 'shell-local',
|
22
|
+
command: @inline.join("\n")
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Amigrind
|
2
|
+
module Blueprints
|
3
|
+
module Provisioners
|
4
|
+
class RemoteShell < Amigrind::Blueprints::Provisioner
|
5
|
+
attribute :inline, Array[String]
|
6
|
+
attribute :scripts, Array[String]
|
7
|
+
attribute :binary, Boolean
|
8
|
+
attribute :env_vars, Hash[String => String]
|
9
|
+
attribute :execute_command, String
|
10
|
+
attribute :inline_shebang, String
|
11
|
+
attribute :remote_path, String
|
12
|
+
attribute :skip_clean, Boolean
|
13
|
+
attribute :start_retry_timeout, ActiveSupport::Duration
|
14
|
+
|
15
|
+
def run_as_root!
|
16
|
+
@execute_command = "{{ .Vars }} sudo -E -S sh '{{ .Path }}'"
|
17
|
+
end
|
18
|
+
|
19
|
+
def command=(cmd)
|
20
|
+
raise "'command' must be a String or an array of String." \
|
21
|
+
unless cmd.is_a?(String) ||
|
22
|
+
(cmd.respond_to?(:all?) && cmd.all? { |l| l.is_a?(String) })
|
23
|
+
|
24
|
+
@inline = cmd.is_a?(String) ? cmd.split("\n") : cmd
|
25
|
+
end
|
26
|
+
|
27
|
+
def environment_vars=(vars)
|
28
|
+
raise "'vars' must be a Hash with Symbol or String keys and stringable values." \
|
29
|
+
unless vars.all? { |k, v| (k.is_a?(Symbol) || k.is_a?(String)) && v.respond_to?(:to_s)}
|
30
|
+
|
31
|
+
@env_vars =
|
32
|
+
(@env_vars || {}).merge(vars.map { |k, v| [ k.to_s, v.to_s ] }.to_h)
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_racker_hash
|
36
|
+
# This trims leading whitespace off of heredoc commands, but leaves
|
37
|
+
# them as inlines. They're easier to read when you have to look at the Packer
|
38
|
+
# output when you do this.
|
39
|
+
trim_count = @inline.map { |line| line[/\A */].size }.min
|
40
|
+
lines = @inline.map { |line| line[trim_count..-1] }
|
41
|
+
|
42
|
+
{
|
43
|
+
type: 'shell',
|
44
|
+
binary: @binary,
|
45
|
+
inline: lines,
|
46
|
+
scripts: @scripts,
|
47
|
+
environment_vars: (@env_vars || {}).map { |k, v| "#{k}=#{v}" },
|
48
|
+
execute_command: @execute_command,
|
49
|
+
inline_shebang: @inline_shebang,
|
50
|
+
start_retry_timeout:
|
51
|
+
@start_retry_timeout.nil? ? nil : "#{@start_retry_timeout}s"
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Amigrind
|
2
|
+
module Build
|
3
|
+
class PackerRunner
|
4
|
+
include Virtus.model
|
5
|
+
include Amigrind::Core::Logging::Mixin
|
6
|
+
|
7
|
+
attribute :template, Hash
|
8
|
+
attribute :amigrind_client, Amigrind::Core::Client
|
9
|
+
attribute :blueprint, Amigrind::Blueprints::Blueprint
|
10
|
+
attribute :repo, Amigrind::Repo
|
11
|
+
|
12
|
+
def initialize(template, amigrind_client, blueprint, repo)
|
13
|
+
@template = template
|
14
|
+
@amigrind_client = amigrind_client
|
15
|
+
@blueprint = blueprint
|
16
|
+
@repo = repo
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
credentials = @amigrind_client.credentials
|
21
|
+
|
22
|
+
aws_env_vars =
|
23
|
+
case credentials.class
|
24
|
+
when Aws::Credentials
|
25
|
+
"AWS_ACCESS_KEY_ID='#{access_key}' AWS_SECRET_ACCESS_KEY='#{secret_key}'"
|
26
|
+
{
|
27
|
+
'AWS_ACCESS_KEY_ID' => credentials.access_key_id,
|
28
|
+
'AWS_SECRET_ACCESS_KEY' => credentials.instance_variable_get(:@secret_access_key)
|
29
|
+
}
|
30
|
+
when Aws::SharedCredentials
|
31
|
+
{
|
32
|
+
'AWS_PROFILE' => credentials.profile_name,
|
33
|
+
'AWS_DEFAULT_PROFILE' => credentials.profile_name
|
34
|
+
}
|
35
|
+
else
|
36
|
+
{}
|
37
|
+
end
|
38
|
+
|
39
|
+
thread = nil
|
40
|
+
retval = {
|
41
|
+
region: @blueprint.aws.region,
|
42
|
+
spools: { stdout: [], stderr: [] }
|
43
|
+
}
|
44
|
+
|
45
|
+
Dir.chdir @repo.path do
|
46
|
+
Open3.popen3(aws_env_vars, 'packer build -machine-readable -') do |i, o, e, thr|
|
47
|
+
thread = thr
|
48
|
+
retval[:pid] = thread.pid
|
49
|
+
|
50
|
+
i.write @template
|
51
|
+
i.flush
|
52
|
+
i.close_write
|
53
|
+
|
54
|
+
streams = [ o, e ]
|
55
|
+
|
56
|
+
stream_names = { o.fileno => :stdout, e.fileno => :stderr }
|
57
|
+
|
58
|
+
until streams.find { |f| !f.eof }.nil?
|
59
|
+
ready = IO.select(streams)
|
60
|
+
|
61
|
+
if ready
|
62
|
+
readable_streams = ready[0]
|
63
|
+
|
64
|
+
readable_streams.each do |stream|
|
65
|
+
stream_name = stream_names[stream.fileno]
|
66
|
+
|
67
|
+
begin
|
68
|
+
data = stream.read_nonblock(8192).strip
|
69
|
+
debug_log data
|
70
|
+
|
71
|
+
retval[:spools][stream_name] << data
|
72
|
+
tokens = data.split(',')
|
73
|
+
|
74
|
+
unless tokens.empty?
|
75
|
+
if stream != o || tokens[2] == 'ui'
|
76
|
+
tokens.last.split('\n').each do |log_line|
|
77
|
+
info_log "packer | #{log_line.gsub('%!(PACKER_COMMA)', ',')}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
if tokens[2] == 'artifact' && tokens[4] == 'id'
|
82
|
+
retval[:amis] =
|
83
|
+
tokens[5].split('%!(PACKER_COMMA)').map { |pair| pair.split(':') }.to_h
|
84
|
+
end
|
85
|
+
end
|
86
|
+
rescue EOFError => _
|
87
|
+
debug_log "#{stream_name} eof"
|
88
|
+
streams.delete stream # this is necessary, otherwise
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
retval[:success] = thread.value.success?
|
97
|
+
retval[:exit_code] = thread.value.exitstatus
|
98
|
+
|
99
|
+
raise "ERROR: packer returned successfully, but couldn't parse AMIs?" \
|
100
|
+
if retval[:success] && (retval[:amis].nil? || retval[:amis].empty?)
|
101
|
+
|
102
|
+
retval
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module Amigrind
|
2
|
+
module Build
|
3
|
+
class Rackerizer
|
4
|
+
include Amigrind::Core::Logging::Mixin
|
5
|
+
include Virtus.model(constructor: false, mass_assignment: false)
|
6
|
+
|
7
|
+
attribute :amigrind_client, Amigrind::Core::Client
|
8
|
+
attribute :blueprint, Amigrind::Blueprints::Blueprint
|
9
|
+
attribute :repo, Amigrind::Repo
|
10
|
+
|
11
|
+
def initialize(amigrind_client, blueprint, repo)
|
12
|
+
@amigrind_client = amigrind_client
|
13
|
+
@blueprint = blueprint
|
14
|
+
@repo = repo
|
15
|
+
end
|
16
|
+
|
17
|
+
def rackerize
|
18
|
+
t = Racker::Template.new
|
19
|
+
|
20
|
+
do_builder(t)
|
21
|
+
do_provisioners(t)
|
22
|
+
|
23
|
+
JSON.pretty_generate(t.to_packer)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def do_builder(t)
|
29
|
+
latest_build =
|
30
|
+
@amigrind_client.get_image_by_channel(name: @blueprint.name, channel: :latest)
|
31
|
+
build_id =
|
32
|
+
if latest_build.nil?
|
33
|
+
1
|
34
|
+
else
|
35
|
+
latest_build.tags.find { |t| t.key == Amigrind::Core::AMIGRIND_ID_TAG }.value.to_i + 1
|
36
|
+
end
|
37
|
+
|
38
|
+
source_ami =
|
39
|
+
if @blueprint.source.is_a?(Amigrind::Blueprints::BaseAMISource)
|
40
|
+
ami_id = @blueprint.source.ids[@blueprint.aws.region]
|
41
|
+
|
42
|
+
raise "source AMI was not provided for region #{@blueprint.aws.region}." if ami_id.nil?
|
43
|
+
ami_id
|
44
|
+
elsif @blueprint.source.is_a?(Amigrind::Blueprints::ParentBlueprintSource)
|
45
|
+
parent_name = @blueprint.source.name
|
46
|
+
parent_channel = @blueprint.source.channel
|
47
|
+
|
48
|
+
parent_image =
|
49
|
+
@amigrind_client.get_image_by_channel(name: parent_name,
|
50
|
+
channel: parent_channel)
|
51
|
+
|
52
|
+
raise "parent image (#{parent_name} #{parent_channel}) not found." if parent_image.nil?
|
53
|
+
|
54
|
+
parent_image.id
|
55
|
+
else
|
56
|
+
raise "blueprint source is unrecognized (#{@blueprint.source.class})"
|
57
|
+
end
|
58
|
+
|
59
|
+
raise "source_ami is nil; check to make sure!" if source_ami.nil?
|
60
|
+
|
61
|
+
amigrind_tags = {
|
62
|
+
Amigrind::Core::AMIGRIND_NAME_TAG => @blueprint.name,
|
63
|
+
Amigrind::Core::AMIGRIND_ID_TAG => build_id
|
64
|
+
}.delete_if { |_, v| v.nil? }
|
65
|
+
|
66
|
+
unless @blueprint.build_channel.nil?
|
67
|
+
channel_tag =
|
68
|
+
Amigrind::Core::AMIGRIND_CHANNEL_TAG % { channel_name: @blueprint.build_channel }
|
69
|
+
amigrind_tags[channel_tag] = 1
|
70
|
+
end
|
71
|
+
|
72
|
+
unless parent_image.nil? # we're in a parented image
|
73
|
+
amigrind_tags[Amigrind::Core::AMIGRIND_PARENT_NAME_TAG] = @blueprint.source.name
|
74
|
+
amigrind_tags[Amigrind::Core::AMIGRIND_PARENT_ID_TAG] =
|
75
|
+
parent_image.tags.find { |t| t.key == Amigrind::Core::AMIGRIND_ID_TAG }.value
|
76
|
+
end
|
77
|
+
|
78
|
+
# Note that we do not pull in credentials here! That would fail hilariously and
|
79
|
+
# with much gnashing of teeth if we're on an instance with IAM credentials.
|
80
|
+
# Instead, when we execute Packer on this script, we pass along environment
|
81
|
+
# variables containing AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY or AWS_PROFILE,
|
82
|
+
# like a boss or boss-shaped object.
|
83
|
+
#
|
84
|
+
# This also lets us avoid having sensitive information in the Packer file that
|
85
|
+
# we might, say, print to stdout.
|
86
|
+
builder = {
|
87
|
+
type: 'amazon-ebs',
|
88
|
+
ami_name: "#{@blueprint.name}-#{build_id.to_s.rjust(6, '0')}",
|
89
|
+
source_ami: source_ami,
|
90
|
+
|
91
|
+
instance_type: @blueprint.aws.instance_type,
|
92
|
+
ssh_username: @blueprint.aws.ssh_username,
|
93
|
+
|
94
|
+
region: @blueprint.aws.region,
|
95
|
+
ami_regions: @blueprint.aws.copy_regions,
|
96
|
+
|
97
|
+
ami_description: @blueprint.description,
|
98
|
+
|
99
|
+
launch_block_device_mappings: @blueprint.aws.launch_block_device_mappings,
|
100
|
+
ami_block_device_mappings: @blueprint.aws.ami_block_device_mappings,
|
101
|
+
|
102
|
+
associate_public_ip_address: @blueprint.aws.associate_public_ip_address,
|
103
|
+
ebs_optimized: @blueprint.aws.ebs_optimized,
|
104
|
+
enhanced_networking: @blueprint.aws.enhanced_networking,
|
105
|
+
force_deregister: false, # no, this will not be allowed
|
106
|
+
iam_instance_profile: @blueprint.aws.iam_instance_profile,
|
107
|
+
run_tags: @blueprint.aws.run_tags,
|
108
|
+
run_volume_tags: @blueprint.aws.run_volume_tags,
|
109
|
+
security_group_ids: @blueprint.aws.security_group_ids,
|
110
|
+
ssh_keypair_name: @blueprint.aws.ssh_keypair_name,
|
111
|
+
ssh_private_ip: @blueprint.aws.ssh_private_ip,
|
112
|
+
subnet_id: @blueprint.aws.subnet_ids.sample, # randomly select from allowed subnets
|
113
|
+
user_data: @blueprint.aws.user_data,
|
114
|
+
vpc_id: @blueprint.aws.vpc_id,
|
115
|
+
|
116
|
+
tags: amigrind_tags
|
117
|
+
}
|
118
|
+
builder[:windows_password_timeout] = "#{@blueprint.aws.windows_password_timeout}s" \
|
119
|
+
unless @blueprint.aws.windows_password_timeout.nil?
|
120
|
+
|
121
|
+
t.builders['amigrind'] = builder.delete_if { |_, v| [ nil, [], {} ].include?(v) }.deep_stringify_keys
|
122
|
+
end
|
123
|
+
|
124
|
+
def do_provisioners(t)
|
125
|
+
@blueprint.provisioners.each do |provisioner|
|
126
|
+
t.provisioners[provisioner.weight.to_i] = {}
|
127
|
+
rh = provisioner.to_racker_hash
|
128
|
+
rh.delete_if { |k, v| v.nil? || v == [] }
|
129
|
+
t.provisioners[provisioner.weight.to_i][provisioner.racker_name] = rh
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Amigrind
|
2
|
+
class Builder
|
3
|
+
include Virtus.model
|
4
|
+
include Amigrind::Core::Logging::Mixin
|
5
|
+
|
6
|
+
attribute :blueprint, Amigrind::Blueprints::Blueprint
|
7
|
+
attribute :repo, Amigrind::Repo
|
8
|
+
|
9
|
+
def initialize(aws_credentials, blueprint, repo)
|
10
|
+
@blueprint = blueprint
|
11
|
+
@repo = repo
|
12
|
+
|
13
|
+
@amigrind_client = Amigrind::Core::Client.new(@blueprint.aws.region, aws_credentials)
|
14
|
+
end
|
15
|
+
|
16
|
+
def build
|
17
|
+
lint
|
18
|
+
template = rackerize
|
19
|
+
|
20
|
+
run(template)
|
21
|
+
end
|
22
|
+
|
23
|
+
def lint
|
24
|
+
errors = []
|
25
|
+
|
26
|
+
errors << "No channel set in the blueprint; this will result in " \
|
27
|
+
"an image that can only be retrieved via :latest, which " \
|
28
|
+
"you may not want." if @blueprint.build_channel.nil?
|
29
|
+
|
30
|
+
errors.each { |e| warn_log(e) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def rackerize
|
34
|
+
template = Build::Rackerizer.new(@amigrind_client, @blueprint, @repo).rackerize
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def run(template)
|
40
|
+
runner = Build::PackerRunner.new(template, @amigrind_client, @blueprint, @repo)
|
41
|
+
|
42
|
+
runner.run
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|