Add patch-active-img command

Required for creating new patched bootstrap image
based on active bootstrap image.

It patches active bootstrap image to skip the step
that add UEFI.

Note:

* You should active your patched image using command:

    fuel-bootstrap activate <img_uuid>

* squashfs-tools should be installed on master node

    yum install squashfs-tools

Usage:

    octane patch-active-img

Closes-bug: 1575054

Change-Id: I7b12db209b5d2db158d4c26bc162bab504aa1590
This commit is contained in:
Sergey Abramov 2016-06-09 13:08:46 +03:00
parent a51ba6e9e3
commit 3a432b63c9
5 changed files with 236 additions and 2 deletions

View File

@ -0,0 +1,104 @@
# 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 hashlib
import logging
import os
import tarfile
import tempfile
import uuid
import yaml
from cliff import command
from octane import magic_consts
from octane.util import patch
from octane.util import subprocess
from octane.util import tempfile as temp_util
LOG = logging.getLogger(__name__)
IMAGE_LABEL = "patched_image"
def _patch_squashfs(root_img, patched_img, *patches):
with temp_util.temp_dir() as patch_dir:
LOG.info("unsquash root image to temporary directory")
subprocess.call(["unsquashfs", "-f", "-d", patch_dir, root_img])
LOG.info("apply patch to root image")
patch.patch_apply(patch_dir, patches)
LOG.info("create new root.squashfs image")
subprocess.call(["mksquashfs", patch_dir, patched_img])
def calculate_md5(filename):
md5 = hashlib.md5()
chunk_size = 4048
with open(filename, "rb") as f:
block = f.read(chunk_size)
while block:
md5.update(block)
block = f.read(chunk_size)
return md5.hexdigest()
def _mk_metadata(src, dst, root_fs_path):
with open(src) as fd:
metadata = yaml.load(fd)
uuid_val = metadata["uuid"]
my_uuid_val = str(uuid.uuid1())
metadata["label"] = IMAGE_LABEL
metadata["uuid"] = my_uuid_val
for module in metadata["modules"].values():
module["uri"] = module["uri"].replace(uuid_val, my_uuid_val)
metadata["modules"]["rootfs"]["raw_size"] = os.path.getsize(root_fs_path)
metadata["modules"]["rootfs"]["raw_md5"] = calculate_md5(root_fs_path)
with open(dst, "w") as fd:
yaml.dump(metadata, fd)
def patch_img():
root_img = os.path.join(magic_consts.ACTIVE_IMG_PATH, "root.squashfs")
active_metadata_path = os.path.join(
magic_consts.ACTIVE_IMG_PATH, "metadata.yaml")
patch_file = os.path.join(magic_consts.CWD, "patches/fuel_agent/patch")
path_archname_pairs = [(os.path.join(magic_consts.ACTIVE_IMG_PATH, p), p)
for p in ["vmlinuz", "initrd.img"]]
with temp_util.temp_dir() as temp_dir:
patched_img = os.path.join(temp_dir, "root.squashfs")
patched_metadata_path = os.path.join(temp_dir, "metadata.yaml")
_patch_squashfs(root_img, patched_img, patch_file)
_mk_metadata(active_metadata_path, patched_metadata_path, patched_img)
path_archname_pairs.append((patched_img, "root.squashfs"))
path_archname_pairs.append((patched_metadata_path, "metadata.yaml"))
with tempfile.NamedTemporaryFile() as archive_file:
with tarfile.open(name=archive_file.name, mode="w:gz") as archive:
for path, archname in path_archname_pairs:
archive.add(path, archname)
LOG.info("Import image using fuel-bootstrap")
subprocess.call(["fuel-bootstrap", "import", archive_file.name])
LOG.info("Activate image using `fuel-bootstrap activate`")
class PatchImgCommand(command.Command):
"""Create patched bootstrap image with label `{0}`""".format(IMAGE_LABEL)
def take_action(self, parsed_args):
patch_img()

View File

@ -77,3 +77,4 @@ CONFIGDRIVE_PART_SIZE = 10
KEYSTONE_CONF = "/etc/keystone/keystone.conf"
KEYSTONE_PASTE = "/etc/keystone/keystone-paste.ini"
ACTIVE_IMG_PATH = "/var/www/nailgun/bootstraps/active_bootstrap/"

View File

@ -1,5 +1,5 @@
--- usr/lib/python2.7/dist-packages/fuel_agent/drivers/nailgun.py
+++ usr/lib/python2.7/dist-packages/fuel_agent/drivers/nailgun.py
--- a/usr/lib/python2.7/dist-packages/fuel_agent/drivers/nailgun.py
+++ b/usr/lib/python2.7/dist-packages/fuel_agent/drivers/nailgun.py
@@ -321,10 +321,6 @@
LOG.debug('Adding bios_grub partition on disk %s: size=24' %
disk['name'])

View File

@ -0,0 +1,128 @@
# 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
import pytest
from octane.commands import patch_active_image
from octane import magic_consts
@pytest.mark.parametrize("root_img", ["root_img_path"])
@pytest.mark.parametrize("patched_img", ["patch_img_path"])
@pytest.mark.parametrize("patches", [("patch_1", ), ("patch_1", "patch_2")])
def test_patch_squashfs(mocker, root_img, patched_img, patches):
temp_dir_mock = mocker.patch("octane.util.tempfile.temp_dir")
temp_dir_mock.return_value.__enter__ = temp_dir_mock
subprocess_mock = mocker.patch("octane.util.subprocess.call")
patch_apply_mock = mocker.patch("octane.util.patch.patch_apply")
patch_active_image._patch_squashfs(root_img, patched_img, *patches)
patch_apply_mock.assert_called_once_with(
temp_dir_mock.return_value, patches)
assert [
mock.call([
"unsquashfs", "-f", "-d", temp_dir_mock.return_value, root_img
]),
mock.call([
"mksquashfs", temp_dir_mock.return_value, patched_img
]),
] == subprocess_mock.call_args_list
@pytest.mark.parametrize("src", ["src_data"])
@pytest.mark.parametrize("dst", ["dst_data"])
@pytest.mark.parametrize("root_fs_path", ["root_fs_path"])
def test_mk_metadata(mocker, mock_open, src, dst, root_fs_path):
os_size_mock = mocker.patch("os.path.getsize")
calc_md5 = mocker.patch("octane.commands.patch_active_image.calculate_md5")
test_uuid = "123-123"
uri = "path/{uuid}/qwer"
data = {
"uuid": test_uuid,
"label": "test_label",
"modules": {
"rootfs": {
"raw_size": 123123,
"raw_md5": 123123,
"uri": uri.format(uuid=test_uuid)
}
}
}
load_mock = mocker.patch("yaml.load", return_value=data)
dump_mock = mocker.patch("yaml.dump")
uuid_mock = mocker.patch("uuid.uuid1", return_value="my_generated_uuid")
results = data.copy()
patch_active_image._mk_metadata(src, dst, root_fs_path)
results["label"] = patch_active_image.IMAGE_LABEL
results["uuid"] = uuid_mock.return_value
results["modules"]["rootfs"]["raw_size"] = os_size_mock.return_value
results["modules"]["rootfs"]["raw_md5"] = calc_md5.return_value
results["modules"]["rootfs"]["uri"] = uri.format(
uuid=uuid_mock.return_value)
dump_mock.assert_called_once_with(results, mock_open.return_value)
assert [mock.call(src), mock.call(dst, "w")] == mock_open.call_args_list
load_mock.assert_called_once_with(mock_open.return_value)
@pytest.mark.parametrize("work_dir", ["/working_dir"])
def test_patch_img(mocker, work_dir):
temp_dir_mock = mocker.patch("octane.util.tempfile.temp_dir")
temp_dir_mock.return_value.__enter__.return_value = work_dir
mock_patch_sqfs = mocker.patch(
"octane.commands.patch_active_image._patch_squashfs")
mock_patch_mk_metadata = mocker.patch(
"octane.commands.patch_active_image._mk_metadata")
mock_named_temp = mocker.patch("tempfile.NamedTemporaryFile")
mock_named_temp.return_value.__enter__.return_value = \
mock_named_temp.return_value
mock_tarfile = mocker.patch("tarfile.open")
mock_tarfile.return_value.__enter__.return_value = \
mock_tarfile.return_value
subprocess_mock = mocker.patch("octane.util.subprocess.call")
root_img = os.path.join(magic_consts.ACTIVE_IMG_PATH, "root.squashfs")
patch_file = os.path.join(magic_consts.CWD, "patches/fuel_agent/patch")
patched_img = os.path.join(work_dir, "root.squashfs")
patch_active_image.patch_img()
mock_patch_sqfs.assert_called_once_with(root_img, patched_img, patch_file)
mock_patch_mk_metadata.assert_called_once_with(
os.path.join(magic_consts.ACTIVE_IMG_PATH, "metadata.yaml"),
os.path.join(work_dir, "metadata.yaml"),
patched_img
)
mock_named_temp.assert_called_once_with()
mock_tarfile.assert_called_once_with(
name=mock_named_temp.return_value.name,
mode="w:gz")
arch_add_calls = ([
mock.call(os.path.join(work_dir, "metadata.yaml"), "metadata.yaml"),
mock.call(os.path.join(work_dir, "root.squashfs"), "root.squashfs"),
] + [
mock.call(os.path.join(magic_consts.ACTIVE_IMG_PATH, p), p)
for p in ["vmlinuz", "initrd.img"]
])
mock_tarfile.return_value.add.assert_has_calls(
arch_add_calls, any_order=True)
subprocess_mock.assert_called_once_with([
"fuel-bootstrap", "import", mock_named_temp.return_value.name
])
def test_parser(mocker, octane_app):
patch_img_mock = mocker.patch(
"octane.commands.patch_active_image.patch_img")
octane_app.run(["patch-active-img"])
patch_img_mock.assert_called_once_with()

View File

@ -41,6 +41,7 @@ octane =
fuel-repo-restore = octane.commands.restore:RestoreRepoCommand
update-bootstrap-centos = octane.commands.update_bootstrap:UpdateCentos
enable-release = octane.commands.enable_release:EnableReleaseCommand
patch-active-img = octane.commands.patch_active_image:PatchImgCommand
octane.handlers.upgrade =
controller = octane.handlers.upgrade.controller:ControllerUpgrade
compute = octane.handlers.upgrade.compute:ComputeUpgrade