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:
-
Der Client meldet sich per Webservice beim opsi-configserver.
-
Der opsi-configserver übermittelt dem Client die Liste der zu installierenden Software.
-
Der opsi-configserver übermittelt dem Client das zentral abgelegte Script zur Auswahl des Depotservers sowie die Liste der möglichen Depots.
-
Der Client führt das Script aus und ermittelt damit das beste Depot.
-
Der Client verbindet sich mit dem ausgewählten Depotserver, um sich von dort die zu installierende Software zu holen.
-
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:
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.
Die Depoteigenschaften rufen Sie über den Button 'Depoteigenschaften' rechts oben im Managementinterface auf.
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'
Musstrue
sein, damit diesem Depot ein Client zugewiesen werden kann. Wird hierfalse
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 vonhost_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)
(...)