From: Frank Brehm Date: Fri, 22 Sep 2023 10:15:40 +0000 (+0200) Subject: Separating definition of LdapConnectionInfo and LdapConnectionDict into a separate... X-Git-Tag: 3.0.0^2~22 X-Git-Url: https://git.uhu-banane.net/?a=commitdiff_plain;h=d220a4a003458f30b70c29a8bba305cbe5ba4ca4;p=pixelpark%2Fcreate-vmware-tpl.git Separating definition of LdapConnectionInfo and LdapConnectionDict into a separate module --- diff --git a/lib/cr_vmware_tpl/__init__.py b/lib/cr_vmware_tpl/__init__.py index 7e25320..735a8dc 100644 --- a/lib/cr_vmware_tpl/__init__.py +++ b/lib/cr_vmware_tpl/__init__.py @@ -3,11 +3,22 @@ import time -__version__ = '2.8.0' +__version__ = '2.8.1' DEFAULT_CONFIG_DIR = 'pixelpark' DEFAULT_DISTRO_ARCH = 'x86_64' MAX_PORT_NUMBER = (2 ** 16) - 1 +DEFAULT_PORT_LDAP = 389 +DEFAULT_PORT_LDAPS = 636 +DEFAULT_TIMEOUT = 20 +MAX_TIMEOUT = 3600 + +DEFAULT_LDAP_ADMIN_FILTER = ( + '(&(inetuserstatus=active)(mailuserstatus=active)(objectclass=pppixelaccount)(mail=*)' + '(sshPublicKey=*)' + '(memberOf=cn=Administratoren Pixelpark Berlin,ou=Groups,o=Pixelpark,o=isp))' +) + # ------------------------------------------------------------------------- def print_section_start(name, header=None, collapsed=False): diff --git a/lib/cr_vmware_tpl/config.py b/lib/cr_vmware_tpl/config.py deleted file mode 100644 index 53d27d3..0000000 --- a/lib/cr_vmware_tpl/config.py +++ /dev/null @@ -1,1704 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -@author: Frank Brehm -@contact: frank.brehm@pixelpark.com -@copyright: © 2018 by Frank Brehm, Berlin -@summary: A module for providing a configuration -""" -from __future__ import absolute_import - -# Standard module -import logging -import re -import copy -import crypt -import os -import ipaddress - -# Third party modules -from pathlib import Path - -# Own modules -from fb_tools.common import is_sequence, pp, to_bool -from fb_tools.obj import FbGenericBaseObject, FbBaseObject -from fb_tools.collections import CIStringSet -from fb_tools.multi_config import BaseMultiConfig -from fb_tools.multi_config import DEFAULT_ENCODING -from fb_tools.xlate import format_list - -from fb_vmware.config import VSPhereConfigInfo - -from . import DEFAULT_CONFIG_DIR, DEFAULT_DISTRO_ARCH, MAX_PORT_NUMBER - -from .errors import CrTplConfigError - -from .xlate import XLATOR - -__version__ = '2.3.3' -LOG = logging.getLogger(__name__) - -_ = XLATOR.gettext -ngettext = XLATOR.ngettext - -DEFAULT_PORT_LDAP = 389 -DEFAULT_PORT_LDAPS = 636 -DEFAULT_TIMEOUT = 20 -MAX_TIMEOUT = 3600 -DEFAULT_ADMIN_FILTER = ( - '(&(inetuserstatus=active)(mailuserstatus=active)(objectclass=pppixelaccount)(mail=*)' - '(sshPublicKey=*)' - '(memberOf=cn=Administratoren Pixelpark Berlin,ou=Groups,o=Pixelpark,o=isp))' -) - - -# ============================================================================= -class LdapConnectionInfo(FbBaseObject): - """Encapsulating all necessary data to connect to a LDAP server.""" - - re_host_key = re.compile(r'^\s*(?:host|server)\s*$', re.IGNORECASE) - re_ldaps_key = re.compile(r'^\s*(?:use[_-]?)?(?:ldaps|ssl)\s*$', re.IGNORECASE) - re_port_key = re.compile(r'^\s*port\s*$', re.IGNORECASE) - re_base_dn_key = re.compile(r'^\s*base[_-]*dn\s*$', re.IGNORECASE) - re_bind_dn_key = re.compile(r'^\s*bind[_-]*dn\s*$', re.IGNORECASE) - re_bind_pw_key = re.compile(r'^\s*bind[_-]*pw\s*$', re.IGNORECASE) - re_admin_filter_key = re.compile(r'^\s*(?:admin[_-]?)?filter\s*$', re.IGNORECASE) - - # ------------------------------------------------------------------------- - def __init__( - self, appname=None, verbose=0, version=__version__, base_dir=None, - host=None, use_ldaps=False, port=DEFAULT_PORT_LDAP, base_dn=None, - bind_dn=None, bind_pw=None, admin_filter=None, initialized=False): - - self._host = None - self._use_ldaps = False - self._port = DEFAULT_PORT_LDAP - self._base_dn = None - self._bind_dn = None - self._bind_pw = None - self._admin_filter = DEFAULT_ADMIN_FILTER - - super(LdapConnectionInfo, self).__init__( - appname=appname, verbose=verbose, version=version, base_dir=base_dir, - initialized=False) - - self.host = host - self.use_ldaps = use_ldaps - self.port = port - if base_dn: - self.base_dn = base_dn - self.bind_dn = bind_dn - self.bind_pw = bind_pw - if admin_filter: - self.admin_filter = admin_filter - - if initialized: - self.initialized = True - - # ------------------------------------------------------------------------- - def as_dict(self, short=True): - """ - Transforms the elements of the object into a dict - - @param short: don't include local properties in resulting dict. - @type short: bool - - @return: structure as dict - @rtype: dict - """ - - res = super(LdapConnectionInfo, self).as_dict(short=short) - - res['host'] = self.host - res['use_ldaps'] = self.use_ldaps - res['port'] = self.port - res['base_dn'] = self.base_dn - res['bind_dn'] = self.bind_dn - res['bind_pw'] = None - res['schema'] = self.schema - res['url'] = self.url - res['admin_filter'] = self.admin_filter - - if self.bind_pw: - if self.verbose > 4: - res['bind_pw'] = self.bind_pw - else: - res['bind_pw'] = '******' - - return res - - # ----------------------------------------------------------- - @property - def host(self): - """The host name (or IP address) of the LDAP server.""" - return self._host - - @host.setter - def host(self, value): - if value is None or str(value).strip() == '': - self._host = None - return - self._host = str(value).strip().lower() - - # ----------------------------------------------------------- - @property - def use_ldaps(self): - """Should there be used LDAPS for communicating with the LDAP server?""" - return self._use_ldaps - - @use_ldaps.setter - def use_ldaps(self, value): - self._use_ldaps = to_bool(value) - - # ----------------------------------------------------------- - @property - def port(self): - "The TCP port number of the LDAP server." - return self._port - - @port.setter - def port(self, value): - v = int(value) - if v < 1 or v > MAX_PORT_NUMBER: - raise CrTplConfigError(_("Invalid port {!r} for LDAP server given.").format(value)) - self._port = v - - # ----------------------------------------------------------- - @property - def base_dn(self): - """The DN used to connect to the LDAP server, anonymous bind is used, if - this DN is empty or None.""" - return self._base_dn - - @base_dn.setter - def base_dn(self, value): - if value is None or str(value).strip() == '': - msg = _("An empty Base DN for LDAP searches is not allowed.") - raise CrTplConfigError(msg) - self._base_dn = str(value).strip() - - # ----------------------------------------------------------- - @property - def bind_dn(self): - """The DN used to connect to the LDAP server, anonymous bind is used, if - this DN is empty or None.""" - return self._bind_dn - - @bind_dn.setter - def bind_dn(self, value): - if value is None or str(value).strip() == '': - self._bind_dn = None - return - self._bind_dn = str(value).strip() - - # ----------------------------------------------------------- - @property - def bind_pw(self): - """The password of the DN used to connect to the LDAP server.""" - return self._bind_pw - - @bind_pw.setter - def bind_pw(self, value): - if value is None or str(value).strip() == '': - self._bind_pw = None - return - self._bind_pw = str(value).strip() - - # ----------------------------------------------------------- - @property - def admin_filter(self): - """The LDAP filter to get the list of administrators from LDAP.""" - return self._admin_filter - - @admin_filter.setter - def admin_filter(self, value): - if value is None or str(value).strip() == '': - self._admin_filter = None - return - self._admin_filter = str(value).strip() - - # ----------------------------------------------------------- - @property - def schema(self): - """The schema as part of the URL to connect to the LDAP server.""" - if self.use_ldaps: - return 'ldaps' - return 'ldap' - - # ----------------------------------------------------------- - @property - def url(self): - """The URL, which ca be used to connect to the LDAP server.""" - if not self.host: - return None - - port = '' - if self.use_ldaps: - if self.port != DEFAULT_PORT_LDAPS: - port = ':{}'.format(self.port) - else: - if self.port != DEFAULT_PORT_LDAP: - port = ':{}'.format(self.port) - - return '{s}://{h}{p}'.format(s=self.schema, h=self.host, p=port) - - # ------------------------------------------------------------------------- - def __repr__(self): - """Typecasting into a string for reproduction.""" - - out = "<%s(" % (self.__class__.__name__) - - fields = [] - fields.append("appname={!r}".format(self.appname)) - fields.append("host={!r}".format(self.host)) - fields.append("use_ldaps={!r}".format(self.use_ldaps)) - fields.append("port={!r}".format(self.port)) - fields.append("base_dn={!r}".format(self.base_dn)) - fields.append("bind_dn={!r}".format(self.bind_dn)) - fields.append("bind_pw={!r}".format(self.bind_pw)) - fields.append("admin_filter={!r}".format(self.admin_filter)) - fields.append("initialized={!r}".format(self.initialized)) - - out += ", ".join(fields) + ")>" - return out - - # ------------------------------------------------------------------------- - def __copy__(self): - - new = self.__class__( - appname=self.appname, verbose=self.verbose, base_dir=self.base_dir, host=self.host, - use_ldaps=self.use_ldaps, port=self.port, base_dn=self.base_dn, bind_dn=self.bind_dn, - bind_pw=self.bind_pw, admin_filter=self.admin_filter, initialized=self.initialized) - - return new - - # ------------------------------------------------------------------------- - @classmethod - def init_from_config(cls, name, data, appname=None, verbose=0, base_dir=None): - - new = cls(appname=appname, verbose=verbose, base_dir=base_dir) - - s_name = "ldap:" + name - msg_invalid = _("Invalid value {val!r} in section {sec!r} for a LDAP {what}.") - - for key in data.keys(): - value = data[key] - - if cls.re_host_key.match(key): - if value.strip(): - new.host = value - else: - msg = msg_invalid.format(val=value, sec=s_name, what='host') - LOG.error(msg) - continue - - if cls.re_ldaps_key.match(key): - new.use_ldaps = value - continue - - if cls.re_port_key.match(key): - port = DEFAULT_PORT_LDAP - try: - port = int(value) - except (ValueError, TypeError) as e: - msg = msg_invalid.format(val=value, sec=s_name, what='port') - msg += ' ' + str(e) - LOG.error(msg) - continue - if port <= 0 or port > MAX_PORT_NUMBER: - msg = msg_invalid.format(val=value, sec=s_name, what='port') - LOG.error(msg) - continue - new.port = port - continue - - if cls.re_base_dn_key.match(key): - if value.strip(): - new.base_dn = value - else: - msg = msg_invalid.format(val=value, sec=s_name, what='base_dn') - LOG.error(msg) - continue - - if cls.re_bind_dn_key.match(key): - new.bind_dn = value - continue - - if cls.re_bind_pw_key.match(key): - new.bind_pw = value - continue - - if cls.re_admin_filter_key.match(key): - new.admin_filter = value - continue - - if key.lower() in ['is_admin', 'readonly', 'tier', 'sync-source']: - continue - - msg = _("Unknown LDAP configuration key {key} found in section {sec!r}.").format( - key=key, sec=s_name) - LOG.error(msg) - - new.initialized = True - - return new - - -# ============================================================================= -class CobblerDistroInfo(FbGenericBaseObject): - """Class for encapsulation all necessary data of a Repo definition in Cobbler.""" - - re_dashes = re.compile(r'[_-]') - valid_arch = ( - 'i386', 'x86_64', 'ia64', 'ppc', 'ppc64', 'ppc64el', 'ppc64le', - 's390', 's390x', 'arm', 'aarch64') - - # ------------------------------------------------------------------------- - def __init__( - self, name, shortname=None, distro=None, description=None, arch=DEFAULT_DISTRO_ARCH, - ks_repo_url=None, packages=None, repos=None, snippets=None): - - self._name = None - self._shortname = None - self._distro = None - self._description = None - self._arch = DEFAULT_DISTRO_ARCH - self._ks_repo_url = None - self._is_rhel = False - self._ks_template = 'el8-standard.ks' - self.packages = [] - self.repos = CIStringSet() - self.snippets = CIStringSet() - - self.name = name - self.shortname = shortname - self.distro = distro - self.description = description - self.arch = arch - - if packages: - if is_sequence(packages): - for pkg in packages: - if pkg not in packages: - self.packages.append(pkg) - else: - msg = _("The given parameter {p!r} must be sequential type (given: {v!r}).") - raise TypeError(msg.format(p='packages', v=repos)) - - if repos: - if is_sequence(repos): - for repo in repos: - self.repos.add(repo) - else: - msg = _("The given parameter {p!r} must be sequential type (given: {v!r}).") - raise TypeError(msg.format(p='repos', v=repos)) - - if snippets: - if is_sequence(repos): - self.snippets = copy.copy(snippets) - for snippet in snippets: - self.snippets.add(snippet) - else: - msg = _("The given parameter {p!r} must be sequential type (given: {v!r}).") - raise TypeError(msg.format(p='snippets', v=snippets)) - - # ------------------------------------------------------------------------- - @property - def name(self): - """The canonical name of the distribution.""" - return getattr(self, '_name', None) - - @name.setter - def name(self, value): - - name = value.strip() - if name == '': - msg = _("The name of a Cobbler distro may not be empty.") - raise ValueError(msg) - - self._name = name - - # ------------------------------------------------------------------------- - @property - def shortname(self): - """The short name of the distro, how to be used e.g. as part of the template name.""" - - shortname = getattr(self, '_shortname', None) - if shortname is None: - name = self.name - if name is None: - return None - shortname = self.re_dashes.sub('', name) - return shortname - - @shortname.setter - def shortname(self, value): - if value is None: - self._shortname = None - return - shortname = value.strip() - if shortname == '': - self._shortname = None - return - self._shortname = shortname - - # ------------------------------------------------------------------------- - @property - def distro(self): - """The name of the underlaying (real) cobbler distro.""" - return getattr(self, '_distro', None) - - @distro.setter - def distro(self, value): - if value is None: - self._distro = None - return - - dis = value.strip() - if dis == '': - self._distro = None - return - - self._distro = dis - - # ------------------------------------------------------------------------- - @property - def arch(self): - """The name of the underlaying (real) cobbler distro.""" - return getattr(self, '_arch', DEFAULT_DISTRO_ARCH) - - @arch.setter - def arch(self, value): - - arch = value.strip().lower() - if arch not in self.valid_arch: - msg = _( - "Invalid architecture {a!r} for distro {n!r} given. Valid architectures are {v}.") - msg = msg.format(a=value, n=self.name, v=format_list(self.valid_arch, do_repr=True)) - raise ValueError(msg) - - self._arch = arch - - # ------------------------------------------------------------------------- - @property - def description(self): - """The name of the underlaying (real) cobbler distro.""" - return getattr(self, '_description', None) - - @description.setter - def description(self, value): - if value is None: - self._description = None - return - - desc = value.strip() - if desc == '': - self._description = None - return - - self._description = desc - - # ------------------------------------------------------------------------- - @property - def ks_repo_url(self): - """The URL for the base installation repository.""" - return getattr(self, '_ks_repo_url', None) - - @ks_repo_url.setter - def ks_repo_url(self, value): - if value is None: - self._ks_repo_url = None - return - - ks_repo_url = value.strip() - if ks_repo_url == '': - self._ks_repo_url = None - return - - self._ks_repo_url = ks_repo_url - - # ------------------------------------------------------------------------- - @property - def is_rhel(self): - """Is the currwnt distro a RHEL distro?""" - return self._is_rhel - - @is_rhel.setter - def is_rhel(self, value): - self._is_rhel = to_bool(value) - - # ------------------------------------------------------------------------- - @property - def ks_template(self): - """The filename below templates for generating the final kickstart file.""" - - return getattr(self, '_ks_template', 'el8-standard.ks') - - @ks_template.setter - def ks_template(self, value): - if value is None: - return - template = value.strip() - if template == '': - return - self._ks_template = template - - # ------------------------------------------------------------------------- - @property - def repos_string(self): - """Returns all repos as a string of their space concatinated names.""" - if self.repos: - return ' '.join(self.repos.as_list()) - return '' - - # ------------------------------------------------------------------------- - def __repr__(self): - """Typecasting into a string for reproduction.""" - - out = "<%s()" % (self.__class__.__name__) - - fields = [] - fields.append("name={!r}".format(self.name)) - fields.append("shortname={!r}".format(self._shortname)) - fields.append("distro={!r}".format(self.distro)) - fields.append("arch={!r}".format(self.arch)) - fields.append("is_rhel={!r}".format(self.is_rhel)) - fields.append("ks_template={!r}".format(self.ks_template)) - fields.append("description={!r}".format(self.description)) - fields.append("ks_repo_url={!r}".format(self.ks_repo_url)) - - out += ", ".join(fields) + ")>" - - return out - - # ------------------------------------------------------------------------- - def as_dict(self, short=True): - """ - Transforms the elements of the object into a dict - - @param short: don't include local properties in resulting dict. - @type short: bool - - @return: structure as dict - @rtype: dict - """ - - res = super(CobblerDistroInfo, self).as_dict(short=short) - res['arch'] = self.arch - res['description'] = self.description - res['distro'] = self.distro - res['is_rhel'] = self.is_rhel - res['ks_repo_url'] = self.ks_repo_url - res['ks_template'] = self.ks_template - res['name'] = self.name - res['repos_string'] = self.repos_string - res['shortname'] = self.shortname - - return res - - # ------------------------------------------------------------------------- - def __eq__(self, other): - - if not isinstance(other, CobblerDistroInfo): - return False - - return self.name == other.name - - # ------------------------------------------------------------------------- - def __copy__(self): - - new = self.__class__( - self.name, shortname=self.shortname, distro=self.distro, arch=self.arch, - ks_repo_url=self.ks_repo_url, description=self.description) - - for package in self.packages: - new.packages.append(package) - - for repo in self.repos: - new.repos.add(repo) - - for snippet in self.snippets: - new.snippets.add(snippet) - - return new - - # ------------------------------------------------------------------------- - @classmethod - def init_from_config(cls, name, data, verbose=0): - - new = cls(name) - - for key in data.keys(): - value = data[key] - - if key.lower() == 'shortname' and value.strip() != '': - new.shortname = value.strip() - continue - - if key.lower() == 'distro' and value.strip() != '': - new.distro = value.strip() - continue - - if key.lower() == 'description' and value.strip() != '': - new.description = value.strip() - continue - - if key.lower() == 'arch' and value.strip() != '': - new.arch = value.strip() - continue - - if key.lower() == 'is_rhel': - new.is_rhel = value - continue - - if key.lower() == 'ks_repo_url' and value.strip() != '': - new.ks_repo_url = value.strip() - continue - - if key.lower() == 'ks_template' and value.strip() != '': - new._ks_template = value.strip() - continue - - if key.lower() == 'repos': - cls._update_repos(new, value) - continue - - if key.lower() == 'packages': - cls._update_packages(new, value) - continue - - if key.lower() == 'snippets': - cls._update_snippets(new, value) - continue - - if verbose: - LOG.warn(_( - "Found unknown config parameter {p!r} with value {v!r} in configuration " - "of the Cobbler repository {r!r}.").format(p=key, v=value, r=name)) - - if verbose > 2: - msg = _("Found Cobbler repository configuration:") + '\n' + pp(new.as_dict()) - LOG.debug(msg) - - return new - - # ------------------------------------------------------------------------- - @classmethod - def _update_repos(cls, new, value): - - if is_sequence(value): - for repo in value: - repo = repo.strip() - if repo != '': - new.repos.add(repo) - elif value.strip() != '': - new.repos.add(value.strip()) - - # ------------------------------------------------------------------------- - @classmethod - def _update_packages(cls, new, value): - - if is_sequence(value): - for pkg in value: - pkg = pkg.strip() - if pkg != '' and pkg not in new.packages: - new.packages.append(pkg) - elif value.strip() != '': - new.packages.add(value.strip()) - - # ------------------------------------------------------------------------- - @classmethod - def _update_snippets(cls, new, value): - - if is_sequence(value): - for snippet in value: - snippet = snippet.strip() - if snippet != '': - new.snippets.add(snippet) - elif value.strip() != '': - new.snippets.add(value.strip()) - - -# ============================================================================= -class LdapConnectionDict(dict, FbGenericBaseObject): - """A dictionary containing LdapConnectionInfo as values and their names as keys.""" - - # ------------------------------------------------------------------------- - def as_dict(self, short=True): - """ - Transforms the elements of the object into a dict - - @param short: don't include local properties in resulting dict. - @type short: bool - - @return: structure as dict - @rtype: dict - """ - - res = super(LdapConnectionDict, self).as_dict(short=short) - - for key in self.keys(): - res[key] = self[key].as_dict(short=short) - - return res - - # ------------------------------------------------------------------------- - def __copy__(self): - - new = self.__class__() - - for key in self.keys(): - new[key] = copy.copy(self[key]) - - return new - - -# ============================================================================= -class CrTplConfiguration(BaseMultiConfig): - """ - A class for providing a configuration for the CrTplApplication class - and methods to read it from configuration files. - """ - - default_os_id = 'centos-stream-8' - - default_vsphere_host = 'vcs01.ppbrln.internal' - default_vsphere_port = 443 - default_vsphere_user = 'root' - default_vsphere_cluster = 'vmcc-l105-01' - default_dc = 'vmcc' - default_folder = 'templates' - default_template_name = default_os_id + '-template' - default_data_size_gb = 32.0 - default_storage_cluster = 'ds-cluster-hdd-vmcc-l105-01' - default_num_cpus = 2 - default_ram_mb = 4 * 1024 - default_network = '192.168.88.0_23' - default_max_wait_for_general = 15 - default_max_wait_for_shutdown = 600 - default_max_wait_for_finish_install = 60 * 60 - default_max_nr_templates_stay = 4 - default_vmware_cfg_version = 'vmx-15' - default_os_version = 'centos8_64Guest' - min_max_wait_for_finish_general = 2 - min_max_wait_for_finish_install = 3 * 60 - max_max_wait_for_finish_general = 60 * 60 - max_max_wait_for_finish_install = 24 * 60 * 60 - limit_max_nr_templates_stay = 100 - default_root_password = 'testtest' - - default_tpl_vm_domain = 'pixelpark.com' - - default_cobbler_bin = '/bin/cobbler' - default_cobbler_host = 'cobbler.pixelpark.com' - default_cobbler_ssh_port = 22 - default_cobbler_ssh_user = 'root' - default_cobbler_ssh_timeout = 30 - default_cobbler_distro = 'CentOS-8.2-x86_64' - default_cobbler_rootdir = Path('/var/lib/cobbler') - default_cobbler_profile_repos = ['pp-centos8-baseos'] - default_cobbler_nameservers = [ - '93.188.104.82', - '93.188.109.12', - '217.66.52.10', - ] - default_cobbler_dns_search = [ - 'pixelpark.net', - 'pixelpark.com', - 'pixelpark.de', - ] - resolv_conf = Path('/etc/resolv.conf') - evaluated_resolv_conf = False - - default_cobbler_ws_base_url = 'http://cobbler.pixelpark.com' - default_cobbler_ws_docroot = Path('/var/www/html') - default_cobbler_ws_rel_filesdir = Path('custom/vmware-template-files') - - default_cobbler2_templates_dir_rel = 'kickstarts' - default_cobbler2_collections_dir_rel = 'config' - default_cobbler2_profiles_dir_rel = 'profiles.d' - - default_cobbler3_templates_dir_rel = 'templates' - default_cobbler3_collections_dir_rel = 'collections' - default_cobbler3_profiles_dir_rel = 'profiles' - - ssh_privkey = 'id_rsa_cr_vmw_tpl' - - mac_address_template = "00:16:3e:53:{:02x}:{:02x}" - - method_list = {} - for method in crypt.methods: - mname = method.name.lower() - method_list[mname] = method - - valid_system_status = ('development', 'testing', 'acceptance', 'production') - default_system_status = 'development' - - default_cobbler_profile = 'vmware-template.' + default_os_id + '.' + default_system_status - - default_swap_size_mb = 512 - - default_ldap_server = 'prd-ds.pixelpark.com' - use_ssl_on_default = True - default_ldap_port = DEFAULT_PORT_LDAPS - default_base_dn = 'o=isp' - default_bind_dn = 'uid=readonly,ou=People,o=isp' - - re_ldap_section_w_name = re.compile(r'^\s*ldap\s*:\s*(\S+)') - re_resolv_ns_entry = re.compile(r'^\s*nameserver\s+(\S+)') - - # ------------------------------------------------------------------------- - def __init__( - self, appname=None, verbose=0, version=__version__, base_dir=None, - append_appname_to_stems=True, additional_stems=None, config_dir=DEFAULT_CONFIG_DIR, - additional_config_file=None, additional_cfgdirs=None, encoding=DEFAULT_ENCODING, - ensure_privacy=False, use_chardet=True, initialized=False): - - self.eval_resolv_conf() - - add_stems = [] - if additional_stems: - if is_sequence(additional_stems): - for stem in additional_stems: - add_stems.append(stem) - else: - add_stems.append(additional_stems) - - if 'mail' not in add_stems: - add_stems.append('mail') - if 'cobbler-repos' not in add_stems: - add_stems.append('cobbler-distros') - if 'ldap' not in add_stems: - add_stems.append('ldap') - - self.os_id = self.default_os_id - - self.vsphere_cluster = self.default_vsphere_cluster - self.folder = self.default_folder - self.template_name = self.default_template_name - self.data_size_gb = self.default_data_size_gb - self.num_cpus = self.default_num_cpus - self.ram_mb = self.default_ram_mb - self.swap_size_mb = self.default_swap_size_mb - self.network = self.default_network - self.max_wait_for_general = self.default_max_wait_for_general - self.max_wait_for_create_vm = None - self.max_wait_for_poweron_vm = None - self.max_wait_for_shutdown_vm = self.default_max_wait_for_shutdown - self.max_wait_for_purge_vm = None - self.max_wait_for_finish_install = self.default_max_wait_for_finish_install - self.max_nr_templates_stay = self.default_max_nr_templates_stay - self.vmware_cfg_version = self.default_vmware_cfg_version - self.os_version = self.default_os_version - - self.storage_cluster = self.default_storage_cluster - - self.tpl_vm_domain = self.default_tpl_vm_domain - - self.cobbler_profile_given = False - self.template_name_given = False - - self._root_password = self.default_root_password - - self.private_ssh_key = None - - self._cobbler_major_version = 3 - - self.cobbler_bin = self.default_cobbler_bin - self.cobbler_distro = self.default_cobbler_distro - self.cobbler_host = self.default_cobbler_host - self.cobbler_ssh_port = self.default_cobbler_ssh_port - self.cobbler_ssh_user = self.default_cobbler_ssh_user - self.cobbler_ssh_timeout = self.default_cobbler_ssh_timeout - self.cobbler_rootdir = self.default_cobbler_rootdir - self.cobbler_profile = self.default_cobbler_profile - self.cobbler_profile_repos = copy.copy(self.default_cobbler_profile_repos) - self.cobbler_nameservers = copy.copy(self.default_cobbler_nameservers) - self.cobbler_dns_search = copy.copy(self.default_cobbler_dns_search) - self.cobbler_ws_base_url = self.default_cobbler_ws_base_url - self.cobbler_ws_docroot = self.default_cobbler_ws_docroot - self.cobbler_ws_rel_filesdir = self.default_cobbler_ws_rel_filesdir - self.cobbler_distros = {} - self.cobbler_repos = {} - - self.current_distro = None - - self.system_status = self.default_system_status - - self.excluded_datastores = [] - - if config_dir is None: - config_dir = DEFAULT_CONFIG_DIR - LOG.debug("Config dir: {!r}.".format(config_dir)) - - self.ldap_timeout = DEFAULT_TIMEOUT - - super(CrTplConfiguration, self).__init__( - appname=appname, verbose=verbose, version=version, base_dir=base_dir, - append_appname_to_stems=append_appname_to_stems, config_dir=config_dir, - additional_stems=add_stems, additional_config_file=additional_config_file, - additional_cfgdirs=additional_cfgdirs, encoding=encoding, use_chardet=use_chardet, - ensure_privacy=ensure_privacy, initialized=False, - ) - - self.vsphere_info = VSPhereConfigInfo( - host=self.default_vsphere_host, port=self.default_vsphere_port, dc=self.default_dc, - use_https=True, user=self.default_vsphere_user, - appname=self.appname, verbose=self.verbose, base_dir=self.base_dir, - initialized=True) - - self.private_ssh_key = str(self.base_dir.joinpath('keys', self.ssh_privkey)) - - self.ldap_connection = LdapConnectionDict() - - default_connection = LdapConnectionInfo( - appname=self.appname, verbose=self.verbose, base_dir=self.base_dir, - host=self.default_ldap_server, use_ldaps=self.use_ssl_on_default, - port=self.default_ldap_port, base_dn=self.default_base_dn, - bind_dn=self.default_bind_dn, initialized=False) - - self.ldap_connection['default'] = default_connection - - if initialized: - self.initialized = True - - # ------------------------------------------------------------------------- - @property - def cobbler_major_version(self): - """The major version of the used cobbler installation. Valid values are 2 and 3.""" - return self._cobbler_major_version - - @cobbler_major_version.setter - def cobbler_major_version(self, value): - if value not in (2, 3): - msg = _("Unsupported version {ver!r} of {co}, valid versions of {co} are {valid}.") - msg = msg.format(ver=value, co='Cobbler', valid=format_list(['2', '3'])) - raise CrTplConfigError(msg) - - self._cobbler_major_version = value - - # ------------------------------------------------------------------------- - @property - def cobbler_collections_dir(self): - """The absolute pathname of all Cobbler collections.""" - if self.cobbler_major_version == 2: - return self.cobbler_rootdir / self.default_cobbler2_collections_dir_rel - return self.cobbler_rootdir / self.default_cobbler3_collections_dir_rel - - # ------------------------------------------------------------------------- - @property - def cobbler_ks_dir(self): - """The absolute pathname of the directory of the kickstart/template/autoinstall files.""" - if self.cobbler_major_version == 2: - return self.cobbler_rootdir / self.default_cobbler2_templates_dir_rel - return self.cobbler_rootdir / self.default_cobbler3_templates_dir_rel - - # ------------------------------------------------------------------------- - @property - def cobbler_profile_dir(self): - """The absolute pathname of the directory of Cobbler profile configuration files.""" - if self.cobbler_major_version == 2: - return self.cobbler_collections_dir / self.default_cobbler2_profiles_dir_rel - return self.cobbler_collections_dir / self.default_cobbler3_profiles_dir_rel - - # ------------------------------------------------------------------------- - @property - def cobbler_profile_ks(self): - """The absolute pathname of the profile kickstart file.""" - return self.cobbler_ks_dir / ('profile.' + self.cobbler_profile + '.ks') - - # ------------------------------------------------------------------------- - @property - def data_size_mb(self): - """Size of template volume in MiB.""" - return int(self.data_size_gb * 1024.0) - - # ------------------------------------------------------------------------- - @property - def data_size_kb(self): - """Size of template volume in KiB.""" - return self.data_size_mb * 1024 - - # ------------------------------------------------------------------------- - @property - def data_size(self): - """Size of template volume in Bytes.""" - return self.data_size_mb * 1024 * 1024 - - # ------------------------------------------------------------------------- - @property - def ram_gb(self): - """Size of RAM in GiB.""" - return float(self.ram_mb) / 1024 - - # ------------------------------------------------------------------------- - @property - def root_password(self): - """The root password of the VM to create.""" - return self._root_password - - # ------------------------------------------------------------------------- - @property - def system_ks(self): - """The path to the system kickstart file.""" - ks_base = 'template-' + self.os_id + '-' + self.system_status + '.ks' - return self.cobbler_ks_dir / ks_base - - # ------------------------------------------------------------------------- - @property - def snippets_dir(self): - """The path to the snippets dirctory, depending of the system status.""" - return self.cobbler_rootdir / 'snippets' / 'per_status' / self.system_status - - # ------------------------------------------------------------------------- - def as_dict(self, short=True): - """ - Transforms the elements of the object into a dict - - @param short: don't include local properties in resulting dict. - @type short: bool - - @return: structure as dict - @rtype: dict - """ - - res = super(CrTplConfiguration, self).as_dict(short=short) - res['cobbler_major_version'] = self.cobbler_major_version - res['cobbler_collections_dir'] = self.cobbler_collections_dir - res['cobbler_ks_dir'] = self.cobbler_ks_dir - res['cobbler_profile_dir'] = self.cobbler_profile_dir - res['cobbler_profile_ks'] = self.cobbler_profile_ks - res['data_size_mb'] = self.data_size_mb - res['data_size_kb'] = self.data_size_kb - res['data_size'] = self.data_size - res['default_cobbler_nameservers'] = self.default_cobbler_nameservers - res['ram_gb'] = self.ram_gb - res['system_ks'] = self.system_ks - res['snippets_dir'] = self.snippets_dir - - res['cobbler_distros'] = {} - for distro in self.cobbler_distros.keys(): - res['cobbler_distros'][distro] = self.cobbler_distros[distro].as_dict(short=short) - - res['root_password'] = None - if self.root_password: - if self.verbose > 4: - res['root_password'] = self.root_password - else: - res['root_password'] = '********' - - return res - - # ------------------------------------------------------------------------- - def eval(self): - """Evaluating read configuration and storing them in object properties.""" - - super(CrTplConfiguration, self).eval() - - if self.verbose > 1: - LOG.debug(_("Checking for unconfigured options ...")) - - if self.max_wait_for_create_vm is None: - self.max_wait_for_create_vm = self.max_wait_for_general - if self.max_wait_for_poweron_vm is None: - self.max_wait_for_poweron_vm = self.max_wait_for_general - if self.max_wait_for_purge_vm is None: - self.max_wait_for_purge_vm = self.max_wait_for_general - if self.max_wait_for_shutdown_vm is None: - self.max_wait_for_shutdown_vm = self.max_wait_for_general - - if not self.template_name_given: - self.template_name = self.os_id + '-template' - - if not self.cobbler_profile_given: - self.cobbler_profile = 'vmware-template.' + self.os_id + '.' + self.system_status - - self.verify_cobbler_distros() - - if not self.template_name_given: - self.template_name = self.current_distro.shortname + '-template' - - # ------------------------------------------------------------------------- - @classmethod - def eval_resolv_conf(cls): - - if cls.evaluated_resolv_conf: - return True - - if not cls.resolv_conf.exists(): - LOG.error(_("File {!r} not found on current host.").format(str(cls.resolv_conf))) - cls.evaluated_resolv_conf = True - return False - - if not cls.resolv_conf.is_file(): - LOG.error(_("Path {!r} is not a regular file.").format(str(cls.resolv_conf))) - cls.evaluated_resolv_conf = True - return False - - if not os.access(cls.resolv_conf, os.R_OK): - LOG.error(_("File {!r} is not readable.").format(str(cls.resolv_conf))) - cls.evaluated_resolv_conf = True - return False - - LOG.info(_("Evaluating {!r} for nameservers.").format(str(cls.resolv_conf))) - - nameservers = [] - file_content = cls.resolv_conf.read_text() - lines = file_content.splitlines() - - for line in lines: - match = cls.re_resolv_ns_entry.match(line) - if match: - try: - ns_address = ipaddress.ip_address(match.group(1)) - nameservers.append(str(ns_address)) - except ValueError as e: - msg = _("Found invalid IP address {addr!r} as a nameserver in {file!r}:") - msg = msg.format(addr=match.group(1), file=str(cls.resolv_conf)) - msg += ' ' + str(e) - LOG.warn(msg) - - cls.evaluated_resolv_conf = True - msg = _("Found nameservers in {!r}:").format(str(cls.resolv_conf)) - msg += ' {}'.format(pp(nameservers)) - LOG.debug(msg) - - if len(nameservers): - cls.default_cobbler_nameservers = nameservers - return True - - return False - - # ------------------------------------------------------------------------- - def verify_cobbler_distros(self): - - LOG.debug(_("Verifying cobbler distros ...")) - - if not len(self.cobbler_distros): - msg = _("Did not found configured Cobbler distros.") - raise CrTplConfigError(msg) - - for distro_id in self.cobbler_distros.keys(): - distro = self.cobbler_distros[distro_id] - - if not distro.distro: - msg = _("Did not found distro of configured Cobbler distro {!r}.").format( - distro_id) - raise CrTplConfigError(msg) - - if not distro.ks_repo_url: - msg = _( - "Did not found the base install repo URL of configured Cobbler " - "distro {!r}.").format(distro_id) - raise CrTplConfigError(msg) - - if not len(distro.repos): - msg = _( - "Did not found repo definitions for configured Cobbler " - "distro {!r}.").format(distro_id) - LOG.warn(msg) - - if not distro.description: - distro.description = distro_id - - LOG.debug(_("Searching for distro with ID {!r} ...").format(self.os_id)) - - if self.os_id not in self.cobbler_distros: - msg = _("Did not found distro {!r} in configured Cobbler distros.").format(self.os_id) - raise CrTplConfigError(msg) - - self.current_distro = self.cobbler_distros[self.os_id] - self.cobbler_distro = self.current_distro.distro - - LOG.info(_("Using OS {os!r} with cobbler distro {di!r}.").format( - os=self.os_id, di=self.cobbler_distro)) - - # ------------------------------------------------------------------------- - def eval_section(self, section_name): - - re_cobbler_distros = re.compile(r'^\s*cobbler[_-]?distros\s*$', re.IGNORECASE) - re_cobbler_repos = re.compile(r'^\s*cobbler[_-]?repos\s*$', re.IGNORECASE) - - sn = section_name.lower() - section = self.cfg[section_name] - - LOG.debug(_("Evaluating section {!r} ...").format(section_name)) - if self.verbose > 2: - LOG.debug(_("Content of section:") + '\n' + pp(section)) - - super(CrTplConfiguration, self).eval_section(section_name) - - if sn == 'vsphere': - self._eval_config_vsphere(section_name, section) - return - - if sn == 'template': - self._eval_config_template(section_name, section) - return - - if sn == 'timeouts': - self._eval_config_timeouts(section_name, section) - return - - if sn == 'cobbler': - self._eval_config_cobbler(section_name, section) - return - - if re_cobbler_distros.match(section_name): - self._eval_cobbler_distros(section_name, section) - return - - if re_cobbler_repos.match(section_name): - self._eval_cobbler_repos(section_name, section) - return - - if sn == 'ldap': - for key in section.keys(): - sub = section[key] - if key.lower().strip() == 'timeout': - self._eval_ldap_timeout(sub) - continue - self._eval_ldap_connection(key, sub) - return - - match = self.re_ldap_section_w_name.match(sn) - if match: - connection_name = match.group(1) - self._eval_ldap_connection(connection_name, section) - return - - if self.verbose > 1: - LOG.debug(_("Unhandled configuration section {!r}.").format(section_name)) - - # ------------------------------------------------------------------------- - def _eval_ldap_timeout(self, value): - - timeout = DEFAULT_TIMEOUT - msg_invalid = _("Value {!r} for a timeout is invalid.") - - try: - timeout = int(value) - except (ValueError, TypeError) as e: - msg = msg_invalid.format(value) - msg += ': ' + str(e) - LOG.error(msg) - return - if timeout <= 0 or timeout > MAX_TIMEOUT: - msg = msg_invalid.format(value) - LOG.error(msg) - return - - self.ldap_timeout = timeout - - # ------------------------------------------------------------------------- - def _eval_ldap_connection(self, connection_name, section): - - connection = LdapConnectionInfo.init_from_config( - connection_name, section, - appname=self.appname, verbose=self.verbose, base_dir=self.base_dir, - ) - - self.ldap_connection[connection_name] = connection - - # ------------------------------------------------------------------------- - def _eval_config_vsphere(self, section_name, section): - - if self.verbose > 1: - LOG.debug(_("Checking config section {!r} ...").format(section_name)) - - re_excl_ds = re.compile(r'^\s*excluded?[-_]datastores?\s*$', re.IGNORECASE) - re_split_ds = re.compile(r'[,;\s]+') - re_storage_cluster = re.compile(r'^\s*storage[-_]?cluster\s*$', re.IGNORECASE) - - for key in section.keys(): - value = section[key] - - if key.lower() == 'host': - self.vsphere_info.host = value - continue - elif key.lower() == 'port': - self.vsphere_info.port = value - continue - elif key.lower() == 'user': - self.vsphere_info.user = value - continue - elif key.lower() == 'password': - self.vsphere_info.password = value - continue - elif key.lower() == 'cluster': - self.vsphere_cluster = value - continue - elif key.lower() == 'folder': - self.folder = value - elif key.lower() == 'dc': - self.vsphere_info.dc = value - - elif key.lower() == 'max_nr_templates_stay': - v = int(value) - if v < 1: - LOG.error(_( - "Value {val} for {p} is less than {minval}, using {default}.").format( - val=v, minval=1, p='max_nr_templates_stay', - default=self.default_max_nr_templates_stay)) - elif v >= 100: - LOG.error(_( - "Value {val} for {p} is greater than {maxval}, using {default}.").format( - val=v, maxval=100, p='max_nr_templates_stay', - default=self.default_max_nr_templates_stay)) - else: - self.max_nr_templates_stay = v - - elif re_excl_ds.search(key): - datastores = re_split_ds.split(value.strip()) - self.excluded_datastores = datastores - - elif re_storage_cluster.search(key): - cl_name = value.strip() - if cl_name: - self.storage_cluster = cl_name - else: - self.storage_cluster = None - - return - - # ------------------------------------------------------------------------- - def _eval_config_template(self, section_name, section): - - if self.verbose > 1: - LOG.debug(_("Checking config section {!r} ...").format(section_name)) - - re_os_id = re.compile(r'^\s*os[-_]?id\s*$', re.IGNORECASE) - re_os_id_subst = re.compile(r'[^a-z0-9_-]+', re.IGNORECASE) - re_vm_domain = re.compile(r'^\s*(?:vm[-_]?)?domain\s*$', re.IGNORECASE) - re_root_pwd = re.compile(r'^\s*root[-_]?password\s*$', re.IGNORECASE) - re_swap_space = re.compile(r'^\s*swap[-_]?space(?:[-_]?mb)?\s*$', re.IGNORECASE) - - for key in section.keys(): - value = section[key] - - if re_os_id.match(key): - v = value.strip().lower() - v = re_os_id_subst.sub('', v) - if v: - self.os_id = v - continue - if key.lower() == 'name': - self.template_name = value - self.template_name_given = True - continue - if re_vm_domain.match(key) and value.strip(): - self.tpl_vm_domain = value.strip().lower() - continue - if key.lower() == 'data_size_gb': - self.data_size_gb = float(value) - continue - if key.lower() == 'data_size_mb': - self.data_size_gb = float(value) / 1024.0 - continue - if key.lower() == 'data_size_kb': - self.data_size_gb = float(value) / 1024.0 / 1024.0 - continue - if key.lower() == 'data_size': - self.data_size_gb = float(value) / 1024.0 / 1024.0 / 1024.0 - continue - if key.lower() == 'num_cpus': - self.num_cpus = int(value) - continue - if key.lower() == 'ram_gb': - self.ram_mb = int(float(value) * 1024.0) - continue - if key.lower() == 'ram_mb': - self.ram_mb = int(value) - continue - if key.lower() == 'network': - self.network = value.strip() - continue - if key.lower() == 'vmware_cfg_version': - self.vmware_cfg_version = value.strip() - continue - if key.lower() == 'os_version': - self.os_version = value.strip() - continue - if re_root_pwd.match(key) and value.strip(): - self._root_password = value.strip() - continue - if re_swap_space.match(key) and value.strip(): - self.swap_size_mb = int(value) - continue - - return - - # ------------------------------------------------------------------------- - def _eval_timeout_value(self, prop_name, value, min_val, max_val, default_val): - - if self.verbose > 2: - LOG.debug(_("Checking value {v!r} for {p} ...").format(v=value, p=prop_name)) - if self.verbose > 3: - LOG.debug(_( - "Minimal value: {min_val}, maximum value: {max_val}, " - "default value: {def_val}.").format( - min_val=min_val, max_val=max_val, def_val=default_val)) - - v = float(value) - - if v < min_val: - msg = _( - "Value {val} for {prop} is less than {min_val}, " - "using {def_val} seconds.").format(val=v, min_val=min_val, def_val=default_val) - LOG.error(msg) - return - - if v < min_val: - msg = _( - "Value {val} for {prop} is greater than {max_val}, " - "using {def_val} seconds.").format(val=v, max_val=max_val, def_val=default_val) - LOG.error(msg) - return - - if self.verbose > 2: - msg = _("Setting timeout {p!r} to {v:0.1f} seconds.").format(p=prop_name, v=v) - LOG.debug(msg) - setattr(self, prop_name, v) - - # ------------------------------------------------------------------------- - def _eval_config_timeouts(self, section_name, section): - - if self.verbose > 1: - LOG.debug(_("Checking config section {!r} ...").format(section_name)) - - for key in section.keys(): - value = section[key] - - if key.lower() == 'max_wait_for_general': - self._eval_timeout_value( - prop_name='max_wait_for_general', value=value, - min_val=self.min_max_wait_for_finish_general, - max_val=self.max_max_wait_for_finish_general, - default_val=self.default_max_wait_for_general) - continue - - if key.lower() == 'max_wait_for_create_vm': - self._eval_timeout_value( - prop_name='max_wait_for_create_vm', value=value, - min_val=self.min_max_wait_for_finish_general, - max_val=self.max_max_wait_for_finish_general, - default_val=self.max_wait_for_general) - continue - elif key.lower() == 'max_wait_for_poweron_vm': - self._eval_timeout_value( - prop_name='max_wait_for_poweron_vm', value=value, - min_val=self.min_max_wait_for_finish_general, - max_val=self.max_max_wait_for_finish_general, - default_val=self.max_wait_for_general) - continue - elif key.lower() == 'max_wait_for_shutdown_vm': - self._eval_timeout_value( - prop_name='max_wait_for_shutdown_vm', value=value, - min_val=self.min_max_wait_for_finish_general, - max_val=self.max_max_wait_for_finish_general, - default_val=self.default_max_wait_for_shutdown) - continue - elif key.lower() == 'max_wait_for_purge_vm': - self._eval_timeout_value( - prop_name='max_wait_for_purge_vm', value=value, - min_val=self.min_max_wait_for_finish_general, - max_val=self.max_max_wait_for_finish_general, - default_val=self.max_wait_for_general) - continue - elif key.lower() == 'max_wait_for_finish_install': - self._eval_timeout_value( - prop_name='max_wait_for_finish_install', value=value, - min_val=self.min_max_wait_for_finish_install, - max_val=self.max_max_wait_for_finish_install, - default_val=self.default_max_wait_for_finish_install) - continue - - # ------------------------------------------------------------------------- - def _eval_config_cobbler(self, section_name, section): - - if self.verbose > 1: - LOG.debug(_("Checking config section {!r} ...").format(section_name)) - - re_port_key = re.compile(r'^\s*ssh[-_]?port\s*$', re.IGNORECASE) - re_user_key = re.compile(r'^\s*ssh[-_]?user\s*$', re.IGNORECASE) - re_timeout_key = re.compile(r'^\s*ssh[-_]?timeout\s*$', re.IGNORECASE) - re_rootdir_key = re.compile(r'^\s*root[-_]?dir(?:ectory)?\s*$', re.IGNORECASE) - re_split_values = re.compile(r'[,;\s]+') - re_pr_repos_key = re.compile(r'^\s*profile[-_]?repos?\s*$', re.IGNORECASE) - re_nameserver_key = re.compile(r'^\s*nameservers?\s*$', re.IGNORECASE) - re_dns_search_key = re.compile(r'^\s*dns[-_]?search\s*$', re.IGNORECASE) - re_ws_base_url_key = re.compile( - r'^\s*(?:ws|webserver)[-_]?base[-_]?url\s*$', re.IGNORECASE) - re_ws_docroot_key = re.compile( - r'^\s*(?:ws|webserver)[-_]?docroot\s*$', re.IGNORECASE) - re_ws_rel_filesdir_key = re.compile( - r'^\s*(?:ws|webserver)[-_]?rel(?:ative)?[-_]?filesdir\s*$', re.IGNORECASE) - re_system_status = re.compile(r'^\s*system[-_]?status\s*$', re.IGNORECASE) - re_cobbler_bin_key = re.compile(r'^\s*(?:cobbler[_-]?)?bin\s*$', re.IGNORECASE) - - for key in section.keys(): - value = section[key] - - if key.lower() == 'distro' and value.strip() != '': - self.cobbler_distro = value.strip() - continue - if key.lower() == 'host' and value.strip() != '': - self.cobbler_host = value.strip().lower() - continue - if re_port_key.match(key) and value.strip() != '': - self.cobbler_ssh_port = int(value) - continue - if re_user_key.match(key): - self.cobbler_ssh_user = value.strip().lower() - continue - if re_timeout_key.match(key): - self.cobbler_ssh_timeout = int(value) - continue - if re_cobbler_bin_key.match(key) and value.strip() != '': - self.cobbler_bin = value.strip() - continue - if re_rootdir_key.match(key): - dpath = Path(value) - if dpath.is_absolute(): - self.cobbler_rootdir = Path(value) - else: - msg = _("Path for {what} {path!r} is not absolute.").format( - what=_("Cobbler root directory"), path=str(dpath)) - LOG.error(msg) - continue - if key.lower() == 'profile' and value.strip() != '': - self.cobbler_profile = value.strip().lower() - self.cobbler_profile_given = True - continue - if re_pr_repos_key.match(key) and value.strip() != '': - self.cobbler_profile_repos = re_split_values.split(value.strip()) - continue - if re_nameserver_key.match(key) and value.strip() != '': - self.cobbler_nameservers = re_split_values.split(value.strip().lower()) - continue - if re_dns_search_key.match(key) and value.strip() != '': - self.cobbler_dns_search = re_split_values.split(value.strip().lower()) - continue - if re_ws_base_url_key.match(key) and value.strip() != '': - self.cobbler_ws_base_url = value.strip().lower() - continue - if re_ws_docroot_key.match(key) and value.strip() != '': - dpath = Path(value.strip()) - if dpath.is_absolute(): - self.cobbler_ws_docroot = dpath - else: - msg = _("Path for {what} {path!r} is not absolute.").format( - what=_("Webserver document root"), path=str(dpath)) - LOG.error(msg) - continue - if re_ws_rel_filesdir_key.match(key) and value.strip() != '': - self.cobbler_ws_rel_filesdir = Path(value.strip()) - continue - if re_system_status.match(key) and value.strip() != '': - val = value.strip().lower() - if val in self.valid_system_status: - self.system_status = val - else: - msg = _( - "The value of {what!r} must be one of {valid!r}, " - "but found {val!r}.").format( - what='system_status', - valid=self.valid_system_status, val=value) - LOG.error(msg) - continue - - # ------------------------------------------------------------------------- - def _eval_cobbler_distros(self, section_name, section): - - if self.verbose > 1: - LOG.debug(_("Checking config section {!r} ...").format(section_name)) - - for distro_id in section.keys(): - distro_info = section[distro_id] - distro_id = distro_id.lower() - distro = CobblerDistroInfo.init_from_config( - distro_id, distro_info, verbose=self.verbose) - self.cobbler_distros[distro_id] = distro - - # ------------------------------------------------------------------------- - def get_root_pwd_hash(self, method='sha256'): - """Generates a password hash based on the root password.""" - - m = method.lower() - if m not in self.method_list: - msg = _("Given method {!r} is not a valid crypt method.").format(method) - raise ValueError(msg) - - salt = crypt.mksalt(self.method_list[m]) - if self.verbose > 1: - pw_show = self.root_password - if self.verbose < 4: - pw_show = '********' - LOG.debug("Hashing password {pw!r} with salt {sa!r} (method {me!r})..".format( - pw=pw_show, sa=salt, me=m)) - pwd_hash = crypt.crypt(self.root_password, salt) - if self.verbose > 2: - LOG.debug(_("Hashed root password: {!r}").format(pwd_hash)) - - return pwd_hash - - # ------------------------------------------------------------------------- - def _eval_cobbler_repos(self, section_name, section): - - if self.verbose > 1: - LOG.debug(_("Checking config section {!r} ...").format(section_name)) - - for full_repo_name in section.keys(): - repo_data = section[full_repo_name] - if isinstance(repo_data, dict): - repo_info = {} - if self.verbose > 2: - LOG.debug(_("Found Cobbler repository {!r}.").format(full_repo_name)) - for key in repo_data.keys(): - value = repo_data[key] - if key.lower() == 'reponame' and value.strip() != '': - repo_info['reponame'] = value.strip() - continue - if key.lower() == 'filename' and value.strip() != '': - repo_info['filename'] = value.strip() - continue - self.cobbler_repos[full_repo_name] = repo_info - if self.verbose > 3: - LOG.debug(_("Evaluated Cobbler repositories:") + '\n' + pp(self.cobbler_repos)) - - # ------------------------------------------------------------------------- - def strip_unnessecary(self): - """Stripping no more necessary stuff.""" - LOG.debug(_("Stripping no more necessary stuff from configuration ...")) - - if self.verbose > 1: - LOG.debug(_("Stripping {!r} ...").format('cfg.cobbler-distros')) - if 'cobbler-distros' in self.cfg: - del self.cfg['cobbler-distros'] - - if self.verbose > 1: - LOG.debug(_("Stripping {!r} ...").format('cfg.cobbler-repos')) - if 'cobbler-repos' in self.cfg: - del self.cfg['cobbler-repos'] - - if self.verbose > 1: - LOG.debug(_("Stripping {!r} ...").format('cobbler_distros')) - self.cobbler_distros = {} - - if self.verbose > 1: - LOG.debug(_("Stripping {!r} ...").format('cobbler_repos')) - self.cobbler_repos = {} - - if self.verbose > 1: - LOG.debug(_("Stripping {!r} ...").format('configs_raw')) - self.configs_raw = {} - - -# ============================================================================= -if __name__ == "__main__": - - pass - -# ============================================================================= - -# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list diff --git a/lib/cr_vmware_tpl/config/__init__.py b/lib/cr_vmware_tpl/config/__init__.py new file mode 100644 index 0000000..c195dee --- /dev/null +++ b/lib/cr_vmware_tpl/config/__init__.py @@ -0,0 +1,1371 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@author: Frank Brehm +@contact: frank.brehm@pixelpark.com +@copyright: © 2018 by Frank Brehm, Berlin +@summary: A module for providing a configuration +""" +from __future__ import absolute_import + +# Standard module +import logging +import re +import copy +import crypt +import os +import ipaddress + +# Third party modules +from pathlib import Path + +from fb_tools.common import is_sequence, pp, to_bool +from fb_tools.obj import FbGenericBaseObject +from fb_tools.collections import CIStringSet +from fb_tools.multi_config import BaseMultiConfig +from fb_tools.multi_config import DEFAULT_ENCODING +from fb_tools.xlate import format_list + +from fb_vmware.config import VSPhereConfigInfo + +# Own modules +from .. import DEFAULT_CONFIG_DIR, DEFAULT_DISTRO_ARCH +from .. import DEFAULT_PORT_LDAPS, DEFAULT_TIMEOUT, MAX_TIMEOUT + +from ..errors import CrTplConfigError + +from ..xlate import XLATOR + +from .ldap import LdapConnectionInfo, LdapConnectionDict + +__version__ = '3.0.1' +LOG = logging.getLogger(__name__) + +_ = XLATOR.gettext +ngettext = XLATOR.ngettext + + +# ============================================================================= +class CobblerDistroInfo(FbGenericBaseObject): + """Class for encapsulation all necessary data of a Repo definition in Cobbler.""" + + re_dashes = re.compile(r'[_-]') + valid_arch = ( + 'i386', 'x86_64', 'ia64', 'ppc', 'ppc64', 'ppc64el', 'ppc64le', + 's390', 's390x', 'arm', 'aarch64') + + # ------------------------------------------------------------------------- + def __init__( + self, name, shortname=None, distro=None, description=None, arch=DEFAULT_DISTRO_ARCH, + ks_repo_url=None, packages=None, repos=None, snippets=None): + + self._name = None + self._shortname = None + self._distro = None + self._description = None + self._arch = DEFAULT_DISTRO_ARCH + self._ks_repo_url = None + self._is_rhel = False + self._ks_template = 'el8-standard.ks' + self.packages = [] + self.repos = CIStringSet() + self.snippets = CIStringSet() + + self.name = name + self.shortname = shortname + self.distro = distro + self.description = description + self.arch = arch + + if packages: + if is_sequence(packages): + for pkg in packages: + if pkg not in packages: + self.packages.append(pkg) + else: + msg = _("The given parameter {p!r} must be sequential type (given: {v!r}).") + raise TypeError(msg.format(p='packages', v=repos)) + + if repos: + if is_sequence(repos): + for repo in repos: + self.repos.add(repo) + else: + msg = _("The given parameter {p!r} must be sequential type (given: {v!r}).") + raise TypeError(msg.format(p='repos', v=repos)) + + if snippets: + if is_sequence(repos): + self.snippets = copy.copy(snippets) + for snippet in snippets: + self.snippets.add(snippet) + else: + msg = _("The given parameter {p!r} must be sequential type (given: {v!r}).") + raise TypeError(msg.format(p='snippets', v=snippets)) + + # ------------------------------------------------------------------------- + @property + def name(self): + """The canonical name of the distribution.""" + return getattr(self, '_name', None) + + @name.setter + def name(self, value): + + name = value.strip() + if name == '': + msg = _("The name of a Cobbler distro may not be empty.") + raise ValueError(msg) + + self._name = name + + # ------------------------------------------------------------------------- + @property + def shortname(self): + """The short name of the distro, how to be used e.g. as part of the template name.""" + + shortname = getattr(self, '_shortname', None) + if shortname is None: + name = self.name + if name is None: + return None + shortname = self.re_dashes.sub('', name) + return shortname + + @shortname.setter + def shortname(self, value): + if value is None: + self._shortname = None + return + shortname = value.strip() + if shortname == '': + self._shortname = None + return + self._shortname = shortname + + # ------------------------------------------------------------------------- + @property + def distro(self): + """The name of the underlaying (real) cobbler distro.""" + return getattr(self, '_distro', None) + + @distro.setter + def distro(self, value): + if value is None: + self._distro = None + return + + dis = value.strip() + if dis == '': + self._distro = None + return + + self._distro = dis + + # ------------------------------------------------------------------------- + @property + def arch(self): + """The name of the underlaying (real) cobbler distro.""" + return getattr(self, '_arch', DEFAULT_DISTRO_ARCH) + + @arch.setter + def arch(self, value): + + arch = value.strip().lower() + if arch not in self.valid_arch: + msg = _( + "Invalid architecture {a!r} for distro {n!r} given. Valid architectures are {v}.") + msg = msg.format(a=value, n=self.name, v=format_list(self.valid_arch, do_repr=True)) + raise ValueError(msg) + + self._arch = arch + + # ------------------------------------------------------------------------- + @property + def description(self): + """The name of the underlaying (real) cobbler distro.""" + return getattr(self, '_description', None) + + @description.setter + def description(self, value): + if value is None: + self._description = None + return + + desc = value.strip() + if desc == '': + self._description = None + return + + self._description = desc + + # ------------------------------------------------------------------------- + @property + def ks_repo_url(self): + """The URL for the base installation repository.""" + return getattr(self, '_ks_repo_url', None) + + @ks_repo_url.setter + def ks_repo_url(self, value): + if value is None: + self._ks_repo_url = None + return + + ks_repo_url = value.strip() + if ks_repo_url == '': + self._ks_repo_url = None + return + + self._ks_repo_url = ks_repo_url + + # ------------------------------------------------------------------------- + @property + def is_rhel(self): + """Is the currwnt distro a RHEL distro?""" + return self._is_rhel + + @is_rhel.setter + def is_rhel(self, value): + self._is_rhel = to_bool(value) + + # ------------------------------------------------------------------------- + @property + def ks_template(self): + """The filename below templates for generating the final kickstart file.""" + + return getattr(self, '_ks_template', 'el8-standard.ks') + + @ks_template.setter + def ks_template(self, value): + if value is None: + return + template = value.strip() + if template == '': + return + self._ks_template = template + + # ------------------------------------------------------------------------- + @property + def repos_string(self): + """Returns all repos as a string of their space concatinated names.""" + if self.repos: + return ' '.join(self.repos.as_list()) + return '' + + # ------------------------------------------------------------------------- + def __repr__(self): + """Typecasting into a string for reproduction.""" + + out = "<%s()" % (self.__class__.__name__) + + fields = [] + fields.append("name={!r}".format(self.name)) + fields.append("shortname={!r}".format(self._shortname)) + fields.append("distro={!r}".format(self.distro)) + fields.append("arch={!r}".format(self.arch)) + fields.append("is_rhel={!r}".format(self.is_rhel)) + fields.append("ks_template={!r}".format(self.ks_template)) + fields.append("description={!r}".format(self.description)) + fields.append("ks_repo_url={!r}".format(self.ks_repo_url)) + + out += ", ".join(fields) + ")>" + + return out + + # ------------------------------------------------------------------------- + def as_dict(self, short=True): + """ + Transforms the elements of the object into a dict + + @param short: don't include local properties in resulting dict. + @type short: bool + + @return: structure as dict + @rtype: dict + """ + + res = super(CobblerDistroInfo, self).as_dict(short=short) + res['arch'] = self.arch + res['description'] = self.description + res['distro'] = self.distro + res['is_rhel'] = self.is_rhel + res['ks_repo_url'] = self.ks_repo_url + res['ks_template'] = self.ks_template + res['name'] = self.name + res['repos_string'] = self.repos_string + res['shortname'] = self.shortname + + return res + + # ------------------------------------------------------------------------- + def __eq__(self, other): + + if not isinstance(other, CobblerDistroInfo): + return False + + return self.name == other.name + + # ------------------------------------------------------------------------- + def __copy__(self): + + new = self.__class__( + self.name, shortname=self.shortname, distro=self.distro, arch=self.arch, + ks_repo_url=self.ks_repo_url, description=self.description) + + for package in self.packages: + new.packages.append(package) + + for repo in self.repos: + new.repos.add(repo) + + for snippet in self.snippets: + new.snippets.add(snippet) + + return new + + # ------------------------------------------------------------------------- + @classmethod + def init_from_config(cls, name, data, verbose=0): + + new = cls(name) + + for key in data.keys(): + value = data[key] + + if key.lower() == 'shortname' and value.strip() != '': + new.shortname = value.strip() + continue + + if key.lower() == 'distro' and value.strip() != '': + new.distro = value.strip() + continue + + if key.lower() == 'description' and value.strip() != '': + new.description = value.strip() + continue + + if key.lower() == 'arch' and value.strip() != '': + new.arch = value.strip() + continue + + if key.lower() == 'is_rhel': + new.is_rhel = value + continue + + if key.lower() == 'ks_repo_url' and value.strip() != '': + new.ks_repo_url = value.strip() + continue + + if key.lower() == 'ks_template' and value.strip() != '': + new._ks_template = value.strip() + continue + + if key.lower() == 'repos': + cls._update_repos(new, value) + continue + + if key.lower() == 'packages': + cls._update_packages(new, value) + continue + + if key.lower() == 'snippets': + cls._update_snippets(new, value) + continue + + if verbose: + LOG.warn(_( + "Found unknown config parameter {p!r} with value {v!r} in configuration " + "of the Cobbler repository {r!r}.").format(p=key, v=value, r=name)) + + if verbose > 2: + msg = _("Found Cobbler repository configuration:") + '\n' + pp(new.as_dict()) + LOG.debug(msg) + + return new + + # ------------------------------------------------------------------------- + @classmethod + def _update_repos(cls, new, value): + + if is_sequence(value): + for repo in value: + repo = repo.strip() + if repo != '': + new.repos.add(repo) + elif value.strip() != '': + new.repos.add(value.strip()) + + # ------------------------------------------------------------------------- + @classmethod + def _update_packages(cls, new, value): + + if is_sequence(value): + for pkg in value: + pkg = pkg.strip() + if pkg != '' and pkg not in new.packages: + new.packages.append(pkg) + elif value.strip() != '': + new.packages.add(value.strip()) + + # ------------------------------------------------------------------------- + @classmethod + def _update_snippets(cls, new, value): + + if is_sequence(value): + for snippet in value: + snippet = snippet.strip() + if snippet != '': + new.snippets.add(snippet) + elif value.strip() != '': + new.snippets.add(value.strip()) + + +# ============================================================================= +class CrTplConfiguration(BaseMultiConfig): + """ + A class for providing a configuration for the CrTplApplication class + and methods to read it from configuration files. + """ + + default_os_id = 'centos-stream-8' + + default_vsphere_host = 'vcs01.ppbrln.internal' + default_vsphere_port = 443 + default_vsphere_user = 'root' + default_vsphere_cluster = 'vmcc-l105-01' + default_dc = 'vmcc' + default_folder = 'templates' + default_template_name = default_os_id + '-template' + default_data_size_gb = 32.0 + default_storage_cluster = 'ds-cluster-hdd-vmcc-l105-01' + default_num_cpus = 2 + default_ram_mb = 4 * 1024 + default_network = '192.168.88.0_23' + default_max_wait_for_general = 15 + default_max_wait_for_shutdown = 600 + default_max_wait_for_finish_install = 60 * 60 + default_max_nr_templates_stay = 4 + default_vmware_cfg_version = 'vmx-15' + default_os_version = 'centos8_64Guest' + min_max_wait_for_finish_general = 2 + min_max_wait_for_finish_install = 3 * 60 + max_max_wait_for_finish_general = 60 * 60 + max_max_wait_for_finish_install = 24 * 60 * 60 + limit_max_nr_templates_stay = 100 + default_root_password = 'testtest' + + default_tpl_vm_domain = 'pixelpark.com' + + default_cobbler_bin = '/bin/cobbler' + default_cobbler_host = 'cobbler.pixelpark.com' + default_cobbler_ssh_port = 22 + default_cobbler_ssh_user = 'root' + default_cobbler_ssh_timeout = 30 + default_cobbler_distro = 'CentOS-8.2-x86_64' + default_cobbler_rootdir = Path('/var/lib/cobbler') + default_cobbler_profile_repos = ['pp-centos8-baseos'] + default_cobbler_nameservers = [ + '93.188.104.82', + '93.188.109.12', + '217.66.52.10', + ] + default_cobbler_dns_search = [ + 'pixelpark.net', + 'pixelpark.com', + 'pixelpark.de', + ] + resolv_conf = Path('/etc/resolv.conf') + evaluated_resolv_conf = False + + default_cobbler_ws_base_url = 'http://cobbler.pixelpark.com' + default_cobbler_ws_docroot = Path('/var/www/html') + default_cobbler_ws_rel_filesdir = Path('custom/vmware-template-files') + + default_cobbler2_templates_dir_rel = 'kickstarts' + default_cobbler2_collections_dir_rel = 'config' + default_cobbler2_profiles_dir_rel = 'profiles.d' + + default_cobbler3_templates_dir_rel = 'templates' + default_cobbler3_collections_dir_rel = 'collections' + default_cobbler3_profiles_dir_rel = 'profiles' + + ssh_privkey = 'id_rsa_cr_vmw_tpl' + + mac_address_template = "00:16:3e:53:{:02x}:{:02x}" + + method_list = {} + for method in crypt.methods: + mname = method.name.lower() + method_list[mname] = method + + valid_system_status = ('development', 'testing', 'acceptance', 'production') + default_system_status = 'development' + + default_cobbler_profile = 'vmware-template.' + default_os_id + '.' + default_system_status + + default_swap_size_mb = 512 + + default_ldap_server = 'prd-ds.pixelpark.com' + use_ssl_on_default = True + default_ldap_port = DEFAULT_PORT_LDAPS + default_base_dn = 'o=isp' + default_bind_dn = 'uid=readonly,ou=People,o=isp' + + re_ldap_section_w_name = re.compile(r'^\s*ldap\s*:\s*(\S+)') + re_resolv_ns_entry = re.compile(r'^\s*nameserver\s+(\S+)') + + # ------------------------------------------------------------------------- + def __init__( + self, appname=None, verbose=0, version=__version__, base_dir=None, + append_appname_to_stems=True, additional_stems=None, config_dir=DEFAULT_CONFIG_DIR, + additional_config_file=None, additional_cfgdirs=None, encoding=DEFAULT_ENCODING, + ensure_privacy=False, use_chardet=True, initialized=False): + + self.eval_resolv_conf() + + add_stems = [] + if additional_stems: + if is_sequence(additional_stems): + for stem in additional_stems: + add_stems.append(stem) + else: + add_stems.append(additional_stems) + + if 'mail' not in add_stems: + add_stems.append('mail') + if 'cobbler-repos' not in add_stems: + add_stems.append('cobbler-distros') + if 'ldap' not in add_stems: + add_stems.append('ldap') + + self.os_id = self.default_os_id + + self.vsphere_cluster = self.default_vsphere_cluster + self.folder = self.default_folder + self.template_name = self.default_template_name + self.data_size_gb = self.default_data_size_gb + self.num_cpus = self.default_num_cpus + self.ram_mb = self.default_ram_mb + self.swap_size_mb = self.default_swap_size_mb + self.network = self.default_network + self.max_wait_for_general = self.default_max_wait_for_general + self.max_wait_for_create_vm = None + self.max_wait_for_poweron_vm = None + self.max_wait_for_shutdown_vm = self.default_max_wait_for_shutdown + self.max_wait_for_purge_vm = None + self.max_wait_for_finish_install = self.default_max_wait_for_finish_install + self.max_nr_templates_stay = self.default_max_nr_templates_stay + self.vmware_cfg_version = self.default_vmware_cfg_version + self.os_version = self.default_os_version + + self.storage_cluster = self.default_storage_cluster + + self.tpl_vm_domain = self.default_tpl_vm_domain + + self.cobbler_profile_given = False + self.template_name_given = False + + self._root_password = self.default_root_password + + self.private_ssh_key = None + + self._cobbler_major_version = 3 + + self.cobbler_bin = self.default_cobbler_bin + self.cobbler_distro = self.default_cobbler_distro + self.cobbler_host = self.default_cobbler_host + self.cobbler_ssh_port = self.default_cobbler_ssh_port + self.cobbler_ssh_user = self.default_cobbler_ssh_user + self.cobbler_ssh_timeout = self.default_cobbler_ssh_timeout + self.cobbler_rootdir = self.default_cobbler_rootdir + self.cobbler_profile = self.default_cobbler_profile + self.cobbler_profile_repos = copy.copy(self.default_cobbler_profile_repos) + self.cobbler_nameservers = copy.copy(self.default_cobbler_nameservers) + self.cobbler_dns_search = copy.copy(self.default_cobbler_dns_search) + self.cobbler_ws_base_url = self.default_cobbler_ws_base_url + self.cobbler_ws_docroot = self.default_cobbler_ws_docroot + self.cobbler_ws_rel_filesdir = self.default_cobbler_ws_rel_filesdir + self.cobbler_distros = {} + self.cobbler_repos = {} + + self.current_distro = None + + self.system_status = self.default_system_status + + self.excluded_datastores = [] + + if config_dir is None: + config_dir = DEFAULT_CONFIG_DIR + LOG.debug("Config dir: {!r}.".format(config_dir)) + + self.ldap_timeout = DEFAULT_TIMEOUT + + super(CrTplConfiguration, self).__init__( + appname=appname, verbose=verbose, version=version, base_dir=base_dir, + append_appname_to_stems=append_appname_to_stems, config_dir=config_dir, + additional_stems=add_stems, additional_config_file=additional_config_file, + additional_cfgdirs=additional_cfgdirs, encoding=encoding, use_chardet=use_chardet, + ensure_privacy=ensure_privacy, initialized=False, + ) + + self.vsphere_info = VSPhereConfigInfo( + host=self.default_vsphere_host, port=self.default_vsphere_port, dc=self.default_dc, + use_https=True, user=self.default_vsphere_user, + appname=self.appname, verbose=self.verbose, base_dir=self.base_dir, + initialized=True) + + self.private_ssh_key = str(self.base_dir.joinpath('keys', self.ssh_privkey)) + + self.ldap_connection = LdapConnectionDict() + + default_connection = LdapConnectionInfo( + appname=self.appname, verbose=self.verbose, base_dir=self.base_dir, + host=self.default_ldap_server, use_ldaps=self.use_ssl_on_default, + port=self.default_ldap_port, base_dn=self.default_base_dn, + bind_dn=self.default_bind_dn, initialized=False) + + self.ldap_connection['default'] = default_connection + + if initialized: + self.initialized = True + + # ------------------------------------------------------------------------- + @property + def cobbler_major_version(self): + """The major version of the used cobbler installation. Valid values are 2 and 3.""" + return self._cobbler_major_version + + @cobbler_major_version.setter + def cobbler_major_version(self, value): + if value not in (2, 3): + msg = _("Unsupported version {ver!r} of {co}, valid versions of {co} are {valid}.") + msg = msg.format(ver=value, co='Cobbler', valid=format_list(['2', '3'])) + raise CrTplConfigError(msg) + + self._cobbler_major_version = value + + # ------------------------------------------------------------------------- + @property + def cobbler_collections_dir(self): + """The absolute pathname of all Cobbler collections.""" + if self.cobbler_major_version == 2: + return self.cobbler_rootdir / self.default_cobbler2_collections_dir_rel + return self.cobbler_rootdir / self.default_cobbler3_collections_dir_rel + + # ------------------------------------------------------------------------- + @property + def cobbler_ks_dir(self): + """The absolute pathname of the directory of the kickstart/template/autoinstall files.""" + if self.cobbler_major_version == 2: + return self.cobbler_rootdir / self.default_cobbler2_templates_dir_rel + return self.cobbler_rootdir / self.default_cobbler3_templates_dir_rel + + # ------------------------------------------------------------------------- + @property + def cobbler_profile_dir(self): + """The absolute pathname of the directory of Cobbler profile configuration files.""" + if self.cobbler_major_version == 2: + return self.cobbler_collections_dir / self.default_cobbler2_profiles_dir_rel + return self.cobbler_collections_dir / self.default_cobbler3_profiles_dir_rel + + # ------------------------------------------------------------------------- + @property + def cobbler_profile_ks(self): + """The absolute pathname of the profile kickstart file.""" + return self.cobbler_ks_dir / ('profile.' + self.cobbler_profile + '.ks') + + # ------------------------------------------------------------------------- + @property + def data_size_mb(self): + """Size of template volume in MiB.""" + return int(self.data_size_gb * 1024.0) + + # ------------------------------------------------------------------------- + @property + def data_size_kb(self): + """Size of template volume in KiB.""" + return self.data_size_mb * 1024 + + # ------------------------------------------------------------------------- + @property + def data_size(self): + """Size of template volume in Bytes.""" + return self.data_size_mb * 1024 * 1024 + + # ------------------------------------------------------------------------- + @property + def ram_gb(self): + """Size of RAM in GiB.""" + return float(self.ram_mb) / 1024 + + # ------------------------------------------------------------------------- + @property + def root_password(self): + """The root password of the VM to create.""" + return self._root_password + + # ------------------------------------------------------------------------- + @property + def system_ks(self): + """The path to the system kickstart file.""" + ks_base = 'template-' + self.os_id + '-' + self.system_status + '.ks' + return self.cobbler_ks_dir / ks_base + + # ------------------------------------------------------------------------- + @property + def snippets_dir(self): + """The path to the snippets dirctory, depending of the system status.""" + return self.cobbler_rootdir / 'snippets' / 'per_status' / self.system_status + + # ------------------------------------------------------------------------- + def as_dict(self, short=True): + """ + Transforms the elements of the object into a dict + + @param short: don't include local properties in resulting dict. + @type short: bool + + @return: structure as dict + @rtype: dict + """ + + res = super(CrTplConfiguration, self).as_dict(short=short) + res['cobbler_major_version'] = self.cobbler_major_version + res['cobbler_collections_dir'] = self.cobbler_collections_dir + res['cobbler_ks_dir'] = self.cobbler_ks_dir + res['cobbler_profile_dir'] = self.cobbler_profile_dir + res['cobbler_profile_ks'] = self.cobbler_profile_ks + res['data_size_mb'] = self.data_size_mb + res['data_size_kb'] = self.data_size_kb + res['data_size'] = self.data_size + res['default_cobbler_nameservers'] = self.default_cobbler_nameservers + res['ram_gb'] = self.ram_gb + res['system_ks'] = self.system_ks + res['snippets_dir'] = self.snippets_dir + + res['cobbler_distros'] = {} + for distro in self.cobbler_distros.keys(): + res['cobbler_distros'][distro] = self.cobbler_distros[distro].as_dict(short=short) + + res['root_password'] = None + if self.root_password: + if self.verbose > 4: + res['root_password'] = self.root_password + else: + res['root_password'] = '********' + + return res + + # ------------------------------------------------------------------------- + def eval(self): + """Evaluating read configuration and storing them in object properties.""" + + super(CrTplConfiguration, self).eval() + + if self.verbose > 1: + LOG.debug(_("Checking for unconfigured options ...")) + + if self.max_wait_for_create_vm is None: + self.max_wait_for_create_vm = self.max_wait_for_general + if self.max_wait_for_poweron_vm is None: + self.max_wait_for_poweron_vm = self.max_wait_for_general + if self.max_wait_for_purge_vm is None: + self.max_wait_for_purge_vm = self.max_wait_for_general + if self.max_wait_for_shutdown_vm is None: + self.max_wait_for_shutdown_vm = self.max_wait_for_general + + if not self.template_name_given: + self.template_name = self.os_id + '-template' + + if not self.cobbler_profile_given: + self.cobbler_profile = 'vmware-template.' + self.os_id + '.' + self.system_status + + self.verify_cobbler_distros() + + if not self.template_name_given: + self.template_name = self.current_distro.shortname + '-template' + + # ------------------------------------------------------------------------- + @classmethod + def eval_resolv_conf(cls): + + if cls.evaluated_resolv_conf: + return True + + if not cls.resolv_conf.exists(): + LOG.error(_("File {!r} not found on current host.").format(str(cls.resolv_conf))) + cls.evaluated_resolv_conf = True + return False + + if not cls.resolv_conf.is_file(): + LOG.error(_("Path {!r} is not a regular file.").format(str(cls.resolv_conf))) + cls.evaluated_resolv_conf = True + return False + + if not os.access(cls.resolv_conf, os.R_OK): + LOG.error(_("File {!r} is not readable.").format(str(cls.resolv_conf))) + cls.evaluated_resolv_conf = True + return False + + LOG.info(_("Evaluating {!r} for nameservers.").format(str(cls.resolv_conf))) + + nameservers = [] + file_content = cls.resolv_conf.read_text() + lines = file_content.splitlines() + + for line in lines: + match = cls.re_resolv_ns_entry.match(line) + if match: + try: + ns_address = ipaddress.ip_address(match.group(1)) + nameservers.append(str(ns_address)) + except ValueError as e: + msg = _("Found invalid IP address {addr!r} as a nameserver in {file!r}:") + msg = msg.format(addr=match.group(1), file=str(cls.resolv_conf)) + msg += ' ' + str(e) + LOG.warn(msg) + + cls.evaluated_resolv_conf = True + msg = _("Found nameservers in {!r}:").format(str(cls.resolv_conf)) + msg += ' {}'.format(pp(nameservers)) + LOG.debug(msg) + + if len(nameservers): + cls.default_cobbler_nameservers = nameservers + return True + + return False + + # ------------------------------------------------------------------------- + def verify_cobbler_distros(self): + + LOG.debug(_("Verifying cobbler distros ...")) + + if not len(self.cobbler_distros): + msg = _("Did not found configured Cobbler distros.") + raise CrTplConfigError(msg) + + for distro_id in self.cobbler_distros.keys(): + distro = self.cobbler_distros[distro_id] + + if not distro.distro: + msg = _("Did not found distro of configured Cobbler distro {!r}.").format( + distro_id) + raise CrTplConfigError(msg) + + if not distro.ks_repo_url: + msg = _( + "Did not found the base install repo URL of configured Cobbler " + "distro {!r}.").format(distro_id) + raise CrTplConfigError(msg) + + if not len(distro.repos): + msg = _( + "Did not found repo definitions for configured Cobbler " + "distro {!r}.").format(distro_id) + LOG.warn(msg) + + if not distro.description: + distro.description = distro_id + + LOG.debug(_("Searching for distro with ID {!r} ...").format(self.os_id)) + + if self.os_id not in self.cobbler_distros: + msg = _("Did not found distro {!r} in configured Cobbler distros.").format(self.os_id) + raise CrTplConfigError(msg) + + self.current_distro = self.cobbler_distros[self.os_id] + self.cobbler_distro = self.current_distro.distro + + LOG.info(_("Using OS {os!r} with cobbler distro {di!r}.").format( + os=self.os_id, di=self.cobbler_distro)) + + # ------------------------------------------------------------------------- + def eval_section(self, section_name): + + re_cobbler_distros = re.compile(r'^\s*cobbler[_-]?distros\s*$', re.IGNORECASE) + re_cobbler_repos = re.compile(r'^\s*cobbler[_-]?repos\s*$', re.IGNORECASE) + + sn = section_name.lower() + section = self.cfg[section_name] + + LOG.debug(_("Evaluating section {!r} ...").format(section_name)) + if self.verbose > 2: + LOG.debug(_("Content of section:") + '\n' + pp(section)) + + super(CrTplConfiguration, self).eval_section(section_name) + + if sn == 'vsphere': + self._eval_config_vsphere(section_name, section) + return + + if sn == 'template': + self._eval_config_template(section_name, section) + return + + if sn == 'timeouts': + self._eval_config_timeouts(section_name, section) + return + + if sn == 'cobbler': + self._eval_config_cobbler(section_name, section) + return + + if re_cobbler_distros.match(section_name): + self._eval_cobbler_distros(section_name, section) + return + + if re_cobbler_repos.match(section_name): + self._eval_cobbler_repos(section_name, section) + return + + if sn == 'ldap': + for key in section.keys(): + sub = section[key] + if key.lower().strip() == 'timeout': + self._eval_ldap_timeout(sub) + continue + self._eval_ldap_connection(key, sub) + return + + match = self.re_ldap_section_w_name.match(sn) + if match: + connection_name = match.group(1) + self._eval_ldap_connection(connection_name, section) + return + + if self.verbose > 1: + LOG.debug(_("Unhandled configuration section {!r}.").format(section_name)) + + # ------------------------------------------------------------------------- + def _eval_ldap_timeout(self, value): + + timeout = DEFAULT_TIMEOUT + msg_invalid = _("Value {!r} for a timeout is invalid.") + + try: + timeout = int(value) + except (ValueError, TypeError) as e: + msg = msg_invalid.format(value) + msg += ': ' + str(e) + LOG.error(msg) + return + if timeout <= 0 or timeout > MAX_TIMEOUT: + msg = msg_invalid.format(value) + LOG.error(msg) + return + + self.ldap_timeout = timeout + + # ------------------------------------------------------------------------- + def _eval_ldap_connection(self, connection_name, section): + + connection = LdapConnectionInfo.init_from_config( + connection_name, section, + appname=self.appname, verbose=self.verbose, base_dir=self.base_dir, + ) + + self.ldap_connection[connection_name] = connection + + # ------------------------------------------------------------------------- + def _eval_config_vsphere(self, section_name, section): + + if self.verbose > 1: + LOG.debug(_("Checking config section {!r} ...").format(section_name)) + + re_excl_ds = re.compile(r'^\s*excluded?[-_]datastores?\s*$', re.IGNORECASE) + re_split_ds = re.compile(r'[,;\s]+') + re_storage_cluster = re.compile(r'^\s*storage[-_]?cluster\s*$', re.IGNORECASE) + + for key in section.keys(): + value = section[key] + + if key.lower() == 'host': + self.vsphere_info.host = value + continue + elif key.lower() == 'port': + self.vsphere_info.port = value + continue + elif key.lower() == 'user': + self.vsphere_info.user = value + continue + elif key.lower() == 'password': + self.vsphere_info.password = value + continue + elif key.lower() == 'cluster': + self.vsphere_cluster = value + continue + elif key.lower() == 'folder': + self.folder = value + elif key.lower() == 'dc': + self.vsphere_info.dc = value + + elif key.lower() == 'max_nr_templates_stay': + v = int(value) + if v < 1: + LOG.error(_( + "Value {val} for {p} is less than {minval}, using {default}.").format( + val=v, minval=1, p='max_nr_templates_stay', + default=self.default_max_nr_templates_stay)) + elif v >= 100: + LOG.error(_( + "Value {val} for {p} is greater than {maxval}, using {default}.").format( + val=v, maxval=100, p='max_nr_templates_stay', + default=self.default_max_nr_templates_stay)) + else: + self.max_nr_templates_stay = v + + elif re_excl_ds.search(key): + datastores = re_split_ds.split(value.strip()) + self.excluded_datastores = datastores + + elif re_storage_cluster.search(key): + cl_name = value.strip() + if cl_name: + self.storage_cluster = cl_name + else: + self.storage_cluster = None + + return + + # ------------------------------------------------------------------------- + def _eval_config_template(self, section_name, section): + + if self.verbose > 1: + LOG.debug(_("Checking config section {!r} ...").format(section_name)) + + re_os_id = re.compile(r'^\s*os[-_]?id\s*$', re.IGNORECASE) + re_os_id_subst = re.compile(r'[^a-z0-9_-]+', re.IGNORECASE) + re_vm_domain = re.compile(r'^\s*(?:vm[-_]?)?domain\s*$', re.IGNORECASE) + re_root_pwd = re.compile(r'^\s*root[-_]?password\s*$', re.IGNORECASE) + re_swap_space = re.compile(r'^\s*swap[-_]?space(?:[-_]?mb)?\s*$', re.IGNORECASE) + + for key in section.keys(): + value = section[key] + + if re_os_id.match(key): + v = value.strip().lower() + v = re_os_id_subst.sub('', v) + if v: + self.os_id = v + continue + if key.lower() == 'name': + self.template_name = value + self.template_name_given = True + continue + if re_vm_domain.match(key) and value.strip(): + self.tpl_vm_domain = value.strip().lower() + continue + if key.lower() == 'data_size_gb': + self.data_size_gb = float(value) + continue + if key.lower() == 'data_size_mb': + self.data_size_gb = float(value) / 1024.0 + continue + if key.lower() == 'data_size_kb': + self.data_size_gb = float(value) / 1024.0 / 1024.0 + continue + if key.lower() == 'data_size': + self.data_size_gb = float(value) / 1024.0 / 1024.0 / 1024.0 + continue + if key.lower() == 'num_cpus': + self.num_cpus = int(value) + continue + if key.lower() == 'ram_gb': + self.ram_mb = int(float(value) * 1024.0) + continue + if key.lower() == 'ram_mb': + self.ram_mb = int(value) + continue + if key.lower() == 'network': + self.network = value.strip() + continue + if key.lower() == 'vmware_cfg_version': + self.vmware_cfg_version = value.strip() + continue + if key.lower() == 'os_version': + self.os_version = value.strip() + continue + if re_root_pwd.match(key) and value.strip(): + self._root_password = value.strip() + continue + if re_swap_space.match(key) and value.strip(): + self.swap_size_mb = int(value) + continue + + return + + # ------------------------------------------------------------------------- + def _eval_timeout_value(self, prop_name, value, min_val, max_val, default_val): + + if self.verbose > 2: + LOG.debug(_("Checking value {v!r} for {p} ...").format(v=value, p=prop_name)) + if self.verbose > 3: + LOG.debug(_( + "Minimal value: {min_val}, maximum value: {max_val}, " + "default value: {def_val}.").format( + min_val=min_val, max_val=max_val, def_val=default_val)) + + v = float(value) + + if v < min_val: + msg = _( + "Value {val} for {prop} is less than {min_val}, " + "using {def_val} seconds.").format(val=v, min_val=min_val, def_val=default_val) + LOG.error(msg) + return + + if v < min_val: + msg = _( + "Value {val} for {prop} is greater than {max_val}, " + "using {def_val} seconds.").format(val=v, max_val=max_val, def_val=default_val) + LOG.error(msg) + return + + if self.verbose > 2: + msg = _("Setting timeout {p!r} to {v:0.1f} seconds.").format(p=prop_name, v=v) + LOG.debug(msg) + setattr(self, prop_name, v) + + # ------------------------------------------------------------------------- + def _eval_config_timeouts(self, section_name, section): + + if self.verbose > 1: + LOG.debug(_("Checking config section {!r} ...").format(section_name)) + + for key in section.keys(): + value = section[key] + + if key.lower() == 'max_wait_for_general': + self._eval_timeout_value( + prop_name='max_wait_for_general', value=value, + min_val=self.min_max_wait_for_finish_general, + max_val=self.max_max_wait_for_finish_general, + default_val=self.default_max_wait_for_general) + continue + + if key.lower() == 'max_wait_for_create_vm': + self._eval_timeout_value( + prop_name='max_wait_for_create_vm', value=value, + min_val=self.min_max_wait_for_finish_general, + max_val=self.max_max_wait_for_finish_general, + default_val=self.max_wait_for_general) + continue + elif key.lower() == 'max_wait_for_poweron_vm': + self._eval_timeout_value( + prop_name='max_wait_for_poweron_vm', value=value, + min_val=self.min_max_wait_for_finish_general, + max_val=self.max_max_wait_for_finish_general, + default_val=self.max_wait_for_general) + continue + elif key.lower() == 'max_wait_for_shutdown_vm': + self._eval_timeout_value( + prop_name='max_wait_for_shutdown_vm', value=value, + min_val=self.min_max_wait_for_finish_general, + max_val=self.max_max_wait_for_finish_general, + default_val=self.default_max_wait_for_shutdown) + continue + elif key.lower() == 'max_wait_for_purge_vm': + self._eval_timeout_value( + prop_name='max_wait_for_purge_vm', value=value, + min_val=self.min_max_wait_for_finish_general, + max_val=self.max_max_wait_for_finish_general, + default_val=self.max_wait_for_general) + continue + elif key.lower() == 'max_wait_for_finish_install': + self._eval_timeout_value( + prop_name='max_wait_for_finish_install', value=value, + min_val=self.min_max_wait_for_finish_install, + max_val=self.max_max_wait_for_finish_install, + default_val=self.default_max_wait_for_finish_install) + continue + + # ------------------------------------------------------------------------- + def _eval_config_cobbler(self, section_name, section): + + if self.verbose > 1: + LOG.debug(_("Checking config section {!r} ...").format(section_name)) + + re_port_key = re.compile(r'^\s*ssh[-_]?port\s*$', re.IGNORECASE) + re_user_key = re.compile(r'^\s*ssh[-_]?user\s*$', re.IGNORECASE) + re_timeout_key = re.compile(r'^\s*ssh[-_]?timeout\s*$', re.IGNORECASE) + re_rootdir_key = re.compile(r'^\s*root[-_]?dir(?:ectory)?\s*$', re.IGNORECASE) + re_split_values = re.compile(r'[,;\s]+') + re_pr_repos_key = re.compile(r'^\s*profile[-_]?repos?\s*$', re.IGNORECASE) + re_nameserver_key = re.compile(r'^\s*nameservers?\s*$', re.IGNORECASE) + re_dns_search_key = re.compile(r'^\s*dns[-_]?search\s*$', re.IGNORECASE) + re_ws_base_url_key = re.compile( + r'^\s*(?:ws|webserver)[-_]?base[-_]?url\s*$', re.IGNORECASE) + re_ws_docroot_key = re.compile( + r'^\s*(?:ws|webserver)[-_]?docroot\s*$', re.IGNORECASE) + re_ws_rel_filesdir_key = re.compile( + r'^\s*(?:ws|webserver)[-_]?rel(?:ative)?[-_]?filesdir\s*$', re.IGNORECASE) + re_system_status = re.compile(r'^\s*system[-_]?status\s*$', re.IGNORECASE) + re_cobbler_bin_key = re.compile(r'^\s*(?:cobbler[_-]?)?bin\s*$', re.IGNORECASE) + + for key in section.keys(): + value = section[key] + + if key.lower() == 'distro' and value.strip() != '': + self.cobbler_distro = value.strip() + continue + if key.lower() == 'host' and value.strip() != '': + self.cobbler_host = value.strip().lower() + continue + if re_port_key.match(key) and value.strip() != '': + self.cobbler_ssh_port = int(value) + continue + if re_user_key.match(key): + self.cobbler_ssh_user = value.strip().lower() + continue + if re_timeout_key.match(key): + self.cobbler_ssh_timeout = int(value) + continue + if re_cobbler_bin_key.match(key) and value.strip() != '': + self.cobbler_bin = value.strip() + continue + if re_rootdir_key.match(key): + dpath = Path(value) + if dpath.is_absolute(): + self.cobbler_rootdir = Path(value) + else: + msg = _("Path for {what} {path!r} is not absolute.").format( + what=_("Cobbler root directory"), path=str(dpath)) + LOG.error(msg) + continue + if key.lower() == 'profile' and value.strip() != '': + self.cobbler_profile = value.strip().lower() + self.cobbler_profile_given = True + continue + if re_pr_repos_key.match(key) and value.strip() != '': + self.cobbler_profile_repos = re_split_values.split(value.strip()) + continue + if re_nameserver_key.match(key) and value.strip() != '': + self.cobbler_nameservers = re_split_values.split(value.strip().lower()) + continue + if re_dns_search_key.match(key) and value.strip() != '': + self.cobbler_dns_search = re_split_values.split(value.strip().lower()) + continue + if re_ws_base_url_key.match(key) and value.strip() != '': + self.cobbler_ws_base_url = value.strip().lower() + continue + if re_ws_docroot_key.match(key) and value.strip() != '': + dpath = Path(value.strip()) + if dpath.is_absolute(): + self.cobbler_ws_docroot = dpath + else: + msg = _("Path for {what} {path!r} is not absolute.").format( + what=_("Webserver document root"), path=str(dpath)) + LOG.error(msg) + continue + if re_ws_rel_filesdir_key.match(key) and value.strip() != '': + self.cobbler_ws_rel_filesdir = Path(value.strip()) + continue + if re_system_status.match(key) and value.strip() != '': + val = value.strip().lower() + if val in self.valid_system_status: + self.system_status = val + else: + msg = _( + "The value of {what!r} must be one of {valid!r}, " + "but found {val!r}.").format( + what='system_status', + valid=self.valid_system_status, val=value) + LOG.error(msg) + continue + + # ------------------------------------------------------------------------- + def _eval_cobbler_distros(self, section_name, section): + + if self.verbose > 1: + LOG.debug(_("Checking config section {!r} ...").format(section_name)) + + for distro_id in section.keys(): + distro_info = section[distro_id] + distro_id = distro_id.lower() + distro = CobblerDistroInfo.init_from_config( + distro_id, distro_info, verbose=self.verbose) + self.cobbler_distros[distro_id] = distro + + # ------------------------------------------------------------------------- + def get_root_pwd_hash(self, method='sha256'): + """Generates a password hash based on the root password.""" + + m = method.lower() + if m not in self.method_list: + msg = _("Given method {!r} is not a valid crypt method.").format(method) + raise ValueError(msg) + + salt = crypt.mksalt(self.method_list[m]) + if self.verbose > 1: + pw_show = self.root_password + if self.verbose < 4: + pw_show = '********' + LOG.debug("Hashing password {pw!r} with salt {sa!r} (method {me!r})..".format( + pw=pw_show, sa=salt, me=m)) + pwd_hash = crypt.crypt(self.root_password, salt) + if self.verbose > 2: + LOG.debug(_("Hashed root password: {!r}").format(pwd_hash)) + + return pwd_hash + + # ------------------------------------------------------------------------- + def _eval_cobbler_repos(self, section_name, section): + + if self.verbose > 1: + LOG.debug(_("Checking config section {!r} ...").format(section_name)) + + for full_repo_name in section.keys(): + repo_data = section[full_repo_name] + if isinstance(repo_data, dict): + repo_info = {} + if self.verbose > 2: + LOG.debug(_("Found Cobbler repository {!r}.").format(full_repo_name)) + for key in repo_data.keys(): + value = repo_data[key] + if key.lower() == 'reponame' and value.strip() != '': + repo_info['reponame'] = value.strip() + continue + if key.lower() == 'filename' and value.strip() != '': + repo_info['filename'] = value.strip() + continue + self.cobbler_repos[full_repo_name] = repo_info + if self.verbose > 3: + LOG.debug(_("Evaluated Cobbler repositories:") + '\n' + pp(self.cobbler_repos)) + + # ------------------------------------------------------------------------- + def strip_unnessecary(self): + """Stripping no more necessary stuff.""" + LOG.debug(_("Stripping no more necessary stuff from configuration ...")) + + if self.verbose > 1: + LOG.debug(_("Stripping {!r} ...").format('cfg.cobbler-distros')) + if 'cobbler-distros' in self.cfg: + del self.cfg['cobbler-distros'] + + if self.verbose > 1: + LOG.debug(_("Stripping {!r} ...").format('cfg.cobbler-repos')) + if 'cobbler-repos' in self.cfg: + del self.cfg['cobbler-repos'] + + if self.verbose > 1: + LOG.debug(_("Stripping {!r} ...").format('cobbler_distros')) + self.cobbler_distros = {} + + if self.verbose > 1: + LOG.debug(_("Stripping {!r} ...").format('cobbler_repos')) + self.cobbler_repos = {} + + if self.verbose > 1: + LOG.debug(_("Stripping {!r} ...").format('configs_raw')) + self.configs_raw = {} + + +# ============================================================================= +if __name__ == "__main__": + + pass + +# ============================================================================= + +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list diff --git a/lib/cr_vmware_tpl/config/ldap.py b/lib/cr_vmware_tpl/config/ldap.py new file mode 100644 index 0000000..4b734b7 --- /dev/null +++ b/lib/cr_vmware_tpl/config/ldap.py @@ -0,0 +1,369 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@author: Frank Brehm +@contact: frank.brehm@pixelpark.com +@copyright: © 2018 by Frank Brehm, Berlin +@summary: A module for providing LDAP connection infos. +""" +from __future__ import absolute_import + +# Standard module +import logging +import re +import copy + +# Third party modules +from fb_tools.common import to_bool +from fb_tools.obj import FbGenericBaseObject, FbBaseObject + +# Own modules +from .. import MAX_PORT_NUMBER +from .. import DEFAULT_PORT_LDAP, DEFAULT_PORT_LDAPS +from .. import DEFAULT_LDAP_ADMIN_FILTER + +from ..errors import CrTplConfigError + +from ..xlate import XLATOR + +__version__ = '3.0.0' +LOG = logging.getLogger(__name__) + +_ = XLATOR.gettext +ngettext = XLATOR.ngettext + + +# ============================================================================= +class LdapConnectionInfo(FbBaseObject): + """Encapsulating all necessary data to connect to a LDAP server.""" + + re_host_key = re.compile(r'^\s*(?:host|server)\s*$', re.IGNORECASE) + re_ldaps_key = re.compile(r'^\s*(?:use[_-]?)?(?:ldaps|ssl)\s*$', re.IGNORECASE) + re_port_key = re.compile(r'^\s*port\s*$', re.IGNORECASE) + re_base_dn_key = re.compile(r'^\s*base[_-]*dn\s*$', re.IGNORECASE) + re_bind_dn_key = re.compile(r'^\s*bind[_-]*dn\s*$', re.IGNORECASE) + re_bind_pw_key = re.compile(r'^\s*bind[_-]*pw\s*$', re.IGNORECASE) + re_admin_filter_key = re.compile(r'^\s*(?:admin[_-]?)?filter\s*$', re.IGNORECASE) + + # ------------------------------------------------------------------------- + def __init__( + self, appname=None, verbose=0, version=__version__, base_dir=None, + host=None, use_ldaps=False, port=DEFAULT_PORT_LDAP, base_dn=None, + bind_dn=None, bind_pw=None, admin_filter=None, initialized=False): + + self._host = None + self._use_ldaps = False + self._port = DEFAULT_PORT_LDAP + self._base_dn = None + self._bind_dn = None + self._bind_pw = None + self._admin_filter = DEFAULT_LDAP_ADMIN_FILTER + + super(LdapConnectionInfo, self).__init__( + appname=appname, verbose=verbose, version=version, base_dir=base_dir, + initialized=False) + + self.host = host + self.use_ldaps = use_ldaps + self.port = port + if base_dn: + self.base_dn = base_dn + self.bind_dn = bind_dn + self.bind_pw = bind_pw + if admin_filter: + self.admin_filter = admin_filter + + if initialized: + self.initialized = True + + # ------------------------------------------------------------------------- + def as_dict(self, short=True): + """ + Transforms the elements of the object into a dict + + @param short: don't include local properties in resulting dict. + @type short: bool + + @return: structure as dict + @rtype: dict + """ + + res = super(LdapConnectionInfo, self).as_dict(short=short) + + res['host'] = self.host + res['use_ldaps'] = self.use_ldaps + res['port'] = self.port + res['base_dn'] = self.base_dn + res['bind_dn'] = self.bind_dn + res['bind_pw'] = None + res['schema'] = self.schema + res['url'] = self.url + res['admin_filter'] = self.admin_filter + + if self.bind_pw: + if self.verbose > 4: + res['bind_pw'] = self.bind_pw + else: + res['bind_pw'] = '******' + + return res + + # ----------------------------------------------------------- + @property + def host(self): + """The host name (or IP address) of the LDAP server.""" + return self._host + + @host.setter + def host(self, value): + if value is None or str(value).strip() == '': + self._host = None + return + self._host = str(value).strip().lower() + + # ----------------------------------------------------------- + @property + def use_ldaps(self): + """Should there be used LDAPS for communicating with the LDAP server?""" + return self._use_ldaps + + @use_ldaps.setter + def use_ldaps(self, value): + self._use_ldaps = to_bool(value) + + # ----------------------------------------------------------- + @property + def port(self): + "The TCP port number of the LDAP server." + return self._port + + @port.setter + def port(self, value): + v = int(value) + if v < 1 or v > MAX_PORT_NUMBER: + raise CrTplConfigError(_("Invalid port {!r} for LDAP server given.").format(value)) + self._port = v + + # ----------------------------------------------------------- + @property + def base_dn(self): + """The DN used to connect to the LDAP server, anonymous bind is used, if + this DN is empty or None.""" + return self._base_dn + + @base_dn.setter + def base_dn(self, value): + if value is None or str(value).strip() == '': + msg = _("An empty Base DN for LDAP searches is not allowed.") + raise CrTplConfigError(msg) + self._base_dn = str(value).strip() + + # ----------------------------------------------------------- + @property + def bind_dn(self): + """The DN used to connect to the LDAP server, anonymous bind is used, if + this DN is empty or None.""" + return self._bind_dn + + @bind_dn.setter + def bind_dn(self, value): + if value is None or str(value).strip() == '': + self._bind_dn = None + return + self._bind_dn = str(value).strip() + + # ----------------------------------------------------------- + @property + def bind_pw(self): + """The password of the DN used to connect to the LDAP server.""" + return self._bind_pw + + @bind_pw.setter + def bind_pw(self, value): + if value is None or str(value).strip() == '': + self._bind_pw = None + return + self._bind_pw = str(value).strip() + + # ----------------------------------------------------------- + @property + def admin_filter(self): + """The LDAP filter to get the list of administrators from LDAP.""" + return self._admin_filter + + @admin_filter.setter + def admin_filter(self, value): + if value is None or str(value).strip() == '': + self._admin_filter = None + return + self._admin_filter = str(value).strip() + + # ----------------------------------------------------------- + @property + def schema(self): + """The schema as part of the URL to connect to the LDAP server.""" + if self.use_ldaps: + return 'ldaps' + return 'ldap' + + # ----------------------------------------------------------- + @property + def url(self): + """The URL, which ca be used to connect to the LDAP server.""" + if not self.host: + return None + + port = '' + if self.use_ldaps: + if self.port != DEFAULT_PORT_LDAPS: + port = ':{}'.format(self.port) + else: + if self.port != DEFAULT_PORT_LDAP: + port = ':{}'.format(self.port) + + return '{s}://{h}{p}'.format(s=self.schema, h=self.host, p=port) + + # ------------------------------------------------------------------------- + def __repr__(self): + """Typecasting into a string for reproduction.""" + + out = "<%s(" % (self.__class__.__name__) + + fields = [] + fields.append("appname={!r}".format(self.appname)) + fields.append("host={!r}".format(self.host)) + fields.append("use_ldaps={!r}".format(self.use_ldaps)) + fields.append("port={!r}".format(self.port)) + fields.append("base_dn={!r}".format(self.base_dn)) + fields.append("bind_dn={!r}".format(self.bind_dn)) + fields.append("bind_pw={!r}".format(self.bind_pw)) + fields.append("admin_filter={!r}".format(self.admin_filter)) + fields.append("initialized={!r}".format(self.initialized)) + + out += ", ".join(fields) + ")>" + return out + + # ------------------------------------------------------------------------- + def __copy__(self): + + new = self.__class__( + appname=self.appname, verbose=self.verbose, base_dir=self.base_dir, host=self.host, + use_ldaps=self.use_ldaps, port=self.port, base_dn=self.base_dn, bind_dn=self.bind_dn, + bind_pw=self.bind_pw, admin_filter=self.admin_filter, initialized=self.initialized) + + return new + + # ------------------------------------------------------------------------- + @classmethod + def init_from_config(cls, name, data, appname=None, verbose=0, base_dir=None): + + new = cls(appname=appname, verbose=verbose, base_dir=base_dir) + + s_name = "ldap:" + name + msg_invalid = _("Invalid value {val!r} in section {sec!r} for a LDAP {what}.") + + for key in data.keys(): + value = data[key] + + if cls.re_host_key.match(key): + if value.strip(): + new.host = value + else: + msg = msg_invalid.format(val=value, sec=s_name, what='host') + LOG.error(msg) + continue + + if cls.re_ldaps_key.match(key): + new.use_ldaps = value + continue + + if cls.re_port_key.match(key): + port = DEFAULT_PORT_LDAP + try: + port = int(value) + except (ValueError, TypeError) as e: + msg = msg_invalid.format(val=value, sec=s_name, what='port') + msg += ' ' + str(e) + LOG.error(msg) + continue + if port <= 0 or port > MAX_PORT_NUMBER: + msg = msg_invalid.format(val=value, sec=s_name, what='port') + LOG.error(msg) + continue + new.port = port + continue + + if cls.re_base_dn_key.match(key): + if value.strip(): + new.base_dn = value + else: + msg = msg_invalid.format(val=value, sec=s_name, what='base_dn') + LOG.error(msg) + continue + + if cls.re_bind_dn_key.match(key): + new.bind_dn = value + continue + + if cls.re_bind_pw_key.match(key): + new.bind_pw = value + continue + + if cls.re_admin_filter_key.match(key): + new.admin_filter = value + continue + + if key.lower() in ['is_admin', 'readonly', 'tier', 'sync-source']: + continue + + msg = _("Unknown LDAP configuration key {key} found in section {sec!r}.").format( + key=key, sec=s_name) + LOG.error(msg) + + new.initialized = True + + return new + + +# ============================================================================= +class LdapConnectionDict(dict, FbGenericBaseObject): + """A dictionary containing LdapConnectionInfo as values and their names as keys.""" + + # ------------------------------------------------------------------------- + def as_dict(self, short=True): + """ + Transforms the elements of the object into a dict + + @param short: don't include local properties in resulting dict. + @type short: bool + + @return: structure as dict + @rtype: dict + """ + + res = super(LdapConnectionDict, self).as_dict(short=short) + + for key in self.keys(): + res[key] = self[key].as_dict(short=short) + + return res + + # ------------------------------------------------------------------------- + def __copy__(self): + + new = self.__class__() + + for key in self.keys(): + new[key] = copy.copy(self[key]) + + return new + + +# ============================================================================= +if __name__ == "__main__": + + pass + +# ============================================================================= + +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list diff --git a/lib/cr_vmware_tpl/handler.py b/lib/cr_vmware_tpl/handler.py index daf0157..748374d 100644 --- a/lib/cr_vmware_tpl/handler.py +++ b/lib/cr_vmware_tpl/handler.py @@ -47,8 +47,9 @@ from fb_vmware.iface import VsphereVmInterface from fb_vmware.datastore import VsphereDatastore from . import print_section_start, print_section_end +from . import DEFAULT_PORT_LDAP, DEFAULT_PORT_LDAPS -from .config import CrTplConfiguration, DEFAULT_PORT_LDAP, DEFAULT_PORT_LDAPS +from .config import CrTplConfiguration from .cobbler import Cobbler