# Copyright (c) 2013-2016 SUSE LLC # # This program is free software; you can redistribute it and/or # modify it under the terms of version 3 of the GNU General Public License as # published by the Free Software Foundation. # # This program 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 this program; if not, contact SUSE LLC. # # To contact SUSE about this file by physical or electronic mail, # you may find current contact information at www.suse.com class Machinery::KiwiConfig < Machinery::Exporter attr_accessor :xml_text, :sh attr_accessor :name def initialize(system_description, options = {}) @name = "kiwi" @system_description = system_description @options = options @system_description.assert_scopes( "repositories", "packages", "os" ) check_exported_os check_existance_of_extracted_files check_repositories generate_config end def write(output_location) inject_users_and_groups(output_location) inject_extracted_files(output_location) @sh << "baseCleanMount\n" @sh << "exit 0\n" File.write(File.join(output_location, "config.xml"), xml_text) File.write(File.join(output_location, "config.sh"), @sh) FileUtils.cp( File.join(Machinery::ROOT, "export_helpers/kiwi_export_readme.md"), File.join(output_location, "README.md") ) post_process_config(output_location) end def export_name "#{@system_description.name}-kiwi" end private def repos_with_credentials?(repo) repo.username && repo.password end def optional_bootstrap_packages [ "glibc-locale", "module-init-tools", "cracklib-dict-full", "ca-certificates", "ca-certificates-mozilla" ] end def add_bootstrap_packages(xml) xml.packages(type: "bootstrap") do xml.package(name: "filesystem") bootstrap_packages = @system_description.packages.select { |package| optional_bootstrap_packages.include?(package.name) } bootstrap_packages.each do |package| xml.package(name: package.name) end end end def pre_process_config enable_ssh if @options[:enable_ssh] end def post_process_config(output_location) enable_dhcp(output_location) if @options[:enable_dhcp] end def inject_users_and_groups(output_location) return if !@system_description.users || !@system_description.groups merge_script_name = "merge_users_and_groups.pl" template = ERB.new( File.read(File.join(Machinery::ROOT, "export_helpers", "#{merge_script_name}.erb")) ) passwd_entries = @system_description.users.map do |u| passwd = [u.name, u.password, u.uid, u.gid, u.comment, u.home, u.shell].join(":") # The shadow file contains an eigth reserved field at the end, so we have # to manually add it, too. shadow = [u.name, u.encrypted_password, u.last_changed_date, u.min_days, u.max_days, u.warn_days, u.disable_days, u.disabled_date, ""].join(":") "['#{passwd}', '#{shadow}']" end.join(",\n") group_entries = @system_description.groups.map do |g| "'#{g.name}:#{g.password}:#{g.gid}:#{g.users.join(",")}'" end.join(",\n") FileUtils.mkdir_p(File.join(output_location, "root", "tmp"), mode: 01777) script_path = File.join(output_location, "root", "tmp", merge_script_name) File.write(script_path, template.result(binding)) @sh << "perl /tmp/#{merge_script_name} /etc/passwd /etc/shadow /etc/group\n" @sh << "rm /tmp/#{merge_script_name}\n" end def inject_extracted_files(output_location) ["changed_managed_files", "changed_config_files"].each do |scope| next unless @system_description.scope_extracted?(scope) output_root_path = File.join(output_location, "root") FileUtils.mkdir_p(output_root_path) @system_description[scope].each do |file| if file.deleted? @sh << "rm -rf '#{quote(file.name)}'\n" elsif file.directory? @sh << <<EOF chmod #{file.mode} '#{quote(file.name)}' chown #{file.user}:#{file.group} '#{quote(file.name)}' EOF elsif file.file? @system_description[scope].write_file(file, output_root_path) @sh << <<EOF chmod #{file.mode} '#{quote(file.name)}' chown #{file.user}:#{file.group} '#{quote(file.name)}' EOF elsif file.link? @sh << <<EOF rm -rf '#{quote(file.name)}' ln -s '#{quote(file.target)}' '#{quote(file.name)}' chown --no-dereference #{file.user}:#{file.group} '#{quote(file.name)}' EOF end end end if @system_description.scope_extracted?("unmanaged_files") destination = File.join(output_location, "root", "tmp", "unmanaged_files") FileUtils.mkdir_p(destination, mode: 01777) filter = "unmanaged_files_#{@name}_excludes" @system_description.unmanaged_files.export_files_as_tarballs(destination) FileUtils.cp( File.join(Machinery::ROOT, "export_helpers/#{filter}"), File.join(output_location, "root", "tmp") ) @sh << "# Apply the extracted unmanaged files\n" @sh << "find /tmp/unmanaged_files -name *.tgz -exec " \ "tar -C / -X '/tmp/#{filter}' -xf {} \\;\n" @sh << "rm -rf '/tmp/unmanaged_files' '/tmp/#{filter}'\n" end end def check_existance_of_extracted_files missing_scopes = [] ["changed_config_files", "changed_managed_files", "unmanaged_files"].each do |scope| if @system_description[scope] && !@system_description.scope_file_store(scope).path missing_scopes << scope end end unless missing_scopes.empty? raise Machinery::Errors::MissingExtractedFiles.new(@system_description, missing_scopes) end end def check_repositories if @system_description.repositories.empty? raise( Machinery::Errors::MissingRequirement.new( "The scope 'repositories' of the system description doesn't contain a" \ " repository, which is necesarry for kiwi." \ " Please make sure that there is at least one accessible repository" \ " with all the required packages." ) ) end end def check_exported_os unless @system_description.os.is_a?(Machinery::OsSuse) raise Machinery::Errors::ExportFailed.new( "Export is not possible because the operating system " \ "'#{@system_description.os.display_name}' is not supported." ) end end def generate_config @sh = <<EOF test -f /.kconfig && . /.kconfig test -f /.profile && . /.profile baseMount suseSetupProduct suseImportBuildKey suseConfig EOF xml = Builder::XmlMarkup.new(indent: 2) xml.instruct! :xml xml.image(schemaversion: "5.8", name: @system_description.name) do xml.description(type: "system") do xml.author "Machinery" xml.contact "" xml.specification "Description of system '#{@system_description.name}' exported by Machinery" end xml.preferences do xml.packagemanager "zypper" xml.version "0.0.1" xml.type( image: "vmx", filesystem: "ext3", format: "qcow2" ) end xml.users(group: "root") do xml.user(password: "$1$wYJUgpM5$RXMMeASDc035eX.NbYWFl0", home: "/root", name: "root") end apply_repositories(xml) apply_packages(xml) apply_services end pre_process_config @xml_text = xml.target! end def apply_packages(xml) filter = YAML.load_file( File.join(Machinery::ROOT, "filters", "filter-packages-for-build.yaml") ) || [] add_bootstrap_packages(xml) xml.packages(type: "image") do if @system_description.packages @system_description.packages.each do |package| next if filter.include?(package.name) xml.package(name: "#{package.name}") end end pattern_array = Array.new if @system_description.patterns @system_description.patterns.each do |pattern| xml.namedCollection(name: "#{pattern.name}") end end end end def apply_repositories(xml) if @system_description.repositories usable_repositories = false @system_description.repositories.each do |repo| # workaround kiwi issue by replacing spaces # the final image is not affected because the repositories are added by the config.sh parameters = { alias: repo.alias.gsub(" ", "-"), type: repo.type, priority: repo.priority } if repo.username && repo.password parameters[:username] = repo.username parameters[:password] = repo.password end # only use accessible repositories as source for kiwi build if repo.enabled && !repo.type.nil? && !repo.external_medium? usable_repositories = true xml.repository(parameters) do xml.source(path: repo.url) end end next if repos_with_credentials?(repo) @sh << "zypper -n ar --name='#{repo.name}' " @sh << "--type='#{repo.type}' " if repo.type @sh << "--refresh " if repo.autorefresh @sh << "--disable " unless repo.enabled @sh << "'#{repo.url}' '#{repo.alias}'\n" @sh << "zypper -n mr --priority=#{repo.priority} '#{repo.name}'\n" end unless usable_repositories raise( Machinery::Errors::MissingRequirement.new( "The system description doesn't contain any enabled or network reachable repository." \ " Please make sure that there is at least one accessible repository with all the" \ " required packages." ) ) end end end def apply_services if @system_description["services"] init_system = @system_description["services"].init_system case init_system when "sysvinit" @system_description["services"].each do |service| @sh << if service.state == "on" "chkconfig #{service.name} on\n" else "chkconfig #{service.name} off\n" end end when "systemd" # possible systemd service states: # http://www.freedesktop.org/software/systemd/man/systemctl.html#Unit%20File%20Commands @system_description["services"].each do |service| case service.state when "enabled" @sh << "systemctl enable #{service.name}\n" when "disabled" @sh << "systemctl disable #{service.name}\n" when "masked" @sh << "systemctl mask #{service.name}\n" when "static" # Don't do anything because the unit is not meant to be # enabled/disabled manually. when "linked" # Don't do anything because linking doesn't mean enabling # nor disabling. when "indirect" # Don't do anything because indirect doesn't mean enabling # nor disabling. when "enabled-runtime" when "linked-runtime" when "masked-runtime" when "transient" when "generated" # Don't do anything because these states are not supposed # to be permanent. when "bad" # Don't do anything because the unit file is broken else raise Machinery::Errors::ExportFailed.new( "The systemd unit state #{service.state} is unknown." ) end end else Machinery::Ui.warn( "Warning: Containers do not have an init system, so the default service" \ " configuration provided by the packages will be used for the image." ) end end end def enable_dhcp(output_location) write_dhcp_network_config(output_location, "lan0") write_persistent_net_rules(output_location) puts "DHCP in built image will be enabled for the first device" end def write_dhcp_network_config(output_location, device) network_location = File.join(output_location, "root/etc/sysconfig/network") FileUtils.mkdir_p(network_location) File.write(File.join(network_location, "ifcfg-#{device}"), "BOOTPROTO='dhcp'\nSTARTMODE='onboot'" ) end def write_persistent_net_rules(output_location) udev_location = File.join(output_location, "root/etc/udev/rules.d") persistent_net_rule = [ 'SUBSYSTEM=="net"', 'ACTION=="add"', 'DRIVERS=="?*"', 'ATTR{address}=="?*"', 'ATTR{dev_id}=="0x0"', 'ATTR{type}=="1"', 'KERNEL=="?*"', 'NAME="lan0"' ] FileUtils.mkdir_p(udev_location) File.write( File.join(udev_location, "70-persistent-net.rules"), persistent_net_rule.join(", ") ) end def enable_ssh @sh << "suseInsertService sshd\n" end end