lxdev 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|