linecook-gem 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: daf691a1760137d1407850c46df82f52f30ab117
4
+ data.tar.gz: 830e47b2c9d7267c1f362a62ed214b78512e2185
5
+ SHA512:
6
+ metadata.gz: d5cbe11357a3fa8ea4bae785803da3548e2aebfc30d2a5616ea4403ffb6a3d9b5b349cf38b63129ee4698fa9f6fdaa3bf32a5ee4b431ee63dca6c0906c2b14cd
7
+ data.tar.gz: d51d2a394fdd2af2a07e9ba70aef43102f305dfadc9843da66f2bdc2bd5aed650673eb79a833a6b63a55ea8d79cab418e23bbb16a3e6bbd75c34549c9c816dbe
data/bin/linecook ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
3
+ require 'linecook/cli'
4
+ require 'benchmark'
5
+
6
+ begin
7
+ ENV['THOR_DEBUG'] = '1'
8
+ etime = Benchmark.realtime { Linecook::CLI.start(ARGV) }
9
+ $stderr.puts "Completed in #{etime}s"
10
+ rescue Thor::UndefinedCommandError, Thor::UnknownArgumentError, Thor::AmbiguousCommandError, Thor::InvocationError => e
11
+ $stderr.puts(e.message)
12
+ exit(64)
13
+ rescue Thor::Error => e
14
+ $stderr.puts(e.message)
15
+ exit(1)
16
+ end
@@ -0,0 +1,49 @@
1
+ require 'securerandom'
2
+
3
+ require 'chef-provisioner'
4
+ require 'chefdepartie'
5
+
6
+ require 'linecook/build'
7
+
8
+ module Linecook
9
+ module Baker
10
+ extend self
11
+
12
+ def bake
13
+ chef_config = setup
14
+ script = ChefProvisioner::Bootstrap.generate(
15
+ node_name: chef_config[:node_name],
16
+ chef_version: chef_config[:version] || nil,
17
+ first_boot: {
18
+ run_list: chef_config[:run_list]
19
+ }
20
+ )
21
+
22
+ puts "Establishing connection to build..."
23
+ build = Linecook::Build.new('test', 'ubuntu-base.squashfs')
24
+ build.start
25
+ build.ssh.forward(chef_port)
26
+ build.ssh.upload(script, '/tmp/chef_bootstrap')
27
+ build.ssh.run('sudo bash /tmp/chef_bootstrap')
28
+ build.ssh.stop_forwarding
29
+ end
30
+
31
+ private
32
+
33
+ def setup
34
+ ChefProvisioner::Config.setup(client: 'linecook', listen: 'localhost')
35
+ config = Linecook::Config.load_config
36
+
37
+ chef_config = config[:chef]
38
+ chef_config.merge!(node_name: "linecook-#{SecureRandom.uuid}",
39
+ chef_server_url: ChefProvisioner::Config.server)
40
+ # FIXME: sort out cache copying here for concurrent builds of different refs
41
+ Chefdepartie.run(background: true, config: chef_config, cache: '/tmp/linecook-cache')
42
+ chef_config
43
+ end
44
+
45
+ def chef_port
46
+ ChefProvisioner::Config.server.split(':')[-1].to_i
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,21 @@
1
+ require 'forwardable'
2
+ require 'linecook/builder'
3
+
4
+ module Linecook
5
+ class Build
6
+ extend Forwardable
7
+
8
+ def_instance_delegators :@container, :stop, :start, :ip, :info
9
+
10
+ def initialize(name, image)
11
+ Linecook::Builder.start
12
+ @name = name
13
+ @image = image
14
+ @container = Linecook::Lxc::Container.new(name: @name, image: @image, remote: Linecook::Builder.ssh)
15
+ end
16
+
17
+ def ssh
18
+ @ssh ||= Linecook::SSH.new(@container.ip, username: 'ubuntu', password: 'ubuntu', proxy: Linecook::Builder.ssh)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,81 @@
1
+ require 'forwardable'
2
+ require 'sshkey'
3
+
4
+ require 'linecook/lxc'
5
+ require 'linecook/darwin_backend'
6
+ require 'linecook/linux_backend'
7
+ #
8
+ # Linux builder:
9
+ # - just checks for a bridge interface
10
+ # - download live image, if not already
11
+ # - sets up lxc container using local lxc config
12
+ # - copies base image into builder container
13
+
14
+ # OS X builder:
15
+ # - download live ISO
16
+ # - hdiutil
17
+ # - create cache loopback image
18
+ # - dd, based on config file.
19
+ # - start xhyve using gem
20
+ # - keep track of PID and IP
21
+ # - copy base image into xhyve cache
22
+
23
+ # One linecook instance per build, but many linecook instances can share a single builder
24
+ # FIXME: How to deal with concurrent builds on different branches / revisions?
25
+
26
+ module Linecook
27
+ module Builder
28
+ extend self
29
+ extend Forwardable
30
+ BUILD_HOME = '/u/lxc'
31
+
32
+ def_instance_delegators :backend, :stop, :ip, :info, :running?
33
+
34
+ def backend
35
+ @backend ||= backend_for_platform
36
+ end
37
+
38
+ def start
39
+ return if running?
40
+ backend.start
41
+ setup_ssh
42
+ end
43
+
44
+ def ssh
45
+ config = Linecook::Config.load_config[:builder]
46
+ @ssh ||= SSH.new(ip, username: config[:username], password: config[:password])
47
+ end
48
+
49
+ def builds
50
+ ssh.test("[ -d #{BUILD_HOME} ]") ? ssh.capture("find #{BUILD_HOME} -maxdepth 1 -mindepth 1 -type d -printf \"%f\n\"").delete(';').lines : []
51
+ end
52
+
53
+ def build_info
54
+ info = {}
55
+ builds.each do |build|
56
+ info[build] = Linecook::Build.new(build, nil).info
57
+ end
58
+ info
59
+ end
60
+
61
+ private
62
+
63
+ def setup_ssh
64
+ pubkey = SSHKey.new(File.read(File.expand_path("~/.ssh/id_rsa"))).ssh_public_key
65
+ config = Linecook::Config.load_config[:builder]
66
+ ssh.run("mkdir -p /home/#{config[:username]}/.ssh")
67
+ ssh.upload(pubkey, "/home/#{config[:username]}/.ssh/authorized_keys")
68
+ end
69
+
70
+ def backend_for_platform
71
+ case Config.platform
72
+ when 'linux'
73
+ LinuxBuilder.backend
74
+ when 'darwin'
75
+ OSXBuilder.backend
76
+ else
77
+ fail "Cannot find supported backend for #{Config.platform}"
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,55 @@
1
+ require 'thor'
2
+ require 'linecook'
3
+
4
+ class Builder < Thor
5
+ desc 'info', 'Show builder info'
6
+ def info
7
+ puts Linecook::Builder.info
8
+ end
9
+
10
+ desc 'start', 'Start the builder'
11
+ def start
12
+ Linecook::Builder.start
13
+ end
14
+
15
+ desc 'stop', 'Stop the builder'
16
+ def stop
17
+ Linecook::Builder.stop
18
+ end
19
+
20
+ desc 'ip', 'Show the external ip address of the builder'
21
+ def ip
22
+ puts Linecook::Builder.ip
23
+ end
24
+ end
25
+
26
+ class Build < Thor
27
+ desc 'list', 'Show all builds'
28
+ def list
29
+ puts Linecook::Builder.builds
30
+ end
31
+
32
+ desc 'info', 'Show build info' # FIXME: accept the build name
33
+ def info
34
+ puts Linecook::Builder.build_info
35
+ end
36
+ end
37
+
38
+ class Linecook::CLI < Thor
39
+ desc 'builder SUBCOMMAND', 'Manage builders'
40
+ subcommand 'builder', Builder
41
+
42
+ desc 'build SUBCOMMAND', 'Manage builds'
43
+ subcommand 'build', Build
44
+
45
+ desc 'bake', 'Bake a new image'
46
+ def bake
47
+ Linecook::Baker.bake
48
+ end
49
+
50
+ desc 'fetch IMAGE_NAME', 'Fetch an image by name'
51
+ method_options name: :string
52
+ def fetch(name)
53
+ Linecook::ImageFetcher.fetch(name)
54
+ end
55
+ end
@@ -0,0 +1,68 @@
1
+ require 'yaml'
2
+ require 'fileutils'
3
+
4
+ require 'xhyve'
5
+
6
+ module Linecook
7
+ module Config
8
+ extend self
9
+ attr_reader :config
10
+
11
+ CONFIG_PATH = File.join(Dir.pwd, 'linecook.yml').freeze # File.expand_path('../../../config/config.yml', __FILE__)
12
+ LINECOOK_HOME = File.expand_path('~/.linecook').freeze
13
+ DEFAULT_CONFIG_PATH = File.join(LINECOOK_HOME, 'config.yml').freeze
14
+ DEFAULT_CONFIG = {
15
+ builder: {
16
+ image: :live_image,
17
+ name: 'builder',
18
+ home: '/u/lxc',
19
+ username: 'ubuntu',
20
+ password: 'ubuntu'
21
+ },
22
+ images: {
23
+ live_iso: 'livesys.iso',
24
+ live_image: 'livesys.squashfs',
25
+ base_image: 'ubuntu-base.squashfs'
26
+ }
27
+ }
28
+
29
+ def setup
30
+ FileUtils.mkdir_p(LINECOOK_HOME)
31
+ File.write(DEFAULT_CONFIG_PATH, YAML.dump(DEFAULT_CONFIG)) unless File.exist?(DEFAULT_CONFIG_PATH)
32
+ check_perms if platform == 'darwin'
33
+ end
34
+
35
+ def check_perms
36
+ fix_perms if (File.stat(Xhyve::BINARY_PATH).uid != 0 || !File.stat(Xhyve::BINARY_PATH).setuid?)
37
+ end
38
+
39
+ def fix_perms
40
+ puts "Xhyve requires root until https://github.com/mist64/xhyve/issues/60 is resolved\nPlease enter your sudo password to setuid on the xhyve binary"
41
+ system("sudo chown root #{Xhyve::BINARY_PATH}")
42
+ system("sudo chmod +s #{Xhyve::BINARY_PATH}")
43
+ end
44
+
45
+ def load_config
46
+ @config ||= begin
47
+ config = YAML.load(File.read(DEFAULT_CONFIG_PATH)) if File.exist?(DEFAULT_CONFIG_PATH)
48
+ config.merge!(YAML.load(File.read(CONFIG_PATH))) if File.exist?(CONFIG_PATH)
49
+ # fail "Cookbook path not provided or doesn't exist" unless (config[:chef][:cookbook_path] && Dir.exists?(config[:chef][:cookbook_path]))
50
+ # fail "Databag secret not provided or doesn't exist" unless (config[:chef][:encrypted_data_bag_secret] && File.exists?(config[:chef][:encrypted_data_bag_secret]))
51
+ (config || {}).deep_symbolize_keys
52
+ end
53
+ end
54
+
55
+ def platform
56
+ case RbConfig::CONFIG['host_os'].downcase
57
+ when /linux/
58
+ 'linux'
59
+ when /darwin/
60
+ 'darwin'
61
+ else
62
+ fail 'Linux and OS X are the only supported systems'
63
+ end
64
+ end
65
+
66
+ setup
67
+ end
68
+ end
@@ -0,0 +1,96 @@
1
+ require 'securerandom'
2
+ require 'linecook/image'
3
+ # FIXME: read config values from config file
4
+
5
+ module Linecook
6
+ module OSXBuilder
7
+ extend self
8
+ def backend
9
+ XhyveBackend.new
10
+ end
11
+ end
12
+
13
+ class XhyveBackend
14
+ RUN_SPEC_PATH = File.join(Linecook::Config::LINECOOK_HOME, 'xhyve.yml').freeze
15
+ LINUX_CMDLINE = 'boot=live root=/dev/ram0 live-media=initramfs console=ttyS1,115200 console=tty0 net.ifnames=0 biosdevname=0'.freeze
16
+
17
+ attr_reader :ip
18
+ def initialize
19
+ spec = load_run_spec
20
+ @pid = spec[:pid]
21
+ @ip = spec[:ip]
22
+ @uuid = spec[:uuid]
23
+ end
24
+
25
+ def info
26
+ {ip: @ip, pid: @pid, uuid: @uuid}
27
+ end
28
+
29
+ def start
30
+ launch_guest unless running?
31
+ end
32
+
33
+ def stop
34
+ return false unless @pid
35
+ Process.kill('KILL', @pid)
36
+ @ip = nil
37
+ @pid = nil
38
+ save_run_spec
39
+ end
40
+
41
+ def running?
42
+ return false unless @pid
43
+ (true if Process.kill(0, @pid) rescue false)
44
+ end
45
+
46
+ private
47
+
48
+ def launch_guest
49
+ get_iso
50
+ boot_path = File.join(mount_iso, 'BOOT')
51
+ puts "Starting xhyve guest..."
52
+ @uuid ||= SecureRandom.uuid
53
+ guest = Xhyve::Guest.new(
54
+ kernel: File.join(boot_path, 'VMLINUZ'),
55
+ initrd: File.join(boot_path, 'INITRD'),
56
+ cmdline: LINUX_CMDLINE, # boot flags to linux
57
+ #blockdevs: 'loop.img', # path to img files to use as block devs # FIXME
58
+ uuid: @uuid,
59
+ serial: 'com2',
60
+ memory: '4G', # FIXME
61
+ processors: 1, # number of processors #FIXME
62
+ networking: true, # Enable networking? (requires sudo)
63
+ acpi: true, # set up acpi? (required for clean shutdown)
64
+ )
65
+ guest.start
66
+ puts "Started with #{guest.mac}... waiting for network"
67
+ @ip = guest.ip
68
+ @pid = guest.pid
69
+ unless @ip
70
+ @guest.kill
71
+ fail 'Could not acquire ip'
72
+ end
73
+ puts "Network acquired, IP is #{@ip}"
74
+ save_run_spec
75
+ # wait_ssh # FIXME: we need to wait until SSH is actually up or we will sometimes timeout
76
+ end
77
+
78
+ # get and mount the iso
79
+ def get_iso
80
+ image = Linecook::Config.load_config[:images][:live_iso]
81
+ @image_path = Linecook::ImageFetcher.fetch(image)
82
+ end
83
+
84
+ def mount_iso
85
+ `hdiutil mount #{@image_path}`.strip.split(/\s+/).last
86
+ end
87
+
88
+ def save_run_spec
89
+ File.write(RUN_SPEC_PATH, YAML.dump(info))
90
+ end
91
+
92
+ def load_run_spec
93
+ File.exists?(RUN_SPEC_PATH) ? YAML.load(File.read(RUN_SPEC_PATH)) : {}
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,51 @@
1
+ require 'open-uri'
2
+ require 'fileutils'
3
+ require 'digest'
4
+
5
+ require 'octokit'
6
+ require 'ruby-progressbar'
7
+
8
+ module Linecook
9
+ module ImageFetcher
10
+ extend self
11
+
12
+ def fetch(name, upgrade:false)
13
+ dir = File.join(Config::LINECOOK_HOME, 'images')
14
+ path = File.join(dir, name)
15
+ download(image(name)[:browser_download_url], path) unless File.exist?(path) || upgrade
16
+ path
17
+ end
18
+
19
+ private
20
+
21
+ def client
22
+ @client ||= Octokit::Client.new
23
+ end
24
+
25
+ def source
26
+ @source ||= (Config.load_config['source_repo'] || 'dalehamel/lxb')
27
+ end
28
+
29
+ def latest
30
+ client.releases(source).sort_by { |r| r[:published_at] }.last
31
+ end
32
+
33
+ def image(name)
34
+ latest[:assets].find { |a| a[:name] =~ /#{name}/ }
35
+ end
36
+
37
+ def download(url, path)
38
+ FileUtils.mkdir_p(File.dirname(path))
39
+ File.open(path, 'w') do |f|
40
+ pbar = ProgressBar.create(title: File.basename(path), total: nil)
41
+ IO.copy_stream(open(url,
42
+ content_length_proc: lambda do|t|
43
+ pbar.total = t if t && 0 < t
44
+ end,
45
+ progress_proc: lambda do|s|
46
+ pbar.progress = s
47
+ end), f)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,21 @@
1
+ module Linecook
2
+ module LinuxBuilder
3
+ extend self
4
+ LXC_MIN_VERSION = '1.0.7'
5
+
6
+ def backend
7
+ check_lxc_version
8
+ config = Linecook::Config.load_config[:builder]
9
+ images = Linecook::Config.load_config[:images]
10
+ Linecook::Lxc::Container.new(name: config[:name], home: config[:home], image: images[config[:image]])
11
+ end
12
+
13
+ private
14
+
15
+ # FIXME: move to dependency check during initial setup if on linux
16
+ def check_lxc_version
17
+ version = `lxc-info --version`
18
+ fail "lxc too old (<#{LXC_MIN_VERSION}) or not present" unless Gem::Version.new(version) >= Gem::Version.new(LXC_MIN_VERSION)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,261 @@
1
+ require 'linecook/ssh'
2
+ require 'linecook/image'
3
+ require 'linecook/config'
4
+ require 'tempfile'
5
+ require 'ipaddress'
6
+
7
+ module Linecook
8
+ module Lxc
9
+ class Container
10
+ attr_reader :config
11
+ def initialize(name: 'linecook', home: '/u/lxc', image: nil, remote: :local)
12
+ @remote = remote == :local ? false : remote
13
+ config = { utsname: name, rootfs: File.join(home, name, 'rootfs') }
14
+ config.merge!(network: { type: 'veth', flags: 'up', link: 'lxcbr0' }) # FIXME
15
+ @config = Linecook::Lxc::Config.generate(config) # FIXME read link from config
16
+ @source_image = image || Linecook::Config.load_config[:images][:base_image]
17
+ @name = name
18
+ @home = home
19
+ end
20
+
21
+ def start
22
+ setup_image
23
+ setup_dirs
24
+ mount_all
25
+ write_config
26
+ execute("lxc-start #{container_str} -d")
27
+ # wait_running # FIXME - poll until it's running, or we can reach a race condition
28
+ # Don't start a cgmanager if we're already in a container
29
+ execute('[ -f /etc/init/cgmanager.conf ] && sudo status cgmanager | grep -q running && sudo stop cgmanager || true') if lxc?
30
+ setup_bridge unless @remote
31
+ wait_ssh
32
+ unmount unless running?
33
+ end
34
+
35
+ def stop
36
+ setup_dirs
37
+ execute("lxc-stop #{container_str} -k")
38
+ unmount
39
+ end
40
+
41
+ def ip
42
+ attempts = 10
43
+ attempt = 0
44
+ until info[:ip] || attempt >= attempts
45
+ attempt += 1
46
+ sleep(1)
47
+ end
48
+ info[:ip].is_a?(Array) ? info[:ip].find { |ip| bridge_network.include?(IPAddress(ip)) } : info[:ip]
49
+ end
50
+
51
+ def running?
52
+ info[:state] == 'RUNNING'
53
+ end
54
+
55
+ def pid
56
+ info[:pid]
57
+ end
58
+
59
+ def info
60
+ @info = {}
61
+ capture("lxc-info #{container_str}").lines.each do |line|
62
+ k, v = line.strip.split(/:\s+/)
63
+ key = k.downcase.to_sym
64
+ @info[key] = @info[key] ? [@info[key]].flatten << v : v
65
+ end
66
+ @info
67
+ end
68
+
69
+ private
70
+
71
+ def wait_ssh
72
+ running = false
73
+ max_attempts = 60
74
+ attempts = 0
75
+ until running || attempts > max_attempts
76
+ break if capture("lxc-attach -n #{@name} -P #{@home} status ssh") =~ /running/
77
+ attempts += 1
78
+ sleep(1)
79
+ end
80
+ end
81
+
82
+ def lxc?
83
+ namespaces = capture('cat /proc/1/cgroup').lines.map{ |l| l.strip.split(':').last }.uniq
84
+ namespaces.length != 1 || namespaces.first != '/'
85
+ end
86
+
87
+ def setup_dirs
88
+ @lower_dir = tmpdir(label: 'lower')
89
+ @upper_base = tmpdir(label: 'upper')
90
+ @upper_dir = File.join(@upper_base, '/upper')
91
+ @work_dir = File.join(@upper_base, '/work')
92
+ @overlay = File.join(@home, @name, '/rootfs')
93
+ @socket_dirs = []
94
+ (Linecook::Config.load_config[:socket_dirs] ||[]).each{ |sock| @socket_dirs << File.join(@overlay, sock) }
95
+ end
96
+
97
+ def write_config
98
+ path = if @remote
99
+ @remote.upload(@config, '/tmp/lxc-config')
100
+ '/tmp/lxc-config'
101
+ else
102
+ file = Tempfile.new('lxc-config')
103
+ file.write(@config)
104
+ file.close
105
+ file.path
106
+ end
107
+ execute("mv #{path} #{File.join(@home, @name, 'config')}")
108
+ end
109
+
110
+ def mount_all
111
+ # Prepare an overlayfs
112
+ execute("mkdir -p #{@overlay}")
113
+ execute("mkdir -p #{@lower_dir}")
114
+ execute("mkdir -p #{@upper_base}")
115
+ mount(@image_path, @lower_dir, options: '-o loop')
116
+ mount('tmpfs', @upper_base, type: '-t tmpfs', options:'-o noatime') # FIXME: - don't always be tmpfs
117
+ execute("mkdir -p #{@work_dir}")
118
+ execute("mkdir -p #{@upper_dir}")
119
+ mount('overlay', @overlay, type: '-t overlay', options: "-o lowerdir=#{@lower_dir},upperdir=#{@upper_dir},workdir=#{@work_dir}")
120
+ # Overlayfs doesn't support unix domain sockets
121
+ @socket_dirs.each do |sock|
122
+ execute("mkdir -p #{sock}")
123
+ mount('tmpfs', sock, type: '-t tmpfs', options:'-o noatime')
124
+ end
125
+ end
126
+
127
+ def mount(source, dest, type: '', options:'')
128
+ execute("grep -q #{dest} /etc/mtab || sudo mount #{type} #{options} #{source} #{dest}")
129
+ end
130
+
131
+ def unmount
132
+ @socket_dirs.each { |sock| execute("umount #{sock}") }
133
+ execute("umount #{@overlay}")
134
+ execute("umount #{@upper_base}")
135
+ execute("umount #{@lower_dir}")
136
+ execute("rmdir #{@lower_dir}")
137
+ execute("rmdir #{@upper_base}")
138
+ end
139
+
140
+ def bridge_network
141
+ broad_ip = Socket.getifaddrs.find do |a|
142
+ a.name == 'lxcbr0' && a.broadaddr && a.broadaddr.afamily == 2 && !a.broadaddr.inspect_sockaddr.start_with?('169.254')
143
+ end.broadaddr.inspect_sockaddr
144
+ IPAddress("#{broad_ip.to_s}/24")
145
+ end
146
+
147
+ def setup_bridge
148
+ bridge_config = <<-eos
149
+ auto lo
150
+ iface lo inet loopback
151
+
152
+ auto lxcbr0
153
+ iface lxcbr0 inet dhcp
154
+ bridge_ports eth0
155
+ bridge_fd 0
156
+ bridge_maxwait 0
157
+ eos
158
+ interfaces = Tempfile.new('interfaces')
159
+ interfaces.write(bridge_config)
160
+ interfaces.close
161
+ execute("mv #{interfaces.path} #{File.join(@home, @name, 'rootfs', 'etc', 'network', 'interfaces')}")
162
+
163
+ execute("lxc-attach -n #{@name} -P #{@home} ifup lxcbr0")
164
+ end
165
+
166
+
167
+ def setup_image
168
+ @source_path = Linecook::ImageFetcher.fetch(@source_image)
169
+ if @remote
170
+ dest = "#{File.basename(@source_path)}"
171
+ @remote.upload(@source_path, dest) unless @remote.test("[ -f #{dest} ]")
172
+ @image_path = dest
173
+ else
174
+ @image_path = @source_path
175
+ end
176
+ end
177
+
178
+ def tmpdir(label: 'tmp')
179
+ "/tmp/#{@name}-#{label}"
180
+ end
181
+
182
+ def container_str
183
+ "-n #{@name} -P #{@home}"
184
+ end
185
+
186
+ def capture(command, sudo: true)
187
+ execute(command, sudo: sudo, capture: true)
188
+ end
189
+
190
+ def execute(command, sudo: true, capture: false)
191
+ command = "sudo #{command}" if sudo
192
+ if @remote
193
+ if capture
194
+ return @remote.capture(command)
195
+ else
196
+ @remote.run(command)
197
+ end
198
+ else
199
+ if capture
200
+ return `#{command}`
201
+ else
202
+ system(command)
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ module Config
209
+ extend self
210
+ DEFAULT_LXC_CONFIG = {
211
+ include: '/usr/share/lxc/config/ubuntu.common.conf',
212
+ aa_profile: 'lxc-container-default-with-nesting',
213
+ arch: 'x86.64',
214
+ utsname: 'linecook',
215
+ rootfs: '/u/lxc/linecook/rootfs',
216
+ network: {
217
+ type: 'veth',
218
+ flags: 'up',
219
+ link: 'br0'
220
+ },
221
+ mount: {
222
+ auto: 'cgroup'
223
+ },
224
+ cgroup: {
225
+ devices: {
226
+ allow: [
227
+ 'b 7:* rwm',
228
+ 'c 10:237 rwm'
229
+ ]
230
+ }
231
+ }
232
+ }.freeze
233
+
234
+ def generate(**kwargs)
235
+ cfg = []
236
+ flatten(DEFAULT_LXC_CONFIG.merge(kwargs || {})).each do |k, v|
237
+ [v].flatten.each do |val|
238
+ cfg << "lxc.#{k}=#{val}"
239
+ end
240
+ end
241
+ cfg.join("\n")
242
+ end
243
+
244
+ private
245
+
246
+ def flatten(hash)
247
+ flattened = {}
248
+ hash.each do |k, v|
249
+ if v.is_a?(Hash)
250
+ flatten(v).each do |key, val|
251
+ flattened["#{k}.#{key}"] = val
252
+ end
253
+ else
254
+ flattened[k] = v
255
+ end
256
+ end
257
+ flattened
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,128 @@
1
+ require 'sshkit'
2
+ require 'sshkit/dsl'
3
+ require 'net/ssh'
4
+ require 'net/ssh/proxy/command'
5
+
6
+ require 'linecook/config'
7
+
8
+ module Linecook
9
+ class SSHKit::Formatter::Linecook < SSHKit::Formatter::Pretty
10
+ def write_command(command)
11
+ log_command_start(command) unless command.started?
12
+ log_command_stdout(command) unless command.stdout.empty?
13
+ log_command_stderr(command) unless command.stderr.empty?
14
+ log_command_finished(command) if command.finished?
15
+ end
16
+
17
+ def log_command_start(command)
18
+ print(command, 'run'.colorize(:green), command.to_s.colorize(:yellow))
19
+ end
20
+
21
+ def log_command_stdout(command)
22
+ command.stdout.lines.each do |line|
23
+ print(command, 'out'.colorize(:green), line)
24
+ end
25
+ command.stdout = ''
26
+ end
27
+
28
+ def log_command_stderr(command)
29
+ command.stderr.lines.each do |line|
30
+ print(command, 'err'.colorize(:yellow), line)
31
+ end
32
+ command.stderr = ''
33
+ end
34
+
35
+ def log_command_finished(command)
36
+ if command.failure?
37
+ print(command, 'failed'.colorize(:red), "with status #{command.exit_status} #{command.to_s.colorize(:yellow)} in #{sprintf('%5.3f seconds', command.runtime)}")
38
+ else
39
+ print(command, 'done'.colorize(:green), "#{command.to_s.colorize(:yellow)} in #{sprintf('%5.3f seconds', command.runtime)}")
40
+ end
41
+ end
42
+
43
+ def print(command, state, message)
44
+ line = "[#{command.host.to_s.colorize(:blue)}][#{state}] #{message}"
45
+ line << "\n" unless line.end_with?("\n")
46
+ original_output << line
47
+ end
48
+ end
49
+
50
+ class SSH
51
+
52
+ attr_reader :username, :hostname
53
+ def initialize(hostname, username: 'ubuntu', password: nil, proxy: nil)
54
+ @username = username
55
+ @password = password
56
+ @hostname = hostname
57
+ @proxy = proxy_command(proxy) if proxy
58
+ end
59
+
60
+ def forward(local, remote:nil)
61
+ remote ||= local
62
+ opts = { password: @password }
63
+ opts.merge!({ proxy: @proxy }) if @proxy
64
+ @session = Net::SSH.start(@hostname, @username, opts)
65
+ @session.forward.remote(local, '127.0.0.1', remote)
66
+ # Block to ensure it's open
67
+ @session.loop { !@session.forward.active_remotes.include?([remote, '127.0.0.1']) }
68
+ @keep_forwarding = true
69
+ @forward = Thread.new do
70
+ @session.loop(0.1) { @keep_forwarding }
71
+ end
72
+ end
73
+
74
+ def stop_forwarding
75
+ @keep_forwarding = false
76
+ @forward.join
77
+ @session.close unless @session.closed?
78
+ end
79
+
80
+ def test(check)
81
+ result = nil
82
+ on linecook_host do |_host|
83
+ result = test(check)
84
+ end
85
+ result
86
+ end
87
+
88
+ def run(command)
89
+ on linecook_host do |_host|
90
+ execute(command)
91
+ end
92
+ end
93
+
94
+ def capture(command)
95
+ output = nil
96
+ on linecook_host do |_host|
97
+ output = capture(command)
98
+ end
99
+ output
100
+ end
101
+
102
+ def upload(data, path)
103
+ on linecook_host do |_host|
104
+ contents = File.exist?(data) ? data : StringIO.new(data)
105
+ upload! contents, path
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ def linecook_host
112
+ @host ||= begin
113
+
114
+ host = SSHKit::Host.new(user: @username, hostname: @hostname)
115
+ host.password = @password if @password
116
+ host.ssh_options = { proxy: @proxy } if @proxy
117
+ host
118
+ end
119
+ end
120
+
121
+ def proxy_command(proxy)
122
+ ssh_command = "ssh #{proxy.username}@#{proxy.hostname} nc %h %p"
123
+ Net::SSH::Proxy::Command.new(ssh_command)
124
+ end
125
+ end
126
+ end
127
+
128
+ SSHKit.config.output = SSHKit::Formatter::Linecook.new($stdout)
@@ -0,0 +1,3 @@
1
+ module Linecook
2
+ VERSION = '0.0.1'
3
+ end
data/lib/linecook.rb ADDED
@@ -0,0 +1,8 @@
1
+ $LOAD_PATH.unshift(File.expand_path('../lib', __FILE__))
2
+
3
+ require 'active_support/all'
4
+ require 'linecook/version'
5
+ require 'linecook/config'
6
+ require 'linecook/build'
7
+ require 'linecook/bake'
8
+ require 'linecook/image'
data/man/LINECOOK.1 ADDED
@@ -0,0 +1,59 @@
1
+ .TH LINECOOK 1 "December 2015" Unix "User Manuals"
2
+ .SH NAME
3
+ .PP
4
+ linecook \- system image builder
5
+ .SH SYNOPSIS
6
+ .PP
7
+ linecook help [\fB\fCCOMMAND\fR]\- for specific command help
8
+ .SH DESCRIPTION
9
+ .PP
10
+ Linecook
11
+ .SH CONFIGURATION
12
+ .PP
13
+ Describe config file here once it's been determined
14
+ .SH FILES
15
+ .TP
16
+ \fI\&./linecook.yml\fP
17
+ Local config file. Gets deep merged over the system config file. If not explicitly specified, found from current directory if exists.
18
+ .TP
19
+ \fI~/linecook/config.yml\fP
20
+ The system wide configuration file, base or 'common' configuration. Other configurations get deep merged on top of this.
21
+ .SH DEPENDENCIES
22
+ .PP
23
+ Ruby 2.0 or greater, gem, and bundler.
24
+ .PP
25
+ Does not work and will never work on Windows.
26
+ .SS Linux
27
+ .PP
28
+ Only tested on Gentoo and Ubuntu
29
+ .RS
30
+ .IP \(bu 2
31
+ lxc >= 1.0.7
32
+ .IP \(bu 2
33
+ brutils
34
+ .IP \(bu 2
35
+ dnsmasq
36
+ .IP \(bu 2
37
+ iptables with masquerade support
38
+ .IP \(bu 2
39
+ Linux 3.19 or greater with support for cgroups, and netfilter as described by lxc and iptables for NAT.
40
+ .RE
41
+ .SS OS X
42
+ .RS
43
+ .IP \(bu 2
44
+ OS X 10.10 or later (Hypervisor.framework required for Xhyve)
45
+ .RE
46
+ .SH QUIRKS
47
+ .RS
48
+ .IP \(bu 2
49
+ Overlayfs doesn't support unix domain sockets (yet), so anything using a unix domain socket outside of the /run tree should do manually symlink to /run.
50
+ .IP \(bu 2
51
+ Config file will allow you to explicitly mount tmpfs over things that don't do /run if you need to create unix domain sockets
52
+ .RE
53
+ .SH BUGS
54
+ .PP
55
+ Report bugs against github.com/dalehamel/linecook
56
+ .SH AUTHOR
57
+ .PP
58
+ Dale Hamel
59
+ \[la]dale.hamel@srvthe.net\[ra]
metadata ADDED
@@ -0,0 +1,242 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: linecook-gem
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Dale Hamel
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-11-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: xhyve-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.5
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.5
27
+ - !ruby/object:Gem::Dependency
28
+ name: sshkit
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 1.7.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 1.7.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: sshkey
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 1.8.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 1.8.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: octokit
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 4.2.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: 4.2.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: chefdepartie
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: 0.0.7
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 0.0.7
83
+ - !ruby/object:Gem::Dependency
84
+ name: chef-provisioner
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '='
88
+ - !ruby/object:Gem::Version
89
+ version: 0.1.2
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '='
95
+ - !ruby/object:Gem::Version
96
+ version: 0.1.2
97
+ - !ruby/object:Gem::Dependency
98
+ name: activesupport
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '='
102
+ - !ruby/object:Gem::Version
103
+ version: 4.2.5
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '='
109
+ - !ruby/object:Gem::Version
110
+ version: 4.2.5
111
+ - !ruby/object:Gem::Dependency
112
+ name: ruby-progressbar
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '='
116
+ - !ruby/object:Gem::Version
117
+ version: 1.7.5
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '='
123
+ - !ruby/object:Gem::Version
124
+ version: 1.7.5
125
+ - !ruby/object:Gem::Dependency
126
+ name: ipaddress
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '='
130
+ - !ruby/object:Gem::Version
131
+ version: 0.8.0
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '='
137
+ - !ruby/object:Gem::Version
138
+ version: 0.8.0
139
+ - !ruby/object:Gem::Dependency
140
+ name: rake
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - '='
144
+ - !ruby/object:Gem::Version
145
+ version: 10.4.2
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - '='
151
+ - !ruby/object:Gem::Version
152
+ version: 10.4.2
153
+ - !ruby/object:Gem::Dependency
154
+ name: simplecov
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - '='
158
+ - !ruby/object:Gem::Version
159
+ version: 0.10.0
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - '='
165
+ - !ruby/object:Gem::Version
166
+ version: 0.10.0
167
+ - !ruby/object:Gem::Dependency
168
+ name: rspec
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - '='
172
+ - !ruby/object:Gem::Version
173
+ version: 3.2.0
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - '='
179
+ - !ruby/object:Gem::Version
180
+ version: 3.2.0
181
+ - !ruby/object:Gem::Dependency
182
+ name: md2man
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - '='
186
+ - !ruby/object:Gem::Version
187
+ version: 4.0.0
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - '='
193
+ - !ruby/object:Gem::Version
194
+ version: 4.0.0
195
+ description: Build and snapshot a system image for distribution, CI, or both using
196
+ real chef cookbooks and a fake server.
197
+ email: dale.hamel@srvthe.net
198
+ executables:
199
+ - linecook
200
+ extensions: []
201
+ extra_rdoc_files: []
202
+ files:
203
+ - bin/linecook
204
+ - lib/linecook.rb
205
+ - lib/linecook/bake.rb
206
+ - lib/linecook/build.rb
207
+ - lib/linecook/builder.rb
208
+ - lib/linecook/cli.rb
209
+ - lib/linecook/config.rb
210
+ - lib/linecook/darwin_backend.rb
211
+ - lib/linecook/image.rb
212
+ - lib/linecook/linux_backend.rb
213
+ - lib/linecook/lxc.rb
214
+ - lib/linecook/ssh.rb
215
+ - lib/linecook/version.rb
216
+ - man/LINECOOK.1
217
+ homepage: http://rubygems.org/gems/linecook
218
+ licenses:
219
+ - MIT
220
+ metadata: {}
221
+ post_install_message:
222
+ rdoc_options: []
223
+ require_paths:
224
+ - lib
225
+ required_ruby_version: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: '0'
230
+ required_rubygems_version: !ruby/object:Gem::Requirement
231
+ requirements:
232
+ - - ">="
233
+ - !ruby/object:Gem::Version
234
+ version: '0'
235
+ requirements: []
236
+ rubyforge_project:
237
+ rubygems_version: 2.4.6
238
+ signing_key:
239
+ specification_version: 4
240
+ summary: Build system images using chef zero, LXC, and packer
241
+ test_files: []
242
+ has_rdoc: