Implemeted do_bootloader

Change-Id: I342e85190d297d36b2a038efe682c8b537d31c2f
Implements: blueprint image-based-provisioning
This commit is contained in:
Vladimir Kozhukalov
2014-08-08 12:49:09 +04:00
parent 4fadda85c9
commit daa0b13db7
13 changed files with 1037 additions and 145 deletions

View File

@@ -129,6 +129,8 @@ class Nailgun(object):
for disk in self.ks_disks:
parted = partition_scheme.add_parted(
name=self._disk_dev(disk), label='gpt')
# we install bootloader on every disk
parted.install_bootloader = True
# legacy boot partition
parted.add_partition(size=24, flags=['bios_grub'])
# uefi partition (for future use)
@@ -211,6 +213,8 @@ class Nailgun(object):
fs_type=volume.get('file_system', 'xfs'),
fs_label=self._getlabel(volume.get('disk_label')))
partition_scheme.append_kernel_params(
self.data['ks_meta']['pm_data']['kernel_params'])
return partition_scheme
def configdrive_scheme(self):
@@ -257,11 +261,28 @@ class Nailgun(object):
def image_scheme(self, partition_scheme):
data = self.data
image_scheme = objects.ImageScheme()
root_image_uri = data['ks_meta']['image_uri']
image_scheme.add_image(
uri=root_image_uri,
target_device=partition_scheme.root_device(),
image_format=data['ks_meta']['image_format'],
container=data['ks_meta']['image_container'],
)
for fs in partition_scheme.fss:
if fs.mount == 'swap':
continue
# We assume for every file system user needs to provide
# file system image. For example if partitioning scheme has
# /, /boot, /var/lib file systems then will try to download
# profile.img.gz, profile-boot.img.gz and profile-var-lib.img.gz
# files and copy them to corresponding volumes.
uri = 'http://%s/targetimages/%s%s.img.gz' % (
data['ks_meta']['master_ip'],
data['profile'].split('_')[0],
# / => ''
# /boot => '-boot'
# /var/lib => '-var-lib'
'-'.join(fs.mount.split('/')).rstrip('-')
)
image_scheme.add_image(
uri=uri,
target_device=fs.device,
# In the future we will get image_format and container format
# from provision.json, but currently it is hard coded.
image_format='ext4',
container='gzip',
)
return image_scheme

View File

@@ -122,3 +122,11 @@ class ProcessExecutionError(Exception):
'stdout': stdout,
'stderr': stderr}
super(ProcessExecutionError, self).__init__(message)
class GrubUtilsError(BaseError):
pass
class FsUtilsError(BaseError):
pass

View File

@@ -17,8 +17,9 @@ import os
from oslo.config import cfg
from fuel_agent import errors
from fuel_agent.utils import artifact_utils as au
from fuel_agent.utils import fs_utils as fu
from fuel_agent.utils import img_utils as iu
from fuel_agent.utils import grub_utils as gu
from fuel_agent.utils import lvm_utils as lu
from fuel_agent.utils import md_utils as mu
from fuel_agent.utils import partition_utils as pu
@@ -136,22 +137,70 @@ class Manager(object):
def do_copyimage(self):
for image in self.image_scheme.images:
processing = iu.Chain()
processing = au.Chain()
processing.append(image.uri)
if image.uri.startswith('http://'):
processing.append(iu.HttpUrl)
processing.append(au.HttpUrl)
elif image.uri.startswith('file://'):
processing.append(iu.LocalFile)
processing.append(au.LocalFile)
if image.container == 'gzip':
processing.append(iu.GunzipStream)
processing.append(au.GunzipStream)
processing.append(image.target_device)
processing.process()
# For every file system in partitioning scheme we call
# make_fs utility. That means we do not care whether fs image
# is available for a particular file system.
# If image is not available we assume user wants to
# leave this file system un-touched.
try:
processing.process()
except Exception:
pass
def mount_target(self, chroot):
# Here we are going to mount all file systems in partition scheme.
# Shorter paths earlier. We sort all mount points by their depth.
# ['/', '/boot', '/var', '/var/lib/mysql']
key = lambda x: len(x.mount.rstrip('/').split('/'))
for fs in sorted(self.partition_scheme.fss, key=key):
if fs.mount == 'swap':
continue
mount = chroot + fs.mount
if not os.path.isdir(mount):
os.makedirs(mount, mode=0o755)
fu.mount_fs(fs.type, fs.device, mount)
fu.mount_bind(chroot, '/sys')
fu.mount_bind(chroot, '/dev')
fu.mount_bind(chroot, '/proc')
def umount_target(self, chroot):
key = lambda x: len(x.mount.rstrip('/').split('/'))
for fs in sorted(self.partition_scheme.fss, key=key, reverse=True):
fu.umount_fs(fs.device)
fu.umount_fs(chroot + '/proc')
fu.umount_fs(chroot + '/dev')
fu.umount_fs(chroot + '/sys')
def do_bootloader(self):
pass
chroot = '/tmp/target'
self.mount_target(chroot)
grub_version = gu.grub_version_guess(chroot=chroot)
boot_device = self.partition_scheme.boot_device(grub_version)
install_devices = [d.name for d in self.partition_scheme.parteds
if d.install_bootloader]
kernel_params = self.partition_scheme.kernel_params
if grub_version == 1:
gu.grub1_cfg(kernel_params=kernel_params, chroot=chroot)
gu.grub1_install(install_devices, boot_device, chroot=chroot)
else:
gu.grub2_cfg(kernel_params=kernel_params, chroot=chroot)
gu.grub2_install(install_devices, chroot=chroot)
self.umount_target(chroot)
def do_provisioning(self):
self.do_parsing()

View File

@@ -23,6 +23,7 @@ class Parted(object):
self.name = name
self.label = label
self.partitions = []
self.install_bootloader = False
def add_partition(self, **kwargs):
# TODO(kozhukalov): validate before appending
@@ -186,6 +187,7 @@ class PartitionScheme(object):
self.vgs = []
self.lvs = []
self.fss = []
self.kernel_params = ''
def add_parted(self, **kwargs):
parted = Parted(**kwargs)
@@ -282,19 +284,61 @@ class PartitionScheme(object):
if found:
return found[0]
def root_device(self):
for fs in self.fss:
if fs.mount == '/':
return fs.device
raise errors.WrongPartitionSchemeError(
'Error while trying to find root device: '
'root file system not found')
def lv_by_device_name(self, device_name):
found = filter(lambda x: x.device_name == device_name, self.lvs)
if found:
return found[0]
def root_device(self):
fs = self.fs_by_mount('/')
if not fs:
raise errors.WrongPartitionSchemeError(
'Error while trying to find root device: '
'root file system not found')
return fs.device
def boot_device(self, grub_version=2):
# We assume /boot is a separate partition. If it is not
# then we try to use root file system
boot_fs = self.fs_by_mount('/boot') or self.fs_by_mount('/')
if not boot_fs:
raise errors.WrongPartitionSchemeError(
'Error while trying to find boot device: '
'boot file system not fount, '
'it must be a separate mount point')
if grub_version == 1:
# Legacy GRUB has a limitation. It is not able to mount MD devices.
# If it is MD compatible it is only able to ignore MD metadata
# and to mount one of those devices which are parts of MD device,
# but it is possible only if MD device is a MIRROR.
md = self.md_by_name(boot_fs.device)
if md:
try:
return md.devices[0]
except IndexError:
raise errors.WrongPartitionSchemeError(
'Error while trying to find boot device: '
'md device %s does not have devices attached' %
md.name)
# Legacy GRUB is not able to mount LVM devices.
if self.lv_by_device_name(boot_fs.device):
raise errors.WrongPartitionSchemeError(
'Error while trying to find boot device: '
'found device is %s but legacy grub is not able to '
'mount logical volumes' %
boot_fs.device)
return boot_fs.device
# Configdrive device must be a small (about 10M) partition
# on one of node hard drives. This partition is necessary
# only if one uses cloud-init with configdrive.
def configdrive_device(self):
# Configdrive device must be a small (about 10M) partition
# on one of node hard drives. This partition is necessary
# only if one uses cloud-init with configdrive.
for parted in self.parteds:
for prt in parted.partitions:
if prt.configdrive:
return prt.name
def append_kernel_params(self, kernel_params):
self.kernel_params += ' ' + kernel_params

View File

@@ -17,18 +17,18 @@ from oslotest import base as test_base
import requests
import zlib
from fuel_agent.utils import img_utils as iu
from fuel_agent.utils import artifact_utils as au
class TestTarget(test_base.BaseTestCase):
def setUp(self):
super(TestTarget, self).setUp()
self.tgt = iu.Target()
self.tgt = au.Target()
def test_target_next(self):
self.assertRaises(StopIteration, self.tgt.next)
@mock.patch.object(iu.Target, '__iter__')
@mock.patch.object(au.Target, '__iter__')
def test_target_target(self, mock_iter):
mock_iter.return_value = iter(['chunk1', 'chunk2', 'chunk3'])
m = mock.mock_open()
@@ -46,7 +46,7 @@ class TestTarget(test_base.BaseTestCase):
class TestLocalFile(test_base.BaseTestCase):
def setUp(self):
super(TestLocalFile, self).setUp()
self.lf = iu.LocalFile('/dev/null')
self.lf = au.LocalFile('/dev/null')
def test_localfile_next(self):
self.lf.fileobj = mock.Mock()
@@ -61,7 +61,8 @@ class TestHttpUrl(test_base.BaseTestCase):
def test_httpurl_iter(self, mock_r_get):
content = ['fake content #1', 'fake content #2']
mock_r_get.return_value.iter_content.return_value = content
httpurl = iu.HttpUrl('fake_url')
mock_r_get.return_value.status_code = 200
httpurl = au.HttpUrl('fake_url')
for data in enumerate(httpurl):
self.assertEqual(content[data[0]], data[1])
self.assertEqual('fake_url', httpurl.url)
@@ -71,7 +72,7 @@ class TestGunzipStream(test_base.BaseTestCase):
def test_gunzip_stream_next(self):
content = ['fake content #1']
compressed_stream = [zlib.compress(data) for data in content]
gunzip_stream = iu.GunzipStream(compressed_stream)
gunzip_stream = au.GunzipStream(compressed_stream)
for data in enumerate(gunzip_stream):
self.assertEqual(content[data[0]], data[1])
@@ -79,7 +80,7 @@ class TestGunzipStream(test_base.BaseTestCase):
class TestChain(test_base.BaseTestCase):
def setUp(self):
super(TestChain, self).setUp()
self.chain = iu.Chain()
self.chain = au.Chain()
def test_append(self):
self.assertEqual(0, len(self.chain.processors))
@@ -89,7 +90,7 @@ class TestChain(test_base.BaseTestCase):
def test_process(self):
self.chain.processors.append('fake_uri')
fake_processor = mock.Mock(spec=iu.Target)
fake_processor = mock.Mock(spec=au.Target)
self.chain.processors.append(fake_processor)
self.chain.processors.append('fake_target')
self.chain.process()

View File

@@ -15,6 +15,7 @@
import mock
from oslotest import base as test_base
from fuel_agent import errors
from fuel_agent.utils import fs_utils as fu
from fuel_agent.utils import utils
@@ -32,3 +33,56 @@ class TestFSUtils(test_base.BaseTestCase):
fu.make_fs('swap', ' -f ', ' -L fake_label ', '/dev/fake')
mock_exec.assert_called_once_with('mkswap', '-f', '-L', 'fake_label',
'/dev/fake')
@mock.patch.object(utils, 'execute')
def test_extend_fs_ok_ext3(self, mock_exec):
fu.extend_fs('ext3', '/dev/fake')
expected_calls = [
mock.call('e2fsck', ' -f', '/dev/fake', check_exit_code=[0]),
mock.call('resize2fs', '/dev/fake', check_exit_code=[0])
]
self.assertEqual(mock_exec.call_args_list, expected_calls)
@mock.patch.object(utils, 'execute')
def test_extend_fs_ok_ext4(self, mock_exec):
fu.extend_fs('ext4', '/dev/fake')
expected_calls = [
mock.call('e2fsck', ' -f', '/dev/fake', check_exit_code=[0]),
mock.call('resize2fs', '/dev/fake', check_exit_code=[0])
]
self.assertEqual(mock_exec.call_args_list, expected_calls)
@mock.patch.object(utils, 'execute')
def test_extend_fs_ok_xfs(self, mock_exec):
fu.extend_fs('xfs', '/dev/fake')
mock_exec.assert_called_once_with(
'xfs_growfs', '/dev/fake', check_exit_code=[0])
@mock.patch.object(utils, 'execute')
def test_extend_fs_unsupported_fs(self, mock_exec):
self.assertRaises(errors.FsUtilsError, fu.extend_fs,
'unsupported', '/dev/fake')
@mock.patch.object(utils, 'execute')
def test_mount_fs(self, mock_exec):
fu.mount_fs('ext3', '/dev/fake', '/target')
mock_exec.assert_called_once_with(
'mount', '-t', 'ext3', '/dev/fake', '/target', check_exit_code=[0])
@mock.patch.object(utils, 'execute')
def test_mount_bind_no_path2(self, mock_exec):
fu.mount_bind('/target', '/fake')
mock_exec.assert_called_once_with(
'mount', '--bind', '/fake', '/target/fake', check_exit_code=[0])
@mock.patch.object(utils, 'execute')
def test_mount_bind_path2(self, mock_exec):
fu.mount_bind('/target', '/fake', '/fake2')
mock_exec.assert_called_once_with(
'mount', '--bind', '/fake', '/target/fake2', check_exit_code=[0])
@mock.patch.object(utils, 'execute')
def test_umount_fs(self, mock_exec):
fu.umount_fs('/fake')
mock_exec.assert_called_once_with(
'umount', '/fake', check_exit_code=[0])

View File

@@ -0,0 +1,390 @@
# Copyright 2014 Mirantis, Inc.
#
# 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 mock
import os
from oslotest import base as test_base
import six
import StringIO
if six.PY2:
OPEN_FUNCTION_NAME = '__builtin__.open'
else:
OPEN_FUNCTION_NAME = 'builtins.open'
from fuel_agent import errors
from fuel_agent.utils import grub_utils as gu
from fuel_agent.utils import utils
class TestGrubUtils(test_base.BaseTestCase):
@mock.patch.object(os.path, 'isfile')
def test_guess_grub2_conf(self, mock_isfile):
side_effect_values = {
'/target/boot/grub/grub.cfg': True,
'/target/boot/grub2/grub.cfg': False
}
def side_effect(key):
return side_effect_values[key]
mock_isfile.side_effect = side_effect
self.assertEqual(gu.guess_grub2_conf('/target'),
'/boot/grub/grub.cfg')
side_effect_values = {
'/target/boot/grub/grub.cfg': False,
'/target/boot/grub2/grub.cfg': True
}
self.assertEqual(gu.guess_grub2_conf('/target'),
'/boot/grub2/grub.cfg')
@mock.patch.object(os.path, 'isfile')
def test_guess_grub2_default(self, mock_isfile):
side_effect_values = {
'/target/etc/default/grub': True,
'/target/etc/sysconfig/grub': False
}
def side_effect(key):
return side_effect_values[key]
mock_isfile.side_effect = side_effect
self.assertEqual(gu.guess_grub2_default('/target'),
'/etc/default/grub')
side_effect_values = {
'/target/etc/default/grub': False,
'/target/etc/sysconfig/grub': True
}
self.assertEqual(gu.guess_grub2_default('/target'),
'/etc/sysconfig/grub')
@mock.patch.object(os.path, 'isfile')
def test_guess_grub2_mkconfig(self, mock_isfile):
side_effect_values = {
'/target/sbin/grub-mkconfig': True,
'/target/sbin/grub2-mkconfig': False,
'/target/usr/sbin/grub-mkconfig': False,
'/target/usr/sbin/grub2-mkconfig': False
}
def side_effect(key):
return side_effect_values[key]
mock_isfile.side_effect = side_effect
self.assertEqual(gu.guess_grub2_mkconfig('/target'),
'/sbin/grub-mkconfig')
side_effect_values = {
'/target/sbin/grub-mkconfig': False,
'/target/sbin/grub2-mkconfig': True,
'/target/usr/sbin/grub-mkconfig': False,
'/target/usr/sbin/grub2-mkconfig': False
}
self.assertEqual(gu.guess_grub2_mkconfig('/target'),
'/sbin/grub2-mkconfig')
side_effect_values = {
'/target/sbin/grub-mkconfig': False,
'/target/sbin/grub2-mkconfig': False,
'/target/usr/sbin/grub-mkconfig': True,
'/target/usr/sbin/grub2-mkconfig': False
}
self.assertEqual(gu.guess_grub2_mkconfig('/target'),
'/usr/sbin/grub-mkconfig')
side_effect_values = {
'/target/sbin/grub-mkconfig': False,
'/target/sbin/grub2-mkconfig': False,
'/target/usr/sbin/grub-mkconfig': False,
'/target/usr/sbin/grub2-mkconfig': True
}
self.assertEqual(gu.guess_grub2_mkconfig('/target'),
'/usr/sbin/grub2-mkconfig')
@mock.patch.object(gu, 'guess_grub_install')
@mock.patch.object(utils, 'execute')
def test_guess_grub_version_1(self, mock_exec, mock_ggi):
mock_ggi.return_value = '/grub_install'
mock_exec.return_value = ('foo 0.97 bar', '')
version = gu.guess_grub_version('/target')
mock_exec.assert_called_once_with('/target/grub_install', '-v')
self.assertEqual(version, 1)
@mock.patch.object(gu, 'guess_grub_install')
@mock.patch.object(utils, 'execute')
def test_guess_grub_version_2(self, mock_exec, mock_ggi):
mock_ggi.return_value = '/grub_install'
mock_exec.return_value = ('foo bar', '')
version = gu.guess_grub_version('/target')
mock_exec.assert_called_once_with('/target/grub_install', '-v')
self.assertEqual(version, 2)
@mock.patch.object(os.path, 'isfile')
def test_guess_grub(self, mock_isfile):
side_effect_values = {
'/target/sbin/grub': True,
'/target/usr/sbin/grub': False
}
def side_effect(key):
return side_effect_values[key]
mock_isfile.side_effect = side_effect
self.assertEqual(gu.guess_grub('/target'),
'/sbin/grub')
side_effect_values = {
'/target/sbin/grub': False,
'/target/usr/sbin/grub': True
}
self.assertEqual(gu.guess_grub('/target'),
'/usr/sbin/grub')
side_effect_values = {
'/target/sbin/grub': False,
'/target/usr/sbin/grub': False
}
self.assertRaises(errors.GrubUtilsError, gu.guess_grub, '/target')
@mock.patch.object(os.path, 'isfile')
def test_grub_install(self, mock_isfile):
side_effect_values = {
'/target/sbin/grub-install': True,
'/target/sbin/grub2-install': False,
'/target/usr/sbin/grub-install': False,
'/target/usr/sbin/grub2-install': False
}
def side_effect(key):
return side_effect_values[key]
mock_isfile.side_effect = side_effect
self.assertEqual(gu.guess_grub_install('/target'),
'/sbin/grub-install')
side_effect_values = {
'/target/sbin/grub-install': False,
'/target/sbin/grub2-install': True,
'/target/usr/sbin/grub-install': False,
'/target/usr/sbin/grub2-install': False
}
self.assertEqual(gu.guess_grub_install('/target'),
'/sbin/grub2-install')
side_effect_values = {
'/target/sbin/grub-install': False,
'/target/sbin/grub2-install': False,
'/target/usr/sbin/grub-install': True,
'/target/usr/sbin/grub2-install': False
}
self.assertEqual(gu.guess_grub_install('/target'),
'/usr/sbin/grub-install')
side_effect_values = {
'/target/sbin/grub-install': False,
'/target/sbin/grub2-install': False,
'/target/usr/sbin/grub-install': False,
'/target/usr/sbin/grub2-install': True
}
self.assertEqual(gu.guess_grub_install('/target'),
'/usr/sbin/grub2-install')
@mock.patch.object(os, 'listdir')
def test_guess_kernel(self, mock_listdir):
mock_listdir.return_value = ['1', '2', 'vmlinuz-version', '3']
self.assertEqual(gu.guess_kernel('/target'), 'vmlinuz-version')
mock_listdir.return_value = ['1', '2', '3']
self.assertRaises(errors.GrubUtilsError, gu.guess_kernel, '/target')
@mock.patch.object(os, 'listdir')
def test_guess_initrd(self, mock_listdir):
mock_listdir.return_value = ['1', '2', 'initramfs-version', '3']
self.assertEqual(gu.guess_initrd('/target'), 'initramfs-version')
mock_listdir.return_value = ['1', '2', 'initrd-version', '3']
self.assertEqual(gu.guess_initrd('/target'), 'initrd-version')
mock_listdir.return_value = ['1', '2', '3']
self.assertRaises(errors.GrubUtilsError, gu.guess_initrd, '/target')
@mock.patch.object(gu, 'grub1_mbr')
def test_grub1_install(self, mock_mbr):
install_devices = ['/dev/foo', '/dev/bar']
expected_calls_mbr = []
for install_device in install_devices:
expected_calls_mbr.append(
mock.call(install_device, '/dev/foo', '0', chroot='/target'))
gu.grub1_install(install_devices, '/dev/foo1', '/target')
self.assertEqual(expected_calls_mbr, mock_mbr.call_args_list)
# should raise exception if boot_device (second argument)
# is not a partition but a whole disk
self.assertRaises(errors.GrubUtilsError, gu.grub1_install,
'/dev/foo', '/dev/foo', chroot='/target')
@mock.patch.object(gu, 'guess_grub')
@mock.patch.object(os, 'chmod')
@mock.patch.object(utils, 'execute')
def test_grub1_mbr_install_differs_boot(self, mock_exec,
mock_chmod, mock_guess):
mock_guess.return_value = '/sbin/grub'
# install_device != boot_disk
batch = 'device (hd0) /dev/foo\n'
batch += 'geometry (hd0) 130 255 63\n'
batch += 'device (hd1) /dev/bar\n'
batch += 'geometry (hd1) 130 255 63\n'
batch += 'root (hd1,0)\n'
batch += 'setup (hd0)\n'
batch += 'quit\n'
script = 'cat /tmp/grub.batch | /sbin/grub --no-floppy --batch'
mock_open = mock.mock_open()
with mock.patch(OPEN_FUNCTION_NAME, new=mock_open, create=True):
gu.grub1_mbr('/dev/foo', '/dev/bar', '0', chroot='/target')
self.assertEqual(
mock_open.call_args_list,
[mock.call('/target/tmp/grub.batch', 'wb'),
mock.call('/target/tmp/grub.sh', 'wb')]
)
mock_open_file = mock_open()
self.assertEqual(
mock_open_file.write.call_args_list,
[mock.call(batch), mock.call(script)]
)
mock_chmod.assert_called_once_with('/target/tmp/grub.sh', 0o755)
mock_exec.assert_called_once_with(
'chroot', '/target', '/tmp/grub.sh',
run_as_root=True, check_exit_code=[0])
@mock.patch.object(gu, 'guess_grub')
@mock.patch.object(os, 'chmod')
@mock.patch.object(utils, 'execute')
def test_grub1_mbr_install_same_as_boot(self, mock_exec,
mock_chmod, mock_guess):
mock_guess.return_value = '/sbin/grub'
# install_device == boot_disk
batch = 'device (hd0) /dev/foo\n'
batch += 'geometry (hd0) 130 255 63\n'
batch += 'root (hd0,0)\n'
batch += 'setup (hd0)\n'
batch += 'quit\n'
script = 'cat /tmp/grub.batch | /sbin/grub --no-floppy --batch'
mock_open = mock.mock_open()
with mock.patch(OPEN_FUNCTION_NAME, new=mock_open, create=True):
gu.grub1_mbr('/dev/foo', '/dev/foo', '0', chroot='/target')
self.assertEqual(
mock_open.call_args_list,
[mock.call('/target/tmp/grub.batch', 'wb'),
mock.call('/target/tmp/grub.sh', 'wb')]
)
mock_open_file = mock_open()
self.assertEqual(
mock_open_file.write.call_args_list,
[mock.call(batch), mock.call(script)]
)
mock_chmod.assert_called_once_with('/target/tmp/grub.sh', 0o755)
mock_exec.assert_called_once_with(
'chroot', '/target', '/tmp/grub.sh',
run_as_root=True, check_exit_code=[0])
@mock.patch.object(gu, 'guess_kernel')
@mock.patch.object(gu, 'guess_initrd')
def test_grub1_cfg_kernel_initrd_are_not_set(self, mock_initrd,
mock_kernel):
mock_kernel.return_value = 'kernel-version'
mock_initrd.return_value = 'initrd-version'
config = """
default=0
timeout=5
title Default (kernel-version)
kernel /kernel-version kernel-params
initrd /initrd-version
"""
mock_open = mock.mock_open()
with mock.patch(OPEN_FUNCTION_NAME, new=mock_open, create=True):
gu.grub1_cfg(chroot='/target', kernel_params='kernel-params')
mock_open.assert_called_once_with('/target/boot/grub/grub.conf', 'wb')
mock_open_file = mock_open()
mock_open_file.write.assert_called_once_with(config)
def test_grub1_cfg_kernel_initrd_are_set(self):
config = """
default=0
timeout=5
title Default (kernel-version-set)
kernel /kernel-version-set kernel-params
initrd /initrd-version-set
"""
mock_open = mock.mock_open()
with mock.patch(OPEN_FUNCTION_NAME, new=mock_open, create=True):
gu.grub1_cfg(kernel='kernel-version-set',
initrd='initrd-version-set',
chroot='/target', kernel_params='kernel-params')
mock_open.assert_called_once_with('/target/boot/grub/grub.conf', 'wb')
mock_open_file = mock_open()
mock_open_file.write.assert_called_once_with(config)
@mock.patch.object(utils, 'execute')
@mock.patch.object(gu, 'guess_grub_install')
def test_grub2_install(self, mock_guess_grub, mock_exec):
mock_guess_grub.return_value = '/sbin/grub'
expected_calls = [
mock.call('chroot', '/target', '/sbin/grub', '/dev/foo',
run_as_root=True, check_exit_code=[0]),
mock.call('chroot', '/target', '/sbin/grub', '/dev/bar',
run_as_root=True, check_exit_code=[0])
]
gu.grub2_install(['/dev/foo', '/dev/bar'], chroot='/target')
self.assertEqual(mock_exec.call_args_list, expected_calls)
@mock.patch.object(gu, 'guess_grub2_conf')
@mock.patch.object(gu, 'guess_grub2_mkconfig')
@mock.patch.object(utils, 'execute')
@mock.patch.object(gu, 'guess_grub2_default')
def test_grub2_cfg(self, mock_def, mock_exec, mock_mkconfig, mock_conf):
mock_def.return_value = '/etc/default/grub'
mock_mkconfig.return_value = '/sbin/grub-mkconfig'
mock_conf.return_value = '/boot/grub/grub.cfg'
orig_content = """foo
GRUB_CMDLINE_LINUX="kernel-params-orig"
bar"""
new_content = """foo
GRUB_CMDLINE_LINUX="kernel-params-new"
bar"""
# mock_open = mock.mock_open(read_data=orig_content)
with mock.patch(OPEN_FUNCTION_NAME,
new=mock.mock_open(read_data=orig_content),
create=True) as mock_open:
mock_open.return_value = mock.MagicMock(spec=file)
handle = mock_open.return_value.__enter__.return_value
handle.__iter__.return_value = StringIO.StringIO(orig_content)
gu.grub2_cfg(kernel_params='kernel-params-new', chroot='/target')
self.assertEqual(
mock_open.call_args_list,
[mock.call('/target/etc/default/grub'),
mock.call('/target/etc/default/grub', 'wb')]
)
handle.write.assert_called_once_with(new_content)
mock_exec.assert_called_once_with('chroot', '/target',
'/sbin/grub-mkconfig',
'-o', '/boot/grub/grub.cfg',
run_as_root=True)

View File

@@ -21,9 +21,9 @@ from fuel_agent import errors
from fuel_agent import manager
from fuel_agent.objects import partition
from fuel_agent.tests import test_nailgun
from fuel_agent.utils import artifact_utils as au
from fuel_agent.utils import fs_utils as fu
from fuel_agent.utils import hardware_utils as hu
from fuel_agent.utils import img_utils as iu
from fuel_agent.utils import lvm_utils as lu
from fuel_agent.utils import md_utils as mu
from fuel_agent.utils import partition_utils as pu
@@ -131,7 +131,7 @@ class TestManager(test_base.BaseTestCase):
def test_do_configdrive(self, mock_lbd, mock_u_ras, mock_u_e):
mock_lbd.return_value = test_nailgun.LIST_BLOCK_DEVICES_SAMPLE
self.mgr.do_parsing()
self.assertEqual(1, len(self.mgr.image_scheme.images))
self.assertEqual(4, len(self.mgr.image_scheme.images))
self.mgr.do_configdrive()
mock_u_ras_expected_calls = [
mock.call(CONF.nc_template_path, 'cloud_config_ubuntu.jinja2',
@@ -155,7 +155,7 @@ class TestManager(test_base.BaseTestCase):
'%s/%s' % (CONF.tmp_path, 'user-data'),
'%s/%s' % (CONF.tmp_path, 'meta-data'))]
self.assertEqual(mock_u_e_expected_calls, mock_u_e.call_args_list)
self.assertEqual(2, len(self.mgr.image_scheme.images))
self.assertEqual(5, len(self.mgr.image_scheme.images))
cf_drv_img = self.mgr.image_scheme.images[-1]
self.assertEqual('file://%s' % CONF.config_drive_path, cf_drv_img.uri)
self.assertEqual('/dev/sda7',
@@ -175,15 +175,15 @@ class TestManager(test_base.BaseTestCase):
self.assertRaises(errors.WrongPartitionSchemeError,
self.mgr.do_configdrive)
@mock.patch.object(iu, 'GunzipStream')
@mock.patch.object(iu, 'LocalFile')
@mock.patch.object(iu, 'HttpUrl')
@mock.patch.object(iu, 'Chain')
@mock.patch.object(au, 'GunzipStream')
@mock.patch.object(au, 'LocalFile')
@mock.patch.object(au, 'HttpUrl')
@mock.patch.object(au, 'Chain')
@mock.patch.object(utils, 'execute')
@mock.patch.object(utils, 'render_and_save')
@mock.patch.object(hu, 'list_block_devices')
def test_do_copyimage(self, mock_lbd, mock_u_ras, mock_u_e, mock_iu_c,
mock_iu_h, mock_iu_l, mock_iu_g):
def test_do_copyimage(self, mock_lbd, mock_u_ras, mock_u_e, mock_au_c,
mock_au_h, mock_au_l, mock_au_g):
class FakeChain(object):
processors = []
@@ -195,14 +195,24 @@ class TestManager(test_base.BaseTestCase):
pass
mock_lbd.return_value = test_nailgun.LIST_BLOCK_DEVICES_SAMPLE
mock_iu_c.return_value = FakeChain()
mock_au_c.return_value = FakeChain()
self.mgr.do_parsing()
self.mgr.do_configdrive()
self.mgr.do_copyimage()
imgs = self.mgr.image_scheme.images
self.assertEqual(2, len(imgs))
expected_processors_list = [imgs[0].uri, iu.HttpUrl, iu.GunzipStream,
imgs[0].target_device, imgs[1].uri,
iu.LocalFile, imgs[1].target_device]
self.assertEqual(5, len(imgs))
expected_processors_list = []
for img in imgs[:-1]:
expected_processors_list += [
img.uri,
au.HttpUrl,
au.GunzipStream,
img.target_device
]
expected_processors_list += [
imgs[-1].uri,
au.LocalFile,
imgs[-1].target_device
]
self.assertEqual(expected_processors_list,
mock_iu_c.return_value.processors)
mock_au_c.return_value.processors)

View File

@@ -17,6 +17,7 @@ from oslotest import base as test_base
from fuel_agent.drivers import nailgun
from fuel_agent import errors
from fuel_agent.objects import image
from fuel_agent.utils import hardware_utils as hu
@@ -477,13 +478,28 @@ class TestNailgun(test_base.BaseTestCase):
mock_lbd.return_value = LIST_BLOCK_DEVICES_SAMPLE
p_scheme = self.drv.partition_scheme()
i_scheme = self.drv.image_scheme(p_scheme)
self.assertEqual(1, len(i_scheme.images))
img = i_scheme.images[0]
self.assertEqual('gzip', img.container)
self.assertEqual('ext4', img.image_format)
self.assertEqual('/dev/mapper/os-root', img.target_device)
self.assertEqual('http://fake_image_url', img.uri)
self.assertEqual(None, img.size)
expected_images = []
for fs in p_scheme.fss:
if fs.mount == 'swap':
continue
expected_images.append(image.Image(
uri='http://%s/targetimages/%s%s.img.gz' % (
self.drv.data['ks_meta']['master_ip'],
self.drv.data['profile'].split('_')[0],
'-'.join(fs.mount.split('/')).rstrip('-')),
target_device=fs.device,
image_format='ext4',
container='gzip'
))
expected_images = sorted(expected_images, key=lambda x: x.uri)
for i, img in enumerate(sorted(i_scheme.images, key=lambda x: x.uri)):
self.assertEqual(img.uri, expected_images[i].uri)
self.assertEqual(img.target_device,
expected_images[i].target_device)
self.assertEqual(img.image_format,
expected_images[i].image_format)
self.assertEqual(img.container,
expected_images[i].container)
def test_getlabel(self):
self.assertEqual('', self.drv._getlabel(None))

View File

@@ -0,0 +1,172 @@
# Copyright 2014 Mirantis, Inc.
#
# 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 abc
import requests
import tarfile
import tempfile
import zlib
class Target(object):
__metaclass__ = abc.ABCMeta
def __iter__(self):
return self
def next(self):
raise StopIteration()
def target(self, filename='/dev/null'):
with open(filename, 'wb') as f:
for chunk in self:
f.write(chunk)
f.flush()
class LocalFile(Target):
def __init__(self, filename):
self.filename = str(filename)
self.fileobj = None
def next(self):
if not self.fileobj:
self.fileobj = open(self.filename, 'rb')
buffer = self.fileobj.read(1048576)
if buffer:
return buffer
else:
self.fileobj.close()
raise StopIteration()
class HttpUrl(Target):
def __init__(self, url):
self.url = str(url)
def __iter__(self):
response = requests.get(self.url, stream=True)
if response.status_code != 200:
raise Exception('Can not get %s' % self.url)
return iter(response.iter_content(1048576))
class GunzipStream(Target):
def __init__(self, stream):
self.stream = iter(stream)
#NOTE(agordeev): toggle automatic header detection on
self.decompressor = zlib.decompressobj(zlib.MAX_WBITS | 32)
def next(self):
try:
return self.decompressor.decompress(self.stream.next())
except StopIteration:
raise
class ForwardFileStream(Target):
def __init__(self, stream):
self.stream = iter(stream)
self.position = 0
self.chunk = ''
self.closed = False
def next(self):
buffer = self.read()
if buffer:
return buffer
else:
raise StopIteration()
def close(self):
self.closed = True
def tell(self):
if self.closed:
raise ValueError('I/O operation on closed file')
return self.position
def seek(self, position):
if self.closed:
raise ValueError('I/O operation on closed file')
if position < self.position:
raise ValueError('Backward seek operation is impossible')
elif position < self.position + len(self.chunk):
self.chunk = self.chunk[(position - self.position):]
self.position = position
else:
try:
current = self.position + len(self.chunk)
while True:
chunk = self.stream.next()
if current + len(chunk) >= position:
self.chunk = chunk[(position - current):]
self.position = position
break
current += len(chunk)
except StopIteration:
self.chunk = None
self.position = position
def read(self, length=1048576):
# NOTE(kozhukalov): default lenght = 1048576 is not usual behaviour,
# but that is ok for our use case.
if self.closed:
raise ValueError('I/O operation on closed file')
if self.chunk is None:
return None
try:
while len(self.chunk) < length:
self.chunk += self.stream.next()
result = self.chunk[:length]
self.chunk = self.chunk[length:]
except StopIteration:
result = self.chunk
self.chunk = None
self.position += len(result)
return result
class TarStream(Target):
def __init__(self, stream):
self.stream = iter(stream)
self.tarobj = None
def target(self, filename=None):
if not self.tarobj:
self.tarobj = tarfile.open(
fileobj=ForwardFileStream(self.stream), mode='r:')
self.tarobj.extractall(path=(filename or tempfile.gettempdir()))
class Chain(object):
def __init__(self):
self.processors = []
def append(self, processor):
self.processors.append(processor)
def process(self):
def jump(proc, next_proc):
# if next_proc is just a string we assume it is a filename
# and we save stream into a file
if isinstance(next_proc, (str, unicode)):
proc.target(next_proc)
return LocalFile(next_proc)
# if next_proc is not a string we return new instance
# initialized with the previous one
else:
return next_proc(proc)
return reduce(jump, self.processors)

View File

@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from fuel_agent import errors
from fuel_agent.utils import utils
@@ -27,3 +28,32 @@ def make_fs(fs_type, fs_options, fs_label, dev):
cmd_line.extend([s for s in opt.split(' ') if s])
cmd_line.append(dev)
utils.execute(*cmd_line)
def extend_fs(fs_type, fs_dev):
if fs_type in ('ext3', 'ext4'):
# ext3,4 file system can be mounted
# must be checked with e2fsck -f
utils.execute('e2fsck', ' -f', fs_dev, check_exit_code=[0])
utils.execute('resize2fs', fs_dev, check_exit_code=[0])
elif fs_type == 'xfs':
# xfs file system must be mounted
utils.execute('xfs_growfs', fs_dev, check_exit_code=[0])
else:
raise errors.FsUtilsError('Unsupported file system type')
def mount_fs(fs_type, fs_dev, fs_mount):
utils.execute('mount', '-t', fs_type, fs_dev, fs_mount,
check_exit_code=[0])
def mount_bind(chroot, path, path2=None):
if not path2:
path2 = path
utils.execute('mount', '--bind', path, chroot + path2,
check_exit_code=[0])
def umount_fs(fs_mount):
utils.execute('umount', fs_mount, check_exit_code=[0])

View File

@@ -0,0 +1,188 @@
# Copyright 2014 Mirantis, Inc.
#
# 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 os
import re
from fuel_agent import errors
from fuel_agent.openstack.common import log as logging
from fuel_agent.utils import utils
LOG = logging.getLogger(__name__)
def guess_grub2_conf(chroot=''):
for filename in ('/boot/grub/grub.cfg', '/boot/grub2/grub.cfg'):
if os.path.isfile(chroot + filename):
return filename
def guess_grub2_default(chroot=''):
for filename in ('/etc/default/grub', '/etc/sysconfig/grub'):
if os.path.isfile(chroot + filename):
return filename
def guess_grub2_mkconfig(chroot=''):
for grub_mkconfig in \
('/sbin/grub-mkconfig', '/sbin/grub2-mkconfig',
'/usr/sbin/grub-mkconfig', '/usr/sbin/grub2-mkconfig'):
if os.path.isfile(chroot + grub_mkconfig):
return grub_mkconfig
def guess_grub_version(chroot=''):
grub_install = guess_grub_install(chroot=chroot)
LOG.debug('Trying to run %s -v' % grub_install)
result = utils.execute(chroot + grub_install, '-v')
version = 1 if result[0].find('0.97') > 0 else 2
LOG.debug('Looks like grub version is %s' % version)
return version
def guess_grub(chroot=''):
for grub in ('/sbin/grub', '/usr/sbin/grub'):
LOG.debug('Looking for grub: trying %s' % grub)
if os.path.isfile(chroot + grub):
LOG.debug('grub found: %s' % grub)
return grub
raise errors.GrubUtilsError('grub not found')
def guess_grub_install(chroot=''):
for grub_install in ('/sbin/grub-install', '/sbin/grub2-install',
'/usr/sbin/grub-install', '/usr/sbin/grub2-install'):
LOG.debug('Looking for grub-install: trying %s' % grub_install)
if os.path.isfile(chroot + grub_install):
LOG.debug('grub-install found: %s' % grub_install)
return grub_install
raise errors.GrubUtilsError('grub-install not found')
def guess_kernel(chroot=''):
for filename in sorted(os.listdir(chroot + '/boot'), reverse=True):
# We assume kernel name is always starts with vmlinuz.
# We use the newest one.
if filename.startswith('vmlinuz'):
return filename
raise errors.GrubUtilsError('Error while trying to find kernel: not found')
def guess_initrd(chroot=''):
for filename in sorted(os.listdir(chroot + '/boot'), reverse=True):
# We assume initrd starts either with initramfs or initrd.
if filename.startswith('initramfs') or \
filename.startswith('initrd'):
return filename
raise errors.GrubUtilsError('Error while trying to find initrd: not found')
def grub1_install(install_devices, boot_device, chroot=''):
match = re.search(r'(.+?)(p?)(\d*)$', boot_device)
# Checking whether boot device is a partition
# !!! It must be a partition not a whole disk. !!!
if not match.group(3):
raise errors.GrubUtilsError(
'Error while installing legacy grub: '
'boot device must be a partition')
boot_disk = match.group(1)
boot_part = str(int(match.group(3)) - 1)
for install_device in install_devices:
grub1_mbr(install_device, boot_disk, boot_part, chroot=chroot)
def grub1_mbr(install_device, boot_disk, boot_part, chroot=''):
# The device on which we are going to install
# stage1 needs to be mapped as hd0, otherwise system won't be able to boot.
batch = 'device (hd0) {0}\n'.format(install_device)
# That is much easier to use grub-install, but unfortunately
# it is not able to install bootloader on huge disks.
# Instead we set drive geometry manually to avoid grub register
# overlapping. We set it so as to make grub
# thinking that disk size is equal to 1G.
# 130 cylinders * (16065 * 512 = 8225280 bytes) = 1G
# We also assume that boot partition is in the beginning
# of disk between 0 and 1G.
batch += 'geometry (hd0) 130 255 63\n'
if boot_disk != install_device:
batch += 'device (hd1) {0}\n'.format(boot_disk)
batch += 'geometry (hd1) 130 255 63\n'
batch += 'root (hd1,{0})\n'.format(boot_part)
else:
batch += 'root (hd0,{0})\n'.format(boot_part)
batch += 'setup (hd0)\n'
batch += 'quit\n'
with open(chroot + '/tmp/grub.batch', 'wb') as f:
LOG.debug('Grub batch content: \n%s' % batch)
f.write(batch)
script = 'cat /tmp/grub.batch | {0} --no-floppy --batch'.format(
guess_grub(chroot=chroot))
with open(chroot + '/tmp/grub.sh', 'wb') as f:
LOG.debug('Grub script content: \n%s' % script)
f.write(script)
os.chmod(chroot + '/tmp/grub.sh', 0o755)
cmd = ['/tmp/grub.sh']
if chroot:
cmd[:0] = ['chroot', chroot]
utils.execute(*cmd, run_as_root=True, check_exit_code=[0])
def grub1_cfg(kernel=None, initrd=None,
kernel_params='', chroot=''):
if not kernel:
kernel = guess_kernel(chroot=chroot)
if not initrd:
initrd = guess_initrd(chroot=chroot)
config = """
default=0
timeout=5
title Default ({kernel})
kernel /{kernel} {kernel_params}
initrd /{initrd}
""".format(kernel=kernel, initrd=initrd,
kernel_params=kernel_params)
with open(chroot + '/boot/grub/grub.conf', 'wb') as f:
f.write(config)
def grub2_install(install_devices, chroot=''):
grub_install = guess_grub_install()
for install_device in install_devices:
cmd = [grub_install, install_device]
if chroot:
cmd[:0] = ['chroot', chroot]
utils.execute(*cmd, run_as_root=True, check_exit_code=[0])
def grub2_cfg(kernel_params='', chroot=''):
grub_defaults = chroot + guess_grub2_default(chroot=chroot)
regex = re.compile(r'^.*GRUB_CMDLINE_LINUX.*')
new_content = ''
with open(grub_defaults) as f:
for line in f:
new_content += regex.sub(
'GRUB_CMDLINE_LINUX="{kernel_params}"'.
format(kernel_params=kernel_params), line)
with open(grub_defaults, 'wb') as f:
f.write(new_content)
cmd = [guess_grub2_mkconfig(), '-o', guess_grub2_conf()]
if chroot:
cmd[:0] = ['chroot', chroot]
utils.execute(*cmd, run_as_root=True)

View File

@@ -1,91 +0,0 @@
# Copyright 2014 Mirantis, Inc.
#
# 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 abc
import requests
import zlib
class Target(object):
__metaclass__ = abc.ABCMeta
def __iter__(self):
return self
def next(self):
raise StopIteration()
def target(self, filename='/dev/null'):
with open(filename, 'wb') as f:
for chunk in self:
f.write(chunk)
f.flush()
class LocalFile(Target):
def __init__(self, filename):
self.filename = str(filename)
self.fileobj = None
def next(self):
if not self.fileobj:
self.fileobj = open(self.filename, 'rb')
buffer = self.fileobj.read(1048576)
if buffer:
return buffer
else:
self.fileobj.close()
raise StopIteration()
class HttpUrl(Target):
def __init__(self, url):
self.url = str(url)
def __iter__(self):
return iter(requests.get(self.url, stream=True).iter_content(1048576))
class GunzipStream(Target):
def __init__(self, stream):
self.stream = iter(stream)
#NOTE(agordeev): toggle automatic header detection on
self.decompressor = zlib.decompressobj(zlib.MAX_WBITS | 32)
def next(self):
try:
return self.decompressor.decompress(self.stream.next())
except StopIteration:
raise
class Chain(object):
def __init__(self):
self.processors = []
def append(self, processor):
self.processors.append(processor)
def process(self):
def jump(proc, next_proc):
# if next_proc is just a string we assume it is a filename
# and we save stream into a file
if isinstance(next_proc, (str, unicode)):
proc.target(next_proc)
return LocalFile(next_proc)
# if next_proc is not a string we return new instance
# initialized with the previous one
else:
return next_proc(proc)
return reduce(jump, self.processors)