diff --git a/ansible/group_vars/all/kolla b/ansible/group_vars/all/kolla
index 33aef2c69..932ce4738 100644
--- a/ansible/group_vars/all/kolla
+++ b/ansible/group_vars/all/kolla
@@ -470,7 +470,7 @@ kolla_enable_barbican: "no"
kolla_enable_blazar: "no"
kolla_enable_ceilometer: "no"
kolla_enable_central_logging: "no"
-kolla_enable_chrony: "yes"
+kolla_enable_chrony: "no"
kolla_enable_cinder: "no"
kolla_enable_cinder_backend_iscsi: "{{ kolla_enable_cinder_backend_lvm | bool or kolla_enable_cinder_backend_zfssa_iscsi | bool }}"
kolla_enable_cinder_backend_lvm: "no"
diff --git a/ansible/group_vars/all/time b/ansible/group_vars/all/time
index a2b277c1e..6ab7708e1 100644
--- a/ansible/group_vars/all/time
+++ b/ansible/group_vars/all/time
@@ -6,3 +6,36 @@
# Name of the local timezone.
timezone: "{{ ansible_date_time.tz }}"
+
+###############################################################################
+# Network Time Protocol (NTP).
+
+# Kayobe default time sources
+chrony_ntp_servers_default:
+ - server: pool.ntp.org
+ type: pool
+ options:
+ - option: iburst
+ - option: minpoll
+ val: 8
+
+# List of NTP time sources to configure. Format is a list of dictionaries with
+# the following keys:
+# server: host or pool
+# type: (Optional) Defaults to server. Maps to a time source in the
+# configuration file. Can be one of server, peer, pool.
+# options: (Optional) List of options that depends on type, see Chrony
+# documentation for details.
+# See: https://chrony.tuxfamily.org/doc/4.0/chrony.conf.html
+#
+# Example of configuring a pool and customising the pool specific maxsources
+# option:
+# chrony_ntp_servers:
+# - server: pool.ntp.org
+# type: pool
+# options:
+# - option: maxsources
+# val: 3
+#
+chrony_ntp_servers: "{{ chrony_ntp_servers_default }}"
+###############################################################################
diff --git a/ansible/roles/ntp/defaults/main.yml b/ansible/roles/ntp/defaults/main.yml
new file mode 100644
index 000000000..dea948074
--- /dev/null
+++ b/ansible/roles/ntp/defaults/main.yml
@@ -0,0 +1,6 @@
+---
+ntp_actions: ["validate", "prepare", "deploy"]
+
+ntp_service_disable_list:
+ - ntp.service
+ - systemd-timesyncd.service
diff --git a/ansible/roles/ntp/tasks/deploy.yml b/ansible/roles/ntp/tasks/deploy.yml
new file mode 100644
index 000000000..eee45417c
--- /dev/null
+++ b/ansible/roles/ntp/tasks/deploy.yml
@@ -0,0 +1,4 @@
+---
+- name: Import role to configure chrony
+ import_role:
+ name: mrlesmithjr.chrony
diff --git a/ansible/roles/ntp/tasks/main.yml b/ansible/roles/ntp/tasks/main.yml
new file mode 100644
index 000000000..efb655192
--- /dev/null
+++ b/ansible/roles/ntp/tasks/main.yml
@@ -0,0 +1,12 @@
+---
+- name: Validate configuration
+ include_tasks: validate.yml
+ when: '"validate" in ntp_actions'
+
+- name: Pre-deploy preparation
+ include_tasks: prepare.yml
+ when: '"prepare" in ntp_actions'
+
+- name: Deploy service
+ include_tasks: deploy.yml
+ when: '"deploy" in ntp_actions'
diff --git a/ansible/roles/ntp/tasks/prepare.yml b/ansible/roles/ntp/tasks/prepare.yml
new file mode 100644
index 000000000..b9c2b70d0
--- /dev/null
+++ b/ansible/roles/ntp/tasks/prepare.yml
@@ -0,0 +1,27 @@
+---
+
+- name: Populate service facts.
+ service_facts:
+
+- name: Mask alternative NTP clients to prevent conflicts
+ vars:
+ service_exists: "{{ item in services }}"
+ systemd:
+ name: "{{ item }}"
+ enabled: "{{ 'false' if service_exists else omit }}"
+ masked: true
+ state: "{{ 'stopped' if service_exists else omit }}"
+ become: true
+ with_items: "{{ ntp_service_disable_list }}"
+
+- name: Remove kolla-ansible installed chrony container
+ docker_container:
+ name: chrony
+ state: absent
+ become: true
+ # NOTE(wszumski): There is an ordering issue where on a fresh host, docker
+ # will not have been configured, but if that is the case, the chrony container
+ # can't possibly exist, but trying to execute this unconditionally will fail
+ # with: No module named 'docker' as we have not yet added the docker package
+ # to the kayobe target venv.
+ when: "'docker.service' in services"
diff --git a/ansible/roles/ntp/tasks/validate.yml b/ansible/roles/ntp/tasks/validate.yml
new file mode 100644
index 000000000..0d1b4266e
--- /dev/null
+++ b/ansible/roles/ntp/tasks/validate.yml
@@ -0,0 +1,11 @@
+---
+
+- name: Validate structure of chrony_ntp_servers dictionary
+ assert:
+ that:
+ - chrony_ntp_servers is sequence
+ - chrony_ntp_servers | selectattr('server', 'undefined') | list | length == 0
+ msg: "chrony_ntp_servers set to invalid value"
+ when:
+ - chrony_ntp_servers is defined
+ - chrony_ntp_servers | length > 0
diff --git a/ansible/time.yml b/ansible/time.yml
new file mode 100644
index 000000000..609723fa1
--- /dev/null
+++ b/ansible/time.yml
@@ -0,0 +1,18 @@
+---
+- name: Ensure timezone is configured
+ hosts: seed-hypervisor:seed:overcloud
+ tags:
+ - timezone
+ tasks:
+ - import_role:
+ name: yatesr.timezone
+ become: True
+
+- name: Ensure Chrony is installed and configured
+ hosts: ntp
+ tags:
+ - ntp
+ tasks:
+ - import_role:
+ name: ntp
+ when: not kolla_enable_chrony | bool
diff --git a/ansible/timezone.yml b/ansible/timezone.yml
index 2bf278743..c8b718910 100644
--- a/ansible/timezone.yml
+++ b/ansible/timezone.yml
@@ -1,8 +1,22 @@
---
-- name: Ensure timezone is configured
- hosts: seed-hypervisor:seed:overcloud
- tags:
- - timezone
- roles:
- - role: yatesr.timezone
- become: True
+# Timezone configuration has moved to time.yml.
+# This will be removed in the Xena release.
+
+# NOTE(wszumski): Making this a non-empty playbook has the benefit of
+# silencing the tox syntax check which doesn't like empty playbooks.
+
+- hosts: localhost
+ tasks:
+ - name: Warn about deprecation of this playbook
+ fail:
+ msg: |
+ This playbook has been deprecated, please use time.yml instead.
+ Kayobe should not run this playbook, so if you are seeing this
+ message then either something has gone wrong, or you are trying
+ to run it manually. This playbook will be removed in the Xena
+ release.
+ # NOTE(wszumski): We want this to print a nice big red warning and
+ # not to fail the run.
+ ignore_errors: yes
+
+- import_playbook: time.yml
diff --git a/doc/source/configuration/reference/hosts.rst b/doc/source/configuration/reference/hosts.rst
index 1625a9f4c..86c3c2652 100644
--- a/doc/source/configuration/reference/hosts.rst
+++ b/doc/source/configuration/reference/hosts.rst
@@ -406,23 +406,48 @@ timezone. For example:
NTP
===
+*tags:*
+ | ``ntp``
-Since the Ussuri release, Kayobe no longer supports configuration of an NTP
-daemon on the host, since the ``ntp`` package is no longer available in CentOS
-8.
+Kayobe will configure `Chrony `__ on all hosts in the
+``ntp`` group. The default hosts in this group are::
-Kolla Ansible can deploy a chrony container on overcloud hosts, and from the
-Ussuri release chrony is enabled by default. There is no support for running a
-chrony container on the seed or seed hypervisor hosts.
+.. code-block:: console
-To disable the containerised chrony daemon, set the following in
-``${KAYOBE_CONFIG_PATH}/kolla.yml``:
+ [ntp:children]
+ # Kayobe will configure Chrony on members of this group.
+ seed
+ seed-hypervisor
+ overcloud
+
+This provides a flexible way to opt in or out of having kayobe manage
+the NTP service.
+
+Variables
+---------
+
+Network Time Protocol (NTP) may be configured via variables in
+``${KAYOBE_CONFIG_PATH}/time.yml``. The list of NTP servers is
+configured via ``chrony_ntp_servers``, and by default the ``pool.ntp.org``
+servers are used.
+
+Internally, kayobe uses the the `mrlesmithjr.chrony
+`__ Ansible role. Rather than
+maintain a mapping between the ``kayobe`` and ``mrlesmithjr.chrony`` worlds, all
+variables are simply passed through. This means you can use all variables that
+the role defines. For example to change ``chrony_maxupdateskew`` and override
+the kayobe defaults for ``chrony_ntp_servers``:
.. code-block:: yaml
+ :caption: ``time.yml``
- kolla_enable_chrony: false
-
-.. _configuration-hosts-mdadm:
+ chrony_ntp_servers:
+ - server: 0.debian.pool.ntp.org
+ options:
+ - option: iburst
+ - option: minpoll
+ val: 8
+ chrony_maxupdateskew: 150.0
Software RAID
=============
diff --git a/etc/kayobe/inventory/groups b/etc/kayobe/inventory/groups
index a009693e3..fa1ced47c 100644
--- a/etc/kayobe/inventory/groups
+++ b/etc/kayobe/inventory/groups
@@ -42,7 +42,7 @@ storage
compute
###############################################################################
-# Docker groups.
+# Service groups.
[docker:children]
# Hosts in this group will have Docker installed.
@@ -59,6 +59,12 @@ compute
# registries which may become unsynchronized.
seed
+[ntp:children]
+# Kayobe will configure Chrony on members of this group.
+seed
+seed-hypervisor
+overcloud
+
###############################################################################
# Baremetal compute node groups.
diff --git a/etc/kayobe/time.yml b/etc/kayobe/time.yml
index c0a86d7c7..f5a0cbbfd 100644
--- a/etc/kayobe/time.yml
+++ b/etc/kayobe/time.yml
@@ -7,6 +7,32 @@
# Name of the local timezone.
#timezone:
+###############################################################################
+# Network Time Protocol (NTP).
+
+# Kayobe default time sources
+#chrony_ntp_servers_default:
+
+# List of NTP time sources to configure. Format is a list of dictionaries with
+# the following keys:
+# server: host or pool
+# type: (Optional) Defaults to server. Maps to a time source in the
+# configuration file. Can be one of server, peer, pool.
+# options: (Optional) List of options that depends on type, see Chrony
+# documentation for details.
+# See: https://chrony.tuxfamily.org/doc/4.0/chrony.conf.html
+#
+# Example of configuring a pool and customising the pool specific maxsources
+# option:
+# chrony_ntp_servers:
+# - server: pool.ntp.org
+# type: pool
+# options:
+# - option: maxsources
+# val: 3
+#
+#chrony_ntp_servers:
+
###############################################################################
# Dummy variable to allow Ansible to accept this file.
workaround_ansible_issue_8743: yes
diff --git a/kayobe/cli/commands.py b/kayobe/cli/commands.py
index 8ff034f8b..6b91b7881 100644
--- a/kayobe/cli/commands.py
+++ b/kayobe/cli/commands.py
@@ -414,7 +414,7 @@ class SeedHypervisorHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin,
* Configure user accounts, group associations, and authorised SSH keys.
* Configure the host's network interfaces.
* Set sysctl parameters.
- * Configure timezone.
+ * Configure timezone and ntp.
* Optionally, configure software RAID arrays.
* Optionally, configure encryption.
* Configure LVM volumes.
@@ -453,7 +453,7 @@ class SeedHypervisorHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin,
if parsed_args.wipe_disks:
playbooks += _build_playbook_list("wipe-disks")
playbooks += _build_playbook_list(
- "users", "dev-tools", "network", "sysctl", "timezone",
+ "users", "dev-tools", "network", "sysctl", "time",
"mdadm", "luks", "lvm", "seed-hypervisor-libvirt-host")
self.run_kayobe_playbooks(parsed_args, playbooks,
limit="seed-hypervisor")
@@ -574,7 +574,7 @@ class SeedHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
* Set sysctl parameters.
* Configure IP routing and source NAT.
* Disable bootstrap interface configuration.
- * Configure timezone.
+ * Configure timezone and ntp.
* Optionally, configure software RAID arrays.
* Optionally, configure encryption.
* Configure LVM volumes.
@@ -608,7 +608,7 @@ class SeedHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
playbooks += _build_playbook_list("wipe-disks")
playbooks += _build_playbook_list(
"users", "dev-tools", "disable-selinux", "network",
- "sysctl", "ip-routing", "snat", "disable-glean", "timezone",
+ "sysctl", "ip-routing", "snat", "disable-glean", "time",
"mdadm", "luks", "lvm", "docker-devicemapper",
"kolla-ansible-user", "kolla-pip", "kolla-target-venv")
self.run_kayobe_playbooks(parsed_args, playbooks, limit="seed")
@@ -948,7 +948,7 @@ class OvercloudHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
* Configure the host's network interfaces.
* Set sysctl parameters.
* Disable bootstrap interface configuration.
- * Configure timezone.
+ * Configure timezone and ntp.
* Optionally, configure software RAID arrays.
* Optionally, configure encryption.
* Configure LVM volumes.
@@ -981,7 +981,7 @@ class OvercloudHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
playbooks += _build_playbook_list("wipe-disks")
playbooks += _build_playbook_list(
"users", "dev-tools", "disable-selinux", "network",
- "sysctl", "disable-glean", "disable-cloud-init", "timezone",
+ "sysctl", "disable-glean", "disable-cloud-init", "time",
"mdadm", "luks", "lvm", "docker-devicemapper",
"kolla-ansible-user", "kolla-pip", "kolla-target-venv")
self.run_kayobe_playbooks(parsed_args, playbooks, limit="overcloud")
diff --git a/kayobe/tests/unit/cli/test_commands.py b/kayobe/tests/unit/cli/test_commands.py
index e4b3dcce1..988bc2642 100644
--- a/kayobe/tests/unit/cli/test_commands.py
+++ b/kayobe/tests/unit/cli/test_commands.py
@@ -325,7 +325,7 @@ class TestCase(unittest.TestCase):
utils.get_data_files_path("ansible", "dev-tools.yml"),
utils.get_data_files_path("ansible", "network.yml"),
utils.get_data_files_path("ansible", "sysctl.yml"),
- utils.get_data_files_path("ansible", "timezone.yml"),
+ utils.get_data_files_path("ansible", "time.yml"),
utils.get_data_files_path("ansible", "mdadm.yml"),
utils.get_data_files_path("ansible", "luks.yml"),
utils.get_data_files_path("ansible", "lvm.yml"),
@@ -500,7 +500,7 @@ class TestCase(unittest.TestCase):
utils.get_data_files_path("ansible", "ip-routing.yml"),
utils.get_data_files_path("ansible", "snat.yml"),
utils.get_data_files_path("ansible", "disable-glean.yml"),
- utils.get_data_files_path("ansible", "timezone.yml"),
+ utils.get_data_files_path("ansible", "time.yml"),
utils.get_data_files_path("ansible", "mdadm.yml"),
utils.get_data_files_path("ansible", "luks.yml"),
utils.get_data_files_path("ansible", "lvm.yml"),
@@ -1045,7 +1045,7 @@ class TestCase(unittest.TestCase):
utils.get_data_files_path("ansible", "disable-glean.yml"),
utils.get_data_files_path(
"ansible", "disable-cloud-init.yml"),
- utils.get_data_files_path("ansible", "timezone.yml"),
+ utils.get_data_files_path("ansible", "time.yml"),
utils.get_data_files_path("ansible", "mdadm.yml"),
utils.get_data_files_path("ansible", "luks.yml"),
utils.get_data_files_path("ansible", "lvm.yml"),
diff --git a/playbooks/kayobe-overcloud-host-configure-base/overrides.yml.j2 b/playbooks/kayobe-overcloud-host-configure-base/overrides.yml.j2
index 39b1b6c81..791c38756 100644
--- a/playbooks/kayobe-overcloud-host-configure-base/overrides.yml.j2
+++ b/playbooks/kayobe-overcloud-host-configure-base/overrides.yml.j2
@@ -122,3 +122,11 @@ dnf_custom_repos:
# Enable DNF Automatic.
dnf_automatic_enabled: true
{% endif %}
+
+# Override the default NTP pool
+chrony_ntp_servers:
+ - server: time.cloudflare.com
+ type: pool
+ options:
+ - option: maxsources
+ val: 2
diff --git a/playbooks/kayobe-overcloud-host-configure-base/tests/test_overcloud_host_configure.py b/playbooks/kayobe-overcloud-host-configure-base/tests/test_overcloud_host_configure.py
index 58654cf90..a6662bcd7 100644
--- a/playbooks/kayobe-overcloud-host-configure-base/tests/test_overcloud_host_configure.py
+++ b/playbooks/kayobe-overcloud-host-configure-base/tests/test_overcloud_host_configure.py
@@ -134,6 +134,43 @@ def test_timezone(host):
assert "Pacific/Honolulu" in status
+def test_ntp_alternative_services_disabled(host):
+ # Tests that we don't have any conflicting NTP servers running
+ # NOTE(wszumski): We always mask services even if they don't exist
+ ntpd_service = host.service("ntp")
+ assert ntpd_service.is_masked
+ assert not ntpd_service.is_running
+
+ timesyncd_service = host.service("systemd-timesyncd")
+ assert timesyncd_service.is_masked
+ assert not timesyncd_service.is_running
+
+
+def test_ntp_running(host):
+ # Tests that NTP services are enabled and running
+ assert host.package("chrony").is_installed
+ assert host.service("chronyd").is_enabled
+ assert host.service("chronyd").is_running
+
+
+def test_ntp_non_default_time_server(host):
+ # Tests that the NTP pool has been changed from pool.ntp.org to
+ # time.cloudflare.com
+ if 'centos' in host.system_info.distribution.lower():
+ chrony_config = host.file("/etc/chrony.conf")
+ else:
+ # Debian based distributions use the following path
+ chrony_config = host.file("/etc/chrony/chrony.conf")
+ assert chrony_config.exists
+ assert "time.cloudflare.com" in chrony_config.content_string
+
+
+def test_ntp_clock_synchronized(host):
+ # Tests that the clock is synchronized
+ status_output = host.check_output("timedatectl status")
+ assert "synchronized: yes" in status_output
+
+
@pytest.mark.parametrize('repo', ["AppStream", "BaseOS", "Extras", "epel",
"epel-modular"])
@pytest.mark.skipif(not _is_dnf(), reason="DNF only supported on CentOS 8")
diff --git a/releasenotes/notes/install-chrony-on-the-host-283fa6fdd9cf9b4c.yaml b/releasenotes/notes/install-chrony-on-the-host-283fa6fdd9cf9b4c.yaml
new file mode 100644
index 000000000..acf6587ef
--- /dev/null
+++ b/releasenotes/notes/install-chrony-on-the-host-283fa6fdd9cf9b4c.yaml
@@ -0,0 +1,11 @@
+---
+upgrade:
+ - |
+ Updates the NTP implementation from the chrony container deployed by
+ kolla-ansible to configuring chrony as a host service. Chrony is now
+ installed on all hosts in the ``ntp`` group, which defaults to include
+ the seed, overcloud, and seed-hypervisor groups. On existing deployments,
+ you should run `kayobe overcloud host configure` to migrate from the
+ kolla-ansible deployed container. This can optionally be scoped to just
+ use the ``ntp`` tag. You can continue to use the kolla container by
+ setting `kolla_enable_chrony` to ``true``.
diff --git a/requirements.yml b/requirements.yml
index b879dde18..1d7a5ca48 100644
--- a/requirements.yml
+++ b/requirements.yml
@@ -8,6 +8,8 @@
version: 8438592c84585c86e62ae07e526d3da53629b377
- src: MichaelRigart.interfaces
version: v1.11.1
+- src: mrlesmithjr.chrony
+ version: v0.1.0
- src: mrlesmithjr.manage-lvm
version: v0.1.4
- src: mrlesmithjr.mdadm