shu-san-scripts 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,104 @@
1
+ module Cri
2
+
3
+ # Cri::Command represents a command that can be executed on the commandline.
4
+ # It is an abstract superclass for all commands.
5
+ class Command
6
+
7
+ attr_accessor :base
8
+
9
+ # Returns a string containing the name of thi command. Subclasses must
10
+ # implement this method.
11
+ def name
12
+ raise NotImplementedError.new("Command subclasses should override #name")
13
+ end
14
+
15
+ # Returns an array of strings containing the aliases for this command.
16
+ # Subclasses must implement this method.
17
+ def aliases
18
+ raise NotImplementedError.new("Command subclasses should override #aliases")
19
+ end
20
+
21
+ # Returns a string containing this command's short description, which
22
+ # should not be longer than 50 characters. Subclasses must implement this
23
+ # method.
24
+ def short_desc
25
+ raise NotImplementedError.new("Command subclasses should override #short_desc")
26
+ end
27
+
28
+ # Returns a string containing this command's complete description, which
29
+ # should explain what this command does and how it works in detail.
30
+ # Subclasses must implement this method.
31
+ def long_desc
32
+ raise NotImplementedError.new("Command subclasses should override #long_desc")
33
+ end
34
+
35
+ # Returns a string containing this command's usage. Subclasses must
36
+ # implement this method.
37
+ def usage
38
+ raise NotImplementedError.new("Command subclasses should override #usage")
39
+ end
40
+
41
+ # Returns an array containing this command's option definitions. See the
42
+ # documentation for Cri::OptionParser for details on what option
43
+ # definitions look like. Subclasses may implement this method if the
44
+ # command has options.
45
+ def option_definitions
46
+ []
47
+ end
48
+
49
+ # Executes the command. Subclasses must implement this method
50
+ # (obviously... what's the point of a command that can't be run?).
51
+ #
52
+ # +options+:: A hash containing the parsed commandline options. For
53
+ # example, '--foo=bar' will be converted into { :foo => 'bar'
54
+ # }. See the Cri::OptionParser documentation for details.
55
+ #
56
+ # +arguments+:: An array of strings representing the commandline arguments
57
+ # given to this command.
58
+ def run(options, arguments)
59
+ raise NotImplementedError.new("Command subclasses should override #run")
60
+ end
61
+
62
+ # Returns the help text for this command.
63
+ def help
64
+ text = ''
65
+
66
+ # Append usage
67
+ text << usage + "\n"
68
+
69
+ # Append aliases
70
+ unless aliases.empty?
71
+ text << "\n"
72
+ text << "aliases: #{aliases.join(' ')}\n"
73
+ end
74
+
75
+ # Append short description
76
+ text << "\n"
77
+ text << short_desc + "\n"
78
+
79
+ # Append long description
80
+ text << "\n"
81
+ text << long_desc.wrap_and_indent(78, 4) + "\n"
82
+
83
+ # Append options
84
+ unless option_definitions.empty?
85
+ text << "\n"
86
+ text << "options:\n"
87
+ text << "\n"
88
+ option_definitions.sort { |x,y| x[:long] <=> y[:long] }.each do |opt_def|
89
+ text << sprintf(" -%1s --%-10s %s\n\n", opt_def[:short], opt_def[:long], opt_def[:desc].wrap_and_indent(78, 20).lstrip)
90
+ end
91
+ end
92
+
93
+ # Return text
94
+ text
95
+ end
96
+
97
+ # Compares this command's name to the other given command's name.
98
+ def <=>(other)
99
+ self.name <=> other.name
100
+ end
101
+
102
+ end
103
+
104
+ end
@@ -0,0 +1,8 @@
1
+ module Cri::CoreExtensions
2
+ end
3
+
4
+ require 'SANStore/cri/core_ext/string'
5
+
6
+ class String
7
+ include Cri::CoreExtensions::String
8
+ end
@@ -0,0 +1,41 @@
1
+ module Cri::CoreExtensions
2
+
3
+ module String
4
+
5
+ # Word-wraps and indents the string.
6
+ #
7
+ # +width+:: The maximal width of each line. This also includes indentation,
8
+ # i.e. the actual maximal width of the text is width-indentation.
9
+ #
10
+ # +indentation+:: The number of spaces to indent each wrapped line.
11
+ def wrap_and_indent(width, indentation)
12
+ # Split into paragraphs
13
+ paragraphs = self.split("\n").map { |p| p.strip }.reject { |p| p == '' }
14
+
15
+ # Wrap and indent each paragraph
16
+ paragraphs.map do |paragraph|
17
+ # Initialize
18
+ lines = []
19
+ line = ''
20
+
21
+ # Split into words
22
+ paragraph.split(/\s/).each do |word|
23
+ # Begin new line if it's too long
24
+ if (line + ' ' + word).length >= width
25
+ lines << line
26
+ line = ''
27
+ end
28
+
29
+ # Add word to line
30
+ line += (line == '' ? '' : ' ' ) + word
31
+ end
32
+ lines << line
33
+
34
+ # Join lines
35
+ lines.map { |l| ' '*indentation + l }.join("\n")
36
+ end.join("\n\n")
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,186 @@
1
+ module Cri
2
+
3
+ # Cri::OptionParser is used for parsing commandline options.
4
+ class OptionParser
5
+
6
+ # Error that will be raised when an unknown option is encountered.
7
+ class IllegalOptionError < RuntimeError ; end
8
+
9
+ # Error that will be raised when an option without argument is
10
+ # encountered.
11
+ class OptionRequiresAnArgumentError < RuntimeError ; end
12
+
13
+ # Parses the commandline arguments in +arguments_and_options+, using the
14
+ # commandline option definitions in +definitions+.
15
+ #
16
+ # +arguments_and_options+ is an array of commandline arguments and
17
+ # options. This will usually be +ARGV+.
18
+ #
19
+ # +definitions+ contains a list of hashes defining which options are
20
+ # allowed and how they will be handled. Such a hash has three keys:
21
+ #
22
+ # :short:: The short name of the option, e.g. +a+. Do not include the '-'
23
+ # prefix.
24
+ #
25
+ # :long:: The long name of the option, e.g. +all+. Do not include the '--'
26
+ # prefix.
27
+ #
28
+ # :argument:: Whether this option's argument is required (:required),
29
+ # optional (:optional) or forbidden (:forbidden).
30
+ #
31
+ # A sample array of definition hashes could look like this:
32
+ #
33
+ # [
34
+ # { :short => 'a', :long => 'all', :argument => :forbidden },
35
+ # { :short => 'p', :long => 'port', :argument => :required },
36
+ # ]
37
+ #
38
+ # During parsing, two errors can be raised:
39
+ #
40
+ # IllegalOptionError:: An unrecognised option was encountered, i.e. an
41
+ # option that is not present in the list of option
42
+ # definitions.
43
+ #
44
+ # OptionRequiresAnArgumentError:: An option was found that did not have a
45
+ # value, even though this value was
46
+ # required.
47
+ #
48
+ # What will be returned, is a hash with two keys, :arguments and :options.
49
+ # The :arguments value contains a list of arguments, and the :options
50
+ # value contains a hash with key-value pairs for each option. Options
51
+ # without values will have a +nil+ value instead.
52
+ #
53
+ # For example, the following commandline options (which should not be
54
+ # passed as a string, but as an array of strings):
55
+ #
56
+ # foo -xyz -a hiss -s -m please --level 50 --father=ani -n luke squeak
57
+ #
58
+ # with the following option definitions:
59
+ #
60
+ # [
61
+ # { :short => 'x', :long => 'xxx', :argument => :forbidden },
62
+ # { :short => 'y', :long => 'yyy', :argument => :forbidden },
63
+ # { :short => 'z', :long => 'zzz', :argument => :forbidden },
64
+ # { :short => 'a', :long => 'all', :argument => :forbidden },
65
+ # { :short => 's', :long => 'stuff', :argument => :optional },
66
+ # { :short => 'm', :long => 'more', :argument => :optional },
67
+ # { :short => 'l', :long => 'level', :argument => :required },
68
+ # { :short => 'f', :long => 'father', :argument => :required },
69
+ # { :short => 'n', :long => 'name', :argument => :required }
70
+ # ]
71
+ #
72
+ # will be translated into:
73
+ #
74
+ # {
75
+ # :arguments => [ 'foo', 'hiss', 'squeak' ],
76
+ # :options => {
77
+ # :xxx => true,
78
+ # :yyy => true,
79
+ # :zzz => true,
80
+ # :all => true,
81
+ # :stuff => true,
82
+ # :more => 'please',
83
+ # :level => '50',
84
+ # :father => 'ani',
85
+ # :name => 'luke'
86
+ # }
87
+ # }
88
+ def self.parse(arguments_and_options, definitions)
89
+ # Don't touch original argument
90
+ unprocessed_arguments_and_options = arguments_and_options.dup
91
+
92
+ # Initialize
93
+ arguments = []
94
+ options = {}
95
+
96
+ # Determines whether we've passed the '--' marker or not
97
+ no_more_options = false
98
+
99
+ loop do
100
+ # Get next item
101
+ e = unprocessed_arguments_and_options.shift
102
+ break if e.nil?
103
+
104
+ # Handle end-of-options marker
105
+ if e == '--'
106
+ no_more_options = true
107
+ # Handle incomplete options
108
+ elsif e =~ /^--./ and !no_more_options
109
+ # Get option key, and option value if included
110
+ if e =~ /^--([^=]+)=(.+)$/
111
+ option_key = $1
112
+ option_value = $2
113
+ else
114
+ option_key = e[2..-1]
115
+ option_value = nil
116
+ end
117
+
118
+ # Find definition
119
+ definition = definitions.find { |d| d[:long] == option_key }
120
+ raise IllegalOptionError.new(option_key) if definition.nil?
121
+
122
+ if [ :required, :optional ].include?(definition[:argument])
123
+ # Get option value if necessary
124
+ if option_value.nil?
125
+ option_value = unprocessed_arguments_and_options.shift
126
+ if option_value.nil? || option_value =~ /^-/
127
+ if definition[:argument] == :required
128
+ raise OptionRequiresAnArgumentError.new(option_key)
129
+ else
130
+ unprocessed_arguments_and_options.unshift(option_value)
131
+ option_value = true
132
+ end
133
+ end
134
+ end
135
+
136
+ # Store option
137
+ options[definition[:long].to_sym] = option_value
138
+ else
139
+ # Store option
140
+ options[definition[:long].to_sym] = true
141
+ end
142
+ # Handle -xyz options
143
+ elsif e =~ /^-./ and !no_more_options
144
+ # Get option keys
145
+ option_keys = e[1..-1].scan(/./)
146
+
147
+ # For each key
148
+ option_keys.each do |option_key|
149
+ # Find definition
150
+ definition = definitions.find { |d| d[:short] == option_key }
151
+ raise IllegalOptionError.new(option_key) if definition.nil?
152
+
153
+ if option_keys.length > 1 and definition[:argument] == :required
154
+ # This is a combined option and it requires an argument, so complain
155
+ raise OptionRequiresAnArgumentError.new(option_key)
156
+ elsif [ :required, :optional ].include?(definition[:argument])
157
+ # Get option value
158
+ option_value = unprocessed_arguments_and_options.shift
159
+ if option_value.nil? || option_value =~ /^-/
160
+ if definition[:argument] == :required
161
+ raise OptionRequiresAnArgumentError.new(option_key)
162
+ else
163
+ unprocessed_arguments_and_options.unshift(option_value)
164
+ option_value = true
165
+ end
166
+ end
167
+
168
+ # Store option
169
+ options[definition[:long].to_sym] = option_value
170
+ else
171
+ # Store option
172
+ options[definition[:long].to_sym] = true
173
+ end
174
+ end
175
+ # Handle normal arguments
176
+ else
177
+ arguments << e
178
+ end
179
+ end
180
+
181
+ { :options => options, :arguments => arguments }
182
+ end
183
+
184
+ end
185
+
186
+ end
@@ -0,0 +1,35 @@
1
+ # Copyright (c) 2010-2011 David Love
2
+ #
3
+ # Permission to use, copy, modify, and/or distribute this software for
4
+ # any purpose with or without fee is hereby granted, provided that the
5
+ # above copyright notice and this permission notice appear in all copies.
6
+ #
7
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
+ #
15
+
16
+ # @author David Love
17
+
18
+ require "uuidtools"
19
+
20
+ # Extendes the {UUIDTools::UUID} class, adding the ability to
21
+ # generate sequential UUID's
22
+ class UUIDTools::UUID
23
+
24
+ # Increments the internal UUID representation, returning a
25
+ # new UUID that is different from, but greater, than the
26
+ # current sequence number
27
+ def next
28
+ next_uuid = self.to_i
29
+ next_uuid += 1
30
+
31
+ # Return the newly created UUID
32
+ return UUIDTools::UUID::parse_int(next_uuid)
33
+ end
34
+
35
+ end
@@ -0,0 +1,239 @@
1
+ # Copyright (c) 2010-2011 David Love
2
+ #
3
+ # Permission to use, copy, modify, and/or distribute this software for
4
+ # any purpose with or without fee is hereby granted, provided that the
5
+ # above copyright notice and this permission notice appear in all copies.
6
+ #
7
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
+ #
15
+
16
+ # @author David Love
17
+
18
+ # Defines a class capable of creating iSCSI shares using the new
19
+ # COMStar framework. This classes uses the various command line tools,
20
+ # not the C API.
21
+ class COMStar
22
+
23
+ # Create a new, ready to use, iSCSI target. Functionally this is command
24
+ # is equivalent to the old "shareiscsi=on" ZFS volume property.
25
+ def self.new_target(volume_path, volume_guid)
26
+
27
+ # Create a new disk target for the ZFS volume
28
+ SANStore::CLI::Logger.instance.log_level(:low, :create, "New SCSI block device at /dev/zvol/rdsk/#{volume_path}")
29
+ disk_target = %x[sbdadm create-lu /dev/zvol/rdsk/#{volume_path}]
30
+
31
+ # Get the ID of the new disk target from the output of the
32
+ # target creation command
33
+ id = disk_target.split(/$/)[4].split[0]
34
+ SANStore::CLI::Logger.instance.log_level(:low, :info, "Using #{id} as the logical unit identifier")
35
+
36
+ # Modify the just created logical unit to include the path of the
37
+ # volume backing it. This makes it much easier to get rid of the
38
+ # relevant volume later
39
+ SANStore::CLI::Logger.instance.log_level(:low, :update, "Storing the volume GUID as the logical unit alias")
40
+ modify_lu = %x[stmfadm modify-lu --lu-prop alias=#{volume_guid} #{id}]
41
+
42
+ # Link the new disk target to the iSCSI framework
43
+ SANStore::CLI::Logger.instance.log_level(:low, :update, "Attaching logical unit #{id} into the iSCSI framework")
44
+ vol_frame = %x[stmfadm add-view #{id}]
45
+
46
+ # Create the target...
47
+ SANStore::CLI::Logger.instance.log_level(:low, :create, "iSCSI block target")
48
+ target = %x[itadm create-target]
49
+ target_name = target.split[1]
50
+
51
+ # Store the volume GUID as the alias so we can find it later
52
+ SANStore::CLI::Logger.instance.log_level(:low, :update, "Storing the volume GUID as the iSCSI target alias")
53
+ %x[itadm modify-target --alias #{volume_guid} #{target_name}]
54
+
55
+ # Return the target name to the caller
56
+ return target_name
57
+ end
58
+
59
+ # Delete an iSCSI target. This returns the name of the underlying ZFS store in case the
60
+ # caller wants to delete that as well
61
+ def self.delete_target(target_name)
62
+
63
+ # Before we kill the target, get the store GUID so that we can clean up properly
64
+ target_map = self.TargetToGUIDMap
65
+ target_guid = target_map[target_name]
66
+
67
+ # Get the maps of the LU identifers to the underlying stores
68
+ vol_map = self.LUToVolMap
69
+
70
+ # Look for the store with the GUID of the target we want to delete
71
+ SANStore::CLI::Logger.instance.log_level(:low, :info, "Looking for the name of the volume #{target_guid} backing #{target_name}")
72
+ vol_name = ""
73
+ vol_store = ""
74
+ vol_map.each{|key, value|
75
+ if value[:guid] == target_guid then
76
+ # We have found the right entry, so update the name of the
77
+ # volume store and the name of the volume backing it
78
+ vol_name = key
79
+ vol_store = value[:data_file]
80
+ break
81
+ end
82
+ }
83
+
84
+ # Abort if we can't find what we are looking for
85
+ if vol_name.empty? or vol_store.empty? then
86
+ SANStore::CLI::Logger.instance.log_level(:high, :error, "Could not delete the file system for the iSCSI target. The target has been removed, but the file system is still around!")
87
+ return ""
88
+ end
89
+
90
+ # Delete the target (and force any clients off the soon to die share)
91
+ SANStore::CLI::Logger.instance.log_level(:low, :warning, "Closing all sessions for #{target_name}")
92
+ target = %x[itadm delete-target -f #{target_name}]
93
+
94
+ # Now unlink the SCSI logical unit, so that we can free the underlying ZFS volume
95
+ SANStore::CLI::Logger.instance.log_level(:low, :delete, "Removing logical units associated with the deleted target")
96
+ disk_target = %x[sbdadm delete-lu #{vol_name}]
97
+
98
+ # The file store is now ready for deletion. We will tell the caller what to remove, but we
99
+ # don't actually do this ourselves (file systems are someone elses problem)
100
+ return vol_store
101
+ end
102
+
103
+ # List the current iSCSI targets defined on this host
104
+ def self.list_vols
105
+ raw_list = %x[itadm list-target]
106
+
107
+ # Create a hash for the final list of targets
108
+ target_list = Array.new
109
+
110
+ # Run through the raw list of targets
111
+ target_array = raw_list.split(/$/)
112
+ target_array.delete_at(0)
113
+
114
+ target_array.each{|row|
115
+ row_fragments = row.split
116
+ row_hash = Hash.new
117
+
118
+ row_hash[:name] = row_fragments[0]
119
+ row_hash[:state] = row_fragments[1]
120
+ row_hash[:sessions] = row_fragments[2]
121
+
122
+ target_list << row_hash
123
+ }
124
+
125
+ # return the list to the caller
126
+ return target_list
127
+ end
128
+
129
+ # Walks over the list of iSCSI targets, looking for the store GUID (stored in the target alias field).
130
+ # Returns a map of the target names and associated aliases
131
+ def self.TargetToGUIDMap
132
+
133
+ # Get the raw map
134
+ SANStore::CLI::Logger.instance.log_level(:low, :info, "Finding the GUID's associated with iSCSI targets")
135
+ raw_list = %x[stmfadm list-target -v]
136
+ raw_map = raw_list.split(/$/)
137
+
138
+ # Create a hash from this map
139
+ map_hash = Hash.new
140
+ map_index = nil
141
+ map_entry = nil
142
+ raw_index = 0
143
+
144
+ while raw_index < raw_map.length do
145
+ # Is this line the start of a new target entry
146
+ if raw_map[raw_index].index(/Target\:/) then
147
+
148
+ # Store the old entry
149
+ unless map_entry.nil? then
150
+ map_hash[map_index] = map_entry
151
+ end
152
+
153
+ # Create a new map entry
154
+ map_entry = String.new
155
+ map_index = raw_map[raw_index].partition(/Target\:/)[2].strip
156
+
157
+ else
158
+
159
+ # Split the line to find the key and value
160
+ entry = raw_map[raw_index].partition(/\s?\:\s/)
161
+
162
+ case entry[0].strip
163
+ when "Alias"
164
+ map_entry = entry[2].strip
165
+ end
166
+
167
+ end
168
+
169
+ raw_index += 1
170
+ end
171
+
172
+ # Add the last entry
173
+ unless map_entry.nil? then
174
+ map_hash[map_index] = map_entry
175
+ end
176
+
177
+ # Return the hash map
178
+ return map_hash
179
+
180
+ end
181
+
182
+ # Walks over the list of Logical Units, working out where the ZFS volume
183
+ # backing the LU is. Returns a hash, giving the correct backing store for
184
+ # each LU
185
+ def self.LUToVolMap
186
+
187
+ # Get the raw map
188
+ SANStore::CLI::Logger.instance.log_level(:low, :info, "Finding the backing stores for the logical units")
189
+ raw_list = %x[stmfadm list-lu -v]
190
+ raw_map = raw_list.split(/$/)
191
+
192
+ # Create a hash from this map
193
+ map_hash = Hash.new
194
+ map_index = nil
195
+ map_entry = nil
196
+ raw_index = 0
197
+
198
+ while raw_index < raw_map.length do
199
+ # Is this line the start of a new LU entry
200
+ if raw_map[raw_index].index(/LU Name\:/) then
201
+
202
+ # Store the old entry
203
+ unless map_entry.nil? then
204
+ map_hash[map_index] = map_entry
205
+ end
206
+
207
+ # Create a new map entry
208
+ map_entry = Hash.new
209
+ map_index = raw_map[raw_index].partition(/LU Name\:/)[2].strip
210
+
211
+ else
212
+
213
+ # Split the line to find the key and value
214
+ entry = raw_map[raw_index].partition(/\s?\:\s/)
215
+
216
+ case entry[0].strip
217
+ when "Alias"
218
+ map_entry[:guid] = entry[2].strip
219
+ when "Data File"
220
+ map_entry[:data_file] = entry[2].strip
221
+ end
222
+
223
+ end
224
+
225
+ raw_index += 1
226
+ end
227
+
228
+ # Add the last entry
229
+ unless map_entry.nil? then
230
+ map_hash[map_index] = map_entry
231
+ end
232
+
233
+ # Return the hash map
234
+ return map_hash
235
+
236
+ end
237
+
238
+ end
239
+