knife-hmc 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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