diff --git a/ironic-lib/0003-Disk-partitioning-implementation.patch b/ironic-lib/0003-Disk-partitioning-implementation.patch new file mode 100644 index 0000000..6a6d956 --- /dev/null +++ b/ironic-lib/0003-Disk-partitioning-implementation.patch @@ -0,0 +1,706 @@ +From afc04dfa6a798356dea0ce5d78dd32cf9c386f42 Mon Sep 17 00:00:00 2001 +From: "Grzegorz Grasza (xek)" +Date: Thu, 11 Jan 2018 16:49:05 +0100 +Subject: [PATCH 03/11] Disk partitioning implementation + +Co-Authored-By: Grzegorz Grasza +Co-Authored-By: Marta Mucek +--- + ironic_lib/system_installer/exceptions.py | 4 + + ironic_lib/system_installer/filesystemsetup.py | 9 + + ironic_lib/system_installer/partitionsetup.py | 219 +++++++++++++++++ + ironic_lib/system_installer/tools.py | 7 + + ironic_lib/tests/test_examples.py | 5 +- + ironic_lib/tests/test_partitionsetup.py | 324 +++++++++++++++++++++++++ + ironic_lib/tests/test_tools.py | 35 +++ + 7 files changed, 601 insertions(+), 2 deletions(-) + create mode 100644 ironic_lib/tests/test_partitionsetup.py + create mode 100644 ironic_lib/tests/test_tools.py + +diff --git a/ironic_lib/system_installer/exceptions.py b/ironic_lib/system_installer/exceptions.py +index 6dcfa46..2f7998f 100644 +--- a/ironic_lib/system_installer/exceptions.py ++++ b/ironic_lib/system_installer/exceptions.py +@@ -24,3 +24,7 @@ class ConfError(SystemInstallerException): + + class EnvError(SystemInstallerException): + """Error in the setup environment""" ++ ++ ++class AssignFail(EnvError): ++ pass +diff --git a/ironic_lib/system_installer/filesystemsetup.py b/ironic_lib/system_installer/filesystemsetup.py +index e3157d2..6ed64d6 100644 +--- a/ironic_lib/system_installer/filesystemsetup.py ++++ b/ironic_lib/system_installer/filesystemsetup.py +@@ -32,3 +32,12 @@ class FilesystemSetup(Setup): + + def get_disks_by_labels(self): + return {} ++ ++ def is_efi_boot(self): ++ return False ++ ++ def get_boot_partition(self): ++ pass # TODO(xek) ++ ++ def get_fstype(self, label): ++ return '' # TODO(xek) +diff --git a/ironic_lib/system_installer/partitionsetup.py b/ironic_lib/system_installer/partitionsetup.py +index af318d1..8f22a9d 100644 +--- a/ironic_lib/system_installer/partitionsetup.py ++++ b/ironic_lib/system_installer/partitionsetup.py +@@ -13,7 +13,22 @@ + # See the License for the specific language governing permissions and + # limitations under the License. + ++from collections import OrderedDict ++import json ++ ++import bitmath ++from oslo_concurrency import processutils ++from oslo_log import log ++ ++from ironic_lib.disk_partitioner import DiskPartitioner ++from ironic_lib import disk_utils + from ironic_lib.system_installer.base import Setup ++from ironic_lib.system_installer import exceptions ++from ironic_lib.system_installer.filesystemsetup import FilesystemSetup ++from ironic_lib.system_installer import tools ++from ironic_lib import utils ++ ++LOG = log.getLogger() + + + class PartitionSetup(Setup): +@@ -21,7 +36,211 @@ class PartitionSetup(Setup): + + conf_key = 'blockdev' + ++ def __init__(self, conf): ++ if FilesystemSetup.conf_key in conf: ++ self._filesystems = FilesystemSetup(conf) ++ else: # this case is rare, mainly for testing and running independent ++ self._filesystems = FilesystemSetup({FilesystemSetup.conf_key: {}}) ++ self._boot_partition = self._filesystems.get_boot_partition() ++ self._efi_boot = self._filesystems.is_efi_boot() ++ default_partition_table = 'mbr' ++ if self._efi_boot: ++ default_partition_table = 'gpt' ++ types_translation = {'mbr': 'msdos'} ++ table = conf.get('partition_table', default_partition_table) ++ self._partition_type = types_translation.get(table, table) ++ super(PartitionSetup, self).__init__(conf) ++ ++ def validate_conf(self): ++ for disk_name, disk in self.conf.items(): ++ minsizes = 0 ++ number_of_partitions = 0 ++ for label, partition in disk.get('partitions', {}).items(): ++ number_of_partitions += 1 ++ if 'minsize' in partition: ++ minsizes += 1 ++ if minsizes > 1: ++ raise exceptions.ConfError( ++ 'More than one minsize not supported') ++ if number_of_partitions > 4 and \ ++ self._partition_type != 'gpt': ++ raise exceptions.ConfError( ++ 'More than 4 partitions per disk are not allowed') ++ ++ def validate_env(self): ++ """Check that disks exist""" ++ lsblk_version, err = utils.execute("lsblk --version", shell=True) ++ if float('.'.join(lsblk_version.split()[-1].split('.')[:2])) < 2.27: ++ raise exceptions.EnvError('lsblk version >= 2.27 is required') ++ if len(self._get_physical_drives()) < len([ ++ v for v in self.conf.values() if 'candidates' in v]): ++ raise exceptions.EnvError('Not enough physical drives') ++ candidates = self._assign_candidates() ++ for label, partition in self.conf.items(): ++ if 'candidates' in partition: ++ try: ++ candidates[label] ++ except KeyError: ++ raise exceptions.EnvError( ++ 'No candidates for blockdev {}'.format(label)) ++ + def get_dst_names(self): + partitions = [list(v.get('partitions', {})) + for v in self.conf.values()] + return sum(partitions, []) ++ ++ def setup_disks(self, devices): ++ devices.update(self._assign_candidates(devices)) ++ disksizes = {d['name']: d['size'] for d in self._get_physical_drives()} ++ for label, disk in self.conf.items(): ++ if not disk.get('partitions'): ++ continue ++ try: ++ device_name = devices[label] ++ except KeyError: ++ raise exceptions.EnvError( ++ 'No candidates for blockdev {}'.format(label)) ++ remaining_size = bitmath.Byte(disksizes[device_name]) ++ partitioner = DiskPartitioner(device_name, self._partition_type) ++ last_partition = None ++ names = [] ++ for name, partition in disk['partitions'].items(): ++ names.append(name) ++ if 'size' in partition: ++ if partition['size'] == 'memsize': ++ size = bitmath.parse_string_unsafe( ++ tools.get_memsize_kB()) ++ else: ++ size = bitmath.parse_string_unsafe(partition['size']) ++ # TODO(xek): create partition labels for preserve function? ++ partitioner.add_partition( ++ int(size.to_MiB().value), ++ fs_type=partition.get( ++ 'type', self._filesystems.get_fstype(label)), ++ boot_flag=self._boot_flag(name)) ++ remaining_size -= size ++ if remaining_size <= 0: ++ raise exceptions.EnvError( ++ "Partitions don't fit on disk") ++ elif 'minsize' in partition: ++ last_partition = partition ++ else: ++ raise exceptions.EnvError( ++ 'No candidates for {}'.format(name)) ++ if last_partition: ++ # TODO(xek): create partition labels for preserve function? ++ partitioner.add_partition( ++ int(remaining_size.to_MiB().value - 1 ++ - disk_utils.MAX_CONFIG_DRIVE_SIZE_MB), ++ boot_flag=self._boot_flag(name)) ++ partitioner.commit() ++ created = self._gather_created_devices(device_name) ++ devices.update(zip(names, created)) ++ ++ return devices ++ ++ def get_not_partitioned_candidates(self, candidates=None): ++ if not candidates: ++ candidates = self._assign_candidates() ++ try: ++ return {label: candidates[label] ++ for label, disk in self.conf.items() ++ if not disk.get('partitions')} ++ except KeyError as e: ++ raise exceptions.EnvError( ++ 'No candidates for blockdev {}'.format(e.args[0])) ++ ++ def _boot_flag(self, name): ++ if self._boot_partition == name: ++ if self._partition_type == 'gpt' and not self._efi_boot: ++ return 'bios_grub' ++ return 'boot' ++ return None ++ ++ def _assign_candidates(self, devices={}): ++ "Returns dict of {label: candidate}, given a dict of already assigned." ++ def assign_disks(devices, partitions): ++ if not partitions: ++ return {} ++ for label, partition in partitions.items(): ++ candidates = self._filter_candidates( ++ partition['candidates'], devices) ++ for device in candidates: ++ assigned = {label: device['name']} ++ devices_ = devices[:] ++ devices_.remove(device) ++ partitions_ = partitions.copy() ++ del partitions_[label] ++ try: ++ assigned.update(assign_disks(devices_, partitions_)) ++ return assigned ++ except exceptions.AssignFail: ++ pass ++ raise exceptions.AssignFail('No candidates') ++ # Don't assign disk candidates to devices with no candidates entry. ++ with_candidates = OrderedDict([ ++ (label, part) for label, part in self.conf.items() ++ if 'candidates' in part and label not in devices]) ++ # Already assigned disks are present in the devices dict. ++ return assign_disks(self._get_physical_drives(), with_candidates) ++ ++ @staticmethod ++ def _filter_candidates(hints, devices): ++ " Filter devices according to hints. " ++ if hints == 'any': ++ return devices ++ if 'serial' in hints: ++ return [d for d in devices if d['serial'] == hints['serial']] ++ if 'model' in hints: # assuming all instances of model have same size ++ return [d for d in devices if d['model'] == hints['model']] ++ if 'type' in hints: ++ devices = [d for d in devices if d['type'] == hints['type']] ++ if 'size' in hints: ++ devices = [d for d in devices ++ if d['size'] <= int(hints['max_disk_size_gb']) * 1e+9] ++ return devices ++ ++ @staticmethod ++ def _get_physical_drives(): ++ try: ++ drives_json, err = utils.execute( ++ 'lsblk', '-Jbde1,7', '-o', 'NAME,ROTA,SERIAL,MODEL,SIZE') ++ if err: ++ raise processutils.ProcessExecutionError(err) ++ blockdevices = json.loads(drives_json)['blockdevices'] ++ for d in blockdevices: ++ d['name'] = '/dev/' + d['name'] ++ d['size'] = int(d['size']) ++ rotating = int(d['rota']) ++ if rotating: ++ d['type'] = 'HDD' ++ elif 'nvme' in d['name']: ++ d['type'] = 'NVMe' ++ else: ++ d['type'] = 'SSD' ++ del d['rota'] ++ return [b for b in blockdevices if all(b.values())] ++ except (processutils.ProcessExecutionError, OSError): ++ raise exceptions.EnvError("Error getting details of devices.") ++ ++ @staticmethod ++ def _gather_created_devices(dev): ++ try: ++ drives_json, err = utils.execute('lsblk', '-Jpo', 'NAME', dev) ++ if err: ++ raise processutils.ProcessExecutionError(err) ++ devices = json.loads(drives_json)['blockdevices'] ++ except (processutils.ProcessExecutionError, OSError): ++ raise exceptions.EnvError("Error getting details of devices.") ++ ++ result = [] ++ ++ def add_devices(tree): ++ for dev in tree: ++ if 'children' in dev: ++ add_devices(dev['children']) ++ else: ++ result.append(dev['name']) ++ ++ add_devices(devices) ++ return result +diff --git a/ironic_lib/system_installer/tools.py b/ironic_lib/system_installer/tools.py +index 71f6d60..aaacf67 100644 +--- a/ironic_lib/system_installer/tools.py ++++ b/ironic_lib/system_installer/tools.py +@@ -34,3 +34,10 @@ def ordered_load(disk_conf): + yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, + construct_mapping) + return yaml.load(disk_conf, OrderedLoader) ++ ++ ++def get_memsize_kB(): ++ """Get memory size of machine""" ++ with open('/proc/meminfo', 'r') as f: ++ # take first line which is MemTotal and grab its value with unit ++ return f.read().split('\n')[0].split()[1] +diff --git a/ironic_lib/tests/test_examples.py b/ironic_lib/tests/test_examples.py +index 05b5cc7..e69bc2d 100644 +--- a/ironic_lib/tests/test_examples.py ++++ b/ironic_lib/tests/test_examples.py +@@ -15,6 +15,7 @@ + + from oslotest import base as test_base + ++from ironic_lib.system_installer import exceptions + from ironic_lib.system_installer import SystemInstaller + + EXAMPLES_DIR = 'ironic_lib/tests/examples/' +@@ -61,7 +62,7 @@ class SystemInstallerExamplesTestCase(test_base.BaseTestCase): + + def test_yse_bnode_4disk(self): + y = SystemInstaller(open(EXAMPLES_DIR + 'yse_bnode_4disk.yaml').read()) +- y.validate_conf() ++ self.assertRaises(exceptions.ConfError, y.validate_conf) + + def test_yse_bnode_4disk_gpt(self): + y = SystemInstaller( +@@ -71,7 +72,7 @@ class SystemInstallerExamplesTestCase(test_base.BaseTestCase): + def test_yse_bnode_fsprofile(self): + y = SystemInstaller( + open(EXAMPLES_DIR + 'yse_bnode_fsprofile.yaml').read()) +- y.validate_conf() ++ self.assertRaises(exceptions.ConfError, y.validate_conf) + + def test_megacli_partitions(self): + y = SystemInstaller( +diff --git a/ironic_lib/tests/test_partitionsetup.py b/ironic_lib/tests/test_partitionsetup.py +new file mode 100644 +index 0000000..69c14b4 +--- /dev/null ++++ b/ironic_lib/tests/test_partitionsetup.py +@@ -0,0 +1,324 @@ ++# Copyright (c) 2018 Intel Corporation ++# ++# Licensed under the Apache License, Version 2.0 (the "License"); ++# you may not use this file except in compliance with the License. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or ++# implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++ ++from collections import OrderedDict ++import unittest ++ ++import mock ++ ++from ironic_lib.system_installer import exceptions ++from ironic_lib.system_installer import partitionsetup ++ ++ ++class PartitionSetupTestCase(unittest.TestCase): ++ ++ def test_validate_conf(self): ++ partition_setup = partitionsetup.PartitionSetup( ++ {'blockdev': {'sda': { ++ 'candidates': 'any', ++ 'partitions': { ++ 'd0p1': {'size': '512M'}, ++ 'd0p2': {'minsize': '2G'}} ++ }}}) ++ partition_setup.validate_conf() ++ ++ def test_validate_conf_error(self): ++ partition_setup = partitionsetup.PartitionSetup( ++ {'blockdev': {'sda': { ++ 'candidates': 'any', ++ 'partitions': { ++ 'd0p1': {'size': '512M'}, ++ 'd0p2': {'minsize': '2G'}, ++ 'd0p3': {'minsize': '2G'}} ++ }}}) ++ with self.assertRaises(exceptions.ConfError): ++ partition_setup.validate_conf() ++ ++ def test_get_dst_names(self): ++ partition_setup = partitionsetup.PartitionSetup( ++ {'blockdev': { ++ 'sda': { ++ 'candidates': 'any', ++ 'partitions': { ++ 'd0p1': {'size': '512M'}, ++ 'd0p2': {'minsize': '2G'}}}, ++ 'sdb': { ++ 'candidates': 'any', ++ 'partitions': { ++ 'd1p1': {'size': '512M'}, ++ 'd1p2': {'minsize': '2G'}}}}}) ++ self.assertEqual(set(partition_setup.get_dst_names()), ++ {'d0p1', 'd0p2', 'd1p1', 'd1p2'}) ++ ++ def test_get_not_partitioned_candidates_empty(self): ++ with mock.patch.object( ++ partitionsetup.PartitionSetup, '_get_physical_drives', ++ return_value=[ ++ {'model': 'fake_model', ++ 'name': 'sda', ++ 'serial': 'fake_serial', ++ 'size': 250000000000, ++ 'type': 'HDD'}, ++ {'model': 'fake_model', ++ 'name': 'sdb', ++ 'serial': 'fake_serial', ++ 'size': 250000000000, ++ 'type': 'HDD'}]): ++ partition_setup = partitionsetup.PartitionSetup( ++ {'blockdev': { ++ 'sda_label': { ++ 'candidates': 'any', ++ 'partitions': { ++ 'd0p1': {'size': '512M'}, ++ 'd0p2': {'minsize': '2G'}}}, ++ 'sdb_label': { ++ 'candidates': 'any', ++ 'partitions': { ++ 'd1p1': {'size': '512M'}, ++ 'd1p2': {'minsize': '2G'}}}}}) ++ self.assertEqual(partition_setup.get_not_partitioned_candidates(), ++ {}) ++ ++ def test_get_not_partitioned_candidates(self): ++ with mock.patch.object( ++ partitionsetup.PartitionSetup, '_get_physical_drives', ++ return_value=[ ++ {'model': 'fake_model', ++ 'name': '/dev/sda', ++ 'serial': 'fake_serial', ++ 'size': 250000000000, ++ 'type': 'HDD'}, ++ {'model': 'fake_model', ++ 'name': '/dev/sdb', ++ 'serial': 'fake_serial', ++ 'size': 250000000000, ++ 'type': 'HDD'}]): ++ partition_setup = partitionsetup.PartitionSetup( ++ {'blockdev': OrderedDict([ ++ ('sda_label', { ++ 'candidates': 'any', ++ 'partitions': { ++ 'd0p1': {'size': '512M'}, ++ 'd0p2': {'minsize': '2G'}} ++ }), ++ ('sdb_label', { ++ 'candidates': 'any' ++ })])}) ++ self.assertDictEqual( ++ partition_setup.get_not_partitioned_candidates(), ++ {'sdb_label': '/dev/sdb'}) ++ ++ def test_validate_env(self): ++ with mock.patch.object( ++ partitionsetup.PartitionSetup, '_get_physical_drives', ++ return_value=[ ++ {'model': 'fake_model', ++ 'name': '/dev/sda', ++ 'serial': 'fake_serial', ++ 'size': 250000000000, ++ 'type': 'HDD'}]): ++ partition_setup = partitionsetup.PartitionSetup( ++ {'blockdev': {'sda': {'candidates': 'any', ++ 'partitions': { ++ 'd0p1': {'size': '512M'}, ++ 'd0p2': {'minsize': '2G'}}}}}) ++ partition_setup.validate_env() ++ ++ def test_validate_env_match_fail(self): ++ with mock.patch.object( ++ partitionsetup.PartitionSetup, '_get_physical_drives', ++ return_value=[ ++ {'model': 'fake_model', ++ 'name': '/dev/sda', ++ 'serial': 'fake_serial', ++ 'size': 250000000000, ++ 'type': 'HDD'}]): ++ partition_setup = partitionsetup.PartitionSetup( ++ {'blockdev': { ++ 'sda': { ++ 'candidates': 'any', ++ 'partitions': { ++ 'd0p1': {'size': '512M'}, ++ 'd0p2': {'minsize': '2G'}}}, ++ 'sdb': { ++ 'candidates': 'any', ++ 'partitions': { ++ 'd1p1': {'size': '512M'}, ++ 'd1p2': {'minsize': '2G'}}}}}) ++ with self.assertRaises(exceptions.EnvError): ++ partition_setup.validate_env() ++ ++ @mock.patch.object(partitionsetup.PartitionSetup, '_get_physical_drives') ++ def test_validate_env_physical_count(self, get_drives_mock): ++ get_drives_mock.return_value = [ ++ {'model': 'fake_model', ++ 'name': 'sdb', ++ 'serial': 'fake_serial', ++ 'size': 250000000000, ++ 'type': 'HDD'}, ++ {'model': 'fake_model', ++ 'name': 'sdc', ++ 'serial': 'fake_serial', ++ 'size': 250000000000, ++ 'type': 'HDD'}] ++ partition_setup = partitionsetup.PartitionSetup( ++ {'blockdev': { ++ 'sda': {'candidates': 'any'}, ++ 'sdb': {'candidates': 'any'}, ++ 'raid1': { ++ 'partitions': { ++ 'd0p1': {'size': '512M'}, ++ 'd0p2': {'minsize': '2G'}, ++ 'd0p3': {'minsize': '2G'}} ++ }}}) ++ partition_setup.validate_env() ++ ++ @mock.patch.object(partitionsetup.PartitionSetup, '_get_physical_drives') ++ def test_validate_env_physical_count_error(self, get_drives_mock): ++ get_drives_mock.return_value = [ ++ {'model': 'fake_model', ++ 'name': 'sdb', ++ 'serial': 'fake_serial', ++ 'size': 250000000000, ++ 'type': 'HDD'}] ++ partition_setup = partitionsetup.PartitionSetup( ++ {'blockdev': { ++ 'sda': {'candidates': 'any'}, ++ 'sdb': {'candidates': 'any'}, ++ 'raid1': { ++ 'partitions': { ++ 'd0p1': {'size': '512M'}, ++ 'd0p2': {'minsize': '2G'}, ++ 'd0p3': {'minsize': '2G'}} ++ }}}) ++ with self.assertRaises(exceptions.EnvError): ++ partition_setup.validate_env() ++ ++ def test_setup_disks(self): ++ ++ def execute_result(command, *args, **kwargs): ++ if command == 'lsblk': ++ return ("""{ ++ "blockdevices": [ ++ {"name": "/dev/sda", ++ "children": [ ++ {"name": "/dev/sda1"}, ++ {"name": "/dev/sda2"}, ++ {"name": "/dev/sda3", ++ "children": [ ++ {"name": "/dev/sda5"}, ++ {"name": "/dev/sda6"} ++ ] ++ } ++ ] ++ } ++ ] ++ }""", '') ++ return ('', '') ++ ++ physical_drives = [ ++ {'model': 'fake_model', ++ 'name': '/dev/sda', ++ 'serial': 'fake_serial', ++ 'size': 250000000000, ++ 'type': 'HDD'}] ++ ++ with mock.patch.object( ++ partitionsetup.PartitionSetup, '_get_physical_drives', ++ return_value=physical_drives), \ ++ mock.patch('ironic_lib.utils.execute', ++ side_effect=execute_result) as exec_mock: ++ ++ partition_setup = partitionsetup.PartitionSetup( ++ {'partition_table': 'gpt', ++ 'blockdev': {'root_disk': { ++ 'candidates': 'any', ++ 'partitions': OrderedDict([ ++ ('d0p1', {'size': '512M'}), ++ ('d0p2', {'size': '512M', 'type': 'ntfs'}), ++ ('d0p3', {'size': '512M'}), ++ ('d0p4', {'minsize': '2G'})])}}}) ++ result = partition_setup.setup_disks({}) ++ self.assertDictEqual( ++ result, {'d0p1': '/dev/sda1', 'd0p2': '/dev/sda2', ++ 'd0p3': '/dev/sda5', 'd0p4': '/dev/sda6', ++ 'root_disk': '/dev/sda'}) ++ self.assertIn('gpt', exec_mock.mock_calls[0][1]) ++ self.assertIn('ntfs', exec_mock.mock_calls[2][1]) ++ ++ def test_setup_disks_size_fail(self): ++ with mock.patch.object( ++ partitionsetup.PartitionSetup, '_get_physical_drives', ++ return_value=[ ++ {'model': 'fake_model', ++ 'name': '/dev/sda', ++ 'serial': 'fake_serial', ++ 'size': 250000000000, ++ 'type': 'HDD'}]): ++ ++ partition_setup = partitionsetup.PartitionSetup( ++ {'blockdev': {'sda': { ++ 'candidates': 'any', ++ 'partitions': OrderedDict([ ++ ('d0p1', {'size': '512G'}), ++ ('d0p2', {'size': '512G'}), ++ ('d0p3', {'size': '512G'}), ++ ('d0p4', {'minsize': '2G'})]) ++ }}}) ++ with self.assertRaises(exceptions.EnvError): ++ partition_setup.setup_disks({}) ++ ++ def test_assign_candidates(self): ++ with mock.patch.object( ++ partitionsetup.PartitionSetup, '_get_physical_drives', ++ return_value=[{u'name': u'/dev/sda', ++ u'model': u'INTEL SSDSC2BB24', ++ 'type': 'SSD', ++ u'serial': u'55cd2e404c02b114', ++ u'size': 240057409536}, ++ {u'name': u'/dev/sdb', ++ u'model': u'INTEL SSDSC2BB24', ++ 'type': 'SSD', ++ u'serial': u'55cd2e404c02b181', ++ u'size': 240057409536}]): ++ partition_setup = partitionsetup.PartitionSetup( ++ {'blockdev': {'sda': { ++ 'candidates': 'any', ++ 'partitions': { ++ 'd0p1': { ++ 'size': '512M'}, ++ 'd0p2': { ++ 'minsize': '2G'}}}}}) ++ partition_setup._assign_candidates() ++ ++ def test_assign_candidates_fail(self): ++ with mock.patch.object( ++ partitionsetup.PartitionSetup, '_get_physical_drives', ++ return_value=[{u'name': u'/dev/sda', ++ u'model': u'INTEL SSDSC2BB24', ++ 'type': 'SSD', ++ u'serial': u'55cd2e404c02b114', ++ u'size': 240057409536}, ++ {u'name': u'/dev/sdb', ++ u'model': u'INTEL SSDSC2BB24', ++ 'type': 'SSD', ++ u'serial': u'55cd2e404c02b181', ++ u'size': 240057409536}]): ++ partition_setup = partitionsetup.PartitionSetup( ++ {'blockdev': {'sda': {'candidates': 'any'}, ++ 'sdb': {'candidates': 'any'}, ++ 'sdc': {'candidates': 'any'}}}) ++ with self.assertRaises(exceptions.EnvError): ++ partition_setup._assign_candidates() +diff --git a/ironic_lib/tests/test_tools.py b/ironic_lib/tests/test_tools.py +new file mode 100644 +index 0000000..6e0cd12 +--- /dev/null ++++ b/ironic_lib/tests/test_tools.py +@@ -0,0 +1,35 @@ ++# Copyright (c) 2018 Intel Corporation ++# ++# Licensed under the Apache License, Version 2.0 (the "License"); ++# you may not use this file except in compliance with the License. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or ++# implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++ ++import unittest ++ ++import ironic_lib.system_installer.tools as tools ++import mock ++ ++ ++class ToolsTest(unittest.TestCase): ++ ++ @mock.patch('ironic_lib.system_installer.tools.open') ++ def test_get_memsize(self, op): ++ meminfo = "\n".join(["MemTotal: 32825036 kB", ++ "MemFree: 18416836 kB", ++ "MemAvailable: 25979788 kB", ++ "Buffers: 1561440 kB"]) ++ ++ op.return_value = mock.mock_open(read_data=meminfo).return_value ++ ++ memsize = tools.get_memsize_kB() ++ print(memsize) ++ self.assertEqual("32825036", memsize) +-- +2.14.1 +