diff --git a/debian/control b/debian/control
index cdbddb3..76fd21c 100644
--- a/debian/control
+++ b/debian/control
@@ -53,6 +53,7 @@ Depends: probert-common (= ${source:Version}),
lvm2,
mdadm,
multipath-tools,
+ s390-tools [s390x],
zfsutils-linux,
${misc:Depends},
${python3:Depends},
diff --git a/probert/dasd.py b/probert/dasd.py
new file mode 100644
index 0000000..ab7d461
--- /dev/null
+++ b/probert/dasd.py
@@ -0,0 +1,143 @@
+# Copyright 2020 Canonical, Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import logging
+import os
+import platform
+import pyudev
+import re
+import subprocess
+
+log = logging.getLogger('probert.dasd')
+
+DASD_FORMAT = r"^format\s+:.+\s+(?P\w+\s\w+)$"
+DASD_BLKSIZE = r"^blocksize\s+:\shex\s\w+\s+dec\s(?P\d+)$"
+
+
+def _search(regex, content, groupkey):
+ m = re.search(regex, content, re.MULTILINE)
+ if m:
+ return m.group(groupkey)
+
+
+def blocksize(dasdview_output):
+ """ Read and return device_id's 'blocksize' value.
+
+ :param: device_id: string of device ccw bus_id.
+ :returns: int: the device's current blocksize.
+ """
+ if not dasdview_output:
+ return
+
+ blksize = _search(DASD_BLKSIZE, dasdview_output, 'blksize')
+ if blksize:
+ return int(blksize)
+
+
+def disk_format(dasdview_output):
+ """ Read and return specified device "disk_layout" value.
+
+ :returns: string: One of ['cdl', 'ldl', 'not-formatted'].
+ :raises: ValueError if dasdview result missing 'format' section.
+
+ """
+ if not dasdview_output:
+ return
+
+ mapping = {
+ 'cdl formatted': 'cdl',
+ 'ldl formatted': 'ldl',
+ 'not formatted': 'not-formatted',
+ }
+ diskfmt = _search(DASD_FORMAT, dasdview_output, 'format')
+ if diskfmt:
+ return mapping.get(diskfmt.lower())
+
+
+def dasdview(devname):
+ ''' Run dasdview on devname and return the output.
+
+ dasdview --extended has 3 sections
+ general (2:6), geometry (8:12), extended (14:)
+
+ '''
+ if not os.path.exists(devname):
+ raise ValueError("Invalid dasd device name: '%s'" % devname)
+
+ cmd = ['dasdview', '--extended', devname]
+ try:
+ result = subprocess.run(cmd, stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL)
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ log.error('Failed to run cmd: %s', cmd)
+ return None
+
+ return result.stdout.decode('utf-8')
+
+
+def get_dasd_info(device):
+ """ from a udev blockdev device entry, return all required dasd info
+
+ """
+ name = device.get('DEVNAME')
+ device_id = device.get('ID_PATH', '').replace('ccw-', '')
+ dasdview_output = dasdview(name)
+ diskfmt = disk_format(dasdview_output)
+ blksize = blocksize(dasdview_output)
+ if not all([name, device_id, diskfmt, blksize]):
+ vals = ("name=%s device_id=%s format=%s blksize=%s" % (
+ name, device_id, diskfmt, blksize))
+ log.debug('Failed to probe some DASD values: %s', vals)
+ return None
+
+ return {'name': name, 'device_id': device_id,
+ 'disk_layout': diskfmt, 'blocksize': blksize}
+
+
+def probe(context=None):
+ """Examine all dasd devices present and extract configuration attributes
+
+ This data is useful for determining if the dasd device has been
+ formatted, if so what the block size, the partition layout used
+ and the s390x device_id used to uniquely identify the device.
+ """
+ log.debug('Probing DASD devies')
+ machine = platform.machine()
+ if machine != "s390x":
+ log.debug('DASD devices only present on s390x, arch=%s', machine)
+ return {}
+
+ dasds = {}
+ if not context:
+ context = pyudev.Context()
+
+ for device in context.list_devices(subsystem='block'):
+ # dasd devices have MAJOR 94
+ if device['MAJOR'] != "94":
+ continue
+ # ignore dasd partitions
+ if 'PARTN' in device:
+ continue
+
+ try:
+ dasd_info = get_dasd_info(device)
+ except ValueError as e:
+ log.error('Error probing dasd device %s: %s', device['DEVNAME'], e)
+ dasd_info = None
+
+ if dasd_info:
+ dasds[device['DEVNAME']] = dasd_info
+
+ return dasds
diff --git a/probert/storage.py b/probert/storage.py
index a27ebe1..1b1eb05 100644
--- a/probert/storage.py
+++ b/probert/storage.py
@@ -19,7 +19,7 @@
import subprocess
from probert.utils import udev_get_attributes, read_sys_block_size_bytes
-from probert import (bcache, dmcrypt, filesystem, lvm, mount, multipath,
+from probert import (bcache, dasd, dmcrypt, filesystem, lvm, mount, multipath,
raid, zfs)
log = logging.getLogger('probert.storage')
@@ -145,6 +145,7 @@ class Storage():
probe_map = {
'bcache': bcache.probe,
'blockdev': blockdev_probe,
+ 'dasd': dasd.probe,
'dmcrypt': dmcrypt.probe,
'filesystem': filesystem.probe,
'lvm': lvm.probe,
diff --git a/probert/tests/data/dasdd.view b/probert/tests/data/dasdd.view
new file mode 100644
index 0000000..23ab64e
--- /dev/null
+++ b/probert/tests/data/dasdd.view
@@ -0,0 +1,52 @@
+
+--- general DASD information --------------------------------------------------
+device node : /dev/dasdd
+busid : 0.0.1544
+type : ECKD
+device type : hex 3390 dec 13200
+
+--- DASD geometry -------------------------------------------------------------
+number of cylinders : hex 7563 dec 30051
+tracks per cylinder : hex f dec 15
+blocks per track : hex c dec 12
+blocksize : hex 1000 dec 4096
+
+--- extended DASD information -------------------------------------------------
+real device number : hex 0 dec 0
+subchannel identifier : hex 1a4 dec 420
+CU type (SenseID) : hex 3990 dec 14736
+CU model (SenseID) : hex e9 dec 233
+device type (SenseID) : hex 3390 dec 13200
+device model (SenseID) : hex c dec 12
+open count : hex 2 dec 2
+req_queue_len : hex 0 dec 0
+chanq_len : hex 0 dec 0
+status : hex 5 dec 5
+label_block : hex 2 dec 2
+FBA_layout : hex 0 dec 0
+characteristics_size : hex 40 dec 64
+confdata_size : hex 100 dec 256
+format : hex 2 dec 2 CDL formatted
+features : hex 0 dec 0 default
+
+characteristics : 3990e933 900c5e0c 39f72032 7563000f
+ e000e5a2 05940222 13090674 00000000
+ 00000000 00000000 32321502 dfee0001
+ 0677080f 007f4800 1f3c0000 00007563
+
+configuration_data : dc010100 f0f0f2f1 f0f7f9f0 f0c9c2d4
+ f7f5f0f0 f0f0f0f0 f0c4e7d7 f7f10844
+ d4020000 f0f0f2f1 f0f7f9f6 f1c9c2d4
+ f7f5f0f0 f0f0f0f0 f0c4e7d7 f7f10800
+ d0000000 f0f0f2f1 f0f7f9f6 f1c9c2d4
+ f7f5f0f0 f0f0f0f0 f0c4e7d7 f7f00800
+ f0000001 f0f0f2f1 f0f7f9f0 f0c9c2d4
+ f7f5f0f0 f0f0f0f0 f0c4e7d7 f7f10800
+ 00000000 00000000 00000000 00000000
+ 00000000 00000000 00000000 00000000
+ 00000000 00000000 00000000 00000000
+ 00000000 00000000 00000000 00000000
+ 00000000 00000000 00000000 00000000
+ 00000000 00000000 00000000 00000000
+ 81000003 2d001e00 15000247 000c0016
+ 000cc044 0a0f61ca 00030000 0000a000
diff --git a/probert/tests/data/dasde.view b/probert/tests/data/dasde.view
new file mode 100644
index 0000000..c7090f1
--- /dev/null
+++ b/probert/tests/data/dasde.view
@@ -0,0 +1,52 @@
+
+--- general DASD information --------------------------------------------------
+device node : /dev/dasde
+busid : 0.0.2520
+type : ECKD
+device type : hex 3390 dec 13200
+
+--- DASD geometry -------------------------------------------------------------
+number of cylinders : hex 2721 dec 10017
+tracks per cylinder : hex f dec 15
+blocks per track : hex 0 dec 0
+blocksize : hex 200 dec 512
+
+--- extended DASD information -------------------------------------------------
+real device number : hex 0 dec 0
+subchannel identifier : hex 5e0 dec 1504
+CU type (SenseID) : hex 3990 dec 14736
+CU model (SenseID) : hex e9 dec 233
+device type (SenseID) : hex 3390 dec 13200
+device model (SenseID) : hex c dec 12
+open count : hex 1 dec 1
+req_queue_len : hex 0 dec 0
+chanq_len : hex 0 dec 0
+status : hex 3 dec 3
+label_block : hex 2 dec 2
+FBA_layout : hex 1 dec 1
+characteristics_size : hex 40 dec 64
+confdata_size : hex 100 dec 256
+format : hex 0 dec 0 NOT formatted
+features : hex 0 dec 0 default
+
+characteristics : 3990e933 900c5e0c 39f72032 2721000f
+ e000e5a2 05940222 13090674 00000000
+ 00000000 00000000 32321502 dfee0001
+ 0677080f 007f4800 1f3c0000 00002721
+
+configuration_data : dc010100 f0f0f2f1 f0f7f9f0 f0c9c2d4
+ f7f5f0f0 f0f0f0f0 f0c4e7d7 f7f10920
+ d4020000 f0f0f2f1 f0f7f9f6 f1c9c2d4
+ f7f5f0f0 f0f0f0f0 f0c4e7d7 f7f10900
+ d0000000 f0f0f2f1 f0f7f9f6 f1c9c2d4
+ f7f5f0f0 f0f0f0f0 f0c4e7d7 f7f00900
+ f0000001 f0f0f2f1 f0f7f9f0 f0c9c2d4
+ f7f5f0f0 f0f0f0f0 f0c4e7d7 f7f10900
+ 00000000 00000000 00000000 00000000
+ 00000000 00000000 00000000 00000000
+ 00000000 00000000 00000000 00000000
+ 00000000 00000000 00000000 00000000
+ 00000000 00000000 00000000 00000000
+ 00000000 00000000 00000000 00000000
+ 81000003 2d001e00 25000246 000c0016
+ 000cc020 84228958 00030000 0000a000
diff --git a/probert/tests/test_dasd.py b/probert/tests/test_dasd.py
new file mode 100644
index 0000000..f3b4377
--- /dev/null
+++ b/probert/tests/test_dasd.py
@@ -0,0 +1,171 @@
+import mock
+import subprocess
+import testtools
+
+from probert import dasd
+from probert.tests import fakes
+from probert.tests.helpers import random_string
+
+
+class TestDasd(testtools.TestCase):
+
+ def _load_test_data(self, data_fname):
+ testfile = fakes.TEST_DATA + '/' + data_fname
+ with open(testfile, 'r') as fh:
+ return fh.read()
+
+ @mock.patch('probert.dasd.os.path.exists')
+ @mock.patch('probert.dasd.subprocess.run')
+ def test_dasdview_returns_stdout(self, m_run, m_exists):
+ devname = random_string()
+ dasdview_out = random_string()
+ cp = subprocess.CompletedProcess(args=['foo'], returncode=0,
+ stdout=dasdview_out.encode('utf-8'),
+ stderr="")
+ m_run.return_value = cp
+ m_exists.return_value = True
+ result = dasd.dasdview(devname)
+ self.assertEqual(dasdview_out, result)
+ m_run.assert_called_with(['dasdview', '--extended', devname],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL)
+
+ @mock.patch('probert.dasd.os.path.exists')
+ @mock.patch('probert.dasd.subprocess.run')
+ def test_dasdview_raises_valueerror(self, m_run, m_exists):
+ devname = random_string()
+ m_exists.return_value = False
+ self.assertRaises(ValueError, dasd.dasdview, devname)
+ self.assertEqual(0, m_run.call_count)
+
+ @mock.patch('probert.dasd.os.path.exists')
+ @mock.patch('probert.dasd.subprocess.run')
+ def test_dasdview_returns_none_on_subprocess_error(self, m_run, m_exists):
+ devname = random_string()
+ m_exists.return_value = True
+ m_run.side_effect = subprocess.CalledProcessError(
+ cmd=[random_string()], returncode=1)
+ self.assertEqual(None, dasd.dasdview(devname))
+
+ def test_dasd_parses_blocksize(self):
+ self.assertEqual(4096,
+ dasd.blocksize(self._load_test_data('dasdd.view')))
+
+ def test_dasd_blocksize_returns_none_on_invalid_output(self):
+ self.assertIsNone(dasd.blocksize(random_string()))
+
+ def test_dasd_parses_disk_format(self):
+ self.assertEqual('cdl',
+ dasd.disk_format(self._load_test_data('dasdd.view')))
+ self.assertEqual('not-formatted',
+ dasd.disk_format(self._load_test_data('dasde.view')))
+
+ def test_dasd_parses_disk_format_ldl(self):
+ output = "format : hex 1 dec 1 LDL formatted"
+ self.assertEqual('ldl', dasd.disk_format(output))
+
+ def test_dasd_disk_format_returns_none_on_invalid_output(self):
+ self.assertIsNone(dasd.disk_format(random_string()))
+
+ @mock.patch('probert.dasd.dasdview')
+ def test_get_dasd_info(self, m_dview):
+ devname = random_string()
+ id_path = random_string()
+ device = {'DEVNAME': devname, 'ID_PATH': 'ccw-' + id_path}
+ m_dview.return_value = self._load_test_data('dasdd.view')
+ self.assertEqual({'name': devname, 'device_id': id_path,
+ 'disk_layout': 'cdl', 'blocksize': 4096},
+ dasd.get_dasd_info(device))
+
+ @mock.patch('probert.dasd.dasdview')
+ def test_get_dasd_info_returns_none_if_not_all(self, m_dview):
+ devname = random_string()
+ id_path = random_string()
+ device = {'DEVNAME': devname, 'ID_PATH': 'ccw-' + id_path}
+ m_dview.return_value = random_string()
+ self.assertIsNone(dasd.get_dasd_info(device))
+
+ @mock.patch('probert.dasd.blocksize')
+ @mock.patch('probert.dasd.dasdview')
+ def test_get_dasd_info_returns_none_if_bad_blocksize(self, m_dview,
+ m_block):
+ devname = random_string()
+ id_path = random_string()
+ device = {'DEVNAME': devname, 'ID_PATH': 'ccw-' + id_path}
+ m_dview.return_value = self._load_test_data('dasdd.view')
+ m_block.return_value = None
+ self.assertIsNone(dasd.get_dasd_info(device))
+
+ @mock.patch('probert.dasd.blocksize')
+ @mock.patch('probert.dasd.dasdview')
+ def test_get_dasd_info_returns_none_if_bad_disk_format(self, m_dview,
+ m_disk):
+ devname = random_string()
+ id_path = random_string()
+ device = {'DEVNAME': devname, 'ID_PATH': 'ccw-' + id_path}
+ m_dview.return_value = self._load_test_data('dasdd.view')
+ m_disk.return_value = None
+ self.assertIsNone(dasd.get_dasd_info(device))
+
+ @mock.patch('probert.dasd.platform.machine')
+ def test_dasd_probe_returns_empty_dict_non_s390x_arch(self, m_machine):
+ machine = random_string()
+ self.assertNotEqual("s390x", machine)
+ m_machine.return_value = machine
+ self.assertEqual({}, dasd.probe())
+
+ @mock.patch('probert.dasd.platform.machine')
+ @mock.patch('probert.dasd.dasdview')
+ def test_dasd_probe_dasdd(self, m_dasdview, m_machine):
+ m_machine.return_value = 's390x'
+ m_dasdview.side_effect = iter([self._load_test_data('dasdd.view')])
+
+ context = mock.MagicMock()
+ context.list_devices.side_effect = iter([
+ [{"MAJOR": "94", "DEVNAME": "/dev/dasdd", "ID_SERIAL": "0X1544",
+ "ID_PATH": "ccw-0.0.1544"}],
+ ])
+ expected_results = {
+ '/dev/dasdd': {
+ 'name': '/dev/dasdd', 'device_id': '0.0.1544',
+ 'disk_layout': 'cdl', 'blocksize': 4096},
+ }
+ self.assertEqual(expected_results, dasd.probe(context=context))
+
+ @mock.patch('probert.dasd.platform.machine')
+ @mock.patch('probert.dasd.dasdview')
+ def test_dasd_probe_dasde(self, m_dasdview, m_machine):
+ m_machine.return_value = 's390x'
+ m_dasdview.side_effect = iter([self._load_test_data('dasde.view')])
+
+ context = mock.MagicMock()
+ context.list_devices.side_effect = iter([
+ [{"MAJOR": "94", "DEVNAME": "/dev/dasde",
+ "ID_PATH": "ccw-0.0.2250"}],
+ ])
+ expected_results = {
+ '/dev/dasde': {
+ 'name': '/dev/dasde', 'device_id': '0.0.2250',
+ 'disk_layout': 'not-formatted', 'blocksize': 512},
+ }
+ self.assertEqual(expected_results, dasd.probe(context=context))
+
+ @mock.patch('probert.dasd.platform.machine')
+ @mock.patch('probert.dasd.dasdview')
+ def test_dasd_probe_dasdd_skips_partitions(self, m_dasdview, m_machine):
+ m_machine.return_value = 's390x'
+ m_dasdview.side_effect = iter([self._load_test_data('dasdd.view')])
+
+ context = mock.MagicMock()
+ context.list_devices.side_effect = iter([
+ [{"MAJOR": "94", "DEVNAME": "/dev/dasdd", "ID_SERIAL": "0X1544",
+ "ID_PATH": "ccw-0.0.1544"}],
+ [{"MAJOR": "94", "DEVNAME": "/dev/dasdd1", "ID_SERIAL": "0X1544",
+ "ID_PATH": "ccw-0.0.1544", "PARTN": "1"}],
+ ])
+ expected_results = {
+ '/dev/dasdd': {
+ 'name': '/dev/dasdd', 'device_id': '0.0.1544',
+ 'disk_layout': 'cdl', 'blocksize': 4096},
+ }
+ self.assertEqual(expected_results, dasd.probe(context=context))