lxdev 0.1.0
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/bin/lxdev +77 -0
- data/lib/lxdev.rb +290 -0
- metadata +75 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b44f874a451b8fcbf87c6015993879e1d587f703
|
4
|
+
data.tar.gz: 31fbead89ddd558dc00126c21d3705ba759a720d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dee6338ab4a878bcd51552da860f80db522ca855160ae774c9d75a85a0f4888947d1ec1af002f8e65b19d94c76d79ded6f60e99789db07a1edaa1ef04279a511
|
7
|
+
data.tar.gz: abe2b8ff716ae61d65dc9db7f43fac06886fa48b8129c96f015616b8217091234a2bb92b2343dada06f4d018d63236975eb99d43da381f0658d6e264a2e9363d
|
data/bin/lxdev
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'lxdev'
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
def option_parser
|
6
|
+
opt_parser = OptionParser.new do |opts|
|
7
|
+
opts.banner = "Usage: lxdev [options] command"
|
8
|
+
|
9
|
+
opts.on("-h", "--help", "Prints this help") do
|
10
|
+
puts opts
|
11
|
+
exit
|
12
|
+
end
|
13
|
+
|
14
|
+
opts.on("-v", "--version", "Prints the version") do
|
15
|
+
puts "Version #{LxDev::VERSION}"
|
16
|
+
exit
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
opt_parser.separator ""
|
21
|
+
opt_parser.separator <<-EOS
|
22
|
+
Commands:
|
23
|
+
up Bring container up
|
24
|
+
status Show status of the container
|
25
|
+
ssh Log into the container
|
26
|
+
halt Stop the container
|
27
|
+
destroy Destroy the container
|
28
|
+
provision Run provisioning commands from YAML file
|
29
|
+
exec Run command as root in container
|
30
|
+
sudoers Create a sudoers file, allowing you to use lxdev without entering the sudo password
|
31
|
+
EOS
|
32
|
+
opt_parser.parse!()
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def execute_main_command(lxdev)
|
38
|
+
if ARGV.empty?
|
39
|
+
puts "No arguments.\nRun \"lxdev --help\" for info"
|
40
|
+
exit 1
|
41
|
+
end
|
42
|
+
|
43
|
+
case ARGV.first
|
44
|
+
when 'up'
|
45
|
+
lxdev.up()
|
46
|
+
when 'status'
|
47
|
+
lxdev.status()
|
48
|
+
when 'ssh'
|
49
|
+
lxdev.ssh(ARGV[1..-1])
|
50
|
+
when 'halt'
|
51
|
+
lxdev.halt()
|
52
|
+
when 'destroy'
|
53
|
+
lxdev.destroy()
|
54
|
+
when 'provision'
|
55
|
+
lxdev.provision()
|
56
|
+
when 'exec'
|
57
|
+
command = ARGV[1..-1].join(" ")
|
58
|
+
if LxDev::SHELLS.include?(command)
|
59
|
+
lxdev.execute(command, interactive: true)
|
60
|
+
else
|
61
|
+
lxdev.execute(command)
|
62
|
+
end
|
63
|
+
when 'sudoers'
|
64
|
+
LxDev.create_sudoers_file()
|
65
|
+
else
|
66
|
+
puts "Unknown command.\nRun \"lxdev --help\" for info"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
|
72
|
+
option_parser
|
73
|
+
lxdev = LxDev.setup()
|
74
|
+
if lxdev
|
75
|
+
execute_main_command(lxdev)
|
76
|
+
lxdev.save_state
|
77
|
+
end
|
data/lib/lxdev.rb
ADDED
@@ -0,0 +1,290 @@
|
|
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.0'
|
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? ? 'bash --noprofile' : "'#{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{sudo 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{sudo 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{printf "#{user} ALL=(ALL) NOPASSWD: ALL\n" | sudo lxc exec #{@name} -- tee -a /etc/sudoers}
|
215
|
+
%x{sudo lxc exec #{@name} -- chmod 0440 /etc/sudoers}
|
216
|
+
end
|
217
|
+
|
218
|
+
def wait_for_boot
|
219
|
+
BOOT_TIMEOUT.times do |t|
|
220
|
+
@status = nil # reset status for each iteration to refresh IP
|
221
|
+
break if get_container_ip
|
222
|
+
abort_boot if t == (BOOT_TIMEOUT-1)
|
223
|
+
sleep 1
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def forward_ports(ports)
|
228
|
+
redir_pids = []
|
229
|
+
ports.each do |guest, host|
|
230
|
+
puts "Forwarding #{get_container_ip}:#{guest} to local port #{host}"
|
231
|
+
pid = spawn %{sudo redir --caddr=#{get_container_ip} --cport=#{guest} --lport=#{host}}
|
232
|
+
redir_pids << pid
|
233
|
+
Process.detach(pid)
|
234
|
+
end
|
235
|
+
@state['redir_pids'] = redir_pids
|
236
|
+
end
|
237
|
+
|
238
|
+
def cleanup_forwarded_ports
|
239
|
+
if @state.empty?
|
240
|
+
return
|
241
|
+
end
|
242
|
+
@state['redir_pids'].each do |pid|
|
243
|
+
%x{sudo kill #{pid}}
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def map_folders(folders)
|
248
|
+
counter = 0
|
249
|
+
folders.each do |host, guest|
|
250
|
+
counter = counter + 1
|
251
|
+
puts "Mounting #{host} in #{guest}"
|
252
|
+
absolute_path = %x{readlink -f #{host}}.chomp
|
253
|
+
%x{sudo lxc config device add #{@name} shared_folder_#{counter} disk source=#{absolute_path} path=#{guest}}
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def abort_boot
|
258
|
+
puts "Timeout waiting for container to boot"
|
259
|
+
exit 1
|
260
|
+
end
|
261
|
+
|
262
|
+
def self.create_sudoers_file
|
263
|
+
user=%x{whoami}.chomp
|
264
|
+
puts <<-EOS
|
265
|
+
!! WARNING !!
|
266
|
+
This will create a file, /etc/sudoers.d/lxdev,
|
267
|
+
which will give your user #{user} access to running
|
268
|
+
the following commands :
|
269
|
+
#{WHITELISTED_SUDO_COMMANDS.join(" ")}
|
270
|
+
with superuser privileges. If you do not know what you're
|
271
|
+
doing, this can be dangerous and insecure.
|
272
|
+
|
273
|
+
If you want to do this, type 'yesplease'
|
274
|
+
EOS
|
275
|
+
action = STDIN.gets.chomp
|
276
|
+
unless action == 'yesplease'
|
277
|
+
puts "Not creating sudoers file"
|
278
|
+
return
|
279
|
+
end
|
280
|
+
content = []
|
281
|
+
content << "# Created by lxdev #{Time.now}"
|
282
|
+
WHITELISTED_SUDO_COMMANDS.each do |cmd|
|
283
|
+
cmd_with_path=%x{which #{cmd}}.chomp
|
284
|
+
content << "#{user} ALL=(root) NOPASSWD: #{cmd_with_path}"
|
285
|
+
end
|
286
|
+
%x{printf '#{content.join("\n")}\n' | sudo tee /etc/sudoers.d/lxdev}
|
287
|
+
%x{sudo chmod 0440 /etc/sudoers.d/lxdev}
|
288
|
+
puts "Created sudoers file."
|
289
|
+
end
|
290
|
+
end
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lxdev
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Christian Lønaas
|
8
|
+
- Eivind Mork
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2018-06-22 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: json
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '2.1'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '2.1'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: terminal-table
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '1.8'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '1.8'
|
42
|
+
description: Lightweight vagrant-like system using LXD
|
43
|
+
email: christian.lonaas@gyldendal.no
|
44
|
+
executables:
|
45
|
+
- lxdev
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- bin/lxdev
|
50
|
+
- lib/lxdev.rb
|
51
|
+
homepage: https://github.com/GyldendalDigital/lxdev
|
52
|
+
licenses:
|
53
|
+
- MIT
|
54
|
+
metadata: {}
|
55
|
+
post_install_message:
|
56
|
+
rdoc_options: []
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
requirements: []
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 2.5.1
|
72
|
+
signing_key:
|
73
|
+
specification_version: 4
|
74
|
+
summary: Automagic development environment with LXD
|
75
|
+
test_files: []
|