from pathlib import Path
# Own modules
-from fb_tools.common import is_sequence, pp
-from fb_tools.obj import FbGenericBaseObject
+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 MultiConfigError, BaseMultiConfig
from fb_tools.multi_config import DEFAULT_ENCODING
from fb_vmware.config import VSPhereConfigInfo
-from . import DEFAULT_CONFIG_DIR, DEFAULT_DISTRO_ARCH
+from . import DEFAULT_CONFIG_DIR, DEFAULT_DISTRO_ARCH, MAX_PORT_NUMBER
from .xlate import XLATOR
-__version__ = '1.9.5'
+__version__ = '2.0.0'
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=*)'
+ '(memberOf=cn=Administratoren Pixelpark Berlin,ou=Groups,o=Pixelpark,o=isp))'
+)
+
# =============================================================================
class CrTplConfigError(MultiConfigError):
pass
+# =============================================================================
+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
+
+ 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."""
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
+
+
# =============================================================================
class CrTplConfiguration(BaseMultiConfig):
"""
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+)')
+
# -------------------------------------------------------------------------
def __init__(
self, appname=None, verbose=0, version=__version__, base_dir=None,
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
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,
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
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)
- sn = section_name.lower()
- section = self.cfg[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):
from pyVmomi import vim
+import ldap3
+from ldap3.core.exceptions import LDAPInvalidDnError, LDAPInvalidValueError
+from ldap3.core.exceptions import LDAPException, LDAPBindError
+
# Own modules
from fb_tools.common import pp, to_str
from . import print_section_start, print_section_end
-from .config import CrTplConfiguration
+from .config import CrTplConfiguration, DEFAULT_PORT_LDAP, DEFAULT_PORT_LDAPS
from .cobbler import Cobbler
from .xlate import XLATOR
-__version__ = '2.1.3'
+__version__ = '2.2.0'
LOG = logging.getLogger(__name__)
TZ = pytz.timezone('Europe/Berlin')
self.abort = False
self.postinstall_errors = None
self.cobbler = None
+ self.ldap = None
+ self.ldap_server = None
self.vsphere = VsphereConnection(
self.cfg.vsphere_info, cluster=self.cfg.vsphere_cluster,
return res
+ # -------------------------------------------------------------------------
+ def connect_ldap(self):
+
+ ldap_config = self.cfg.ldap_connection['default']
+
+ server_opts = {}
+ if ldap_config.use_ldaps:
+ server_opts['use_ssl'] = True
+ if ldap_config.port != DEFAULT_PORT_LDAPS:
+ server_opts['port'] = ldap_config.port
+ else:
+ server_opts['use_ssl'] = False
+ if ldap_config.port != DEFAULT_PORT_LDAP:
+ server_opts['port'] = ldap_config.port
+
+ server_opts['get_info'] = ldap3.DSA
+ server_opts['mode'] = ldap3.IP_V4_PREFERRED
+ server_opts['connect_timeout'] = self.cfg.ldap_timeout
+
+ LOG.info(_("Connecting to LDAP server {!r} ...").format(ldap_config.url))
+
+ if self.verbose > 1:
+ msg = _("Connect options to LDAP server {!r}:").format(ldap_config.url)
+ msg += '\n' + pp(server_opts)
+ LOG.debug(msg)
+
+ self.ldap_server = ldap3.Server(ldap_config.host, **server_opts)
+
+ if self.verbose > 2:
+ LOG.debug("LDAP server {s}: {re}".format(
+ s=ldap_config.host, re=repr(self.ldap_server)))
+
+ self.ldap = ldap3.Connection(
+ self.ldap_server, ldap_config.bind_dn, ldap_config.bind_pw,
+ client_strategy=ldap3.SAFE_SYNC, auto_bind=True)
+
+ if self.verbose > 2:
+ msg = _("Info about LDAP server {}:").format(ldap_config.url)
+ msg += '\n' + repr(self.ldap_server.info)
+ LOG.debug(msg)
+
+ # -------------------------------------------------------------------------
+ def disconnect_ldap(self):
+
+ if 'default' in self.cfg.ldap_connection:
+ ldap_config = self.cfg.ldap_connection['default']
+ ldap_server = ldap_config.url
+ else:
+ ldap_server = 'unknown'
+
+ if self.ldap:
+ LOG.info(_("Unbinding from LDAP server {} ...").format(ldap_server))
+ self.ldap.unbind()
+ self.ldap = None
+
+ if self.ldap_server:
+ LOG.info(_("Disconnecting from LDAP server {} ...").format(ldap_server))
+ self.ldap_server = None
+
+ # -------------------------------------------------------------------------
+ def __del__(self):
+ """Destructor."""
+
+ self.disconnect_ldap()
+
# -------------------------------------------------------------------------
def __call__(self):
"""Executing the underlying action."""
self.cobbler.get_cobbler_version()
self.check_for_cobbler_distro()
self.cobbler.ensure_profile_ks()
+ self.create_root_authkeys()
return 0
self.cobbler.ensure_profile()
self.cobbler.ensure_root_authkeys()
LOG.debug(_("Object {!r} is now a VMWare template.").format(self.cfg.template_name))
print_section_end('rename_and_change_vm')
+ # -------------------------------------------------------------------------
+ def create_root_authkeys(self):
+
+ LOG.info(_("Creating authorized keys of root from LDAP ..."))
+
+ try:
+ self.connect_ldap()
+ finally:
+ self.disconnect_ldap()
+
# =============================================================================
if __name__ == "__main__":