ruby-cute 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|