]> Frank Brehm's Git Trees - pixelpark/create-vmware-tpl.git/commitdiff
Moving lib/cr_vmware_tpl/cobbler.py to lib/cr_vmware_tpl/cobbler/__init__.py
authorFrank Brehm <frank.brehm@pixelpark.com>
Fri, 22 Sep 2023 08:20:58 +0000 (10:20 +0200)
committerFrank Brehm <frank.brehm@pixelpark.com>
Fri, 22 Sep 2023 08:20:58 +0000 (10:20 +0200)
lib/cr_vmware_tpl/cobbler.py [deleted file]
lib/cr_vmware_tpl/cobbler/__init__.py [new file with mode: 0644]

diff --git a/lib/cr_vmware_tpl/cobbler.py b/lib/cr_vmware_tpl/cobbler.py
deleted file mode 100644 (file)
index 72d03c9..0000000
+++ /dev/null
@@ -1,1281 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-@author: Frank Brehm
-@contact: frank.brehm@pixelpark.com
-@copyright: © 2020 by Frank Brehm, Berlin
-@summary: A handler module for executing cobbler actions
-"""
-from __future__ import absolute_import, print_function
-
-# Standard module
-import logging
-import re
-import datetime
-import pipes
-import hashlib
-import textwrap
-import ipaddress
-import tempfile
-import os
-
-from pathlib import Path
-
-# Third party modules
-import paramiko
-from paramiko.ssh_exception import SSHException
-
-from packaging.version import Version
-
-import jinja2
-
-from six.moves import configparser
-
-# Own modules
-
-from fb_tools.common import pp, to_str, is_sequence, to_bool
-from fb_tools.handling_obj import CompletedProcess
-from fb_tools.handler import BaseHandler
-from fb_tools.xlate import format_list
-
-from . import print_section_start, print_section_end
-
-from .config import CrTplConfiguration
-
-from .errors import CobblerError, ExpectedCobblerError
-
-from .xlate import XLATOR
-
-__version__ = '0.10.1'
-
-LOG = logging.getLogger(__name__)
-
-_ = XLATOR.gettext
-ngettext = XLATOR.ngettext
-
-
-# =============================================================================
-class Cobbler(BaseHandler):
-    """
-    A handler class for executing cobbler actions.
-    """
-
-    dhcpd_leases_file = Path('/var') / 'lib' / 'dhcpd' / 'dhcpd.leases'
-
-    # -------------------------------------------------------------------------
-    def __init__(
-        self, appname=None, verbose=0, version=__version__, base_dir=None,
-            cfg=None, terminal_has_colors=False, simulate=None, force=None, initialized=False):
-
-        if not isinstance(cfg, CrTplConfiguration):
-            msg = _("{w} is not an instance of {c}, but an instance of {i} instead.").format(
-                w='Parameter cfg', c='CrTplConfiguration', i=cfg.__class__.__name__)
-            raise CobblerError(msg)
-
-        self.host = CrTplConfiguration.default_cobbler_host
-        self.cobbler_bin = CrTplConfiguration.default_cobbler_bin
-        self.ssh_port = CrTplConfiguration.default_cobbler_ssh_port
-        self.ssh_user = CrTplConfiguration.default_cobbler_ssh_user
-        self.private_ssh_key = None
-        self.ssh = None
-        self.ssh_timeout = CrTplConfiguration.default_cobbler_ssh_timeout
-        self.root_dir = CrTplConfiguration.default_cobbler_rootdir
-        self.cfg = cfg
-        self.cobbler_version = None
-        self.local_ks_file = None
-
-        super(Cobbler, self).__init__(
-            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
-            terminal_has_colors=terminal_has_colors, simulate=simulate,
-            force=force, initialized=False,
-        )
-
-        self.private_ssh_key = str(self.base_dir.joinpath('keys', CrTplConfiguration.ssh_privkey))
-
-        self.cobbler_bin = cfg.cobbler_bin
-        self.private_ssh_key = cfg.private_ssh_key
-        self.host = cfg.cobbler_host
-        self.ssh_port = cfg.cobbler_ssh_port
-        self.ssh_user = cfg.cobbler_ssh_user
-        self.ssh_timeout = cfg.cobbler_ssh_timeout
-        self.root_dir = cfg.cobbler_rootdir
-
-        if initialized:
-            self.initialized = True
-
-    # -------------------------------------------------------------------------
-    def __del__(self):
-
-        if self.local_ks_file:
-            if self.local_ks_file.exists():
-                self.local_ks_file.unlink()
-
-    # -------------------------------------------------------------------------
-    def exec_cobbler(self, cmd, no_simulate=False, show_output=True):
-
-        simulate = self.simulate
-        if no_simulate:
-            simulate = False
-
-        cmds = []
-        if simulate:
-            cmds.append('echo')
-        if self.ssh_user != 'root':
-            cmds.append('sudo')
-        cmds.append(self.cobbler_bin)
-        if cmd is not None:
-            if is_sequence(cmd):
-                cmds += cmd
-            else:
-                c = to_str(cmd)
-                if not isinstance(c, str):
-                    msg = _(
-                        "Command {c!r} is neither an Array nor a String, "
-                        "but a {t!r} instead.").format(
-                        c=cmd, t=cmd.__class__.__name__)
-                    raise TypeError(msg)
-                cmds.append(c)
-
-        cmd_str = ' '.join(map(lambda x: pipes.quote(x), cmds))
-        LOG.debug("Exec cobbler: " + cmd_str)
-        return self.exec_ssh(cmd_str, show_output=show_output)
-
-    # -------------------------------------------------------------------------
-    def exec_ssh(self, cmd, show_output=False):
-
-        ssh = None
-        proc = None
-
-        try:
-
-            if self.verbose > 2:
-                LOG.debug(_("Initializing {} ...").format('paramiko SSHClient'))
-            ssh = paramiko.SSHClient()
-            if self.verbose > 2:
-                LOG.debug(_("Loading SSH system host keys."))
-            ssh.load_system_host_keys()
-            if self.verbose > 2:
-                LOG.debug(_("Setting SSH missing host key policy to {}.").format('AutoAddPolicy'))
-            ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
-
-            start_dt = datetime.datetime.now()
-
-            if self.verbose > 1:
-                LOG.debug(_("Connecting to {h!r}, port {p} as {u!r} per SSH ...").format(
-                    h=self.host, p=self.ssh_port, u=self.ssh_user))
-            ssh.connect(
-                self.host, port=self.ssh_port, timeout=self.ssh_timeout,
-                username=self.ssh_user, key_filename=self.private_ssh_key)
-
-            if self.verbose > 1:
-                LOG.debug(_("Executing: {!r}").format(cmd))
-
-            stdin, stdout, stderr = ssh.exec_command(cmd, timeout=self.ssh_timeout)
-            end_dt = datetime.datetime.now()
-            retcode = stdout.channel.recv_exit_status()
-
-            output = to_str(stdout.read()).strip()
-            err = to_str(stderr.read()).strip()
-
-            if show_output:
-                if output == '' and err == '':
-                    LOG.debug(_("No output."))
-                if output:
-                    LOG.debug(_("Output on {}:").format('STDOUT') + ' ' + output)
-                if err:
-                    LOG.debug(_("Output on {}:").format('STDERR') + ' ' + err)
-
-            proc = CompletedProcess(cmd, retcode, output, err, start_dt=start_dt, end_dt=end_dt)
-
-        except SSHException as e:
-            msg = _("Could not connect via {w} to {user}@{host}: {e}").format(
-                w='SSH', user=self.ssh_user, host=self.host, e=e)
-            raise ExpectedCobblerError(msg)
-
-        finally:
-            if ssh:
-                if self.verbose > 2:
-                    LOG.debug(_("Closing SSH connection."))
-                ssh.close()
-
-        if self.verbose > 2:
-            LOG.debug(_("Completed SSH process:") + "\n{}".format(proc))
-        return proc
-
-    # -------------------------------------------------------------------------
-    def scp_to(self, local_file, remote_file):
-
-        ssh = None
-
-        try:
-
-            if self.verbose > 2:
-                LOG.debug(_("Initializing {} ...").format('paramiko SSHClient'))
-            ssh = paramiko.SSHClient()
-            if self.verbose > 2:
-                LOG.debug(_("Loading SSH system host keys."))
-            ssh.load_system_host_keys()
-            if self.verbose > 2:
-                LOG.debug(_("Setting SSH missing host key policy to {}.").format('AutoAddPolicy'))
-            ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
-
-            if self.verbose > 1:
-                LOG.debug(_("Connecting to {h!r}, port {p} as {u!r} per SSH ...").format(
-                    h=self.host, p=self.ssh_port, u=self.ssh_user))
-
-            if self.simulate:
-                LOG.debug(_(
-                    "Simulating SCP of {local!r} to {user}@{host}:{remote} ...").format(
-                    local=str(local_file), user=self.ssh_user,
-                    host=self.host, remote=str(remote_file)))
-
-            else:
-                ssh.connect(
-                    self.host, port=self.ssh_port, timeout=self.ssh_timeout,
-                    username=self.ssh_user, key_filename=self.private_ssh_key)
-
-                sftp = ssh.open_sftp()
-
-                LOG.debug(_("SCP of {local!r} to {user}@{host}:{remote} ...").format(
-                    local=str(local_file), user=self.ssh_user,
-                    host=self.host, remote=str(remote_file)))
-
-                sftp.put(str(local_file), str(remote_file))
-
-        except SSHException as e:
-            msg = _("Could not connect via {w} to {user}@{host}: {e}").format(
-                w='SCP', user=self.ssh_user, host=self.host, e=e)
-            raise ExpectedCobblerError(msg)
-
-        finally:
-            sftp = None
-            if ssh:
-                if self.verbose > 2:
-                    LOG.debug(_("Closing SSH connection."))
-                ssh.close()
-
-    # -------------------------------------------------------------------------
-    def get_cobbler_version(self):
-        """Trying to evaluate the version of Cobbler on the cobbler host."""
-
-        proc = self.exec_cobbler('version', no_simulate=True, show_output=False)
-
-        if proc.returncode != 0:
-            err = _('No error message')
-            if proc.stderr:
-                err = proc.stderr
-            elif proc.stdout:
-                err = proc.stdout
-            msg = _("Could not get version of cobbler: {}").format(err)
-            raise ExpectedCobblerError(msg)
-
-        first_line = proc.stdout.splitlines()[0]
-        cobbler_version = re.sub(r'^\s*cobbler\s+', '', first_line, flags=re.IGNORECASE).strip()
-        self.cobbler_version = Version(cobbler_version)
-
-        LOG.info(_("Version of {} is:").format("Cobbler") + " {!r}".format(cobbler_version))
-
-        if self.cobbler_version.major not in (2, 3):
-            msg = _("Unsupported version {ver!r} of {co}, valid versions of {co} are {valid}.")
-            msg = msg.format(ver=cobbler_version, co='Cobbler', valid=format_list(['2.X', '3.X']))
-            raise ExpectedCobblerError(msg)
-
-        self.cfg.cobbler_major_version = self.cobbler_version.major
-
-        self.check_remote_directory(self.cfg.cobbler_rootdir, _('Cobbler root directory'))
-
-        if self.verbose > 3:
-            LOG.debug("Current configuration:\n" + pp(self.cfg.as_dict()))
-
-        return cobbler_version
-
-    # -------------------------------------------------------------------------
-    def check_remote_directory(self, rdir, desc=None):
-
-        if self.verbose > 1:
-            msg = _("Checking existence of remote directory {!r} ...").format(str(rdir))
-            LOG.debug(msg)
-
-        cmd = textwrap.dedent("""\
-        if [ -d {rdir!r} ] ; then
-            exit 0
-        fi
-        exit 7
-        """).format(rdir=str(rdir))
-
-        proc = self.exec_ssh(cmd)
-        if proc.returncode != 0:
-            dsc = _('Remote directory')
-            if desc:
-                dsc = desc
-            msg = _(
-                "{dsc} {rdir!r} on host {host!r} does not exists or is not a directory.").format(
-                    dsc=dsc, rdir=str(rdir), host=self.host)
-            raise ExpectedCobblerError(msg)
-
-    # -------------------------------------------------------------------------
-    def ensure_remote_directory(self, rdir, desc=None):
-
-        if self.verbose:
-            msg = _("Ensuring existence of remote directory {!r} ...").format(str(rdir))
-            LOG.debug(msg)
-
-        verb = ''
-        if self.verbose:
-            verb = " --verbose"
-
-        cmd = textwrap.dedent("""\
-        if [ -d {rdir!r} ] ; then
-            exit 0
-        fi
-        if [ -e {rdir!r} ] ; then
-            echo "Path {rdir!r} exists, but is not a directory." >&2
-            exit 7
-        fi
-        mkdir --parents{verb} {rdir!r}
-        """).format(rdir=str(rdir), verb=verb)
-
-        proc = self.exec_ssh(cmd)
-        if proc.returncode == 0:
-            if proc.stdout:
-                LOG.debug(_("Output:") + "\n{}".format(proc.stdout))
-        else:
-            dsc = _('Remote directory')
-            if desc:
-                dsc = desc
-            err = _('No error message')
-            if proc.stderr:
-                err = proc.stderr
-            elif proc.stdout:
-                err = proc.stdout
-            msg = _(
-                "{dsc} {rdir!r} on host {host!r} could not be created: {err}").format(
-                dsc=dsc, rdir=str(rdir), host=self.host, err=err)
-            raise ExpectedCobblerError(msg)
-
-    # -------------------------------------------------------------------------
-    def get_distro_list(self):
-        """Trying to get a list of all configured distros."""
-
-        distro_list = []
-        proc = self.exec_cobbler(('distro', 'list'), no_simulate=True, show_output=False)
-        for line in proc.stdout.splitlines():
-            distro = line.strip()
-            if distro:
-                distro_list.append(distro)
-        distro_list.sort(key=str.lower)
-        if self.verbose > 1:
-            LOG.debug(_("Sorted list of found distros:") + "\n{}".format(pp(distro_list)))
-        return distro_list
-
-    # -------------------------------------------------------------------------
-    def get_repo_list(self):
-        """Trying to get a list of all configured repositories."""
-
-        repo_list = []
-
-        proc = self.exec_cobbler(('repo', 'list'), no_simulate=True, show_output=False)
-        for line in proc.stdout.splitlines():
-            repo = line.strip()
-            if repo:
-                repo_list.append(repo)
-        repo_list.sort(key=str.lower)
-        if self.verbose > 1:
-            LOG.debug(_("Sorted list of found repositories:") + "\n{}".format(pp(repo_list)))
-        return repo_list
-
-    # -------------------------------------------------------------------------
-    def verify_distro_repos(self, distro):
-
-        repo_list = self.get_repo_list()
-
-        LOG.debug(_("Checking existence of repos for distro {!r}.").format(distro.name))
-
-        all_ok = True
-        for repo in distro.repos:
-            if repo not in repo_list:
-                msg = _("Repo {r!r} for distro {d!r} not found on cobbler server.").format(
-                    r=repo, d=distro.name)
-                LOG.warn(msg)
-                all_ok = False
-            elif self.verbose > 1:
-                msg = _("Found repo {r!r} for distro {d!r}.").format(r=repo, d=distro.name)
-                LOG.debug(msg)
-
-        return all_ok
-
-    # -------------------------------------------------------------------------
-    def get_profile_list(self):
-        """Trying to get a list of all configured cobbler profiles."""
-
-        profile_list = []
-        proc = self.exec_cobbler(('profile', 'list'), no_simulate=True, show_output=False)
-        for line in proc.stdout.splitlines():
-            profile = line.strip()
-            if profile:
-                profile_list.append(profile)
-        profile_list.sort(key=str.lower)
-        if self.verbose > 1:
-            LOG.debug(_("Sorted list of found profiles:") + "\n{}".format(pp(profile_list)))
-        return profile_list
-
-    # -------------------------------------------------------------------------
-    def ensure_remote_file(self, local_file, remote_file, check_parent=True):
-
-        if check_parent:
-            self.check_remote_directory(remote_file.parent)
-
-        msg = _("Checking remote file {rfile!r} based on local {lfile!r} ...").format(
-            rfile=str(remote_file), lfile=str(local_file))
-        LOG.debug(msg)
-
-        if not local_file.exists() or not local_file.is_file():
-            msg = _("Local file {!r} either not exists or is not a regular file.").format(
-                str(local_file))
-            raise ExpectedCobblerError(msg)
-        local_file_content = local_file.read_bytes()
-        digest = hashlib.sha256(local_file_content).hexdigest()
-        if self.verbose > 1:
-            LOG.debug(_('{typ} sum of {ks!r} is: {dig}').format(
-                typ='SHA256', ks=str(local_file), dig=digest))
-
-        cmd = textwrap.dedent("""\
-        if [ -f {rfile!r} ] ; then
-            digest=$(sha256sum {rfile!r} | awk '{{print $1}}')
-            echo "Digest: ${{digest}}"
-            if [ "${{digest}}" != {dig!r} ] ; then
-                echo "SHA256 sum does not match." >&2
-                exit 4
-            fi
-            exit 0
-        else
-            exit 3
-        fi
-        """).format(rfile=str(remote_file), dig=digest)
-
-        proc = self.exec_ssh(cmd)
-        if proc.returncode == 0:
-            LOG.debug(_("Remote file {!r} has the correct content.").format(
-                str(remote_file)))
-            return
-
-        msg = _("File {!r} has to be copied.").format(str(local_file))
-        LOG.warn(msg)
-
-        self.scp_to(local_file, remote_file)
-
-    # -------------------------------------------------------------------------
-    def get_remote_filecontent(self, remote_file):
-
-        LOG.debug(_("Getting content of remote file {!r} ...").format(str(remote_file)))
-
-        cmd = textwrap.dedent("""\
-        if [ -f {rfile!r} ] ; then
-            cat {rfile!r}
-        else
-            echo "Remote file does not exists." >&2
-            exit 7
-        fi
-        """).format(rfile=str(remote_file))
-
-        proc = self.exec_ssh(cmd)
-        if proc.returncode:
-            err = _('No error message')
-            if proc.stderr:
-                err = proc.stderr
-            elif proc.stdout:
-                err = proc.stdout
-            msg = _(
-                "Error getting content of {rfile!r} on host {host!r} - "
-                "returncode was {rc}: {err}").format(
-                rfile=str(remote_file), host=self.host, rc=proc.returncode, err=err)
-            raise ExpectedCobblerError(msg)
-
-        return proc.stdout
-
-    # -------------------------------------------------------------------------
-    def ensure_root_authkeys(self, tmp_auth_keys_file=None):
-
-        bname = 'auth_keys_pp_betrieb'
-        if tmp_auth_keys_file:
-            local_file = tmp_auth_keys_file
-        else:
-            local_file = self.base_dir / 'keys' / bname
-        remote_file = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir / bname
-
-        self.ensure_remote_file(local_file, remote_file)
-
-    # -------------------------------------------------------------------------
-    def ensure_rsyslog_cfg_files(self):
-
-        files_dir = self.base_dir / 'files'
-        docroot = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir
-        remote_dir = docroot / self.cfg.system_status
-
-        LOG.info(_("Ensuring currentness of rsyslog config files ..."))
-        print_section_start(
-            'ensure_rsyslog_cfg_files', 'Ensuring rsyslog config files.', collapsed=True)
-
-        for local_cfg_file in files_dir.glob('*rsyslog.conf*'):
-            remote_cfg_file = remote_dir / local_cfg_file.name
-            LOG.debug(_("Ensuring {loc!r} => {rem!r}.").format(
-                loc=str(local_cfg_file), rem=str(remote_cfg_file)))
-            self.ensure_remote_file(local_cfg_file, remote_cfg_file, check_parent=False)
-
-        print_section_end('ensure_rsyslog_cfg_files')
-
-    # -------------------------------------------------------------------------
-    def ensure_profile_ks(self):
-
-        ks_template_name = self.cfg.current_distro.ks_template
-
-        LOG.info(_("Using {!r} as a template for the kickstart file.").format(
-            './templates/' + ks_template_name))
-
-        prefix = 'tmp.' + self.cfg.cobbler_profile + '.'
-        (fh, tmp_ks) = tempfile.mkstemp(prefix=prefix, suffix='.ks', text=True)
-        os.close(fh)
-        self.local_ks_file = Path(tmp_ks)
-        LOG.debug(_("Using temporary kickstart file {!r}.").format(tmp_ks))
-
-        snippet_lst = []
-        for snippet in sorted(self.cfg.current_distro.snippets.values(), key=str.lower):
-            snippet_lst.append(snippet)
-
-        jinja_env = jinja2.Environment(
-            loader=jinja2.FileSystemLoader(str(self.base_dir / 'templates')),
-            autoescape=jinja2.select_autoescape(),
-        )
-        ks_template = jinja_env.get_template(ks_template_name)
-        ks_content = ks_template.render(
-            distro=self.cfg.current_distro, snippets=snippet_lst) + '\n\n'
-        if self.verbose > 1:
-            LOG.debug(_("Generated kickstart file content:") + '\n' + ks_content)
-
-        self.local_ks_file.write_text(ks_content)
-
-        remote_ks = self.cfg.cobbler_profile_ks
-        LOG.info(_("Ensuring currentness of profile kickstart script {!r}.").format(
-            str(remote_ks)))
-
-        self.ensure_remote_file(self.local_ks_file, remote_ks)
-
-        LOG.debug(_("Removing {!r} ...").format(str(self.local_ks_file)))
-        self.local_ks_file.unlink()
-        self.local_ks_file = None
-
-    # -------------------------------------------------------------------------
-    def ensure_profile(self):
-        """Ensure the existence and the correctnes of the given profile."""
-
-        profile = self.cfg.cobbler_profile
-
-        LOG.info(_("Ensuring profile {!r} ...").format(profile))
-        print_section_start(
-            'cobbler_ensure_profile', 'Ensuring profile.', collapsed=True)
-
-        profile_list = self.get_profile_list()
-
-        if profile in profile_list:
-            self.change_profile()
-        else:
-            self.add_profile()
-
-        print_section_end('cobbler_ensure_profile')
-
-    # -------------------------------------------------------------------------
-    def change_profile(self):
-        """Ensure correctnes of an existing profile."""
-
-        profile = self.cfg.cobbler_profile
-
-        LOG.debug(_("Checking existing profile {!r} ...").format(profile))
-
-        profile_vars = self.get_profile_vars(profile)
-
-        if self.verbose > 1:
-            LOG.debug(_("Got profile variables:") + '\n' + pp(profile_vars))
-        self._change_profile(profile_vars)
-
-    # -------------------------------------------------------------------------
-    def get_profile_vars(self, profile):
-
-        vars_out = '[main]\n'
-
-        kwargs = {
-            'allow_no_value': True,
-            'strict': False,
-        }
-
-        cmd = ('profile', 'dumpvars', '--name', profile)
-
-        proc = self.exec_cobbler(cmd, no_simulate=True, show_output=False)
-        vars_out += proc.stdout
-
-        parser = configparser.RawConfigParser(**kwargs)
-        try:
-            parser.read_string(vars_out)
-        except Exception as e:
-            msg = _("Got {what} on reading and parsing of profile {p!r}:").format(
-                what=e.__class__.__name__, p=profile)
-            msg += ' ' + str(e)
-            raise ExpectedCobblerError(msg)
-
-        data = {}
-
-        for (key, value) in parser.items('main'):
-            k = key.lower()
-            data[k] = value
-
-        return data
-
-    # -------------------------------------------------------------------------
-    def _change_profile(self, profile_vars):
-
-        profile = self.cfg.cobbler_profile
-        distro = self.cfg.cobbler_distro
-        distro_info = self.cfg.current_distro
-        status = self.cfg.system_status
-
-        LOG.debug(_("Checking existing profile {n!r} ({d}) ...").format(
-            n=profile, d=distro_info.description))
-
-        repos = []
-        if distro_info.repos:
-            repos = distro_info.repos.as_list()
-        repos_str = ' '.join(repos)
-
-        comment = "Profile for creating a {} VM.".format(distro_info.description)
-        name_servers = '[' + ', '.join(
-            map(lambda x: "'" + x + "'", self.cfg.cobbler_nameservers)) + ']'
-        dns_search = '[' + ', '.join(
-            map(lambda x: "'" + x + "'", self.cfg.cobbler_dns_search)) + ']'
-
-        args = []
-
-        if self.verbose > 1:
-            msg = _("Checking for distro:") + ' ' + distro
-            LOG.debug(msg)
-        if profile_vars['distro'] != distro:
-            args.append('--distro')
-            args.append(distro)
-
-        enable_menu = to_bool(profile_vars['enable_menu'])
-        if not enable_menu:
-            args.append('--enable-menu')
-            args.append('1')
-
-        if self.cfg.cobbler_major_version == 3:
-            if profile_vars['autoinstall'] != str(self.cfg.cobbler_profile_ks.name):
-                args.append('--autoinstall')
-                args.append(str(self.cfg.cobbler_profile_ks.name))
-        else:
-            if profile_vars['kickstart'] != str(self.cfg.cobbler_profile_ks):
-                args.append('--kickstart')
-                args.append(str(self.cfg.cobbler_profile_ks))
-
-        if self.verbose > 1:
-            msg = _("Checking for repos:") + ' ' + repos_str
-            LOG.debug(msg)
-        if profile_vars['repos'] != repos_str:
-            args.append('--repos')
-            args.append(repos_str)
-
-        if profile_vars['comment'] != comment:
-            args.append('--comment')
-            args.append(comment)
-
-        if self.verbose > 1:
-            msg = _("Checking for nameservers:") + ' ' + name_servers
-            LOG.debug(msg)
-        if profile_vars['name_servers'] != name_servers:
-            args.append('--name-servers')
-            args.append(' '.join(self.cfg.cobbler_nameservers))
-
-        if self.verbose > 1:
-            msg = _("Checking for DNS search domains:") + ' ' + dns_search
-            LOG.debug(msg)
-        if profile_vars['name_servers_search'] != dns_search:
-            args.append('--name-servers-search')
-            args.append(' '.join(self.cfg.cobbler_dns_search))
-
-        ks_meta_ok = True
-        ks_meta_vars = {}
-        if 'autoinstall_meta' in profile_vars:
-            ks_meta_vars = self.xform_ks_meta(profile_vars['autoinstall_meta'])
-        if 'ROOT_PWD_HASH' not in ks_meta_vars:
-            LOG.debug(_('Profile ks_meta {!r} is not ok.').format('ROOT_PWD_HASH'))
-            ks_meta_ok = False
-        if ('SWAP_SIZE_MB' not in ks_meta_vars or
-                ks_meta_vars['SWAP_SIZE_MB'] != str(self.cfg.swap_size_mb)):
-            LOG.debug(_('Profile ks_meta {!r} is not ok.').format('SWAP_SIZE_MB'))
-            ks_meta_ok = False
-        if ('SYSTEM_STATUS' not in ks_meta_vars or
-                ks_meta_vars['SYSTEM_STATUS'] != status):
-            LOG.debug(_('Profile ks_meta {!r} is not ok.').format('SYSTEM_STATUS'))
-            ks_meta_ok = False
-        if ('WS_REL_FILESDIR' not in ks_meta_vars or
-                ks_meta_vars['WS_REL_FILESDIR'] != str(self.cfg.cobbler_ws_rel_filesdir)):
-            LOG.debug(_('Profile ks_meta {!r} is not ok.').format('WS_REL_FILESDIR'))
-            ks_meta_ok = False
-        if ('COBBLER_URL' not in ks_meta_vars or
-                ks_meta_vars['COBBLER_URL'] != "http://{}".format(self.cfg.cobbler_host)):
-            LOG.debug(_('Profile ks_meta {!r} is not ok.').format('COBBLER_URL'))
-            ks_meta_ok = False
-
-        if not ks_meta_ok:
-            ks_meta_list = []
-            ks_meta_list.append("ROOT_PWD_HASH={}".format(self.cfg.get_root_pwd_hash()))
-            ks_meta_list.append("SWAP_SIZE_MB={}".format(self.cfg.swap_size_mb))
-            ks_meta_list.append("SYSTEM_STATUS={}".format(status))
-            ks_meta_list.append("WS_REL_FILESDIR={}".format(self.cfg.cobbler_ws_rel_filesdir))
-            ks_meta_list.append("COBBLER_URL=http://{}".format(self.cfg.cobbler_host))
-
-            ks_meta = ' '.join(ks_meta_list)
-
-            args.append('--autoinstall-meta')
-            args.append(ks_meta)
-
-        if self.verbose:
-            LOG.debug("Args for 'profile edit:\n{}".format(pp(args)))
-
-        if not args:
-            LOG.debug(_("No need for changing profile {!r}").format(profile))
-            return
-
-        args = ['profile', 'edit', '--name', profile] + args
-
-        if self.verbose > 1:
-            LOG.debug('Arguments for changing profile:\n' + pp(args))
-        return
-
-        proc = self.exec_cobbler(args)
-
-        if proc.returncode:
-            err = _('No error message')
-            if proc.stderr:
-                err = proc.stderr
-            elif proc.stdout:
-                err = proc.stdout
-            msg = _("Error editing a cobbler profile - returncode was {rc}: {err}").format(
-                rc=proc.returncode, err=err)
-            raise ExpectedCobblerError(msg)
-
-    # -------------------------------------------------------------------------
-    def xform_ks_meta(self, ks_meta):
-
-        data = {}
-
-        re_ws = re.compile(r'\s+')
-        re_key_value = re.compile(r'(\S+)=(.*)')
-
-        for token in re_ws.split(ks_meta):
-            m = re_key_value.match(token)
-            if m:
-                data[m.group(1)] = m.group(2)
-
-        if self.verbose > 1:
-            LOG.debug("Got autoinstall_meta from profile:\n" + pp(data))
-
-        return data
-
-    # -------------------------------------------------------------------------
-    def add_profile(self):
-        """Creating a new profile."""
-
-        profile = self.cfg.cobbler_profile
-
-        LOG.info(_("Creating new profile {!r} ...").format(profile))
-
-        distro_info = self.cfg.current_distro
-        comment = "Profile for creating a {} VM.".format(distro_info.description)
-        status = self.cfg.system_status
-
-        LOG.debug("Using kickstart file {!r}".format(self.cfg.cobbler_profile_ks))
-
-        ks_meta_list = []
-        ks_meta_list.append("ROOT_PWD_HASH={}".format(self.cfg.get_root_pwd_hash()))
-        ks_meta_list.append("SWAP_SIZE_MB={}".format(self.cfg.swap_size_mb))
-        ks_meta_list.append("SYSTEM_STATUS={}".format(status))
-        ks_meta_list.append("WS_REL_FILESDIR={}".format(self.cfg.cobbler_ws_rel_filesdir))
-        ks_meta_list.append("COBBLER_URL=http://{}".format(self.cfg.cobbler_host))
-
-        ks_meta = None
-        if ks_meta_list:
-            ks_meta = ' '.join(ks_meta_list)
-
-        repos = []
-        if distro_info.repos:
-            repos = distro_info.repos.as_list()
-
-        args = ['profile', 'add']
-        args.append('--name')
-        args.append(self.cfg.cobbler_profile)
-        args.append('--distro')
-        args.append(distro_info.distro)
-        args.append('--enable-menu')
-        args.append('1')
-        if self.cfg.cobbler_major_version == 3:
-            args.append('--autoinstall')
-            args.append(str(self.cfg.cobbler_profile_ks.name))
-        else:
-            args.append('--kickstart')
-            args.append(str(self.cfg.cobbler_profile_ks))
-        if repos:
-            args.append('--repos')
-            args.append(' '.join(repos))
-        args.append('--comment')
-        args.append(comment)
-        if ks_meta:
-            if self.cfg.cobbler_major_version == 3:
-                args.append('--autoinstall-meta')
-            else:
-                args.append('--ksmeta')
-            args.append(ks_meta)
-        args.append('--virt-cpus')
-        args.append('2')
-        args.append('--virt-file-size')
-        args.append('20')
-        args.append('--virt-ram')
-        args.append('4096')
-        args.append('--virt-type')
-        args.append('vmware')
-        args.append('--virt-bridge')
-        args.append('br0')
-        args.append('--virt-disk-driver')
-        args.append('vmdk')
-        args.append('--name-servers')
-        args.append(' '.join(self.cfg.cobbler_nameservers))
-        args.append('--name-servers-search')
-        args.append(' '.join(self.cfg.cobbler_dns_search))
-
-        proc = self.exec_cobbler(args)
-
-        if self.verbose > 1:
-            LOG.debug(_("Completed SSH process:") + "\n{}".format(proc))
-
-        if proc.returncode:
-            err = _('No error message')
-            if proc.stderr:
-                err = proc.stderr
-            elif proc.stdout:
-                err = proc.stdout
-            msg = _("Error creating a cobbler profile - returncode was {rc}: {err}").format(
-                rc=proc.returncode, err=err)
-            raise ExpectedCobblerError(msg)
-
-        if proc.stderr:
-            msg = _("There was an error message on creating profile {!r}:").format(
-                self.cfg.cobbler_profile)
-            msg += ' ' + proc.stderr
-            LOG.error(msg)
-
-        if self.simulate:
-            return
-
-        profile_list = self.get_profile_list()
-        if self.cfg.cobbler_profile not in profile_list:
-            msg = _("Did not found profile {!r} after trying creation.").format(
-                self.cfg.cobbler_profile)
-            raise ExpectedCobblerError(msg)
-
-    # -------------------------------------------------------------------------
-    def ensure_system_ks(self):
-
-        local_ks_base = 'template-' + self.cfg.os_id + '.ks'
-        local_ks = self.base_dir / 'kickstart' / local_ks_base
-        remote_ks = self.cfg.system_ks
-        LOG.info(_("Ensuring currentness of system kickstart script {!r}.").format(
-            str(remote_ks)))
-
-        jinja_env = jinja2.Environment(
-            loader=jinja2.FileSystemLoader(str(self.base_dir / 'templates')),
-            autoescape=jinja2.select_autoescape(),
-        )
-        ks_template = jinja_env.get_template('el-standard.ks')
-        ks_content = ks_template.render(distro=self.cfg.current_distro)
-        if self.verbose > 1:
-            LOG.debug(_("Generated kickstart file content:") + '\n' + ks_content)
-
-        return
-        print_section_start(
-            'ensure_system_ks', 'Ensuring currentness of system kickstart script', collapsed=True)
-
-        self.ensure_remote_file(local_ks, remote_ks)
-        print_section_end('ensure_system_ks')
-
-    # -------------------------------------------------------------------------
-    def ensure_snippets(self):
-
-        local_snippets_dir = self.base_dir / 'snippets'
-        self.ensure_remote_directory(self.cfg.snippets_dir)
-
-        LOG.info(_("Ensuring currentness of snippets below {!r}.").format(
-            str(self.cfg.snippets_dir)))
-        print_section_start('ensure_snippets', "Ensuring currentness of snippets", collapsed=True)
-
-        for local_snippet in local_snippets_dir.glob('*'):
-            remote_snippet = self.cfg.snippets_dir / local_snippet.name
-            LOG.debug(_("Ensuring {loc!r} => {rem!r}.").format(
-                loc=str(local_snippet), rem=str(remote_snippet)))
-            self.ensure_remote_file(local_snippet, remote_snippet, check_parent=False)
-
-        print_section_end('ensure_snippets')
-
-    # -------------------------------------------------------------------------
-    def ensure_bashrc(self):
-
-        files_dir = self.base_dir / 'files'
-        docroot = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir
-        remote_dir = docroot / self.cfg.system_status
-
-        LOG.info(_("Ensuring currentness of bashrc files."))
-        print_section_start(
-            'ensure_bashrc', 'Ensuring currentness of bashrc files.', collapsed=True)
-
-        for local_rc_file in files_dir.glob('bashrc*'):
-            remote_rc_file = remote_dir / local_rc_file.name
-            LOG.debug(_("Ensuring {loc!r} => {rem!r}.").format(
-                loc=str(local_rc_file), rem=str(remote_rc_file)))
-            self.ensure_remote_file(local_rc_file, remote_rc_file, check_parent=False)
-
-        print_section_end('ensure_bashrc')
-
-    # -------------------------------------------------------------------------
-    def ensure_vimrc(self):
-
-        files_dir = self.base_dir / 'files'
-        docroot = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir
-        remote_dir = docroot / self.cfg.system_status
-
-        LOG.info(_("Ensuring currentness of vimrc files."))
-        print_section_start(
-            'ensure_vimrc', "Ensuring currentness of vimrc files.", collapsed=True)
-
-        for local_rc_file in files_dir.glob('vimrc*'):
-            remote_rc_file = remote_dir / local_rc_file.name
-            LOG.debug(_("Ensuring {loc!r} => {rem!r}.").format(
-                loc=str(local_rc_file), rem=str(remote_rc_file)))
-            self.ensure_remote_file(local_rc_file, remote_rc_file, check_parent=False)
-
-        print_section_end('ensure_vimrc')
-
-    # -------------------------------------------------------------------------
-    def ensure_logrotate_files(self):
-
-        files_dir = self.base_dir / 'files'
-        docroot = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir
-        remote_dir = docroot / self.cfg.system_status
-
-        LOG.info(_("Ensuring currentness of logrotate files."))
-        print_section_start(
-            'ensure_logrotate_files', "Ensuring currentness of logrotate files.",
-            collapsed=True)
-
-        for local_file in files_dir.glob('logrotate*'):
-            remote_file = remote_dir / local_file.name
-            LOG.debug(_("Ensuring {loc!r} => {rem!r}.").format(
-                loc=str(local_file), rem=str(remote_file)))
-            self.ensure_remote_file(local_file, remote_file, check_parent=False)
-
-        print_section_end('ensure_logrotate_files')
-
-    # -------------------------------------------------------------------------
-    def ensure_create_motd(self):
-
-        local_script = self.base_dir / 'bin' / 'create-motd.sh'
-        docroot = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir
-        remote_dir = docroot / self.cfg.system_status
-        remote_script = remote_dir / local_script.name
-
-        LOG.info(_("Ensuring currentness of create-motd.sh."))
-        print_section_start('ensure_create_motd', "Ensuring currentness of create-motd.sh.")
-
-        LOG.debug(_("Ensuring {loc!r} => {rem!r}.").format(
-            loc=str(local_script), rem=str(remote_script)))
-        self.ensure_remote_file(local_script, remote_script, check_parent=False)
-
-        print_section_end('ensure_create_motd')
-
-    # -------------------------------------------------------------------------
-    def add_system(self, name, fqdn, mac_address, comment=None):
-        """Creating a new system."""
-
-        profile = self.cfg.cobbler_profile
-        os_id = self.cfg.os_id
-
-        LOG.info(_("Creating new system {!r} ...").format(name))
-
-        if not comment:
-            comment = "VMWare template for creating a {} system.".format(os_id)
-        status = self.cfg.system_status
-
-        ks_meta_list = []
-        ks_meta_list.append("ROOT_PWD_HASH={}".format(self.cfg.get_root_pwd_hash()))
-        ks_meta_list.append("SWAP_SIZE_MB={}".format(self.cfg.swap_size_mb))
-        ks_meta_list.append("SYSTEM_STATUS={}".format(status))
-        ks_meta_list.append("WS_REL_FILESDIR={}".format(self.cfg.cobbler_ws_rel_filesdir))
-        ks_meta_list.append("COBBLER_URL=http://{}".format(self.cfg.cobbler_host))
-
-        ks_meta = None
-        if ks_meta_list:
-            ks_meta = ' '.join(ks_meta_list)
-
-        args = ['system', 'add']
-        args.append('--name')
-        args.append(name)
-        args.append('--profile')
-        args.append(profile)
-        args.append('--status')
-        args.append(status)
-        args.append('--comment')
-        args.append(comment)
-        if ks_meta:
-            if self.cfg.cobbler_major_version == 3:
-                args.append('--autoinstall-meta')
-            else:
-                args.append('--ksmeta')
-            args.append(ks_meta)
-        args.append('--power-type')
-        args.append('apc')
-        args.append('--hostname')
-        args.append(fqdn)
-        args.append('--mac-address')
-        args.append(mac_address)
-        args.append('--interface')
-        args.append('eth0')
-        args.append('--management')
-        args.append('true')
-
-        proc = self.exec_cobbler(args)
-
-        if proc.returncode:
-            err = _('No error message')
-            if proc.stderr:
-                err = proc.stderr
-            elif proc.stdout:
-                err = proc.stdout
-            msg = _("Error creating a cobbler system - returncode was {rc}: {err}").format(
-                rc=proc.returncode, err=err)
-            raise ExpectedCobblerError(msg)
-
-        self.sync()
-
-    # -------------------------------------------------------------------------
-    def remove_system(self, name):
-        """Removing the given system."""
-
-        LOG.info(_("Removing system {!r} ...").format(name))
-        print_section_start('remove_system', "Removing system ...", collapsed=True)
-
-        args = ['system', 'remove']
-        args.append('--name')
-        args.append(name)
-
-        proc = self.exec_cobbler(args)
-
-        if proc.returncode:
-            err = _('No error message')
-            if proc.stderr:
-                err = proc.stderr
-            elif proc.stdout:
-                err = proc.stdout
-            msg = _("Error removing the cobbler system {n!r} - returncode was {rc}: {err}").format(
-                n=name, rc=proc.returncode, err=err)
-            print_section_end('remove_system')
-            raise ExpectedCobblerError(msg)
-
-        self.sync()
-        print_section_end('remove_system')
-
-    # -------------------------------------------------------------------------
-    def sync(self):
-        """Executing 'cobbler sync' to apply environment, especially DHCPD configuration."""
-
-        proc = self.exec_cobbler('sync', show_output=False)
-        LOG.info(_("Executing cobbler sync ..."))
-
-        if proc.returncode != 0:
-            err = _('No error message')
-            if proc.stderr:
-                err = proc.stderr
-            elif proc.stdout:
-                err = proc.stdout
-            msg = _("Could syncing cobbler: {}").format(err)
-            raise ExpectedCobblerError(msg)
-
-        if self.verbose > 1:
-            if proc.stdout:
-                LOG.debug(_("Output on {}:").format('STDOUT') + '\n' + proc.stdout)
-        if self.verbose:
-            if proc.stderr:
-                LOG.debug(_("Output on {}:").format('STDERR') + '\n' + proc.stderr)
-
-    # -------------------------------------------------------------------------
-    def ensure_keys(self, tmp_auth_keys_file=None):
-
-        local_keys_dir = self.base_dir / 'keys'
-        if tmp_auth_keys_file:
-            auth_keys_file = tmp_auth_keys_file
-        else:
-            auth_keys_file = local_keys_dir / "auth_keys_pp_betrieb"
-        docroot = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir
-        remote_dir = docroot / self.cfg.system_status / 'keys'
-        remote_file = remote_dir / "auth_keys_pp_betrieb"
-
-        LOG.info(_("Ensuring currentness of authorized_keys file of root {!r}.").format(
-            str(remote_file)))
-        print_section_start(
-            'ensure_keys', "Ensuring authorized_keys of root.", collapsed=True)
-        self.ensure_remote_directory(remote_dir)
-        self.ensure_remote_file(auth_keys_file, remote_file, check_parent=False)
-        print_section_end('ensure_keys')
-
-    # -------------------------------------------------------------------------
-    def ensure_repo_files(self):
-
-        files_dir = self.base_dir / 'files'
-        docroot = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir
-        remote_dir = docroot / self.cfg.system_status / 'repos'
-
-        LOG.info(_("Ensuring currentness of repo files below {!r}.").format(str(files_dir)))
-        print_section_start(
-            'ensure_repo_files', "Ensuring repo files.", collapsed=True)
-
-        for local_repo_dir in files_dir.glob('repos-*'):
-            if not local_repo_dir.is_dir():
-                LOG.warn(_("Local path {!r} is not a directory.").format(str(local_repo_dir)))
-                continue
-            dirname = str(local_repo_dir.name)
-            os_id = dirname.replace('repos-', '', 1)
-            LOG.debug(_("Ensuring repo files for {}.").format(os_id))
-            remote_dir_os = remote_dir / os_id
-            self.ensure_remote_directory(remote_dir_os)
-            for local_repo_file in local_repo_dir.glob('*.repo'):
-                remote_file = remote_dir_os / local_repo_file.name
-                self.ensure_remote_file(local_repo_file, remote_file, check_parent=False)
-
-        LOG.debug(_("Finished with repo files."))
-        print_section_end('ensure_repo_files')
-
-    # -------------------------------------------------------------------------
-    def ensure_postfix_files(self):
-
-        files_dir = self.base_dir / 'files' / 'postfix'
-        docroot = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir
-        remote_dir = docroot / self.cfg.system_status / 'postfix'
-
-        LOG.info(_("Ensuring currentness of postfix files below {!r}.").format(str(files_dir)))
-        print_section_start(
-            'ensure_postfix_files', "Ensuring postfix files.", collapsed=True)
-
-        self.ensure_remote_directory(remote_dir)
-        for local_file in files_dir.glob('*'):
-            remote_file = remote_dir / local_file.name
-            self.ensure_remote_file(local_file, remote_file, check_parent=False)
-
-        LOG.debug(_("Finished with postfix files."))
-        print_section_end('ensure_postfix_files')
-
-    # -------------------------------------------------------------------------
-    def get_dhcp_ip(self, mac_address):
-
-        mac = mac_address.lower()
-        LOG.debug(_("Trying to get IP of MAC address {!r} given by DHCP ...").format(mac))
-        all_leases = self.get_remote_filecontent(self.dhcpd_leases_file)
-
-        cur_ip = None
-        assigments = {}
-        re_lease_start = re.compile(r'^\s*lease\s+((?:\d{1,3}\.){3}\d{1,3})\s+', re.IGNORECASE)
-        re_mac = re.compile(
-            r'^\s*hardware\s+ethernet\s+((?:[0-9a-f]{2}:){5}[0-9a-f]{2})\s*;', re.IGNORECASE)
-
-        for line in all_leases.splitlines():
-            match = re_lease_start.match(line)
-            if match:
-                try:
-                    ip = ipaddress.ip_address(match.group(1))
-                    cur_ip = str(ip)
-                except ValueError as e:
-                    msg = _("Found invalid IP address {ip!r} in leases file: {err}").format(
-                        ip=match.group(1), err=e)
-                    LOG.error(msg)
-                continue
-
-            match = re_mac.match(line)
-            if match:
-                found_mac = match.group(1).lower()
-                if cur_ip:
-                    assigments[found_mac] = cur_ip
-                continue
-
-        if self.verbose > 2:
-            LOG.debug(_("Found DHCP IP assignments:") + "\n" + pp(assigments))
-        if mac in assigments:
-            return assigments[mac]
-        return None
-
-    # -------------------------------------------------------------------------
-    def get_dhcp_ips(self, mac_address):
-
-        mac = mac_address.lower()
-        LOG.debug(_("Trying to get IP of MAC address {!r} given by DHCP ...").format(mac))
-        all_leases = self.get_remote_filecontent(self.dhcpd_leases_file)
-
-        ips = []
-
-        cur_ip = None
-        assigments = {}
-        re_lease_start = re.compile(r'^\s*lease\s+((?:\d{1,3}\.){3}\d{1,3})\s+', re.IGNORECASE)
-        re_mac = re.compile(
-            r'^\s*hardware\s+ethernet\s+((?:[0-9a-f]{2}:){5}[0-9a-f]{2})\s*;', re.IGNORECASE)
-
-        for line in all_leases.splitlines():
-            match = re_lease_start.match(line)
-            if match:
-                try:
-                    ip = ipaddress.ip_address(match.group(1))
-                    cur_ip = str(ip)
-                except ValueError as e:
-                    msg = _("Found invalid IP address {ip!r} in leases file: {err}").format(
-                        ip=match.group(1), err=e)
-                    LOG.error(msg)
-                continue
-
-            match = re_mac.match(line)
-            if match:
-                found_mac = match.group(1).lower()
-                if cur_ip:
-                    assigments[cur_ip] = found_mac
-                continue
-
-        for ip in assigments.keys():
-            found_mac = assigments[ip]
-            if mac == found_mac:
-                ips.append(ip)
-
-        if self.verbose > 2:
-            LOG.debug(_("Found DHCP IP assignments:") + "\n" + pp(assigments))
-
-        return ips
-
-    # -------------------------------------------------------------------------
-    def ensure_webroot(self):
-
-        docroot = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir
-        webroot = docroot / self.cfg.system_status
-        desc = _("Webroot directory")
-        LOG.info(_("Ensuring existence of {what} {dir!r}...").format(
-            what=desc, dir=str(webroot)))
-
-        self.ensure_remote_directory(webroot, desc)
-
-
-# =============================================================================
-if __name__ == "__main__":
-
-    pass
-
-# =============================================================================
-
-# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/lib/cr_vmware_tpl/cobbler/__init__.py b/lib/cr_vmware_tpl/cobbler/__init__.py
new file mode 100644 (file)
index 0000000..031f5d6
--- /dev/null
@@ -0,0 +1,1281 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+@author: Frank Brehm
+@contact: frank.brehm@pixelpark.com
+@copyright: © 2023 by Frank Brehm, Berlin
+@summary: A handler module for executing cobbler actions
+"""
+from __future__ import absolute_import, print_function
+
+# Standard module
+import logging
+import re
+import datetime
+import pipes
+import hashlib
+import textwrap
+import ipaddress
+import tempfile
+import os
+
+from pathlib import Path
+
+# Third party modules
+import paramiko
+from paramiko.ssh_exception import SSHException
+
+from packaging.version import Version
+
+import jinja2
+
+from six.moves import configparser
+
+# Own modules
+
+from fb_tools.common import pp, to_str, is_sequence, to_bool
+from fb_tools.handling_obj import CompletedProcess
+from fb_tools.handler import BaseHandler
+from fb_tools.xlate import format_list
+
+from .. import print_section_start, print_section_end
+
+from ..config import CrTplConfiguration
+
+from ..errors import CobblerError, ExpectedCobblerError
+
+from ..xlate import XLATOR
+
+__version__ = '0.11.0'
+
+LOG = logging.getLogger(__name__)
+
+_ = XLATOR.gettext
+ngettext = XLATOR.ngettext
+
+
+# =============================================================================
+class Cobbler(BaseHandler):
+    """
+    A handler class for executing cobbler actions.
+    """
+
+    dhcpd_leases_file = Path('/var') / 'lib' / 'dhcpd' / 'dhcpd.leases'
+
+    # -------------------------------------------------------------------------
+    def __init__(
+        self, appname=None, verbose=0, version=__version__, base_dir=None,
+            cfg=None, terminal_has_colors=False, simulate=None, force=None, initialized=False):
+
+        if not isinstance(cfg, CrTplConfiguration):
+            msg = _("{w} is not an instance of {c}, but an instance of {i} instead.").format(
+                w='Parameter cfg', c='CrTplConfiguration', i=cfg.__class__.__name__)
+            raise CobblerError(msg)
+
+        self.host = CrTplConfiguration.default_cobbler_host
+        self.cobbler_bin = CrTplConfiguration.default_cobbler_bin
+        self.ssh_port = CrTplConfiguration.default_cobbler_ssh_port
+        self.ssh_user = CrTplConfiguration.default_cobbler_ssh_user
+        self.private_ssh_key = None
+        self.ssh = None
+        self.ssh_timeout = CrTplConfiguration.default_cobbler_ssh_timeout
+        self.root_dir = CrTplConfiguration.default_cobbler_rootdir
+        self.cfg = cfg
+        self.cobbler_version = None
+        self.local_ks_file = None
+
+        super(Cobbler, self).__init__(
+            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
+            terminal_has_colors=terminal_has_colors, simulate=simulate,
+            force=force, initialized=False,
+        )
+
+        self.private_ssh_key = str(self.base_dir.joinpath('keys', CrTplConfiguration.ssh_privkey))
+
+        self.cobbler_bin = cfg.cobbler_bin
+        self.private_ssh_key = cfg.private_ssh_key
+        self.host = cfg.cobbler_host
+        self.ssh_port = cfg.cobbler_ssh_port
+        self.ssh_user = cfg.cobbler_ssh_user
+        self.ssh_timeout = cfg.cobbler_ssh_timeout
+        self.root_dir = cfg.cobbler_rootdir
+
+        if initialized:
+            self.initialized = True
+
+    # -------------------------------------------------------------------------
+    def __del__(self):
+
+        if self.local_ks_file:
+            if self.local_ks_file.exists():
+                self.local_ks_file.unlink()
+
+    # -------------------------------------------------------------------------
+    def exec_cobbler(self, cmd, no_simulate=False, show_output=True):
+
+        simulate = self.simulate
+        if no_simulate:
+            simulate = False
+
+        cmds = []
+        if simulate:
+            cmds.append('echo')
+        if self.ssh_user != 'root':
+            cmds.append('sudo')
+        cmds.append(self.cobbler_bin)
+        if cmd is not None:
+            if is_sequence(cmd):
+                cmds += cmd
+            else:
+                c = to_str(cmd)
+                if not isinstance(c, str):
+                    msg = _(
+                        "Command {c!r} is neither an Array nor a String, "
+                        "but a {t!r} instead.").format(
+                        c=cmd, t=cmd.__class__.__name__)
+                    raise TypeError(msg)
+                cmds.append(c)
+
+        cmd_str = ' '.join(map(lambda x: pipes.quote(x), cmds))
+        LOG.debug("Exec cobbler: " + cmd_str)
+        return self.exec_ssh(cmd_str, show_output=show_output)
+
+    # -------------------------------------------------------------------------
+    def exec_ssh(self, cmd, show_output=False):
+
+        ssh = None
+        proc = None
+
+        try:
+
+            if self.verbose > 2:
+                LOG.debug(_("Initializing {} ...").format('paramiko SSHClient'))
+            ssh = paramiko.SSHClient()
+            if self.verbose > 2:
+                LOG.debug(_("Loading SSH system host keys."))
+            ssh.load_system_host_keys()
+            if self.verbose > 2:
+                LOG.debug(_("Setting SSH missing host key policy to {}.").format('AutoAddPolicy'))
+            ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
+
+            start_dt = datetime.datetime.now()
+
+            if self.verbose > 1:
+                LOG.debug(_("Connecting to {h!r}, port {p} as {u!r} per SSH ...").format(
+                    h=self.host, p=self.ssh_port, u=self.ssh_user))
+            ssh.connect(
+                self.host, port=self.ssh_port, timeout=self.ssh_timeout,
+                username=self.ssh_user, key_filename=self.private_ssh_key)
+
+            if self.verbose > 1:
+                LOG.debug(_("Executing: {!r}").format(cmd))
+
+            stdin, stdout, stderr = ssh.exec_command(cmd, timeout=self.ssh_timeout)
+            end_dt = datetime.datetime.now()
+            retcode = stdout.channel.recv_exit_status()
+
+            output = to_str(stdout.read()).strip()
+            err = to_str(stderr.read()).strip()
+
+            if show_output:
+                if output == '' and err == '':
+                    LOG.debug(_("No output."))
+                if output:
+                    LOG.debug(_("Output on {}:").format('STDOUT') + ' ' + output)
+                if err:
+                    LOG.debug(_("Output on {}:").format('STDERR') + ' ' + err)
+
+            proc = CompletedProcess(cmd, retcode, output, err, start_dt=start_dt, end_dt=end_dt)
+
+        except SSHException as e:
+            msg = _("Could not connect via {w} to {user}@{host}: {e}").format(
+                w='SSH', user=self.ssh_user, host=self.host, e=e)
+            raise ExpectedCobblerError(msg)
+
+        finally:
+            if ssh:
+                if self.verbose > 2:
+                    LOG.debug(_("Closing SSH connection."))
+                ssh.close()
+
+        if self.verbose > 2:
+            LOG.debug(_("Completed SSH process:") + "\n{}".format(proc))
+        return proc
+
+    # -------------------------------------------------------------------------
+    def scp_to(self, local_file, remote_file):
+
+        ssh = None
+
+        try:
+
+            if self.verbose > 2:
+                LOG.debug(_("Initializing {} ...").format('paramiko SSHClient'))
+            ssh = paramiko.SSHClient()
+            if self.verbose > 2:
+                LOG.debug(_("Loading SSH system host keys."))
+            ssh.load_system_host_keys()
+            if self.verbose > 2:
+                LOG.debug(_("Setting SSH missing host key policy to {}.").format('AutoAddPolicy'))
+            ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
+
+            if self.verbose > 1:
+                LOG.debug(_("Connecting to {h!r}, port {p} as {u!r} per SSH ...").format(
+                    h=self.host, p=self.ssh_port, u=self.ssh_user))
+
+            if self.simulate:
+                LOG.debug(_(
+                    "Simulating SCP of {local!r} to {user}@{host}:{remote} ...").format(
+                    local=str(local_file), user=self.ssh_user,
+                    host=self.host, remote=str(remote_file)))
+
+            else:
+                ssh.connect(
+                    self.host, port=self.ssh_port, timeout=self.ssh_timeout,
+                    username=self.ssh_user, key_filename=self.private_ssh_key)
+
+                sftp = ssh.open_sftp()
+
+                LOG.debug(_("SCP of {local!r} to {user}@{host}:{remote} ...").format(
+                    local=str(local_file), user=self.ssh_user,
+                    host=self.host, remote=str(remote_file)))
+
+                sftp.put(str(local_file), str(remote_file))
+
+        except SSHException as e:
+            msg = _("Could not connect via {w} to {user}@{host}: {e}").format(
+                w='SCP', user=self.ssh_user, host=self.host, e=e)
+            raise ExpectedCobblerError(msg)
+
+        finally:
+            sftp = None
+            if ssh:
+                if self.verbose > 2:
+                    LOG.debug(_("Closing SSH connection."))
+                ssh.close()
+
+    # -------------------------------------------------------------------------
+    def get_cobbler_version(self):
+        """Trying to evaluate the version of Cobbler on the cobbler host."""
+
+        proc = self.exec_cobbler('version', no_simulate=True, show_output=False)
+
+        if proc.returncode != 0:
+            err = _('No error message')
+            if proc.stderr:
+                err = proc.stderr
+            elif proc.stdout:
+                err = proc.stdout
+            msg = _("Could not get version of cobbler: {}").format(err)
+            raise ExpectedCobblerError(msg)
+
+        first_line = proc.stdout.splitlines()[0]
+        cobbler_version = re.sub(r'^\s*cobbler\s+', '', first_line, flags=re.IGNORECASE).strip()
+        self.cobbler_version = Version(cobbler_version)
+
+        LOG.info(_("Version of {} is:").format("Cobbler") + " {!r}".format(cobbler_version))
+
+        if self.cobbler_version.major not in (2, 3):
+            msg = _("Unsupported version {ver!r} of {co}, valid versions of {co} are {valid}.")
+            msg = msg.format(ver=cobbler_version, co='Cobbler', valid=format_list(['2.X', '3.X']))
+            raise ExpectedCobblerError(msg)
+
+        self.cfg.cobbler_major_version = self.cobbler_version.major
+
+        self.check_remote_directory(self.cfg.cobbler_rootdir, _('Cobbler root directory'))
+
+        if self.verbose > 3:
+            LOG.debug("Current configuration:\n" + pp(self.cfg.as_dict()))
+
+        return cobbler_version
+
+    # -------------------------------------------------------------------------
+    def check_remote_directory(self, rdir, desc=None):
+
+        if self.verbose > 1:
+            msg = _("Checking existence of remote directory {!r} ...").format(str(rdir))
+            LOG.debug(msg)
+
+        cmd = textwrap.dedent("""\
+        if [ -d {rdir!r} ] ; then
+            exit 0
+        fi
+        exit 7
+        """).format(rdir=str(rdir))
+
+        proc = self.exec_ssh(cmd)
+        if proc.returncode != 0:
+            dsc = _('Remote directory')
+            if desc:
+                dsc = desc
+            msg = _(
+                "{dsc} {rdir!r} on host {host!r} does not exists or is not a directory.").format(
+                    dsc=dsc, rdir=str(rdir), host=self.host)
+            raise ExpectedCobblerError(msg)
+
+    # -------------------------------------------------------------------------
+    def ensure_remote_directory(self, rdir, desc=None):
+
+        if self.verbose:
+            msg = _("Ensuring existence of remote directory {!r} ...").format(str(rdir))
+            LOG.debug(msg)
+
+        verb = ''
+        if self.verbose:
+            verb = " --verbose"
+
+        cmd = textwrap.dedent("""\
+        if [ -d {rdir!r} ] ; then
+            exit 0
+        fi
+        if [ -e {rdir!r} ] ; then
+            echo "Path {rdir!r} exists, but is not a directory." >&2
+            exit 7
+        fi
+        mkdir --parents{verb} {rdir!r}
+        """).format(rdir=str(rdir), verb=verb)
+
+        proc = self.exec_ssh(cmd)
+        if proc.returncode == 0:
+            if proc.stdout:
+                LOG.debug(_("Output:") + "\n{}".format(proc.stdout))
+        else:
+            dsc = _('Remote directory')
+            if desc:
+                dsc = desc
+            err = _('No error message')
+            if proc.stderr:
+                err = proc.stderr
+            elif proc.stdout:
+                err = proc.stdout
+            msg = _(
+                "{dsc} {rdir!r} on host {host!r} could not be created: {err}").format(
+                dsc=dsc, rdir=str(rdir), host=self.host, err=err)
+            raise ExpectedCobblerError(msg)
+
+    # -------------------------------------------------------------------------
+    def get_distro_list(self):
+        """Trying to get a list of all configured distros."""
+
+        distro_list = []
+        proc = self.exec_cobbler(('distro', 'list'), no_simulate=True, show_output=False)
+        for line in proc.stdout.splitlines():
+            distro = line.strip()
+            if distro:
+                distro_list.append(distro)
+        distro_list.sort(key=str.lower)
+        if self.verbose > 1:
+            LOG.debug(_("Sorted list of found distros:") + "\n{}".format(pp(distro_list)))
+        return distro_list
+
+    # -------------------------------------------------------------------------
+    def get_repo_list(self):
+        """Trying to get a list of all configured repositories."""
+
+        repo_list = []
+
+        proc = self.exec_cobbler(('repo', 'list'), no_simulate=True, show_output=False)
+        for line in proc.stdout.splitlines():
+            repo = line.strip()
+            if repo:
+                repo_list.append(repo)
+        repo_list.sort(key=str.lower)
+        if self.verbose > 1:
+            LOG.debug(_("Sorted list of found repositories:") + "\n{}".format(pp(repo_list)))
+        return repo_list
+
+    # -------------------------------------------------------------------------
+    def verify_distro_repos(self, distro):
+
+        repo_list = self.get_repo_list()
+
+        LOG.debug(_("Checking existence of repos for distro {!r}.").format(distro.name))
+
+        all_ok = True
+        for repo in distro.repos:
+            if repo not in repo_list:
+                msg = _("Repo {r!r} for distro {d!r} not found on cobbler server.").format(
+                    r=repo, d=distro.name)
+                LOG.warn(msg)
+                all_ok = False
+            elif self.verbose > 1:
+                msg = _("Found repo {r!r} for distro {d!r}.").format(r=repo, d=distro.name)
+                LOG.debug(msg)
+
+        return all_ok
+
+    # -------------------------------------------------------------------------
+    def get_profile_list(self):
+        """Trying to get a list of all configured cobbler profiles."""
+
+        profile_list = []
+        proc = self.exec_cobbler(('profile', 'list'), no_simulate=True, show_output=False)
+        for line in proc.stdout.splitlines():
+            profile = line.strip()
+            if profile:
+                profile_list.append(profile)
+        profile_list.sort(key=str.lower)
+        if self.verbose > 1:
+            LOG.debug(_("Sorted list of found profiles:") + "\n{}".format(pp(profile_list)))
+        return profile_list
+
+    # -------------------------------------------------------------------------
+    def ensure_remote_file(self, local_file, remote_file, check_parent=True):
+
+        if check_parent:
+            self.check_remote_directory(remote_file.parent)
+
+        msg = _("Checking remote file {rfile!r} based on local {lfile!r} ...").format(
+            rfile=str(remote_file), lfile=str(local_file))
+        LOG.debug(msg)
+
+        if not local_file.exists() or not local_file.is_file():
+            msg = _("Local file {!r} either not exists or is not a regular file.").format(
+                str(local_file))
+            raise ExpectedCobblerError(msg)
+        local_file_content = local_file.read_bytes()
+        digest = hashlib.sha256(local_file_content).hexdigest()
+        if self.verbose > 1:
+            LOG.debug(_('{typ} sum of {ks!r} is: {dig}').format(
+                typ='SHA256', ks=str(local_file), dig=digest))
+
+        cmd = textwrap.dedent("""\
+        if [ -f {rfile!r} ] ; then
+            digest=$(sha256sum {rfile!r} | awk '{{print $1}}')
+            echo "Digest: ${{digest}}"
+            if [ "${{digest}}" != {dig!r} ] ; then
+                echo "SHA256 sum does not match." >&2
+                exit 4
+            fi
+            exit 0
+        else
+            exit 3
+        fi
+        """).format(rfile=str(remote_file), dig=digest)
+
+        proc = self.exec_ssh(cmd)
+        if proc.returncode == 0:
+            LOG.debug(_("Remote file {!r} has the correct content.").format(
+                str(remote_file)))
+            return
+
+        msg = _("File {!r} has to be copied.").format(str(local_file))
+        LOG.warn(msg)
+
+        self.scp_to(local_file, remote_file)
+
+    # -------------------------------------------------------------------------
+    def get_remote_filecontent(self, remote_file):
+
+        LOG.debug(_("Getting content of remote file {!r} ...").format(str(remote_file)))
+
+        cmd = textwrap.dedent("""\
+        if [ -f {rfile!r} ] ; then
+            cat {rfile!r}
+        else
+            echo "Remote file does not exists." >&2
+            exit 7
+        fi
+        """).format(rfile=str(remote_file))
+
+        proc = self.exec_ssh(cmd)
+        if proc.returncode:
+            err = _('No error message')
+            if proc.stderr:
+                err = proc.stderr
+            elif proc.stdout:
+                err = proc.stdout
+            msg = _(
+                "Error getting content of {rfile!r} on host {host!r} - "
+                "returncode was {rc}: {err}").format(
+                rfile=str(remote_file), host=self.host, rc=proc.returncode, err=err)
+            raise ExpectedCobblerError(msg)
+
+        return proc.stdout
+
+    # -------------------------------------------------------------------------
+    def ensure_root_authkeys(self, tmp_auth_keys_file=None):
+
+        bname = 'auth_keys_pp_betrieb'
+        if tmp_auth_keys_file:
+            local_file = tmp_auth_keys_file
+        else:
+            local_file = self.base_dir / 'keys' / bname
+        remote_file = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir / bname
+
+        self.ensure_remote_file(local_file, remote_file)
+
+    # -------------------------------------------------------------------------
+    def ensure_rsyslog_cfg_files(self):
+
+        files_dir = self.base_dir / 'files'
+        docroot = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir
+        remote_dir = docroot / self.cfg.system_status
+
+        LOG.info(_("Ensuring currentness of rsyslog config files ..."))
+        print_section_start(
+            'ensure_rsyslog_cfg_files', 'Ensuring rsyslog config files.', collapsed=True)
+
+        for local_cfg_file in files_dir.glob('*rsyslog.conf*'):
+            remote_cfg_file = remote_dir / local_cfg_file.name
+            LOG.debug(_("Ensuring {loc!r} => {rem!r}.").format(
+                loc=str(local_cfg_file), rem=str(remote_cfg_file)))
+            self.ensure_remote_file(local_cfg_file, remote_cfg_file, check_parent=False)
+
+        print_section_end('ensure_rsyslog_cfg_files')
+
+    # -------------------------------------------------------------------------
+    def ensure_profile_ks(self):
+
+        ks_template_name = self.cfg.current_distro.ks_template
+
+        LOG.info(_("Using {!r} as a template for the kickstart file.").format(
+            './templates/' + ks_template_name))
+
+        prefix = 'tmp.' + self.cfg.cobbler_profile + '.'
+        (fh, tmp_ks) = tempfile.mkstemp(prefix=prefix, suffix='.ks', text=True)
+        os.close(fh)
+        self.local_ks_file = Path(tmp_ks)
+        LOG.debug(_("Using temporary kickstart file {!r}.").format(tmp_ks))
+
+        snippet_lst = []
+        for snippet in sorted(self.cfg.current_distro.snippets.values(), key=str.lower):
+            snippet_lst.append(snippet)
+
+        jinja_env = jinja2.Environment(
+            loader=jinja2.FileSystemLoader(str(self.base_dir / 'templates')),
+            autoescape=jinja2.select_autoescape(),
+        )
+        ks_template = jinja_env.get_template(ks_template_name)
+        ks_content = ks_template.render(
+            distro=self.cfg.current_distro, snippets=snippet_lst) + '\n\n'
+        if self.verbose > 1:
+            LOG.debug(_("Generated kickstart file content:") + '\n' + ks_content)
+
+        self.local_ks_file.write_text(ks_content)
+
+        remote_ks = self.cfg.cobbler_profile_ks
+        LOG.info(_("Ensuring currentness of profile kickstart script {!r}.").format(
+            str(remote_ks)))
+
+        self.ensure_remote_file(self.local_ks_file, remote_ks)
+
+        LOG.debug(_("Removing {!r} ...").format(str(self.local_ks_file)))
+        self.local_ks_file.unlink()
+        self.local_ks_file = None
+
+    # -------------------------------------------------------------------------
+    def ensure_profile(self):
+        """Ensure the existence and the correctnes of the given profile."""
+
+        profile = self.cfg.cobbler_profile
+
+        LOG.info(_("Ensuring profile {!r} ...").format(profile))
+        print_section_start(
+            'cobbler_ensure_profile', 'Ensuring profile.', collapsed=True)
+
+        profile_list = self.get_profile_list()
+
+        if profile in profile_list:
+            self.change_profile()
+        else:
+            self.add_profile()
+
+        print_section_end('cobbler_ensure_profile')
+
+    # -------------------------------------------------------------------------
+    def change_profile(self):
+        """Ensure correctnes of an existing profile."""
+
+        profile = self.cfg.cobbler_profile
+
+        LOG.debug(_("Checking existing profile {!r} ...").format(profile))
+
+        profile_vars = self.get_profile_vars(profile)
+
+        if self.verbose > 1:
+            LOG.debug(_("Got profile variables:") + '\n' + pp(profile_vars))
+        self._change_profile(profile_vars)
+
+    # -------------------------------------------------------------------------
+    def get_profile_vars(self, profile):
+
+        vars_out = '[main]\n'
+
+        kwargs = {
+            'allow_no_value': True,
+            'strict': False,
+        }
+
+        cmd = ('profile', 'dumpvars', '--name', profile)
+
+        proc = self.exec_cobbler(cmd, no_simulate=True, show_output=False)
+        vars_out += proc.stdout
+
+        parser = configparser.RawConfigParser(**kwargs)
+        try:
+            parser.read_string(vars_out)
+        except Exception as e:
+            msg = _("Got {what} on reading and parsing of profile {p!r}:").format(
+                what=e.__class__.__name__, p=profile)
+            msg += ' ' + str(e)
+            raise ExpectedCobblerError(msg)
+
+        data = {}
+
+        for (key, value) in parser.items('main'):
+            k = key.lower()
+            data[k] = value
+
+        return data
+
+    # -------------------------------------------------------------------------
+    def _change_profile(self, profile_vars):
+
+        profile = self.cfg.cobbler_profile
+        distro = self.cfg.cobbler_distro
+        distro_info = self.cfg.current_distro
+        status = self.cfg.system_status
+
+        LOG.debug(_("Checking existing profile {n!r} ({d}) ...").format(
+            n=profile, d=distro_info.description))
+
+        repos = []
+        if distro_info.repos:
+            repos = distro_info.repos.as_list()
+        repos_str = ' '.join(repos)
+
+        comment = "Profile for creating a {} VM.".format(distro_info.description)
+        name_servers = '[' + ', '.join(
+            map(lambda x: "'" + x + "'", self.cfg.cobbler_nameservers)) + ']'
+        dns_search = '[' + ', '.join(
+            map(lambda x: "'" + x + "'", self.cfg.cobbler_dns_search)) + ']'
+
+        args = []
+
+        if self.verbose > 1:
+            msg = _("Checking for distro:") + ' ' + distro
+            LOG.debug(msg)
+        if profile_vars['distro'] != distro:
+            args.append('--distro')
+            args.append(distro)
+
+        enable_menu = to_bool(profile_vars['enable_menu'])
+        if not enable_menu:
+            args.append('--enable-menu')
+            args.append('1')
+
+        if self.cfg.cobbler_major_version == 3:
+            if profile_vars['autoinstall'] != str(self.cfg.cobbler_profile_ks.name):
+                args.append('--autoinstall')
+                args.append(str(self.cfg.cobbler_profile_ks.name))
+        else:
+            if profile_vars['kickstart'] != str(self.cfg.cobbler_profile_ks):
+                args.append('--kickstart')
+                args.append(str(self.cfg.cobbler_profile_ks))
+
+        if self.verbose > 1:
+            msg = _("Checking for repos:") + ' ' + repos_str
+            LOG.debug(msg)
+        if profile_vars['repos'] != repos_str:
+            args.append('--repos')
+            args.append(repos_str)
+
+        if profile_vars['comment'] != comment:
+            args.append('--comment')
+            args.append(comment)
+
+        if self.verbose > 1:
+            msg = _("Checking for nameservers:") + ' ' + name_servers
+            LOG.debug(msg)
+        if profile_vars['name_servers'] != name_servers:
+            args.append('--name-servers')
+            args.append(' '.join(self.cfg.cobbler_nameservers))
+
+        if self.verbose > 1:
+            msg = _("Checking for DNS search domains:") + ' ' + dns_search
+            LOG.debug(msg)
+        if profile_vars['name_servers_search'] != dns_search:
+            args.append('--name-servers-search')
+            args.append(' '.join(self.cfg.cobbler_dns_search))
+
+        ks_meta_ok = True
+        ks_meta_vars = {}
+        if 'autoinstall_meta' in profile_vars:
+            ks_meta_vars = self.xform_ks_meta(profile_vars['autoinstall_meta'])
+        if 'ROOT_PWD_HASH' not in ks_meta_vars:
+            LOG.debug(_('Profile ks_meta {!r} is not ok.').format('ROOT_PWD_HASH'))
+            ks_meta_ok = False
+        if ('SWAP_SIZE_MB' not in ks_meta_vars or
+                ks_meta_vars['SWAP_SIZE_MB'] != str(self.cfg.swap_size_mb)):
+            LOG.debug(_('Profile ks_meta {!r} is not ok.').format('SWAP_SIZE_MB'))
+            ks_meta_ok = False
+        if ('SYSTEM_STATUS' not in ks_meta_vars or
+                ks_meta_vars['SYSTEM_STATUS'] != status):
+            LOG.debug(_('Profile ks_meta {!r} is not ok.').format('SYSTEM_STATUS'))
+            ks_meta_ok = False
+        if ('WS_REL_FILESDIR' not in ks_meta_vars or
+                ks_meta_vars['WS_REL_FILESDIR'] != str(self.cfg.cobbler_ws_rel_filesdir)):
+            LOG.debug(_('Profile ks_meta {!r} is not ok.').format('WS_REL_FILESDIR'))
+            ks_meta_ok = False
+        if ('COBBLER_URL' not in ks_meta_vars or
+                ks_meta_vars['COBBLER_URL'] != "http://{}".format(self.cfg.cobbler_host)):
+            LOG.debug(_('Profile ks_meta {!r} is not ok.').format('COBBLER_URL'))
+            ks_meta_ok = False
+
+        if not ks_meta_ok:
+            ks_meta_list = []
+            ks_meta_list.append("ROOT_PWD_HASH={}".format(self.cfg.get_root_pwd_hash()))
+            ks_meta_list.append("SWAP_SIZE_MB={}".format(self.cfg.swap_size_mb))
+            ks_meta_list.append("SYSTEM_STATUS={}".format(status))
+            ks_meta_list.append("WS_REL_FILESDIR={}".format(self.cfg.cobbler_ws_rel_filesdir))
+            ks_meta_list.append("COBBLER_URL=http://{}".format(self.cfg.cobbler_host))
+
+            ks_meta = ' '.join(ks_meta_list)
+
+            args.append('--autoinstall-meta')
+            args.append(ks_meta)
+
+        if self.verbose:
+            LOG.debug("Args for 'profile edit:\n{}".format(pp(args)))
+
+        if not args:
+            LOG.debug(_("No need for changing profile {!r}").format(profile))
+            return
+
+        args = ['profile', 'edit', '--name', profile] + args
+
+        if self.verbose > 1:
+            LOG.debug('Arguments for changing profile:\n' + pp(args))
+        return
+
+        proc = self.exec_cobbler(args)
+
+        if proc.returncode:
+            err = _('No error message')
+            if proc.stderr:
+                err = proc.stderr
+            elif proc.stdout:
+                err = proc.stdout
+            msg = _("Error editing a cobbler profile - returncode was {rc}: {err}").format(
+                rc=proc.returncode, err=err)
+            raise ExpectedCobblerError(msg)
+
+    # -------------------------------------------------------------------------
+    def xform_ks_meta(self, ks_meta):
+
+        data = {}
+
+        re_ws = re.compile(r'\s+')
+        re_key_value = re.compile(r'(\S+)=(.*)')
+
+        for token in re_ws.split(ks_meta):
+            m = re_key_value.match(token)
+            if m:
+                data[m.group(1)] = m.group(2)
+
+        if self.verbose > 1:
+            LOG.debug("Got autoinstall_meta from profile:\n" + pp(data))
+
+        return data
+
+    # -------------------------------------------------------------------------
+    def add_profile(self):
+        """Creating a new profile."""
+
+        profile = self.cfg.cobbler_profile
+
+        LOG.info(_("Creating new profile {!r} ...").format(profile))
+
+        distro_info = self.cfg.current_distro
+        comment = "Profile for creating a {} VM.".format(distro_info.description)
+        status = self.cfg.system_status
+
+        LOG.debug("Using kickstart file {!r}".format(self.cfg.cobbler_profile_ks))
+
+        ks_meta_list = []
+        ks_meta_list.append("ROOT_PWD_HASH={}".format(self.cfg.get_root_pwd_hash()))
+        ks_meta_list.append("SWAP_SIZE_MB={}".format(self.cfg.swap_size_mb))
+        ks_meta_list.append("SYSTEM_STATUS={}".format(status))
+        ks_meta_list.append("WS_REL_FILESDIR={}".format(self.cfg.cobbler_ws_rel_filesdir))
+        ks_meta_list.append("COBBLER_URL=http://{}".format(self.cfg.cobbler_host))
+
+        ks_meta = None
+        if ks_meta_list:
+            ks_meta = ' '.join(ks_meta_list)
+
+        repos = []
+        if distro_info.repos:
+            repos = distro_info.repos.as_list()
+
+        args = ['profile', 'add']
+        args.append('--name')
+        args.append(self.cfg.cobbler_profile)
+        args.append('--distro')
+        args.append(distro_info.distro)
+        args.append('--enable-menu')
+        args.append('1')
+        if self.cfg.cobbler_major_version == 3:
+            args.append('--autoinstall')
+            args.append(str(self.cfg.cobbler_profile_ks.name))
+        else:
+            args.append('--kickstart')
+            args.append(str(self.cfg.cobbler_profile_ks))
+        if repos:
+            args.append('--repos')
+            args.append(' '.join(repos))
+        args.append('--comment')
+        args.append(comment)
+        if ks_meta:
+            if self.cfg.cobbler_major_version == 3:
+                args.append('--autoinstall-meta')
+            else:
+                args.append('--ksmeta')
+            args.append(ks_meta)
+        args.append('--virt-cpus')
+        args.append('2')
+        args.append('--virt-file-size')
+        args.append('20')
+        args.append('--virt-ram')
+        args.append('4096')
+        args.append('--virt-type')
+        args.append('vmware')
+        args.append('--virt-bridge')
+        args.append('br0')
+        args.append('--virt-disk-driver')
+        args.append('vmdk')
+        args.append('--name-servers')
+        args.append(' '.join(self.cfg.cobbler_nameservers))
+        args.append('--name-servers-search')
+        args.append(' '.join(self.cfg.cobbler_dns_search))
+
+        proc = self.exec_cobbler(args)
+
+        if self.verbose > 1:
+            LOG.debug(_("Completed SSH process:") + "\n{}".format(proc))
+
+        if proc.returncode:
+            err = _('No error message')
+            if proc.stderr:
+                err = proc.stderr
+            elif proc.stdout:
+                err = proc.stdout
+            msg = _("Error creating a cobbler profile - returncode was {rc}: {err}").format(
+                rc=proc.returncode, err=err)
+            raise ExpectedCobblerError(msg)
+
+        if proc.stderr:
+            msg = _("There was an error message on creating profile {!r}:").format(
+                self.cfg.cobbler_profile)
+            msg += ' ' + proc.stderr
+            LOG.error(msg)
+
+        if self.simulate:
+            return
+
+        profile_list = self.get_profile_list()
+        if self.cfg.cobbler_profile not in profile_list:
+            msg = _("Did not found profile {!r} after trying creation.").format(
+                self.cfg.cobbler_profile)
+            raise ExpectedCobblerError(msg)
+
+    # -------------------------------------------------------------------------
+    def ensure_system_ks(self):
+
+        local_ks_base = 'template-' + self.cfg.os_id + '.ks'
+        local_ks = self.base_dir / 'kickstart' / local_ks_base
+        remote_ks = self.cfg.system_ks
+        LOG.info(_("Ensuring currentness of system kickstart script {!r}.").format(
+            str(remote_ks)))
+
+        jinja_env = jinja2.Environment(
+            loader=jinja2.FileSystemLoader(str(self.base_dir / 'templates')),
+            autoescape=jinja2.select_autoescape(),
+        )
+        ks_template = jinja_env.get_template('el-standard.ks')
+        ks_content = ks_template.render(distro=self.cfg.current_distro)
+        if self.verbose > 1:
+            LOG.debug(_("Generated kickstart file content:") + '\n' + ks_content)
+
+        return
+        print_section_start(
+            'ensure_system_ks', 'Ensuring currentness of system kickstart script', collapsed=True)
+
+        self.ensure_remote_file(local_ks, remote_ks)
+        print_section_end('ensure_system_ks')
+
+    # -------------------------------------------------------------------------
+    def ensure_snippets(self):
+
+        local_snippets_dir = self.base_dir / 'snippets'
+        self.ensure_remote_directory(self.cfg.snippets_dir)
+
+        LOG.info(_("Ensuring currentness of snippets below {!r}.").format(
+            str(self.cfg.snippets_dir)))
+        print_section_start('ensure_snippets', "Ensuring currentness of snippets", collapsed=True)
+
+        for local_snippet in local_snippets_dir.glob('*'):
+            remote_snippet = self.cfg.snippets_dir / local_snippet.name
+            LOG.debug(_("Ensuring {loc!r} => {rem!r}.").format(
+                loc=str(local_snippet), rem=str(remote_snippet)))
+            self.ensure_remote_file(local_snippet, remote_snippet, check_parent=False)
+
+        print_section_end('ensure_snippets')
+
+    # -------------------------------------------------------------------------
+    def ensure_bashrc(self):
+
+        files_dir = self.base_dir / 'files'
+        docroot = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir
+        remote_dir = docroot / self.cfg.system_status
+
+        LOG.info(_("Ensuring currentness of bashrc files."))
+        print_section_start(
+            'ensure_bashrc', 'Ensuring currentness of bashrc files.', collapsed=True)
+
+        for local_rc_file in files_dir.glob('bashrc*'):
+            remote_rc_file = remote_dir / local_rc_file.name
+            LOG.debug(_("Ensuring {loc!r} => {rem!r}.").format(
+                loc=str(local_rc_file), rem=str(remote_rc_file)))
+            self.ensure_remote_file(local_rc_file, remote_rc_file, check_parent=False)
+
+        print_section_end('ensure_bashrc')
+
+    # -------------------------------------------------------------------------
+    def ensure_vimrc(self):
+
+        files_dir = self.base_dir / 'files'
+        docroot = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir
+        remote_dir = docroot / self.cfg.system_status
+
+        LOG.info(_("Ensuring currentness of vimrc files."))
+        print_section_start(
+            'ensure_vimrc', "Ensuring currentness of vimrc files.", collapsed=True)
+
+        for local_rc_file in files_dir.glob('vimrc*'):
+            remote_rc_file = remote_dir / local_rc_file.name
+            LOG.debug(_("Ensuring {loc!r} => {rem!r}.").format(
+                loc=str(local_rc_file), rem=str(remote_rc_file)))
+            self.ensure_remote_file(local_rc_file, remote_rc_file, check_parent=False)
+
+        print_section_end('ensure_vimrc')
+
+    # -------------------------------------------------------------------------
+    def ensure_logrotate_files(self):
+
+        files_dir = self.base_dir / 'files'
+        docroot = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir
+        remote_dir = docroot / self.cfg.system_status
+
+        LOG.info(_("Ensuring currentness of logrotate files."))
+        print_section_start(
+            'ensure_logrotate_files', "Ensuring currentness of logrotate files.",
+            collapsed=True)
+
+        for local_file in files_dir.glob('logrotate*'):
+            remote_file = remote_dir / local_file.name
+            LOG.debug(_("Ensuring {loc!r} => {rem!r}.").format(
+                loc=str(local_file), rem=str(remote_file)))
+            self.ensure_remote_file(local_file, remote_file, check_parent=False)
+
+        print_section_end('ensure_logrotate_files')
+
+    # -------------------------------------------------------------------------
+    def ensure_create_motd(self):
+
+        local_script = self.base_dir / 'bin' / 'create-motd.sh'
+        docroot = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir
+        remote_dir = docroot / self.cfg.system_status
+        remote_script = remote_dir / local_script.name
+
+        LOG.info(_("Ensuring currentness of create-motd.sh."))
+        print_section_start('ensure_create_motd', "Ensuring currentness of create-motd.sh.")
+
+        LOG.debug(_("Ensuring {loc!r} => {rem!r}.").format(
+            loc=str(local_script), rem=str(remote_script)))
+        self.ensure_remote_file(local_script, remote_script, check_parent=False)
+
+        print_section_end('ensure_create_motd')
+
+    # -------------------------------------------------------------------------
+    def add_system(self, name, fqdn, mac_address, comment=None):
+        """Creating a new system."""
+
+        profile = self.cfg.cobbler_profile
+        os_id = self.cfg.os_id
+
+        LOG.info(_("Creating new system {!r} ...").format(name))
+
+        if not comment:
+            comment = "VMWare template for creating a {} system.".format(os_id)
+        status = self.cfg.system_status
+
+        ks_meta_list = []
+        ks_meta_list.append("ROOT_PWD_HASH={}".format(self.cfg.get_root_pwd_hash()))
+        ks_meta_list.append("SWAP_SIZE_MB={}".format(self.cfg.swap_size_mb))
+        ks_meta_list.append("SYSTEM_STATUS={}".format(status))
+        ks_meta_list.append("WS_REL_FILESDIR={}".format(self.cfg.cobbler_ws_rel_filesdir))
+        ks_meta_list.append("COBBLER_URL=http://{}".format(self.cfg.cobbler_host))
+
+        ks_meta = None
+        if ks_meta_list:
+            ks_meta = ' '.join(ks_meta_list)
+
+        args = ['system', 'add']
+        args.append('--name')
+        args.append(name)
+        args.append('--profile')
+        args.append(profile)
+        args.append('--status')
+        args.append(status)
+        args.append('--comment')
+        args.append(comment)
+        if ks_meta:
+            if self.cfg.cobbler_major_version == 3:
+                args.append('--autoinstall-meta')
+            else:
+                args.append('--ksmeta')
+            args.append(ks_meta)
+        args.append('--power-type')
+        args.append('apc')
+        args.append('--hostname')
+        args.append(fqdn)
+        args.append('--mac-address')
+        args.append(mac_address)
+        args.append('--interface')
+        args.append('eth0')
+        args.append('--management')
+        args.append('true')
+
+        proc = self.exec_cobbler(args)
+
+        if proc.returncode:
+            err = _('No error message')
+            if proc.stderr:
+                err = proc.stderr
+            elif proc.stdout:
+                err = proc.stdout
+            msg = _("Error creating a cobbler system - returncode was {rc}: {err}").format(
+                rc=proc.returncode, err=err)
+            raise ExpectedCobblerError(msg)
+
+        self.sync()
+
+    # -------------------------------------------------------------------------
+    def remove_system(self, name):
+        """Removing the given system."""
+
+        LOG.info(_("Removing system {!r} ...").format(name))
+        print_section_start('remove_system', "Removing system ...", collapsed=True)
+
+        args = ['system', 'remove']
+        args.append('--name')
+        args.append(name)
+
+        proc = self.exec_cobbler(args)
+
+        if proc.returncode:
+            err = _('No error message')
+            if proc.stderr:
+                err = proc.stderr
+            elif proc.stdout:
+                err = proc.stdout
+            msg = _("Error removing the cobbler system {n!r} - returncode was {rc}: {err}").format(
+                n=name, rc=proc.returncode, err=err)
+            print_section_end('remove_system')
+            raise ExpectedCobblerError(msg)
+
+        self.sync()
+        print_section_end('remove_system')
+
+    # -------------------------------------------------------------------------
+    def sync(self):
+        """Executing 'cobbler sync' to apply environment, especially DHCPD configuration."""
+
+        proc = self.exec_cobbler('sync', show_output=False)
+        LOG.info(_("Executing cobbler sync ..."))
+
+        if proc.returncode != 0:
+            err = _('No error message')
+            if proc.stderr:
+                err = proc.stderr
+            elif proc.stdout:
+                err = proc.stdout
+            msg = _("Could syncing cobbler: {}").format(err)
+            raise ExpectedCobblerError(msg)
+
+        if self.verbose > 1:
+            if proc.stdout:
+                LOG.debug(_("Output on {}:").format('STDOUT') + '\n' + proc.stdout)
+        if self.verbose:
+            if proc.stderr:
+                LOG.debug(_("Output on {}:").format('STDERR') + '\n' + proc.stderr)
+
+    # -------------------------------------------------------------------------
+    def ensure_keys(self, tmp_auth_keys_file=None):
+
+        local_keys_dir = self.base_dir / 'keys'
+        if tmp_auth_keys_file:
+            auth_keys_file = tmp_auth_keys_file
+        else:
+            auth_keys_file = local_keys_dir / "auth_keys_pp_betrieb"
+        docroot = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir
+        remote_dir = docroot / self.cfg.system_status / 'keys'
+        remote_file = remote_dir / "auth_keys_pp_betrieb"
+
+        LOG.info(_("Ensuring currentness of authorized_keys file of root {!r}.").format(
+            str(remote_file)))
+        print_section_start(
+            'ensure_keys', "Ensuring authorized_keys of root.", collapsed=True)
+        self.ensure_remote_directory(remote_dir)
+        self.ensure_remote_file(auth_keys_file, remote_file, check_parent=False)
+        print_section_end('ensure_keys')
+
+    # -------------------------------------------------------------------------
+    def ensure_repo_files(self):
+
+        files_dir = self.base_dir / 'files'
+        docroot = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir
+        remote_dir = docroot / self.cfg.system_status / 'repos'
+
+        LOG.info(_("Ensuring currentness of repo files below {!r}.").format(str(files_dir)))
+        print_section_start(
+            'ensure_repo_files', "Ensuring repo files.", collapsed=True)
+
+        for local_repo_dir in files_dir.glob('repos-*'):
+            if not local_repo_dir.is_dir():
+                LOG.warn(_("Local path {!r} is not a directory.").format(str(local_repo_dir)))
+                continue
+            dirname = str(local_repo_dir.name)
+            os_id = dirname.replace('repos-', '', 1)
+            LOG.debug(_("Ensuring repo files for {}.").format(os_id))
+            remote_dir_os = remote_dir / os_id
+            self.ensure_remote_directory(remote_dir_os)
+            for local_repo_file in local_repo_dir.glob('*.repo'):
+                remote_file = remote_dir_os / local_repo_file.name
+                self.ensure_remote_file(local_repo_file, remote_file, check_parent=False)
+
+        LOG.debug(_("Finished with repo files."))
+        print_section_end('ensure_repo_files')
+
+    # -------------------------------------------------------------------------
+    def ensure_postfix_files(self):
+
+        files_dir = self.base_dir / 'files' / 'postfix'
+        docroot = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir
+        remote_dir = docroot / self.cfg.system_status / 'postfix'
+
+        LOG.info(_("Ensuring currentness of postfix files below {!r}.").format(str(files_dir)))
+        print_section_start(
+            'ensure_postfix_files', "Ensuring postfix files.", collapsed=True)
+
+        self.ensure_remote_directory(remote_dir)
+        for local_file in files_dir.glob('*'):
+            remote_file = remote_dir / local_file.name
+            self.ensure_remote_file(local_file, remote_file, check_parent=False)
+
+        LOG.debug(_("Finished with postfix files."))
+        print_section_end('ensure_postfix_files')
+
+    # -------------------------------------------------------------------------
+    def get_dhcp_ip(self, mac_address):
+
+        mac = mac_address.lower()
+        LOG.debug(_("Trying to get IP of MAC address {!r} given by DHCP ...").format(mac))
+        all_leases = self.get_remote_filecontent(self.dhcpd_leases_file)
+
+        cur_ip = None
+        assigments = {}
+        re_lease_start = re.compile(r'^\s*lease\s+((?:\d{1,3}\.){3}\d{1,3})\s+', re.IGNORECASE)
+        re_mac = re.compile(
+            r'^\s*hardware\s+ethernet\s+((?:[0-9a-f]{2}:){5}[0-9a-f]{2})\s*;', re.IGNORECASE)
+
+        for line in all_leases.splitlines():
+            match = re_lease_start.match(line)
+            if match:
+                try:
+                    ip = ipaddress.ip_address(match.group(1))
+                    cur_ip = str(ip)
+                except ValueError as e:
+                    msg = _("Found invalid IP address {ip!r} in leases file: {err}").format(
+                        ip=match.group(1), err=e)
+                    LOG.error(msg)
+                continue
+
+            match = re_mac.match(line)
+            if match:
+                found_mac = match.group(1).lower()
+                if cur_ip:
+                    assigments[found_mac] = cur_ip
+                continue
+
+        if self.verbose > 2:
+            LOG.debug(_("Found DHCP IP assignments:") + "\n" + pp(assigments))
+        if mac in assigments:
+            return assigments[mac]
+        return None
+
+    # -------------------------------------------------------------------------
+    def get_dhcp_ips(self, mac_address):
+
+        mac = mac_address.lower()
+        LOG.debug(_("Trying to get IP of MAC address {!r} given by DHCP ...").format(mac))
+        all_leases = self.get_remote_filecontent(self.dhcpd_leases_file)
+
+        ips = []
+
+        cur_ip = None
+        assigments = {}
+        re_lease_start = re.compile(r'^\s*lease\s+((?:\d{1,3}\.){3}\d{1,3})\s+', re.IGNORECASE)
+        re_mac = re.compile(
+            r'^\s*hardware\s+ethernet\s+((?:[0-9a-f]{2}:){5}[0-9a-f]{2})\s*;', re.IGNORECASE)
+
+        for line in all_leases.splitlines():
+            match = re_lease_start.match(line)
+            if match:
+                try:
+                    ip = ipaddress.ip_address(match.group(1))
+                    cur_ip = str(ip)
+                except ValueError as e:
+                    msg = _("Found invalid IP address {ip!r} in leases file: {err}").format(
+                        ip=match.group(1), err=e)
+                    LOG.error(msg)
+                continue
+
+            match = re_mac.match(line)
+            if match:
+                found_mac = match.group(1).lower()
+                if cur_ip:
+                    assigments[cur_ip] = found_mac
+                continue
+
+        for ip in assigments.keys():
+            found_mac = assigments[ip]
+            if mac == found_mac:
+                ips.append(ip)
+
+        if self.verbose > 2:
+            LOG.debug(_("Found DHCP IP assignments:") + "\n" + pp(assigments))
+
+        return ips
+
+    # -------------------------------------------------------------------------
+    def ensure_webroot(self):
+
+        docroot = self.cfg.cobbler_ws_docroot / self.cfg.cobbler_ws_rel_filesdir
+        webroot = docroot / self.cfg.system_status
+        desc = _("Webroot directory")
+        LOG.info(_("Ensuring existence of {what} {dir!r}...").format(
+            what=desc, dir=str(webroot)))
+
+        self.ensure_remote_directory(webroot, desc)
+
+
+# =============================================================================
+if __name__ == "__main__":
+
+    pass
+
+# =============================================================================
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list