From: Frank Brehm Date: Fri, 22 Sep 2023 08:20:58 +0000 (+0200) Subject: Moving lib/cr_vmware_tpl/cobbler.py to lib/cr_vmware_tpl/cobbler/__init__.py X-Git-Tag: 3.0.0^2~29 X-Git-Url: https://git.uhu-banane.org/?a=commitdiff_plain;h=01ddb8cfb4caf2c2dd705808ecd19c77c364b30d;p=pixelpark%2Fcreate-vmware-tpl.git Moving lib/cr_vmware_tpl/cobbler.py to lib/cr_vmware_tpl/cobbler/__init__.py --- diff --git a/lib/cr_vmware_tpl/cobbler.py b/lib/cr_vmware_tpl/cobbler.py deleted file mode 100644 index 72d03c9..0000000 --- a/lib/cr_vmware_tpl/cobbler.py +++ /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 index 0000000..031f5d6 --- /dev/null +++ b/lib/cr_vmware_tpl/cobbler/__init__.py @@ -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