tsm 0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README +26 -0
  2. data/lib/tsm.rb +283 -0
  3. data/tests/ts_tsm.rb +21 -0
  4. 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
@@ -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
@@ -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
+