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.
@@ -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