kiwi-el8/kiwi/system_setup.py
Thomas Schraitle 45bc5a03a7 Refactor into subpackage to fix #23
Codecoverage are 100% and tests are green

Changes:
* Refactor archive_*.py -> archive subpackage
* Refactor partitioner_*.py -> partitioner subpackage
* Refactor package_manager_*.py -> package_manager/ subpackage
* Refactor bootloader_config*.py -> bootloader/config/ subpackage
* Refactor bootloader_template*.py -> bootloader/template/ subpackage
* Refactor bootloader_install*.py -> bootloader/install/ subpackage
* Refactor repository*.py -> repository/ subpackage
* Refactor filesystem*.py -> filesystem/ subpackage
* Refactor dist_*.py -> dist/dformat subpackage
  The name `dformat` as package name is needed to avoid any name
  conflicts with the built-in function `format`.
* Refactor volume_manager*.py -> volume_manager/ subpackage
* Refactor boot_image*.py -> boot/image/ subpackage
2016-02-27 10:31:31 +01:00

593 lines
22 KiB
Python

# 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/>
#
import os
import platform
from collections import OrderedDict
from tempfile import NamedTemporaryFile
# project
from .command import Command
from .command_process import CommandProcess
from .logger import log
from .defaults import Defaults
from .users import Users
from .shell import Shell
from .path import Path
from .archive.tar import ArchiveTar
from .compress import Compress
from .exceptions import (
KiwiScriptFailed
)
class SystemSetup(object):
"""
Implementation of system setup steps supported by kiwi.
kiwi is not responsible for the system configuration, however
some setup steps needs to be performed in order to provide
a minimal work environment inside of the image according to
the desired image type.
"""
def __init__(self, xml_state, description_dir, root_dir):
self.xml_state = xml_state
self.description_dir = description_dir
self.root_dir = root_dir
self.__preferences_lookup()
self.__oemconfig_lookup()
def import_description(self):
"""
import XML descriptions, custom scripts and script helper methods
"""
log.info('Importing Image description to system tree')
description = self.root_dir + '/image/config.xml'
log.info('--> Importing state XML description as image/config.xml')
Path.create(self.root_dir + '/image')
with open(description, 'w') as config:
config.write('<?xml version="1.0" encoding="utf-8"?>')
self.xml_state.xml_data.export(outfile=config, level=0)
need_script_helper_functions = False
config_script = self.description_dir + '/config.sh'
image_script = self.description_dir + '/images.sh'
bootloader_scripts = {
'edit_boot_config.sh':
self.xml_state.build_type.get_editbootconfig(),
'edit_boot_install.sh':
self.xml_state.build_type.get_editbootinstall()
}
sorted_bootloader_scripts = OrderedDict(
sorted(bootloader_scripts.items())
)
script_target = self.root_dir + '/image/'
for name, bootloader_script in list(sorted_bootloader_scripts.items()):
if bootloader_script:
script_file = self.description_dir + '/' + bootloader_script
if os.path.exists(script_file):
log.info(
'--> Importing %s script as %s',
bootloader_script, 'image/' + name
)
Command.run(
[
'cp', script_file, script_target + name
]
)
else:
raise KiwiScriptFailed(
'Specified script %s does not exist' % script_file
)
if os.path.exists(config_script):
log.info('--> Importing config script as image/config.sh')
Command.run(['cp', config_script, script_target])
need_script_helper_functions = True
if os.path.exists(image_script):
log.info('--> Importing image script as image/images.sh')
Command.run(['cp', image_script, script_target])
need_script_helper_functions = True
if need_script_helper_functions:
script_functions = Defaults.get_common_functions_file()
script_functions_target = self.root_dir + '/.kconfig'
log.info('--> Importing script helper functions')
Command.run([
'cp', script_functions, script_functions_target
])
def cleanup(self):
"""
delete all traces of a kiwi description which are not
required in the later image
"""
Command.run(['rm', '-r', '-f', '/.kconfig', '/image'])
def import_shell_environment(self, profile):
"""
create profile environment to let scripts consume
information from the XML description.
"""
profile_file = self.root_dir + '/.profile'
log.info('Creating .profile environment')
profile_environment = profile.create()
with open(profile_file, 'w') as profile:
for line in profile_environment:
profile.write(line + '\n')
log.debug('--> %s', line)
def import_overlay_files(
self, follow_links=False, preserve_owner_group=False
):
"""
copy overlay files from the image description to
the image root tree. Supported are a root/ directory
or a root.tar.gz tarball. The root/ directory takes
precedence over the tarball
"""
overlay_directory = self.description_dir + '/root/'
overlay_archive = self.description_dir + '/root.tar.gz'
if os.path.exists(overlay_directory):
log.info('Copying user defined files to image tree')
rsync_options = [
'-r', '-p', '-t', '-D', '-H', '-X', '-A', '--one-file-system'
]
if follow_links:
rsync_options.append('--copy-links')
else:
rsync_options.append('--links')
if preserve_owner_group:
rsync_options.append('-o')
rsync_options.append('-g')
Command.run(
['rsync'] + rsync_options + [
overlay_directory, self.root_dir
]
)
elif os.path.exists(overlay_archive):
log.info('Extracting user defined files from archive to image tree')
archive = ArchiveTar(overlay_archive)
archive.extract(self.root_dir)
def setup_hardware_clock(self):
if 'hwclock' in self.preferences:
log.info(
'Setting up hardware clock: %s', self.preferences['hwclock']
)
Command.run([
'chroot', self.root_dir,
'hwclock', '--adjust', '--' + self.preferences['hwclock']
])
def setup_keyboard_map(self):
if 'keytable' in self.preferences:
keyboard_config = self.root_dir + '/etc/sysconfig/keyboard'
if os.path.exists(keyboard_config):
log.info(
'Setting up keytable: %s', self.preferences['keytable']
)
Shell.run_common_function(
'baseUpdateSysConfig', [
keyboard_config, 'KEYTABLE',
'"' + self.preferences['keytable'] + '"'
]
)
else:
log.warning(
'keyboard setup skipped etc/sysconfig/keyboard not found'
)
def setup_locale(self):
if 'locale' in self.preferences:
lang_config = self.root_dir + '/etc/sysconfig/language'
if os.path.exists(lang_config):
log.info(
'Setting up locale: %s', self.preferences['locale']
)
Shell.run_common_function(
'baseUpdateSysConfig', [
lang_config, 'RC_LANG',
self.preferences['locale'].split(',')[0] + '.UTF-8'
]
)
else:
log.warning(
'locale setup skipped etc/sysconfig/language not found'
)
def setup_timezone(self):
if 'timezone' in self.preferences:
log.info(
'Setting up timezone: %s', self.preferences['timezone']
)
zoneinfo = '/usr/share/zoneinfo/' + self.preferences['timezone']
Command.run([
'chroot', self.root_dir,
'ln', '-s', '-f', zoneinfo, '/etc/localtime'
])
def setup_groups(self):
"""
add groups for configured users
"""
system_users = Users(self.root_dir)
for users in self.xml_state.get_users():
if not system_users.group_exists(users.group_name):
options = []
if users.group_id:
options.append('-g')
options.append(users.group_id)
log.info('Adding group %s', users.group_name)
system_users.group_add(
users.group_name, options
)
def setup_users(self):
"""
add/modify configured users
"""
system_users = Users(self.root_dir)
for users in self.xml_state.get_users():
for user in users.user_sections:
log.info('Setting up user %s', user.get_name())
password = user.get_password()
password_format = user.get_pwdformat()
home_path = user.get_home()
user_name = user.get_name()
user_id = user.get_id()
user_realname = user.get_realname()
user_shell = user.get_shell()
user_exists = system_users.user_exists(user_name)
options = []
if password_format == 'plain':
password = self.__create_passwd_hash(password)
if password:
options.append('-p')
options.append(password)
if user_shell:
options.append('-s')
options.append(user_shell)
if users.group_id or users.group_name:
options.append('-g')
if users.group_id:
options.append(users.group_id)
else:
options.append(users.group_name)
if user_id:
options.append('-u')
options.append(user_id)
if user_realname:
options.append('-c')
options.append(user_realname)
if not user_exists and home_path:
options.append('-m')
options.append('-d')
options.append(home_path)
if user_exists:
log.info(
'--> Modifying user: %s [%s]',
user_name, users.group_name
)
system_users.user_modify(user_name, options)
else:
log.info(
'--> Adding user: %s [%s]',
user_name, users.group_name
)
system_users.user_add(user_name, options)
if home_path:
log.info(
'--> Setting permissions for %s', home_path
)
system_users.setup_home_for_user(
user_name, users.group_name, home_path
)
def import_image_identifier(self):
"""
create an /etc/ImageID identifier file
"""
image_id = self.xml_state.xml_data.get_id()
if image_id and os.path.exists(self.root_dir + '/etc'):
image_id_file = self.root_dir + '/etc/ImageID'
log.info('Creating identifier: %s as %s', image_id, image_id_file)
with open(image_id_file, 'w') as identifier:
identifier.write('%s\n' % image_id)
def export_modprobe_setup(self, target_root_dir):
"""
export etc/modprobe.d to given root_dir
"""
modprobe_config = self.root_dir + '/etc/modprobe.d'
if os.path.exists(modprobe_config):
log.info('Export modprobe configuration')
Path.create(target_root_dir + '/etc')
Command.run(
['rsync', '-zav', modprobe_config, target_root_dir + '/etc/']
)
def export_rpm_package_list(self, target_dir):
"""
export image rpm package list as metadata reference
used by the open buildservice
"""
if os.path.exists(self.root_dir + '/var/lib/rpm/Packages'):
log.info('Export rpm packages metadata')
filename = ''.join(
[
target_dir, '/',
self.xml_state.xml_data.get_name(),
'.' + platform.machine(),
'-' + self.xml_state.get_image_version(),
'.packages'
]
)
query_call = Command.run(
[
'rpm', '--root', self.root_dir, '-qa', '--qf',
'|'.join(
[
'%{NAME}', '%{EPOCH}', '%{VERSION}', '%{RELEASE}',
'%{ARCH}', '%{DISTURL}', '\\n'
]
)
]
)
with open(filename, 'w') as packages:
packages.write(query_call.output)
return filename
def export_rpm_package_verification(self, target_dir):
"""
export rpm package verification result as metadata reference
used by the open buildservice
"""
if os.path.exists(self.root_dir + '/var/lib/rpm/Packages'):
log.info('Export rpm verification metadata')
filename = ''.join(
[
target_dir, '/',
self.xml_state.xml_data.get_name(),
'.' + platform.machine(),
'-' + self.xml_state.get_image_version(),
'.verified'
]
)
query_call = Command.run(
command=['rpm', '--root', self.root_dir, '-Va'],
raise_on_error=False
)
with open(filename, 'w') as verified:
verified.write(query_call.output)
return filename
def call_config_script(self):
self.__call_script('config.sh')
def call_image_script(self):
self.__call_script('images.sh')
def call_edit_boot_config_script(self, filesystem, boot_part_id):
self.__call_script_no_chroot(
'edit_boot_config.sh', [filesystem, format(boot_part_id)]
)
def call_edit_boot_install_script(self, diskname, boot_device_node):
self.__call_script_no_chroot(
'edit_boot_install.sh', [diskname, boot_device_node]
)
def create_init_link_from_linuxrc(self):
"""
kiwi boot images provides the linuxrc script, however the kernel
also expects an init executable to be present. This method creates
a hard link to the linuxrc file
"""
Command.run(
['ln', self.root_dir + '/linuxrc', self.root_dir + '/init']
)
def create_recovery_archive(self):
"""
create a compressed recovery archive from the root tree
for use with kiwi's recvoery system. The method creates
additional data into the image root filesystem which is
deleted prior to the creation of a new recovery data set
"""
# cleanup
bash_comand = [
'rm', '-f', self.root_dir + '/recovery.*'
]
Command.run(['bash', '-c', ' '.join(bash_comand)])
if not self.oemconfig['recovery']:
return
# recovery.tar
log.info('Creating recovery tar archive')
metadata = {
'archive_name':
self.root_dir + '/recovery.tar',
'archive_filecount':
self.root_dir + '/recovery.tar.files',
'archive_size':
self.root_dir + '/recovery.tar.size',
'partition_size':
self.root_dir + '/recovery.partition.size',
'partition_filesystem':
self.root_dir + '/recovery.tar.filesystem'
}
recovery_archive = NamedTemporaryFile(
delete=False
)
archive = ArchiveTar(
filename=recovery_archive.name,
create_from_file_list=False
)
archive.create(
source_dir=self.root_dir,
exclude=['dev', 'proc', 'sys'],
options=[
'--numeric-owner',
'--hard-dereference',
'--preserve-permissions'
]
)
Command.run(
['mv', recovery_archive.name, metadata['archive_name']]
)
# recovery.tar.filesystem
recovery_filesystem = self.xml_state.build_type.get_filesystem()
with open(metadata['partition_filesystem'], 'w') as partfs:
partfs.write('%s' % recovery_filesystem)
log.info(
'--> Recovery partition filesystem: %s', recovery_filesystem
)
# recovery.tar.files
bash_comand = [
'tar', '-tf', metadata['archive_name'], '|', 'wc', '-l'
]
tar_files_call = Command.run(
['bash', '-c', ' '.join(bash_comand)]
)
tar_files_count = int(tar_files_call.output.rstrip('\n'))
with open(metadata['archive_filecount'], 'w') as files:
files.write('%d\n' % tar_files_count)
log.info(
'--> Recovery file count: %d files', tar_files_count
)
# recovery.tar.size
recovery_archive_size_bytes = os.path.getsize(metadata['archive_name'])
with open(metadata['archive_size'], 'w') as size:
size.write('%d' % recovery_archive_size_bytes)
log.info(
'--> Recovery uncompressed size: %d mbytes',
int(recovery_archive_size_bytes / 1048576)
)
# recovery.tar.gz
log.info('--> Compressing recovery archive')
compress = Compress(self.root_dir + '/recovery.tar')
compress.gzip()
# recovery.partition.size
recovery_archive_gz_size_mbytes = int(
os.path.getsize(metadata['archive_name'] + '.gz') / 1048576
)
recovery_partition_mbytes = recovery_archive_gz_size_mbytes \
+ Defaults.get_recovery_spare_mbytes()
with open(metadata['partition_size'], 'w') as gzsize:
gzsize.write('%d' % recovery_partition_mbytes)
log.info(
'--> Recovery partition size: %d mbytes',
recovery_partition_mbytes
)
# delete recovery archive if inplace recovery is requested
# In this mode the recovery archive is created at install time
# and not at image creation time. However the recovery metadata
# is preserved in order to be able to check if enough space
# is available on the disk to create the recovery archive.
if self.oemconfig['recovery_inplace']:
log.info(
'--> Inplace recovery requested, deleting archive'
)
Path.wipe(metadata['archive_name'] + '.gz')
def __call_script(self, name):
if os.path.exists(self.root_dir + '/image/' + name):
config_script = Command.call(
['chroot', self.root_dir, 'bash', '/image/' + name]
)
process = CommandProcess(
command=config_script, log_topic='Calling ' + name + ' script'
)
result = process.poll_and_watch()
if result.returncode != 0:
raise KiwiScriptFailed(
'%s failed: %s' % (name, format(result.stderr))
)
def __call_script_no_chroot(self, name, option_list):
if os.path.exists(self.root_dir + '/image/' + name):
bash_command = [
'cd', self.root_dir, '&&',
'bash', '--norc', 'image/' + name, ' '.join(option_list)
]
config_script = Command.call(
['bash', '-c', ' '.join(bash_command)]
)
process = CommandProcess(
command=config_script, log_topic='Calling ' + name + ' script'
)
result = process.poll_and_watch()
if result.returncode != 0:
raise KiwiScriptFailed(
'%s failed: %s' % (name, format(result.stderr))
)
def __create_passwd_hash(self, password):
openssl = Command.run(
['openssl', 'passwd', '-1', '-salt', 'xyz', password]
)
return openssl.output
def __preferences_lookup(self):
self.preferences = {}
for preferences in self.xml_state.get_preferences_sections():
timezone_section = preferences.get_timezone()
locale_section = preferences.get_locale()
hwclock_section = preferences.get_hwclock()
keytable_section = preferences.get_keytable()
if timezone_section:
self.preferences['timezone'] = timezone_section[0]
if locale_section:
self.preferences['locale'] = locale_section[0]
if hwclock_section:
self.preferences['hwclock'] = hwclock_section[0]
if keytable_section:
self.preferences['keytable'] = keytable_section[0]
def __oemconfig_lookup(self):
self.oemconfig = {
'recovery_inplace': False,
'recovery': False
}
oemconfig = self.xml_state.get_build_type_oemconfig_section()
if oemconfig:
self.oemconfig['recovery'] = \
self.__text(oemconfig.get_oem_recovery())
self.oemconfig['recovery_inplace'] = \
self.__text(oemconfig.get_oem_inplace_recovery())
def __text(self, section_content):
"""
helper method to return the text for XML elements of the
following structure: <section>text</section>.
"""
if section_content:
content = section_content[0]
if content == 'false':
return False
else:
return content