puter 0.0.1

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