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