Dynamische Depotzuweisung (frei)

Einführung

Bei der Standard Multidepot Unterstützung in opsi, sind die Clients den jeweiligen Depots fest zu geordnet. Dies wird nun erweitert durch einen Mechanismus, mit dem ein Client erkennen kann, von welchem Depot er seine Software am schnellsten beziehen kann.

Eine Zuordnung gemäß IP-Nummern ist in vielen Bereichen die einfachste und passende Lösung. In anderen Netzwerktopologien reicht dies nicht aus, z.B. bei einem sternförmigen VPN-Netzwerk.

Notwendig ist also ein Mechanismus der Clientseitig dynamisch ermittelt, zu welchem Depot die beste Verbindung möglich ist. Die konkrete Implementation eines sinnvollen Algorithmus hierfür hängt wiederum sehr von der tatsächlichen Netzwerktopologie und den Wünschen des Kunden ab. Daher ist es sinnvoll diese konfigurierbar zu gestalten.

Ausgehend von der Überlegung, dass der Client sich nach den aktuellen Gegebenheiten des Netzwerks sein Depot sucht, ist sicherzustellen, dass die zur Auswahl stehenden Depots synchron, d.h. mit den selben Software-Paketen ausgestattet, sind. Da in der Praxis nicht alle Depots einer Multidepot-Umgebung immer synchron sein werden, wird die Liste der Depots, aus der sich ein Client das für ihn Geeignetste aussuchen kann, auf jene Depots beschränkt, die zum Masterdepots des Clients synchron sind. Das Masterdepot eines Clients ist das Depot, dem der Client zugewiesen ist. Damit bestimmt das Masterdepot, welche Software in welcher Version auf dem Client installiert werden kann.

Unser Konzept hierzu sieht wie folgt aus:

Auf dem opsi-configserver wird ein Client-Script hinterlegt, welches bei Bedarf auf den Client übertragen und dort interpretiert wird. Dieses Script entscheidet, welches der zur Verfügung stehenden Depots verwendet wird. Für dieses Clientscript ist definiert: die notwendige Schnittstelle mit dem das Script die Liste der zur Auswahl stehenden Server und aktuelle Client-Konfigurationen (IP-Adresse, Netzmaske, Gateway, …​) übernimmt und über die das Script dem Client das Ergebnis des Auswahlprozesses mitteilt, sowie Schnittstellen zum Logging sowie zur Anwenderinformation über den ablaufenden Prozess.

Die konkrete Implementation dieses Scriptes kann dann jederzeit an die konkrete Situation in der jeweiligen opsi-Umgebung angepasst werden.

Der aus diesem Konzept resultierende Ablauf eines Client-Connects sieht dann wie folgt aus:

  1. Der Client meldet sich per Webservice beim opsi-configserver.

  2. Der opsi-configserver übermittelt dem Client die Liste der zu installierenden Software.

  3. Der opsi-configserver übermittelt dem Client das zentral abgelegte Script zur Auswahl des Depotservers sowie die Liste der möglichen Depots.

  4. Der Client führt das Script aus und ermittelt damit das beste Depot.

  5. Der Client verbindet sich mit dem ausgewählten Depotserver, um sich von dort die zu installierende Software zu holen.

  6. Der Installationsstatus wird an den opsi-configserver zurückgemeldet.

Voraussetzungen

Der kofinanzierungs Prozess zur Finanzierung dieser opsi Erweiterung wurde im März 2013 abgeschlossen.
Weitere Details hierzu finden Sie in Freischaltung kostenpflichtiger Module.

Diese Funktion benötigt mindestens folgende Paketstände:

Tabelle 1. Benötigte Pakete
opsi-Paket Version

opsi-client-agent

>=4.0-11

opsi-configed

>=4.0.1.5-1

python-opsi

>=4.0.0.18-1

Die Depotselektion wird über den opsi-client-agent realisiert. Im Umkehrschluss bedeutet dies, dass diese Funktion keine Anwendung bei Netbootprodukten findet.

Konfiguration

Das Script, welches der Client verwendet um die Depotauswahl durchzuführen, ist auf dem Server in der folgenden Datei abgelegt:
/etc/opsi/backendManager/extend.d/70_dynamic_depot.conf

Um die dynamische Depotauswahl für einen Client zu aktivieren, muss für diesen Client folgender Host-Parameter gesetzt werden:
'clientconfig.depot.dynamic = true'

Dies kann über den opsi-configed im Tab Host-Parameter geschehen.

Natürlich kann dies auch auf der Kommandozeile mit dem Befehl opsi-admin erledigt werden (<client-id> ist hierbei durch den FQDN, z.B. client1.uib.local des Clients zu ersetzen):

opsi-admin -d method configState_create clientconfig.depot.dynamic <client-id> [True]

Kontrolliert werden kann die Ausführung mittels:

opsi-admin -d method configState_getObjects [] '{"configId":"clientconfig.depot.dynamic","objectId":"<client-id>"}'

Editieren der Depoteigenschaften

Die Eigenschaften eines Depots werden zum Teil abgefragt, wenn ein opsi-server über den Befehl opsi-setup --register-depot als Depot registriert wird (siehe Erstellung und Konfiguration eines Depot-Servers).

Die Depoteigenschaften können Sie nachträglich editieren. Dies geht sowohl im opsi Management Interface als auch auf der Kommandozeile.

Aufruf der Depoteigenschaften
Abbildung 1. Aufruf der Depoteigenschaften (2. Button von links)

Die Depoteigenschaften rufen Sie über den Button 'Depoteigenschaften' rechts oben im Managementinterface auf.

Depoteigenschaften im opsi-configed
Abbildung 2. Depoteigenschaften im opsi-configed

Auf der Kommandozeile können die Depoteigenschaften ausgegeben werden mit der Methode host_getObjects. Hier z.B. für das Depot 'dep1.uib.local'.

opsi-admin -d method host_getObjects [] '{"id":"dep1.uib.local"}'

Dieses Beispiel ergibt folgende Ausgabe:

[
          {
          "masterDepotId" : "masterdepot.uib.local",
          "ident" : "dep1.uib.local",
          "networkAddress" : "192.168.101.0/255.255.255.0",
          "description" : "Depot 1 an Master Depot",
          "inventoryNumber" : "",
          "ipAddress" : "192.168.105.1",
          "repositoryRemoteUrl" : "webdavs://dep1.uib.local:4447/repository",
          "depotLocalUrl" : "file:///var/lib/opsi/depot",
          "isMasterDepot" : true,
          "notes" : "",
          "hardwareAddress" : "52:54:00:37:c6:8b",
          "maxBandwidth" : 0,
          "repositoryLocalUrl" : "file:///var/lib/opsi/repository",
          "opsiHostKey" : "6a13da751fe76b9298f4ede127280809",
          "type" : "OpsiDepotserver",
          "id" : "dep1.uib.local",
          "depotWebdavUrl" : "webdavs://dep1.uib.local:4447/depot",
          "depotRemoteUrl" : "smb://dep1/opsi_depot"
          }
]

Um die Depoteigenschaften auf der Kommandozeile zu editieren, wird die Ausgabe in eine Datei geschrieben:

opsi-admin -d method host_getObjects [] '{"id":"dep1.uib.local"}' > /tmp/depot_config.json

Die entstandene Datei (/tmp/depot_config.json) kann nun editiert und mit dem folgenden Befehl wieder zurückgeschrieben werden:

opsi-admin -d method host_createObjects < /tmp/depot_config.json

Die im Rahmen der dynamischen Depotzuweisung wichtigen Depoteigenschaften sind:

  • 'isMasterDepot'
    Muss true sein, damit diesem Depot ein Client zugewiesen werden kann. Wird hier false eingetragen, so können keine Clients zugewiesen werden, aber die Depots werden trotzdem bei der dynamischen Depotzuweisung verwendet.

  • 'networkAddress'
    Adresse des Netzwerks für den dieses Depot zuständig ist. Die Netzwerkadresse kann nach zwei Notationen angegeben werden:

    • Netzwerk/Maske Beispiel: 192.168.101.0/255.255.255.0

    • Netzwerk/Maskenbits Beispiel: 192.168.101.0/24

Ob die 'networkAddress' tatsächlich zur Ermittlung des Depots ausgewertet wird, hängt natürlich von dem im Script übergebenen Algorithmus ab. Der von uib ausgelieferte Default-Algorithmus richtet sich nach diesem Kriterium.

Synchronisation der Depots

Um die Depots synchron zu halten, stellt opsi mehrere Werkzeuge bereit:

  • opsi-package-manager

  • opsi-package-updater

Der opsi-package-manager kann bei der Installation eines opsi-Paketes durch die Verwendung der Parameter -d ALL angewiesen werden, das Paket nicht nur auf dem aktuellen Server sondern auf allen bekannten Depots zu installieren. Beispiel:

opsi-package-manager -i opsi-template_1.0-20.opsi -d ALL

Durch die Verwendung des Parameters -D kann der opsi-package-manager angewiesen werden, die Differenzen zwischen Depots aufzulisten. Auch hierbei muss mit der Option -d eine Liste von Depots angegeben oder mit -d ALL auf alle bekannten Depots verwiesen werden. Beispiel:

opsi-package-manager -D -d ALL

Der opsi-package-manager ist also das Werkzeug, um die Synchronisation auf dem 'push' Weg durchzuführen. Dahingegen ist das Werkzeug opsi-package-updater dafür gedacht, um Depots im 'pull' Verfahren zu synchronisieren.

Der opsi-package-updater kann dazu auf den Depots als cronjob laufen. Dies ermöglicht eine einfache Automatisierung. Bitte entnehmen Sie dem Kapitel [opsi-manual-configuration-tools-opsi-package-updater] weitere Informationen zur Konfiguration.

Wird auf einem opsi-server ein Paket mit opsi-package-manager -i installiert (ohne -d), so landet es nicht im repository Verzeichnis. Damit es dorthin kopiert wird, kann man entweder bei der Installation mit -d explizit den Namen des Depots angeben oder mit opsi-package-manager -u <paketname> den upload in das Repository-Verzeichnis explizit anweisen.

Bitte beachten Sie auch die Beschreibung der beiden Werkzeuge in den entsprechenden Kapiteln des opsi-Handbuchs.

Ablauf

Ist für den Client die Verwendung der dynamischen Depotzuweisung über den Host-Parameter 'clientconfig.depot.dynamic' angeschaltet, so lädt dieser über den Webservice vom Server das dort hinterlegte Script und führt es aus.

Das Script, welches der Client verwendet um die Depotauswahl durchzuführen, liegt auf dem Server in der Datei:
/etc/opsi/backendManager/extend.d/70_dynamic_depot.conf

Der in diesem Script definierten Funktion 'selectDepot' werden die folgenden Parameter übergeben:

  • clientConfig
    Informationen zur aktuelle Client-Konfiguration (Hash).
    Die Keys des clientConfig-Hashes sind momentan:

    • "clientId": opsi-Host-ID des Clients (FQDN)

    • "ipAddress": IP-Adresse des Netzwerk-Schnittstelle zum configserver

    • "netmask" : Netzwerk-Maske der Netzwerk-Schnittstelle

    • "defaultGateway": Standard-Gateway

  • masterDepot
    Informationen zum Masterdepot ('opsi-Depotserver'-Objekt). Das Masterdepot ist das Depot, dem der Client im Managementinterface zugewiesen ist. Die Attribute des übergebenen 'opsi-Depotserver'-Objekts entsprechen den Attributen, wie sie von host_getObjects (siehe Editieren der Depoteigenschaften) ausgegeben werden.

  • alternativeDepots
    Informationen zu den alternativen Depots (Liste von 'opsi-Depotserver'-Objekten). Die Liste der alternativen Depots bestimmt sich aus den Depots, welche bezüglich der gerade benötigten Produkte identisch zum Masterdepot sind.

Auf Basis dieser Informationen kann der Algorithmus nun ein Depot aus der Liste auswählen. Das 'opsi-Depotserver'-Objekt des zu verwendenden Depots muss von der Funktion zurückgegeben werden. Findet der Algorithmus kein passendes Depot aus der Liste der alternativen Depots oder ist diese leer, so sollte das Masterdepot zurückgegeben werden.

Template des Auswahlscripts

Im Templatescript sind drei Funktionen zur Auswahl eines Depots vor implementiert.
Die Funktion depotSelectionAlgorithmByNetworkAddress überprüft die Netzwerkadressen der übergebenen Depots und wählt jenes Depot aus, bei dem die eigene aktuelle IP-Nummer im Netz des Depots liegt.
Die Funktion depotSelectionAlgorithmByLatency sendet ICMP Echo-Request-Pakete (ping) an die übergebenen Depots und wählt das Depot mit der niedrigsten Latenzzeit aus.
Die Funktion depotSelectionAlgorithmByMasterDepotAndLatency ist gedacht für Umgebungen mit mehreren Master-Depots, denen ihrerseits weitere Slave-Depots zugeordnet sein können. Es wird dabei aus der Menge von Masterdepot des Clients und den zugehörigen Slave-Depots das Depot ausgewählt, welches die geringste Latenzzeit vorweisen kann.
Die Funktion getDepotSelectionAlgorithmByNetworkAddressBestMatch arbeitet analog zu depotSelectionAlgorithmByNetworkAddress mit der Änderung, dass das am besten passende (also kleinste) Netz bevorzugt wird. Die Funktion getDepotSelectionAlgorithmByRandom wählt unter allen verfügbaren Depots zufällig eins aus. Diese Funktion kann zur Lastverteilung genutzt werden, wobei jedoch besonders darauf geachtet werden sollte, dass alle Depots auf dem gleichen Paketstand arbeiten. Die Funktion getDepotSelectionAlgorithm wird vom Client aufgerufen und gibt den Algorithmus zurück, der für die Auswahl des Depots verwendet werden soll. Ohne Änderung am Templatescript wird hier die Funktion depotSelectionAlgorithmByNetworkAddress zurückgegeben.

Nach einer Änderung des gewählten Algorithmus (durch ein/auskommentieren in getDepotSelectionAlgorithm) muss der opsiconfd neu gestartet werden, damit das neue Verhalten gilt.

# -*- coding: utf-8 -*-

global showDepotInfoFunction
showDepotInfoFunction = \
'''
	def showDepotInfo():
		logger.info("Choosing depot from list of depots:")
		logger.info("   Master depot: %s", masterDepot)
		for alternativeDepot in alternativeDepots:
			logger.info("   Alternative depot: %s", alternativeDepot)
'''

global getDepotWithLowestLatencyFunction
getDepotWithLowestLatencyFunction = \
'''
	def getDepotWithLowestLatency(latency):
		"""
		Given a dict with depot as key and latency as value it will \
return the depot with the lowest latency.

		Will return None if no depot can be determined.
		"""
		selectedDepot = None
		if latency:
			minValue = 1000
			for (depot, value) in latency.items():
				if value < minValue:
					minValue = value
					selectedDepot = depot
			logger.notice("Depot with lowest latency: %s (%0.3f ms)", selectedDepot, minValue*1000)

		return selectedDepot
'''

global getLatencyInformationFunction
getLatencyInformationFunction = \
'''
	def getLatencyInformation(depots):
		"""
		Pings the given depots and returns the latency information in \
a dict with depot as key and the latency as value.

		Depots that can't be reached in time will not be included.
		"""
		from OPSI.Util.Ping import ping
		from urllib.parse import urlparse

		latency = {}
		for depot in depots:
			if not depot.repositoryRemoteUrl:
				logger.info("Skipping {depot} because repositoryRemoteUrl is missing.", depot)
				continue

			try:
				host = urlparse(depot.repositoryRemoteUrl).hostname
				# To increase the timeout (in seconds) for the ping you
				# can implement it in the following way:
				#  depotLatency = ping(host, timeout=5)
				depotLatency = ping(host)

				if depotLatency is None:
					logger.info("Ping to depot %s timed out.", depot)
				else:
					logger.info("Latency of depot %s: %0.3f ms", depot, depotLatency * 1000)
					latency[depot] = depotLatency
			except Exception as e:
				logger.warning(e)

		return latency
'''


def getDepotSelectionAlgorithmByMasterDepotAndLatency(self):
	return '''\
def selectDepot(clientConfig, masterDepot, alternativeDepots=[]):
	{getLatencyInformationFunction}
	{getDepotWithLowestLatencyFunction}
	{showDepotInfoFunction}

	showDepotInfo()

	if alternativeDepots:
		from collections import defaultdict

		# Mapping of depots to its master.
		# key: Master depot
		# value: All slave depots + master
		depotsByMaster = defaultdict(list)

		allDepots = [masterDepot] + alternativeDepots

		for depot in allDepots:
			if depot.masterDepotId:
				depotsByMaster[depot.masterDepotId].append(depot)
			else:
				depotsByMaster[depot.id].append(depot)

		depotsWithLatency = getLatencyInformation(depotsByMaster[masterDepot.id])
		depotWithLowestLatency = getDepotWithLowestLatency(depotsWithLatency)

		if not depotWithLowestLatency:
			logger.info('No depot with lowest latency. Falling back to master depot.')
			depotWithLowestLatency = masterDepot

		return depotWithLowestLatency

	return masterDepot
'''.format(
	showDepotInfoFunction=showDepotInfoFunction,
	getLatencyInformationFunction=getLatencyInformationFunction,
	getDepotWithLowestLatencyFunction=getDepotWithLowestLatencyFunction
)

def getDepotSelectionAlgorithmByLatency(self):
	return '''\
def selectDepot(clientConfig, masterDepot, alternativeDepots=[]):
	{getLatencyInformationFunction}
	{getDepotWithLowestLatencyFunction}
	{showDepotInfoFunction}

	showDepotInfo()

	selectedDepot = masterDepot
	if alternativeDepots:
		depotsWithLatency = getLatencyInformation([masterDepot] + alternativeDepots)
		selectedDepot = getDepotWithLowestLatency(depotsWithLatency)

		if not selectedDepot:
			logger.info('No depot with lowest latency. Falling back to master depot.')
			selectedDepot = masterDepot

	return selectedDepot
'''.format(
	showDepotInfoFunction=showDepotInfoFunction,
	getLatencyInformationFunction=getLatencyInformationFunction,
	getDepotWithLowestLatencyFunction=getDepotWithLowestLatencyFunction
)

def getDepotSelectionAlgorithmByRandom(self):
	return '''\
def selectDepot(clientConfig, masterDepot, alternativeDepots=[]):
	{showDepotInfoFunction}

	showDepotInfo()

	import random

	allDepots = [masterDepot]
	allDepots.extend(alternativeDepots)
	return random.choice(allDepots)
'''.format(
	showDepotInfoFunction=showDepotInfoFunction
)

def getDepotSelectionAlgorithmByNetworkAddress(self):
	return '''\
def selectDepot(clientConfig, masterDepot, alternativeDepots=[]):
	{showDepotInfoFunction}

	showDepotInfo()

	selectedDepot = masterDepot
	if alternativeDepots:
		from OPSI.Util import ipAddressInNetwork

		depots = [masterDepot]
		depots.extend(alternativeDepots)
		for depot in depots:
			if not depot.networkAddress:
				logger.warning("Network address of depot '%s' not known", depot)
				continue

			if ipAddressInNetwork(clientConfig['ipAddress'], depot.networkAddress):
				logger.notice("Choosing depot with networkAddress %s for ip %s", depot.networkAddress, clientConfig['ipAddress'])
				selectedDepot = depot
				break
			else:
				logger.info("IP %s does not match networkAddress %s of depot %s", clientConfig['ipAddress'], depot.networkAddress, depot)

	return selectedDepot
'''.format(
	showDepotInfoFunction=showDepotInfoFunction,
)


def getDepotSelectionAlgorithmByNetworkAddressBestMatch(self):
	return '''\
def selectDepot(clientConfig, masterDepot, alternativeDepots=[]):
	{showDepotInfoFunction}

	showDepotInfo()
	logger.debug("Alternative Depots are: %s", alternativeDepots)
	selectedDepot = masterDepot
	if alternativeDepots:
		from OPSI.Util import ipAddressInNetwork
		import ipaddress

		depots = [masterDepot]
		depots.extend(alternativeDepots)
		logger.debug("All considered Depots are: %s",depots)
		sorted_depots = sorted(depots, key=lambda depot: ipaddress.ip_network(depot.networkAddress), reverse=True)
		logger.debug("Sorted depots: %s", sorted_depots)
		for depot in sorted_depots:
			logger.debug("Considering Depot %s with NetworkAddress %s", depot, depot.networkAddress)
			if not depot.networkAddress:
				logger.warning("Network address of depot '%s' not known", depot)
				continue

			if ipAddressInNetwork(clientConfig['ipAddress'], depot.networkAddress):
				logger.notice("Choosing depot with networkAddress %s for ip %s", depot.networkAddress, clientConfig['ipAddress'])
				selectedDepot = depot
				break
			else:
				logger.info("IP %s does not match networkAddress %s of depot %s", clientConfig['ipAddress'], depot.networkAddress, depot)

	return selectedDepot
'''.format(
	showDepotInfoFunction=showDepotInfoFunction,
)

def getDepotSelectionAlgorithm(self):
	""" Returns the selected depot selection algorythm.	"""
	# return self.getDepotSelectionAlgorithmByMasterDepotAndLatency()
	# return self.getDepotSelectionAlgorithmByLatency()
	return self.getDepotSelectionAlgorithmByNetworkAddress()
	# return self.getDepotSelectionAlgorithmByNetworkAddressBestMatch
	# return self.getDepotSelectionAlgorithmByRandom()

Logging

Wenn die dynamische Depotzuweisung aktiviert ist, so finden sich entsprechende Eintragungen von der Depotauswahl im opsiclientd.log. Hier der Log einer gekürzten Beispielsitzung. In dieser ist der Server bonifax.uib.local Configserver und Masterdepot für den Client pctrydetlef.uib.local. Als Masterserver hat die bonifax hier die Netzwerkaddresse 192.168.1.0/255.255.255.0. Als alternatives Depot steht die stb-40-srv-001.uib.local zur Verfügung mit der Netzwerkaddresse 192.168.2.0/255.255.255.0. Der Client pctry4detlef.uib.local hat die IP-Adresse 192.168.2.109, liegt also im Netz des alternativen Depots.

(...)
[6] [Dec 02 18:25:27] [ opsiclientd                   ] Connection established to: 192.168.1.14   (HTTP.pyo|421)
[5] [Dec 02 18:25:28] [ event processing gui_startup  ]    [ 1] product opsi-client-agent:   setup   (EventProcessing.pyo|446)
[5] [Dec 02 18:25:28] [ event processing gui_startup  ] Start processing action requests   (EventProcessing.pyo|453)
[5] [Dec 02 18:25:28] [ event processing gui_startup  ] Selecting depot for products [u'opsi-client-agent']   (Config.pyo|314)
[5] [Dec 02 18:25:28] [ event processing gui_startup  ] Selecting depot for products [u'opsi-client-agent']   (__init__.pyo|36)
(...)
[6] [Dec 02 18:25:28] [ event processing gui_startup  ] Dynamic depot selection enabled   (__init__.pyo|78)
(...)
[6] [Dec 02 18:25:28] [ event processing gui_startup  ] Master depot for products [u'opsi-client-agent'] is bonifax.uib.local   (__init__.pyo|106)
[6] [Dec 02 18:25:28] [ event processing gui_startup  ] Got alternative depots for products: [u'opsi-client-agent']   (__init__.pyo|110)
[6] [Dec 02 18:25:28] [ event processing gui_startup  ] 1. alternative depot is stb-40-srv-001.uib.local   (__init__.pyo|112)
(...)
[6] [Dec 02 18:25:28] [ event processing gui_startup  ] Verifying modules file signature   (__init__.pyo|129)
[5] [Dec 02 18:25:28] [ event processing gui_startup  ] Modules file signature verified (customer: uib GmbH)   (__init__.pyo|143)
(...)
[6] [Dec 02 18:25:28] [ event processing gui_startup  ] Choosing depot from list of depots:   (<string>|4)
[6] [Dec 02 18:25:28] [ event processing gui_startup  ]    Master depot: <OpsiConfigserver id 'bonifax.uib.local'>   (<string>|5)
[6] [Dec 02 18:25:28] [ event processing gui_startup  ]    Alternative depot: <OpsiDepotserver id 'stb-40-srv-001.uib.local'>   (<string>|7)
[5] [Dec 02 18:25:28] [ event processing gui_startup  ] Choosing depot with networkAddress 192.168.2.0/255.255.255.0 for ip 192.168.2.109   (<string>|40)
[5] [Dec 02 18:25:28] [ event processing gui_startup  ] Selected depot is: <OpsiDepotserver id 'stb-40-srv-001.uib.local'>   (__init__.pyo|171)
(...)
[5] [Dec 02 18:25:28] [ event processing gui_startup  ] Mounting depot share smb://stb-40-srv-001/opsi_depot   (EventProcessing.pyo|415)
(...)