From 743bfb44148c3cef716e241878cbfa87e568bb0a Mon Sep 17 00:00:00 2001 From: Frank Brehm Date: Fri, 27 Sep 2019 18:13:14 +0200 Subject: [PATCH] Refactored lib/cr_tf/config.py --- lib/cr_tf/config.py | 470 +++++++++++++++++++++++++++++++++++--------- lib/cr_tf/errors.py | 12 +- 2 files changed, 392 insertions(+), 90 deletions(-) diff --git a/lib/cr_tf/config.py b/lib/cr_tf/config.py index af2a2d7..0ff942c 100644 --- a/lib/cr_tf/config.py +++ b/lib/cr_tf/config.py @@ -21,32 +21,26 @@ import pytz from fb_tools.obj import FbBaseObject -from fb_tools.config import ConfigError, BaseConfiguration +from fb_tools.config import BaseConfiguration -from fb_tools.common import to_bool, RE_FQDN +from fb_tools.common import to_bool, RE_FQDN, is_sequence from fb_tools.pdns import DEFAULT_PORT as DEFAULT_PDNS_API_PORT from fb_tools.pdns import DEFAULT_TIMEOUT as DEFAULT_PDNS_API_TIMEOUT # noqa from fb_tools.pdns import DEFAULT_API_PREFIX as DEFAULT_PDNS_API_PREFIX # noqa from fb_tools.pdns import DEFAULT_USE_HTTPS as DEFAULT_PDNS_API_USE_HTTPS +from .errors import CrTfConfigError + from .xlate import XLATOR -__version__ = '1.2.1' +__version__ = '1.3.1' LOG = logging.getLogger(__name__) _ = XLATOR.gettext ngettext = XLATOR.ngettext -# ============================================================================= -class CrTfConfigError(ConfigError): - """Base error class for all exceptions happened during - execution this configured application""" - - pass - - # ============================================================================= class VsphereConfig(FbBaseObject): """Class for encapsulation of config data of a connection to a VSPhere center.""" @@ -55,6 +49,8 @@ class VsphereConfig(FbBaseObject): default_user = 'Administrator@vsphere.local' default_dc = 'vmcc' default_cluster = 'vmcc-l105-01' + default_min_root_size_gb = 32.0 + default_guest_id = "oracleLinux7_64Guest" default_template_name = 'oracle-linux-7-template' @@ -62,7 +58,8 @@ class VsphereConfig(FbBaseObject): def __init__( self, appname=None, verbose=0, version=__version__, base_dir=None, name=None, host=None, port=None, user=None, password=None, dc=None, cluster=None, - template_name=None, initialized=False): + template_name=None, min_root_size_gb=None, excluded_ds=None, guest_id=None, + initialized=False): self._name = None self._host = None @@ -72,6 +69,9 @@ class VsphereConfig(FbBaseObject): self._dc = self.default_dc self._cluster = self.default_cluster self._template_name = self.default_template_name + self._min_root_size_gb = self.default_min_root_size_gb + self._guest_id = self.default_guest_id + self.excluded_ds = [] super(VsphereConfig, self).__init__( appname=appname, verbose=verbose, version=version, @@ -94,10 +94,317 @@ class VsphereConfig(FbBaseObject): self.cluster = cluster if template_name is not None: self.template_name = template_name + if min_root_size_gb is not None: + self.min_root_size_gb = min_root_size_gb + if guest_id is not None: + self.guest_id = guest_id + + if excluded_ds: + if is_sequence(excluded_ds): + for ds in excluded_ds: + self.excluded_ds.append(str(ds)) + else: + self.excluded_ds.append(str(excluded_ds)) if initialized: self.initialized = True + # ----------------------------------------------------------- + @property + def name(self): + """The name of the VSphere.""" + return self._name + + @name.setter + def name(self, value): + if value is None: + self._name = None + return + val = str(value).strip().lower() + if val == '': + self._name = None + else: + self._name = val + + # ----------------------------------------------------------- + @property + def host(self): + """The host name or address of the VSphere server.""" + return self._host + + @host.setter + def host(self, value): + if value is None: + self._host = None + return + val = str(value).strip().lower() + if val == '': + self._host = None + else: + self._host = val + + # ----------------------------------------------------------- + @property + def port(self): + """The TCP port number, where the API is listening on the VSphere server.""" + return self._port + + @port.setter + def port(self, value): + if value is None: + self._port = self.default_port + return + val = self.default_port + try: + val = int(value) + if val < 1: + msg = _("a port may not be less than 1: {}.").format(val) + raise CrTfConfigError(msg) + max_val = (2**16) -1 + if val > max_val: + msg = _("a port may not be greater than {m}: {v}.").format( + m=max_val, v=val) + raise CrTfConfigError(msg) + except ValueError as e: + msg = _("Wrong port number {v!r}: {e}").format(v=value, e=e) + LOG.error(msg) + else: + self._port = val + + # ----------------------------------------------------------- + @property + def user(self): + """The user name to connect to the VSphere server.""" + return self._user + + @user.setter + def user(self, value): + if value is None: + self._user = self.default_user + return + val = str(value).strip() + if val == '': + self._user = self.default_user + else: + self._user = val + + # ----------------------------------------------------------- + @property + def password(self): + """The password of the VSphere user.""" + return self._password + + @password.setter + def password(self, value): + if value is None: + self._password = None + return + val = str(value) + if val == '': + self._password = None + else: + self._password = val + + # ----------------------------------------------------------- + @property + def dc(self): + """The name of the datacenter in VSphere.""" + return self._dc + + @dc.setter + def dc(self, value): + if value is None: + self._dc = self.default_dc + return + val = str(value).strip() + if val == '': + self._dc = self.default_dc + else: + self._dc = val + + # ----------------------------------------------------------- + @property + def cluster(self): + """The name of the default cluster in VSphere.""" + return self._cluster + + @cluster.setter + def cluster(self, value): + if value is None: + self._cluster = self.default_cluster + return + val = str(value).strip() + if val == '': + self._cluster = self.default_cluster + else: + self._cluster = val + + # ----------------------------------------------------------- + @property + def template_name(self): + """The name of the default cluster in VSphere.""" + return self._template_name + + @template_name.setter + def template_name(self, value): + if value is None: + self._template_name = self.default_template_name + return + val = str(value).strip() + if val == '': + self._template_name = self.default_template_name + else: + self._template_name = val + + # ----------------------------------------------------------- + @property + def min_root_size_gb(self): + """The minimum size of a root disk in GiB.""" + return self._min_root_size_gb + + @min_root_size_gb.setter + def min_root_size_gb(self, value): + if value is None: + self._min_root_size_gb = self.default_min_root_size_gb + return + val = self.default_min_root_size_gb + try: + val = float(value) + if val < 10: + msg = _("may not be less than 10: {:0.1f}.").format(val) + raise CrTfConfigError(msg) + max_val = 4 * 1024 + if val > max_val: + msg = _("may not be greater than {m}: {v:0.1f}.").format( + m=max_val, v=val) + raise CrTfConfigError(msg) + except ValueError as e: + msg = _("Wrong minimum root size in GiB {v!r}: {e}").format(v=value, e=e) + LOG.error(msg) + else: + self._min_root_size_gb = val + + # ----------------------------------------------------------- + @property + def guest_id(self): + """The user name to connect to the VSphere server.""" + return self._guest_id + + @guest_id.setter + def guest_id(self, value): + if value is None: + self._guest_id = self.default_guest_id + return + val = str(value).strip() + if val == '': + self._guest_id = self.default_guest_id + else: + self._guest_id = val + + # ------------------------------------------------------------------------- + def as_dict(self, short=True, show_secrets=False): + """ + 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(VsphereConfig, self).as_dict(short=short) + + res['name'] = self.name + res['host'] = self.host + res['port'] = self.port + res['user'] = self.user + res['dc'] = self.dc + res['cluster'] = self.cluster + res['template_name'] = self.template_name + res['min_root_size_gb'] = self.min_root_size_gb + res['guest_id'] = self.guest_id + + if self.password: + if show_secrets or self.verbose > 4: + res['password'] = self.password + else: + res['password'] = '*******' + + return res + + # ------------------------------------------------------------------------- + def __copy__(self): + + vsphere = self.__class__( + appname=self.appname, verbose=self.verbose, base_dir=self.base_dir, + initialized=self.initialized, host=self.host, port=self.port, user=self.user, + password=self.password, dc=self.dc, cluster=self.cluster, + template_name=self.template_name, excluded_ds=self.excluded_ds, + min_root_size_gb=self.min_root_size_gb, guest_id=self.guest_id) + return vsphere + + # ------------------------------------------------------------------------- + def __eq__(self, other): + + if self.verbose > 4: + LOG.debug(_("Comparing {} objects ...").format(self.__class__.__name__)) + + if not isinstance(other, VsphereConfig): + return False + + if self.name != other.name: + return False + if self.host != other.host: + return False + if self.port != other.port: + return False + if self.user != other.user: + return False + if self.password != other.password: + return False + if self.dc != other.dc: + return False + if self.cluster != other.cluster: + return False + if self.template_name != other.template_name: + return False + if self.min_root_size_gb != other.min_root_size_gb: + return False + if self.guest_id != other.guest_id: + return False + if self.excluded_ds != other.excluded_ds: + return False + + return True + + # ------------------------------------------------------------------------- + def is_valid(self, raise_on_error=False): + + error_lst = [] + + attribs = ('name', 'host', 'user', 'password') + for attrib in attribs: + cur_val = getattr(self, attrib, None) + if not cur_val: + name = '<{}>'.format(_('unknown')) + if self.name: + name = self.name + msg = _("Attribute {a!r} of the {o}-object {n!r} is not set.").format( + a=attrib, o=self.__class__.__name__, n=name) + error_lst.append(msg) + if not raise_on_error: + LOG.error(msg) + if error_lst: + if raise_on_error: + nr = len(error_lst) + msg = ngettext( + 'Found an error in VSPhere configuration', + 'Found {} errors in VSPhere configuration', nr) + msg = msg.format(nr) + '\n * ' + '\n * '.join(error_lst) + raise CrTfConfigError(msg) + return False + return True # ============================================================================= class CrTfConfiguration(BaseConfiguration): @@ -106,13 +413,6 @@ class CrTfConfiguration(BaseConfiguration): and methods to read it from configuration files. """ - default_vsphere_port = 443 - default_vsphere_user = 'Administrator@vsphere.local' - default_vsphere_dc = 'vmcc' - default_vsphere_cluster = 'vmcc-l105-01' - - default_template_name = 'oracle-linux-7-template' - default_pdns_master_server = 'master.pp-dns.com' default_pdns_api_port = DEFAULT_PDNS_API_PORT default_pdns_api_use_https = bool(DEFAULT_PDNS_API_USE_HTTPS) @@ -149,13 +449,6 @@ class CrTfConfiguration(BaseConfiguration): self, appname=None, verbose=0, version=__version__, base_dir=None, simulate=False, encoding=None, config_dir=None, config_file=None, initialized=False): - self.vsphere_host = self.default_vsphere_host - self.vsphere_port = self.default_vsphere_port - self.vsphere_user = self.default_vsphere_user - self._vsphere_password = None - self.vsphere_dc = self.default_vsphere_dc - self.vsphere_cluster = self.default_vsphere_cluster - self.template_name = self.default_template_name self.pdns_master_server = self.default_pdns_master_server self.pdns_api_port = self.default_pdns_api_port self._pdns_api_key = None @@ -172,6 +465,8 @@ class CrTfConfiguration(BaseConfiguration): self.puppetca = self.default_puppetca self.pdns_comment_account = self.default_pdns_comment_account + self.vsphere = {} + self._disk_size = self.default_disk_size self._root_min_size = self.default_root_min_size @@ -209,23 +504,6 @@ class CrTfConfiguration(BaseConfiguration): def simulate(self, value): self._simulate = to_bool(value) - # ----------------------------------------------------------- - @property - def vsphere_password(self): - """The password of the VSphere user.""" - return self._vsphere_password - - @vsphere_password.setter - def vsphere_password(self, value): - if value is None: - self._vsphere_password = None - return - val = str(value) - if val == '': - self._vsphere_password = None - else: - self._vsphere_password = val - # ----------------------------------------------------------- @property def pdns_api_key(self): @@ -425,7 +703,6 @@ class CrTfConfiguration(BaseConfiguration): res['simulate'] = self.simulate res['pdns_api_use_https'] = self.pdns_api_use_https res['pdns_api_timeout'] = self.pdns_api_timeout - res['vsphere_password'] = None res['vm_root_password'] = None res['pdns_api_key'] = None res['relative_tf_dir'] = self.relative_tf_dir @@ -436,11 +713,10 @@ class CrTfConfiguration(BaseConfiguration): res['root_min_size'] = self.root_min_size res['root_max_size'] = self.root_max_size - if self.vsphere_password: - if show_secrets or self.verbose > 4: - res['vsphere_password'] = self.vsphere_password - else: - res['vsphere_password'] = '*******' + res['vsphere'] = {} + for vsphere_name in self.vsphere.keys(): + res['vsphere'][vsphere_name] = self.vsphere[vsphere_name].as_dict( + short=short, show_secrets=show_secrets) if self.pdns_api_key: if show_secrets or self.verbose > 4: @@ -462,11 +738,20 @@ class CrTfConfiguration(BaseConfiguration): super(CrTfConfiguration, self).eval_config_section(config, section_name) - if section_name.lower() == 'vsphere': - self.eval_config_vsphere(config, section_name) - elif section_name.lower() == 'powerdns' or section_name.lower() == 'pdns': + sn = section_name.lower() + + if sn == 'vsphere' or sn.startswith('vsphere:'): + if sn.startswith('vsphere:'): + vsphere_name = sn.replace('vsphere:', '').strip() + if vsphere_name == '': + LOG.error(_("Empty VSpehre name found.")) + else: + self.eval_config_vsphere(config, section_name, vsphere_name) + else: + self.eval_config_vsphere(config, section_name, '_default') + elif sn == 'powerdns' or sn == 'pdns': self.eval_config_pdns(config, section_name) - elif section_name.lower() == 'terraform': + elif sn == 'terraform': self.eval_config_terraform(config, section_name) # ------------------------------------------------------------------------- @@ -489,29 +774,30 @@ class CrTfConfiguration(BaseConfiguration): try: tz = pytz.timezone(val) # noqa except pytz.exceptions.UnknownTimeZoneError as e: - raise ConfigError(self.msg_invalid_type.format( + raise CrTfConfigError(self.msg_invalid_type.format( f=self.config_file, s=section_name, v=value, n='time_zone', e=e)) self.tz_name = value.strip() elif re_puppetmaster.search(key) and value.strip(): val = value.strip() if not RE_FQDN.search(val): - raise ConfigError(self.msg_invalid_type.format( + raise CrTfConfigError(self.msg_invalid_type.format( f=self.config_file, s=section_name, v=value, n='puppet_master', e='Invalid Host FQDN for puppetmaster')) self.puppetmaster = val.lower() elif re_puppetca.search(key) and value.strip(): val = value.strip() if not RE_FQDN.search(val): - raise ConfigError(self.msg_invalid_type.format( + raise CrTfConfigError(self.msg_invalid_type.format( f=self.config_file, s=section_name, v=value, n='puppet_ca', e='Invalid Host FQDN for puppetca')) self.puppetca = val.lower() # ------------------------------------------------------------------------- - def eval_config_vsphere(self, config, section): + def eval_config_vsphere(self, config, section_name, vsphere_name): if self.verbose > 2: - LOG.debug(_("Checking config section {!r} ...").format(section)) + LOG.debug(_("Checking config section {s!r} ({n}) ...").format( + s=section_name, n=vsphere_name)) re_excl_ds = re.compile(r'^\s*excluded?[-_]datastores?\s*$', re.IGNORECASE) re_split_ds = re.compile(r'[,;\s]+') @@ -520,47 +806,53 @@ class CrTfConfiguration(BaseConfiguration): r'^\s*min[-_\.]?root[-_\.]?size(?:[-_\.]?gb)\s*$', re.IGNORECASE) re_guest_id = re.compile(r'^\s*guest[-_]?id\s*$', re.IGNORECASE) - for (key, value) in config.items(section): + params = { + 'appname': self.appname, + 'verbose': self.verbose, + 'base_dir': self.base_dir, + 'name': vsphere_name, + } + + for (key, value) in config.items(section_name): if key.lower() == 'host' and value.strip(): - self.vsphere_host = value.strip().lower() + params['host'] = value.strip() elif key.lower() == 'port': - val = 0 - try: - val = int(value) - except ValueError as e: - raise ConfigError(self.msg_invalid_type.format( - f=self.config_file, s=section, v=value, n='port', e=e)) - if val < 0: - raise ConfigError(self.msg_val_negative.format( - f=self.config_file, s=section, v=value, n='port')) - self.vsphere_port = val + params['port'] = value elif key.lower() == 'user' and value.strip(): - self.vsphere_user = value.strip() + params['port'] = value.strip() elif key.lower() == 'password': - self.vsphere_password = value + params['password'] = value elif key.lower() == 'dc' and value.strip(): - self.vsphere_dc = value.strip() + params['dc'] = value.strip() elif key.lower() == 'cluster' and value.strip(): - self.vsphere_cluster = value.strip() + params['cluster'] = value.strip() elif re_template.search(key) and value.strip(): - self.template_name = value.strip() + params['template_name'] = value.strip() elif re_excl_ds.search(key) and value.strip(): datastores = re_split_ds.split(value.strip()) - self.excluded_datastores = datastores + params['excluded_ds'] = datastores elif re_min_root_size.search(key) and value.strip(): - val = 0.0 - try: - val = float(value.strip()) - except ValueError as e: - raise ConfigError(self.msg_invalid_type.format( - f=self.config_file, s=section, v=value, n=key, e=e)) - if val < 0: - raise ConfigError(self.msg_val_negative.format( - f=self.config_file, s=section, v=value, n=key)) - self.min_root_size_gb = val + params['min_root_size_gb'] = value elif re_guest_id.search(key) and value.strip(): - self.guest_id = value.strip() + params['guest_id'] = datastores + else: + msg = _( + "Unknown configuration parameter {k!r} with value {v!r} for VSPhere {n!r} " + "found.").format(k=key, v=value, n=vsphere_name) + LOG.warning(msg) + + if self.verbose > 2: + msg = _("Creating a {}-object with parameters:").format('VsphereConfig') + msg += '\n' + pp(params) + LOG.debug(msg) + vsphere = VsphereConfig(**params) + if self.verbose > 2: + LOG.debug(_("Created object:") + '\n' + pp(vsphere.as_dict())) + + vsphere.is_valid(raise_on_error=True) + + self.vsphere[vsphere_name] = vsphere return @@ -586,10 +878,10 @@ class CrTfConfiguration(BaseConfiguration): try: val = int(value.strip()) except ValueError as e: - raise ConfigError(self.msg_invalid_type.format( + raise CrTfConfigError(self.msg_invalid_type.format( f=self.config_file, s=section, v=value, n=key, e=e)) if val < 0: - raise ConfigError(self.msg_val_negative.format( + raise CrTfConfigError(self.msg_val_negative.format( f=self.config_file, s=section, v=value, n=key)) self.pdns_api_port = val elif re_key.search(key) and value.strip(): diff --git a/lib/cr_tf/errors.py b/lib/cr_tf/errors.py index 5fe1cf5..cd26e34 100644 --- a/lib/cr_tf/errors.py +++ b/lib/cr_tf/errors.py @@ -12,9 +12,11 @@ from __future__ import absolute_import # Own modules from fb_tools.errors import FbHandlerError, ExpectedHandlerError +from fb_tools.config import ConfigError + from .xlate import XLATOR -__version__ = '1.0.1' +__version__ = '1.1.0' _ = XLATOR.gettext ngettext = XLATOR.ngettext @@ -57,6 +59,14 @@ class NetworkNotExistingError(ExpectedHandlerError): return msg +# ============================================================================= +class CrTfConfigError(ConfigError): + """Base error class for all exceptions happened during + evaluation of the cofiguration.""" + + pass + + # ============================================================================= if __name__ == "__main__": -- 2.39.5