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.
- data/README.rdoc +6 -4
- data/lib/SANStore.rb +24 -0
- data/lib/SANStore/cli.rb +29 -0
- data/lib/SANStore/cli/base.rb +103 -0
- data/lib/SANStore/cli/commands.rb +31 -0
- data/lib/SANStore/cli/commands/delete_vol.rb +113 -0
- data/lib/SANStore/cli/commands/help.rb +106 -0
- data/lib/SANStore/cli/commands/list_vols.rb +103 -0
- data/lib/SANStore/cli/commands/new_vol.rb +134 -0
- data/lib/SANStore/cli/logger.rb +92 -0
- data/lib/SANStore/cri.rb +12 -0
- data/lib/SANStore/cri/base.rb +153 -0
- data/lib/SANStore/cri/command.rb +104 -0
- data/lib/SANStore/cri/core_ext.rb +8 -0
- data/lib/SANStore/cri/core_ext/string.rb +41 -0
- data/lib/SANStore/cri/option_parser.rb +186 -0
- data/lib/SANStore/helpers/uuid.rb +35 -0
- data/lib/SANStore/iSCSI/comstar.rb +239 -0
- data/lib/SANStore/zfs/zfs.rb +59 -0
- metadata +22 -4
@@ -0,0 +1,134 @@
|
|
1
|
+
# Copyright (c) 2009 Denis Defreyne, 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
|
+
# Volume names are based on RFC 4122 UUIDs
|
16
|
+
require "uuidtools"
|
17
|
+
|
18
|
+
# Use the ANSI library to report the status to the user
|
19
|
+
require "ansi/code"
|
20
|
+
|
21
|
+
# Use the Socket API to get the first IPv4 address
|
22
|
+
require "socket"
|
23
|
+
|
24
|
+
# Use the ZFS library
|
25
|
+
require "SANStore/zfs/zfs"
|
26
|
+
|
27
|
+
# Use the COMStar iSCSI library
|
28
|
+
require "SANStore/iSCSI/comstar.rb"
|
29
|
+
|
30
|
+
module SANStore::CLI::Commands
|
31
|
+
|
32
|
+
# @author David Love
|
33
|
+
#
|
34
|
+
# The +new_vol+ command adds a new ZFS volume to the specified volume
|
35
|
+
# store, and sets it up as an iSCSI target. Defaults are supplied to
|
36
|
+
# all arguments, but can be overridden by the user for slightly
|
37
|
+
# customised set-up of the volume store. However, the user interface
|
38
|
+
# should be kept as simple as possible.
|
39
|
+
class NewVol < Cri::Command
|
40
|
+
|
41
|
+
# The name of the sub-command (as it appears in the command line app)
|
42
|
+
def name
|
43
|
+
'new_vol'
|
44
|
+
end
|
45
|
+
|
46
|
+
# The aliases this sub-command is known by
|
47
|
+
def aliases
|
48
|
+
[
|
49
|
+
"new", "add", "add_vol"
|
50
|
+
]
|
51
|
+
end
|
52
|
+
|
53
|
+
# A short help text describing the purpose of this command
|
54
|
+
def short_desc
|
55
|
+
'Create a new iSCSI volume in the SAN volume store.'
|
56
|
+
end
|
57
|
+
|
58
|
+
# A longer description, detailing both the purpose and the
|
59
|
+
# use of this command
|
60
|
+
def long_desc
|
61
|
+
'By default, this command creates a 20G ZFS volume, and marks it for ' +
|
62
|
+
'sharing as an iSCSI target on the local network.' + "\n\n" +
|
63
|
+
'Warning: By default this commands sets up the iSCSI target with NO ' +
|
64
|
+
'security. This is fine for testing and use in the labs, but obviously ' +
|
65
|
+
'is not ideal if you care about the data stored on this new volume...'
|
66
|
+
end
|
67
|
+
|
68
|
+
# Show the user the basic syntax of this command
|
69
|
+
def usage
|
70
|
+
"store new_volume [--volume-store ZFS_PATH][--name GUID] [--size INTEGER]"
|
71
|
+
end
|
72
|
+
|
73
|
+
# Define the options for this command
|
74
|
+
def option_definitions
|
75
|
+
[
|
76
|
+
{ :short => 'v', :long => 'volume_store', :argument => :optional,
|
77
|
+
:desc => 'specifify the ZFS root of the new iSCSI volume. Defaults to "store/volumes".'
|
78
|
+
},
|
79
|
+
{ :short => 'n', :long => 'name', :argument => :optional,
|
80
|
+
:desc => 'the name of the new volume. This must be a valid ZFS volume name, and defaults to ' +
|
81
|
+
'an RFC 4122 GUID.'
|
82
|
+
},
|
83
|
+
{ :short => 's', :long => 'size', :argument => :optional,
|
84
|
+
:desc => 'the size of the new iSCSI volume. Note that while ZFS allows you to change the size ' +
|
85
|
+
'of the new volume relatively easily, because the iSCSI initiator sees this volume as a raw ' +
|
86
|
+
'device changing the size later may be very easy or very difficult depending on the initiators ' +
|
87
|
+
'operating system (and the specific file system being used). In other words, choose with care: ' +
|
88
|
+
'by default this command uses a size of 20G, which should be enough for most tasks in the labs.'
|
89
|
+
},
|
90
|
+
]
|
91
|
+
end
|
92
|
+
|
93
|
+
# Execute the command
|
94
|
+
def run(options, arguments)
|
95
|
+
|
96
|
+
# Look at the options, and if we don't find any (or some are
|
97
|
+
# missing), set them to the default values
|
98
|
+
if options[:volume_store].nil? or options[:volume_store].empty? then
|
99
|
+
volume_store = "store/volumes"
|
100
|
+
options[:volume_store] = volume_store
|
101
|
+
end
|
102
|
+
|
103
|
+
if options[:name].nil? or options[:name].empty? then
|
104
|
+
name = UUIDTools::UUID.timestamp_create
|
105
|
+
options[:name] = name.to_s
|
106
|
+
end
|
107
|
+
|
108
|
+
if options[:size].nil? or options[:size].empty? then
|
109
|
+
size = "20G"
|
110
|
+
options[:size] = size
|
111
|
+
end
|
112
|
+
|
113
|
+
SANStore::CLI::Logger.instance.log_level(:low, :info, "Using #{options[:name]} as the volume identifier")
|
114
|
+
|
115
|
+
# Ask for a new volume
|
116
|
+
ZFS.new_volume(options[:volume_store] + "/" + options[:name], options[:size])
|
117
|
+
|
118
|
+
# Set the volume up as an iSCSI target
|
119
|
+
target_name = COMStar.new_target(options[:volume_store] + "/" + options[:name], options[:name])
|
120
|
+
|
121
|
+
# Tell the caller what the new volume name is
|
122
|
+
text = "\n"
|
123
|
+
text << "A new iSCSI target has been created with the following properties\n"
|
124
|
+
text << "\n"
|
125
|
+
text << sprintf(" %-25s %s\n", ANSI.red{ "Name:" }, target_name.wrap_and_indent(78, 20).lstrip)
|
126
|
+
text << sprintf(" %-25s %s\n", ANSI.red{ "IPv4 Address:" }, Socket::getaddrinfo(Socket.gethostname,"echo",Socket::AF_INET)[0][3])
|
127
|
+
text << "\n"
|
128
|
+
|
129
|
+
puts text
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# Copyright (c) 2009 Denis Defreyne, 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
|
+
require 'singleton'
|
17
|
+
require 'facets'
|
18
|
+
|
19
|
+
module SANStore::CLI
|
20
|
+
|
21
|
+
# SANStore::CLI::Logger is a singleton class responsible for generating
|
22
|
+
# feedback in the terminal.
|
23
|
+
class Logger
|
24
|
+
|
25
|
+
# ANSI console codes (escape sequences) for highlighting particular
|
26
|
+
# log outputs.
|
27
|
+
ACTION_COLORS = {
|
28
|
+
:create => "\e[1m" + "\e[32m", # bold + green
|
29
|
+
:delete => "\e[1m" + "\e[31m", # bold + red
|
30
|
+
:error => "\e[1m" + "\e[31m", # bold + red
|
31
|
+
:identical => "\e[1m" + "\e[34m", # bold + blue
|
32
|
+
:info => "\e[1m" + "\e[34m", # bold + blue
|
33
|
+
:update => "\e[1m" + "\e[33m", # bold + yellow
|
34
|
+
:warning => "\e[1m" + "\e[33m", # bold + yellow
|
35
|
+
}
|
36
|
+
|
37
|
+
include Singleton
|
38
|
+
|
39
|
+
# The log level, which can be :high, :low or :off (which will log all
|
40
|
+
# messages, only high-priority messages, or no messages at all,
|
41
|
+
# respectively).
|
42
|
+
attr_accessor :level
|
43
|
+
|
44
|
+
# Whether to use color in log messages or not
|
45
|
+
attr_accessor :color
|
46
|
+
alias_method :color?, :color
|
47
|
+
|
48
|
+
def initialize
|
49
|
+
@level = :high
|
50
|
+
@color = true
|
51
|
+
end
|
52
|
+
|
53
|
+
# Logs a messsage, using appropriate colours to highlight different
|
54
|
+
# levels.
|
55
|
+
#
|
56
|
+
# +level+:: The importance of this action. Can be :high or :low.
|
57
|
+
#
|
58
|
+
# +action+:: The kind of file action. Can be :create, :update or
|
59
|
+
# :identical.
|
60
|
+
#
|
61
|
+
# +message+:: The identifier of the item the action was performed on.
|
62
|
+
def log_level(level, action, message)
|
63
|
+
log(
|
64
|
+
level,
|
65
|
+
'%s%12s%s: %s' % [
|
66
|
+
color? ? ACTION_COLORS[action.to_sym] : '',
|
67
|
+
action.to_s.capitalize,
|
68
|
+
color? ? "\e[0m" : '',
|
69
|
+
message.word_wrap(60).indent(15).lstrip
|
70
|
+
]
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Logs a message.
|
75
|
+
#
|
76
|
+
# +level+:: The importance of this message. Can be :high or :low.
|
77
|
+
#
|
78
|
+
# +message+:: The message to be logged.
|
79
|
+
#
|
80
|
+
# +io+:: The IO instance to which the message will be written. Defaults to
|
81
|
+
# standard output.
|
82
|
+
def log(level, message, io=$stdout)
|
83
|
+
# Don't log when logging is disabled
|
84
|
+
return if @level == :off
|
85
|
+
|
86
|
+
# Log when level permits it
|
87
|
+
io.puts(message) if (@level == :low or @level == level)
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
data/lib/SANStore/cri.rb
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
module Cri
|
2
|
+
|
3
|
+
# Cri::Base is the central class representing a commandline tool. It has a
|
4
|
+
# list of commands.
|
5
|
+
class Base
|
6
|
+
|
7
|
+
# The CLI's list of commands (should also contain the help command)
|
8
|
+
attr_reader :commands
|
9
|
+
|
10
|
+
# The CLI's help command (required)
|
11
|
+
attr_accessor :help_command
|
12
|
+
|
13
|
+
# Creates a new instance of the commandline tool.
|
14
|
+
def initialize(tool_name)
|
15
|
+
@tool_name = tool_name
|
16
|
+
|
17
|
+
@commands = []
|
18
|
+
end
|
19
|
+
|
20
|
+
# Parses the given commandline arguments and executes the requested
|
21
|
+
# command.
|
22
|
+
def run(args)
|
23
|
+
# Check arguments
|
24
|
+
if args.length == 0
|
25
|
+
@help_command.run([], [])
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
|
29
|
+
# Partition options
|
30
|
+
opts_before_command = []
|
31
|
+
command_name = nil
|
32
|
+
opts_and_args_after_command = []
|
33
|
+
stage = 0
|
34
|
+
args.each do |arg|
|
35
|
+
# Update stage if necessary
|
36
|
+
stage = 1 if stage == 0 && !is_option?(arg)
|
37
|
+
|
38
|
+
# Add
|
39
|
+
opts_before_command << arg if stage == 0
|
40
|
+
command_name = arg if stage == 1
|
41
|
+
opts_and_args_after_command << arg if stage == 2
|
42
|
+
|
43
|
+
# Update stage if necessary
|
44
|
+
stage = 2 if stage == 1
|
45
|
+
end
|
46
|
+
|
47
|
+
# Handle options before command
|
48
|
+
begin
|
49
|
+
parsed_arguments = Cri::OptionParser.parse(opts_before_command, global_option_definitions)
|
50
|
+
rescue Cri::OptionParser::IllegalOptionError => e
|
51
|
+
$stderr.puts "illegal option -- #{e}"
|
52
|
+
exit 1
|
53
|
+
end
|
54
|
+
parsed_arguments[:options].keys.each do |option|
|
55
|
+
handle_option(option)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Get command
|
59
|
+
if command_name.nil?
|
60
|
+
$stderr.puts "no command given"
|
61
|
+
exit 1
|
62
|
+
end
|
63
|
+
command = command_named(command_name)
|
64
|
+
if command.nil?
|
65
|
+
$stderr.puts "no such command: #{command_name}"
|
66
|
+
exit 1
|
67
|
+
end
|
68
|
+
|
69
|
+
# Parse arguments
|
70
|
+
option_definitions = command.option_definitions + global_option_definitions
|
71
|
+
begin
|
72
|
+
parsed_arguments = Cri::OptionParser.parse(opts_and_args_after_command, option_definitions)
|
73
|
+
rescue Cri::OptionParser::IllegalOptionError => e
|
74
|
+
$stderr.puts "illegal option -- #{e}"
|
75
|
+
exit 1
|
76
|
+
rescue Cri::OptionParser::OptionRequiresAnArgumentError => e
|
77
|
+
$stderr.puts "option requires an argument -- #{e}"
|
78
|
+
exit 1
|
79
|
+
end
|
80
|
+
|
81
|
+
# Handle global options
|
82
|
+
global_options = global_option_definitions.map { |o| o[:long] }
|
83
|
+
global_options.delete_if { |o| !parsed_arguments[:options].keys.include?(o.to_sym) }
|
84
|
+
global_options.each { |o| handle_option(o.to_sym) }
|
85
|
+
|
86
|
+
if parsed_arguments[:options].has_key?(:help)
|
87
|
+
# Show help for this command
|
88
|
+
show_help(command)
|
89
|
+
else
|
90
|
+
# Run command
|
91
|
+
command.run(parsed_arguments[:options], parsed_arguments[:arguments])
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns the command with the given name.
|
96
|
+
def command_named(name)
|
97
|
+
# Find by exact name or alias
|
98
|
+
command = @commands.find { |c| c.name == name or c.aliases.include?(name) }
|
99
|
+
return command unless command.nil?
|
100
|
+
|
101
|
+
# Find by approximation
|
102
|
+
commands = @commands.select { |c| c.name[0, name.length] == name }
|
103
|
+
if commands.length > 1
|
104
|
+
$stderr.puts "#{@tool_name}: '#{name}' is ambiguous:"
|
105
|
+
$stderr.puts " #{commands.map { |c| c.name }.join(' ') }"
|
106
|
+
exit 1
|
107
|
+
elsif commands.length == 0
|
108
|
+
$stderr.puts "#{@tool_name}: unknown command '#{name}'\n"
|
109
|
+
show_help
|
110
|
+
exit 1
|
111
|
+
else
|
112
|
+
return commands[0]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Shows the help text for the given command, or shows the general help
|
117
|
+
# text if no command is given.
|
118
|
+
def show_help(command=nil)
|
119
|
+
if command.nil?
|
120
|
+
@help_command.run([], [])
|
121
|
+
else
|
122
|
+
@help_command.run([], [ command.name ])
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns the list of global option definitionss.
|
127
|
+
def global_option_definitions
|
128
|
+
[]
|
129
|
+
end
|
130
|
+
|
131
|
+
# Adds the given command to the list of commands. Adding a command will
|
132
|
+
# also cause the command's +base+ to be set to this instance.
|
133
|
+
def add_command(command)
|
134
|
+
@commands << command
|
135
|
+
command.base = self
|
136
|
+
end
|
137
|
+
|
138
|
+
# Handles the given optio
|
139
|
+
def handle_option(option)
|
140
|
+
false
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
# Returns true if the given string is an option (i.e. -foo or --foo),
|
146
|
+
# false otherwise.
|
147
|
+
def is_option?(string)
|
148
|
+
string =~ /^-/
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
@@ -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
|