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')