puter 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/NOTES.md +54 -0
- data/README.md +124 -0
- data/Rakefile +4 -0
- data/bin/puter +5 -0
- data/examples/Puterfile +25 -0
- data/examples/localfile.txt +1 -0
- data/examples/localfile_with_a_really_really_longname.txt +1 -0
- data/lib/puter.rb +23 -0
- data/lib/puter/backend/ssh.rb +69 -0
- data/lib/puter/cli.rb +57 -0
- data/lib/puter/cli/aws.rb +18 -0
- data/lib/puter/cli/vm.rb +167 -0
- data/lib/puter/providers/vm.rb +107 -0
- data/lib/puter/puterfile.rb +198 -0
- data/lib/puter/ui.rb +24 -0
- data/lib/puter/version.rb +3 -0
- data/puter.gemspec +30 -0
- data/spec/cli/vm_spec.rb +0 -0
- data/spec/fixtures/Puterfile +3 -0
- data/spec/fixtures/Puterfile.bad +4 -0
- data/spec/providers/vm_spec.rb +16 -0
- data/spec/puterfile/parse_operation_spec.rb +162 -0
- data/spec/puterfile/parse_spec.rb +24 -0
- data/spec/spec_helper.rb +19 -0
- metadata +195 -0
data/lib/puter/cli/vm.rb
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'puter/providers/vm'
|
2
|
+
require 'puter/backend/ssh'
|
3
|
+
|
4
|
+
module Puter
|
5
|
+
module CLI
|
6
|
+
class Vm < Thor
|
7
|
+
images_option = [:images, {:type => :string, :default => '/Puter/Images', :banner => '/path/to/Puter/Images', :desc => 'Override the default Images vSphere folder.' }]
|
8
|
+
instances_option = [:instances, {:type => :string, :default => '/Puter/Instances', :banner => '/path/to/Puter/Instances', :desc => 'Override the default Instances vSphere folder.' }]
|
9
|
+
build_option = [:build, {:type => :string, :default => '/Puter/Build', :banner => '/path/to/Puter/Build', :desc => 'Override the default Build vSphere folder.' }]
|
10
|
+
|
11
|
+
desc 'images', 'Lists available Puter images.'
|
12
|
+
method_option *images_option
|
13
|
+
def images()
|
14
|
+
CLI.run_cli do
|
15
|
+
vm.images(options[:images]).each { |i| Puter.ui.info i }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "apply INSTANCE CONTEXT", "Applies Puterfile to an existing & running VM"
|
20
|
+
method_option *instances_option
|
21
|
+
def apply(instance_name, context)
|
22
|
+
CLI.run_cli do
|
23
|
+
instance_path = "#{options[:instances]}/#{instance_name}"
|
24
|
+
puterfile_path = File.expand_path 'Puterfile', context
|
25
|
+
puterfile = Puter::Puterfile.from_path puterfile_path
|
26
|
+
|
27
|
+
vm.host(instance_path) do |host|
|
28
|
+
Puter.ui.info "Applying '#{puterfile_path}' to '#{instance_path}' at #{host}"
|
29
|
+
backend = Puter::Backend::Ssh.new(host, Puter::CLI::SSH_OPTS)
|
30
|
+
ret = puterfile.apply(context, backend, Puter.ui)
|
31
|
+
end
|
32
|
+
Puter.ui.info "Successfully applied '#{puterfile_path}' to '#{instance_name}'"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "build IMAGE CONTEXT", "Builds a new Puter image"
|
37
|
+
method_option *images_option
|
38
|
+
method_option *build_option
|
39
|
+
option :force, :type => :boolean, :default => false, :description => "Replaces Image specified by NAME if it exists"
|
40
|
+
def build(image_name, context)
|
41
|
+
CLI.run_cli do
|
42
|
+
build_path = "#{options[:build]}/#{image_name}"
|
43
|
+
images_path = "#{options[:images]}/#{image_name}"
|
44
|
+
|
45
|
+
puterfile_path = File.expand_path 'Puterfile', context
|
46
|
+
puterfile = Puter::Puterfile.from_path puterfile_path
|
47
|
+
|
48
|
+
Puter.ui.info "Building '#{images_path}' FROM '#{options[:images]}/#{puterfile.from}'"
|
49
|
+
Puter.ui.info "Waiting for SSH"
|
50
|
+
vm.build(build_path, images_path, "#{options[:images]}/#{puterfile.from}", options) do |host|
|
51
|
+
Puter.ui.info "Applying '#{puterfile_path}' to '#{build_path}' at #{host}"
|
52
|
+
backend = Puter::Backend::Ssh.new(host, Puter::CLI::SSH_OPTS)
|
53
|
+
ret = puterfile.apply(context, backend, Puter.ui)
|
54
|
+
Puter.ui.info "Stopping '#{build_path}' and moving to '#{images_path}'"
|
55
|
+
end
|
56
|
+
Puter.ui.info "Successfully built '#{image_name}'"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
desc "rmi IMAGE", "Removes (deletes) a Puter image"
|
61
|
+
method_option *images_option
|
62
|
+
def rmi(image_name)
|
63
|
+
image_path = "#{options[:images]}/#{image_name}"
|
64
|
+
CLI.run_cli do
|
65
|
+
vm.rmi image_path
|
66
|
+
Puter.ui.info "Removed image '#{image_path}'"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
desc "create IMAGE INSTANCE", "Creates (clones) a Puter instance from IMAGE as NAME"
|
71
|
+
method_option *images_option
|
72
|
+
method_option *instances_option
|
73
|
+
option :force, :type => :boolean, :default => false, :description => "Replaces Instance specified by NAME if it exists"
|
74
|
+
def create(image_name, instance_name)
|
75
|
+
CLI.run_cli do
|
76
|
+
image_path = "#{options[:images]}/#{image_name}"
|
77
|
+
instance_path = "#{options[:instances]}/#{instance_name}"
|
78
|
+
|
79
|
+
vm.create image_path, instance_path, options
|
80
|
+
Puter.ui.info "Created instance '#{instance_path}' from '#{image_path}'"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
desc "ps", "Lists Puter instances"
|
85
|
+
method_option *instances_option
|
86
|
+
option :all, :type => :boolean, :default => false, :description => 'Includes non-running instances.'
|
87
|
+
def ps()
|
88
|
+
CLI.run_cli do
|
89
|
+
vm.ps(options[:instances], options[:all]).each { |i| Puter.ui.info i }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
desc "start INSTANCE", "Starts a Puter instance"
|
94
|
+
method_option *instances_option
|
95
|
+
def start(instance_name)
|
96
|
+
CLI.run_cli do
|
97
|
+
instance_path = "#{options[:instances]}/#{instance_name}"
|
98
|
+
|
99
|
+
Puter.ui.info "Starting instance '#{instance_path}', waiting for SSH..."
|
100
|
+
vm.start instance_path do |host|
|
101
|
+
Puter.ui.info "Started '#{instance_path}' at #{host}."
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
desc "stop INSTANCE", "Stops a Puter instance"
|
107
|
+
method_option *instances_option
|
108
|
+
def stop(instance_name)
|
109
|
+
CLI.run_cli do
|
110
|
+
instance_path = "#{options[:instances]}/#{instance_name}"
|
111
|
+
|
112
|
+
Puter.ui.info "Stopping instance '#{instance_path}', waiting for shutdown..."
|
113
|
+
vm.stop instance_path
|
114
|
+
Puter.ui.info "Stopped '#{instance_path}'."
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
desc "kill INSTANCE", "Kills a Puter instance"
|
119
|
+
method_option *instances_option
|
120
|
+
def kill(instance_name)
|
121
|
+
CLI.run_cli do
|
122
|
+
instance_path = "#{options[:instances]}/#{instance_name}"
|
123
|
+
|
124
|
+
Puter.ui.info "Killing instance '#{instance_path}'"
|
125
|
+
vm.kill instance_path
|
126
|
+
Puter.ui.info "Killed '#{instance_path}'."
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
desc "rm INSTANCE", "Removes (deletes) a Puter instance"
|
131
|
+
method_option *instances_option
|
132
|
+
def rm(instance_name)
|
133
|
+
CLI.run_cli do
|
134
|
+
instance_path = "#{options[:instances]}/#{instance_name}"
|
135
|
+
vm.rm instance_path
|
136
|
+
Puter.ui.info "Removed instance '#{instance_path}'"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
desc "init PATH", "Initializes Puter VM folders in VMware"
|
141
|
+
long_desc <<-LONGDESC
|
142
|
+
Initializes Puter VM folders in VMware.
|
143
|
+
|
144
|
+
Creates the following folder hierarchy:
|
145
|
+
|
146
|
+
PATH/ - default: /Puter
|
147
|
+
Build/ - working folder for building Puter images
|
148
|
+
Images/ - Puter images (VM Templates)
|
149
|
+
Instances/ - Putere instances (VMs)
|
150
|
+
|
151
|
+
PATH must be the full vSphere folder path name, e.g. '/Puter'.
|
152
|
+
LONGDESC
|
153
|
+
def init(path = '/Puter')
|
154
|
+
CLI.run_cli do
|
155
|
+
vm.init(path)
|
156
|
+
Puter.ui.info "Create Puter folders under #{path}"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def vm
|
163
|
+
@vm ||= Puter::Provider::Vm.new
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'vmonkey'
|
2
|
+
require 'puter/puterfile'
|
3
|
+
|
4
|
+
module Puter
|
5
|
+
module Provider
|
6
|
+
class Vm
|
7
|
+
|
8
|
+
def images(path, sub="")
|
9
|
+
folder = vmonkey.folder!(path)
|
10
|
+
imgs = folder.templates.collect { |t| "#{sub}#{t.name}" }
|
11
|
+
folder.folders.each do |sub_folder|
|
12
|
+
imgs += images("#{path}/#{sub_folder.name}", "#{sub}#{sub_folder.name}/")
|
13
|
+
end
|
14
|
+
imgs
|
15
|
+
end
|
16
|
+
|
17
|
+
def host(name, &block)
|
18
|
+
target = vmonkey.vm! name
|
19
|
+
block.call(target.guest_ip)
|
20
|
+
end
|
21
|
+
|
22
|
+
def build(build_path, image_path, template_name, opts, &block)
|
23
|
+
vmonkey.folder('/').mk_parent_folder build_path
|
24
|
+
|
25
|
+
template = vmonkey.vm! template_name
|
26
|
+
if opts[:force]
|
27
|
+
build = template.clone_to! build_path
|
28
|
+
else
|
29
|
+
build = template.clone_to build_path
|
30
|
+
end
|
31
|
+
|
32
|
+
build.start
|
33
|
+
build.wait_for_port 22
|
34
|
+
block.call(build.guest_ip) if block
|
35
|
+
build.stop
|
36
|
+
build.MarkAsTemplate()
|
37
|
+
|
38
|
+
vmonkey.folder('/').mk_parent_folder image_path
|
39
|
+
|
40
|
+
if opts[:force]
|
41
|
+
build.move_to! image_path
|
42
|
+
else
|
43
|
+
build.move_to image_path
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def rmi(path)
|
48
|
+
vmonkey.template!(path).destroy
|
49
|
+
end
|
50
|
+
|
51
|
+
def create(image_path, instance_path, opts)
|
52
|
+
vmonkey.folder('/').mk_parent_folder instance_path
|
53
|
+
|
54
|
+
if opts[:force]
|
55
|
+
vmonkey.vm!(image_path).clone_to! instance_path
|
56
|
+
else
|
57
|
+
vmonkey.vm!(image_path).clone_to instance_path
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def start(instance_name, &block)
|
62
|
+
instance = vmonkey.vm! instance_name
|
63
|
+
instance.start
|
64
|
+
instance.wait_for_port 22
|
65
|
+
block.call(instance.guest_ip) if block
|
66
|
+
end
|
67
|
+
|
68
|
+
def stop(instance_name)
|
69
|
+
instance = vmonkey.vm! instance_name
|
70
|
+
instance.stop
|
71
|
+
end
|
72
|
+
|
73
|
+
def kill(instance_name)
|
74
|
+
instance = vmonkey.vm! instance_name
|
75
|
+
instance.kill
|
76
|
+
end
|
77
|
+
|
78
|
+
def ps(instances_path, all, sub="")
|
79
|
+
folder = vmonkey.folder! instances_path
|
80
|
+
instances = folder.vms
|
81
|
+
instances.select! { |vm| vm.runtime.powerState == 'poweredOn' } unless all
|
82
|
+
|
83
|
+
ret = instances.collect { |i| "#{sub}#{i.name}" }
|
84
|
+
folder.folders.each do |sub_folder|
|
85
|
+
ret += ps("#{instances_path}/#{sub_folder.name}", all, "#{sub}#{sub_folder.name}/")
|
86
|
+
end
|
87
|
+
ret
|
88
|
+
end
|
89
|
+
|
90
|
+
def rm(path)
|
91
|
+
vmonkey.vm!(path).destroy
|
92
|
+
end
|
93
|
+
|
94
|
+
def init(path)
|
95
|
+
root = vmonkey.folder '/'
|
96
|
+
root.mk_folder "#{path}/Build"
|
97
|
+
root.mk_folder "#{path}/Images"
|
98
|
+
root.mk_folder "#{path}/Instances"
|
99
|
+
end
|
100
|
+
|
101
|
+
def vmonkey
|
102
|
+
@vmonkey ||= VMonkey.connect
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
module Puter
|
2
|
+
class SyntaxError < Exception
|
3
|
+
end
|
4
|
+
|
5
|
+
class RunError < Exception
|
6
|
+
{:cmd=>"sudo -u root -s -- sh -c 'exit 3'", :stdout=>"", :stderr=>"", :exit_status=>3, :exit_signal=>nil}
|
7
|
+
|
8
|
+
attr_accessor :result
|
9
|
+
attr_accessor :cmd
|
10
|
+
attr_accessor :exit_status
|
11
|
+
attr_accessor :exit_signal
|
12
|
+
|
13
|
+
def initialize(message, result)
|
14
|
+
super(message)
|
15
|
+
@result = result
|
16
|
+
@cmd = result[:cmd]
|
17
|
+
@exit_status = result[:exit_status]
|
18
|
+
@exit_signal = result[:exit_signal]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Puterfile
|
23
|
+
|
24
|
+
attr_accessor :raw
|
25
|
+
attr_accessor :lines
|
26
|
+
attr_accessor :from
|
27
|
+
attr_accessor :operations
|
28
|
+
attr_accessor :executable_ops
|
29
|
+
|
30
|
+
BACKSLASH = "\\"
|
31
|
+
|
32
|
+
FROM = :from
|
33
|
+
RUN = :run
|
34
|
+
COPY = :copy
|
35
|
+
BLANK = :blank
|
36
|
+
CONTINUE = :continue
|
37
|
+
COMMENT = :comment
|
38
|
+
|
39
|
+
COMMANDS = [ FROM, RUN, COPY ]
|
40
|
+
|
41
|
+
class << self
|
42
|
+
def from_path(path)
|
43
|
+
parse File.open(path, 'rb') { |f| f.read }
|
44
|
+
end
|
45
|
+
|
46
|
+
def parse(raw)
|
47
|
+
p = Puterfile.new
|
48
|
+
p.raw = raw
|
49
|
+
p.lines = raw.to_s.split "\n"
|
50
|
+
p.operations = parse_operations(p.lines)
|
51
|
+
p.executable_ops = executable_operations(p.operations)
|
52
|
+
p.from = p.operations[0][:data]
|
53
|
+
p
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_operations(lines)
|
57
|
+
raise Puter::SyntaxError.new "File is empty. First line must be a FROM command" if lines.length == 0
|
58
|
+
|
59
|
+
ops = []
|
60
|
+
previous_line = ""
|
61
|
+
lines.each_with_index do | line, i |
|
62
|
+
begin
|
63
|
+
ops << parse_operation(line, previous_line)
|
64
|
+
if i == 0
|
65
|
+
raise Puter::SyntaxError.new "First line must be a FROM command" unless ops[i][:operation] == FROM
|
66
|
+
end
|
67
|
+
rescue Puter::SyntaxError => se
|
68
|
+
raise Puter::SyntaxError.new "On line #{i+1}: #{se.message}"
|
69
|
+
end
|
70
|
+
previous_line = line
|
71
|
+
end
|
72
|
+
ops
|
73
|
+
end
|
74
|
+
|
75
|
+
def parse_operation(line, previous_line="")
|
76
|
+
op = {}
|
77
|
+
line = line.rstrip unless line.nil?
|
78
|
+
|
79
|
+
case
|
80
|
+
when line.nil?
|
81
|
+
raise SyntaxError.new 'cannot parse nil lines'
|
82
|
+
|
83
|
+
# blank line
|
84
|
+
when line.strip.empty?
|
85
|
+
op[:operation] = BLANK
|
86
|
+
op[:data] = line
|
87
|
+
op[:continue] = false
|
88
|
+
|
89
|
+
# commented line
|
90
|
+
when line =~ /\s*\#/
|
91
|
+
op[:operation] = COMMENT
|
92
|
+
op[:data] = line
|
93
|
+
op[:continue] = line[-1] == BACKSLASH
|
94
|
+
|
95
|
+
# continuation of a previous line
|
96
|
+
when line =~ /\s/ && previous_line.rstrip[-1] == BACKSLASH
|
97
|
+
op[:operation] = CONTINUE
|
98
|
+
op[:data] = line.lstrip
|
99
|
+
op[:continue] = line[-1] == BACKSLASH
|
100
|
+
|
101
|
+
# must be an operation (FROM, COPY, RUN, ...)
|
102
|
+
else
|
103
|
+
parts = line.split(/\s+/, 2)
|
104
|
+
cmd = parts[0].downcase.to_sym
|
105
|
+
data = parts[1]
|
106
|
+
|
107
|
+
raise SyntaxError.new "Unknown operation [#{cmd.to_s.upcase}]" unless COMMANDS.include? cmd
|
108
|
+
raise SyntaxError.new "Operation [#{cmd.to_s.upcase}] has no data" if data.nil?
|
109
|
+
op[:operation] = cmd
|
110
|
+
op[:data] = data
|
111
|
+
op[:continue] = line[-1] == BACKSLASH
|
112
|
+
|
113
|
+
end
|
114
|
+
op[:data][-1] = " " if op[:continue]
|
115
|
+
|
116
|
+
op
|
117
|
+
end
|
118
|
+
|
119
|
+
def executable_operations(operations)
|
120
|
+
execs = []
|
121
|
+
operations.each_with_index do |op, i|
|
122
|
+
case op[:operation]
|
123
|
+
when COPY, RUN, FROM
|
124
|
+
exec = {
|
125
|
+
:operation => op[:operation],
|
126
|
+
:data => op[:data].dup
|
127
|
+
}
|
128
|
+
exec[:start_line] = exec[:end_line] = i
|
129
|
+
execs << exec
|
130
|
+
when CONTINUE
|
131
|
+
execs.last[:data] << op[:data]
|
132
|
+
execs.last[:end_line] = i
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
execs.select { |e| e[:operation] == COPY }.each do |e|
|
137
|
+
e[:from], e[:to] = e[:data].strip.split /\s+/, 2
|
138
|
+
raise SyntaxError.new "COPY operation requires two parameters #{e.inspect}" if e[:from].nil? || e[:to].nil?
|
139
|
+
end
|
140
|
+
|
141
|
+
execs
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
def apply(context, backend, ui)
|
147
|
+
dependency_check(context)
|
148
|
+
ret = { :exit_status => 0, :exit_signal => nil }
|
149
|
+
|
150
|
+
executable_ops.each_with_index do |op, step|
|
151
|
+
ui.info "Step #{step} : #{op[:operation].to_s.upcase} #{op[:data]}"
|
152
|
+
|
153
|
+
case op[:operation]
|
154
|
+
when COPY
|
155
|
+
backend.copy path_in_context(op[:from], context), op[:to]
|
156
|
+
when RUN
|
157
|
+
ret = backend.run op[:data] do | type, data |
|
158
|
+
case type
|
159
|
+
when :stderr
|
160
|
+
ui.remote_stderr data
|
161
|
+
when :stdout
|
162
|
+
ui.remote_stdout data
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
if ret[:exit_status] != 0
|
167
|
+
line = "#{op[:start_line]+1}"
|
168
|
+
plural = ""
|
169
|
+
if op[:start_line] != op[:end_line]
|
170
|
+
line << "..#{op[:end_line]+1}"
|
171
|
+
plural = "s"
|
172
|
+
end
|
173
|
+
|
174
|
+
raise RunError.new "On line#{plural} #{line}: RUN command exited non-zero.", ret
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
ret
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
def path_in_context(path, context)
|
184
|
+
File.expand_path(path, context)
|
185
|
+
end
|
186
|
+
|
187
|
+
def dependency_check(context)
|
188
|
+
executable_ops.each do |op|
|
189
|
+
case op[:operation]
|
190
|
+
when COPY
|
191
|
+
File.open(path_in_context(op[:from], context), 'r') {}
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|