Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CA-369899: Add NTP options DHCP|Default|Manual|None #35

Merged
merged 8 commits into from
Mar 19, 2024
13 changes: 10 additions & 3 deletions XSConsoleAuth.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@

import XenAPI


if sys.version_info >= (3, 0):
getTimeStamp = time.monotonic
else:
getTimeStamp = time.time


class Auth:
instance = None

Expand Down Expand Up @@ -68,7 +75,7 @@ def IsTestMode(self):

def AuthAge(self):
if self.isAuthenticated:
retVal = time.time() - self.authTimestampSeconds
retVal = getTimeStamp() - self.authTimestampSeconds
else:
raise Exception("Cannot get age - not authenticated")
return retVal
Expand All @@ -77,7 +84,7 @@ def KeepAlive(self):
if self.isAuthenticated:
if self.AuthAge() <= State.Inst().AuthTimeoutSeconds():
# Auth still valid, so update timestamp to now
self.authTimestampSeconds = time.time()
self.authTimestampSeconds = getTimeStamp()

def LoggedInUsername(self):
if (self.isAuthenticated):
Expand Down Expand Up @@ -126,7 +133,7 @@ def ProcessLogin(self, inUsername, inPassword):
if self.testingHost is not None:
# Store password when testing only
self.loggedInPassword = inPassword
self.authTimestampSeconds = time.time()
self.authTimestampSeconds = getTimeStamp()
self.isAuthenticated = True
XSLog('User authenticated successfully')

Expand Down
2 changes: 2 additions & 0 deletions XSConsoleConstants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Default NTP domains are <int>.[centos|xenserver].pool.ntp.org
DEFAULT_NTP_DOMAINS = [".centos.pool.ntp.org", ".xenserver.pool.ntp.org"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion to move the static definition of the default NTP server domain to the constants:

Suggested change
DEFAULT_NTP_DOMAINS = [".centos.pool.ntp.org", ".xenserver.pool.ntp.org"]
CENTOS_NTP_DOMAIN = "centos.pool.ntp.org"
DEFAULT_NTP_DOMAINS = [CENTOS_NTP_DOMAIN, "xenserver.pool.ntp.org"]
DEFAULT_NTP_SERVERS = ["%d.%s" % (i, CENTOS_NTP_DOMAIN) for i in range(4)]

149 changes: 131 additions & 18 deletions XSConsoleData.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@

import XenAPI
import datetime
import subprocess, re, shutil, sys, tempfile, socket, os
import time
import subprocess, re, shutil, sys, tempfile, socket, os, stat
from pprint import pprint
from simpleconfig import SimpleConfigFile

Expand Down Expand Up @@ -486,9 +487,32 @@ def UpdateFromHostname(self):
self.ScanHostname(output.split("\n"))

def UpdateFromNTPConf(self):
(status, output) = getstatusoutput("/bin/cat /etc/chrony.conf")
if status == 0:
self.ScanNTPConf(output.split("\n"))
if not 'ntp' in self.data:
self.data['ntp'] = {}

if os.path.isfile("/etc/chrony.conf"):
with open("/etc/chrony.conf", "r") as confFile:
self.ScanNTPConf(confFile.read().splitlines())

self.data['ntp']['method'] = ""
chronyPerm = os.stat("/etc/dhcp/dhclient.d/chrony.sh").st_mode
if (
chronyPerm & stat.S_IXUSR
and chronyPerm & stat.S_IXGRP
and chronyPerm & stat.S_IXOTH
):
self.data['ntp']['method'] = "DHCP"
elif self.data['ntp']['servers']:
self.data['ntp']['method'] = "Manual"

servers = self.data['ntp']['servers']
if len(servers) == 4 and all(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some coding standards consider the use of magic numbers a defect, I tried to get some references, because I don't understand the meaning of 4 in this context:

Today, I got feedback from a code review and learned something new. It was about "magic numbers", which are unique values with unexplained meaning or multiple occurrences which could (preferably) be replaced with named constants.
https://dev.to/anthonyjdella/magic-numbers-in-programming-19h0

Initially, skipped adding a review comment about it, but now I recognize that the meaning 4 in this case is not obvious to me. Hence, it may deserve an explanatory comment or (and?) the use of a symbolic constant (if that is sufficient)

Copy link
Collaborator

@bernhardkaindl bernhardkaindl Mar 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if len(servers) == 4 and all(
# If 4 NTP servers are set and they are default NTP servers, set method to Default:
if len(servers) == 4 and all(

inDefaultNTPDomains(server)
for server in self.data['ntp']['servers']
):
self.data['ntp']['method'] = "Default"
else:
self.data['ntp']['method'] = "Disabled"

def StringToBool(self, inString):
return inString.lower().startswith('true')
Expand Down Expand Up @@ -528,17 +552,107 @@ def SaveToNTPConf(self):
# Double-check authentication
Auth.Inst().AssertAuthenticated()

file = None
try:
file = open("/etc/chrony.conf", "w")
for other in self.ntp.othercontents([]):
file.write(other+"\n")
for server in self.ntp.servers([]):
file.write("server "+server+" iburst\n")
with open("/etc/chrony.conf", "w") as confFile:
for other in self.ntp.othercontents([]):
confFile.write(other + "\n")
for server in self.ntp.servers([]):
confFile.write("server " + server + " iburst\n")
finally:
if file is not None: file.close()
self.UpdateFromNTPConf()

# Force chronyd to update the time
if self.data['ntp']['method'] != "Disabled":
# Prompt chrony to update the time immediately
getoutput("chronyc makestep")
# Write the system time (set by the single shot NTP) to the HW clock
getoutput("hwclock -w")

def AddDHCPNTP(self):
# Double-check authentication
Auth.Inst().AssertAuthenticated()

oldPermissions = os.stat("/etc/dhcp/dhclient.d/chrony.sh").st_mode
newPermissions = oldPermissions | (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
os.chmod("/etc/dhcp/dhclient.d/chrony.sh", newPermissions)

interfaces = self.GetDHClientInterfaces()
for interface in interfaces:
ntpServer = self.GetDHCPNTPServer(interface)

with open("/var/lib/dhclient/chrony.servers.%s" % interface, "w") as chronyFile:
chronyFile.write("%s iburst prefer\n" % ntpServer)
GeraldEV marked this conversation as resolved.
Show resolved Hide resolved

# Ensure chrony is enabled
self.EnableService("chronyd")
self.EnableService("chrony-wait")

def ResetDefaultNTPServers(self):
# Double-check authentication
Auth.Inst().AssertAuthenticated()

Data.Inst().NTPServersSet(
["%d%s" % (i, DEFAULT_NTP_DOMAINS[0]) for i in range(4)]
Copy link
Collaborator

@bernhardkaindl bernhardkaindl Mar 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, I see the magic number 4 again, it is more clear here.
Here, [0] in DEFAULT_NTP_DOMAINS[0] looks like another non-obvious number to me.

Checking above, I see it means ".centos.pool.ntp.org", so the code would be more obvious to read this way:

CENTOS_NTP_POOL_DOMAIN = ".centos.pool.ntp.org"
DEFAULT_NTP_DOMAINS = [CENTOS_NTP_POOL_DOMAIN, ".xenserver.pool.ntp.org"]
           ["%d%s" % (ntp_host, CENTOS_NTP_POOL_DOMAIN) for ntp_host in range(4)]

Or it could be nice to move the "." to the format string of the host:

CENTOS_NTP_POOL_DOMAIN = "centos.pool.ntp.org"
DEFAULT_NTP_DOMAINS = [CENTOS_NTP_POOL_DOMAIN, "xenserver.pool.ntp.org"]
           ["%d.%s" % (ntp_host, CENTOS_NTP_POOL_DOMAIN) for ntp_host in range(4)]

I know this also affects inDefaultNTPDomains(), but not significantly.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And then use the new constant:

Suggested change
Data.Inst().NTPServersSet(
["%d%s" % (i, DEFAULT_NTP_DOMAINS[0]) for i in range(4)]
Data.Inst().NTPServersSet(DEFAULT_NTP_SERVERS)

)
GeraldEV marked this conversation as resolved.
Show resolved Hide resolved

def GetDHClientInterfaces(self):
try:
leases = [filename for filename in os.listdir("/var/lib/xcp") if "leases" in filename]
except OSError:
return []

pattern = "dhclient-(.*).leases"
interfaces = []
for dhclientFile in leases:
match = re.match(pattern, dhclientFile)
if match:
interfaces.append(match.group(1))

return interfaces

def GetDHCPNTPServer(self, interface):
expectedLeaseFile = "/var/lib/xcp/dhclient-%s.leases" % interface
if not os.path.isfile(expectedLeaseFile):
return None

with open(expectedLeaseFile, "r") as leaseFile:
data = leaseFile.read().splitlines()

ntpServerLines = [line for line in data if "ntp-servers" in line]

# Extract <ip-addr> from " option ntp-servers <ip-addr>;"
try:
ntpServer = ntpServerLines[-1].split()[-1][:-1]
except:
ntpServer = None

return ntpServer

def RemoveDHCPNTP(self):
# Double-check authentication
Auth.Inst().AssertAuthenticated()

oldPermissions = os.stat("/etc/dhcp/dhclient.d/chrony.sh").st_mode
newPermissions = oldPermissions & ~(stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
os.chmod("/etc/dhcp/dhclient.d/chrony.sh", newPermissions)

getstatusoutput("rm -f /var/lib/dhclient/chrony.servers.*")

def SetTimeManually(self, date):
# Double-check authentication
Auth.Inst().AssertAuthenticated()

self.NTPServersSet([])
self.RemoveDHCPNTP()
self.SaveToNTPConf()

self.DisableService("chrony-wait")
GeraldEV marked this conversation as resolved.
Show resolved Hide resolved
self.DisableService("chronyd")

timestring = date.strftime("%Y-%m-%d %H:%M:00")
getstatusoutput("date --set='%s'" % timestring)
getstatusoutput("hwclock --utc --systohc")

def SaveToResolveConf(self):
# Double-check authentication
Auth.Inst().AssertAuthenticated()
Expand Down Expand Up @@ -724,9 +838,6 @@ def ScanHostname(self, inLines):
self.data['sysconfig']['network']['hostname'] = inLines[0]

def ScanNTPConf(self, inLines):
if not 'ntp' in self.data:
self.data['ntp'] = {}

self.data['ntp']['servers'] = []
self.data['ntp']['othercontents'] = []

Expand Down Expand Up @@ -953,6 +1064,12 @@ def DisableManagement(self):
# Network reconfigured so this link is potentially no longer valid
self.session = Auth.Inst().CloseSession(self.session)

def AdjustNTPForStaticNetwork(self):
self.RemoveDHCPNTP()
if not self.data['ntp']['servers']: # No NTP servers after removing DHCP
self.ResetDefaultNTPServers()
self.SaveToNTPConf()

def LocalHostEnable(self):
Auth.Inst().AssertAuthenticatedOrPasswordUnset()
self.RequireSession()
Expand Down Expand Up @@ -1131,10 +1248,6 @@ def StopService(self, service):
if status != 0:
raise Exception(output)

def NTPStatus(self):
status, output = getstatusoutput("/usr/bin/ntpstat")
return output

def SetVerboseBoot(self, inVerbose):
if inVerbose:
name = 'noisy'
Expand Down
4 changes: 2 additions & 2 deletions XSConsoleDialoguePane.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,10 @@ def AddStatusField(self, inName, inValue):
self.AddBodyFieldObj(TextField(str(inName), self.brightColour, Field.FLOW_RIGHT))
self.AddBodyFieldObj(WrappedTextField(str(inValue), self.baseColour, Field.FLOW_RETURN))

def AddInputField(self, inName, inValue, inLabel, inLengthLimit = None):
def AddInputField(self, inName, inValue, inLabel, inLengthLimit = None, inputWidth=INPUT_FIELD_DEFAULT_WIDTH, flow=Field.FLOW_RETURN):
self.AddBodyFieldObj(TextField(str(inName), self.brightColour, Field.FLOW_RIGHT))
self.AddInputFieldObj(InputField(str(inValue), self.highlightColour, self.selectedColour,
Field.FLOW_RETURN, inLengthLimit), inLabel)
flow, inLengthLimit, width=inputWidth), inLabel)

def AddPasswordField(self, inName, inValue, inLabel, inLengthLimit = None):
self.AddBodyFieldObj(TextField(str(inName), self.brightColour, Field.FLOW_RIGHT))
Expand Down
7 changes: 4 additions & 3 deletions XSConsoleFields.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,18 @@ def Width(self):
def Height(self):
return 1

INPUT_FIELD_DEFAULT_WIDTH = 40 # Declared for use in constructor
class InputField(Field):
MIN_WIDTH = 40 # Minimum width for input fields
MIN_WIDTH = 5 # Minimum width for input fields

def __init__(self, text, colour, selectedColour, flow, lengthLimit):
def __init__(self, text, colour, selectedColour, flow, lengthLimit, width=INPUT_FIELD_DEFAULT_WIDTH):
ParamsToAttr()
self.activated = False
self.cursorPos = len(self.text)
self.hideText = False
self.selected = True
self.scrollPos = 0
self.width = self.MIN_WIDTH
self.UpdateWidth(width)
if self.lengthLimit is None:
self.lengthLimit = 4096

Expand Down
1 change: 1 addition & 0 deletions XSConsoleStandard.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from XSConsoleAuth import *
from XSConsoleBases import *
from XSConsoleConfig import *
from XSConsoleConstants import *
from XSConsoleData import *
from XSConsoleDataUtils import *
from XSConsoleDialogueBases import *
Expand Down
21 changes: 14 additions & 7 deletions XSConsoleTerm.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
from XSConsoleRootDialogue import *
from XSConsoleState import *


if sys.version_info >= (3, 0):
getTimeStamp = time.monotonic
else:
getTimeStamp = time.time


class App:
__instance = None

Expand All @@ -53,9 +60,9 @@ def Build(self, inDirs = None):
Importer.ImportRelativeDir(dir)

def Enter(self):
startTime = time.time()
startTime = getTimeStamp()
Data.Inst().Update()
elapsedTime = time.time() - startTime
elapsedTime = getTimeStamp() - startTime
XSLog('Loaded initial xapi and system data in %.3f seconds' % elapsedTime)

doQuit = False
Expand Down Expand Up @@ -158,7 +165,7 @@ def NeedsRefresh(self):
def HandleKeypress(self, inKeypress):
handled = True
Auth.Inst().KeepAlive()
self.lastWakeSeconds = time.time()
self.lastWakeSeconds = getTimeStamp()
if self.layout.TopDialogue().HandleKey(inKeypress):
State.Inst().SaveIfRequired()
self.needsRefresh = True
Expand All @@ -180,7 +187,7 @@ def HandleKeypress(self, inKeypress):

def MainLoop(self):
doQuit= False
startSeconds = time.time()
startSeconds = getTimeStamp()
lastDataUpdateSeconds = startSeconds
lastScreenUpdateSeconds = startSeconds
lastGarbageCollectSeconds = startSeconds
Expand All @@ -193,7 +200,7 @@ def MainLoop(self):
while not doQuit:
self.needsRefresh = False
gotTestCommand = RemoteTest.Inst().Poll()
secondsNow = time.time()
secondsNow = getTimeStamp()
try:
if gotTestCommand:
gotKey = None # Prevent delay whilst waiting for a keypress
Expand All @@ -205,7 +212,7 @@ def MainLoop(self):
XSLog('Entering sleep due to inactivity - xsconsole is now blocked waiting for a keypress')
self.layout.Window(Layout.WIN_MAIN).GetKeyBlocking()
XSLog('Exiting sleep')
self.lastWakeSeconds = time.time()
self.lastWakeSeconds = getTimeStamp()
self.needsRefresh = True
Layout.Inst().PopDialogue()
else:
Expand Down Expand Up @@ -234,7 +241,7 @@ def MainLoop(self):
gotKey = None
break

secondsNow = time.time()
secondsNow = getTimeStamp()
secondsRunning = secondsNow - startSeconds

if data.host.address('') == '' or len(data.derived.managementpifs([])) == 0:
Expand Down
5 changes: 5 additions & 0 deletions XSConsoleUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from pprint import pprint

from XSConsoleBases import *
from XSConsoleConstants import *
from XSConsoleLang import *

# Utils that need to access Data must go in XSConsoleDataUtils,
Expand Down Expand Up @@ -330,3 +331,7 @@ def SRSizeString(cls, inBytes):
def DiskSizeString(cls, inBytes):
return cls.BinarySizeString(inBytes)+' ('+cls.DecimalSizeString(inBytes)+')'


def inDefaultNTPDomains(ntpServer):
return any(ntpServer.endswith(defaultDomain) for defaultDomain in DEFAULT_NTP_DOMAINS)

4 changes: 4 additions & 0 deletions plugins-base/XSFeatureInterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,11 +423,15 @@ def Commit(self):
if self.nic is None:
self.mode = None
data.DisableManagement()
data.AdjustNTPForStaticNetwork()
Layout.Inst().PushDialogue(InfoDialogue(Lang("Removed DHCP NTP server")))
else:
pif = data.host.PIFs()[self.nic]
if self.mode.lower().startswith('static'):
# Comma-separated list of nameserver IPs
dns = ','.join(data.dns.nameservers([]))
data.AdjustNTPForStaticNetwork()
Layout.Inst().PushDialogue(InfoDialogue(Lang("Removed DHCP NTP server")))
else:
dns = ''

Expand Down
Loading
Loading