ruby-cute 0.0.1 → 0.0.2
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 +6 -0
- data/.yardopts +2 -0
- data/Gemfile +6 -0
- data/README.md +137 -6
- data/Rakefile +48 -0
- data/bin/cute +22 -0
- data/debian/changelog +5 -0
- data/debian/compat +1 -0
- data/debian/control +15 -0
- data/debian/copyright +33 -0
- data/debian/ruby-cute.docs +2 -0
- data/debian/ruby-tests.rb +2 -0
- data/debian/rules +19 -0
- data/debian/source/format +1 -0
- data/debian/watch +2 -0
- data/examples/distem-bootstrap +516 -0
- data/examples/g5k_exp1.rb +41 -0
- data/examples/g5k_exp_virt.rb +129 -0
- data/lib/cute.rb +7 -2
- data/lib/cute/bash.rb +337 -0
- data/lib/cute/configparser.rb +404 -0
- data/lib/cute/execute.rb +272 -0
- data/lib/cute/extensions.rb +38 -0
- data/lib/cute/g5k_api.rb +1190 -0
- data/lib/cute/net-ssh.rb +144 -0
- data/lib/cute/net.rb +29 -0
- data/lib/cute/synchronization.rb +89 -0
- data/lib/cute/taktuk.rb +554 -0
- data/lib/cute/version.rb +3 -0
- data/ruby-cute.gemspec +32 -0
- data/spec/extensions_spec.rb +17 -0
- data/spec/g5k_api_spec.rb +192 -0
- data/spec/spec_helper.rb +66 -0
- data/spec/taktuk_spec.rb +129 -0
- data/test/test_bash.rb +71 -0
- metadata +204 -47
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'cute'
|
2
|
+
|
3
|
+
# This example tests two libraries for the execution of commands over several machines.
|
4
|
+
# These two libraries use two different approaches: 1) SSH, 1) TakTuk.
|
5
|
+
# This example uses Grid'5000 but it can be used with any set of machines that can be accessed through SSH.
|
6
|
+
|
7
|
+
g5k = Cute::G5K::API.new()
|
8
|
+
# We reuse a job if there is one available.
|
9
|
+
if g5k.get_my_jobs("grenoble").empty? then
|
10
|
+
job = g5k.reserve(:nodes => 5, :site => 'grenoble', :walltime => '00:30:00')
|
11
|
+
else
|
12
|
+
job =g5k.get_my_jobs("grenoble").first
|
13
|
+
end
|
14
|
+
|
15
|
+
nodes = job["assigned_nodes"]
|
16
|
+
|
17
|
+
results = {}
|
18
|
+
|
19
|
+
# please change user by your Grid'5000 user.
|
20
|
+
Net::SSH::Multi.start do |session|
|
21
|
+
|
22
|
+
nodes.each{ |node| session.use "user@#{node}" }
|
23
|
+
session.exec 'hostname'
|
24
|
+
session.loop
|
25
|
+
results = session.exec! 'df'
|
26
|
+
session.exec 'uptime'
|
27
|
+
end
|
28
|
+
|
29
|
+
puts results
|
30
|
+
|
31
|
+
Cute::TakTuk.start(nodes,:user => "user" ) do |tak|
|
32
|
+
|
33
|
+
results = tak.exec!("hostname")
|
34
|
+
tak.loop()
|
35
|
+
tak.exec("df")
|
36
|
+
tak.exec("uname -r")
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
puts results
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# = Virtualization example on Grid'5000
|
2
|
+
|
3
|
+
# This script implements the example virtualization and subnet reservation in Grid'5000.
|
4
|
+
# In a nutshell, a machine and a sub network is reserved per site.
|
5
|
+
# Configuration files are generated to make kvm vms take the IPs addresses reserved, then several virtual
|
6
|
+
# machines are booted per machine (default 2). At the end all virtual machines are contacted via SSH.
|
7
|
+
# The example is described in https://www.grid5000.fr/mediawiki/index.php/Virtualization_on_Grid%275000
|
8
|
+
|
9
|
+
require 'grid5000/subnets'
|
10
|
+
require 'cute'
|
11
|
+
require 'net/scp'
|
12
|
+
|
13
|
+
g5k = Cute::G5K::API.new()
|
14
|
+
# We reuse a job if there is one available.
|
15
|
+
G5K_SITES = [:lille, :rennes, :lyon, :grenoble]
|
16
|
+
|
17
|
+
threads = []
|
18
|
+
jobs = {}
|
19
|
+
grid5000_opt = {:user => "oar", :keys => ["~/.ssh/id_rsa"], :port => 6667 }
|
20
|
+
num_vm = 2 # Number of vms per site
|
21
|
+
|
22
|
+
G5K_SITES.each{ |site|
|
23
|
+
threads << Thread.new {
|
24
|
+
if g5k.get_my_jobs(site).empty?
|
25
|
+
# As the platform could be busy and we could wait for a long time.
|
26
|
+
# here, we set wait_time to 200 seconds, like that we would just use the sites that are free.
|
27
|
+
begin
|
28
|
+
jobs[site] = g5k.reserve(:site => site, :resources => "slash_22=1+{virtual!='none'}/nodes=1",
|
29
|
+
:walltime =>"01:00:00",:keys => "~/.ssh/id_rsa", :wait_time => 200)
|
30
|
+
rescue Cute::G5K::EventTimeout
|
31
|
+
puts "We waited long enough, releasing job in site #{site}"
|
32
|
+
g5k.release(jobs[site]) # we release the job
|
33
|
+
jobs.delete(site)
|
34
|
+
end
|
35
|
+
else
|
36
|
+
jobs[site] = g5k.get_my_jobs(site).first
|
37
|
+
end
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
threads.each{ |t| t.join}
|
42
|
+
|
43
|
+
nodes = []
|
44
|
+
jobs.each{ |k,v| nodes+=v["assigned_nodes"]}
|
45
|
+
|
46
|
+
puts("Nodes reserved: #{nodes.inspect}")
|
47
|
+
|
48
|
+
# Creating vm configuration files
|
49
|
+
vm_dir = Dir.mktmpdir("vm_def")
|
50
|
+
|
51
|
+
system("wget -O #{vm_dir}/vm-template.xml http://public.nancy.grid5000.fr/~cruizsanabria/vm-template.xml")
|
52
|
+
|
53
|
+
template = ERB.new(File.read("#{vm_dir}/vm-template.xml"))
|
54
|
+
|
55
|
+
vms = []
|
56
|
+
|
57
|
+
G5K_SITES.each{ |site|
|
58
|
+
subnet = g5k.get_subnets(jobs[site]).first
|
59
|
+
ips = subnet.map{ |ip| ip.to_s }
|
60
|
+
num_vm.times{ |n|
|
61
|
+
@vm_name = "node#{n}.#{site}"
|
62
|
+
@vm_mac = ip2mac(ips[n+1])
|
63
|
+
vms.push(ips[n+1]) # avoiding .0 last octet
|
64
|
+
@tap_device = "tap#{n}"
|
65
|
+
File.open("#{vm_dir}/node_#{n}.#{site}.xml",'w+') do |f|
|
66
|
+
f.puts(template.result()) # ERB replaces @vm_name, @vm_mac and, @tap_device in the file.
|
67
|
+
end
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
puts("vm's ip assigned #{vms.inspect}")
|
72
|
+
|
73
|
+
# Setting up VMs
|
74
|
+
Cute::TakTuk.start(nodes, grid5000_opt) do |tak|
|
75
|
+
|
76
|
+
tak.exec!("mkdir -p ~/vm_definitions")
|
77
|
+
|
78
|
+
tak.exec("wget -q -O /tmp/wheezy-x64-base.qcow2 http://public.nancy.grid5000.fr/~cruizsanabria/wheezy-x64-base.qcow2")
|
79
|
+
puts("Transfering configuration files")
|
80
|
+
Dir.entries(vm_dir).each{ |vm_file|
|
81
|
+
next if vm_file[0] =="." # avoid . and .. files
|
82
|
+
puts File.join(vm_dir,vm_file)
|
83
|
+
tak.put(File.join(vm_dir,vm_file),"/tmp/#{vm_file}")
|
84
|
+
}
|
85
|
+
# Creates a number of tap devices number of vms/number of machines
|
86
|
+
puts("Creating TAP devices")
|
87
|
+
num_vm.times{ tak.exec!("sudo create_tap") }
|
88
|
+
|
89
|
+
# Creating contextualization script to copy our ssh key
|
90
|
+
tak.exec!("mkdir -p ~/kvm-context")
|
91
|
+
tak.exec!("cp ~/.ssh/id_rsa.pub ~/kvm-context/")
|
92
|
+
File.open("/tmp/post-install","w+") do |f|
|
93
|
+
f.puts("#!/bin/sh")
|
94
|
+
f.puts("mkdir -p /root/.ssh")
|
95
|
+
f.puts("cat /mnt/id_rsa.pub >> /root/.ssh/authorized_keys")
|
96
|
+
end
|
97
|
+
tak.put("/tmp/post-install","/tmp/post-install")
|
98
|
+
tak.exec!("cp /tmp/post-install ~/kvm-context/post-install")
|
99
|
+
tak.exec!("chmod 755 ~/kvm_context/post-install")
|
100
|
+
tak.exec!("genisoimage -r -o /tmp/kvm-context.iso ~/kvm-context/")
|
101
|
+
end
|
102
|
+
|
103
|
+
# Starting vms
|
104
|
+
Net::SSH::Multi.start do |session|
|
105
|
+
jobs.each{ |site,job|
|
106
|
+
# We create a group per site
|
107
|
+
session.group site do
|
108
|
+
job["assigned_nodes"].each{ |node|
|
109
|
+
session.use node, grid5000_opt
|
110
|
+
}
|
111
|
+
end
|
112
|
+
num_vm.times{ |n| puts session.with(site).exec!("virsh create /tmp/node_#{n}.#{site}.xml")}
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
puts("Waiting for the machines to start")
|
117
|
+
sleep 60
|
118
|
+
|
119
|
+
# Executing some commands on the vms
|
120
|
+
|
121
|
+
Net::SSH::Multi.start do |session|
|
122
|
+
|
123
|
+
vms.each{ |vm|
|
124
|
+
session.use("root@#{vm}")
|
125
|
+
}
|
126
|
+
session.exec("hostname")
|
127
|
+
session.exec("uptime")
|
128
|
+
|
129
|
+
end
|
data/lib/cute.rb
CHANGED
data/lib/cute/bash.rb
ADDED
@@ -0,0 +1,337 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# Features a cool class to interface with Bash.
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'digest'
|
7
|
+
require 'open3'
|
8
|
+
|
9
|
+
module Cute; module Bash
|
10
|
+
|
11
|
+
class BashError < StandardError; end
|
12
|
+
class BashTimeout < BashError; end
|
13
|
+
class BashPaddingError < BashError; end
|
14
|
+
|
15
|
+
class StatusError < BashError
|
16
|
+
|
17
|
+
attr_reader :status
|
18
|
+
attr_reader :output
|
19
|
+
|
20
|
+
def initialize(cmd, status, output)
|
21
|
+
super("'#{cmd}' returned with status = #{status}")
|
22
|
+
@status = status
|
23
|
+
@cmd = cmd
|
24
|
+
@output = output
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
class Bash
|
30
|
+
|
31
|
+
def initialize(stdin, stdout, debug = false)
|
32
|
+
@stdin = stdin
|
33
|
+
@stdout = stdout
|
34
|
+
@debug = debug
|
35
|
+
@buff = ''
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse(&block)
|
39
|
+
return self.instance_exec(&block)
|
40
|
+
end
|
41
|
+
|
42
|
+
def _loop
|
43
|
+
while true do
|
44
|
+
x = IO::select([@stdout], [], [], 120.0)
|
45
|
+
raise BashTimeout.new if x.nil?
|
46
|
+
bytes = @stdout.sysread(1024)
|
47
|
+
$stderr.write("\nBASH IN: #{bytes}\n") if @debug
|
48
|
+
@buff << bytes
|
49
|
+
break if yield
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def _nonce
|
54
|
+
randee = 4.times.map { rand().to_s }.join('|')
|
55
|
+
return Digest::SHA512.hexdigest(randee).to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
def _run(cmd, opts)
|
59
|
+
# it's a kind of magic
|
60
|
+
$stderr.write("\nBASH CMD: #{cmd}\n") if @debug
|
61
|
+
nonce = _nonce()
|
62
|
+
@stdin.write("#{cmd}; printf '%04d#{nonce}' $?\n")
|
63
|
+
@stdin.flush
|
64
|
+
_loop do
|
65
|
+
@buff.include?(nonce)
|
66
|
+
end
|
67
|
+
raise BashPaddingError.new if !@buff.end_with?(nonce)
|
68
|
+
output = @buff
|
69
|
+
@buff = ''
|
70
|
+
# treat the output
|
71
|
+
output.slice!(-nonce.length..-1)
|
72
|
+
status = output.slice!(-4..-1)
|
73
|
+
raise "Status #{status} > 255?" if status.slice(0..0) != '0'
|
74
|
+
return output, status.to_i
|
75
|
+
end
|
76
|
+
|
77
|
+
def _run_block(cmd, opts)
|
78
|
+
@stdin.write("#{cmd}; printf '%04d#{nonce}' $?\n")
|
79
|
+
end
|
80
|
+
|
81
|
+
def _extend(path, suffix)
|
82
|
+
path = path.chomp('/') if path.end_with?('/')
|
83
|
+
return path + suffix
|
84
|
+
end
|
85
|
+
|
86
|
+
def _unlines(s)
|
87
|
+
return s.lines.map { |l| l.chomp("\n") }
|
88
|
+
end
|
89
|
+
|
90
|
+
def _escape(args)
|
91
|
+
return args.map { |x| "'#{x}'" }.join(' ')
|
92
|
+
end
|
93
|
+
|
94
|
+
# TESTING METHODS
|
95
|
+
|
96
|
+
def assert(condition, msg = 'Assertion error')
|
97
|
+
raise msg if condition != true
|
98
|
+
end
|
99
|
+
|
100
|
+
# PUBLIC METHODS
|
101
|
+
|
102
|
+
def export(name, value)
|
103
|
+
run("export #{name}=#{value}")
|
104
|
+
end
|
105
|
+
|
106
|
+
def run(cmd, opts = {})
|
107
|
+
out, status = _run(cmd, opts)
|
108
|
+
raise StatusError.new(cmd, status, out) if status != 0
|
109
|
+
return out
|
110
|
+
end
|
111
|
+
|
112
|
+
def run_status(cmd, opts = {})
|
113
|
+
out, status = _run(cmd, opts)
|
114
|
+
return status
|
115
|
+
end
|
116
|
+
|
117
|
+
def cd(path)
|
118
|
+
run("cd #{path}")
|
119
|
+
end
|
120
|
+
|
121
|
+
def ls
|
122
|
+
run("ls -1").lines.map { |line| line.chomp("\n") }
|
123
|
+
end
|
124
|
+
|
125
|
+
def pwd
|
126
|
+
run("pwd").strip
|
127
|
+
end
|
128
|
+
|
129
|
+
def untar(name, where = nil)
|
130
|
+
if where.nil?
|
131
|
+
run("tar xvf #{name}")
|
132
|
+
else
|
133
|
+
run("tar -C #{where} -xvf #{name}")
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def echo(text)
|
138
|
+
run("echo #{text}")
|
139
|
+
end
|
140
|
+
|
141
|
+
def bc(text)
|
142
|
+
run("echo '#{text}' | bc").strip
|
143
|
+
end
|
144
|
+
|
145
|
+
def cp(a, b)
|
146
|
+
run("cp #{a} #{b}")
|
147
|
+
end
|
148
|
+
|
149
|
+
def mv(a, b)
|
150
|
+
run("mv #{a} #{b}")
|
151
|
+
end
|
152
|
+
|
153
|
+
def rm(*args)
|
154
|
+
run("rm #{_escape(args)}")
|
155
|
+
end
|
156
|
+
|
157
|
+
def remove_dirs(path)
|
158
|
+
rm "-rf", path
|
159
|
+
end
|
160
|
+
|
161
|
+
def build
|
162
|
+
# builds a standard Unix software
|
163
|
+
run("./configure")
|
164
|
+
run("make")
|
165
|
+
end
|
166
|
+
|
167
|
+
def abspath(path)
|
168
|
+
run("readlink -f #{path}").strip
|
169
|
+
end
|
170
|
+
|
171
|
+
def build_tarball(tarball, path)
|
172
|
+
# builds a tarball containing a std Unix software
|
173
|
+
tarball = abspath(tarball)
|
174
|
+
path = abspath(path)
|
175
|
+
remove_dirs(path)
|
176
|
+
tmp = _extend(path, '-tmp')
|
177
|
+
remove_dirs(tmp)
|
178
|
+
make_dirs(tmp)
|
179
|
+
untar(tarball, tmp)
|
180
|
+
cd tmp
|
181
|
+
# we are in the temp dir
|
182
|
+
if exists('./configure')
|
183
|
+
cd '/'
|
184
|
+
mv tmp, path
|
185
|
+
else
|
186
|
+
ds = dirs()
|
187
|
+
raise 'Too many dirs?' if ds.length != 1
|
188
|
+
mv ds.first, path
|
189
|
+
cd '/'
|
190
|
+
remove_dirs(tmp)
|
191
|
+
end
|
192
|
+
cd path
|
193
|
+
build
|
194
|
+
end
|
195
|
+
|
196
|
+
def tmp_file
|
197
|
+
return run('mktemp').strip
|
198
|
+
end
|
199
|
+
|
200
|
+
def save_machines(machines, path = nil)
|
201
|
+
path = tmp_file if path.nil?
|
202
|
+
append_lines(path, machines.map { |m| m.to_s })
|
203
|
+
return path
|
204
|
+
end
|
205
|
+
|
206
|
+
def mpirun(machines, params)
|
207
|
+
machines = save_machines(machines) if machines.is_a?(Array)
|
208
|
+
return run("mpirun --mca btl ^openib -machinefile #{machines} #{params}")
|
209
|
+
end
|
210
|
+
|
211
|
+
def join(*args)
|
212
|
+
return File.join(*args)
|
213
|
+
end
|
214
|
+
|
215
|
+
def exists(path)
|
216
|
+
run_status("[[ -e #{path} ]]") == 0
|
217
|
+
end
|
218
|
+
|
219
|
+
def make_dirs(path)
|
220
|
+
run("mkdir -p #{path}")
|
221
|
+
end
|
222
|
+
|
223
|
+
def mkdir(path)
|
224
|
+
run("mkdir #{path}") unless exists(path) # TODO: this changes semantics of mkdir...
|
225
|
+
end
|
226
|
+
|
227
|
+
def files(ignore = true, type = 'f')
|
228
|
+
fs = run("find . -maxdepth 1 -type #{type}")
|
229
|
+
fs = _unlines(fs).reject { |f| f == '.' }.map { |f| f[2..-1] }
|
230
|
+
fs = fs.reject { |f| f.end_with?('~') or f.start_with?('.') } if ignore
|
231
|
+
return fs
|
232
|
+
end
|
233
|
+
|
234
|
+
def which(prog)
|
235
|
+
return run("which #{prog}").strip
|
236
|
+
end
|
237
|
+
|
238
|
+
def dirs(ignore = true)
|
239
|
+
return files(ignore, 'd')
|
240
|
+
end
|
241
|
+
|
242
|
+
def get_type(name)
|
243
|
+
return :dir if run_status("[[ -d #{name} ]]") == 0
|
244
|
+
return :file if run_status("[[ -f #{name} ]]") == 0
|
245
|
+
raise "'#{name}' is neither file nor directory"
|
246
|
+
end
|
247
|
+
|
248
|
+
def expand_path(path)
|
249
|
+
return run("echo #{path}").strip
|
250
|
+
end
|
251
|
+
|
252
|
+
def append_line(path, line)
|
253
|
+
return run("echo '#{line}' >> #{path}")
|
254
|
+
end
|
255
|
+
|
256
|
+
def append_lines(path, lines)
|
257
|
+
lines.each { |line|
|
258
|
+
append_line(path, line)
|
259
|
+
}
|
260
|
+
end
|
261
|
+
|
262
|
+
def contents(name)
|
263
|
+
run("cat #{name}")
|
264
|
+
end
|
265
|
+
|
266
|
+
alias cat contents
|
267
|
+
|
268
|
+
def hostname
|
269
|
+
return run('hostname').strip
|
270
|
+
end
|
271
|
+
|
272
|
+
def touch(path)
|
273
|
+
run("touch #{path}")
|
274
|
+
end
|
275
|
+
|
276
|
+
# BELOW ARE SPECIFIC METHODS FOR XPFLOW
|
277
|
+
|
278
|
+
|
279
|
+
def packages
|
280
|
+
list = run("dpkg -l")
|
281
|
+
list = _unlines(list).map do |p|
|
282
|
+
s, n, v = p.split
|
283
|
+
{ :status => s, :name => n, :version => v }
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def aptget(*args)
|
288
|
+
raise 'Command not given' if args.length == 0
|
289
|
+
cmd = args.first.to_sym
|
290
|
+
args = args.map { |x| x.to_s }.join(' ')
|
291
|
+
status = run_status("DEBIAN_FRONTEND=noninteractive apt-get -y #{args}")
|
292
|
+
if cmd == :purge and status == 100
|
293
|
+
return # ugly hack for the case when the package is not installed
|
294
|
+
end
|
295
|
+
raise StatusError.new('aptget', status, 'none') if status != 0
|
296
|
+
end
|
297
|
+
|
298
|
+
def distribute(path, dest, *nodes)
|
299
|
+
# distributes a file to the given nodes
|
300
|
+
nodes.flatten.each { |node|
|
301
|
+
run("scp -o 'StrictHostKeyChecking no' #{path} #{node}:#{dest}")
|
302
|
+
}
|
303
|
+
end
|
304
|
+
|
305
|
+
def glob(pattern)
|
306
|
+
out, status = _run("ls -1 #{pattern}", {})
|
307
|
+
return [] if status != 0
|
308
|
+
return out.strip.lines.map { |x| x.strip }
|
309
|
+
end
|
310
|
+
|
311
|
+
end
|
312
|
+
|
313
|
+
|
314
|
+
def self.bash(cmd = 'bash', debug = false, &block)
|
315
|
+
if not block_given?
|
316
|
+
sin, sout, serr, thr = Open3.popen3(cmd)
|
317
|
+
return Bash.new(sin, sout, debug)
|
318
|
+
end
|
319
|
+
# run bash interpreter using this command
|
320
|
+
result = nil
|
321
|
+
Open3.popen3(cmd) do |sin, sout, serr, thr|
|
322
|
+
dsl = Bash.new(sin, sout, debug)
|
323
|
+
dsl.cd('~') # go to the home dir
|
324
|
+
result = dsl.parse(&block)
|
325
|
+
end
|
326
|
+
return result
|
327
|
+
end
|
328
|
+
|
329
|
+
end; end
|
330
|
+
|
331
|
+
if __FILE__ == $0
|
332
|
+
Cute::Bash.bash("ssh localhost bash") do
|
333
|
+
cd '/tmp'
|
334
|
+
puts files.inspect
|
335
|
+
run 'rm /'
|
336
|
+
end
|
337
|
+
end
|