ruby-iscsiadm 0.0.1
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.tar.gz.sig +0 -0
- data/History.txt +6 -0
- data/Manifest.txt +17 -0
- data/README.txt +51 -0
- data/README_SUDO.txt +10 -0
- data/Rakefile +14 -0
- data/Todo.txt +11 -0
- data/examples/example_session.rb +21 -0
- data/examples/show_nodes.rb +17 -0
- data/lib/iscsiadm.rb +294 -0
- data/lib/iscsiadm/common.rb +42 -0
- data/lib/iscsiadm/external.rb +41 -0
- data/lib/iscsiadm/iscsiadm.rb +309 -0
- data/lib/iscsiadm/node.rb +14 -0
- data/lib/iscsiadm/portal.rb +39 -0
- data/lib/iscsiadm/proc.rb +38 -0
- data/lib/iscsiadm/session.rb +14 -0
- data/test/test_iscsiadm.rb +0 -0
- metadata +107 -0
- metadata.gz.sig +2 -0
data.tar.gz.sig
ADDED
Binary file
|
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
README.txt
|
4
|
+
README_SUDO.txt
|
5
|
+
Rakefile
|
6
|
+
Todo.txt
|
7
|
+
examples/example_session.rb
|
8
|
+
examples/show_nodes.rb
|
9
|
+
lib/iscsiadm.rb
|
10
|
+
lib/iscsiadm/common.rb
|
11
|
+
lib/iscsiadm/external.rb
|
12
|
+
lib/iscsiadm/iscsiadm.rb
|
13
|
+
lib/iscsiadm/node.rb
|
14
|
+
lib/iscsiadm/portal.rb
|
15
|
+
lib/iscsiadm/proc.rb
|
16
|
+
lib/iscsiadm/session.rb
|
17
|
+
test/test_iscsiadm.rb
|
data/README.txt
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
= ruby-iscsiadm
|
2
|
+
|
3
|
+
* http://ruby-iscsiadm.rubyforge.org
|
4
|
+
* mailto:matt@bravenet.com
|
5
|
+
|
6
|
+
== DESCRIPTION:
|
7
|
+
|
8
|
+
This is a wrapper for the open-iscsi administration utility, iscsiadm. This is
|
9
|
+
a simple binary wrapper and considered a temporary solution until a proper api
|
10
|
+
is finished as per http://thread.gmane.org/gmane.linux.iscsi.open-iscsi/422
|
11
|
+
|
12
|
+
== FEATURES/PROBLEMS:
|
13
|
+
|
14
|
+
* Not finished, is that a problem?
|
15
|
+
|
16
|
+
== SYNOPSIS:
|
17
|
+
|
18
|
+
FIX (code sample of usage)
|
19
|
+
|
20
|
+
== REQUIREMENTS:
|
21
|
+
|
22
|
+
* popen4
|
23
|
+
|
24
|
+
== INSTALL:
|
25
|
+
|
26
|
+
* sudo gem install ruby-iscsiadm
|
27
|
+
|
28
|
+
== LICENSE:
|
29
|
+
|
30
|
+
(The MIT License)
|
31
|
+
|
32
|
+
Copyright (c) 2008 Matthew Kent, Bravenet Web Services Inc.
|
33
|
+
|
34
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
35
|
+
a copy of this software and associated documentation files (the
|
36
|
+
'Software'), to deal in the Software without restriction, including
|
37
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
38
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
39
|
+
permit persons to whom the Software is furnished to do so, subject to
|
40
|
+
the following conditions:
|
41
|
+
|
42
|
+
The above copyright notice and this permission notice shall be
|
43
|
+
included in all copies or substantial portions of the Software.
|
44
|
+
|
45
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
46
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
47
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
48
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
49
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
50
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
51
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README_SUDO.txt
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
Want to run this without using root?
|
2
|
+
|
3
|
+
1) Create an unprivileged user, preferably with the shell set to /sbin/nologin
|
4
|
+
2) Add user to sudoers, eg:
|
5
|
+
|
6
|
+
iscsidev ALL = NOPASSWD: /sbin/iscsiadm
|
7
|
+
|
8
|
+
3) Pass sudo in with the binary, eg:
|
9
|
+
|
10
|
+
Iscsiadm.new(:command => "/usr/bin/sudo /sbin/iscsiadm")
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
$:.unshift(File.dirname(__FILE__) + "/lib")
|
6
|
+
require 'iscsiadm'
|
7
|
+
|
8
|
+
Hoe.new('ruby-iscsiadm', IscsiadmWrapper::VERSION) do |p|
|
9
|
+
p.developer('Matthew Kent', 'matt@bravenet.com')
|
10
|
+
p.extra_deps << ['open4', '>= 0.9.6']
|
11
|
+
p.remote_rdoc_dir = ''
|
12
|
+
end
|
13
|
+
|
14
|
+
# vim: syntax=Ruby
|
data/Todo.txt
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
= to do
|
2
|
+
|
3
|
+
== 0.1.0
|
4
|
+
* don't bother wrapping complex operations, just provide a standard
|
5
|
+
iscsiadm.raw('--login etc'), that traps errors to do work at this level for
|
6
|
+
now.
|
7
|
+
* proper handling of interfaces
|
8
|
+
* implement session --stats
|
9
|
+
* proper documentation
|
10
|
+
* basic test suite
|
11
|
+
* compatibility check and warning, pass flag to shutup
|
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/ruby -w
|
2
|
+
|
3
|
+
$: << File.dirname(__FILE__) + "/../lib"
|
4
|
+
|
5
|
+
require 'iscsiadm'
|
6
|
+
|
7
|
+
iscsi = IscsiadmWrapper::Iscsiadm.new(:command => "/usr/bin/sudo /sbin/iscsiadm", :debug => true)
|
8
|
+
|
9
|
+
iscsi.discover(:ip => "20.20.20.20").each do |n|
|
10
|
+
puts n
|
11
|
+
if n.target =~ /shared.1/
|
12
|
+
p iscsi.show_record(:node => n, :name => 'node.session.auth.authmethod')
|
13
|
+
p iscsi.update_record(:node => n, :name => 'node.session.auth.authmethod', :value => 'CHAP')
|
14
|
+
p iscsi.update_record(:node => n, :name => 'node.session.auth.username', :value => 'shareddata')
|
15
|
+
p iscsi.update_record(:node => n, :name => 'node.session.auth.password', :value => 'xxx')
|
16
|
+
p iscsi.show_record(:node => n, :name => 'node.session.auth.authmethod')
|
17
|
+
p iscsi.login(:node => n)
|
18
|
+
p iscsi.logout(:node => n)
|
19
|
+
p iscsi.show_config(:node => n)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/ruby -w
|
2
|
+
|
3
|
+
$: << File.dirname(__FILE__) + "/../lib"
|
4
|
+
|
5
|
+
require 'iscsiadm'
|
6
|
+
|
7
|
+
iscsi = IscsiadmWrapper::Iscsiadm.new(:command => "/usr/bin/sudo /sbin/iscsiadm")
|
8
|
+
|
9
|
+
puts "nodes"
|
10
|
+
iscsi.list_nodes.each do |n|
|
11
|
+
puts "- #{n.target}"
|
12
|
+
end
|
13
|
+
|
14
|
+
puts "\nsessions"
|
15
|
+
iscsi.list_sessions.each do |n|
|
16
|
+
puts "- #{n.target}"
|
17
|
+
end
|
data/lib/iscsiadm.rb
ADDED
@@ -0,0 +1,294 @@
|
|
1
|
+
# FIXME: proper rdoc summary here
|
2
|
+
#
|
3
|
+
# This is a wrapper for the open-iscsi administration utility, iscsiadm.
|
4
|
+
#
|
5
|
+
# Overview:
|
6
|
+
#
|
7
|
+
# The basic iscsiadm on disk is structure is
|
8
|
+
# - discovery (/var/lib/iscsi/send_targets on CentOS)
|
9
|
+
# portals which have been scanned
|
10
|
+
# - node (/var/lib/iscsi/send_targets on CentOS)
|
11
|
+
# settings records containing target,ip,and some params
|
12
|
+
|
13
|
+
require 'iscsiadm/common'
|
14
|
+
require 'iscsiadm/portal'
|
15
|
+
require 'iscsiadm/node'
|
16
|
+
require 'iscsiadm/session'
|
17
|
+
|
18
|
+
module IscsiadmWrapper
|
19
|
+
VERSION = '0.0.1'
|
20
|
+
COMPAT_VERSION = '2.0-865'
|
21
|
+
|
22
|
+
class Iscsiadm
|
23
|
+
DEFAULT_COMMAND = '/sbin/iscsiadm'
|
24
|
+
|
25
|
+
DEFAULT_TYPE = 'sendtargets'
|
26
|
+
DEFAULT_HOST = 'localhost'
|
27
|
+
DEFAULT_PORT = 3260
|
28
|
+
DEFAULT_INTERFACE = 1
|
29
|
+
|
30
|
+
COMMANDS = {
|
31
|
+
# misc
|
32
|
+
:version => { :cmd => "%s --version",
|
33
|
+
:filter => %r{^iscsiadm version (.*)$} },
|
34
|
+
# discovery mode
|
35
|
+
:discover => { :cmd => "%s --mode discovery --type %s --portal %s:%s",
|
36
|
+
:filter => %r{^.* (.*)$} },
|
37
|
+
:list_portals => { :cmd => "%s --mode discovery",
|
38
|
+
:filter => %r{^(.*) via (.*)$} },
|
39
|
+
# node mode
|
40
|
+
:login => { :cmd => "%s --mode node --target %s --portal %s --login",
|
41
|
+
:filter => %r{^iscsiadm version (.*)$} },
|
42
|
+
:logout => { :cmd => "%s --mode node --target %s --portal %s --logout",
|
43
|
+
:filter => %r{^iscsiadm version (.*)$} },
|
44
|
+
:list_nodes => { :cmd => "%s --mode node",
|
45
|
+
:filter => %r{^(.*) (.*)$} },
|
46
|
+
# session mode
|
47
|
+
:list_sessions => { :cmd => "%s --mode session",
|
48
|
+
:filter => %r{^tcp: \[(\d+)\] (.*) (.*)$} },
|
49
|
+
# config db
|
50
|
+
:show_config => { :cmd => "%s --mode node --target %s --portal %s --op show",
|
51
|
+
:filter => %r{^(.*) = (.*)$} },
|
52
|
+
:new_record => { :cmd => "%s --mode node --target %s --portal %s --op new --name %s --value %s",
|
53
|
+
:filter => %r{^(.*)$} },
|
54
|
+
:delete_record => { :cmd => "%s --mode node --target %s --portal %s --op delete --name %s",
|
55
|
+
:filter => %r{^(.*)$} },
|
56
|
+
:update_record => { :cmd => "%s --mode node --target %s --portal %s --op update --name %s --value %s",
|
57
|
+
:filter => %r{^(.*)$} },
|
58
|
+
# iscsiadm doesn't support this one really, see below
|
59
|
+
:show_record => { :cmd => "%s --mode node --target %s --portal %s --op show --name %s",
|
60
|
+
:filter => %r{^.* = (.*)$} },
|
61
|
+
}
|
62
|
+
|
63
|
+
def initialize(args = {})
|
64
|
+
@command = args[:command] || DEFAULT_COMMAND
|
65
|
+
@use_unsupported_version = args[:use_unsupported_version] || false
|
66
|
+
@debug = args[:debug] || false
|
67
|
+
|
68
|
+
puts "Running in debug mode, outputting commands to stdout." if @debug
|
69
|
+
end
|
70
|
+
|
71
|
+
def refresh_all
|
72
|
+
refresh_sessions
|
73
|
+
refresh_nodes
|
74
|
+
refresh_portals
|
75
|
+
end
|
76
|
+
def refresh_sessions; @sessions = list_sessions; end
|
77
|
+
def refresh_nodes; @nodes = list_nodes; end
|
78
|
+
def refresh_portals; @portals = list_portals; end
|
79
|
+
def sessions; refresh_sessions; @sessions; end
|
80
|
+
def nodes; refresh_nodes; @nodes; end
|
81
|
+
def portals; refresh_portals; @portals; end
|
82
|
+
|
83
|
+
def session(args = {})
|
84
|
+
target = args[:node].target
|
85
|
+
portal = args[:node].portal
|
86
|
+
|
87
|
+
refresh_sessions
|
88
|
+
|
89
|
+
@sessions.each do |s|
|
90
|
+
return s if s.target == target && s.portal == portal
|
91
|
+
end
|
92
|
+
end
|
93
|
+
def session_established?(args = {})
|
94
|
+
target = args[:node].target
|
95
|
+
portal = args[:node].portal
|
96
|
+
|
97
|
+
refresh_sessions
|
98
|
+
|
99
|
+
@sessions.each do |s|
|
100
|
+
return true if s.target == target && s.portal == portal
|
101
|
+
end
|
102
|
+
false
|
103
|
+
end
|
104
|
+
|
105
|
+
# Query iscsiadm version, returning resulting String
|
106
|
+
def version
|
107
|
+
Common::callout(
|
108
|
+
COMMANDS[:version][:cmd] % @command,
|
109
|
+
COMMANDS[:version][:filter]).to_s
|
110
|
+
end
|
111
|
+
|
112
|
+
# Discovery Mode
|
113
|
+
#
|
114
|
+
|
115
|
+
# Runs iscsi discovery against a target portal, returing resulting Array of
|
116
|
+
# Node objects.
|
117
|
+
def discover(args = {})
|
118
|
+
type = args[:type] || DEFAULT_TYPE
|
119
|
+
ip = args[:ip] || DEFAULT_HOST
|
120
|
+
port = args[:port] || DEFAULT_PORT
|
121
|
+
interface = args[:interface] || DEFAULT_INTERFACE
|
122
|
+
|
123
|
+
Common::callout(
|
124
|
+
COMMANDS[:discover][:cmd] % [@command, type, ip, port],
|
125
|
+
COMMANDS[:discover][:filter], @debug)
|
126
|
+
|
127
|
+
refresh_portals
|
128
|
+
refresh_nodes
|
129
|
+
|
130
|
+
list_nodes("#{ip}:#{port},#{interface}")
|
131
|
+
end
|
132
|
+
|
133
|
+
# Get list of portals, returning resulting Array of Portal objects.
|
134
|
+
def list_portals
|
135
|
+
ret = []
|
136
|
+
|
137
|
+
Common::callout(
|
138
|
+
COMMANDS[:list_portals][:cmd] % [@command],
|
139
|
+
COMMANDS[:list_portals][:filter], @debug) do |p,t|
|
140
|
+
|
141
|
+
ret << Portal.parse(p,t)
|
142
|
+
end
|
143
|
+
|
144
|
+
ret
|
145
|
+
end
|
146
|
+
|
147
|
+
# Node Mode
|
148
|
+
#
|
149
|
+
|
150
|
+
# Attempt to login to a given node and portal, returning resulting Session
|
151
|
+
# if successful.
|
152
|
+
def login(args = {})
|
153
|
+
target = args[:node].target
|
154
|
+
portal = args[:node].portal
|
155
|
+
|
156
|
+
Common::callout(
|
157
|
+
COMMANDS[:login][:cmd] % [@command, target, portal],
|
158
|
+
COMMANDS[:login][:filter], @debug)
|
159
|
+
|
160
|
+
session(args)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Logout of a given node and portal, returning resulting Array of remaining
|
164
|
+
# Sessions.
|
165
|
+
def logout(args = {})
|
166
|
+
target = args[:node].target
|
167
|
+
portal = args[:node].portal
|
168
|
+
|
169
|
+
Common::callout(
|
170
|
+
COMMANDS[:logout][:cmd] % [@command, target, portal],
|
171
|
+
COMMANDS[:logout][:filter], @debug)
|
172
|
+
|
173
|
+
refresh_sessions
|
174
|
+
end
|
175
|
+
|
176
|
+
# Get list of nodes, returning resulting Array of Node objects.
|
177
|
+
def list_nodes(args = {})
|
178
|
+
portal = args[:portal]
|
179
|
+
ret = []
|
180
|
+
|
181
|
+
Common::callout(
|
182
|
+
COMMANDS[:list_nodes][:cmd] % [@command],
|
183
|
+
COMMANDS[:list_nodes][:filter], @debug) do |p,t|
|
184
|
+
|
185
|
+
next if portal.nil? == false and portal != p
|
186
|
+
|
187
|
+
ret << Node.new(:portal => p, :target => t)
|
188
|
+
end
|
189
|
+
|
190
|
+
ret
|
191
|
+
end
|
192
|
+
|
193
|
+
# Session Mode
|
194
|
+
#
|
195
|
+
|
196
|
+
# Get list of sessions, returning resulting Array of Session objects.
|
197
|
+
def list_sessions(args = {})
|
198
|
+
portal = args[:portal]
|
199
|
+
ret = []
|
200
|
+
|
201
|
+
Common::callout(
|
202
|
+
COMMANDS[:list_sessions][:cmd] % [@command],
|
203
|
+
COMMANDS[:list_sessions][:filter], @debug) do |r,p,t|
|
204
|
+
|
205
|
+
next if portal.nil? == false and portal != p
|
206
|
+
|
207
|
+
ret << Session.new(:portal => p, :target => t, :id => r)
|
208
|
+
end
|
209
|
+
|
210
|
+
ret
|
211
|
+
end
|
212
|
+
|
213
|
+
# Node Config Mode
|
214
|
+
#
|
215
|
+
|
216
|
+
# Get configuration for a given node and portal, returning resulting Hash of
|
217
|
+
# record values.
|
218
|
+
def show_config(args = {})
|
219
|
+
target = args[:node].target
|
220
|
+
portal = args[:node].portal
|
221
|
+
|
222
|
+
conf = {}
|
223
|
+
|
224
|
+
Common::callout(
|
225
|
+
COMMANDS[:show_config][:cmd] % [@command, target, portal],
|
226
|
+
COMMANDS[:show_config][:filter], @debug) do |k,v|
|
227
|
+
|
228
|
+
# easier to deal with nil
|
229
|
+
conf[k] = (v == '<empty>' ? nil : v)
|
230
|
+
end
|
231
|
+
|
232
|
+
conf
|
233
|
+
end
|
234
|
+
|
235
|
+
# Delete a named config entry for a given target
|
236
|
+
# Returns ?
|
237
|
+
#
|
238
|
+
# Should not be used on a running session so says the man page.
|
239
|
+
def delete_record(args = {})
|
240
|
+
target = args[:node].target
|
241
|
+
portal = args[:node].portal
|
242
|
+
name = args[:name]
|
243
|
+
|
244
|
+
Common::callout(
|
245
|
+
COMMANDS[:delete_record][:cmd] % [@command, target, portal, name],
|
246
|
+
COMMANDS[:delete_record][:filter], @debug)
|
247
|
+
end
|
248
|
+
|
249
|
+
# Create a new config entry for a given target
|
250
|
+
# Returns ?
|
251
|
+
#
|
252
|
+
# Why would you want to create new records again..?
|
253
|
+
def new_record(args = {})
|
254
|
+
target = args[:node].target
|
255
|
+
portal = args[:node].portal
|
256
|
+
name = args[:name]
|
257
|
+
value = args[:value]
|
258
|
+
|
259
|
+
Common::callout(
|
260
|
+
COMMANDS[:new_record][:cmd] % [@command, target, portal, name, value],
|
261
|
+
COMMANDS[:new_record][:filter], @debug)
|
262
|
+
end
|
263
|
+
|
264
|
+
# Update a named config entry for a given target
|
265
|
+
# Returns ?
|
266
|
+
#
|
267
|
+
def update_record(args = {})
|
268
|
+
target = args[:node].target
|
269
|
+
portal = args[:node].portal
|
270
|
+
name = args[:name]
|
271
|
+
value = args[:value]
|
272
|
+
|
273
|
+
Common::callout(
|
274
|
+
COMMANDS[:update_record][:cmd] % [@command, target, portal, name, value],
|
275
|
+
COMMANDS[:update_record][:filter], @debug)
|
276
|
+
end
|
277
|
+
|
278
|
+
# Takes a Node object and entry name as args.
|
279
|
+
#
|
280
|
+
# Returns String.
|
281
|
+
#
|
282
|
+
def show_record(args = {})
|
283
|
+
target = args[:node].target
|
284
|
+
portal = args[:node].portal
|
285
|
+
name = args[:name]
|
286
|
+
|
287
|
+
# iscsiadm doesn't support show by specific name, fake it
|
288
|
+
Common::callout(
|
289
|
+
COMMANDS[:show_record][:cmd] % [@command, target, portal, name],
|
290
|
+
%r{#{name} = (.*)$}, @debug)
|
291
|
+
end
|
292
|
+
|
293
|
+
end # class
|
294
|
+
end # module
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'iscsiadm/external'
|
2
|
+
|
3
|
+
module IscsiadmWrapper
|
4
|
+
class Common
|
5
|
+
|
6
|
+
class<<self
|
7
|
+
# Convert match data to array, dropping the match all.
|
8
|
+
# Returns an array of matches.
|
9
|
+
#
|
10
|
+
def parse(line, filter) #:nodoc:
|
11
|
+
match = filter.match(line).to_a
|
12
|
+
match.shift
|
13
|
+
match
|
14
|
+
end
|
15
|
+
|
16
|
+
# Invoke an external program, passing each line of returned output through
|
17
|
+
# our filter.
|
18
|
+
#
|
19
|
+
# Yield each all matches per line in block or return an array of all matches on every line.
|
20
|
+
#
|
21
|
+
def callout(cmd, filter, debug=false) #:nodoc:
|
22
|
+
puts "Executing `#{cmd}`" if debug
|
23
|
+
|
24
|
+
ret = []
|
25
|
+
External::cmd(cmd) do |line|
|
26
|
+
match = parse(line, filter)
|
27
|
+
match.each do |t|
|
28
|
+
ret << t
|
29
|
+
end
|
30
|
+
if defined? yield
|
31
|
+
yield ret
|
32
|
+
ret.clear
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
ret
|
37
|
+
end
|
38
|
+
|
39
|
+
end # self
|
40
|
+
|
41
|
+
end # class
|
42
|
+
end # module
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'open4'
|
3
|
+
|
4
|
+
module IscsiadmWrapper
|
5
|
+
class External
|
6
|
+
class ExternalFailure < RuntimeError; end
|
7
|
+
|
8
|
+
class<<self
|
9
|
+
# Execute a command, returning the resulting String of standard output.
|
10
|
+
#
|
11
|
+
# The block is optional. If given, it will be invoked for each line of
|
12
|
+
# output.
|
13
|
+
def cmd(cmd)
|
14
|
+
output = []
|
15
|
+
error = nil
|
16
|
+
stat = Open4::popen4(cmd) do |pid, stdin, stdout, stderr|
|
17
|
+
while line = stdout.gets
|
18
|
+
output << line
|
19
|
+
end
|
20
|
+
error = stderr.read.strip
|
21
|
+
end
|
22
|
+
if stat.exited?
|
23
|
+
if stat.exitstatus > 0
|
24
|
+
raise ExternalFailure, "Fatal error, `#{cmd}` returned #{stat.exitstatus} with '#{error}'"
|
25
|
+
end
|
26
|
+
elsif stat.signaled?
|
27
|
+
raise ExternalFailure, "Fatal error, `#{cmd}` got signal #{stat.termsig} and terminated"
|
28
|
+
elsif stat.stopped?
|
29
|
+
raise ExternalFailure, "Fatal error, `#{cmd}` got signal #{stat.stopsig} and is stopped"
|
30
|
+
end
|
31
|
+
|
32
|
+
if block_given?
|
33
|
+
return output.each { |l| yield l }
|
34
|
+
else
|
35
|
+
return output.join
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end # class
|
41
|
+
end # module
|
@@ -0,0 +1,309 @@
|
|
1
|
+
module IscsiadmWrapper
|
2
|
+
# This is a wrapper for the open-iscsi administration utility, iscsiadm.
|
3
|
+
#
|
4
|
+
# Overview:
|
5
|
+
#
|
6
|
+
# The basic iscsiadm on disk is structure is
|
7
|
+
# - discovery (/var/lib/iscsi/send_targets on CentOS)
|
8
|
+
# portals which have been scanned
|
9
|
+
# - node (/var/lib/iscsi/send_targets on CentOS)
|
10
|
+
# settings records containing target,ip,and some params
|
11
|
+
|
12
|
+
require 'iscsiadm/common'
|
13
|
+
require 'iscsiadm/portal'
|
14
|
+
require 'iscsiadm/node'
|
15
|
+
require 'iscsiadm/session'
|
16
|
+
|
17
|
+
class Iscsiadm
|
18
|
+
COMPAT_VERSION = '2.0-865'
|
19
|
+
DEFAULT_BINARY = '/sbin/iscsiadm'
|
20
|
+
|
21
|
+
DEFAULT_TYPE = 'sendtargets'
|
22
|
+
DEFAULT_HOST = 'localhost'
|
23
|
+
DEFAULT_PORT = 3260
|
24
|
+
DEFAULT_INTERFACE = 1
|
25
|
+
|
26
|
+
COMMANDS = {
|
27
|
+
# misc
|
28
|
+
:version => { :cmd => "%s --version",
|
29
|
+
:filter => %r{^iscsiadm version (.*)$} },
|
30
|
+
# discovery mode
|
31
|
+
:discover => { :cmd => "%s --mode discovery --type %s --portal %s:%s",
|
32
|
+
:filter => %r{^.* (.*)$} },
|
33
|
+
:list_portals => { :cmd => "%s --mode discovery",
|
34
|
+
:filter => %r{^(.*) via (.*)$} },
|
35
|
+
# node mode
|
36
|
+
:login => { :cmd => "%s --mode node --target %s --portal %s --login",
|
37
|
+
:filter => %r{^iscsiadm version (.*)$} },
|
38
|
+
:logout => { :cmd => "%s --mode node --target %s --portal %s --logout",
|
39
|
+
:filter => %r{^iscsiadm version (.*)$} },
|
40
|
+
:list_nodes => { :cmd => "%s --mode node",
|
41
|
+
:filter => %r{^(.*) (.*)$} },
|
42
|
+
# session mode
|
43
|
+
:list_sessions => { :cmd => "%s --mode session",
|
44
|
+
:filter => %r{^tcp: \[(\d+)\] (.*) (.*)$} },
|
45
|
+
# config db
|
46
|
+
:show_config => { :cmd => "%s --mode node --target %s --portal %s --op show",
|
47
|
+
:filter => %r{^(.*) = (.*)$} },
|
48
|
+
:new_record => { :cmd => "%s --mode node --target %s --portal %s --op new --name %s --value %s",
|
49
|
+
:filter => %r{^(.*)$} },
|
50
|
+
:delete_record => { :cmd => "%s --mode node --target %s --portal %s --op delete --name %s",
|
51
|
+
:filter => %r{^(.*)$} },
|
52
|
+
:update_record => { :cmd => "%s --mode node --target %s --portal %s --op update --name %s --value %s",
|
53
|
+
:filter => %r{^(.*)$} },
|
54
|
+
# iscsiadm doesn't support this one really, see below
|
55
|
+
:show_record => { :cmd => "%s --mode node --target %s --portal %s --op show --name %s",
|
56
|
+
:filter => %r{^.* = (.*)$} },
|
57
|
+
}
|
58
|
+
|
59
|
+
# Class Methods
|
60
|
+
#
|
61
|
+
class<<self
|
62
|
+
end
|
63
|
+
|
64
|
+
# Instance Methods
|
65
|
+
#
|
66
|
+
def initialize(args = {})
|
67
|
+
@iscsiadm_binary = args[:iscsiadm_binary] || DEFAULT_BINARY[:iscsiadm]
|
68
|
+
@use_unsupported_version = args[:use_unsupported_version] || false
|
69
|
+
@debug = args[:debug] || false
|
70
|
+
|
71
|
+
puts "Running in debug mode, outputting commands to stdout." if @debug
|
72
|
+
end
|
73
|
+
|
74
|
+
# Misc
|
75
|
+
#
|
76
|
+
|
77
|
+
def refresh_all
|
78
|
+
refresh_sessions
|
79
|
+
refresh_nodes
|
80
|
+
refresh_portals
|
81
|
+
end
|
82
|
+
def refresh_sessions; @sessions = list_sessions; end
|
83
|
+
def refresh_nodes; @nodes = list_nodes; end
|
84
|
+
def refresh_portals; @portals = list_portals; end
|
85
|
+
def sessions; refresh_sessions; @sessions; end
|
86
|
+
def nodes; refresh_nodes; @nodes; end
|
87
|
+
def portals; refresh_portals; @portals; end
|
88
|
+
|
89
|
+
def session(args = {})
|
90
|
+
target = args[:node].target
|
91
|
+
portal = args[:node].portal
|
92
|
+
|
93
|
+
refresh_sessions
|
94
|
+
|
95
|
+
@sessions.each do |s|
|
96
|
+
return s if s.target == target && s.portal == portal
|
97
|
+
end
|
98
|
+
end
|
99
|
+
def session_established?(args = {})
|
100
|
+
target = args[:node].target
|
101
|
+
portal = args[:node].portal
|
102
|
+
|
103
|
+
refresh_sessions
|
104
|
+
|
105
|
+
@sessions.each do |s|
|
106
|
+
return true if s.target == target && s.portal == portal
|
107
|
+
end
|
108
|
+
false
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns iscsiadm's version
|
112
|
+
#
|
113
|
+
def version
|
114
|
+
Common::callout(
|
115
|
+
COMMANDS[:version][:cmd] % @iscsiadm_binary,
|
116
|
+
COMMANDS[:version][:filter]).to_s
|
117
|
+
end
|
118
|
+
|
119
|
+
# Discovery Mode
|
120
|
+
#
|
121
|
+
|
122
|
+
# Runs iscsi discovery against a target portal
|
123
|
+
# Returns nodes.
|
124
|
+
#
|
125
|
+
def discover(args = {})
|
126
|
+
type = args[:type] || DEFAULT_TYPE
|
127
|
+
ip = args[:ip] || DEFAULT_HOST
|
128
|
+
port = args[:port] || DEFAULT_PORT
|
129
|
+
interface = args[:interface] || DEFAULT_INTERFACE
|
130
|
+
|
131
|
+
Common::callout(
|
132
|
+
COMMANDS[:discover][:cmd] % [@iscsiadm_binary, type, ip, port],
|
133
|
+
COMMANDS[:discover][:filter], @debug)
|
134
|
+
|
135
|
+
refresh_portals
|
136
|
+
refresh_nodes
|
137
|
+
|
138
|
+
list_nodes("#{ip}:#{port},#{interface}")
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns an array of portal objects.
|
142
|
+
#
|
143
|
+
def list_portals
|
144
|
+
ret = []
|
145
|
+
|
146
|
+
Common::callout(
|
147
|
+
COMMANDS[:list_portals][:cmd] % [@iscsiadm_binary],
|
148
|
+
COMMANDS[:list_portals][:filter], @debug) do |p,t|
|
149
|
+
|
150
|
+
ret << Portal.parse(p,t)
|
151
|
+
end
|
152
|
+
|
153
|
+
ret
|
154
|
+
end
|
155
|
+
|
156
|
+
# Node Mode
|
157
|
+
#
|
158
|
+
|
159
|
+
# Attempt to login to a given node.
|
160
|
+
# Returns session
|
161
|
+
#
|
162
|
+
def login(args = {})
|
163
|
+
target = args[:node].target
|
164
|
+
portal = args[:node].portal
|
165
|
+
|
166
|
+
Common::callout(
|
167
|
+
COMMANDS[:login][:cmd] % [@iscsiadm_binary, target, portal],
|
168
|
+
COMMANDS[:login][:filter], @debug)
|
169
|
+
|
170
|
+
session(args)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Attempt to login to a given node.
|
174
|
+
# Returns ?
|
175
|
+
#
|
176
|
+
def logout(args = {})
|
177
|
+
target = args[:node].target
|
178
|
+
portal = args[:node].portal
|
179
|
+
|
180
|
+
Common::callout(
|
181
|
+
COMMANDS[:logout][:cmd] % [@iscsiadm_binary, target, portal],
|
182
|
+
COMMANDS[:logout][:filter], @debug)
|
183
|
+
|
184
|
+
refresh_sessions
|
185
|
+
end
|
186
|
+
|
187
|
+
# Returns an array of Node objects. Return all by
|
188
|
+
# default
|
189
|
+
#
|
190
|
+
def list_nodes(args = {})
|
191
|
+
portal = args[:portal]
|
192
|
+
ret = []
|
193
|
+
|
194
|
+
Common::callout(
|
195
|
+
COMMANDS[:list_nodes][:cmd] % [@iscsiadm_binary],
|
196
|
+
COMMANDS[:list_nodes][:filter], @debug) do |p,t|
|
197
|
+
|
198
|
+
next if portal.nil? == false and portal != p
|
199
|
+
|
200
|
+
ret << Node.new(:portal => p, :target => t)
|
201
|
+
end
|
202
|
+
|
203
|
+
ret
|
204
|
+
end
|
205
|
+
|
206
|
+
# Session Mode
|
207
|
+
#
|
208
|
+
|
209
|
+
# Returns an array of Session objects. Return all by
|
210
|
+
# default.
|
211
|
+
#
|
212
|
+
def list_sessions(args = {})
|
213
|
+
portal = args[:portal]
|
214
|
+
ret = []
|
215
|
+
|
216
|
+
Common::callout(
|
217
|
+
COMMANDS[:list_sessions][:cmd] % [@iscsiadm_binary],
|
218
|
+
COMMANDS[:list_sessions][:filter], @debug) do |r,p,t|
|
219
|
+
|
220
|
+
next if portal.nil? == false and portal != p
|
221
|
+
|
222
|
+
ret << Session.new(:portal => p, :target => t, :id => r)
|
223
|
+
end
|
224
|
+
|
225
|
+
ret
|
226
|
+
end
|
227
|
+
|
228
|
+
# Node Config Mode
|
229
|
+
#
|
230
|
+
|
231
|
+
# Returns a hash of all config key => value pairs for a given target
|
232
|
+
#
|
233
|
+
def show_config(args = {})
|
234
|
+
target = args[:node].target
|
235
|
+
portal = args[:node].portal
|
236
|
+
|
237
|
+
conf = {}
|
238
|
+
|
239
|
+
Common::callout(
|
240
|
+
COMMANDS[:show_config][:cmd] % [@iscsiadm_binary, target, portal],
|
241
|
+
COMMANDS[:show_config][:filter], @debug) do |k,v|
|
242
|
+
|
243
|
+
# easier to deal with nil
|
244
|
+
conf[k] = (v == '<empty>' ? nil : v)
|
245
|
+
end
|
246
|
+
|
247
|
+
conf
|
248
|
+
end
|
249
|
+
|
250
|
+
# Delete a named config entry for a given target
|
251
|
+
# Returns ?
|
252
|
+
#
|
253
|
+
# Should not be used on a running session so says the man page.
|
254
|
+
def delete_record(args = {})
|
255
|
+
target = args[:node].target
|
256
|
+
portal = args[:node].portal
|
257
|
+
name = args[:name]
|
258
|
+
|
259
|
+
Common::callout(
|
260
|
+
COMMANDS[:delete_record][:cmd] % [@iscsiadm_binary, target, portal, name],
|
261
|
+
COMMANDS[:delete_record][:filter], @debug)
|
262
|
+
end
|
263
|
+
|
264
|
+
# Create a new config entry for a given target
|
265
|
+
# Returns ?
|
266
|
+
#
|
267
|
+
# Why would you want to create new records again..?
|
268
|
+
def new_record(args = {})
|
269
|
+
target = args[:node].target
|
270
|
+
portal = args[:node].portal
|
271
|
+
name = args[:name]
|
272
|
+
value = args[:value]
|
273
|
+
|
274
|
+
Common::callout(
|
275
|
+
COMMANDS[:new_record][:cmd] % [@iscsiadm_binary, target, portal, name, value],
|
276
|
+
COMMANDS[:new_record][:filter], @debug)
|
277
|
+
end
|
278
|
+
|
279
|
+
# Update a named config entry for a given target
|
280
|
+
# Returns ?
|
281
|
+
#
|
282
|
+
def update_record(args = {})
|
283
|
+
target = args[:node].target
|
284
|
+
portal = args[:node].portal
|
285
|
+
name = args[:name]
|
286
|
+
value = args[:value]
|
287
|
+
|
288
|
+
Common::callout(
|
289
|
+
COMMANDS[:update_record][:cmd] % [@iscsiadm_binary, target, portal, name, value],
|
290
|
+
COMMANDS[:update_record][:filter], @debug)
|
291
|
+
end
|
292
|
+
|
293
|
+
# Takes a Node object and entry name as args.
|
294
|
+
#
|
295
|
+
# Returns String.
|
296
|
+
#
|
297
|
+
def show_record(args = {})
|
298
|
+
target = args[:node].target
|
299
|
+
portal = args[:node].portal
|
300
|
+
name = args[:name]
|
301
|
+
|
302
|
+
# iscsiadm doesn't support show by specific name, fake it
|
303
|
+
Common::callout(
|
304
|
+
COMMANDS[:show_record][:cmd] % [@iscsiadm_binary, target, portal, name],
|
305
|
+
%r{#{name} = (.*)$}, @debug)
|
306
|
+
end
|
307
|
+
|
308
|
+
end
|
309
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'iscsiadm/common'
|
2
|
+
require 'iscsiadm/portal'
|
3
|
+
|
4
|
+
module IscsiadmWrapper
|
5
|
+
class Node
|
6
|
+
attr_reader :portal, :target
|
7
|
+
|
8
|
+
def initialize(args = {})
|
9
|
+
@portal = Portal.parse(args[:portal])
|
10
|
+
@target = args[:target]
|
11
|
+
end
|
12
|
+
|
13
|
+
end # class
|
14
|
+
end # module
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'iscsiadm/common'
|
2
|
+
|
3
|
+
module IscsiadmWrapper
|
4
|
+
class Portal
|
5
|
+
attr_reader :ip, :port, :interface, :type
|
6
|
+
|
7
|
+
DEFAULT_TYPE = 'sendtargets'
|
8
|
+
|
9
|
+
# Class Methods
|
10
|
+
#
|
11
|
+
class<<self
|
12
|
+
# Takes the typical portal string from iscsiadm and the type and returns a
|
13
|
+
# new Portal object.
|
14
|
+
#
|
15
|
+
# eg Portal.parse('20.20.20.20:3260,1','sendtargets')
|
16
|
+
#
|
17
|
+
# XXX: whats the trailing ,1 again..?
|
18
|
+
def parse(portal,type=DEFAULT_TYPE)
|
19
|
+
if portal =~ %r{^(.*):(\d+),?(\d+)?$}
|
20
|
+
return Portal.new(:ip => $1, :port => $2, :interface => $3, :type => type)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Instance Methods
|
26
|
+
#
|
27
|
+
def initialize(args = {})
|
28
|
+
@ip = args[:ip]
|
29
|
+
@port = args[:port]
|
30
|
+
@interface = args[:interface]
|
31
|
+
@type = args[:type]
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
"#{@ip}:#{@port},#{@interface}"
|
36
|
+
end
|
37
|
+
|
38
|
+
end # class
|
39
|
+
end # module
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module IscsiadmWrapper
|
2
|
+
class Proc
|
3
|
+
|
4
|
+
class ExternalFailure < RuntimeError; end
|
5
|
+
|
6
|
+
class<<self
|
7
|
+
# Return output of external command
|
8
|
+
# Yield each line or return entire output
|
9
|
+
#
|
10
|
+
def external(cmd) #:nodoc:
|
11
|
+
output = []
|
12
|
+
IO.popen(cmd, "r") do |p|
|
13
|
+
while line = p.gets
|
14
|
+
if defined? yield
|
15
|
+
yield line
|
16
|
+
else
|
17
|
+
output << line
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
stat = $?
|
22
|
+
if stat.exited?
|
23
|
+
if stat.exitstatus > 0
|
24
|
+
raise ExternalFailure, "Fatal error, `#{cmd}` returned #{stat.exitstatus}"
|
25
|
+
end
|
26
|
+
elsif stat.signaled?
|
27
|
+
raise ExternalFailure, "Fatal error, `#{cmd}` got signal #{stat.termsig} and terminated"
|
28
|
+
elsif stat.stopped?
|
29
|
+
raise ExternalFailure, "Fatal error, `#{cmd}` got signal #{stat.stopsig} and is stopped"
|
30
|
+
else
|
31
|
+
output.join
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end # self
|
36
|
+
|
37
|
+
end # class
|
38
|
+
end # module
|
File without changes
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.0
|
3
|
+
specification_version: 1
|
4
|
+
name: ruby-iscsiadm
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.0.1
|
7
|
+
date: 2008-05-30 00:00:00 -07:00
|
8
|
+
summary: This is a wrapper for the open-iscsi administration utility, iscsiadm
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email:
|
12
|
+
- matt@bravenet.com
|
13
|
+
homepage: http://ruby-iscsiadm.rubyforge.org
|
14
|
+
rubyforge_project: ruby-iscsiadm
|
15
|
+
description: This is a wrapper for the open-iscsi administration utility, iscsiadm. This is a simple binary wrapper and considered a temporary solution until a proper api is finished as per http://thread.gmane.org/gmane.linux.iscsi.open-iscsi/422
|
16
|
+
autorequire:
|
17
|
+
default_executable:
|
18
|
+
bindir: bin
|
19
|
+
has_rdoc: true
|
20
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
21
|
+
requirements:
|
22
|
+
- - ">"
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 0.0.0
|
25
|
+
version:
|
26
|
+
platform: ruby
|
27
|
+
signing_key:
|
28
|
+
cert_chain:
|
29
|
+
- |
|
30
|
+
-----BEGIN CERTIFICATE-----
|
31
|
+
MIIDMDCCAhigAwIBAgIBADANBgkqhkiG9w0BAQUFADA+MQ0wCwYDVQQDDARtYXR0
|
32
|
+
MRgwFgYKCZImiZPyLGQBGRYIYnJhdmVuZXQxEzARBgoJkiaJk/IsZAEZFgNjb20w
|
33
|
+
HhcNMDgwNTI5MjM1MDUwWhcNMDkwNTI5MjM1MDUwWjA+MQ0wCwYDVQQDDARtYXR0
|
34
|
+
MRgwFgYKCZImiZPyLGQBGRYIYnJhdmVuZXQxEzARBgoJkiaJk/IsZAEZFgNjb20w
|
35
|
+
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDDGVrJnpk6/hCO5lg6T7As
|
36
|
+
unEksqdmCly6GkRpX0cVxusWG/wbjrZw4Ka5x9pSS2m3GfefEpu5CuWH9wnGB/q0
|
37
|
+
LwbWhrkpQb+uNVD6hEbQcngDEiQbiitaSWZKSvrIymbM5Tl92rIScmvx7Pd7l8tz
|
38
|
+
BLndL/DmrBHrHWQ47xLxOmBAA4NJy0gk7HdsdAFfJwrEoC14AAXqcI3X0qU3KGrr
|
39
|
+
9Fs0jVeIKktx3fthwGVPrpYrMIw3xgiUqOqn7M+V4soD013dopaN7orewmCm9dQL
|
40
|
+
4N06FMnGNjwFaS6+MV612nk8gkVpJwgcwTHE1GNt2NItR5zeK6j+AZ01DijYdjH9
|
41
|
+
AgMBAAGjOTA3MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBQ6jcdN
|
42
|
+
QqNNsx2XxigfTWE0Owy0XzANBgkqhkiG9w0BAQUFAAOCAQEAvWBkYaD7eKP+KHxJ
|
43
|
+
dtQuaebQTZvBX4vjk/Omjn7hj7u8X8aTvmCKyopnmywiApFH1zgDa8NRc1KTsvZt
|
44
|
+
zhSRWIuI7fFB+JFZUatE7Nius56id9UchYO98569t6bNU284XwBtcj4MN5Andfcp
|
45
|
+
LKdUNuuT0yui1InJG4fD0uKnOS1qcXEm+mN2s1uqPGVltEHorG7L/bPNAw8xV+uq
|
46
|
+
NPc7hhQTkbi7HB2Uwxr9uK9IGHEE5tDVnsPWPnJJ4jSOc7Bd1eHZuSMkEPfyREDf
|
47
|
+
h9ljoX3AGXW8qYohFzGjflamaWZ4xYaKZSNqS2i0Kc+M76Sy+r9FldDRNn4FGUBE
|
48
|
+
JqYgHg==
|
49
|
+
-----END CERTIFICATE-----
|
50
|
+
|
51
|
+
post_install_message:
|
52
|
+
authors:
|
53
|
+
- Matthew Kent
|
54
|
+
files:
|
55
|
+
- History.txt
|
56
|
+
- Manifest.txt
|
57
|
+
- README.txt
|
58
|
+
- README_SUDO.txt
|
59
|
+
- Rakefile
|
60
|
+
- Todo.txt
|
61
|
+
- examples/example_session.rb
|
62
|
+
- examples/show_nodes.rb
|
63
|
+
- lib/iscsiadm.rb
|
64
|
+
- lib/iscsiadm/common.rb
|
65
|
+
- lib/iscsiadm/external.rb
|
66
|
+
- lib/iscsiadm/iscsiadm.rb
|
67
|
+
- lib/iscsiadm/node.rb
|
68
|
+
- lib/iscsiadm/portal.rb
|
69
|
+
- lib/iscsiadm/proc.rb
|
70
|
+
- lib/iscsiadm/session.rb
|
71
|
+
- test/test_iscsiadm.rb
|
72
|
+
test_files:
|
73
|
+
- test/test_iscsiadm.rb
|
74
|
+
rdoc_options:
|
75
|
+
- --main
|
76
|
+
- README.txt
|
77
|
+
extra_rdoc_files:
|
78
|
+
- History.txt
|
79
|
+
- Manifest.txt
|
80
|
+
- README.txt
|
81
|
+
- README_SUDO.txt
|
82
|
+
- Todo.txt
|
83
|
+
executables: []
|
84
|
+
|
85
|
+
extensions: []
|
86
|
+
|
87
|
+
requirements: []
|
88
|
+
|
89
|
+
dependencies:
|
90
|
+
- !ruby/object:Gem::Dependency
|
91
|
+
name: open4
|
92
|
+
version_requirement:
|
93
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: 0.9.6
|
98
|
+
version:
|
99
|
+
- !ruby/object:Gem::Dependency
|
100
|
+
name: hoe
|
101
|
+
version_requirement:
|
102
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: 1.5.3
|
107
|
+
version:
|
metadata.gz.sig
ADDED