diff --git a/doc/source/manual/image_resize.rst b/doc/source/manual/image_resize.rst new file mode 100644 index 00000000..39d29f02 --- /dev/null +++ b/doc/source/manual/image_resize.rst @@ -0,0 +1,34 @@ +kiwi image resize +================= + +SYNOPSIS +-------- + +.. program-output:: bash -c "kiwi-ng image resize | awk 'BEGIN{ found=1} /global options:/{found=0} {if (found) print }'" + +DESCRIPTION +----------- + +For disk based images, allow to resize the image to a new disk geometry. +The additional space is free and not in use by the image. In order to +make use of the additional free space a repartition process is required +like it is provided by kiwi's oem boot code. Therefore the resize operation +is useful for oem image builds most of the time. + +OPTIONS +------- + +--root= + + The path to the root directory, if not specified kiwi + searches the root directory in build/image-root below + the specified target directory + +--size= + + New size of the image. The value is either a size in bytes + or can be specified with m=MB or g=GB. Example: 20g + +--target-dir= + + Directory containing the kiwi build results diff --git a/doc/source/manual/kiwi.rst b/doc/source/manual/kiwi.rst index 3632dbec..3e6d98cb 100644 --- a/doc/source/manual/kiwi.rst +++ b/doc/source/manual/kiwi.rst @@ -90,6 +90,7 @@ SERVICES and COMMANDS .. toctree:: + image_resize result_list result_bundle system_prepare diff --git a/kiwi/cli.py b/kiwi/cli.py index b4296752..7c7fb203 100644 --- a/kiwi/cli.py +++ b/kiwi/cli.py @@ -17,6 +17,12 @@ # """ usage: kiwi -h | --help + kiwi [--profile=...] + [--type=] + [--logfile=] + [--debug] + [--color-output] + image [...] kiwi [--debug] [--color-output] result [...] @@ -43,7 +49,7 @@ global options: help show manual page -global options for service: system +global options for services: image, system --logfile= create a log file containing all log information including debug information even if this is was not requested by the @@ -119,11 +125,13 @@ class Cli(object): :return: service name :rtype: string """ - if self.all_args['system']: + if self.all_args.get('image') is True: + return 'image' + elif self.all_args.get('system') is True: return 'system' - elif self.all_args['result']: + elif self.all_args.get('result') is True: return 'result' - elif self.all_args['--compat']: + elif self.all_args.get('--compat') is True: return 'compat' else: raise KiwiUnknownServiceName( diff --git a/kiwi/exceptions.py b/kiwi/exceptions.py index be55c7cc..c40f029c 100644 --- a/kiwi/exceptions.py +++ b/kiwi/exceptions.py @@ -316,6 +316,14 @@ class KiwiHelpNoCommandGiven(KiwiError): """ +class KiwiImageResizeError(KiwiError): + """ + Exception raised if the request to resize a disk image failed. + Reasons could be a missing raw disk reference or a wrong size + specification. + """ + + class KiwiImportDescriptionError(KiwiError): """ Exception raised if the XML description data and scripts could @@ -508,6 +516,21 @@ class KiwiRequestError(KiwiError): """ +class KiwiResizeRawDiskError(KiwiError): + """ + Exception raised if an attempt was made to resize the image disk + to a smaller size than the current one. Simply shrinking a disk image + file is not possible without data corruption because the partitions + were setup to use the entire disk geometry as it fits into the file. + A successful shrinking operation would require the filesystems and + the partition table to be reduced which is not done by the provided + simple storage resize method. In addition without the user overwriting + the disk size in the XML setup, kiwi will calculate the minimum + required size in order to store the data. Thus in almost all cases + it will not be possible to store the data in a smaller disk. + """ + + class KiwiResultError(KiwiError): """ Exception raised if the image build result pickle information diff --git a/kiwi/storage/subformat/__init__.py b/kiwi/storage/subformat/__init__.py index a446b3ac..2b144ab9 100644 --- a/kiwi/storage/subformat/__init__.py +++ b/kiwi/storage/subformat/__init__.py @@ -22,6 +22,7 @@ from .vhdfixed import DiskFormatVhdFixed from .vmdk import DiskFormatVmdk from .gce import DiskFormatGce from .vdi import DiskFormatVdi +from .base import DiskFormatBase from ...exceptions import ( KiwiDiskFormatSetupError @@ -93,6 +94,10 @@ class DiskFormat(object): return DiskFormatVmdk( xml_state, root_dir, target_dir, custom_args ) + elif name == 'raw': + return DiskFormatBase( + xml_state, root_dir, target_dir + ) else: raise KiwiDiskFormatSetupError( 'Support for %s disk format not implemented' % name diff --git a/kiwi/storage/subformat/base.py b/kiwi/storage/subformat/base.py index 4dbc6bca..e2602496 100644 --- a/kiwi/storage/subformat/base.py +++ b/kiwi/storage/subformat/base.py @@ -20,9 +20,11 @@ import platform from collections import OrderedDict from ...exceptions import ( - KiwiFormatSetupError + KiwiFormatSetupError, + KiwiResizeRawDiskError ) +from ...command import Command from ...defaults import Defaults from ...path import Path from ...logger import log @@ -80,6 +82,40 @@ class DiskFormatBase(object): """ pass + def has_raw_disk(self): + """ + Check if the base raw disk image exists + + :rtype: bool + """ + return os.path.exists(self.diskname) + + def resize_raw_disk(self, size_bytes): + """ + Resize raw disk image to specified size. If the request + would actually shrink the disk an exception is raised. + If the disk got changed the method returns True, if + the new size is the same as the current size nothing + gets resized and the method returns False + + :param int size: new size in bytes + :rtype: bool + """ + current_byte_size = os.path.getsize(self.diskname) + size_bytes = int(size_bytes) + if size_bytes < current_byte_size: + raise KiwiResizeRawDiskError( + 'shrinking {0} disk to {1} bytes corrupts the image'.format( + self.diskname, size_bytes + ) + ) + elif size_bytes == current_byte_size: + return False + Command.run( + ['qemu-img', 'resize', self.diskname, format(size_bytes)] + ) + return True + def create_image_format(self): """ Create disk format diff --git a/kiwi/tasks/image_resize.py b/kiwi/tasks/image_resize.py new file mode 100644 index 00000000..b0d30e10 --- /dev/null +++ b/kiwi/tasks/image_resize.py @@ -0,0 +1,131 @@ +# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. +# +# This file is part of kiwi. +# +# kiwi is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# kiwi 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 kiwi. If not, see +# +""" +usage: kiwi image resize -h | --help + kiwi image resize --target-dir= --size= + [--root=] + kiwi image resize help + +commands: + resize + for disk based images, allow to resize the image to a new + disk geometry. The additional space is free and not in use + by the image. In order to make use of the additional free + space a repartition process is required like it is provided + by kiwi's oem boot code. Therefore the resize operation is + useful for oem image builds most of the time + +options: + --root= + the path to the root directory, if not specified kiwi + searches the root directory in build/image-root below + the specified target directory + + --size= + new size of the image. The value is either a size in bytes + or can be specified with m=MB or g=GB. Example: 20g + + --target-dir= + the target directory to expect image build results +""" +import re +import math + +# project +from .base import CliTask +from ..help import Help +from ..logger import log +from ..storage.subformat import DiskFormat + +from ..exceptions import ( + KiwiImageResizeError +) + + +class ImageResizeTask(CliTask): + """ + Implements resizing of disk images and their disk format + + Attributes + + * :attr:`manual` + Instance of Help + """ + def process(self): + """ + reformats raw disk image and its format to a new disk + geometry using the qemu tool chain + """ + self.manual = Help() + if self.command_args.get('help') is True: + return self.manual.show('kiwi::image::resize') + + if self.command_args['--root']: + image_root = self.command_args['--root'] + else: + image_root = ''.join( + [self.command_args['--target-dir'], '/build/image-root'] + ) + + self.load_xml_description( + image_root + ) + + disk_format = self.xml_state.build_type.get_format() + + image_format = DiskFormat( + disk_format or 'raw', self.xml_state, image_root, + self.command_args['--target-dir'] + ) + if not image_format.has_raw_disk(): + raise KiwiImageResizeError( + 'no raw disk image {0} found in build results'.format( + image_format.diskname + ) + ) + + new_disk_size = self._to_bytes(self.command_args['--size']) + + log.info( + 'Resizing raw disk to {0} bytes'.format(new_disk_size) + ) + resize_result = image_format.resize_raw_disk(new_disk_size) + if disk_format and resize_result is True: + log.info( + 'Creating {0} disk format from resized raw disk'.format( + disk_format + ) + ) + image_format.create_image_format() + elif resize_result is False: + log.info( + 'Raw disk is already at {0} bytes'.format(new_disk_size) + ) + + def _to_bytes(self, size_value): + size_format = '^(\d+)([gGmM]{0,1})$' + size = re.search(size_format, size_value) + if not size: + raise KiwiImageResizeError( + 'unsupported size format {0}, must match {1}'.format( + size_value, size_format + ) + ) + size_base = int(size.group(1)) + size_unit = {'g': 3, 'm': 2}.get(size.group(2).lower()) + return size_unit and size_base * math.pow(0x400, size_unit) or size_base diff --git a/test/unit/cli_test.py b/test/unit/cli_test.py index f8366983..1705740f 100644 --- a/test/unit/cli_test.py +++ b/test/unit/cli_test.py @@ -14,6 +14,7 @@ class TestCli(object): 'help': False, '--compat': False, '--type': None, + 'image': False, 'system': True, '-h': False, '--logfile': None, @@ -78,6 +79,16 @@ class TestCli(object): cli = Cli() assert cli.get_servicename() == 'compat' + def test_get_servicename_image(self): + sys.argv = [ + sys.argv[0], + 'image', 'resize', + '--target-dir', 'directory', + '--size', '20g' + ] + cli = Cli() + assert cli.get_servicename() == 'image' + def test_get_servicename_result(self): sys.argv = [ sys.argv[0], diff --git a/test/unit/storage_subformat_base_test.py b/test/unit/storage_subformat_base_test.py index 8e28e39d..ad53a3ea 100644 --- a/test/unit/storage_subformat_base_test.py +++ b/test/unit/storage_subformat_base_test.py @@ -63,6 +63,37 @@ class TestDiskFormatBase(object): use_for_bundle=True ) + @raises(KiwiResizeRawDiskError) + @patch('os.path.getsize') + def test_resize_raw_disk_raises_on_shrink_disk(self, mock_getsize): + mock_getsize.return_value = 42 + self.disk_format.resize_raw_disk(10) + + @patch('os.path.getsize') + @patch('kiwi.storage.subformat.base.Command.run') + def test_resize_raw_disk(self, mock_command, mock_getsize): + mock_getsize.return_value = 42 + assert self.disk_format.resize_raw_disk(1024) == True + mock_command.assert_called_once_with( + [ + 'qemu-img', 'resize', + 'target_dir/some-disk-image.x86_64-1.2.3.raw', '1024' + ] + ) + + @patch('os.path.getsize') + def test_resize_raw_disk_same_size(self, mock_getsize): + mock_getsize.return_value = 42 + assert self.disk_format.resize_raw_disk(42) == False + + @patch('os.path.exists') + def test_has_raw_disk(self, mock_exists): + mock_exists.return_value = True + assert self.disk_format.has_raw_disk() == True + mock_exists.assert_called_once_with( + 'target_dir/some-disk-image.x86_64-1.2.3.raw' + ) + @patch('kiwi.storage.subformat.base.Path.wipe') @patch('os.path.exists') def test_destructor(self, mock_exists, mock_wipe): diff --git a/test/unit/storage_subformat_test.py b/test/unit/storage_subformat_test.py index 7106cd3c..befa29b4 100644 --- a/test/unit/storage_subformat_test.py +++ b/test/unit/storage_subformat_test.py @@ -78,3 +78,11 @@ class TestDiskFormat(object): xml_state, 'root_dir', 'target_dir', {'adapter_type=controller': None, 'subformat=disk-mode': None} ) + + @patch('kiwi.storage.subformat.DiskFormatBase') + def test_disk_format_base(self, mock_base): + xml_state = mock.Mock() + DiskFormat('raw', xml_state, 'root_dir', 'target_dir') + mock_base.assert_called_once_with( + xml_state, 'root_dir', 'target_dir', + ) diff --git a/test/unit/tasks_image_resize_test.py b/test/unit/tasks_image_resize_test.py new file mode 100644 index 00000000..d9386611 --- /dev/null +++ b/test/unit/tasks_image_resize_test.py @@ -0,0 +1,120 @@ +import sys +import mock +from mock import call + +import kiwi + +from .test_helper import raises, patch + +from kiwi.exceptions import KiwiImageResizeError, KiwiConfigFileNotFound + +from kiwi.tasks.image_resize import ImageResizeTask + + +class TestImageResizeTask(object): + def setup(self): + sys.argv = [ + sys.argv[0], '--type', 'vmx', 'image', 'resize', + '--target-dir', 'target_dir', '--size', '20g', + '--root', '../data/root-dir' + ] + kiwi.tasks.image_resize.Help = mock.Mock( + return_value=mock.Mock() + ) + + self.image_format = mock.Mock() + self.image_format.has_raw_disk = mock.Mock() + self.image_format.diskname = 'some-disk.raw' + kiwi.tasks.image_resize.DiskFormat = mock.Mock( + return_value=self.image_format + ) + + self.task = ImageResizeTask() + + def _init_command_args(self): + self.task.command_args = {} + self.task.command_args['help'] = False + self.task.command_args['resize'] = False + self.task.command_args['--target-dir'] = 'target_dir' + self.task.command_args['--size'] = '42g' + self.task.command_args['--root'] = '../data/root-dir' + + @raises(KiwiConfigFileNotFound) + def test_process_no_root_directory_specified(self): + self.task.command_args['--root'] = None + self.task.process() + + @raises(KiwiImageResizeError) + def test_process_no_raw_disk_found(self): + self._init_command_args() + self.image_format.has_raw_disk.return_value = False + self.task.command_args['resize'] = True + self.task.process() + + @raises(KiwiImageResizeError) + def test_process_unsupported_size_format(self): + self._init_command_args() + self.task.command_args['--size'] = '20x' + self.image_format.has_raw_disk.return_value = True + self.task.command_args['resize'] = True + self.task.process() + + def test_process_image_resize_gb(self): + self._init_command_args() + self.task.command_args['resize'] = True + self.image_format.resize_raw_disk.return_value = True + self.task.process() + self.image_format.resize_raw_disk.assert_called_once_with( + 42 * 1024 * 1024 * 1024 + ) + self.image_format.create_image_format.assert_called_once_with() + + def test_process_image_resize_mb(self): + self._init_command_args() + self.task.command_args['resize'] = True + self.task.command_args['--size'] = '42m' + self.image_format.resize_raw_disk.return_value = True + self.task.process() + self.image_format.resize_raw_disk.assert_called_once_with( + 42 * 1024 * 1024 + ) + self.image_format.create_image_format.assert_called_once_with() + + def test_process_image_resize_bytes(self): + self._init_command_args() + self.task.command_args['resize'] = True + self.task.command_args['--size'] = '42' + self.image_format.resize_raw_disk.return_value = True + self.task.process() + self.image_format.resize_raw_disk.assert_called_once_with( + 42 + ) + self.image_format.create_image_format.assert_called_once_with() + + @patch('kiwi.logger.log.info') + def test_process_image_resize_not_needed(self, mock_log_info): + self._init_command_args() + self.task.command_args['resize'] = True + self.task.command_args['--size'] = '42' + self.image_format.resize_raw_disk.return_value = False + self.task.process() + self.image_format.resize_raw_disk.assert_called_once_with( + 42 + ) + assert mock_log_info.call_args_list == [ + call('Loading XML description'), + call('--> loaded %s', '../data/root-dir/image/config.xml'), + call('--> Selected build type: %s', 'vmx'), + call('--> Selected profiles: %s', 'vmxFlavour'), + call('Resizing raw disk to 42 bytes'), + call('Raw disk is already at 42 bytes') + ] + + def test_process_image_resize_help(self): + self._init_command_args() + self.task.command_args['help'] = True + self.task.command_args['resize'] = True + self.task.process() + self.task.manual.show.assert_called_once_with( + 'kiwi::image::resize' + )