linecook 0.6.2 → 1.0.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.
Files changed (72) hide show
  1. data/History +139 -0
  2. data/HowTo/Control Virtual Machines +106 -0
  3. data/HowTo/Generate Scripts +263 -0
  4. data/HowTo/Run Scripts +87 -0
  5. data/HowTo/Setup Virtual Machines +76 -0
  6. data/License.txt +1 -1
  7. data/README +78 -59
  8. data/bin/linecook +12 -5
  9. data/bin/linecook_run +45 -0
  10. data/bin/linecook_scp +50 -0
  11. data/lib/linecook.rb +1 -3
  12. data/lib/linecook/attributes.rb +49 -12
  13. data/lib/linecook/commands.rb +9 -4
  14. data/lib/linecook/commands/build.rb +69 -0
  15. data/lib/linecook/commands/command.rb +13 -3
  16. data/lib/linecook/commands/command_error.rb +6 -0
  17. data/lib/linecook/commands/env.rb +74 -8
  18. data/lib/linecook/commands/helper.rb +271 -24
  19. data/lib/linecook/commands/init.rb +10 -6
  20. data/lib/linecook/commands/package.rb +36 -18
  21. data/lib/linecook/commands/run.rb +66 -0
  22. data/lib/linecook/commands/snapshot.rb +114 -0
  23. data/lib/linecook/commands/ssh.rb +39 -0
  24. data/lib/linecook/commands/start.rb +34 -0
  25. data/lib/linecook/commands/state.rb +32 -0
  26. data/lib/linecook/commands/stop.rb +22 -0
  27. data/lib/linecook/commands/vbox_command.rb +130 -0
  28. data/lib/linecook/cookbook.rb +112 -55
  29. data/lib/linecook/package.rb +293 -109
  30. data/lib/linecook/proxy.rb +19 -0
  31. data/lib/linecook/recipe.rb +321 -62
  32. data/lib/linecook/template.rb +7 -101
  33. data/lib/linecook/test.rb +196 -141
  34. data/lib/linecook/test/command_parser.rb +75 -0
  35. data/lib/linecook/test/file_test.rb +153 -35
  36. data/lib/linecook/test/shell_test.rb +176 -0
  37. data/lib/linecook/utils.rb +25 -7
  38. data/lib/linecook/version.rb +4 -4
  39. data/templates/Rakefile +44 -47
  40. data/templates/_gitignore +1 -1
  41. data/templates/attributes/project_name.rb +4 -4
  42. data/templates/config/ssh +15 -0
  43. data/templates/files/help.txt +1 -0
  44. data/templates/helpers/project_name/assert_content_equal.erb +15 -0
  45. data/templates/helpers/project_name/create_dir.erb +9 -0
  46. data/templates/helpers/project_name/create_file.erb +8 -0
  47. data/templates/helpers/project_name/install_file.erb +8 -0
  48. data/templates/packages/abox.yml +4 -0
  49. data/templates/recipes/abox.rb +22 -0
  50. data/templates/recipes/abox_test.rb +14 -0
  51. data/templates/templates/todo.txt.erb +3 -0
  52. data/templates/test/project_name_test.rb +19 -0
  53. data/templates/test/test_helper.rb +14 -0
  54. metadata +43 -41
  55. data/cookbook +0 -0
  56. data/lib/linecook/commands/helpers.rb +0 -28
  57. data/lib/linecook/commands/vbox.rb +0 -85
  58. data/lib/linecook/helper.rb +0 -117
  59. data/lib/linecook/shell.rb +0 -11
  60. data/lib/linecook/shell/posix.rb +0 -145
  61. data/lib/linecook/shell/test.rb +0 -254
  62. data/lib/linecook/shell/unix.rb +0 -117
  63. data/lib/linecook/shell/utils.rb +0 -138
  64. data/templates/README +0 -90
  65. data/templates/files/file.txt +0 -1
  66. data/templates/helpers/project_name/echo.erb +0 -5
  67. data/templates/recipes/project_name.rb +0 -20
  68. data/templates/scripts/project_name.yml +0 -7
  69. data/templates/templates/template.txt.erb +0 -3
  70. data/templates/vbox/setup/virtual_box +0 -86
  71. data/templates/vbox/ssh/id_rsa +0 -27
  72. data/templates/vbox/ssh/id_rsa.pub +0 -1
@@ -1,4 +1,5 @@
1
1
  require 'linecook/commands/command'
2
+ require 'linecook/utils'
2
3
  require 'fileutils'
3
4
  require 'erb'
4
5
  require 'ostruct'
@@ -6,7 +7,7 @@ require 'ostruct'
6
7
  module Linecook
7
8
  module Commands
8
9
 
9
- # ::desc init a linecook scaffold
10
+ # :startdoc::desc create a linecook scaffold
10
11
  #
11
12
  # Initializes a linecook scaffold in the specified directory. This
12
13
  # initializer is currently very basic; it is not a true generator.
@@ -42,7 +43,10 @@ module Linecook
42
43
 
43
44
  def template(project_dir, project_name=nil)
44
45
  project_name ||= File.basename(project_dir)
45
- context = OpenStruct.new(:project_name => project_name).send(:binding)
46
+ context = OpenStruct.new(
47
+ :project_name => project_name,
48
+ :const_name => Utils.camelize(project_name)
49
+ ).send(:binding)
46
50
 
47
51
  #
48
52
  # Copy template files into place
@@ -71,10 +75,10 @@ module Linecook
71
75
  end
72
76
  end
73
77
 
74
- # Link up scripts into vbox
75
- source = File.join(project_dir, 'scripts')
76
- target = File.join(project_dir, 'vbox/scripts')
77
- FileUtils.ln_s source, target
78
+ # Link up project dir into test
79
+ target = File.join(project_dir, 'test', "#{project_name}_test", "test_#{project_name}")
80
+ FileUtils.mkdir_p File.dirname(target)
81
+ FileUtils.ln_s project_dir, target
78
82
  end
79
83
  end
80
84
  end
@@ -1,38 +1,56 @@
1
1
  require 'linecook/commands/command'
2
2
  require 'linecook/cookbook'
3
- require 'linecook/recipe'
3
+ require 'linecook/package'
4
+ require 'fileutils'
4
5
  require 'yaml'
5
6
 
6
7
  module Linecook
7
8
  module Commands
8
9
 
9
- # ::desc generates a package
10
+ # :startdoc::desc generates a package
10
11
  #
11
- # Generates a package.
12
+ # Generates the package specified at
13
+ # 'project_dir/packages/package_name.yml'. The package file should be a
14
+ # YAML files that specifies a package env. The full path to package file
15
+ # can be given instead of package name using the --file option.
12
16
  #
17
+ # If a cookbook file is present in the project_dir then it will be used to
18
+ # resolve resources available to the package. See the env command to
19
+ # interrogate a package env.
13
20
  class Package < Command
14
- config :cookbook_dir, '.', :short => :d # the cookbook directory
15
- config :force, false, :short => :f, &c.flag # force creation
21
+ config :project_dir, '.', :short => :d # the project directory
22
+ config :force, false, :short => :f, &c.flag # force creation
23
+ config :quiet, false, &c.flag # silence output
16
24
 
17
- def process(source, target=nil)
18
- target ||= default_target(source)
25
+ def process(package_file, package_dir=nil)
26
+ package_dir ||= default_package_dir(package_file)
27
+ package_dir = File.expand_path(package_dir)
28
+ package = Linecook::Package.init(package_file, project_dir)
19
29
 
20
- if File.exists?(target)
21
- if force
22
- FileUtils.rm_r(target)
23
- else
24
- raise "already exists: #{target}"
25
- end
30
+ dependencies = package_dependencies(package) + [package_file]
31
+ if force || !FileUtils.uptodate?(package_dir, dependencies)
32
+ package.build
33
+ package.export(package_dir)
34
+ $stdout.puts package_dir unless quiet
26
35
  end
27
36
 
28
- log :create, File.basename(target)
37
+ package_dir
38
+ end
39
+
40
+ def package_dependencies(package)
41
+ dependencies = []
42
+ package.manifest.values.collect do |resources|
43
+ dependencies.concat resources.values
44
+ end
29
45
 
30
- env = Linecook::Cookbook.init(cookbook_dir).env(source)
31
- Linecook::Recipe.build(env).export(target)
46
+ $LOAD_PATH.each do |path|
47
+ dependencies.concat Dir.glob("#{path}/**/*.rb")
48
+ end
49
+ dependencies
32
50
  end
33
51
 
34
- def default_target(source)
35
- source.chomp(File.extname(source))
52
+ def default_package_dir(package_file)
53
+ package_file.chomp(File.extname(package_file))
36
54
  end
37
55
  end
38
56
  end
@@ -0,0 +1,66 @@
1
+ require 'linecook/commands/command'
2
+ require 'linecook/commands/build'
3
+
4
+ module Linecook
5
+ module Commands
6
+
7
+ # :startdoc::desc run packages
8
+ class Run < Command
9
+ RUN_SCRIPT = File.expand_path('../../../../bin/linecook_run', __FILE__)
10
+ SCP_SCRIPT = File.expand_path('../../../../bin/linecook_scp', __FILE__)
11
+
12
+ config :project_dir, '.', :short => :d # the project directory
13
+ config :remote_dir, 'linecook', :short => :D # the remote package dir
14
+ config :ssh_config_file, 'config/ssh', :short => :F # the ssh config file
15
+ config :quiet, false, :short => :q, &c.flag # silence output
16
+ config :transfer, true, &c.switch # transfer package (or not)
17
+ config :remote_scripts, ['run'],
18
+ :short => :S,
19
+ :long => :remote_script, &c.list # the remote script(s)
20
+
21
+ def glob_package_dirs(package_names)
22
+ if package_names.empty?
23
+ pattern = File.expand_path('packages/*', project_dir)
24
+ Dir.glob(pattern).select {|path| File.directory?(path) }
25
+ else
26
+ package_names.collect do |package_name|
27
+ File.expand_path("packages/#{package_name}", project_dir)
28
+ end
29
+ end
30
+ end
31
+
32
+ def process(*package_names)
33
+ package_dirs = glob_package_dirs(package_names)
34
+
35
+ unless remote_dir[0] == ?/
36
+ self.remote_dir = "$(pwd)/#{remote_dir}"
37
+ self.remote_dir.chomp!('/')
38
+ end
39
+
40
+ opts = {
41
+ 'D' => remote_dir,
42
+ 'F' => ssh_config_file
43
+ }
44
+
45
+ if transfer
46
+ sh! "sh #{SCP_SCRIPT} #{format(opts)} #{package_dirs.join(' ')}"
47
+ end
48
+
49
+ remote_scripts.each do |remote_script|
50
+ script_opts = {'S' => remote_script}.merge(opts)
51
+ sh! "sh #{RUN_SCRIPT} #{format(script_opts)} #{package_dirs.join(' ')}"
52
+ end
53
+ end
54
+
55
+ def format(opts)
56
+ options = []
57
+
58
+ opts.each_pair do |key, value|
59
+ options << "-#{key} '#{value}'"
60
+ end
61
+
62
+ options.sort.join(' ')
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,114 @@
1
+ require 'linecook/commands/vbox_command'
2
+
3
+ module Linecook
4
+ module Commands
5
+
6
+ # :startdoc::desc take a vm snapshop
7
+ #
8
+ # Takes the specified snapshot of one or more VirtualBox virtual machines.
9
+ # By default all virtual machines configured in config/ssh will have a
10
+ # snapshot taken. If the snapshot name is already taken, the previous
11
+ # snapshot will be renamed.
12
+ #
13
+ # Snapshot can also reset a hierarchy of renamed snapshots using the
14
+ # --reset flag. For example, if there exists a snapshot 'CURRENT' then
15
+ # these command will leave you with snapshots CURRENT_0 (the original),
16
+ # CURRENT_1, and CURRENT (the latest):
17
+ #
18
+ # linecook snapshot CURRENT
19
+ # linecook snapshot CURRENT
20
+ #
21
+ # To reset:
22
+ #
23
+ # liencook snapshot --reset CURRENT
24
+ #
25
+ # After which there will only be a single 'CURRENT' snapshot, which
26
+ # corresponds to the original snapshot.
27
+ #
28
+ class Snapshot < VboxCommand
29
+ config :reset, false, :long => :reset, &c.flag # reset a snapshot
30
+
31
+ def process(snapshot, *hosts)
32
+ vm_names = resolve_vm_names(hosts)
33
+ each_vm_name(vm_names) do |vm_name|
34
+ if reset
35
+ reset_snapshot(vm_name, snapshot)
36
+ else
37
+ snapshot(vm_name, snapshot)
38
+ end
39
+ end
40
+ end
41
+
42
+ def parse_snapshots(vm_name)
43
+ info = `VBoxManage -q showvminfo #{vm_name}`
44
+ snapshots = {}
45
+
46
+ stack = [{}]
47
+ parent = nil
48
+
49
+ info.each_line do |line|
50
+ next unless line =~ /^(\s+)Name\: (.*?) \(/
51
+ depth = $1.length / 3
52
+ name = $2
53
+
54
+ if depth > stack.length
55
+ stack.push stack.last[parent]
56
+ elsif depth < stack.length
57
+ stack.pop
58
+ end
59
+
60
+ snapshot = {}
61
+ snapshots[name] = snapshot
62
+ stack.last[name] = snapshot
63
+ parent = name
64
+ end
65
+
66
+ snapshots
67
+ end
68
+
69
+ def reset_snapshot(vm_name, snapshot)
70
+ stop(vm_name) if running?(vm_name)
71
+
72
+ snapshot = snapshot.upcase
73
+ restore(vm_name, snapshot)
74
+
75
+ snapshots = parse_snapshots(vm_name)
76
+ parent = snapshots.keys.select {|key| key =~ /\A#{snapshot}(?:_\d+)\z/ }.first
77
+ parent ||= snapshot
78
+
79
+ children = snapshots[parent]
80
+ children.each do |key, value|
81
+ inside_out_each(key, value) do |child|
82
+ sh! "VBoxManage -q snapshot #{vm_name} delete #{child}"
83
+ end
84
+ end
85
+
86
+ unless parent == snapshot
87
+ sh! "VBoxManage -q snapshot #{vm_name} edit #{parent} --name #{snapshot}"
88
+ end
89
+ end
90
+
91
+ def snapshot(vm_name, snapshot)
92
+ snapshot = snapshot.upcase
93
+ snapshots = parse_snapshots(vm_name)
94
+
95
+ count = snapshots.keys.grep(/\A#{snapshot}(?:_|\z)/).length
96
+ if count > 0
97
+ sh! "VBoxManage -q snapshot #{vm_name} edit #{snapshot} --name #{snapshot}_#{count - 1}"
98
+ end
99
+
100
+ sh! "VBoxManage -q snapshot #{vm_name} take #{snapshot}"
101
+ end
102
+
103
+ private
104
+
105
+ def inside_out_each(key, value, &block) # :nodoc:
106
+ value.each_pair do |k, v|
107
+ inside_out_each(k, v, &block)
108
+ end
109
+
110
+ yield(key)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,39 @@
1
+ require 'linecook/commands/vbox_command'
2
+
3
+ module Linecook
4
+ module Commands
5
+
6
+ # :startdoc::desc ssh to a vm
7
+ #
8
+ # Connects to a virtual machine using ssh, as configured in config/ssh.
9
+ #
10
+ class Ssh < VboxCommand
11
+ undef_config :names
12
+
13
+ def default_host
14
+ load_hosts(ssh_config_file).collect {|host, vm_name| host }.first
15
+ end
16
+
17
+ def process(host=default_host)
18
+ if host.to_s.strip.empty?
19
+ raise CommandError.new("no host specified")
20
+ end
21
+
22
+ ssh = "ssh -F '#{ssh_config_file}' '#{host}' --"
23
+
24
+ # Patterned after vagrant/ssh.rb (circa 0.6.6)
25
+ # Some hackery going on here. On Mac OS X Leopard (10.5), exec fails
26
+ # (GH-51). As a workaround, we fork and wait. On all other platforms, we
27
+ # simply exec.
28
+
29
+ platform = RUBY_PLATFORM.to_s.downcase
30
+ pid = nil
31
+ pid = fork if platform.include?("darwin9") || platform.include?("darwin8")
32
+ Kernel.exec(ssh) if pid.nil?
33
+ Process.wait(pid) if pid
34
+
35
+ exit $?.exitstatus
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ require 'linecook/commands/vbox_command'
2
+
3
+ module Linecook
4
+ module Commands
5
+
6
+ # :startdoc::desc start a vm
7
+ #
8
+ # Starts one or more VirtualBox virtual machines, and resets to a snapshot
9
+ # if provided. By default all virtual machines configured in config/ssh
10
+ # will be reset and started in this way.
11
+ class Start < VboxCommand
12
+ config :type, 'headless' # vm type (headless|gui)
13
+ config :snapshot, '', :short => :s # start snapshot
14
+ config :socket, false, &c.flag
15
+
16
+ def process(*hosts)
17
+ vm_names = resolve_vm_names(hosts)
18
+ each_vm_name(vm_names) do |vm_name|
19
+ if running?(vm_name)
20
+ stop(vm_name)
21
+ sleep 0.5
22
+ end
23
+
24
+ unless snapshot.empty?
25
+ restore(vm_name, snapshot)
26
+ end
27
+
28
+ start(vm_name, type)
29
+ start_ssh_socket(vm_name) if socket
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,32 @@
1
+ require 'linecook/commands/vbox_command'
2
+
3
+ module Linecook
4
+ module Commands
5
+
6
+ # :startdoc::desc print the vm state
7
+ #
8
+ # Prints the state of one or more VirtualBox virtual machines. By default
9
+ # all virtual machines configured in config/ssh will print their state.
10
+ #
11
+ class State < VboxCommand
12
+ config :hosts, false, :short => :n, &c.flag # print state by host
13
+
14
+ def state(vm_name)
15
+ running?(vm_name) ? "running" : "stopped"
16
+ end
17
+
18
+ def process(*hosts)
19
+ vm_names = resolve_vm_names(hosts)
20
+ if hosts
21
+ each_host(vm_names) do |host|
22
+ puts "#{host}: #{state(host_map[host])}"
23
+ end
24
+ else
25
+ each_vm_name(vm_names) do |vm_name|
26
+ puts "#{vm_name}: #{state(vm_name)}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,22 @@
1
+ require 'linecook/commands/vbox_command'
2
+
3
+ module Linecook
4
+ module Commands
5
+
6
+ # :startdoc::desc stop a vm
7
+ #
8
+ # Stops one or more VirtualBox virtual machines using 'poweroff'. By
9
+ # default all virtual machines configured in config/ssh will be stopped.
10
+ #
11
+ class Stop < VboxCommand
12
+ def process(*hosts)
13
+ vm_names = resolve_vm_names(hosts)
14
+ each_vm_name(vm_names) do |vm_name|
15
+ if running?(vm_name)
16
+ stop(vm_name)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,130 @@
1
+ require 'linecook/commands/command'
2
+
3
+ module Linecook
4
+ module Commands
5
+ class VboxCommand < Command
6
+ registry.delete_if {|key, value| value == self }
7
+
8
+ config :ssh_config_file, 'config/ssh', :writer => false # the ssh config file
9
+ config :quiet, false, &c.flag # silence output
10
+ config :names, false, &c.flag # use vm names
11
+
12
+ # Matches a host declaration in a ssh config file. After the match:
13
+ #
14
+ # $1:: The host name
15
+ # (ex: 'Host name' => 'name')
16
+ # $2:: The vm name (if present)
17
+ # (ex: 'Host name # [vm_name]' => 'vm_name')
18
+ #
19
+ HOST_REGEXP = /^\s*Host\s+([^\s#]+)(?:\s*#\s*\[(.*?)\])?/
20
+
21
+ # Returns a hash of (host, vm_name) pairs as declared in a ssh config
22
+ # file. Basically this means parsing out the name in each config like:
23
+ #
24
+ # Host name
25
+ #
26
+ # Normally the vm_name is the same as the host name, but an alternate can
27
+ # be specified as a comment in the form:
28
+ #
29
+ # Host name # [vm_name]
30
+ #
31
+ def load_hosts(ssh_config_file)
32
+ hosts = []
33
+
34
+ File.open(ssh_config_file) do |io|
35
+ io.each_line do |line|
36
+ next unless line =~ HOST_REGEXP
37
+ next if $2 && $2.strip.empty?
38
+ hosts << [$1, $2 || $1]
39
+ end
40
+ end
41
+
42
+ hosts
43
+ end
44
+
45
+ def ssh_config_file=(ssh_config_file)
46
+ @ssh_config_file = ssh_config_file
47
+ @host_list = nil
48
+ @host_map = nil
49
+ end
50
+
51
+ def host_list
52
+ @host_list ||= load_hosts(ssh_config_file)
53
+ end
54
+
55
+ def host_map
56
+ @host_map ||= Hash[*host_list.flatten]
57
+ end
58
+
59
+ def resolve_vm_names(hosts)
60
+ names ? hosts : hosts.collect {|host| host_map[host] }
61
+ end
62
+
63
+ def each_host(hosts=[])
64
+ if hosts.empty?
65
+ hosts = host_list.collect {|host, vm_name| host }
66
+ hosts.delete('*')
67
+ end
68
+
69
+ hosts.uniq.each do |host|
70
+ yield(host)
71
+ end
72
+ end
73
+
74
+ def each_vm_name(vm_names=[])
75
+ if vm_names.empty?
76
+ vm_names = host_list.collect {|host, vm_name| vm_name }
77
+ vm_names.delete('*')
78
+ end
79
+
80
+ vm_names.uniq.each do |vm_name|
81
+ yield(vm_name)
82
+ end
83
+ end
84
+
85
+ def ssh(host, cmd)
86
+ sh "ssh -q -F '#{ssh_config_file}' '#{host}' -- #{cmd}"
87
+ end
88
+
89
+ def ssh!(host, cmd)
90
+ unless ssh(host, cmd)
91
+ raise CommandError, "non-zero exit status: #{$?.exitstatus}"
92
+ end
93
+ end
94
+
95
+ def running?(vm_name)
96
+ `VBoxManage -q list runningvms`.include?(vm_name)
97
+ end
98
+
99
+ def start(vm_name, type='headless')
100
+ sh! "VBoxManage -q startvm #{vm_name} --type #{type}"
101
+ end
102
+
103
+ def stop(vm_name)
104
+ sh! "VBoxManage -q controlvm #{vm_name} poweroff"
105
+ end
106
+
107
+ def restore(vm_name, snapshot)
108
+ sh! "VBoxManage -q snapshot #{vm_name} restore #{snapshot.upcase}"
109
+ end
110
+
111
+ def discardstate(vm_name)
112
+ sh! "VBoxManage discardstate #{vm_name}"
113
+ end
114
+
115
+ def share(vm_name, name, local_dir, remote_dir)
116
+ share_dir = "#{local_dir}/#{vm_name}"
117
+ FileUtils.mkdir_p(share_dir) unless File.exists?(share_dir)
118
+
119
+ ssh vm_name, "sudo umount '#{remote_dir}' > /dev/null 2>&1"
120
+ sh "VBoxManage sharedfolder remove '#{vm_name}' --name '#{name}' --transient > /dev/null 2>&1"
121
+ sh! "VBoxManage sharedfolder add '#{vm_name}' --name '#{name}' --hostpath '#{share_dir}' --transient"
122
+ ssh! vm_name, "sudo mount -t vboxsf -o uid=1000,gid=100 '#{name}' '#{remote_dir}'"
123
+ end
124
+
125
+ def start_ssh_socket(vm_name)
126
+ sh "ssh -MNf -F '#{ssh_config_file}' '#{vm_name}' >/dev/null 2>&1 </dev/null"
127
+ end
128
+ end
129
+ end
130
+ end