Merge pull request #165 from SUSE/disk-resize

Disk resize
This commit is contained in:
Marcus Schäfer 2016-10-19 20:23:27 +02:00 committed by GitHub
commit 4e1e9e651d
11 changed files with 413 additions and 5 deletions

View File

@ -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=<directory>
The path to the root directory, if not specified kiwi
searches the root directory in build/image-root below
the specified target directory
--size=<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>
Directory containing the kiwi build results

View File

@ -90,6 +90,7 @@ SERVICES and COMMANDS
.. toctree::
image_resize
result_list
result_bundle
system_prepare

View File

@ -17,6 +17,12 @@
#
"""
usage: kiwi -h | --help
kiwi [--profile=<name>...]
[--type=<build_type>]
[--logfile=<filename>]
[--debug]
[--color-output]
image <command> [<args>...]
kiwi [--debug]
[--color-output]
result <command> [<args>...]
@ -43,7 +49,7 @@ global options:
help
show manual page
global options for service: system
global options for services: image, system
--logfile=<filename>
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(

View File

@ -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

View File

@ -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

View File

@ -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

131
kiwi/tasks/image_resize.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>
#
"""
usage: kiwi image resize -h | --help
kiwi image resize --target-dir=<directory> --size=<size>
[--root=<directory>]
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=<directory>
the path to the root directory, if not specified kiwi
searches the root directory in build/image-root below
the specified target directory
--size=<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>
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

View File

@ -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],

View File

@ -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):

View File

@ -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',
)

View File

@ -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'
)