shu-san-scripts 0.2.0 → 0.2.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.
@@ -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
+