lxdev 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/lxdev +32 -5
- data/lib/lxdev/main.rb +371 -0
- data/lib/lxdev/system.rb +19 -0
- metadata +5 -4
- data/lib/lxdev.rb +0 -292
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0b31393a23f8385606e4eac0f3bce19b0d9c8d24
|
4
|
+
data.tar.gz: 36d736278c01df9698f937504d759ae0c8605773
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e36ccf0712ada1a0c1ac016405607d60ed1c8674a20425b4513a0ea07f46c46673531d51aeb5b2b5885f469fbb09ab2610795c37cf68b83d79820823aeeb176a
|
7
|
+
data.tar.gz: d364c34c12388a837b5700f8e6eb7f3947ffb714a0d6b1021114b08b33220078ce61d1aabf45565a42a0f41ad4f7caecd0b3c00da14e9891e4f99c1003d840e6
|
data/bin/lxdev
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
require 'lxdev'
|
2
|
+
require 'lxdev/main'
|
3
3
|
require 'optparse'
|
4
4
|
|
5
5
|
def option_parser
|
@@ -12,7 +12,7 @@ def option_parser
|
|
12
12
|
end
|
13
13
|
|
14
14
|
opts.on("-v", "--version", "Prints the version") do
|
15
|
-
puts "Version #{LxDev::VERSION}"
|
15
|
+
puts "Version #{LxDev::Main::VERSION}"
|
16
16
|
exit
|
17
17
|
end
|
18
18
|
|
@@ -27,6 +27,10 @@ halt Stop the container
|
|
27
27
|
destroy Destroy the container
|
28
28
|
provision Run provisioning commands from YAML file
|
29
29
|
exec Run command as root in container
|
30
|
+
snapshot Snapshots the container. Takes a snapshot name as parameter.
|
31
|
+
rmsnapshot Deletes a snapshot. Takes a snapshot name as parameter.
|
32
|
+
restore Restore container to a snapshot with a previous state. Takes a snapshot name as parameter.
|
33
|
+
revert Restore container to the last snapshot taken.
|
30
34
|
sudoers Create a sudoers file, allowing you to use lxdev without entering the sudo password
|
31
35
|
EOS
|
32
36
|
opt_parser.parse!()
|
@@ -55,13 +59,36 @@ def execute_main_command(lxdev)
|
|
55
59
|
lxdev.provision()
|
56
60
|
when 'exec'
|
57
61
|
command = ARGV[1..-1].join(" ")
|
58
|
-
if LxDev::SHELLS.include?(command)
|
62
|
+
if LxDev::Main::SHELLS.include?(command)
|
59
63
|
lxdev.execute(command, interactive: true)
|
60
64
|
else
|
61
65
|
lxdev.execute(command)
|
62
66
|
end
|
63
67
|
when 'sudoers'
|
64
|
-
LxDev.create_sudoers_file()
|
68
|
+
LxDev::Main.create_sudoers_file()
|
69
|
+
when 'snapshot'
|
70
|
+
snapshot_name = ARGV[1]
|
71
|
+
if snapshot_name.nil?
|
72
|
+
puts "Needs a snapshot name!"
|
73
|
+
exit 1
|
74
|
+
end
|
75
|
+
lxdev.snapshot(snapshot_name)
|
76
|
+
when 'restore'
|
77
|
+
snapshot_name = ARGV[1]
|
78
|
+
if snapshot_name.nil?
|
79
|
+
puts "Needs a snapshot name!"
|
80
|
+
exit 1
|
81
|
+
end
|
82
|
+
lxdev.restore(snapshot_name)
|
83
|
+
when 'revert'
|
84
|
+
lxdev.revert()
|
85
|
+
when 'rmsnapshot'
|
86
|
+
snapshot_name = ARGV[1]
|
87
|
+
if snapshot_name.nil?
|
88
|
+
puts "Needs a snapshot name!"
|
89
|
+
exit 1
|
90
|
+
end
|
91
|
+
lxdev.rmsnapshot(snapshot_name)
|
65
92
|
else
|
66
93
|
puts "Unknown command.\nRun \"lxdev --help\" for info"
|
67
94
|
end
|
@@ -70,7 +97,7 @@ end
|
|
70
97
|
|
71
98
|
|
72
99
|
option_parser
|
73
|
-
lxdev = LxDev.setup()
|
100
|
+
lxdev = LxDev::Main.setup()
|
74
101
|
if lxdev
|
75
102
|
execute_main_command(lxdev)
|
76
103
|
lxdev.save_state
|
data/lib/lxdev/main.rb
ADDED
@@ -0,0 +1,371 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'json'
|
3
|
+
require 'terminal-table'
|
4
|
+
require 'lxdev/system'
|
5
|
+
|
6
|
+
module LxDev
|
7
|
+
class Main
|
8
|
+
WHITELISTED_SUDO_COMMANDS = ["lxc", "redir", "kill"]
|
9
|
+
SHELLS = ["bash", "zsh", "sh", "csh", "tcsh", "ash"]
|
10
|
+
BOOT_TIMEOUT = 30
|
11
|
+
VERSION = '0.1.2'
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@uid = System.exec("id -u").output.chomp
|
15
|
+
@gid = System.exec("id -g").output.chomp
|
16
|
+
@config = YAML.load_file('lxdev.yml')
|
17
|
+
@name = @config['box']['name']
|
18
|
+
@image = @config['box']['image']
|
19
|
+
@user = @config['box']['user']
|
20
|
+
@ports = @config['box']['ports'] || {}
|
21
|
+
Dir.mkdir('.lxdev') unless File.directory?('.lxdev')
|
22
|
+
begin
|
23
|
+
@state = YAML.load_file('.lxdev/state')
|
24
|
+
rescue
|
25
|
+
@state = Hash.new
|
26
|
+
end
|
27
|
+
rescue Errno::ENOENT
|
28
|
+
puts "lxdev.yml not found"
|
29
|
+
exit 1
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.setup
|
33
|
+
unless lxd_initialized?
|
34
|
+
puts "Please run 'lxd init' and configure LXD first"
|
35
|
+
return false
|
36
|
+
end
|
37
|
+
lxdev = Main.new
|
38
|
+
unless lxdev.set_ssh_keys
|
39
|
+
puts "No ssh keys detected. Make sure you have an ssh key, a running agent, and the key added to the agent, e.g. with ssh-add."
|
40
|
+
return false
|
41
|
+
end
|
42
|
+
return lxdev
|
43
|
+
end
|
44
|
+
|
45
|
+
def save_state
|
46
|
+
File.open('.lxdev/state', 'w') {|f| f.write @state.to_yaml} unless @state.empty?
|
47
|
+
end
|
48
|
+
|
49
|
+
def set_ssh_keys
|
50
|
+
ssh_keys = System.exec("ssh-add -L").output
|
51
|
+
if ssh_keys[0..3] == 'ssh-'
|
52
|
+
@ssh_keys = ssh_keys
|
53
|
+
else
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def status
|
59
|
+
ensure_container_created
|
60
|
+
|
61
|
+
container_status = get_container_status
|
62
|
+
folders = container_status.first['devices'].map {|name, folders| [name, "#{folders['source']} => #{folders['path']}"] if folders['source']}.compact
|
63
|
+
table = Terminal::Table.new do |t|
|
64
|
+
t.add_row ['Name', container_status.first['name']]
|
65
|
+
t.add_row ['Status', container_status.first['status']]
|
66
|
+
t.add_row ['IP', get_container_ip]
|
67
|
+
t.add_row ['Image', @image]
|
68
|
+
t.add_separator
|
69
|
+
folders.each do |folder|
|
70
|
+
t.add_row folder
|
71
|
+
end
|
72
|
+
t.add_separator
|
73
|
+
@ports.each do |guest, host|
|
74
|
+
t.add_row ['Forwarded port', "guest: #{guest} host: #{host}"]
|
75
|
+
end
|
76
|
+
if container_status.first['snapshots'].any?
|
77
|
+
t.add_separator
|
78
|
+
t.add_row ['Snapshots', '']
|
79
|
+
end
|
80
|
+
container_status.first['snapshots'].each do |snapshot|
|
81
|
+
t.add_row [snapshot['name'].partition('/').last, snapshot['created_at']]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
puts table
|
85
|
+
end
|
86
|
+
|
87
|
+
def up
|
88
|
+
do_provision = false
|
89
|
+
unless @state.empty?
|
90
|
+
puts "Container state .lxdev/state exists, is it running? If not it might have stopped unexpectedly. Please remove the file before starting."
|
91
|
+
exit 1
|
92
|
+
end
|
93
|
+
if get_container_status.empty?
|
94
|
+
create_container
|
95
|
+
do_provision = true
|
96
|
+
else
|
97
|
+
if get_container_status.first['status'] == 'Running'
|
98
|
+
puts "#{@name} is already running!"
|
99
|
+
exit 1
|
100
|
+
else
|
101
|
+
start_container
|
102
|
+
end
|
103
|
+
end
|
104
|
+
puts "Waiting for boot..."
|
105
|
+
wait_for_boot
|
106
|
+
@state['status'] = 'running'
|
107
|
+
puts "Forwarding ports..."
|
108
|
+
forward_ports(@ports)
|
109
|
+
provision if do_provision
|
110
|
+
end
|
111
|
+
|
112
|
+
def halt
|
113
|
+
ensure_container_created
|
114
|
+
System.exec("sudo lxc stop #{@name}")
|
115
|
+
cleanup_forwarded_ports
|
116
|
+
remove_state
|
117
|
+
end
|
118
|
+
|
119
|
+
def destroy
|
120
|
+
ensure_container_created
|
121
|
+
System.exec("sudo lxc delete #{@name}")
|
122
|
+
end
|
123
|
+
|
124
|
+
def ssh(args)
|
125
|
+
ensure_container_created
|
126
|
+
host = get_container_ip
|
127
|
+
if host.nil?
|
128
|
+
puts "#{@name} doesn't seem to be running."
|
129
|
+
exit 1
|
130
|
+
end
|
131
|
+
ssh_command = "ssh -o StrictHostKeyChecking=no -t #{@user}@#{get_container_ip} #{args.empty? ? '' : "'#{args.join(' ')}'"}"
|
132
|
+
exec ssh_command
|
133
|
+
end
|
134
|
+
|
135
|
+
def execute(command, interactive: false)
|
136
|
+
if interactive
|
137
|
+
exec("sudo lxc exec #{@name} #{command}") # execution stops here and gives control to exec
|
138
|
+
end
|
139
|
+
IO.popen("sudo lxc exec #{@name} -- /bin/sh -c '#{command}'", err: [:child, :out]) do |cmd_output|
|
140
|
+
cmd_output.each do |line|
|
141
|
+
puts line
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def provision
|
147
|
+
ensure_container_created
|
148
|
+
if get_container_status.first['status'] != 'Running'
|
149
|
+
puts "#{@name} is not running!"
|
150
|
+
exit 1
|
151
|
+
end
|
152
|
+
provisioning = @config['box']['provisioning']
|
153
|
+
if provisioning.nil?
|
154
|
+
puts "Nothing to do"
|
155
|
+
return
|
156
|
+
end
|
157
|
+
if @config['box']['auto_snapshots']
|
158
|
+
snapshot_name = "provision_#{Time.now.to_i}"
|
159
|
+
snapshot(snapshot_name)
|
160
|
+
end
|
161
|
+
puts "Provisioning #{@name}..."
|
162
|
+
STDOUT.sync = true
|
163
|
+
provisioning.each do |cmd|
|
164
|
+
execute cmd
|
165
|
+
end
|
166
|
+
STDOUT.sync = false
|
167
|
+
end
|
168
|
+
|
169
|
+
def snapshot(snapshot_name)
|
170
|
+
puts "Creating snapshot #{snapshot_name}"
|
171
|
+
System.exec("sudo lxc snapshot #{@name} #{snapshot_name}")
|
172
|
+
end
|
173
|
+
|
174
|
+
def restore(snapshot_name)
|
175
|
+
puts "Restoring snapshot #{snapshot_name}"
|
176
|
+
exitstatus = System.exec("sudo lxc restore #{@name} #{snapshot_name}").exitstatus
|
177
|
+
exitstatus == 0
|
178
|
+
end
|
179
|
+
|
180
|
+
def rmsnapshot(snapshot_name)
|
181
|
+
puts "Deleting snapshot #{snapshot_name}"
|
182
|
+
exitstatus = System.exec("sudo lxc delete #{@name}/#{snapshot_name}").exitstatus
|
183
|
+
exitstatus == 0
|
184
|
+
end
|
185
|
+
|
186
|
+
def revert
|
187
|
+
snapshot = get_container_status.first['snapshots'].last
|
188
|
+
snapshot_name = snapshot['name'].partition('/').last
|
189
|
+
if restore(snapshot_name)
|
190
|
+
puts "Reverted to snapshot #{snapshot_name}"
|
191
|
+
puts "Deleting snapshot"
|
192
|
+
rmsnapshot(snapshot_name)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
private
|
197
|
+
|
198
|
+
def self.lxd_initialized?
|
199
|
+
exitstatus = System.exec("sudo lxc info | grep 'lxd init'").exitstatus
|
200
|
+
exitstatus != 0
|
201
|
+
end
|
202
|
+
|
203
|
+
def ensure_container_created
|
204
|
+
container_status = get_container_status
|
205
|
+
unless container_status.size > 0
|
206
|
+
puts "Container not created yet. Run lxdev up"
|
207
|
+
exit(0)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def remove_state
|
212
|
+
File.delete('.lxdev/state') if File.exists?('.lxdev/state')
|
213
|
+
@state = {}
|
214
|
+
end
|
215
|
+
|
216
|
+
def create_container
|
217
|
+
add_subuid_and_subgid
|
218
|
+
puts "Launching #{@name}..."
|
219
|
+
System.exec("sudo lxc init #{@image} #{@name}")
|
220
|
+
System.exec(%{printf "uid #{@uid} 1001\ngid #{@gid} 1001"| sudo lxc config set #{@name} raw.idmap -})
|
221
|
+
System.exec("sudo lxc start #{@name}")
|
222
|
+
puts "Creating user #{@user}..."
|
223
|
+
create_container_user(@user)
|
224
|
+
puts "Mapping folders.."
|
225
|
+
map_folders(@config['box']['folders'])
|
226
|
+
end
|
227
|
+
|
228
|
+
def start_container
|
229
|
+
puts "Starting #{@name}..."
|
230
|
+
System.exec("sudo lxc start #{@name}")
|
231
|
+
end
|
232
|
+
|
233
|
+
def get_container_status
|
234
|
+
return @status unless @status.nil?
|
235
|
+
command_result = System.exec("sudo lxc list #{@name} --format=json")
|
236
|
+
@status = JSON.parse(command_result.output)
|
237
|
+
end
|
238
|
+
|
239
|
+
def get_container_ip
|
240
|
+
get_container_status.first['state']['network']['eth0']['addresses'].select {|addr| addr['family'] == 'inet'}.first['address']
|
241
|
+
rescue
|
242
|
+
nil
|
243
|
+
end
|
244
|
+
|
245
|
+
def add_subuid_and_subgid
|
246
|
+
need_restart = false
|
247
|
+
if System.exec("grep -q 'root:#{@uid}:1' /etc/subuid").exitstatus != 0
|
248
|
+
System.exec("echo 'root:#{@uid}:1' | sudo tee -a /etc/subuid")
|
249
|
+
need_restart = true
|
250
|
+
end
|
251
|
+
if System.exec("grep -q 'root:#{@gid}:1' /etc/subgid").exitstatus != 0
|
252
|
+
System.exec("echo 'root:#{@gid}:1' | sudo tee -a /etc/subgid")
|
253
|
+
need_restart = true
|
254
|
+
end
|
255
|
+
if need_restart
|
256
|
+
begin
|
257
|
+
System.exec("sudo systemctl restart #{lxd_service_name}")
|
258
|
+
rescue
|
259
|
+
puts "The LXD service needs to be restarted, but the service name cannot be detected. Please restart it manually."
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def create_container_user(user)
|
265
|
+
System.exec("sudo lxc exec #{@name} -- groupadd --gid 1001 #{user}")
|
266
|
+
System.exec("sudo lxc exec #{@name} -- useradd --uid 1001 --gid 1001 -s /bin/bash -m #{user}")
|
267
|
+
System.exec("sudo lxc exec #{@name} -- mkdir /home/#{user}/.ssh")
|
268
|
+
System.exec("sudo lxc exec #{@name} -- chmod 0700 /home/#{user}/.ssh")
|
269
|
+
System.exec("printf '#{@ssh_keys}' | sudo lxc exec #{@name} tee /home/#{user}/.ssh/authorized_keys")
|
270
|
+
System.exec("sudo lxc exec #{@name} -- chown -R #{user} /home/#{user}/.ssh")
|
271
|
+
System.exec("sudo lxc exec #{@name} -- touch /home/#{@user}/.hushlogin")
|
272
|
+
System.exec("sudo lxc exec #{@name} -- chown #{user} /home/#{user}/.hushlogin")
|
273
|
+
System.exec(%{printf "#{user} ALL=(ALL) NOPASSWD: ALL\n" | sudo lxc exec #{@name} -- tee -a /etc/sudoers})
|
274
|
+
System.exec("sudo lxc exec #{@name} -- chmod 0440 /etc/sudoers")
|
275
|
+
end
|
276
|
+
|
277
|
+
def wait_for_boot
|
278
|
+
BOOT_TIMEOUT.times do |t|
|
279
|
+
@status = nil # reset status for each iteration to refresh IP
|
280
|
+
break if get_container_ip
|
281
|
+
abort_boot if t == (BOOT_TIMEOUT - 1)
|
282
|
+
sleep 1
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def forward_ports(ports)
|
287
|
+
redir_pids = []
|
288
|
+
ports.each do |guest, host|
|
289
|
+
puts "Forwarding #{get_container_ip}:#{guest} to local port #{host}"
|
290
|
+
pid = System.spawn_exec("sudo redir --caddr=#{get_container_ip} --cport=#{guest} --lport=#{host}")
|
291
|
+
redir_pids << pid
|
292
|
+
Process.detach(pid)
|
293
|
+
end
|
294
|
+
@state['redir_pids'] = redir_pids
|
295
|
+
end
|
296
|
+
|
297
|
+
def cleanup_forwarded_ports
|
298
|
+
if @state.empty?
|
299
|
+
return
|
300
|
+
end
|
301
|
+
@state['redir_pids'].each do |pid|
|
302
|
+
System.exec("sudo kill #{pid}")
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def map_folders(folders)
|
307
|
+
counter = 0
|
308
|
+
folders.each do |host, guest|
|
309
|
+
counter = counter + 1
|
310
|
+
puts "Mounting #{host} in #{guest}"
|
311
|
+
absolute_path = System.exec("readlink -f #{host}").output.chomp
|
312
|
+
System.exec("sudo lxc config device add #{@name} shared_folder_#{counter} disk source=#{absolute_path} path=#{guest}")
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
def get_snapshots
|
317
|
+
snapshots = []
|
318
|
+
get_container_status.first['snapshots'].each do |snapshot|
|
319
|
+
result = {}
|
320
|
+
result['name'] = snapshot['name']
|
321
|
+
result['date'] = snapshot['created_at']
|
322
|
+
snapshots << result
|
323
|
+
end
|
324
|
+
snapshots
|
325
|
+
end
|
326
|
+
|
327
|
+
def abort_boot
|
328
|
+
puts "Timeout waiting for container to boot"
|
329
|
+
exit 1
|
330
|
+
end
|
331
|
+
|
332
|
+
def lxd_service_name
|
333
|
+
if System.exec("sudo systemctl status lxd.service").exitstatus == 0
|
334
|
+
'lxd.service'
|
335
|
+
elsif System.exec("sudo systemctl status snap.lxd.daemon.service").exitstatus == 0
|
336
|
+
'snap.lxd.daemon.service'
|
337
|
+
else
|
338
|
+
raise 'There seems to be no LXD service on the system!'
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
def self.create_sudoers_file
|
343
|
+
user = System.exec("whoami").output.chomp
|
344
|
+
puts <<-EOS
|
345
|
+
!! WARNING !!
|
346
|
+
This will create a file, /etc/sudoers.d/lxdev,
|
347
|
+
which will give your user #{user} access to running
|
348
|
+
the following commands :
|
349
|
+
#{WHITELISTED_SUDO_COMMANDS.join(" ")}
|
350
|
+
with superuser privileges. If you do not know what you're
|
351
|
+
doing, this can be dangerous and insecure.
|
352
|
+
|
353
|
+
If you want to do this, type 'yesplease'
|
354
|
+
EOS
|
355
|
+
action = STDIN.gets.chomp
|
356
|
+
unless action == 'yesplease'
|
357
|
+
puts "Not creating sudoers file"
|
358
|
+
return
|
359
|
+
end
|
360
|
+
content = []
|
361
|
+
content << "# Created by lxdev #{Time.now}"
|
362
|
+
WHITELISTED_SUDO_COMMANDS.each do |cmd|
|
363
|
+
cmd_with_path = System.exec("which #{cmd}").output.chomp
|
364
|
+
content << "#{user} ALL=(root) NOPASSWD: #{cmd_with_path}"
|
365
|
+
end
|
366
|
+
System.exec(%{printf '#{content.join("\n")}\n' | sudo tee /etc/sudoers.d/lxdev})
|
367
|
+
System.exec("sudo chmod 0440 /etc/sudoers.d/lxdev")
|
368
|
+
puts "Created sudoers file."
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
data/lib/lxdev/system.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module LxDev
|
2
|
+
class System
|
3
|
+
class Result
|
4
|
+
attr_accessor :output
|
5
|
+
attr_accessor :exitstatus
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.exec(cmd)
|
9
|
+
return_object = Result.new
|
10
|
+
return_object.output = %x{#{cmd}}
|
11
|
+
return_object.exitstatus = $?.exitstatus
|
12
|
+
return_object
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.spawn_exec(cmd)
|
16
|
+
spawn(cmd)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lxdev
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christian Lønaas
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2018-06
|
12
|
+
date: 2018-08-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: json
|
@@ -47,7 +47,8 @@ extensions: []
|
|
47
47
|
extra_rdoc_files: []
|
48
48
|
files:
|
49
49
|
- bin/lxdev
|
50
|
-
- lib/lxdev.rb
|
50
|
+
- lib/lxdev/main.rb
|
51
|
+
- lib/lxdev/system.rb
|
51
52
|
homepage: https://github.com/GyldendalDigital/lxdev
|
52
53
|
licenses:
|
53
54
|
- MIT
|
@@ -68,7 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
69
|
version: '0'
|
69
70
|
requirements: []
|
70
71
|
rubyforge_project:
|
71
|
-
rubygems_version: 2.5.1
|
72
|
+
rubygems_version: 2.5.2.1
|
72
73
|
signing_key:
|
73
74
|
specification_version: 4
|
74
75
|
summary: Automagic development environment with LXD
|
data/lib/lxdev.rb
DELETED
@@ -1,292 +0,0 @@
|
|
1
|
-
require 'yaml'
|
2
|
-
require 'json'
|
3
|
-
require 'terminal-table'
|
4
|
-
|
5
|
-
class LxDev
|
6
|
-
WHITELISTED_SUDO_COMMANDS = ["lxc", "redir", "kill"]
|
7
|
-
SHELLS = ["bash", "zsh", "sh", "csh", "tcsh", "ash"]
|
8
|
-
BOOT_TIMEOUT = 30
|
9
|
-
VERSION = '0.1.1'
|
10
|
-
|
11
|
-
def initialize
|
12
|
-
@uid=%x{id -u}.chomp
|
13
|
-
@gid=%x{id -g}.chomp
|
14
|
-
@config = YAML.load_file('lxdev.yml')
|
15
|
-
@name = @config['box']['name']
|
16
|
-
@image = @config['box']['image']
|
17
|
-
@user = @config['box']['user']
|
18
|
-
@ports = @config['box']['ports'] || {}
|
19
|
-
Dir.mkdir('.lxdev') unless File.directory?('.lxdev')
|
20
|
-
begin
|
21
|
-
@state = YAML.load_file('.lxdev/state')
|
22
|
-
rescue
|
23
|
-
@state = Hash.new
|
24
|
-
end
|
25
|
-
rescue Errno::ENOENT
|
26
|
-
puts "lxdev.yml not found"
|
27
|
-
exit 1
|
28
|
-
end
|
29
|
-
|
30
|
-
def self.setup
|
31
|
-
unless lxd_initialized?
|
32
|
-
puts "Please run 'lxd init' and configure LXD first"
|
33
|
-
return false
|
34
|
-
end
|
35
|
-
return LxDev.new
|
36
|
-
end
|
37
|
-
|
38
|
-
def save_state
|
39
|
-
File.open('.lxdev/state', 'w') {|f| f.write @state.to_yaml } unless @state.empty?
|
40
|
-
end
|
41
|
-
|
42
|
-
def status
|
43
|
-
ensure_container_created
|
44
|
-
|
45
|
-
container_status = get_container_status
|
46
|
-
folders = container_status.first['devices'].map{|name, folders| [name,"#{folders['source']} => #{folders['path']}"] if folders['source']}.compact
|
47
|
-
table = Terminal::Table.new do |t|
|
48
|
-
t.add_row ['Name', container_status.first['name']]
|
49
|
-
t.add_row ['Status', container_status.first['status']]
|
50
|
-
t.add_row ['IP', get_container_ip]
|
51
|
-
t.add_row ['Image', @image]
|
52
|
-
t.add_separator
|
53
|
-
folders.each do |folder|
|
54
|
-
t.add_row folder
|
55
|
-
end
|
56
|
-
t.add_separator
|
57
|
-
@ports.each do |guest,host|
|
58
|
-
t.add_row ['Forwarded port', "guest: #{guest} host: #{host}"]
|
59
|
-
end
|
60
|
-
end
|
61
|
-
puts table
|
62
|
-
end
|
63
|
-
|
64
|
-
def up
|
65
|
-
do_provision = false
|
66
|
-
unless @state.empty?
|
67
|
-
puts "Container state .lxdev/state exists, is it running? If not it might have stopped unexpectedly. Please remove the file before starting."
|
68
|
-
exit 1
|
69
|
-
end
|
70
|
-
if get_container_status.empty?
|
71
|
-
create_container
|
72
|
-
do_provision = true
|
73
|
-
else
|
74
|
-
if get_container_status.first['status'] == 'Running'
|
75
|
-
puts "#{@name} is already running!"
|
76
|
-
exit 1
|
77
|
-
else
|
78
|
-
start_container
|
79
|
-
end
|
80
|
-
end
|
81
|
-
puts "Waiting for boot..."
|
82
|
-
wait_for_boot
|
83
|
-
@state['status'] = 'running'
|
84
|
-
puts "Forwarding ports..."
|
85
|
-
forward_ports(@ports)
|
86
|
-
provision if do_provision
|
87
|
-
end
|
88
|
-
|
89
|
-
def halt
|
90
|
-
ensure_container_created
|
91
|
-
%x{sudo lxc stop #{@name}}
|
92
|
-
cleanup_forwarded_ports
|
93
|
-
remove_state
|
94
|
-
end
|
95
|
-
|
96
|
-
def destroy
|
97
|
-
ensure_container_created
|
98
|
-
%x{sudo lxc delete #{@name}}
|
99
|
-
end
|
100
|
-
|
101
|
-
def ssh(args)
|
102
|
-
ensure_container_created
|
103
|
-
host = get_container_ip
|
104
|
-
if host.nil?
|
105
|
-
puts "#{@name} doesn't seem to be running."
|
106
|
-
exit 1
|
107
|
-
end
|
108
|
-
ssh_command = "ssh -o StrictHostKeyChecking=no -t #{@user}@#{get_container_ip} #{args.empty? ? '' : "'#{args.join(' ')}'"}"
|
109
|
-
exec ssh_command
|
110
|
-
end
|
111
|
-
|
112
|
-
def execute(command, interactive: false)
|
113
|
-
if interactive
|
114
|
-
exec("sudo lxc exec #{@name} #{command}") # execution stops here and gives control to exec
|
115
|
-
end
|
116
|
-
IO.popen("sudo lxc exec #{@name} -- /bin/sh -c '#{command}'", err: [:child, :out]) do |cmd_output|
|
117
|
-
cmd_output.each do |line|
|
118
|
-
puts line
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
def provision
|
124
|
-
ensure_container_created
|
125
|
-
if get_container_status.first['status'] != 'Running'
|
126
|
-
puts "#{@name} is not running!"
|
127
|
-
exit 1
|
128
|
-
end
|
129
|
-
provisioning = @config['box']['provisioning']
|
130
|
-
if provisioning.nil?
|
131
|
-
puts "Nothing to do"
|
132
|
-
return
|
133
|
-
end
|
134
|
-
puts "Provisioning #{@name}..."
|
135
|
-
STDOUT.sync = true
|
136
|
-
provisioning.each do |cmd|
|
137
|
-
execute cmd
|
138
|
-
end
|
139
|
-
STDOUT.sync = false
|
140
|
-
end
|
141
|
-
|
142
|
-
private
|
143
|
-
def self.lxd_initialized?
|
144
|
-
%x{sudo lxc info | grep 'lxd init'}
|
145
|
-
$?.exitstatus != 0
|
146
|
-
end
|
147
|
-
|
148
|
-
def ensure_container_created
|
149
|
-
container_status = get_container_status
|
150
|
-
unless container_status.size > 0
|
151
|
-
puts "Container not created yet. Run lxdev up"
|
152
|
-
exit(0)
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
def remove_state
|
157
|
-
File.delete('.lxdev/state') if File.exists?('.lxdev/state')
|
158
|
-
@state = {}
|
159
|
-
end
|
160
|
-
|
161
|
-
def create_container
|
162
|
-
add_subuid_and_subgid
|
163
|
-
puts "Launching #{@name}..."
|
164
|
-
%x{sudo lxc init #{@image} #{@name}}
|
165
|
-
%x{printf "uid #{@uid} 1001\ngid #{@gid} 1001"| sudo lxc config set #{@name} raw.idmap -}
|
166
|
-
%x{sudo lxc start #{@name}}
|
167
|
-
puts "Creating user #{@user}..."
|
168
|
-
create_container_user(@user)
|
169
|
-
puts "Mapping folders.."
|
170
|
-
map_folders(@config['box']['folders'])
|
171
|
-
end
|
172
|
-
|
173
|
-
def start_container
|
174
|
-
puts "Starting #{@name}..."
|
175
|
-
%x{sudo lxc start #{@name}}
|
176
|
-
end
|
177
|
-
|
178
|
-
def get_container_status
|
179
|
-
return @status unless @status.nil?
|
180
|
-
container_status = %x{sudo lxc list #{@name} --format=json}
|
181
|
-
@status = JSON.parse(container_status)
|
182
|
-
end
|
183
|
-
|
184
|
-
def get_container_ip
|
185
|
-
get_container_status.first['state']['network']['eth0']['addresses'].select{|addr| addr['family'] == 'inet'}.first['address']
|
186
|
-
rescue
|
187
|
-
nil
|
188
|
-
end
|
189
|
-
|
190
|
-
def add_subuid_and_subgid
|
191
|
-
need_restart = false
|
192
|
-
%x{grep -q 'root:#{@uid}:1' /etc/subuid}
|
193
|
-
if $?.exitstatus != 0
|
194
|
-
%x{echo 'root:#{@uid}:1' | sudo tee -a /etc/subuid}
|
195
|
-
need_restart = true
|
196
|
-
end
|
197
|
-
%x{grep -q 'root:#{@gid}:1' /etc/subgid}
|
198
|
-
if $?.exitstatus != 0
|
199
|
-
%x{echo 'root:#{@gid}:1' | sudo tee -a /etc/subgid}
|
200
|
-
need_restart = true
|
201
|
-
end
|
202
|
-
if need_restart
|
203
|
-
%x{sudo systemctl restart lxd.service}
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
def create_container_user(user)
|
208
|
-
%x{sudo lxc exec #{@name} -- groupadd --gid 1001 #{user}}
|
209
|
-
%x{sudo lxc exec #{@name} -- useradd --uid 1001 --gid 1001 -s /bin/bash -m #{user}}
|
210
|
-
%x{sudo lxc exec #{@name} -- mkdir /home/#{user}/.ssh}
|
211
|
-
%x{sudo lxc exec #{@name} -- chmod 0700 /home/#{user}/.ssh}
|
212
|
-
%x{ssh-add -L | sudo lxc exec #{@name} tee /home/#{user}/.ssh/authorized_keys}
|
213
|
-
%x{sudo lxc exec #{@name} -- chown -R #{user} /home/#{user}/.ssh}
|
214
|
-
%x{sudo lxc exec #{@name} -- touch /home/#{@user}/.hushlogin}
|
215
|
-
%x{sudo lxc exec #{@name} -- chown #{user} /home/#{user}/.hushlogin}
|
216
|
-
%x{printf "#{user} ALL=(ALL) NOPASSWD: ALL\n" | sudo lxc exec #{@name} -- tee -a /etc/sudoers}
|
217
|
-
%x{sudo lxc exec #{@name} -- chmod 0440 /etc/sudoers}
|
218
|
-
end
|
219
|
-
|
220
|
-
def wait_for_boot
|
221
|
-
BOOT_TIMEOUT.times do |t|
|
222
|
-
@status = nil # reset status for each iteration to refresh IP
|
223
|
-
break if get_container_ip
|
224
|
-
abort_boot if t == (BOOT_TIMEOUT-1)
|
225
|
-
sleep 1
|
226
|
-
end
|
227
|
-
end
|
228
|
-
|
229
|
-
def forward_ports(ports)
|
230
|
-
redir_pids = []
|
231
|
-
ports.each do |guest, host|
|
232
|
-
puts "Forwarding #{get_container_ip}:#{guest} to local port #{host}"
|
233
|
-
pid = spawn %{sudo redir --caddr=#{get_container_ip} --cport=#{guest} --lport=#{host}}
|
234
|
-
redir_pids << pid
|
235
|
-
Process.detach(pid)
|
236
|
-
end
|
237
|
-
@state['redir_pids'] = redir_pids
|
238
|
-
end
|
239
|
-
|
240
|
-
def cleanup_forwarded_ports
|
241
|
-
if @state.empty?
|
242
|
-
return
|
243
|
-
end
|
244
|
-
@state['redir_pids'].each do |pid|
|
245
|
-
%x{sudo kill #{pid}}
|
246
|
-
end
|
247
|
-
end
|
248
|
-
|
249
|
-
def map_folders(folders)
|
250
|
-
counter = 0
|
251
|
-
folders.each do |host, guest|
|
252
|
-
counter = counter + 1
|
253
|
-
puts "Mounting #{host} in #{guest}"
|
254
|
-
absolute_path = %x{readlink -f #{host}}.chomp
|
255
|
-
%x{sudo lxc config device add #{@name} shared_folder_#{counter} disk source=#{absolute_path} path=#{guest}}
|
256
|
-
end
|
257
|
-
end
|
258
|
-
|
259
|
-
def abort_boot
|
260
|
-
puts "Timeout waiting for container to boot"
|
261
|
-
exit 1
|
262
|
-
end
|
263
|
-
|
264
|
-
def self.create_sudoers_file
|
265
|
-
user=%x{whoami}.chomp
|
266
|
-
puts <<-EOS
|
267
|
-
!! WARNING !!
|
268
|
-
This will create a file, /etc/sudoers.d/lxdev,
|
269
|
-
which will give your user #{user} access to running
|
270
|
-
the following commands :
|
271
|
-
#{WHITELISTED_SUDO_COMMANDS.join(" ")}
|
272
|
-
with superuser privileges. If you do not know what you're
|
273
|
-
doing, this can be dangerous and insecure.
|
274
|
-
|
275
|
-
If you want to do this, type 'yesplease'
|
276
|
-
EOS
|
277
|
-
action = STDIN.gets.chomp
|
278
|
-
unless action == 'yesplease'
|
279
|
-
puts "Not creating sudoers file"
|
280
|
-
return
|
281
|
-
end
|
282
|
-
content = []
|
283
|
-
content << "# Created by lxdev #{Time.now}"
|
284
|
-
WHITELISTED_SUDO_COMMANDS.each do |cmd|
|
285
|
-
cmd_with_path=%x{which #{cmd}}.chomp
|
286
|
-
content << "#{user} ALL=(root) NOPASSWD: #{cmd_with_path}"
|
287
|
-
end
|
288
|
-
%x{printf '#{content.join("\n")}\n' | sudo tee /etc/sudoers.d/lxdev}
|
289
|
-
%x{sudo chmod 0440 /etc/sudoers.d/lxdev}
|
290
|
-
puts "Created sudoers file."
|
291
|
-
end
|
292
|
-
end
|