puter 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 +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
|