knife-hmc 1.0.0

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3bcc750013a4018b14ffe0bcc02c18db42f31df1
4
+ data.tar.gz: fb803c448f5e835d188638363b02f95412428bdb
5
+ SHA512:
6
+ metadata.gz: 7f87452613f678c89917a6a51fec584cfd6788456ee1216053a7238786533de1722571904833aa8e4f019a13102ea8f5f412db116065bb2b8e8843e5542ccfcb
7
+ data.tar.gz: ccc414ceeae6e6b383f47f7435dccff86b27308b67bc39ea72db89aea1c3fca4a9cacf87572d8df3613effb8318fe4c6a2ee72f6960efa1ac25f2b7567cf7fe8
@@ -0,0 +1,2 @@
1
+ ## V0.0.1
2
+ * initial version (unreleased)
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in knife-hmc.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ © Copyright IBM Corporation 2014.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,103 @@
1
+ # Knife::Hmc
2
+
3
+ A Chef Knife plugin for creating, deleting, bootstrapping, and managing LPARs and P series virtual infrastructure.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'knife-hmc'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install knife-hmc
18
+
19
+ ## Configuration
20
+ Add the path to the chef-client AIX installable on your Chef Server to your `knife.rb` file
21
+ to add support for Chef bootstrapping an AIX node as a part of 'knife hmc server create'.
22
+
23
+ ```ruby
24
+ log_level :info
25
+ log_location STDOUT
26
+ node_name 'node'
27
+ client_key '/path/to/key.pem'
28
+ validation_client_name 'some-validator'
29
+ validation_key '/path/to/validator.pem'
30
+ chef_server_url 'https://example.com/organizations/org'
31
+ syntax_check_cache_path '/path/to/syntax_check_cache'
32
+ knife[:chef_client_aix_path] = "<CHEF SERVER LOCAL PATH TO AIX BINARY>"
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ See `knife hmc SUBCOMMAND --help` for help on usage. Here are subcommands that usage help
38
+ can be provided for:
39
+
40
+ ```ruby
41
+ knife hmc server create --help
42
+ knife hmc server delete --help
43
+ knife hmc server config --help
44
+ knife hmc server list --help
45
+
46
+ knife hmc image list --help
47
+
48
+ knife hmc disk list --help
49
+ knife hmc disk add --help
50
+ knife hmc disk remove --help
51
+ ```
52
+
53
+ EXAMPLES:
54
+
55
+
56
+ ```bash
57
+ # look at all the LPARs on a frame or in an environment
58
+ user@local> knife hmc server list --hmc_host testhmc.us.ibm.com --hmc_user hscroot --hmc_pass passw0rd \
59
+ [--frame FRAME]
60
+ ```
61
+
62
+ ```bash
63
+ # LPAR creation and BOS install with the minimum arguments
64
+ user@local> knife hmc server create --hmc_host testhmc.us.ibm.com --hmc_user hscroot --hmc_pass passw0rd \
65
+ --frame test_frame \
66
+ --lpar test_lpar \
67
+ --primary_vio test_vio1 \
68
+ --secondary_vio test_vio2 \
69
+ --des_proc 2.0 \
70
+ --des_vcpu 2 \
71
+ --des_mem 2048 \
72
+ --nim_host testnim.us.ibm.com \
73
+ --nim_user root \
74
+ --nim_pass passw0rd \
75
+ --image image_name \
76
+ --ip_address lpar_ip \
77
+ --size 90 \
78
+ --vlan_id vlan \
79
+ --register_node chef_server_url \
80
+ --bootstrap_pass passw0rd
81
+ ```
82
+
83
+ ```bash
84
+ # List all of the images that an environment's
85
+ # NIM can deploy
86
+ user@local> knife hmc image list --nim_host testnim.us.ibm.com --nim_user root --nim_pass passw0rd
87
+ ```
88
+
89
+ ```bash
90
+ # List all of this disks
91
+ # that a VIO pair has access to
92
+ user@local> knife hmc disk list --hmc_host testhmc.us.ibm.com --hmc_user hscroot --hmc_pass passw0rd \
93
+ --primary_vio test_vio_1 \
94
+ --secondary_vio test_vio_2 \
95
+ --frame test_frame \
96
+ [--lpar test_lpar_name | --available | --used]
97
+ ```
98
+
99
+ #### Legal stuff
100
+ Use of this software requires runtime dependencies. Those dependencies and their respective software licenses are listed below.
101
+
102
+ * [net-ssh](https://github.com/net-ssh/net-ssh/) - LICENSE: [MIT](https://github.com/net-ssh/net-ssh/blob/master/LICENSE.txt)
103
+ * [net-scp](https://github.com/net-ssh/net-scp/) - LICENSE: [MIT](https://github.com/net-ssh/net-scp/blob/master/LICENSE.txt)
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'knife-hmc/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "knife-hmc"
8
+ spec.version = Knife::Hmc::VERSION
9
+ spec.authors = ["John J. Rofrano,Christopher M. Wood, John F. Hutchinson"]
10
+ spec.email = ["rofrano@us.ibm.com, woodc@us.ibm.com, jfhutchi@us.ibm.com"]
11
+ spec.summary = %q{IBM Hardware Management Console support for Chef's Knife Command}
12
+ spec.description = "Knife plugin for use with IBM Hardware Management Console"
13
+ spec.homepage = "http://github.com/pseries/knife-hmc"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency("rbvppc", "~> 1.0.0")
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.5"
24
+ spec.add_development_dependency "rake", "~> 0"
25
+ spec.required_ruby_version = '>= 2.1.0'
26
+ end
@@ -0,0 +1,131 @@
1
+ #
2
+ # Authors: Christopher M Wood (<woodc@us.ibm.com>)
3
+ # John F Hutchinson (<jfhutchi@us.ibm.com>)
4
+ # © Copyright IBM Corporation 2015.
5
+ #
6
+ # LICENSE: MIT (http://opensource.org/licenses/MIT)
7
+ #
8
+
9
+ require 'knife-hmc/version'
10
+
11
+ class Chef
12
+ class Knife
13
+ module HmcBase
14
+
15
+ # :nodoc:
16
+ #####################################################
17
+ # included
18
+ #####################################################
19
+ def self.included(includer)
20
+ includer.class_eval do
21
+
22
+ deps do
23
+ require 'chef/json_compat'
24
+ require 'chef/knife'
25
+ require 'readline'
26
+ require 'rbvppc'
27
+ require 'netaddr'
28
+ Chef::Knife.load_deps
29
+ end
30
+
31
+ option :hmc_host,
32
+ :short => "-h HOST",
33
+ :long => "--hmc_host HOST",
34
+ :description => "The fully qualified domain name of the HMC host",
35
+ :proc => Proc.new { |key| Chef::Config[:knife][:hmc_host] = key }
36
+
37
+ option :hmc_username,
38
+ :short => "-U USERNAME",
39
+ :long => "--hmc_user USERNAME",
40
+ :description => "The username for the HMC",
41
+ :proc => Proc.new { |key| Chef::Config[:knife][:hmc_username] = key }
42
+
43
+ option :hmc_password,
44
+ :short => "-P PASSWORD",
45
+ :long => "--hmc_pass PASSWORD",
46
+ :description => "The password for hmc",
47
+ :proc => Proc.new { |key| Chef::Config[:knife][:hmc_password] = key }
48
+
49
+ end
50
+ end
51
+
52
+ #####################################################
53
+ # tcp_ssh_alive
54
+ # Returns true if the hostname specified is
55
+ # accepting SSH connections. Returns false otherwise
56
+ #####################################################
57
+ def tcp_ssh_alive(hostname,port=22)
58
+ tcp_socket = TCPSocket.new(hostname, port)
59
+ readable = IO.select([tcp_socket], nil, nil, 5)
60
+ if readable
61
+ Chef::Log.debug("sshd accepting connections on #{hostname}, banner is #{tcp_socket.gets}")
62
+ true
63
+ else
64
+ false
65
+ end
66
+
67
+ rescue Errno::ETIMEDOUT
68
+ false
69
+ rescue Errno::EPERM
70
+ false
71
+ rescue Errno::ECONNREFUSED
72
+ sleep 2
73
+ false
74
+ rescue Errno::EHOSTUNREACH, Errno::ENETUNREACH
75
+ sleep 2
76
+ false
77
+ ensure
78
+ tcp_socket && tcp_socket.close
79
+ end
80
+
81
+ #####################################################
82
+ # validate!
83
+ #####################################################
84
+ def validate!(keys=[:hmc_host, :hmc_username, :hmc_password])
85
+ errors = []
86
+
87
+ keys.each do |k|
88
+ pretty_key = k.to_s.gsub(/_/, ' ').gsub(/\w+/){ |w| (w =~ /(ssh)|(aws)/i) ? w.upcase : w.capitalize }
89
+ if Chef::Config[:knife][k].nil? and config[k].nil?
90
+ errors << "You did not provide a valid '#{pretty_key}' value."
91
+ end
92
+ end
93
+
94
+ if errors.each{|e| ui.error(e)}.any?
95
+ exit 1
96
+ end
97
+ end
98
+
99
+ #####################################################
100
+ # validate - no exit on errors
101
+ #####################################################
102
+ def validate(keys)
103
+ errors = []
104
+
105
+ keys.each do |k|
106
+ pretty_key = k.to_s.gsub(/_/, ' ').gsub(/\w+/){ |w| (w =~ /(ssh)|(aws)/i) ? w.upcase : w.capitalize }
107
+ if Chef::Config[:knife][k].nil? and config[k].nil?
108
+ errors << "You did not provide a valid '#{pretty_key}' value."
109
+ end
110
+ end
111
+
112
+ if errors.empty?
113
+ return true
114
+ else
115
+ return false
116
+ end
117
+ end
118
+
119
+ #####################################################
120
+ # get config
121
+ #####################################################
122
+ def get_config(key)
123
+ key = key.to_sym
124
+ rval = config[key] || Chef::Config[:knife][key] || $default[key]
125
+ Chef::Log.debug("value for config item #{key}: #{rval}")
126
+ rval
127
+ end
128
+
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,116 @@
1
+ #
2
+ # Authors: Christopher M Wood (<woodc@us.ibm.com>)
3
+ # John F Hutchinson (<jfhutchi@us.ibm.com>)
4
+ # © Copyright IBM Corporation 2015.
5
+ #
6
+ # LICENSE: MIT (http://opensource.org/licenses/MIT)
7
+ #
8
+
9
+ require 'chef/knife/hmc_base'
10
+
11
+ class Chef
12
+ class Knife
13
+ class HmcDiskAdd < Knife
14
+
15
+ include Knife::HmcBase
16
+
17
+ banner "knife hmc disk add (options)"
18
+
19
+ option :frame_name,
20
+ :short => "-f NAME",
21
+ :long => "--frame NAME",
22
+ :description => "Name of the Host in which the LPAR resides."
23
+
24
+ option :lpar_name,
25
+ :short => "-l NAME",
26
+ :long => "--lpar",
27
+ :description => "Name of LPAR you wish to delete."
28
+
29
+ option :vio1_name,
30
+ :short => "-p NAME",
31
+ :long => "--primary_vio NAME",
32
+ :description => "Name of the primary vio."
33
+
34
+ option :vio2_name,
35
+ :short => "-s NAME",
36
+ :long => "--secondary_vio NAME",
37
+ :description => "Name of the secondary vio."
38
+
39
+ option :size,
40
+ :short => "-S SIZE",
41
+ :long => "--size_in_GB SIZE",
42
+ :description => "The size in GB you require, will find the size requested or larger."
43
+
44
+ option :volume_group,
45
+ :short => "-g NAME",
46
+ :long => "--volume_group NAME",
47
+ :description => "Name of volume group disk will be used in. If rootvg is passed then script will find a single LUN vs multiple smaller luns to fit request."
48
+
49
+ def run
50
+ Chef::Log.debug("Adding disk...")
51
+
52
+ validate!([:frame_name,:lpar_name,:vio2_name,:vio1_name,:size])
53
+
54
+ hmc = Hmc.new(get_config(:hmc_host), get_config(:hmc_username) , {:password => get_config(:hmc_password)})
55
+ hmc.connect
56
+
57
+ #Populate hash to make LPAR object
58
+ lpar_hash = hmc.get_lpar_options(get_config(:frame_name),get_config(:lpar_name))
59
+ #Create LPAR object based on hash, and VIO objects
60
+ lpar = Lpar.new(lpar_hash)
61
+ vio1 = Vio.new(hmc, get_config(:frame_name), get_config(:vio1_name))
62
+ vio2 = Vio.new(hmc, get_config(:frame_name), get_config(:vio2_name))
63
+
64
+ #Get vSCSI Information
65
+ lpar_vscsi = lpar.get_vscsi_adapters
66
+ first_slot = nil
67
+ second_slot = nil
68
+ adapter_cnt = 0
69
+
70
+ if lpar_vscsi.empty? == true
71
+ #Add vSCSI Adapters
72
+ lpar.add_vscsi(vio1)
73
+ lpar.add_vscsi(vio2)
74
+ lpar_vscsi = lpar.get_vscsi_adapters
75
+ else
76
+ lpar_vscsi.each do |adapter|
77
+ if adapter.remote_lpar_name == vio1.name
78
+ first_slot = adapter.remote_slot_num
79
+ adapter_cnt += 1
80
+ elsif adapter.remote_lpar_name == vio2.name
81
+ second_slot = adapter.remote_slot_num
82
+ adapter_cnt += 1
83
+ end
84
+ end
85
+
86
+ if first_slot.nil? or second_slot.nil? or adapter_cnt != 2
87
+ #Could not determine which vSCSIs to use
88
+ error = "Unable to determine which vSCSI adapters to use"
89
+ puts "#{error}"
90
+ ui.error(error)
91
+ exit 1
92
+ end
93
+ end
94
+
95
+ #Find the vHosts
96
+ first_vhost = vio1.find_vhost_given_virtual_slot(lpar_vscsi[0].remote_slot_num)
97
+ second_vhost = vio2.find_vhost_given_virtual_slot(lpar_vscsi[1].remote_slot_num)
98
+
99
+ #Check for volume group flag and add LUN to LPAR
100
+ if validate([:volume_group])
101
+ if get_config(:volume_group).to_s.downcase == "rootvg"
102
+ vio1.map_single_disk_by_size(first_vhost,vio2,second_vhost,get_config(:size).to_i)
103
+ puts "Successfully attached LUN to #{get_config(:lpar_name)}"
104
+ else
105
+ vio1.map_by_size(first_vhost,vio2,second_vhost,get_config(:size).to_i)
106
+ puts "Successfully attached LUN(s) to #{get_config(:lpar_name)}"
107
+ end
108
+ else
109
+ vio1.map_by_size(first_vhost,vio2,second_vhost,get_config(:size).to_i)
110
+ puts "Successfully attached LUN(s) to #{get_config(:lpar_name)}"
111
+ end
112
+ hmc.disconnect
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,173 @@
1
+ #
2
+ # Authors: Christopher M Wood (<woodc@us.ibm.com>)
3
+ # John F Hutchinson (<jfhutchi@us.ibm.com>)
4
+ # © Copyright IBM Corporation 2015.
5
+ #
6
+ # LICENSE: MIT (http://opensource.org/licenses/MIT)
7
+ #
8
+
9
+ require 'chef/knife/hmc_base'
10
+
11
+ class Chef
12
+ class Knife
13
+ class HmcDiskList < Knife
14
+
15
+ include Knife::HmcBase
16
+
17
+ banner "knife hmc disk list -r VIONAME1 -s VIONAME2 -f FRAMENAME [-l LPARNAME | -a | -x]"
18
+
19
+ option :primary_vio,
20
+ :short => "-r VIONAME",
21
+ :long => "--primary_vio VIONAME",
22
+ :description => "The LPAR name of the Primary VIO"
23
+
24
+ option :secondary_vio,
25
+ :short => "-s VIONAME",
26
+ :long => "--secondary_vio VIONAME",
27
+ :description => "The LPAR name of the Secondary VIO"
28
+
29
+ option :frame,
30
+ :short => "-f FRAMENAME",
31
+ :long => "--frame FRAMENAME",
32
+ :description => "The name of the Frame in which the VIOs reside"
33
+
34
+ option :lpar,
35
+ :short => "-l LPARNAME",
36
+ :long => "--lpar LPARNAME",
37
+ :description => "The name of the LPAR whose disks should be listed (optional)"
38
+
39
+ option :only_available,
40
+ :short => "-a",
41
+ :long => "--available",
42
+ :boolean => true,
43
+ :default => false,
44
+ :description => "List ONLY the available disks in this VIO pair (optional)"
45
+
46
+ option :only_used,
47
+ :short => "-x",
48
+ :long => "--used",
49
+ :boolean => true,
50
+ :default => false,
51
+ :description => "List ONLY the used disks in this VIO pair (optional)"
52
+
53
+
54
+ def run
55
+ Chef::Log.debug("Listing disks...")
56
+
57
+ validate!
58
+ hmc = Hmc.new(get_config(:hmc_host), get_config(:hmc_username) , {:password => get_config(:hmc_password)})
59
+ hmc.connect
60
+
61
+ validate!([:primary_vio,:secondary_vio,:frame])
62
+
63
+ frame = get_config(:frame)
64
+ primary_vio_name = get_config(:primary_vio)
65
+ secondary_vio_name = get_config(:secondary_vio)
66
+
67
+ #Make Vio objects for the two VIOs
68
+ primary_vio = Vio.new(hmc,frame,primary_vio_name)
69
+ secondary_vio = Vio.new(hmc,frame,secondary_vio_name)
70
+
71
+ #Arrays that will hold the disks to list
72
+ vio1_disks = []
73
+ vio2_disks = []
74
+
75
+ if validate([:lpar])
76
+ #Show only disks attached to the specified LPAR
77
+ lpar_name = get_config(:lpar)
78
+ options_hash = hmc.get_lpar_options(frame,lpar_name)
79
+ lpar = Lpar.new(options_hash)
80
+
81
+ #Get the vSCSIs from this LPAR and determine the virtual adapter
82
+ #slots used by each VIO
83
+ vscsi_adapters = lpar.get_vscsi_adapters
84
+ primary_vio_slot = nil
85
+ secondary_vio_slot = nil
86
+ adapter_cnt=0
87
+ vscsi_adapters.each do |adapter|
88
+ if adapter.remote_lpar_name == primary_vio.name
89
+ primary_vio_slot = adapter.remote_slot_num
90
+ adapter_cnt += 1
91
+ elsif adapter.remote_lpar_name == secondary_vio.name
92
+ secondary_vio_slot = adapter.remote_slot_num
93
+ adapter_cnt += 1
94
+ end
95
+ end
96
+
97
+ if primary_vio_slot.nil? or secondary_vio_slot.nil? or adapter_cnt != 2
98
+ #Could not determine which vSCSIs to use
99
+ error = "Unable to determine which vSCSI adapters have storage attached to it from #{primary_vio_name} and #{secondary_vio_name}\n" +
100
+ "Cannot list disks attached to #{lpar_name}"
101
+ puts "#{error}"
102
+ ui.error(error)
103
+ exit 1
104
+ end
105
+
106
+ #Find the vhosts that hold this LPARs disks
107
+ primary_vhost = primary_vio.find_vhost_given_virtual_slot(primary_vio_slot)
108
+ secondary_vhost = secondary_vio.find_vhost_given_virtual_slot(secondary_vio_slot)
109
+
110
+ #Get the names (known to the VIOs) of the disks attached to the LPAR
111
+ vio1_disks = primary_vio.get_attached_disks(primary_vhost)
112
+ vio2_disks = secondary_vio.get_attached_disks(secondary_vhost)
113
+ elsif get_config(:only_available)
114
+ #Show only available disks
115
+ vio1_disks = primary_vio.available_disks
116
+ vio2_disks = secondary_vio.available_disks
117
+ elsif get_config(:only_used)
118
+ #Show only used disks
119
+ vio1_disks = primary_vio.used_disks
120
+ vio2_disks = secondary_vio.used_disks
121
+ else
122
+ #None of :lpar, :only_available, and :only_used options were specified.
123
+ #Show used *and* available disks
124
+ vio1_disks = primary_vio.available_disks + primary_vio.used_disks
125
+ vio2_disks = secondary_vio.available_disks + secondary_vio.used_disks
126
+ end
127
+
128
+ #List the disks populated in vio1_disks and vio2_disks
129
+ print_header
130
+
131
+ vio1_disks.each do |v1_disk|
132
+ vio2_disks.each do |v2_disk|
133
+ if v1_disk == v2_disk
134
+ print_line(v1_disk,v2_disk)
135
+ end
136
+ end
137
+ end
138
+
139
+ hmc.disconnect
140
+
141
+ end
142
+
143
+ ##################################################
144
+ # print_header
145
+ # => Prints table header for disk list
146
+ ##################################################
147
+ def print_header
148
+ if validate([:lpar])
149
+ puts "Listing information on all disks attached to #{get_config(:lpar)}\n"
150
+ elsif get_config(:only_available)
151
+ puts "Listing only available disks on this VIO Pair\n"
152
+ elsif get_config(:only_used)
153
+ puts "Listing only used disks on this VIO Pair\n"
154
+ else
155
+ puts "Listing all disks on this VIO Pair\n"
156
+ end
157
+
158
+ printf "%-20s %10s %20s %20s\n", "PVID", "Size (MB)", "Name (on #{get_config(:primary_vio)})", "Name (on #{get_config(:secondary_vio)})"
159
+ printf "-----------------------------------------------------------------------------------------\n"
160
+ end
161
+
162
+ ##################################################
163
+ # print_line
164
+ # => Prints a single line of the output table
165
+ # given two Lun objects representing the same
166
+ # disk on a pair of VIOs
167
+ ##################################################
168
+ def print_line(vio1_disk,vio2_disk)
169
+ printf "%-20s %10s %20s %20s\n", vio1_disk.pvid, "#{vio1_disk.size_in_mb} MB", vio1_disk.name, vio2_disk.name
170
+ end
171
+ end
172
+ end
173
+ end