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