lxdev 0.1.1 → 0.1.2
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/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
|