diff --git a/build-tests/x86/tumbleweed/test-image-live/appliance.kiwi b/build-tests/x86/tumbleweed/test-image-live/appliance.kiwi
index f19821fb9e8..a4c84eabcc7 100644
--- a/build-tests/x86/tumbleweed/test-image-live/appliance.kiwi
+++ b/build-tests/x86/tumbleweed/test-image-live/appliance.kiwi
@@ -35,7 +35,7 @@
-
+
@@ -75,7 +75,7 @@
-
+
diff --git a/dracut/modules.d/55kiwi-live/kiwi-live-lib.sh b/dracut/modules.d/55kiwi-live/kiwi-live-lib.sh
index 43d0b8c8386..0bf70d4ff49 100755
--- a/dracut/modules.d/55kiwi-live/kiwi-live-lib.sh
+++ b/dracut/modules.d/55kiwi-live/kiwi-live-lib.sh
@@ -129,6 +129,19 @@ function udev_pending {
udevadm settle --timeout="${limit}"
}
+function set_device_lock {
+ # """
+ # Set device lock, preferrable via udevadm or via
+ # flock as fallback if systemd/udev does not provide
+ # the command
+ # """
+ if udevadm lock --help &>/dev/null;then
+ udevadm lock --device "$@"
+ else
+ flock -x "$@"
+ fi
+}
+
function initGlobalDevices {
if [ -z "$1" ]; then
die "No root device for operation given"
@@ -188,16 +201,92 @@ function mountCompressedContainerFromIso {
squashfs_container="${iso_mount_point}/${live_dir}/${squash_image}"
mkdir -m 0755 -p "${container_mount_point}"
+ if activate_verity "${squashfs_container}";then
+ squashfs_container=/dev/mapper/verityroot
+ fi
+
if activate_luks "${squashfs_container}" luks; then
squashfs_container=/dev/mapper/luks
fi
if ! mount -n "${squashfs_container}" "${container_mount_point}";then
- die "Failed to mount live ISO squashfs container"
+ die "Failed to mount live ISO compressed read-only container"
fi
echo "${container_mount_point}"
}
+function activate_verity {
+ # The method loopback mounts the given compressed root image
+ # and calls veritysetup to activate the verity device. The
+ # activatrion of the verity device can also be done with help
+ # from systemd-veritysetup which would automatically be called
+ # after losetup if the dracut module systemd-veritysetup
+ # would be included to the initrd. Unfortunately no proper
+ # veritytab file can be provided if the root image is based on
+ # squashfs because there is no support for UUIDs in squashfs
+ # which means the veritytab would have to be modified with the
+ # plain loopback device after the losetup call. Such a modification
+ # then requires to call "systemctl daemon-reload" followed by
+ # "systemctl restart systemd-veritysetup@verityroot" to make
+ # systemd-veritysetup aware of the changes in the veritytab.
+ # To avoid this complexity the verity device is activated via
+ # veritysetup directly.
+ local compressed_root=$1
+ local verity_loop
+ local verity_name
+ local data_device
+ local hash_device
+ local root_hash
+ local verity_options
+ local kiwi_verity_options
+ local veritysetup
+ local option
+ if [ -f /etc/veritytab ];then
+ read -r \
+ verity_name data_device hash_device root_hash verity_options \
+ < /etc/veritytab
+ verity_loop=$(losetup --show --find "${compressed_root}")
+ udevadm wait "${verity_loop}"
+ if [ "${data_device}" = "PLACEHOLDER" ];then
+ # squashfs does not support UUIDs, therefore the initial
+ # veritytab only contains a placeholder for the device name
+ sed -ie "s|PLACEHOLDER|${verity_loop}|g" /etc/veritytab
+ fi
+ read -r \
+ verity_name data_device hash_device root_hash verity_options \
+ < /etc/veritytab
+ if [ "$(echo "${data_device}" | cut -f1 -d=)" = "UUID" ];then
+ data_device=/dev/disk/by-uuid/$(echo "${data_device}" | cut -f2 -d=)
+ udevadm wait "${data_device}"
+ fi
+ if [ "$(echo "${hash_device}" | cut -f1 -d=)" = "UUID" ];then
+ hash_device=/dev/disk/by-uuid/$(echo "${hash_device}" | cut -f2 -d=)
+ udevadm wait "${hash_device}"
+ fi
+ # Read kernel command line verity options and merge the options
+ kiwi_verity_options=$(getarg rd.kiwi.verity_options=)
+ if [ -n "${kiwi_verity_options}" ]; then
+ if [ -n "${verity_options}" ]; then
+ verity_options="${verity_options},${kiwi_verity_options}"
+ else
+ verity_options="${kiwi_verity_options}"
+ fi
+ fi
+ veritysetup="veritysetup open "
+ veritysetup="${veritysetup} ${data_device} ${verity_name} "
+ veritysetup="${veritysetup} ${hash_device} ${root_hash}"
+ for option in $(echo "${verity_options}" | tr , " ");do
+ veritysetup="${veritysetup} --${option}"
+ done
+ eval "${veritysetup}"
+ set_device_lock "${verity_loop}" \
+ udevadm wait "/dev/mapper/verityroot"
+ return 0
+ else
+ return 1
+ fi
+}
+
function activate_luks {
local rootfs_image=$1
local mapname=$2
@@ -220,7 +309,7 @@ function mountReadOnlyRootImageFromContainer {
local container_mount_point=$1
local overlay_base
overlay_base=$(getOverlayBaseDirectory)
- local rootfs_image="${container_mount_point}/LiveOS/rootfs.img"
+ local rootfs_image="${container_mount_point}/${live_dir}/rootfs.img"
local root_mount_point="${overlay_base}/rootfsbase"
mkdir -m 0755 -p "${root_mount_point}"
diff --git a/dracut/modules.d/55kiwi-live/module-setup.sh b/dracut/modules.d/55kiwi-live/module-setup.sh
index 8f546d33448..70ad6cca2f6 100755
--- a/dracut/modules.d/55kiwi-live/module-setup.sh
+++ b/dracut/modules.d/55kiwi-live/module-setup.sh
@@ -29,7 +29,8 @@ install() {
inst_multiple \
umount dmsetup partx blkid lsblk dd losetup \
grep cut partprobe find wc fdisk tail mkfs.ext4 mkfs.xfs \
- dialog cat mountpoint curl dolly dd cryptsetup
+ dialog cat mountpoint curl dolly dd cryptsetup veritysetup \
+ flock udevadm sed
dmsquashdir=$(find "${dracutbasedir}/modules.d" -name "*dmsquash-live")
if [ -n "${dmsquashdir}" ] && \
diff --git a/kiwi/builder/live.py b/kiwi/builder/live.py
index e8a81c36fb8..31a00ed7f36 100644
--- a/kiwi/builder/live.py
+++ b/kiwi/builder/live.py
@@ -18,12 +18,20 @@
from contextlib import ExitStack
import os
import logging
-from typing import Dict
+from typing import (
+ Dict, List, Union
+)
import shutil
# project
+import kiwi.defaults as defaults
+
+from kiwi.utils.veritysetup import VeritySetup
+from kiwi.utils.block import BlockID
from kiwi.utils.temporary import Temporary
from kiwi.bootloader.config import create_boot_loader_config
+from kiwi.bootloader.config.grub2 import BootLoaderConfigGrub2
+from kiwi.bootloader.config.systemd_boot import BootLoaderSystemdBoot
from kiwi.bootloader.config.base import BootLoaderConfigBase
from kiwi.filesystem import FileSystem
from kiwi.filesystem.isofs import FileSystemIsoFs
@@ -67,6 +75,8 @@ def __init__(
self.bootloader = xml_state.get_build_type_bootloader_name()
if self.bootloader != 'systemd_boot':
self.bootloader = 'grub2'
+ self.root_filesystem_verity_blocks = \
+ xml_state.build_type.get_verity_blocks()
self.arch = Defaults.get_platform_name()
self.root_dir = root_dir
self.target_dir = target_dir
@@ -76,7 +86,8 @@ def __init__(
Defaults.get_volume_id()
self.mbrid = SystemIdentifier()
self.mbrid.calculate_id()
- self.application_id = self.xml_state.build_type.get_application_id() or \
+ self.application_id = \
+ self.xml_state.build_type.get_application_id() or \
self.mbrid.get_id()
self.publisher = xml_state.build_type.get_publisher() or \
Defaults.get_publisher()
@@ -92,7 +103,8 @@ def __init__(
self.boot_image = BootImageDracut(
xml_state,
- f'{root_dir}/boot' if self.bootloader == 'systemd_boot' else target_dir,
+ f'{root_dir}/boot'
+ if self.bootloader == 'systemd_boot' else target_dir,
self.root_dir
)
self.firmware = FirmWare(
@@ -113,6 +125,19 @@ def __init__(
)
self.result = Result(xml_state)
self.runtime_config = RuntimeConfig()
+ self.custom_iso_args = {
+ 'meta_data': {
+ 'publisher': self.publisher,
+ 'preparer': Defaults.get_preparer(),
+ 'volume_id': self.volume_id,
+ 'mbr_id': self.mbrid.get_id(),
+ 'application_id': self.application_id,
+ 'efi_mode': self.firmware.efi_mode(),
+ 'efi_partition_table': self.firmware.get_partition_table_type(),
+ 'gpt_hybrid_mbr': self.firmware.gpt_hybrid_mbr,
+ 'legacy_bios_mode': self.firmware.legacy_bios_mode()
+ }
+ }
def create(self) -> Result:
"""
@@ -143,20 +168,6 @@ def create(self) -> Result:
log.info('--> Application id: {0}'.format(self.application_id))
log.info('--> Publisher: {0}'.format(self.publisher))
log.info('--> Volume id: {0}'.format(self.volume_id))
- custom_iso_args = {
- 'meta_data': {
- 'publisher': self.publisher,
- 'preparer': Defaults.get_preparer(),
- 'volume_id': self.volume_id,
- 'mbr_id': self.mbrid.get_id(),
- 'application_id': self.application_id,
- 'efi_mode': self.firmware.efi_mode(),
- 'efi_partition_table': self.firmware.get_partition_table_type(),
- 'gpt_hybrid_mbr': self.firmware.gpt_hybrid_mbr,
- 'legacy_bios_mode': self.firmware.legacy_bios_mode()
- }
- }
-
log.info(
'Setting up live image bootloader configuration'
)
@@ -187,47 +198,10 @@ def create(self) -> Result:
self.boot_image.prepare()
# create dracut initrd for live image
- log.info('Creating live ISO boot image')
- live_dracut_modules = Defaults.get_live_dracut_modules_from_flag(
- self.live_type
- )
- live_dracut_modules.append('pollcdrom')
- for dracut_module in live_dracut_modules:
- self.boot_image.include_module(dracut_module)
- self.boot_image.omit_module('multipath')
- self.boot_image.write_system_config_file(
- config={
- 'modules': live_dracut_modules,
- 'omit_modules': ['multipath']
- },
- config_file=self.root_dir + '/etc/dracut.conf.d/02-livecd.conf'
+ log.info('Creating live ISO boot image(s)')
+ self.create_live_iso_boot_images(
+ bootloader_config, modules=['pollcdrom']
)
- self.boot_image.create_initrd(self.mbrid)
- # Clean up leftover dracut config file (which can break installs)
- os.unlink(self.root_dir + '/etc/dracut.conf.d/02-livecd.conf')
- if self.bootloader == 'systemd_boot':
- # make sure the initrd name follows the dracut
- # naming conventions
- boot_names = self.boot_image.get_boot_names()
- if self.boot_image.initrd_filename:
- Command.run(
- [
- 'mv', self.boot_image.initrd_filename,
- self.root_dir + ''.join(
- ['/boot/', boot_names.initrd_name]
- )
- ]
- )
-
- # create EFI FAT image
- if self.firmware.efi_mode():
- efi_loader = Temporary(
- prefix='efi-loader.', path=self.target_dir
- ).new_file()
- bootloader_config._create_embedded_fat_efi_image(
- efi_loader.name
- )
- custom_iso_args['meta_data']['efi_loader'] = efi_loader.name
# setup kernel file(s) and initrd in ISO boot layout
if self.bootloader != 'systemd_boot':
@@ -239,7 +213,7 @@ def create(self) -> Result:
# calculate size and decide if we need UDF
if rootsize.accumulate_mbyte_file_sizes() > 4096:
log.info('ISO exceeds 4G size, using UDF filesystem')
- custom_iso_args['meta_data']['udf'] = True
+ self.custom_iso_args['meta_data']['udf'] = True
# pack system into live boot structure as expected by dracut
log.info(
@@ -287,7 +261,8 @@ def create(self) -> Result:
root_dir=self.root_dir
)
- device_provider = luks_provider.get_device() or DeviceProvider() \
+ device_provider = \
+ luks_provider.get_device() or DeviceProvider() \
if luks_provider else loop_provider
live_filesystem = FileSystem.new(
name=root_filesystem,
@@ -312,7 +287,8 @@ def create(self) -> Result:
).new_dir()
Path.create(self.live_container_dir.name + '/LiveOS')
shutil.copy(
- root_image.name, self.live_container_dir.name + '/LiveOS/rootfs.img'
+ root_image.name,
+ self.live_container_dir.name + '/LiveOS/rootfs.img'
)
with FileSystem.new(
name='squashfs',
@@ -327,6 +303,14 @@ def create(self) -> Result:
live_container_image.create_on_file(
container_image.name
)
+ if self.root_filesystem_verity_blocks:
+ live_container_image.create_verity_layer(
+ self.root_filesystem_verity_blocks if
+ self.root_filesystem_verity_blocks != 'all' else None
+ )
+ self._write_veritytab_to_boot_image(
+ container_image.name, live_container_image.veritysetup
+ )
Path.create(self.media_dir.name + '/LiveOS')
os.chmod(container_image.name, 0o644)
shutil.copy(
@@ -359,6 +343,16 @@ def create(self) -> Result:
get_exclude_list_for_root_data_sync() + Defaults.
get_exclude_list_from_custom_exclude_files(self.root_dir)
)
+
+ if self.root_filesystem_verity_blocks:
+ live_container_image.create_verity_layer(
+ self.root_filesystem_verity_blocks if
+ self.root_filesystem_verity_blocks != 'all' else None
+ )
+ self._write_veritytab_to_boot_image(
+ container_image.name, live_container_image.veritysetup
+ )
+
luks_image = None
if self.luks is not None:
# dump root filesystem blob on top of a LUKS blob
@@ -406,11 +400,20 @@ def create(self) -> Result:
self.media_dir.name + '/LiveOS/squashfs.img'
)
+ if self.root_filesystem_verity_blocks:
+ log.info('Rebuild live ISO boot image(s) to include veritysetup')
+ with self._create_bootloader_instance() as bootloader_config:
+ self.create_live_iso_boot_images(
+ bootloader_config, modules=['pollcdrom']
+ )
+ if self.bootloader != 'systemd_boot':
+ self._setup_live_iso_kernel_and_initrd()
+
# create iso filesystem from media_dir
log.info('Creating live ISO image')
with FileSystemIsoFs(
device_provider=DeviceProvider(), root_dir=self.media_dir.name,
- custom_args=custom_iso_args
+ custom_args=self.custom_iso_args
) as iso_image:
iso_image.create_on_file(self.isoname)
@@ -460,6 +463,84 @@ def create(self) -> Result:
)
return self.result
+ def create_live_iso_boot_images(
+ self,
+ bootloader_config: Union[BootLoaderConfigGrub2, BootLoaderSystemdBoot],
+ modules: List[str] = []
+ ) -> None:
+ live_dracut_modules = Defaults.get_live_dracut_modules_from_flag(
+ self.live_type
+ ) + modules
+ for dracut_module in live_dracut_modules:
+ self.boot_image.include_module(dracut_module)
+ self.boot_image.omit_module('multipath')
+ self.boot_image.write_system_config_file(
+ config={
+ 'modules': live_dracut_modules,
+ 'omit_modules': ['multipath']
+ },
+ config_file=self.root_dir + '/etc/dracut.conf.d/02-livecd.conf'
+ )
+ self.boot_image.create_initrd(self.mbrid)
+ # Clean up leftover dracut config file (which can break installs)
+ os.unlink(self.root_dir + '/etc/dracut.conf.d/02-livecd.conf')
+ if self.bootloader == 'systemd_boot':
+ # make sure the initrd name follows the dracut
+ # naming conventions
+ boot_names = self.boot_image.get_boot_names()
+ if self.boot_image.initrd_filename:
+ Command.run(
+ [
+ 'mv', self.boot_image.initrd_filename,
+ self.root_dir + ''.join(
+ ['/boot/', boot_names.initrd_name]
+ )
+ ]
+ )
+
+ # create EFI FAT image
+ if self.firmware.efi_mode():
+ self.efi_loader = Temporary(
+ prefix='efi-loader.', path=self.target_dir
+ ).new_file()
+ bootloader_config._create_embedded_fat_efi_image(
+ self.efi_loader.name
+ )
+ self.custom_iso_args['meta_data']['efi_loader'] = \
+ self.efi_loader.name
+
+ def _write_veritytab_to_boot_image(
+ self, container_image_name: str, veritysetup: VeritySetup
+ ) -> None:
+ log.info('Creating generic boot image etc/veritytab')
+ veritytab_filename = ''.join([self.root_dir, '/etc/veritytab'])
+ block_operation = BlockID(container_image_name)
+ filesystem = block_operation.get_filesystem()
+ if filesystem == 'squashfs':
+ # squashfs does not provide a label or uuid
+ # The real device name is not known at this point, and
+ # will be replaced in the initrd code with a device name
+ # based on the loopsetup result of the compressed image file
+ device_id = 'PLACEHOLDER'
+ else:
+ device_id = 'UUID={}'.format(
+ block_operation.get_blkid('UUID')
+ )
+ with open(veritytab_filename, 'w') as veritytab:
+ veritytab.write(
+ 'verityroot {0} {0} {1} {2},{3}{4}'.format(
+ device_id, veritysetup.verity_dict.get('Roothash'),
+ f'hash-offset={veritysetup.verity_hash_offset}',
+ f'hash-block-size={defaults.VERITY_HASH_BLOCKSIZE}',
+ os.linesep
+ )
+ )
+ self.boot_image.include_file(
+ filename=os.sep + os.sep.join(
+ ['etc', os.path.basename(veritytab_filename)]
+ ), delete_after_include=True
+ )
+
def _create_bootloader_instance(self) -> BootLoaderConfigBase:
return create_boot_loader_config(
name=self.bootloader, xml_state=self.xml_state,
diff --git a/kiwi/schema/kiwi.rnc b/kiwi/schema/kiwi.rnc
index 9a65b598b7a..6119779e57e 100644
--- a/kiwi/schema/kiwi.rnc
+++ b/kiwi/schema/kiwi.rnc
@@ -1800,7 +1800,7 @@ div {
attribute verity_blocks { blocks-type }
>> sch:pattern [ id = "verity_blocks" is-a = "image_type"
sch:param [ name = "attr" value = "verity_blocks" ]
- sch:param [ name = "types" value = "oem" ]
+ sch:param [ name = "types" value = "oem iso" ]
]
k.type.embed_verity_metadata.attribute =
## In combination with the verity_blocks attribute, embed
diff --git a/kiwi/schema/kiwi.rng b/kiwi/schema/kiwi.rng
index 35f5360bdc8..ebbf86544e3 100644
--- a/kiwi/schema/kiwi.rng
+++ b/kiwi/schema/kiwi.rng
@@ -2675,7 +2675,7 @@ file the value 'all' can be specified.
-
+
diff --git a/test/unit/builder/live_test.py b/test/unit/builder/live_test.py
index d9995c43076..42e8b289e00 100644
--- a/test/unit/builder/live_test.py
+++ b/test/unit/builder/live_test.py
@@ -18,6 +18,9 @@ def setup(self):
Defaults.set_platform_name('x86_64')
self.firmware = Mock()
+ self.firmware.bios_mode.return_value = False
+ self.firmware.get_partition_table_type.return_value = 'gpt'
+ self.firmware.gpt_hybrid_mbr = False
self.firmware.legacy_bios_mode = Mock(
return_value=True
)
@@ -74,6 +77,9 @@ def setup(self):
)
self.xml_state = Mock()
+ self.xml_state.build_type.get_verity_blocks = Mock(
+ return_value=None
+ )
self.xml_state.get_fs_mount_option_list = Mock(
return_value=['async']
)
@@ -181,6 +187,94 @@ def test_create_overlay_structure_boot_on_systemd_boot(
['mv', 'kiwi_used_initrd_name', 'root_dir/boot/dracut_initrd_name']
)
+ @mark.parametrize('xml_filesystem', [None, 'squashfs', 'erofs'])
+ @patch('kiwi.builder.live.create_boot_loader_config')
+ @patch('kiwi.builder.live.LoopDevice')
+ @patch('kiwi.builder.live.DeviceProvider')
+ @patch('kiwi.builder.live.IsoToolsBase.setup_media_loader_directory')
+ @patch('kiwi.builder.live.Temporary')
+ @patch('kiwi.builder.live.shutil')
+ @patch('kiwi.builder.live.Iso.set_media_tag')
+ @patch('kiwi.builder.live.Iso')
+ @patch('kiwi.builder.live.FileSystemIsoFs')
+ @patch('kiwi.builder.live.FileSystem.new')
+ @patch('kiwi.builder.live.SystemSize')
+ @patch('kiwi.builder.live.Defaults.get_grub_boot_directory_name')
+ @patch('os.unlink')
+ @patch('os.path.exists')
+ @patch('os.chmod')
+ @patch('kiwi.builder.live.BlockID')
+ def test_create_overlay_structure_boot_verity_baked(
+ self,
+ mock_BlockID,
+ mock_chmod,
+ mock_exists,
+ mock_unlink,
+ mock_grub_dir,
+ mock_size,
+ mock_filesystem,
+ mock_isofs,
+ mock_Iso,
+ mock_tag,
+ mock_shutil,
+ mock_Temporary,
+ mock_setup_media_loader_directory,
+ mock_DeviceProvider,
+ mock_LoopDevice,
+ mock_create_boot_loader_config,
+ xml_filesystem
+ ):
+ if not xml_filesystem or xml_filesystem == 'squashfs':
+ mock_BlockID.return_value.get_filesystem.return_value = 'squashfs'
+ if xml_filesystem and xml_filesystem == 'erofs':
+ mock_BlockID.return_value.get_filesystem.return_value = 'erofs'
+ bootloader_config = Mock()
+ mock_create_boot_loader_config.return_value.__enter__.return_value = \
+ bootloader_config
+ loop_provider = Mock()
+ mock_LoopDevice.return_value.__enter__.return_value = loop_provider
+ mock_exists.return_value = True
+ mock_unlink.return_value = True
+ mock_grub_dir.return_value = 'grub2'
+ temp_squashfs = Mock()
+ temp_squashfs.name = 'temp-squashfs'
+ temp_media_dir = Mock()
+ temp_media_dir.name = 'temp_media_dir'
+ tmpdir_name = [temp_squashfs, temp_media_dir]
+
+ def side_effect():
+ return tmpdir_name.pop()
+
+ mock_Temporary.return_value.new_dir.side_effect = side_effect
+ mock_Temporary.return_value.new_file.return_value.name = 'kiwi-tmpfile'
+ self.live_image.live_type = 'overlay'
+ self.live_image.root_filesystem_verity_blocks = 'all'
+ self.xml_state.build_type.get_filesystem = Mock(
+ return_value=xml_filesystem
+ )
+ iso_image = Mock()
+ iso_image.create_on_file.return_value = 'offset'
+ mock_isofs.return_value.__enter__.return_value = iso_image
+ rootsize = Mock()
+ rootsize.accumulate_mbyte_file_sizes = Mock(
+ return_value=8192
+ )
+ mock_size.return_value = rootsize
+ self.setup.export_package_changes.return_value = '.changes'
+ self.setup.export_package_verification.return_value = '.verified'
+ self.setup.export_package_list.return_value = '.packages'
+
+ with patch('builtins.open', create=True):
+ self.live_image.create()
+
+ if xml_filesystem is None:
+ mock_filesystem.return_value.__enter__.return_value.\
+ create_verity_layer.assert_called_once_with(None)
+
+ if xml_filesystem == 'squashfs':
+ mock_filesystem.return_value.\
+ create_verity_layer.assert_called_once_with(None)
+
@mark.parametrize('xml_filesystem', ['xfs'])
@patch('kiwi.builder.live.create_boot_loader_config')
@patch('kiwi.builder.live.LoopDevice')
@@ -395,9 +489,6 @@ def side_effect():
self.setup.export_package_verification.return_value = '.verified'
self.setup.export_package_list.return_value = '.packages'
- self.firmware.bios_mode.return_value = False
- self.firmware.get_partition_table_type.return_value = 'gpt'
- self.firmware.gpt_hybrid_mbr = False
self.live_image.create()
self.setup.import_cdroot_files.assert_called_once_with('temp_media_dir')