← Back to team overview

cloud-init-dev team mailing list archive

Re: [Merge] ~raharper/cloud-init:feature/update-ntp-spec into cloud-init:master

 


Diff comments:

> diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py
> index cbd0237..57a732f 100644
> --- a/cloudinit/config/cc_ntp.py
> +++ b/cloudinit/config/cc_ntp.py
> @@ -94,68 +264,84 @@ schema = {
>  __doc__ = get_schema_doc(schema)  # Supplement python help()
>  
>  
> -def handle(name, cfg, cloud, log, _args):
> -    """Enable and configure ntp."""
> -    if 'ntp' not in cfg:
> -        LOG.debug(
> -            "Skipping module named %s, not present or disabled by cfg", name)
> -        return
> -    ntp_cfg = cfg['ntp']
> -    if ntp_cfg is None:
> -        ntp_cfg = {}  # Allow empty config which will install the package
> +def distro_ntp_client_configs(distro):
> +    """Construct a distro-specific ntp client config dictionary by merging
> +       distro specific changes into base config.
>  
> -    # TODO drop this when validate_cloudconfig_schema is strict=True
> -    if not isinstance(ntp_cfg, (dict)):
> -        raise RuntimeError(
> -            "'ntp' key existed in config, but not a dictionary type,"
> -            " is a {_type} instead".format(_type=type_utils.obj_name(ntp_cfg)))
> -
> -    validate_cloudconfig_schema(cfg, schema)
> -    if ntp_installable():
> -        service_name = 'ntp'
> -        confpath = NTP_CONF
> -        template_name = None
> -        packages = ['ntp']
> -        check_exe = 'ntpd'
> -    else:
> -        service_name = 'systemd-timesyncd'
> -        confpath = TIMESYNCD_CONF
> -        template_name = 'timesyncd.conf'
> -        packages = []
> -        check_exe = '/lib/systemd/systemd-timesyncd'
> -
> -    rename_ntp_conf()
> -    # ensure when ntp is installed it has a configuration file
> -    # to use instead of starting up with packaged defaults
> -    write_ntp_config_template(ntp_cfg, cloud, confpath, template=template_name)
> -    install_ntp(cloud.distro.install_packages, packages=packages,
> -                check_exe=check_exe)
> -
> -    try:
> -        reload_ntp(service_name, systemd=cloud.distro.uses_systemd())
> -    except util.ProcessExecutionError as e:
> -        LOG.exception("Failed to reload/start ntp service: %s", e)
> -        raise
> +    @param distro: String providing the distro class name.
> +    @returns: Dict of distro configurations for ntp clients.
> +    """
> +    dcfg = DISTRO_CLIENT_CONFIG
> +    cfg = copy.deepcopy(NTP_CLIENT_CONFIG)
> +    if distro in dcfg:
> +        cfg = util.mergemanydict([cfg, dcfg[distro]], reverse=True)
> +    return cfg
>  
>  
> -def ntp_installable():
> -    """Check if we can install ntp package
> +def select_ntp_client(usercfg, distro):

Can we make this api more explicit to only take a ntp_client_name instead of the whole config dict since that's the only value or None that we are looking at here?  the callsite  select_ntp_client(ntp_cfg.get('ntp_client', 'auto'), cloud.distro) would work here. Then we know what we are actually relying on without having to read all of select_ntp_client.

> +    """Determine which ntp client is to be used, consulting the distro
> +       for its preference.
>  
> -    Ubuntu-Core systems do not have an ntp package available, so
> -    we always return False.  Other systems require package managers to install
> -    the ntp package If we fail to find one of the package managers, then we
> -    cannot install ntp.
> +    @param usercfg: Dict of user-provided cloud-config.
> +    @param distro: Distro class instance.
> +    @returns: Dict of the selected ntp client or {} if none selected.
>      """
> -    if util.system_is_snappy():
> -        return False
>  
> -    if any(map(util.which, ['apt-get', 'dnf', 'yum', 'zypper'])):
> -        return True
> +    # construct distro-specific ntp_client_config dict
> +    distro_cfg = distro_ntp_client_configs(distro.name)
> +
> +    # user specified client, return its config
> +    if 'ntp_client' in usercfg:

This logic won't work if the cloud-config provides 'ntp_client': 'auto'. Can we change the logic a bit here to permit that value from the user. since we document it?

> +        LOG.debug('Selected NTP client "%s" via user-data configuration',
> +                  usercfg.get('ntp_client'))
> +        return distro_cfg.get(usercfg.get('ntp_client'))
> +
> +    # default to auto if unset in distro
> +    distro_ntp_client = distro.get_option('ntp_client', 'auto')
> +
> +    clientcfg = {}
> +    if distro_ntp_client == "auto":
> +        for client in distro.preferred_ntp_clients:
> +            cfg = distro_cfg.get(client)
> +            if not cfg:
> +                LOG.warning(
> +                    'Skipping NTP client "%s", no configuration available',
> +                    client)
> +                continue
> +            check = cfg.get('check_exe')
> +            if not check:

These checks are on the distro default client configuration, which is all hard-coded in cc_ntp.py right? Shouldn't we instead (or additionally) be validating user-data which could accidentally miss the key definitions? Maybe we just move all ntp configuration validation into a single validate_ntp_config function after we've merged user-provided into distro-provided builtin config as called from handle(). then we can be certain we cover both our builtins and user-config content.

> +                LOG.warning(
> +                    'Skipping NTP client "%s", missing "check_exe" option',
> +                    client)
> +                continue
> +            if util.which(check):
> +                LOG.debug('Selected NTP client "%s", already installed',
> +                          client)
> +                clientcfg = cfg
> +                break
> +
> +        if not clientcfg:
> +            # cloud-init has never automatically installed ntp clients without
> +            # user-data configuring ntp so we must keep the same behavior here.
> +            LOG.debug('ntp_client="auto" and no built-in NTP clients found,'
> +                      'skipping automatic configuration')
> +    else:
> +        LOG.debug('Selected NTP client "%s" via distro system config',
> +                  distro_ntp_client)
> +        clientcfg = distro_cfg.get(distro_ntp_client, {})
> +
> +    return clientcfg
>  
> -    return False
>  
> +def install_ntp_client(install_func, packages=None, check_exe="ntpd"):
> +    """Install ntp client package if not already installed.
>  
> -def install_ntp(install_func, packages=None, check_exe="ntpd"):
> +    @param install_func: function.  This parameter is invoked with the contents
> +    of the packages parameter.
> +    @param packages: list.  This parameter defaults to ['ntp'].
> +    @param check_exec: string.  The name of a binary that indicates the package
> +    the specified package is already installed.
> +    """
>      if util.which(check_exe):
>          return
>      if packages is None:
> @@ -185,34 +379,60 @@ def generate_server_names(distro):
>      return names
>  
>  
> -def write_ntp_config_template(cfg, cloud, path, template=None):
> -    servers = cfg.get('servers', [])
> -    pools = cfg.get('pools', [])
> +def write_ntp_config_template(distro_name, servers=None, pools=None,
> +                              path=None, template_fn=None, template=None):
> +    """Render a ntp client configuration for the specified client.
> +
> +    @param distro_name: string.  The distro class name.
> +    @param servers: A list of strings specifying ntp servers. Defaults to empty
> +    list.
> +    @param pools: A list of strings specifying ntp pools. Defaults to empty
> +    list.
> +    @param path: A string to specify where to write the rendered template.
> +    @param template_fn: A string to specify the template source file.
> +    @param template: A string specifying the contents of the template. This
> +    content will be written to a temporary file before being used to render
> +    the configuration file.
> +
> +    @raises: ValueError when path is None.
> +    @raises: ValueError when template_fn is None and template is None.
> +    """
> +    if not servers:
> +        servers = []
> +    if not pools:
> +        pools = []
>  
>      if len(servers) == 0 and len(pools) == 0:
> -        pools = generate_server_names(cloud.distro.name)
> +        pools = generate_server_names(distro_name)
>          LOG.debug(
>              'Adding distro default ntp pool servers: %s', ','.join(pools))
>  
> -    params = {
> -        'servers': servers,
> -        'pools': pools,
> -    }
> +    if not path:
> +        raise ValueError('Invalid value for path parameter')
>  
> -    if template is None:
> -        template = 'ntp.conf.%s' % cloud.distro.name
> +    if not template_fn and not template:

cloud be:     if not any([template_fn, template]):

> +        raise ValueError('Not template_fn or template provided')

'Not template_fn or template provided'  ->  'Missing values for both template_fn and template'

>  
> -    template_fn = cloud.get_template_filename(template)
> -    if not template_fn:
> -        template_fn = cloud.get_template_filename('ntp.conf')
> -        if not template_fn:
> -            raise RuntimeError(
> -                'No template found, not rendering {path}'.format(path=path))
> +    params = {'servers': servers, 'pools': pools}
> +    if template:
> +        tfile = temp_utils.mkstemp(prefix='template_name-', suffix=".tmpl")
> +        template_fn = tfile[1]  # filepath is second item in tuple
> +        util.write_file(template_fn, content=template)
>  
>      templater.render_to_file(template_fn, path, params)
> +    # clean up temporary template
> +    if template:
> +        util.del_file(template_fn)
>  
>  
>  def reload_ntp(service, systemd=False):
> +    """Restart or reload an ntp system service.
> +
> +    @param service: A string specifying the name of the service to be affected.
> +    @param systemd: A boolean indicating if the distro uses systemd, defaults
> +    to False.
> +    @returns: A tuple of stdout, stderr results from executing the action.
> +    """
>      if systemd:
>          cmd = ['systemctl', 'reload-or-restart', service]
>      else:
> diff --git a/tests/cloud_tests/testcases/modules/ntp_timesyncd.py b/tests/cloud_tests/testcases/modules/ntp_timesyncd.py
> new file mode 100644
> index 0000000..d433eaf
> --- /dev/null
> +++ b/tests/cloud_tests/testcases/modules/ntp_timesyncd.py
> @@ -0,0 +1,22 @@
> +# This file is part of cloud-init. See LICENSE file for license information.
> +
> +"""cloud-init Integration Test Verify Script."""
> +from tests.cloud_tests.testcases import base
> +
> +
> +class TestNtpTimesyncd(base.CloudTestCase):
> +    """Test ntp module with systemd-timesyncd client"""
> +
> +# vi: ts=4 expandtab
> +
> +    def test_timesyncd_conf_exists(self):

could drop this test as we should expect content in test_timesyncd_entries below.

> +        """Test timesyncd configured"""
> +        out = self.get_data_file('timesyncd_conf_exists')
> +        self.assertEqual(0, int(out))
> +
> +    def test_timesyncd_entires(self):

spelling: entries

> +        """Test timesyncd config entries"""
> +        out = self.get_data_file('timesyncd_conf')
> +        self.assertIn('.pool.ntp.org', out)
> +
> +# vi: ts=4 expandtab


-- 
https://code.launchpad.net/~raharper/cloud-init/+git/cloud-init/+merge/339438
Your team cloud-init commiters is requested to review the proposed merge of ~raharper/cloud-init:feature/update-ntp-spec into cloud-init:master.


References