linecook 0.6.2 → 1.0.0

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