tsm 0.9
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.
- data/README +26 -0
- data/lib/tsm.rb +283 -0
- data/tests/ts_tsm.rb +21 -0
- metadata +48 -0
data/README
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
== Description
|
2
|
+
A wrapper for TSM dsmadmc and dsmc commands
|
3
|
+
|
4
|
+
== Prerequisites
|
5
|
+
* Ruby 1.8.4 or later
|
6
|
+
|
7
|
+
== Usage
|
8
|
+
TSM::Dsmadmc(id,pass) { |t|
|
9
|
+
t.add_observer(TestObserver.new())
|
10
|
+
t.command('q pr')
|
11
|
+
}
|
12
|
+
|
13
|
+
t = TSM::Dsmadmc(id,pass)
|
14
|
+
t.open
|
15
|
+
t.command('q pr')
|
16
|
+
t.command('quit')
|
17
|
+
t.open
|
18
|
+
t.command('select count(*) from volumes')
|
19
|
+
t.close
|
20
|
+
|
21
|
+
== Installation
|
22
|
+
=== Manual Installation
|
23
|
+
=== Gem Installation
|
24
|
+
gem install tsm-<version>.gem
|
25
|
+
|
26
|
+
== Notes
|
data/lib/tsm.rb
ADDED
@@ -0,0 +1,283 @@
|
|
1
|
+
|
2
|
+
require 'pty'
|
3
|
+
require 'observer'
|
4
|
+
require 'parsedate'
|
5
|
+
|
6
|
+
module TSM
|
7
|
+
class Error < RuntimeError
|
8
|
+
class FatalError < Error; end
|
9
|
+
class CommandError < Error; end
|
10
|
+
class PTYError < Error; end
|
11
|
+
|
12
|
+
class SqlError < Error
|
13
|
+
class NoData < SqlError; end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
class Command; end #:nodoc:
|
17
|
+
|
18
|
+
FORCE_OPT = {:displ => 'list',:out => true,:noc => true}
|
19
|
+
|
20
|
+
class Dsmadmc < Command
|
21
|
+
def initialize(id,password,opts = {},&block)
|
22
|
+
opts[:id] = id
|
23
|
+
opts[:pa] = password
|
24
|
+
|
25
|
+
@path = ['/opt/tivoli/tsm/client/ba/bin','/usr/tivoli/tsm/client/ba/bin']
|
26
|
+
|
27
|
+
super opts,&block
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Dsmc < Command
|
32
|
+
def initialize(opts = {},&block)
|
33
|
+
@path = ['/opt/tivoli/tsm/client/admin/bin','/usr/tivoli/tsm/client/ba/bin']
|
34
|
+
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Command
|
40
|
+
include Observable
|
41
|
+
|
42
|
+
attr_accessor :timeout
|
43
|
+
attr_reader :host, :header, :prompt
|
44
|
+
attr_reader :pty, :cmd
|
45
|
+
attr_accessor :path, :app
|
46
|
+
|
47
|
+
def initialize(opt = {},&block)
|
48
|
+
args = [find(self.class.to_s.split(":").last.downcase)]
|
49
|
+
|
50
|
+
opt.merge(FORCE_OPT).each{|k,v| args.push("-#{k.to_s}#{'=' << v.to_s if v.class != TrueClass}") }
|
51
|
+
opt.each_key do |k|
|
52
|
+
opt.each_key {|k2| opt.delete(k2) if k.to_s.casecmp(k2.to_s) == -1}
|
53
|
+
end
|
54
|
+
|
55
|
+
@cmd = args.join(" ")
|
56
|
+
|
57
|
+
yield self if block_given?
|
58
|
+
|
59
|
+
self
|
60
|
+
ensure
|
61
|
+
close if block_given? and not closed?
|
62
|
+
end
|
63
|
+
|
64
|
+
def open(&block)
|
65
|
+
return self unless closed?
|
66
|
+
|
67
|
+
r,w,pid = PTY.spawn(@cmd)
|
68
|
+
@header,@prompt,@host = r.prompt([/tsm: ([A-Z0-9]*)>/i,/Enter your password: /i],@timeout || 60)
|
69
|
+
Response.new(@header.to_s).validate(Error::CommandError) if @host.to_s.empty?
|
70
|
+
|
71
|
+
@established = Time.now()
|
72
|
+
@pty = {:r => r,:w => w,:pid => pid}
|
73
|
+
|
74
|
+
notify('open',{'host'=>@host})
|
75
|
+
|
76
|
+
yield self if block_given?
|
77
|
+
|
78
|
+
self
|
79
|
+
rescue PTY::ChildExited => details
|
80
|
+
ensure
|
81
|
+
close if block_given? and not closed?
|
82
|
+
end
|
83
|
+
|
84
|
+
def close
|
85
|
+
notify('close',{'closed' => closed?})
|
86
|
+
if @pty
|
87
|
+
@pty[:r].close unless @pty[:r].closed?
|
88
|
+
@pty[:w].close unless @pty[:w].closed?
|
89
|
+
end
|
90
|
+
@pty = nil
|
91
|
+
end
|
92
|
+
|
93
|
+
def closed?
|
94
|
+
return true unless @pty
|
95
|
+
return true if @pty[:r].closed? and @pty[:w].closed?
|
96
|
+
|
97
|
+
begin
|
98
|
+
Process.kill(0,@pty[:pid])
|
99
|
+
rescue Errno::ESRCH
|
100
|
+
return true
|
101
|
+
end
|
102
|
+
|
103
|
+
false
|
104
|
+
end
|
105
|
+
|
106
|
+
def command(cmd,&block)
|
107
|
+
cmd.to_s.strip!
|
108
|
+
|
109
|
+
notify('execute',{'command' => cmd,'closed'=>closed?})
|
110
|
+
|
111
|
+
begin
|
112
|
+
return self
|
113
|
+
ensure
|
114
|
+
close unless closed?
|
115
|
+
end if cmd =~ /quit*/i
|
116
|
+
|
117
|
+
raise Error::PTYError, 'No PTY' if closed?
|
118
|
+
|
119
|
+
@pty[:w].puts(cmd.to_s)
|
120
|
+
|
121
|
+
start = Time.now()
|
122
|
+
res = Response.new
|
123
|
+
@pty[:r].response(@prompt) do |line|
|
124
|
+
res.parse(line,&block)
|
125
|
+
end
|
126
|
+
duration = Time.now() - start
|
127
|
+
|
128
|
+
notify('complete',{'duration' => "%.2f" % duration,'rows' => res[:count] || 0})
|
129
|
+
|
130
|
+
res
|
131
|
+
rescue Error::PTYError
|
132
|
+
open
|
133
|
+
retry
|
134
|
+
end
|
135
|
+
|
136
|
+
def server
|
137
|
+
raise Error::PTYError, "No connection was established" unless @header
|
138
|
+
|
139
|
+
svr = {}
|
140
|
+
@header.each {|l|
|
141
|
+
l = l.chomp
|
142
|
+
l.strip!
|
143
|
+
next if l.empty?
|
144
|
+
|
145
|
+
if (m=/Command Line ([\S^-]+)([\s\S]+) - Version (\d+), Release (\d+), Level (\d(.\d))/i.match(l))
|
146
|
+
svr[:interface] = {:version => m[3],:release => m[4],:level =>m[5],:type => m[1].downcase}
|
147
|
+
elsif (m=/Session established with server (\w+): (\S+)/i.match(l))
|
148
|
+
svr[:host] = {:hostname => m[1],:arch => m[2]}
|
149
|
+
elsif (m=/Server Version (\d+), Release (\d+), Level (\d(.\d))/i.match(l))
|
150
|
+
svr[:version] = {:version => m[1],:release => m[2],:level => m[3]}
|
151
|
+
elsif (m=/Server date.time: ([0-9\/]+) ([0-9:]+)(\s+)Last access: ([0-9\/]+) ([0-9:]+)/i.match(l))
|
152
|
+
svr[:established] = Time.local(*(ParseDate.parsedate("#{m[1]} #{m[2]}")))
|
153
|
+
svr[:access] = Time.local(*(ParseDate.parsedate("#{m[4]} #{m[5]}")))
|
154
|
+
end
|
155
|
+
}
|
156
|
+
svr[:time] = @established if @established
|
157
|
+
|
158
|
+
svr
|
159
|
+
end
|
160
|
+
|
161
|
+
def path
|
162
|
+
@path || ['/usr/local/bin']
|
163
|
+
end
|
164
|
+
|
165
|
+
def path=(path)
|
166
|
+
@path = [path].flatten
|
167
|
+
end
|
168
|
+
|
169
|
+
protected
|
170
|
+
|
171
|
+
def notify(st,opts = {})
|
172
|
+
pid = @pty[:pid] if @pty
|
173
|
+
changed and notify_observers(opts.merge({'status' => st,'time' => Time.now,'pid' => pid || 0}))
|
174
|
+
end
|
175
|
+
|
176
|
+
def find(name)
|
177
|
+
(ENV['PATH'].split(File::PATH_SEPARATOR) + self.path).each { |p|
|
178
|
+
if File.executable?(f=File.join(p,name))
|
179
|
+
return f
|
180
|
+
end
|
181
|
+
}
|
182
|
+
end
|
183
|
+
|
184
|
+
class Response < Hash
|
185
|
+
def initialize(str=nil)
|
186
|
+
{:messages => [],:rows => [],:count => 0,:command => nil}.each do |k,v|
|
187
|
+
self.store(k,v)
|
188
|
+
end
|
189
|
+
|
190
|
+
self.parse(str) if str
|
191
|
+
|
192
|
+
self
|
193
|
+
end
|
194
|
+
|
195
|
+
def parse(chrs,&block)
|
196
|
+
chrs.each { |line|
|
197
|
+
line = line.chomp
|
198
|
+
line.strip!
|
199
|
+
|
200
|
+
if (m = /(([ABDEI][DEKNC][ERSDNOUIP])(\d+)([IESWK])) (.*)/.match(line))
|
201
|
+
self[:messages].push({:id=>m[1],:msg=>m[5]}) and next
|
202
|
+
end
|
203
|
+
|
204
|
+
self[:command] = line and next unless self[:command]
|
205
|
+
|
206
|
+
if line.empty? and not @tmp.empty?
|
207
|
+
self[:count] += 1
|
208
|
+
|
209
|
+
yield @tmp if block_given?
|
210
|
+
|
211
|
+
self[:rows].to_a.push(@tmp.dup)
|
212
|
+
@tmp.clear
|
213
|
+
|
214
|
+
next
|
215
|
+
end if @tmp
|
216
|
+
|
217
|
+
if (sp = line.index(":")) != nil
|
218
|
+
@tmp = {} unless @tmp
|
219
|
+
@tmp[line[0...sp].downcase.tr(" ","_").to_sym] = line[(sp+1)..-1].lstrip
|
220
|
+
end
|
221
|
+
}
|
222
|
+
|
223
|
+
chrs
|
224
|
+
end
|
225
|
+
|
226
|
+
def validate(err=Error::FatalError)
|
227
|
+
self[:messages].each do |msg|
|
228
|
+
raise Error::SqlError::NoData, msg[:msg] if msg[:id] =~ /ANR2034E/i
|
229
|
+
raise err, msg[:msg] if msg[:id][-1..-1] =~ /^[ES]$/i
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
class IO
|
237
|
+
def prompt(pat,timeout=9999999)
|
238
|
+
buf = ''
|
239
|
+
pat = [pat] unless pat.respond_to?('each')
|
240
|
+
result = nil
|
241
|
+
while true
|
242
|
+
return result if IO.select([self],nil,nil,timeout).nil? or eof?
|
243
|
+
begin
|
244
|
+
buf << getc.chr
|
245
|
+
rescue Errno::EIO
|
246
|
+
result = [buf,''] and break
|
247
|
+
end
|
248
|
+
|
249
|
+
pat.each { |p|
|
250
|
+
if mat=p.match(buf)
|
251
|
+
result = [buf,*mat.to_a] and break
|
252
|
+
end
|
253
|
+
}
|
254
|
+
break if result
|
255
|
+
end
|
256
|
+
result
|
257
|
+
end
|
258
|
+
|
259
|
+
def response(str,timeout=9999999,&block)
|
260
|
+
tbuf = '' and buf = ''
|
261
|
+
mode = :chr
|
262
|
+
while true
|
263
|
+
result = nil and break if IO.select([self],nil,nil,timeout).nil? or eof?
|
264
|
+
if mode == :chr
|
265
|
+
begin
|
266
|
+
tbuf << getc.chr
|
267
|
+
rescue Errno::EIO
|
268
|
+
result = result.to_s
|
269
|
+
result << tbuf and break
|
270
|
+
end
|
271
|
+
buf << tbuf and tbuf = '' and mode = :line if str[0...tbuf.size] != tbuf
|
272
|
+
buf << tbuf and result = buf and break if tbuf == str
|
273
|
+
buf << tbuf if tbuf[-($/.size)..-1] == $/
|
274
|
+
next
|
275
|
+
end
|
276
|
+
|
277
|
+
buf << gets and mode = :chr
|
278
|
+
yield buf
|
279
|
+
buf = ''
|
280
|
+
end
|
281
|
+
result
|
282
|
+
end
|
283
|
+
end
|
data/tests/ts_tsm.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'tsm'
|
2
|
+
require 'yaml'
|
3
|
+
require 'test/unit'
|
4
|
+
|
5
|
+
USER = YAML.load(File.open(File.expand_path("~/auth.yaml")))
|
6
|
+
|
7
|
+
class TestTsm < Test::Unit::TestCase
|
8
|
+
|
9
|
+
def test_simple
|
10
|
+
assert_raise(TSM::Error::CommandError) {
|
11
|
+
TSM::Dsmadmc.new(USER[:user],'pass') { |t|
|
12
|
+
t.open
|
13
|
+
}
|
14
|
+
}
|
15
|
+
assert_raise(TSM::Error::PTYError) {
|
16
|
+
TSM::Dsmadmc.new(USER[:user],USER[:pass]) { |t|
|
17
|
+
t.server
|
18
|
+
}
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.0
|
3
|
+
specification_version: 1
|
4
|
+
name: tsm
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: "0.9"
|
7
|
+
date: 2006-07-30 00:00:00 -05:00
|
8
|
+
summary: Wrapper for TSM client and administrative command interfaces
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: s.e.collison@gmail.com
|
12
|
+
homepage: http://tsm-command.rubyforge.org/
|
13
|
+
rubyforge_project:
|
14
|
+
description:
|
15
|
+
autorequire: tsm
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: "true"
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Sarah Collison
|
31
|
+
files:
|
32
|
+
- tests/ts_tsm.rb
|
33
|
+
- lib/tsm.rb
|
34
|
+
- README
|
35
|
+
test_files:
|
36
|
+
- tests/ts_tsm.rb
|
37
|
+
rdoc_options: []
|
38
|
+
|
39
|
+
extra_rdoc_files:
|
40
|
+
- README
|
41
|
+
executables: []
|
42
|
+
|
43
|
+
extensions: []
|
44
|
+
|
45
|
+
requirements: []
|
46
|
+
|
47
|
+
dependencies: []
|
48
|
+
|