]> Frank Brehm's Git Trees - pixelpark/create-terraform.git/commitdiff
Refactored lib/cr_tf/config.py
authorFrank Brehm <frank.brehm@pixelpark.com>
Fri, 27 Sep 2019 16:13:14 +0000 (18:13 +0200)
committerFrank Brehm <frank.brehm@pixelpark.com>
Fri, 27 Sep 2019 16:13:14 +0000 (18:13 +0200)
lib/cr_tf/config.py
lib/cr_tf/errors.py

index af2a2d789f394d94d9b9666917756fbf06a49a8b..0ff942cbaeee185ecc576c763caafc2fc7fd26c6 100644 (file)
@@ -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():
index 5fe1cf5aefdc35937cbf72a9c6c6d65538dbd15c..cd26e34668b672c8c3b78fd17fd3c7cf8dbf274e 100644 (file)
@@ -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__":