tenderloin 0.2.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.
- data/LICENCE +21 -0
- data/README.md +50 -0
- data/Version +1 -0
- data/bin/loin +5 -0
- data/config/default.rb +26 -0
- data/lib/tenderloin/actions/base.rb +93 -0
- data/lib/tenderloin/actions/box/add.rb +22 -0
- data/lib/tenderloin/actions/box/destroy.rb +14 -0
- data/lib/tenderloin/actions/box/download.rb +63 -0
- data/lib/tenderloin/actions/box/unpackage.rb +46 -0
- data/lib/tenderloin/actions/runner.rb +138 -0
- data/lib/tenderloin/actions/vm/boot.rb +52 -0
- data/lib/tenderloin/actions/vm/destroy.rb +18 -0
- data/lib/tenderloin/actions/vm/halt.rb +14 -0
- data/lib/tenderloin/actions/vm/import.rb +32 -0
- data/lib/tenderloin/actions/vm/move_hard_drive.rb +53 -0
- data/lib/tenderloin/actions/vm/provision.rb +71 -0
- data/lib/tenderloin/actions/vm/reload.rb +17 -0
- data/lib/tenderloin/actions/vm/shared_folders.rb +47 -0
- data/lib/tenderloin/actions/vm/start.rb +17 -0
- data/lib/tenderloin/actions/vm/up.rb +55 -0
- data/lib/tenderloin/box.rb +143 -0
- data/lib/tenderloin/busy.rb +73 -0
- data/lib/tenderloin/cli.rb +59 -0
- data/lib/tenderloin/commands.rb +154 -0
- data/lib/tenderloin/config.rb +144 -0
- data/lib/tenderloin/downloaders/base.rb +13 -0
- data/lib/tenderloin/downloaders/file.rb +21 -0
- data/lib/tenderloin/downloaders/http.rb +47 -0
- data/lib/tenderloin/env.rb +156 -0
- data/lib/tenderloin/fusion_vm.rb +85 -0
- data/lib/tenderloin/ssh.rb +49 -0
- data/lib/tenderloin/util.rb +51 -0
- data/lib/tenderloin/vm.rb +63 -0
- data/lib/tenderloin/vmx_file.rb +28 -0
- data/lib/tenderloin.rb +14 -0
- data/script/tenderloin-ssh-expect.sh +23 -0
- data/templates/Tenderfile +8 -0
- data/test/tenderloin/actions/base_test.rb +32 -0
- data/test/tenderloin/actions/box/add_test.rb +37 -0
- data/test/tenderloin/actions/box/destroy_test.rb +18 -0
- data/test/tenderloin/actions/box/download_test.rb +118 -0
- data/test/tenderloin/actions/box/unpackage_test.rb +100 -0
- data/test/tenderloin/actions/runner_test.rb +262 -0
- data/test/tenderloin/actions/vm/boot_test.rb +55 -0
- data/test/tenderloin/actions/vm/destroy_test.rb +24 -0
- data/test/tenderloin/actions/vm/down_test.rb +32 -0
- data/test/tenderloin/actions/vm/export_test.rb +88 -0
- data/test/tenderloin/actions/vm/forward_ports_test.rb +50 -0
- data/test/tenderloin/actions/vm/halt_test.rb +27 -0
- data/test/tenderloin/actions/vm/import_test.rb +36 -0
- data/test/tenderloin/actions/vm/move_hard_drive_test.rb +108 -0
- data/test/tenderloin/actions/vm/package_test.rb +181 -0
- data/test/tenderloin/actions/vm/provision_test.rb +103 -0
- data/test/tenderloin/actions/vm/reload_test.rb +44 -0
- data/test/tenderloin/actions/vm/resume_test.rb +27 -0
- data/test/tenderloin/actions/vm/shared_folders_test.rb +117 -0
- data/test/tenderloin/actions/vm/start_test.rb +28 -0
- data/test/tenderloin/actions/vm/suspend_test.rb +27 -0
- data/test/tenderloin/actions/vm/up_test.rb +98 -0
- data/test/tenderloin/box_test.rb +139 -0
- data/test/tenderloin/busy_test.rb +83 -0
- data/test/tenderloin/commands_test.rb +269 -0
- data/test/tenderloin/config_test.rb +123 -0
- data/test/tenderloin/downloaders/base_test.rb +20 -0
- data/test/tenderloin/downloaders/file_test.rb +32 -0
- data/test/tenderloin/downloaders/http_test.rb +40 -0
- data/test/tenderloin/env_test.rb +345 -0
- data/test/tenderloin/ssh_test.rb +103 -0
- data/test/tenderloin/util_test.rb +64 -0
- data/test/tenderloin/vm_test.rb +89 -0
- data/test/test_helper.rb +92 -0
- metadata +241 -0
@@ -0,0 +1,144 @@
|
|
1
|
+
module Tenderloin
|
2
|
+
def self.config
|
3
|
+
Config.config
|
4
|
+
end
|
5
|
+
|
6
|
+
class Config
|
7
|
+
@config = nil
|
8
|
+
@config_runners = []
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def reset!
|
12
|
+
@config = nil
|
13
|
+
config_runners.clear
|
14
|
+
end
|
15
|
+
|
16
|
+
def config
|
17
|
+
@config ||= Config::Top.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def config_runners
|
21
|
+
@config_runners ||= []
|
22
|
+
end
|
23
|
+
|
24
|
+
def run(&block)
|
25
|
+
config_runners << block
|
26
|
+
end
|
27
|
+
|
28
|
+
def execute!
|
29
|
+
config_runners.each do |block|
|
30
|
+
block.call(config)
|
31
|
+
end
|
32
|
+
|
33
|
+
config.loaded!
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Config
|
39
|
+
class Base
|
40
|
+
def [](key)
|
41
|
+
send(key)
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_json
|
45
|
+
instance_variables_hash.to_json
|
46
|
+
end
|
47
|
+
|
48
|
+
def instance_variables_hash
|
49
|
+
instance_variables.inject({}) do |acc, iv|
|
50
|
+
acc[iv.to_s[1..-1].to_sym] = instance_variable_get(iv)
|
51
|
+
acc
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class SSHConfig < Base
|
57
|
+
attr_accessor :username
|
58
|
+
attr_accessor :password
|
59
|
+
attr_accessor :host
|
60
|
+
attr_accessor :max_tries
|
61
|
+
attr_accessor :timeout
|
62
|
+
end
|
63
|
+
|
64
|
+
class VMConfig < Base
|
65
|
+
attr_accessor :box
|
66
|
+
attr_accessor :box_vmx
|
67
|
+
attr_accessor :project_directory
|
68
|
+
attr_accessor :hd_location
|
69
|
+
|
70
|
+
|
71
|
+
def initialize
|
72
|
+
end
|
73
|
+
|
74
|
+
def hd_location=(val)
|
75
|
+
raise Exception.new "disk_storage must be set to a directory" unless File.directory?(val)
|
76
|
+
@hd_location=val
|
77
|
+
end
|
78
|
+
|
79
|
+
def base
|
80
|
+
File.expand_path(@base)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class PackageConfig < Base
|
85
|
+
attr_accessor :name
|
86
|
+
attr_accessor :extension
|
87
|
+
end
|
88
|
+
|
89
|
+
class ChefConfig < Base
|
90
|
+
attr_accessor :cookbooks_path
|
91
|
+
attr_accessor :provisioning_path
|
92
|
+
attr_accessor :json
|
93
|
+
attr_accessor :enabled
|
94
|
+
|
95
|
+
def initialize
|
96
|
+
@enabled = false
|
97
|
+
end
|
98
|
+
|
99
|
+
def to_json
|
100
|
+
# Overridden so that the 'json' key could be removed, since its just
|
101
|
+
# merged into the config anyways
|
102
|
+
data = instance_variables_hash
|
103
|
+
data.delete(:json)
|
104
|
+
data.to_json
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class TenderloinConfig < Base
|
109
|
+
attr_accessor :dotfile_name
|
110
|
+
attr_accessor :log_output
|
111
|
+
attr_accessor :home
|
112
|
+
|
113
|
+
def home
|
114
|
+
File.expand_path(@home)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class Top < Base
|
119
|
+
attr_reader :package
|
120
|
+
attr_reader :ssh
|
121
|
+
attr_reader :vm
|
122
|
+
attr_reader :chef
|
123
|
+
attr_reader :tenderloin
|
124
|
+
|
125
|
+
def initialize
|
126
|
+
@ssh = SSHConfig.new
|
127
|
+
@vm = VMConfig.new
|
128
|
+
@chef = ChefConfig.new
|
129
|
+
@tenderloin = TenderloinConfig.new
|
130
|
+
@package = PackageConfig.new
|
131
|
+
|
132
|
+
@loaded = false
|
133
|
+
end
|
134
|
+
|
135
|
+
def loaded?
|
136
|
+
@loaded
|
137
|
+
end
|
138
|
+
|
139
|
+
def loaded!
|
140
|
+
@loaded = true
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Tenderloin
|
2
|
+
module Downloaders
|
3
|
+
# Represents a base class for a downloader. A downloader handles
|
4
|
+
# downloading a box file to a temporary file.
|
5
|
+
class Base
|
6
|
+
include Tenderloin::Util
|
7
|
+
|
8
|
+
# Downloads the source file to the destination file. It is up to
|
9
|
+
# implementors of this class to handle the logic.
|
10
|
+
def download!(source_url, destination_file); end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Tenderloin
|
2
|
+
module Downloaders
|
3
|
+
# "Downloads" a file to a temporary file. Basically, this downloader
|
4
|
+
# simply does a file copy.
|
5
|
+
class File < Base
|
6
|
+
BUFFERSIZE = 1048576 # 1 MB
|
7
|
+
|
8
|
+
def download!(source_url, destination_file)
|
9
|
+
# For now we read the contents of one into a buffer
|
10
|
+
# and copy it into the other. In the future, we should do
|
11
|
+
# a system-level file copy (FileUtils.cp).
|
12
|
+
open(source_url) do |f|
|
13
|
+
loop do
|
14
|
+
break if f.eof?
|
15
|
+
destination_file.write(f.read(BUFFERSIZE))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Tenderloin
|
2
|
+
module Downloaders
|
3
|
+
# Downloads a file from an HTTP URL to a temporary file. This
|
4
|
+
# downloader reports its progress to stdout while downloading.
|
5
|
+
class HTTP < Base
|
6
|
+
# ANSI escape code to clear lines from cursor to end of line
|
7
|
+
CL_RESET = "\r\e[0K"
|
8
|
+
|
9
|
+
def download!(source_url, destination_file)
|
10
|
+
Net::HTTP.get_response(URI.parse(source_url)) do |response|
|
11
|
+
total = response.content_length
|
12
|
+
progress = 0
|
13
|
+
segment_count = 0
|
14
|
+
|
15
|
+
response.read_body do |segment|
|
16
|
+
# Report the progress out
|
17
|
+
progress += segment.length
|
18
|
+
segment_count += 1
|
19
|
+
|
20
|
+
# Progress reporting is limited to every 25 segments just so
|
21
|
+
# we're not constantly updating
|
22
|
+
if segment_count % 25 == 0
|
23
|
+
report_progress(progress, total)
|
24
|
+
segment_count = 0
|
25
|
+
end
|
26
|
+
|
27
|
+
# Store the segment
|
28
|
+
destination_file.write(segment)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
complete_progress
|
33
|
+
end
|
34
|
+
|
35
|
+
def report_progress(progress, total)
|
36
|
+
percent = (progress.to_f / total.to_f) * 100
|
37
|
+
print "#{CL_RESET}Download Progress: #{percent.to_i}% (#{progress} / #{total})"
|
38
|
+
$stdout.flush
|
39
|
+
end
|
40
|
+
|
41
|
+
def complete_progress
|
42
|
+
# Just clear the line back out
|
43
|
+
print "#{CL_RESET}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module Tenderloin
|
2
|
+
class Env
|
3
|
+
BOXFILE_NAME = "Tenderfile"
|
4
|
+
HOME_SUBDIRS = ["tmp", "boxes"]
|
5
|
+
|
6
|
+
# Initialize class variables used
|
7
|
+
@@persisted_vm = nil
|
8
|
+
@@root_path = nil
|
9
|
+
@@box = nil
|
10
|
+
|
11
|
+
extend Tenderloin::Util
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def box; @@box; end
|
15
|
+
def persisted_vm; @@persisted_vm; end
|
16
|
+
def root_path; @@root_path; end
|
17
|
+
def dotfile_path
|
18
|
+
File.join(root_path, $ROOTFILE_NAME + ".loinstate")
|
19
|
+
end
|
20
|
+
def home_path; File.expand_path(Tenderloin.config.tenderloin.home); end
|
21
|
+
def tmp_path; File.join(home_path, "tmp"); end
|
22
|
+
def boxes_path; File.join(home_path, "boxes"); end
|
23
|
+
def vms_path; File.join(home_path, "vms"); end
|
24
|
+
|
25
|
+
def load!
|
26
|
+
load_root_path!
|
27
|
+
load_config!
|
28
|
+
load_home_directory!
|
29
|
+
load_box!
|
30
|
+
load_vm!
|
31
|
+
end
|
32
|
+
|
33
|
+
def load_config!
|
34
|
+
# Prepare load paths for config files
|
35
|
+
load_paths = [File.join(PROJECT_ROOT, "config", "default.rb")]
|
36
|
+
load_paths << File.join(box.directory, BOXFILE_NAME) if box
|
37
|
+
load_paths << File.join(root_path, $ROOTFILE_NAME) if root_path
|
38
|
+
|
39
|
+
# Then clear out the old data
|
40
|
+
Config.reset!
|
41
|
+
|
42
|
+
load_paths.each do |path|
|
43
|
+
logger.info "Loading config from #{path}..."
|
44
|
+
load path if File.exist?(path)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Execute the configurations
|
48
|
+
Config.execute!
|
49
|
+
end
|
50
|
+
|
51
|
+
def load_home_directory!
|
52
|
+
home_dir = File.expand_path(Tenderloin.config.tenderloin.home)
|
53
|
+
|
54
|
+
dirs = HOME_SUBDIRS.collect { |path| File.join(home_dir, path) }
|
55
|
+
dirs.unshift(home_dir)
|
56
|
+
|
57
|
+
dirs.each do |dir|
|
58
|
+
next if File.directory?(dir)
|
59
|
+
|
60
|
+
logger.info "Creating home directory since it doesn't exist: #{dir}"
|
61
|
+
FileUtils.mkdir_p(dir)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def load_box!
|
66
|
+
return unless root_path
|
67
|
+
|
68
|
+
@@box = Box.find(Tenderloin.config.vm.box) if Tenderloin.config.vm.box
|
69
|
+
|
70
|
+
if @@box
|
71
|
+
logger.info("Reloading configuration to account for loaded box...")
|
72
|
+
load_config!
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def load_vm!
|
77
|
+
return if !root_path || !File.file?(dotfile_path)
|
78
|
+
|
79
|
+
File.open(dotfile_path) do |f|
|
80
|
+
@@persisted_vm = Tenderloin::VM.find(f.read)
|
81
|
+
end
|
82
|
+
rescue Errno::ENOENT
|
83
|
+
@@persisted_vm = nil
|
84
|
+
end
|
85
|
+
|
86
|
+
def persist_vm(vm_id)
|
87
|
+
File.open(dotfile_path, 'w+') do |f|
|
88
|
+
f.write(vm_id)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def load_root_path!(path=nil)
|
93
|
+
path ||= Pathname.new(Dir.pwd)
|
94
|
+
|
95
|
+
# Stop if we're at the root. 2nd regex matches windows drives
|
96
|
+
# such as C:. and Z:. Portability of this check will need to be
|
97
|
+
# researched.
|
98
|
+
return false if path.to_s == '/' || path.to_s =~ /^[A-Z]:\.$/
|
99
|
+
|
100
|
+
file = "#{path}/#{$ROOTFILE_NAME}"
|
101
|
+
if File.exist?(file)
|
102
|
+
@@root_path = path.to_s
|
103
|
+
return true
|
104
|
+
end
|
105
|
+
|
106
|
+
load_root_path!(path.parent)
|
107
|
+
end
|
108
|
+
|
109
|
+
def require_root_path
|
110
|
+
if !root_path
|
111
|
+
error_and_exit(<<-msg)
|
112
|
+
A `#{$ROOTFILE_NAME}` was not found! This file is required for tenderloin to run
|
113
|
+
since it describes the expected environment that tenderloin is supposed
|
114
|
+
to manage. Please create a #{$ROOTFILE_NAME} and place it in your project
|
115
|
+
root.
|
116
|
+
msg
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def require_box
|
121
|
+
require_root_path
|
122
|
+
|
123
|
+
if !box
|
124
|
+
if !Tenderloin.config.vm.box
|
125
|
+
error_and_exit(<<-msg)
|
126
|
+
No base box was specified! A base box is required as a staring point
|
127
|
+
for every tenderloin virtual machine. Please specify one in your Tenderfile
|
128
|
+
using `config.vm.box`
|
129
|
+
msg
|
130
|
+
else
|
131
|
+
error_and_exit(<<-msg)
|
132
|
+
Specified box `#{Tenderloin.config.vm.box}` does not exist!
|
133
|
+
|
134
|
+
The box must be added through the `tenderloin box add` command. Please view
|
135
|
+
the documentation associated with the command for more information.
|
136
|
+
msg
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def require_persisted_vm
|
142
|
+
require_root_path
|
143
|
+
|
144
|
+
if !persisted_vm
|
145
|
+
error_and_exit(<<-error)
|
146
|
+
The task you're trying to run requires that the tenderloin environment
|
147
|
+
already be created, but unfortunately this tenderloin still appears to
|
148
|
+
have no box! You can setup the environment by setting up your
|
149
|
+
#{$ROOTFILE_NAME} and running `tenderloin up`
|
150
|
+
error
|
151
|
+
return
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Tenderloin
|
2
|
+
class FusionVM
|
3
|
+
VMRUN = "/Applications/VMware\\ Fusion.app/Contents/Library/vmrun"
|
4
|
+
|
5
|
+
def initialize(vmx)
|
6
|
+
@vmx = vmx
|
7
|
+
end
|
8
|
+
|
9
|
+
def run(cmd, opts='')
|
10
|
+
retrycount = 0
|
11
|
+
while true
|
12
|
+
res = `#{VMRUN} #{cmd} #{@vmx} #{opts}`
|
13
|
+
if $? == 0
|
14
|
+
return res
|
15
|
+
else
|
16
|
+
if res =~ /VMware Tools are not running/
|
17
|
+
sleep 1; next unless retrycount > 10
|
18
|
+
end
|
19
|
+
raise "Error running vmrun command #{cmd}: " + res
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def start_fusion
|
25
|
+
# Ensure fusion is running.
|
26
|
+
`if [[ -z $(pgrep 'VMware Fusion') ]]; then open /Applications/VMware\\ Fusion.app ; sleep 5 ; fi`
|
27
|
+
end
|
28
|
+
|
29
|
+
def running?()
|
30
|
+
`#{VMRUN} list | grep "#{@vmx}"`
|
31
|
+
$? == 0 ? true : false
|
32
|
+
end
|
33
|
+
|
34
|
+
def start(opts = {})
|
35
|
+
gui_opt = opts[:headless] == true ? "nogui" : "gui"
|
36
|
+
run('start', gui_opt)
|
37
|
+
end
|
38
|
+
|
39
|
+
def stop(opts = {})
|
40
|
+
hard_opt = opts[:force] == true ? "hard" : "soft"
|
41
|
+
run 'stop', hard_opt
|
42
|
+
end
|
43
|
+
|
44
|
+
def delete()
|
45
|
+
run 'deleteVM'
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_guest_var(var)
|
49
|
+
run 'readVariable', 'guestVar ' + var
|
50
|
+
end
|
51
|
+
|
52
|
+
def ip
|
53
|
+
ip = get_guest_var('ip').strip
|
54
|
+
unless ip =~ /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/
|
55
|
+
mac_address = VMXFile.load(@vmx)["ethernet0.generatedAddress"]
|
56
|
+
ip = dhcp_leases[mac_address]
|
57
|
+
end
|
58
|
+
ip
|
59
|
+
end
|
60
|
+
|
61
|
+
def enable_shared_folders
|
62
|
+
run 'enableSharedFolders'
|
63
|
+
end
|
64
|
+
|
65
|
+
def share_folder(name, hostpath)
|
66
|
+
run 'addSharedFolder', "#{name} #{hostpath}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def dhcp_leases
|
70
|
+
mac_ip = {}
|
71
|
+
curLeaseIp = nil
|
72
|
+
Dir['/var/db/vmware/vmnet-dhcpd*.leases'].each do |f|
|
73
|
+
File.open(f).each do |line|
|
74
|
+
case line
|
75
|
+
when /lease (.*) \{/
|
76
|
+
curLeaseIp = $1
|
77
|
+
when /hardware ethernet (.*);/
|
78
|
+
mac_ip[$1] = curLeaseIp
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
mac_ip
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Tenderloin
|
2
|
+
class SSH
|
3
|
+
SCRIPT = File.join(File.dirname(__FILE__), '..', '..', 'script', 'tenderloin-ssh-expect.sh')
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def connect(opts={})
|
7
|
+
options = {}
|
8
|
+
[:host, :password, :username].each do |param|
|
9
|
+
options[param] = opts[param] || Tenderloin.config.ssh.send(param)
|
10
|
+
end
|
11
|
+
|
12
|
+
Kernel.exec "#{SCRIPT} #{options[:username]} #{options[:password]} #{options[:host]} #{port(opts)}".strip
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute(ip)
|
16
|
+
Net::SSH.start(ip, Tenderloin.config[:ssh][:username], :port => port, :password => Tenderloin.config[:ssh][:password]) do |ssh|
|
17
|
+
yield ssh
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def upload!(from, to)
|
22
|
+
execute do |ssh|
|
23
|
+
scp = Net::SCP.new(ssh)
|
24
|
+
scp.upload!(from, to)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def up?(ip)
|
29
|
+
check_thread = Thread.new do
|
30
|
+
begin
|
31
|
+
Thread.current[:result] = false
|
32
|
+
Net::SSH.start(ip, Tenderloin.config.ssh.username, :port => port, :password => Tenderloin.config.ssh.password, :timeout => Tenderloin.config.ssh.timeout) do |ssh|
|
33
|
+
Thread.current[:result] = true
|
34
|
+
end
|
35
|
+
rescue Errno::ECONNREFUSED, Net::SSH::Disconnect
|
36
|
+
# False, its defaulted above
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
check_thread.join(Tenderloin.config.ssh.timeout)
|
41
|
+
return check_thread[:result]
|
42
|
+
end
|
43
|
+
|
44
|
+
def port(opts={})
|
45
|
+
opts[:port] || 22
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Tenderloin
|
2
|
+
module Util
|
3
|
+
def self.included(base)
|
4
|
+
base.extend Tenderloin::Util
|
5
|
+
end
|
6
|
+
|
7
|
+
def wrap_output
|
8
|
+
puts "====================================================================="
|
9
|
+
yield
|
10
|
+
puts "====================================================================="
|
11
|
+
end
|
12
|
+
|
13
|
+
def error_and_exit(error)
|
14
|
+
abort <<-error
|
15
|
+
=====================================================================
|
16
|
+
Tenderloin experienced an error!
|
17
|
+
|
18
|
+
#{error.chomp}
|
19
|
+
=====================================================================
|
20
|
+
error
|
21
|
+
end
|
22
|
+
|
23
|
+
def logger
|
24
|
+
Logger.singleton_logger
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Logger < ::Logger
|
29
|
+
@@singleton_logger = nil
|
30
|
+
|
31
|
+
class << self
|
32
|
+
def singleton_logger
|
33
|
+
# TODO: Buffer messages until config is loaded, then output them?
|
34
|
+
if Tenderloin.config.loaded?
|
35
|
+
@@singleton_logger ||= Tenderloin::Logger.new(Tenderloin.config.tenderloin.log_output)
|
36
|
+
else
|
37
|
+
Tenderloin::Logger.new(nil)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def reset_logger!
|
42
|
+
@@singleton_logger = nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def format_message(level, time, progname, msg)
|
47
|
+
"[#{level} #{time.strftime('%m-%d-%Y %X')}] Tenderloin: #{msg}\n"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Tenderloin
|
2
|
+
class VM < Actions::Runner
|
3
|
+
include Tenderloin::Util
|
4
|
+
|
5
|
+
attr_accessor :vm_id
|
6
|
+
attr_accessor :from
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# Finds a virtual machine by a given UUID and either returns
|
10
|
+
# a Tenderloin::VM object or returns nil.
|
11
|
+
def find(uuid)
|
12
|
+
# TODO - just validate the existence - there's no 'loading' needed
|
13
|
+
new(uuid) if File.exists? File.join(Tenderloin::Env.vms_path, uuid, uuid + ".vmx")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(vm=nil)
|
18
|
+
@vm_id = vm
|
19
|
+
end
|
20
|
+
|
21
|
+
def package(out_path, include_files=[])
|
22
|
+
add_action(Actions::VM::Export)
|
23
|
+
add_action(Actions::VM::Package, out_path, include_files)
|
24
|
+
execute!
|
25
|
+
end
|
26
|
+
|
27
|
+
def vmx_path
|
28
|
+
File.join(Tenderloin::Env.vms_path, @vm_id, @vm_id + ".vmx")
|
29
|
+
end
|
30
|
+
|
31
|
+
def fusion_vm
|
32
|
+
@fusion_vm ||= FusionVM.new(vmx_path) if vmx_path
|
33
|
+
end
|
34
|
+
|
35
|
+
def start
|
36
|
+
return if running?
|
37
|
+
|
38
|
+
execute!(Actions::VM::Start)
|
39
|
+
end
|
40
|
+
|
41
|
+
def running?
|
42
|
+
fusion_vm.running?
|
43
|
+
end
|
44
|
+
|
45
|
+
def destroy
|
46
|
+
execute!(Actions::VM::Destroy)
|
47
|
+
end
|
48
|
+
|
49
|
+
def suspend
|
50
|
+
execute!(Actions::VM::Suspend)
|
51
|
+
end
|
52
|
+
|
53
|
+
def resume
|
54
|
+
execute!(Actions::VM::Resume)
|
55
|
+
end
|
56
|
+
|
57
|
+
def saved?
|
58
|
+
@vm.saved?
|
59
|
+
end
|
60
|
+
|
61
|
+
def powered_off?; @vm.powered_off? end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Tenderloin
|
2
|
+
class VMXFile
|
3
|
+
|
4
|
+
def self.load(filename)
|
5
|
+
data = {}
|
6
|
+
File.open(filename).each do |line|
|
7
|
+
parts = line.split('=')
|
8
|
+
data[parts[0].strip] = parts[1].strip.gsub!(/^"(.*?)"$/,'\1')
|
9
|
+
end
|
10
|
+
data
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.write(filename, data)
|
14
|
+
File.open(filename, 'w') do |f|
|
15
|
+
data.each do |k,v|
|
16
|
+
f.puts "#{k} = \"#{v}\""
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.with_vmx_data(filename, &block)
|
22
|
+
data = load(filename)
|
23
|
+
block.call(data)
|
24
|
+
write(filename, data)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
data/lib/tenderloin.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
libdir = File.dirname(__FILE__)
|
2
|
+
$:.unshift(libdir)
|
3
|
+
PROJECT_ROOT = File.join(libdir, '..') unless defined?(PROJECT_ROOT)
|
4
|
+
|
5
|
+
# The libs which must be loaded prior to the rest
|
6
|
+
%w{tempfile open-uri json pathname logger uri net/http net/ssh archive/tar/minitar
|
7
|
+
net/scp fileutils tenderloin/util tenderloin/actions/base tenderloin/downloaders/base tenderloin/actions/runner}.each do |f|
|
8
|
+
require f
|
9
|
+
end
|
10
|
+
|
11
|
+
# Glob require the rest
|
12
|
+
Dir[File.join(PROJECT_ROOT, "lib", "tenderloin", "**", "*.rb")].each do |f|
|
13
|
+
require f
|
14
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/expect
|
2
|
+
|
3
|
+
set uname [lrange $argv 0 0]
|
4
|
+
set password [lrange $argv 1 1]
|
5
|
+
set host [lrange $argv 2 2]
|
6
|
+
set port [lrange $argv 3 3]
|
7
|
+
|
8
|
+
if { $port != "" } {
|
9
|
+
set port_option "-p $port"
|
10
|
+
} else {
|
11
|
+
set port_option ""
|
12
|
+
}
|
13
|
+
|
14
|
+
spawn ssh $port_option -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $uname@$host
|
15
|
+
|
16
|
+
set timeout 30
|
17
|
+
expect "*password: " {
|
18
|
+
send "$password\r"
|
19
|
+
} timeout {
|
20
|
+
send_user "Error connecting"
|
21
|
+
}
|
22
|
+
|
23
|
+
interact
|