commit
4e1e9e651d
34
doc/source/manual/image_resize.rst
Normal file
34
doc/source/manual/image_resize.rst
Normal 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
|
||||
@ -90,6 +90,7 @@ SERVICES and COMMANDS
|
||||
|
||||
.. toctree::
|
||||
|
||||
image_resize
|
||||
result_list
|
||||
result_bundle
|
||||
system_prepare
|
||||
|
||||
16
kiwi/cli.py
16
kiwi/cli.py
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
131
kiwi/tasks/image_resize.py
Normal 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
|
||||
@ -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],
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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',
|
||||
)
|
||||
|
||||
120
test/unit/tasks_image_resize_test.py
Normal file
120
test/unit/tasks_image_resize_test.py
Normal 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'
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user