From 83e032f3a16861d538b6eae7e1ed781d90783fa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Mon, 5 Jun 2023 11:07:25 +0100 Subject: [PATCH] Add separate class for managing zFCP --- src/lib/y2s390/zfcp.rb | 183 +++++++++++++++++++++++++++++ test/y2s390/zfcp_test.rb | 244 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 427 insertions(+) create mode 100644 src/lib/y2s390/zfcp.rb create mode 100755 test/y2s390/zfcp_test.rb diff --git a/src/lib/y2s390/zfcp.rb b/src/lib/y2s390/zfcp.rb new file mode 100644 index 00000000..6735243b --- /dev/null +++ b/src/lib/y2s390/zfcp.rb @@ -0,0 +1,183 @@ +# Copyright (c) [2023] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "yast" +require "yaml" + +module Y2S390 + # Manager for zFCP devices + class ZFCP + include Yast + include Yast::Logger + + # Detected controllers + # + # @return [Array] keys for each hash: + # "sysfs_bus_id", "resource" + attr_reader :controllers + + # Detected LUN disks + # + # @return [Array] keys for each hash: + # "dev_name", "detail", "vendor", "device" and "io". + attr_reader :disks + + def initialize + @controllers = [] + @disks = [] + end + + # Probes the zFCP controllers + def probe_controllers + make_all_devices_visible + + storage_devices = Yast::SCR.Read(path(".probe.storage")) + controllers = storage_devices.select { |i| i["device"] == "zFCP controller" } + + @controllers = controllers.map { |c| c.slice("sysfs_bus_id", "resource") } + end + + # Probes the zFCP disks + def probe_disks + storage_disks = read_mock_disks || Yast::SCR.Read(path(".probe.disk")) + zfcp_disks = storage_disks.select { |d| d["driver"] == "zfcp" } + + tapes = Yast::SCR.Read(path(".probe.tape")) + scsi_tapes = tapes.select { |t| t["bus"] == "SCSI" } + + disks = zfcp_disks + scsi_tapes + + @disks = disks.map { |d| d.slice("dev_name", "detail", "vendor", "device", "io") } + end + + # Runs the command for activating a controller + # + # @note All LUNs are automatically activated if "allow_lun_scan" is active, see + # https://www.ibm.com/docs/en/linux-on-systems?topic=wsd-configuring-devices. + # + # @param channel [String] E.g., "0.0.fa00" + # @return [Hash] See {#run}. The exit code corresponds to the chzdev one. + def activate_controller(channel) + command = format("/sbin/zfcp_host_configure '%s' %d", channel, 1) + run(command) + end + + # Whether the controller is activated + # + # @param channel [String] E.g., "0.0.fa00" + # @return [Boolean] + def activated_controller?(channel) + controller = controllers.find { |c| c["sysfs_bus_id"] == channel } + return false unless controller + + io = controller.dig("resource", "io") || [] + io.any? { |i| i["active"] } + end + + # Runs the command for activating a zFCP disk + # + # @param channel [String] E.g., "0.0.fa00" + # @param wwpn [String] E.g., "0x500507630708d3b3" + # @param lun [String] E.g., "0x0013000000000000" + # + # @return [Hash] See {#run}. The exit code corresponds to the chzdev one. + def activate_disk(channel, wwpn, lun) + command = format("/sbin/zfcp_disk_configure '%s' '%s' '%s' %d", channel, wwpn, lun, 1) + run(command) + end + + # Runs the command for deactivating a zFCP disk + # + # @note Deactivate fails if "allow_lun_scan" is active, see + # https://www.ibm.com/docs/en/linux-on-systems?topic=wsd-configuring-devices. + # + # @param channel [String] E.g., "0.0.fa00" + # @param wwpn [String] E.g., "0x500507630708d3b3" + # @param lun [String] E.g., "0x0013000000000000" + # + # @return [Hash] See {#run}. The exit code corresponds to the chzdev one. + def deactivate_disk(channel, wwpn, lun) + command = format("/sbin/zfcp_disk_configure '%s' '%s' '%s' %d", channel, wwpn, lun, 0) + run(command) + end + + # Runs the command for finding WWPNs + # + # @param channel [String] E.g., "0.0.fa00" + # @return [Hash] See {#run} + def find_wwpns(channel) + command = format("zfcp_san_disc -b '%s' -W", channel) + run(command) + end + + # Runs the command for finding LUNs + # + # @param channel [String] E.g., "0.0.fa00" + # @param wwpn [String] E.g., "0x500507630708d3b3" + # + # @return [Hash] See {#run} + def find_luns(channel, wwpn) + command = format("zfcp_san_disc -b '%s' -p '%s' -L", channel, wwpn) + run(command) + end + + private + + # Sets all detected controllers as visible + def make_all_devices_visible + # Checking if it is a z/VM and evaluating all FCP controllers in order to activate + output = run("/sbin/vmcp q v fcp") + return if output["exit"] != 0 + + fcp_lines = output["stdout"].map(&:split).select { |l| l.first == "FCP" } + devices = fcp_lines.map { |l| l[1].downcase } + + # Remove all needed devices from CIO device driver blacklist in order to see it + devices.each do |device| + log.info "Removing #{device} from the CIO device driver blacklist" + run("/sbin/cio_ignore -r #{device}") + end + end + + # Runs the given command + # + # @param command [String] + # @return [Hash] Output of the command which has these keys: "exit", "stdout", "stderr". + def run(command) + Yast::SCR.Execute(path(".target.bash_output"), command).tap do |output| + log.info("command #{command} output #{output}") + + output["exit"] = output["exit"].to_i + output["stdout"] = output["stdout"].split("\n").reject(&:empty?) + end + end + + # Reads the mock disks from the YAML file pointed by YAST2_S390_PROBE_DISK + # + # Suggestion: YAST2_S390_PROBE_DISK=test/data/probe_disk.yml rake run[zfcp] + # + # @return [Array, nil] List of mocked LUN disks or nil if no file found + def read_mock_disks + mock_filename = ENV["YAST2_S390_PROBE_DISK"] + return nil unless mock_filename + + YAML.safe_load(File.read(mock_filename)) + end + end +end diff --git a/test/y2s390/zfcp_test.rb b/test/y2s390/zfcp_test.rb new file mode 100755 index 00000000..8fdbcd7a --- /dev/null +++ b/test/y2s390/zfcp_test.rb @@ -0,0 +1,244 @@ +#!/usr/bin/env rspec + +# Copyright (c) [2023] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../test_helper" +require "y2s390/zfcp" + +describe Y2S390::ZFCP do + describe "#probe_controllers" do + before do + allow(Yast::SCR).to receive(:Read).with(Yast.path(".probe.storage")) + .and_return(controllers_data) + + allow(Yast::SCR).to receive(:Execute).with(anything, /\/sbin\/vmcp q v fcp/) + .and_return(vmcp_output) + + allow(Yast::SCR).to receive(:Execute).with(anything, /\/sbin\/cio_ignore -r/) + .and_return("exit" => 0, "stdout" => "") + end + + let(:controllers_data) { load_data("probe_storage.yml") } + + let(:vmcp_output) do + { + "exit" => 0, + "stdout" => "FCP F800 ON FCP F807 CHPID 1C SUBCHANNEL = 000B\n" \ + "F800 TOKEN = 0000000362A42C00" + } + end + + it "removes all FCP devices from the blacklist" do + expect(Yast::SCR).to receive(:Execute).with(anything, /\/sbin\/cio_ignore -r f800/) + + subject.probe_controllers + end + + it "reads all the zFCP controllers" do + expect(subject.controllers).to eq([]) + + subject.probe_controllers + + expect(subject.controllers).to contain_exactly( + hash_including("sysfs_bus_id" => "0.0.f800"), + hash_including("sysfs_bus_id" => "0.0.f900"), + hash_including("sysfs_bus_id" => "0.0.fa00"), + hash_including("sysfs_bus_id" => "0.0.fc00") + ) + end + end + + describe "#probe_disks" do + before do + allow(Yast::SCR).to receive(:Read).with(Yast.path(".probe.disk")) + .and_return(disks_data) + + allow(Yast::SCR).to receive(:Read).with(Yast.path(".probe.tape")).and_return([]) + end + + let(:disks_data) { load_data("probe_disk.yml") } + + it "reads all the zFCP disks" do + expect(subject.disks).to eq([]) + + subject.probe_disks + + expect(subject.disks).to contain_exactly( + hash_including("dev_name" => "/dev/sda"), + hash_including("dev_name" => "/dev/sdb") + ) + end + end + + describe "#activate_controller" do + before do + allow(Yast::SCR).to receive(:Execute).with(anything, command).and_return(output) + end + + let(:command) { /\/sbin\/zfcp_host_configure '0.0.fc00' 1/ } + + let(:output) { { "exit" => 0, "stdout" => "" } } + + it "tries to activate the given controller" do + expect(Yast::SCR).to receive(:Execute).with(anything, command) + + subject.activate_controller("0.0.fc00") + end + + it "returns the output of the command" do + result = subject.activate_controller("0.0.fc00") + + expect(result).to eq(output) + end + end + + describe "#activated_controller?" do + before do + allow(subject).to receive(:controllers).and_return(controllers) + end + + let(:controllers) do + [ + { + "sysfs_bus_id" => "0.0.fa00", + "resource" => { "io" => [{ "active" => true }] } + }, + { + "sysfs_bus_id" => "0.0.fc00", + "resource" => { "io" => [{ "active" => false }] } + } + ] + end + + context "if the given controller is activated" do + let(:channel) { "0.0.fa00" } + + it "returns true" do + expect(subject.activated_controller?(channel)).to eq(true) + end + end + + context "if the given controller is not activated" do + let(:channel) { "0.0.fc00" } + + it "returns false" do + expect(subject.activated_controller?(channel)).to eq(false) + end + end + end + + describe "#activate_disk" do + before do + allow(Yast::SCR).to receive(:Execute).with(anything, command).and_return(output) + end + + let(:command) do + /\/sbin\/zfcp_disk_configure '0.0.fc00' '0x500507630708d3b3' '0x0000000000000005' 1/ + end + + let(:output) { { "exit" => 1, "stdout" => "An error" } } + + it "tries to activate a zFCP disk" do + expect(Yast::SCR).to receive(:Execute).with(anything, command) + + subject.activate_disk("0.0.fc00", "0x500507630708d3b3", "0x0000000000000005") + end + + it "returns the output of the command" do + result = subject.activate_disk("0.0.fc00", "0x500507630708d3b3", "0x0000000000000005") + + expect(result).to eq(output) + end + end + + describe "#deactivate_disk" do + before do + allow(Yast::SCR).to receive(:Execute).with(anything, command).and_return(output) + end + + let(:command) do + /\/sbin\/zfcp_disk_configure '0.0.fc00' '0x500507630708d3b3' '0x0000000000000005' 0/ + end + + let(:output) { { "exit" => 0, "stdout" => "" } } + + it "tries to deactivate a zFCP disk" do + expect(Yast::SCR).to receive(:Execute).with(anything, command) + + subject.deactivate_disk("0.0.fc00", "0x500507630708d3b3", "0x0000000000000005") + end + + it "returns the output of the command" do + result = subject.deactivate_disk("0.0.fc00", "0x500507630708d3b3", "0x0000000000000005") + + expect(result).to eq(output) + end + end + + describe "#find_wwpns" do + before do + allow(Yast::SCR).to receive(:Execute).with(anything, command).and_return(output) + end + + let(:command) { /zfcp_san_disc -b '0.0.fc00' -W/ } + + let(:output) { { "exit" => 0, "stdout" => "0x500507630703d3b3\n0x500507630708d3b3" } } + + it "runs the command for finding WWPNs" do + expect(Yast::SCR).to receive(:Execute).with(anything, command) + + subject.find_wwpns("0.0.fc00") + end + + it "returns the output of the command" do + result = subject.find_wwpns("0.0.fc00") + + expect(result).to eq( + "exit" => 0, + "stdout" => ["0x500507630703d3b3", "0x500507630708d3b3"] + ) + end + end + + describe "#find_luns" do + before do + allow(Yast::SCR).to receive(:Execute).with(anything, command).and_return(output) + end + + let(:command) { /zfcp_san_disc -b '0.0.fc00' -p '0x500507630708d3b3' -L/ } + + let(:output) { { "exit" => 0, "stdout" => "0x0000000000000005\n0x0000000000000006" } } + + it "runs the command for finding LUNs" do + expect(Yast::SCR).to receive(:Execute).with(anything, command) + + subject.find_luns("0.0.fc00", "0x500507630708d3b3") + end + + it "returns the output of the command" do + result = subject.find_luns("0.0.fc00", "0x500507630708d3b3") + + expect(result).to eq( + "exit" => 0, + "stdout" => ["0x0000000000000005", "0x0000000000000006"] + ) + end + end +end