linecook-gem 0.0.3 → 0.0.4
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 +4 -4
- data/lib/linecook/builder/build.rb +28 -0
- data/lib/linecook/{darwin_backend.rb → builder/darwin_backend.rb} +4 -1
- data/lib/linecook/{linux_backend.rb → builder/linux_backend.rb} +3 -3
- data/lib/linecook/{lxc.rb → builder/lxc.rb} +25 -43
- data/lib/linecook/builder/manager.rb +56 -0
- data/lib/linecook/cli.rb +77 -12
- data/lib/linecook/image/crypt.rb +49 -0
- data/lib/linecook/image/github.rb +27 -0
- data/lib/linecook/image/manager.rb +43 -0
- data/lib/linecook/image/s3.rb +47 -0
- data/lib/linecook/packager/ebs.rb +270 -0
- data/lib/linecook/packager/manager.rb +23 -0
- data/lib/linecook/{chef.rb → provisioner/chef-zero.rb} +3 -2
- data/lib/linecook/provisioner/manager.rb +34 -0
- data/lib/linecook/provisioner/packer.rb +82 -0
- data/lib/linecook/{config.rb → util/config.rb} +44 -4
- data/lib/linecook/util/downloader.rb +35 -0
- data/lib/linecook/util/executor.rb +33 -0
- data/lib/linecook/{ssh.rb → util/ssh.rb} +59 -2
- data/lib/linecook/version.rb +1 -1
- data/lib/linecook.rb +5 -4
- metadata +76 -11
- data/lib/linecook/bake.rb +0 -21
- data/lib/linecook/build.rb +0 -21
- data/lib/linecook/builder.rb +0 -93
- data/lib/linecook/image.rb +0 -51
@@ -0,0 +1,270 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'timeout'
|
4
|
+
require 'tmpdir'
|
5
|
+
|
6
|
+
require 'linecook/util/ssh'
|
7
|
+
require 'linecook/util/executor'
|
8
|
+
|
9
|
+
require 'aws-sdk'
|
10
|
+
|
11
|
+
module Linecook
|
12
|
+
# Installs a linecook image on a target device
|
13
|
+
module Packager
|
14
|
+
class EBS
|
15
|
+
include Executor
|
16
|
+
|
17
|
+
def initialize(hvm: true, size: 10, region: 'us-east-1')
|
18
|
+
@hvm = hvm
|
19
|
+
@size = size
|
20
|
+
@region = region
|
21
|
+
end
|
22
|
+
|
23
|
+
def package(image)
|
24
|
+
@image = image
|
25
|
+
setup_remote unless instance_id
|
26
|
+
prepare
|
27
|
+
execute("tar -C #{@mountpoint} -cpf - . | sudo tar -C #{@root} -xpf -")
|
28
|
+
finalize
|
29
|
+
snapshot
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def prepare
|
35
|
+
@mountpoint = Dir.mktmpdir
|
36
|
+
execute("mkdir -p #{@mountpoint}")
|
37
|
+
execute("mount #{@image} #{@mountpoint}")
|
38
|
+
create_volume
|
39
|
+
format_and_mount
|
40
|
+
end
|
41
|
+
|
42
|
+
def finalize
|
43
|
+
execute("echo \"UUID=\\\"$(blkid -o value -s UUID #{@rootdev})\\\" / ext4 defaults 1 2\" > /tmp/fstab")
|
44
|
+
execute("mv /tmp/fstab #{@root}/etc/fstab")
|
45
|
+
chroot_exec('apt-get update')
|
46
|
+
chroot_exec('apt-get install -y --force-yes grub-pc grub-legacy-ec2')
|
47
|
+
chroot_exec('update-grub')
|
48
|
+
execute("grub-install --root-directory=#{@root} $(echo #{@rootdev} | sed \"s/[0-9]*//g\")") if @hvm
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
def chroot_exec(command)
|
53
|
+
execute("mount -o bind /dev #{@root}/dev")
|
54
|
+
execute("mount -o bind /sys #{@root}/sys")
|
55
|
+
execute("mount -t proc none #{@root}/proc")
|
56
|
+
execute("cp /etc/resolv.conf #{@root}/etc")
|
57
|
+
execute("chroot #{@root} #{command}")
|
58
|
+
execute("umount #{@root}/dev")
|
59
|
+
execute("umount #{@root}/sys")
|
60
|
+
execute("umount #{@root}/proc")
|
61
|
+
end
|
62
|
+
|
63
|
+
def partition
|
64
|
+
execute("parted -s #{@rootdev} mklabel msdos")
|
65
|
+
execute("parted -s #{@rootdev} mkpart primary ext2 0% 100%")
|
66
|
+
@rootdev = "#{@rootdev}1"
|
67
|
+
end
|
68
|
+
|
69
|
+
def format_and_mount
|
70
|
+
partition if @hvm
|
71
|
+
execute("mkfs.ext4 #{@rootdev}")
|
72
|
+
@root = Dir.mktmpdir
|
73
|
+
execute("mkdir -p #{@root}")
|
74
|
+
execute("mount #{@rootdev} #{@root}")
|
75
|
+
end
|
76
|
+
|
77
|
+
def instance_id
|
78
|
+
@instance_id ||= metadata('instance-id')
|
79
|
+
end
|
80
|
+
|
81
|
+
def availability_zone
|
82
|
+
@availability_zone ||= metadata('placement/availability-zone')
|
83
|
+
end
|
84
|
+
|
85
|
+
def client
|
86
|
+
@client ||= begin
|
87
|
+
credentials = Aws::Credentials.new(Linecook::Config.secrets['aws_access_key'], Linecook::Config.secrets['aws_secret_key'])
|
88
|
+
Aws::EC2::Client.new(region: @region, credentials: credentials)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def create_volume
|
93
|
+
resp = client.create_volume({
|
94
|
+
size: @size,
|
95
|
+
availability_zone: availability_zone, # required
|
96
|
+
volume_type: "standard", # accepts standard, io1, gp2
|
97
|
+
})
|
98
|
+
|
99
|
+
@volume_id = resp.volume_id
|
100
|
+
rootdev = free_device
|
101
|
+
|
102
|
+
puts "Waiting for volume to become available"
|
103
|
+
wait_for_state('available', 120) do
|
104
|
+
client.describe_volumes(volume_ids: [@volume_id]).volumes.first.state
|
105
|
+
end
|
106
|
+
|
107
|
+
resp = client.attach_volume({
|
108
|
+
volume_id: @volume_id,
|
109
|
+
instance_id: instance_id,
|
110
|
+
device: rootdev,
|
111
|
+
})
|
112
|
+
|
113
|
+
puts "Waiting for volume to attach"
|
114
|
+
wait_for_state('attached', 120) do
|
115
|
+
client.describe_volumes(volume_ids: [@volume_id]).volumes.first.attachments.first.state
|
116
|
+
end
|
117
|
+
@rootdev = "/dev/#{rootdev}"
|
118
|
+
end
|
119
|
+
|
120
|
+
def snapshot
|
121
|
+
execute("umount #{@root}")
|
122
|
+
puts 'Creating snapshot'
|
123
|
+
resp = client.detach_volume(volume_id: @volume_id)
|
124
|
+
wait_for_state('available', 120) do
|
125
|
+
client.describe_volumes(volume_ids: [@volume_id]).volumes.first.state
|
126
|
+
end
|
127
|
+
resp = client.create_snapshot(volume_id: @volume_id, description: "Snapshot of #{File.basename(@image)}")
|
128
|
+
tag(resp.snapshot_id, Name: 'Linecook snapshot', image: File.basename(@image), hvm: @hvm.to_s)
|
129
|
+
client.delete_volume(volume_id: @volume_id)
|
130
|
+
end
|
131
|
+
|
132
|
+
def free_device
|
133
|
+
prefix = device_prefix
|
134
|
+
('f'..'zzz').to_a.each do |suffix|
|
135
|
+
device = "#{prefix}#{suffix}"
|
136
|
+
if free_device?(device)
|
137
|
+
lock_device(device)
|
138
|
+
return device
|
139
|
+
end
|
140
|
+
end
|
141
|
+
return nil
|
142
|
+
end
|
143
|
+
|
144
|
+
def free_device?(device)
|
145
|
+
test("[ ! -e /dev/#{device} ]") && test("[ ! -e /run/lock/linecook-#{device} ]")
|
146
|
+
end
|
147
|
+
|
148
|
+
def lock_device(device)
|
149
|
+
execute("echo #{Process.pid} > /run/lock/linecook-#{device}")
|
150
|
+
end
|
151
|
+
|
152
|
+
def unlock_device(device)
|
153
|
+
execute("rm /run/lock/linecook-#{device}")
|
154
|
+
end
|
155
|
+
|
156
|
+
def device_prefix
|
157
|
+
prefixes = ['xvd', 'sd']
|
158
|
+
capture('ls -1 /sys/block').lines.each do |dev|
|
159
|
+
prefixes.each do |prefix|
|
160
|
+
return prefix if dev =~ /^#{prefix}/
|
161
|
+
end
|
162
|
+
end
|
163
|
+
return nil
|
164
|
+
end
|
165
|
+
|
166
|
+
def setup_remote
|
167
|
+
start_node
|
168
|
+
path = "/tmp/#{File.basename(@image)}"
|
169
|
+
@remote.run("wget '#{Linecook::ImageManager.url(File.basename(@image))}' -nv -O #{path}")
|
170
|
+
@image = Linecook::Crypto.new(remote: @remote).decrypt_file(path)
|
171
|
+
end
|
172
|
+
|
173
|
+
def start_node
|
174
|
+
resp = client.run_instances(
|
175
|
+
image_id: find_ami,
|
176
|
+
min_count: 1,
|
177
|
+
max_count: 1,
|
178
|
+
instance_type: 'c4.large',
|
179
|
+
instance_initiated_shutdown_behavior: 'terminate',
|
180
|
+
security_groups: [security_group],
|
181
|
+
key_name: key_pair
|
182
|
+
)
|
183
|
+
@instance_id = resp.instances.first.instance_id
|
184
|
+
@availability_zone = resp.instances.first.placement.availability_zone
|
185
|
+
|
186
|
+
puts 'Waiting for temporary instance to come online'
|
187
|
+
wait_for_state('running', 300) do
|
188
|
+
client.describe_instances(instance_ids: [@instance_id]).reservations.first.instances.first.state.name
|
189
|
+
end
|
190
|
+
tag(@instance_id, Name: 'linecook-temporary-installer-node')
|
191
|
+
@remote = Linecook::SSH.new(instance_ip, username: 'ubuntu', keyfile: Linecook::SSH.private_key)
|
192
|
+
@remote.upload("exec shutdown -h 60 'Delayed shutdown started'", '/tmp/delay-shutdown')
|
193
|
+
execute('mv /tmp/delay-shutdown /etc/init/delay-shutdown.conf') # ubuntism is ok, since the temporary host can always be ubuntu
|
194
|
+
execute('start delay-shutdown')
|
195
|
+
end
|
196
|
+
|
197
|
+
def find_ami
|
198
|
+
url = "http://uec-images.ubuntu.com/query/trusty/server/released.current.txt"
|
199
|
+
type = @hvm ? 'hvm' : 'paravirtual'
|
200
|
+
data = open(url).read.split("\n").map{|l| l.split}.detect do |ary|
|
201
|
+
ary[4] == 'ebs' &&
|
202
|
+
ary[5] == 'amd64' &&
|
203
|
+
ary[6] == @region &&
|
204
|
+
ary.last == type
|
205
|
+
end
|
206
|
+
data[7]
|
207
|
+
end
|
208
|
+
|
209
|
+
def instance_ip
|
210
|
+
client.describe_instances(instance_ids: [@instance_id]).reservations.first.instances.first.public_ip_address
|
211
|
+
end
|
212
|
+
|
213
|
+
def security_group
|
214
|
+
group_name = 'linecook-global-ssh'
|
215
|
+
resp = client.describe_security_groups(filters: [{name: 'group-name', values: [group_name]}])
|
216
|
+
if resp.security_groups.length < 1
|
217
|
+
resp = client.create_security_group({
|
218
|
+
group_name: group_name,
|
219
|
+
description: "Allow global ssh for linecook temporary builder instances",
|
220
|
+
})
|
221
|
+
|
222
|
+
resp = client.authorize_security_group_ingress({
|
223
|
+
group_name: group_name,
|
224
|
+
ip_protocol: "tcp",
|
225
|
+
from_port: 22,
|
226
|
+
to_port: 22,
|
227
|
+
cidr_ip: "0.0.0.0/0",
|
228
|
+
})
|
229
|
+
end
|
230
|
+
group_name
|
231
|
+
end
|
232
|
+
|
233
|
+
def tag(id, **kwargs)
|
234
|
+
resp = client.create_tags(resources: [id], tags: kwargs.map{ |k,v| {key: k, value: v } })
|
235
|
+
end
|
236
|
+
|
237
|
+
def key_pair
|
238
|
+
pubkey = Linecook::SSH.public_key
|
239
|
+
resp = client.describe_key_pairs({
|
240
|
+
filters: [ { name: 'fingerprint', values: [Linecook::SSH.sshv2_fingerprint(pubkey)] } ]
|
241
|
+
})
|
242
|
+
|
243
|
+
if resp.key_pairs.length >= 1
|
244
|
+
return resp.key_pairs.first.key_name
|
245
|
+
else
|
246
|
+
keyname = "linecook-#{SecureRandom.uuid}"
|
247
|
+
resp = client.import_key_pair({
|
248
|
+
key_name: keyname,
|
249
|
+
public_key_material: pubkey,
|
250
|
+
})
|
251
|
+
return keyname
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def metadata(key)
|
256
|
+
(Timeout::timeout(1) { Net::HTTP.get(URI(File.join("http://169.254.169.254/latest/meta-data", key))) } rescue nil)
|
257
|
+
end
|
258
|
+
|
259
|
+
def wait_for_state(desired, timeout)
|
260
|
+
attempts = 0
|
261
|
+
state = nil
|
262
|
+
while attempts < timeout && state != desired
|
263
|
+
state = yield
|
264
|
+
attempts += 1
|
265
|
+
sleep(1)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'linecook/packager/ebs'
|
2
|
+
|
3
|
+
module Linecook
|
4
|
+
module Packager
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def package(image)
|
8
|
+
provider.package(image)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
def provider
|
13
|
+
name = Linecook::Config.load_config[:packager][:provider]
|
14
|
+
config = Linecook::Config.load_config[:packager][name]
|
15
|
+
case name
|
16
|
+
when :ebs
|
17
|
+
Linecook::Packager::EBS.new(**config)
|
18
|
+
else
|
19
|
+
fail "No packager implemented for for #{name}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -8,13 +8,14 @@ module Linecook
|
|
8
8
|
module Chef
|
9
9
|
extend self
|
10
10
|
|
11
|
-
def provision(build)
|
11
|
+
def provision(build, role)
|
12
12
|
chef_config = setup
|
13
|
+
role_config = Linecook::Config.load_config[:roles][role.to_sym]
|
13
14
|
script = ChefProvisioner::Bootstrap.generate(
|
14
15
|
node_name: chef_config[:node_name],
|
15
16
|
chef_version: chef_config[:version] || nil,
|
16
17
|
first_boot: {
|
17
|
-
run_list:
|
18
|
+
run_list: role_config[:run_list]
|
18
19
|
}
|
19
20
|
)
|
20
21
|
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
require 'linecook/builder/build'
|
4
|
+
require 'linecook/provisioner/chef-zero'
|
5
|
+
require 'linecook/provisioner/packer'
|
6
|
+
|
7
|
+
module Linecook
|
8
|
+
module Baker
|
9
|
+
extend self
|
10
|
+
|
11
|
+
def bake(name: nil, image: nil, snapshot: nil, upload: nil, package: nil, build: nil)
|
12
|
+
build_agent = Linecook::Build.new(name, image: image)
|
13
|
+
provider(name).provision(build_agent, name) if build
|
14
|
+
snapshot = build_agent.snapshot(save: true) if snapshot || upload || package
|
15
|
+
Linecook::ImageManager.upload(snapshot) if upload || package
|
16
|
+
Linecook::Packager.package(snapshot) if package
|
17
|
+
build_agent.stop
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def provider(name)
|
23
|
+
provisioner = Linecook::Config.load_config[:roles][name.to_sym][:provisioner] || Linecook::Config.load_config[:provisioner][:default_provider]
|
24
|
+
case provisioner
|
25
|
+
when :chefzero
|
26
|
+
Linecook::Chef
|
27
|
+
when :packer
|
28
|
+
Linecook::Packer
|
29
|
+
else
|
30
|
+
fail "Unsupported provisioner #{provisioner}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'mkmf'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
require 'linecook/util/executor'
|
7
|
+
|
8
|
+
|
9
|
+
module Linecook
|
10
|
+
module Packer
|
11
|
+
def self.provision(build, role)
|
12
|
+
Runner.new(build, role).run
|
13
|
+
end
|
14
|
+
|
15
|
+
class Runner
|
16
|
+
SOURCE_URL = 'https://releases.hashicorp.com/packer/'
|
17
|
+
PACKER_VERSION = '0.8.6'
|
18
|
+
PACKER_PATH = File.join(Linecook::Config::LINECOOK_HOME, 'bin', 'packer')
|
19
|
+
|
20
|
+
include Executor
|
21
|
+
|
22
|
+
def initialize(build, role)
|
23
|
+
role_config = Linecook::Config.load_config[:roles][role.to_sym]
|
24
|
+
@packer = packer_path
|
25
|
+
@build = build
|
26
|
+
@template = role_config[:template_path]
|
27
|
+
end
|
28
|
+
|
29
|
+
def run
|
30
|
+
@build.start
|
31
|
+
packer_template = inject_builder
|
32
|
+
Tempfile.open('packer-linecook') do |template|
|
33
|
+
template.write(JSON.dump(packer_template))
|
34
|
+
template.flush
|
35
|
+
execute("#{@packer} build #{template.path}", sudo: false)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def packer_path
|
42
|
+
found = File.exists?(PACKER_PATH) ? PACKER_PATH : find_executable('packer')
|
43
|
+
path = if found
|
44
|
+
version = execute("#{found} --version", sudo: false, capture: true)
|
45
|
+
Gem::Version.new(version) >= Gem::Version.new(PACKER_VERSION) ? found : nil
|
46
|
+
end
|
47
|
+
|
48
|
+
path ||= get_packer
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_packer
|
52
|
+
puts "packer too old (<#{PACKER_VERSION}) or not present, getting latest packer"
|
53
|
+
arch = 1.size == 8 ? 'amd64' : '386'
|
54
|
+
path = File.join(File.dirname(PACKER_PATH), 'packer.zip')
|
55
|
+
url = File.join(SOURCE_URL, PACKER_VERSION, "packer_#{PACKER_VERSION}_#{Linecook::Config.platform}_#{arch}.zip")
|
56
|
+
Linecook::Downloader.download(url, path)
|
57
|
+
Linecook::Downloader.unzip(path)
|
58
|
+
PACKER_PATH
|
59
|
+
end
|
60
|
+
|
61
|
+
def inject_builder
|
62
|
+
packer_template = JSON.load(File.read(@template)).symbolize_keys
|
63
|
+
packer_template.merge(builders: null_builder)
|
64
|
+
end
|
65
|
+
|
66
|
+
def null_builder
|
67
|
+
[ communicator.merge(type: 'null') ]
|
68
|
+
end
|
69
|
+
|
70
|
+
def communicator
|
71
|
+
{
|
72
|
+
ssh_host: @build.ssh.hostname,
|
73
|
+
ssh_username: @build.ssh.username,
|
74
|
+
ssh_private_key_file: @build.ssh.keyfile,
|
75
|
+
ssh_bastion_host: Linecook::Builder.ssh.hostname,
|
76
|
+
ssh_bastion_username: Linecook::Builder.ssh.username,
|
77
|
+
ssh_bastion_private_key_file: Linecook::Builder.ssh.keyfile,
|
78
|
+
}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'yaml'
|
2
|
+
require 'json'
|
2
3
|
require 'fileutils'
|
3
4
|
|
4
5
|
require 'xhyve'
|
@@ -19,13 +20,52 @@ module Linecook
|
|
19
20
|
username: 'ubuntu',
|
20
21
|
password: 'ubuntu'
|
21
22
|
},
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
provisioner: {
|
24
|
+
default_provider: :chefzero,
|
25
|
+
default_image: :base_image,
|
26
|
+
},
|
27
|
+
image: {
|
28
|
+
provider: {
|
29
|
+
public: :github,
|
30
|
+
private: :s3,
|
31
|
+
},
|
32
|
+
images: {
|
33
|
+
live_iso: {
|
34
|
+
name: 'livesys.iso',
|
35
|
+
profile: :public,
|
36
|
+
},
|
37
|
+
live_image: {
|
38
|
+
name: 'livesys.squashfs',
|
39
|
+
profile: :public,
|
40
|
+
},
|
41
|
+
base_image: {
|
42
|
+
name: 'ubuntu-base.squashfs',
|
43
|
+
profile: :public,
|
44
|
+
}
|
45
|
+
}
|
46
|
+
},
|
47
|
+
packager: {
|
48
|
+
provider: :ebs,
|
49
|
+
ebs: {
|
50
|
+
hvm: true,
|
51
|
+
size: 10,
|
52
|
+
region: 'us-east-1'
|
53
|
+
}
|
54
|
+
},
|
55
|
+
roles: {
|
26
56
|
}
|
27
57
|
}
|
28
58
|
|
59
|
+
def secrets
|
60
|
+
@secrets ||= begin
|
61
|
+
if File.exists?('secrets.ejson')
|
62
|
+
JSON.load(`ejson decrypt secrets.ejson`)
|
63
|
+
else
|
64
|
+
{}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
29
69
|
def setup
|
30
70
|
FileUtils.mkdir_p(LINECOOK_HOME)
|
31
71
|
File.write(DEFAULT_CONFIG_PATH, YAML.dump(DEFAULT_CONFIG)) unless File.exist?(DEFAULT_CONFIG_PATH)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
require 'zip'
|
5
|
+
require 'ruby-progressbar'
|
6
|
+
|
7
|
+
module Linecook
|
8
|
+
module Downloader
|
9
|
+
def self.download(url, path)
|
10
|
+
FileUtils.mkdir_p(File.dirname(path))
|
11
|
+
File.open(path, 'w') do |f|
|
12
|
+
pbar = ProgressBar.create(title: File.basename(path), total: nil)
|
13
|
+
IO.copy_stream(open(url,
|
14
|
+
content_length_proc: lambda do|t|
|
15
|
+
pbar.total = t if t && 0 < t
|
16
|
+
end,
|
17
|
+
progress_proc: lambda do|s|
|
18
|
+
pbar.progress = s
|
19
|
+
end), f)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.unzip(source, dest: nil)
|
24
|
+
puts "Extracting #{source}..."
|
25
|
+
dest ||= File.dirname(source)
|
26
|
+
Zip::File.open(source) do |zip_file|
|
27
|
+
zip_file.each do |f|
|
28
|
+
file_path = File.join(dest, f.name)
|
29
|
+
FileUtils.mkdir_p(File.dirname(file_path))
|
30
|
+
zip_file.extract(f, file_path)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Linecook
|
2
|
+
module Executor
|
3
|
+
def capture(command, sudo: true)
|
4
|
+
execute(command, sudo: sudo, capture: true)
|
5
|
+
end
|
6
|
+
|
7
|
+
def test(check)
|
8
|
+
if @remote
|
9
|
+
return @remote.test(check)
|
10
|
+
else
|
11
|
+
`#{check}`
|
12
|
+
return $?.exitstatus == 0
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute(command, sudo: true, capture: false)
|
17
|
+
command = "sudo #{command}" if sudo
|
18
|
+
if @remote
|
19
|
+
if capture
|
20
|
+
return @remote.capture(command)
|
21
|
+
else
|
22
|
+
@remote.run(command)
|
23
|
+
end
|
24
|
+
else
|
25
|
+
if capture
|
26
|
+
return `#{command}`
|
27
|
+
else
|
28
|
+
system(command)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -1,9 +1,11 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
1
3
|
require 'sshkit'
|
2
4
|
require 'sshkit/dsl'
|
3
5
|
require 'net/ssh'
|
4
6
|
require 'net/ssh/proxy/command'
|
5
7
|
|
6
|
-
require 'linecook/config'
|
8
|
+
require 'linecook/util/config'
|
7
9
|
|
8
10
|
module Linecook
|
9
11
|
class SSHKit::Formatter::Linecook < SSHKit::Formatter::Pretty
|
@@ -48,14 +50,40 @@ module Linecook
|
|
48
50
|
end
|
49
51
|
|
50
52
|
class SSH
|
53
|
+
MAX_RETRIES = 5
|
54
|
+
attr_reader :username, :password, :hostname, :keyfile
|
55
|
+
|
56
|
+
def self.private_key
|
57
|
+
userkey = File.expand_path("~/.ssh/id_rsa")
|
58
|
+
dedicated_key = File.join(Linecook::Config::LINECOOK_HOME, 'linecook_ssh.pem')
|
59
|
+
unless File.exists?(dedicated_key)
|
60
|
+
File.write(dedicated_key, SSHKey.generate.private_key)
|
61
|
+
FileUtils.chmod(0600, dedicated_key)
|
62
|
+
end
|
63
|
+
File.exists?(userkey) ? userkey : dedicated_key
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.public_key(keyfile: nil)
|
67
|
+
SSHKey.new(File.read(keyfile || private_key)).ssh_public_key
|
68
|
+
end
|
69
|
+
|
70
|
+
# Generate a fingerprint for an SSHv2 key used by amazon, yes, this is ugly
|
71
|
+
def self.sshv2_fingerprint(key)
|
72
|
+
_, blob = key.split(/ /)
|
73
|
+
blob = blob.unpack("m*").first
|
74
|
+
reader = Net::SSH::Buffer.new(blob)
|
75
|
+
k=reader.read_key
|
76
|
+
OpenSSL::Digest.new('md5',k.to_der).hexdigest.scan(/../).join(":")
|
77
|
+
end
|
51
78
|
|
52
|
-
attr_reader :username, :hostname
|
53
79
|
def initialize(hostname, username: 'ubuntu', password: nil, keyfile: nil, proxy: nil)
|
54
80
|
@username = username
|
55
81
|
@password = password
|
56
82
|
@hostname = hostname
|
57
83
|
@keyfile = keyfile
|
58
84
|
@proxy = proxy_command(proxy) if proxy
|
85
|
+
wait_for_connection
|
86
|
+
setup_ssh_key if @keyfile
|
59
87
|
end
|
60
88
|
|
61
89
|
def forward(local, remote:nil)
|
@@ -107,8 +135,37 @@ module Linecook
|
|
107
135
|
end
|
108
136
|
end
|
109
137
|
|
138
|
+
def download(path, local: nil)
|
139
|
+
on linecook_host do |_host|
|
140
|
+
download! path, local || File.basename(path)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
110
144
|
private
|
111
145
|
|
146
|
+
def wait_for_connection
|
147
|
+
puts "Waiting for SSH connection"
|
148
|
+
attempts = 0
|
149
|
+
while attempts < MAX_RETRIES
|
150
|
+
begin
|
151
|
+
run("echo connected")
|
152
|
+
return
|
153
|
+
rescue SSHKit::Runner::ExecuteError
|
154
|
+
puts "Retrying SSH connection"
|
155
|
+
sleep(5)
|
156
|
+
attempts += 1
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def setup_ssh_key
|
162
|
+
pubkey = Linecook::SSH.public_key(keyfile: @keyfile)
|
163
|
+
config = Linecook::Config.load_config[:builder]
|
164
|
+
run("mkdir -p /home/#{config[:username]}/.ssh")
|
165
|
+
upload(pubkey, "/home/#{config[:username]}/.ssh/authorized_keys")
|
166
|
+
end
|
167
|
+
|
168
|
+
|
112
169
|
def linecook_host
|
113
170
|
@host ||= begin
|
114
171
|
host = SSHKit::Host.new(user: @username, hostname: @hostname)
|
data/lib/linecook/version.rb
CHANGED
data/lib/linecook.rb
CHANGED
@@ -2,7 +2,8 @@ $LOAD_PATH.unshift(File.expand_path('../lib', __FILE__))
|
|
2
2
|
|
3
3
|
require 'active_support/all'
|
4
4
|
require 'linecook/version'
|
5
|
-
require 'linecook/config'
|
6
|
-
require 'linecook/
|
7
|
-
require 'linecook/
|
8
|
-
require 'linecook/
|
5
|
+
require 'linecook/util/config'
|
6
|
+
require 'linecook/image/manager'
|
7
|
+
require 'linecook/builder/manager'
|
8
|
+
require 'linecook/provisioner/manager'
|
9
|
+
require 'linecook/packager/manager'
|