+++ /dev/null
-#!/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
--- /dev/null
+#!/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