diff --git a/common/nodes/time-server.xml b/common/nodes/time-server.xml
index 24785b9a6..a90d214d7 100644
--- a/common/nodes/time-server.xml
+++ b/common/nodes/time-server.xml
@@ -12,6 +12,7 @@ https://github.com/Teradata/stacki/blob/master/LICENSE.txt
/opt/stack/bin/stack set attr attr=time.protocol value=chrony
/opt/stack/bin/stack set appliance attr frontend attr=time.protocol value=chrony
+/opt/stack/bin/stack set appliance attr frontend attr=time.orphantype value=parent
/opt/stack/bin/stack report host time &hostname; | /opt/stack/bin/stack report script | sh
diff --git a/common/src/stack/command/stack/commands/report/host/time/__init__.py b/common/src/stack/command/stack/commands/report/host/time/__init__.py
index d39480952..99a0655c1 100644
--- a/common/src/stack/command/stack/commands/report/host/time/__init__.py
+++ b/common/src/stack/command/stack/commands/report/host/time/__init__.py
@@ -12,23 +12,35 @@
from stack.exception import *
## DESIGN CONSIDERATIONS
-# The NTP system set up by default in Stacki has the
+# The timekeeping system set up by default in Stacki has the
# following design considerations.
#
-# 1. The Frontend syncs its own time from external servers. - stored in time.servers attribute
-# 2. The Frontend serves NTP to all backends on all networks that are PXE enabled.
-# 3. By default, the Backends sync time from the frontend.
+# 1. All servers in the cluster, can sync times from 2 sources. Any external server
+# specified in the time.servers attribute, and any servers with time.orphantype value
+# set to "parent"
+# 2. By default, the frontend is configured as a parent, and can serve time to the rest
+# of the cluster.
+# 3. time.servers attribute should ideally be set for the frontend, so that it can pick
+# up
# 4. If the time.servers attribute is set for a backend host,
# then those servers are appended to the list of servers to
# sync from.
# 5. If certain nodes are set as parent servers, they can form a time island, from which
# all other servers can sync
+# 6. If Stacki shouldn't be managing time at all, then the time.protocol attribute can
+# be unset, and the admin can manage the time by themselves
class Command(stack.commands.HostArgumentProcessor,
stack.commands.NetworkArgumentProcessor,
stack.commands.report.command):
"""
Create a time configuration report (NTP or chrony).
+ At a minimum at least one server in the cluster has to be designated as a "parent"
+ time server. This ensures that there's atleast one server in the cluster that can
+ serve time. By default, the stacki frontend is a parent timekeeper.
+ Ideally, a "parent" time server will also have the "time.servers" attribute set
+ to talk to an external time server, so that the cluster is in sync with the external
+ time-keeping entity.
Relevant Attributes:
@@ -36,13 +48,16 @@ class Command(stack.commands.HostArgumentProcessor,
or resolvable names) that dictate the servers to use for time-keeping.
This list is in *addition* to the Stacki frontend.
- time.orphantype - Optional - Only the value of "parent" affects the behaviour
- if the NTP service. If a host is designated as a parent Orphan-type,
- that means this host can co-ordinate time with other peers to maintain
+ time.orphantype - Required for at-least one host in the cluster.
+ Only the value of "parent" affects the behaviour
+ of the NTP service. If a host is designated as a
+ parent Orphan-type, that means this host can
+ co-ordinate time with other peers to maintain
time on the time island.
time.protocol - Optional - Can take the value of "chrony" or "ntp". Default
- Stacki behaviour is to use chrony for the hosts.
+ Stacki behaviour is to use chrony for all hosts, except SLES 11 hosts.
+ SLES 11 hosts use NTP by default
Host name of machine
@@ -50,13 +65,38 @@ class Command(stack.commands.HostArgumentProcessor,
Create a time configuration file for backend-0-0.
+
+ *Configuration*
+ Scenario 1: Use Frontend for time keeping
+ If the desired behavior is to use the frontend as the primary, and only time server,
+ no configuration changes have to be made in Stacki. This is the default behavior.
+
+ Scenario 2: Use frontend for time keeping, in sync with an external time server.
+ Set time.servers attribute on the frontend to the IP Address, or Hostname of an external time server.
+ This will set the frontend to sync time against an external time server, and all other hosts to
+ sync time from the frontend.
+
+ Scenario 3: Sync time on all hosts using an external time server only.
+ Unset the frontend appliance attribute of time.orphantype. This will disable the frontend
+ from serving time.
+ Set the time.servers attribute for all hosts to the IP address, or Hostname of external
+ time server. This will require that all hosts can contact, and connect to the external time
+ server.
+
+ Scenario 4: Sync time on some hosts from external time servers, and create a time island.
+ Set some of the hosts' time.orphantype attribute to parent. Then set those hosts' time.servers
+ attribute to the IP address, or Hostname of external time server. The list of parent time servers
+ can include the frontend or not.
+
+ Scenario 5: Don't use Stacki to sync time.
+ To do this, make sure that the time.protocol attribute is not set for a host or a set of hosts.
"""
def getNTPPeers(self, host):
peerlist = []
- parents = [ h for h in self.attrs if ( self.attrs[h].get('time.orphantype') == 'parent' and h != host and h != self.frontend ) ]
+ parents = [ h for h in self.attrs if ( self.attrs[h].get('time.orphantype') == 'parent' and h != host) ]
if parents:
op = self.call('list.host.interface', parents + ['expanded=true'])
networks = self.getNTPNetworkNames(host)
@@ -65,6 +105,7 @@ def getNTPPeers(self, host):
for i in n:
if i not in peerlist:
peerlist.append(i)
+
return peerlist
def getNTPNetworkNames(self, host):
@@ -82,7 +123,8 @@ def getNTPNetworkNames(self, host):
if ntp_net_extra:
for net in ntp_net_extra.split(','):
if net not in stacki_networks:
- raise CommandError(self, f"Network {net} is unknown to Stacki. Fix 'time.networks' attribute for host {host}")
+ raise CommandError(self, f"Network {net} is unknown to Stacki\n" + \
+ "Fix 'time.networks' attribute for host {host}")
networks.append(net)
return networks
@@ -103,20 +145,11 @@ def getNTPServers(self, host):
if timeservers:
timeservers = timeservers.split(",")
- if self.appliance == 'frontend':
- if not timeservers:
- timeservers = [self.attrs[host].get('Kickstart_PublicNTPHost')]
- else:
- n = []
- output = self.call('list.host.interface', [ host , 'expanded=true'])
- for o in output:
- if o['network'] in self.frontend_ntp_addrs:
- n.append(self.frontend_ntp_addrs[o['network']])
- if not timeservers:
- timeservers = n
- else:
- timeservers = n + timeservers
+ if not timeservers:
+ timeservers = []
+ for peer in self.getNTPPeers(host):
+ timeservers.append(peer)
return timeservers
def set_timezone(self, host):
@@ -163,12 +196,17 @@ def run(self, params, args):
self.appliance = self.attrs[host].get('appliance')
self.osversion = self.attrs[host].get('os.version')
+ if protocol == None:
+ continue
+
if self.osversion == '11.x':
protocol = 'ntp'
self.timeservers = self.getNTPServers(host)
if len(self.timeservers) == 0:
- raise CommandError(self, f'No time servers specified for host {host}. Check network interface assignments')
+ if not 'time.orphantype' in self.attrs[host] or not self.attrs[host]['time.orphantype'] == 'parent':
+ raise CommandError(self, f'No time servers specified for host {host}\n' +
+ 'Check time.* attributes, and network interface assignments')
self.runImplementation('time_%s' % protocol, (host))
diff --git a/common/src/stack/command/stack/commands/report/host/time/imp_time_chrony.py b/common/src/stack/command/stack/commands/report/host/time/imp_time_chrony.py
index 75b239001..88a11e7c1 100644
--- a/common/src/stack/command/stack/commands/report/host/time/imp_time_chrony.py
+++ b/common/src/stack/command/stack/commands/report/host/time/imp_time_chrony.py
@@ -93,7 +93,8 @@ def run(self, host):
else:
self.client(host)
- self.owner.addOutput(host, "/usr/sbin/chronyd -q 'server %s iburst'" % self.owner.timeservers[0])
+ if self.owner.timeservers:
+ self.owner.addOutput(host, "/usr/sbin/chronyd -q 'server %s iburst'" % self.owner.timeservers[0])
self.owner.addOutput(host, "systemctl enable chronyd")
self.owner.addOutput(host, 'systemctl start chronyd')
diff --git a/common/src/stack/command/stack/commands/report/host/time/imp_time_ntp.py b/common/src/stack/command/stack/commands/report/host/time/imp_time_ntp.py
index 930a5e7bb..631c8cda4 100644
--- a/common/src/stack/command/stack/commands/report/host/time/imp_time_ntp.py
+++ b/common/src/stack/command/stack/commands/report/host/time/imp_time_ntp.py
@@ -125,7 +125,8 @@ def run(self, host):
#
# set the clock right now
#
- self.owner.addOutput(host, '/usr/sbin/ntpdate %s' % self.owner.timeservers[0])
+ if self.owner.timeservers:
+ self.owner.addOutput(host, '/usr/sbin/ntpdate %s' % self.owner.timeservers[0])
# Restart the NTPD service
if self.owner.osversion == '11.x':
self.owner.addOutput(host, 'service ntp start')