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
137 changes: 122 additions & 15 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,10 +487,33 @@ def UpdateFromHostname(self):
self.ScanHostname(output.split("\n"))

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

(status, output) = getstatusoutput("/bin/cat /etc/chrony.conf")
GeraldEV marked this conversation as resolved.
Show resolved Hide resolved
if status == 0:
self.ScanNTPConf(output.split("\n"))

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(

"centos.pool.ntp.org" in server
GeraldEV marked this conversation as resolved.
Show resolved Hide resolved
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,101 @@ 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":
servers = self.data['ntp']['servers']
if self.data['ntp']['method'] == "DHCP":
servers = self.GetDHClientInterfaces()

if servers:
self.StopService("chronyd")
GeraldEV marked this conversation as resolved.
Show resolved Hide resolved
# Single shot time update like: `ntpdate servers[0]`
getoutput("chronyd -q 'server %s iburst'" % servers[0])
# Write the system time (set by the single shot NTP) to the HW clock
getoutput("hwclock -w")
self.StartService("chronyd")

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

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

Data.Inst().NTPServersSet(
[
"0.centos.pool.ntp.org",
GeraldEV marked this conversation as resolved.
Show resolved Hide resolved
"1.centos.pool.ntp.org",
"2.centos.pool.ntp.org",
"3.centos.pool.ntp.org",
]
)
GeraldEV marked this conversation as resolved.
Show resolved Hide resolved

def GetDHClientInterfaces(self):
(status, output) = getstatusoutput("ls /var/lib/xcp/ | grep leases")
if status != 0:
return []

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

return interfaces

def GetDHCPNTPServer(self, interface):
ntpServer = getoutput("grep ntp-servers /var/lib/xcp/dhclient-%s.leases | tail -1 | awk '{{ print ( $3 ) }}'" % interface).strip()[:-1]
GeraldEV marked this conversation as resolved.
Show resolved Hide resolved
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 +832,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 +1058,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 +1242,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
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
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