2 Kleine vielleicht nützliche Funktionsideen

Antworten
Michael H.
Beiträge: 12
Registriert: 10 Feb 2016, 11:28

2 Kleine vielleicht nützliche Funktionsideen

Beitrag von Michael H. »

Hallo Forum,
1. Funktionsidee einzelnes Produkt Setup starten.
Ich habe bei einem Client z.B. mehrere Produkte auf Setup stehen die ggf. recht lange dauern und nicht so dringend sind. Da wäre es manchmal vielleicht nützlich ein einzelnes Paket was schnell benötigt wird einzeln starten zu können ohne bei allen anderen das Setup rauszunehmen und im Anschluss wieder reinzunehmen.

2. Funktionsidee Produkte eines Clients kopieren.
Ein User mit PC-A bekommt einen neuen PC-B (hat schon andere Software installiert) oder muss an einen anderen PC-C (hat schon andere Software installiert).
Wenn ich nun will das der User an den anderen PC's arbeiten kann wäre es nützlich einen Rechtsklick auf den Client PC-A zu machen und dort auf Kopieren zu klicken.
Bei PC-B einen Rechtsklick auf "einfügen und ersetzen" werden alle kopierten Produkte sofern noch nicht installiert auf Setup gesetzt und alles was an schon falscher Software installiert ist auf uninstall gesetzt.
Bei PC-C einen Rechtsklick auf "einfügen und ergänzen" werden alle kopierten Produkte sofern noch nicht installiert auf Setup gesetzt und die andere Software die schon installiert ist bleibt auch installiert.

Das ganze dann so oder so ähnlich wäre manchmal nice to have.
Ich hoffe es war verständlich was ich geschrieben habe.

Gruß
Michael
Benutzeravatar
r.roeder
uib-Team
Beiträge: 522
Registriert: 02 Jul 2008, 10:08

Re: 2 Kleine vielleicht nützliche Funktionsideen

Beitrag von r.roeder »

Hallo Michael,

vielen Dank für das Mitdenken und Anregen!

Funktionsidee 2 ist tatsächlich schon länger auf der internen Vorhabenliste. Seit zwei Jahren ist es auch, na sage ich mal (nach außen) zu einem Drittel, von der technischen Basis her zu drei Vierleln realisiert, nur ein bisschen versteckt: Man könnte beim einem PC die gewünschten die Produkte in einer temporären ad-hoc-Produktgruppe zusammenfassen und dann beim neuen PC diese Produktgruppe aufrufen und alle nunmehr selektierten Produkte auf setup setzen. Was fehlt: (a) die Erledigung dieser Schritte mehr im Hintergrund durch Kopieren in eine Konfigurationszwischenablage und die Möglichkeit, die Zwischenablage woanders einzufügen, (b) die Berücksichtigung, wenn ein Produkt auf dem neuen PC bereits installiert ist, (c) das Uninstall für auf dem neuen PC nicht geforderte Produkte. Die Umsetzung dieser Dinge auf der inzwischen vorhandenen technischen Basis steht auf der Prioliste - nur leider oder zum Glück mit vielen anderen Dingen (Zaunpfahl: Aufträge an uib sind die Basis für die Entwicklungsarbeit und erst recht deren Beschleunigung)

Funktionsidee 1 ist auch etwas, was mir durch den Kopf geht, weil ich den Bedarf im Alltag selbst oft erlebe. Vielleicht ließe sich das auch im configed realisieren, indem in die künftige Zwischenablage die etup-Anforderungen aufgenommen werden, aber in der gültigen Konfiguration entfernt werden und statt die eine, direkt auszuführende Aktion gesetzt wird.

Grüße
Rupert
opsi support - uib gmbh
For productive opsi installations we recommend maintainance + support contracts which are the base of opsi development.


Wondering who's using opsi? Have a look at the opsi map: http://opsi.org/opsi-map/.
Andreas T.
Beiträge: 7
Registriert: 10 Mai 2015, 17:38

Re: 2 Kleine vielleicht nützliche Funktionsideen

Beitrag von Andreas T. »

r.roeder hat geschrieben: Funktionsidee 1 ist auch etwas, was mir durch den Kopf geht, weil ich den Bedarf im Alltag selbst oft erlebe. Vielleicht ließe sich das auch im configed realisieren, indem in die künftige Zwischenablage die etup-Anforderungen aufgenommen werden, aber in der gültigen Konfiguration entfernt werden und statt die eine, direkt auszuführende Aktion gesetzt wird.
Wir erleben das auch öfters und machen dann den Workaround (Setup entfernen und wieder neu setzen). Aber das funktioniert mit dem OPSI Kiosk so ja nicht. Gerade da wäre es aber oft praktisch, nur ein einzelnes Paket zu installieren.
dark alex
Beiträge: 322
Registriert: 11 Mär 2015, 10:09

Re: 2 Kleine vielleicht nützliche Funktionsideen

Beitrag von dark alex »

Zu 1. fände ich es besser, wnen man ein event definieren würde, das dann mit einem array aus Paketnamen (Zwecks Multiselektion) gecallt wird.
Es sollte dann alles on_demand installiert werden, das auf setup steht und im call aufgelistet oder eine Abhängigkeit derer Pakete ist.

Wieso? Ganz einfacher praktischer grund:
Wenn der configed sich merken muss, was auf setup steht, muss er das ja nach dem on_demand wieder setzen.
Punkt 1: Wann passiert das? NAch einer fixen Zeit? Nach einem event?
Punkt 2: Was, wenn der configed zwischenzeitlich geschlossen wird?
mattiasmab
Beiträge: 90
Registriert: 29 Jan 2021, 12:17

Re: 2 Kleine vielleicht nützliche Funktionsideen

Beitrag von mattiasmab »

Michael H. hat geschrieben: 26 Apr 2021, 10:25 2. Funktionsidee Produkte eines Clients kopieren.
Also für das kopieren der Properties (productpropertystate und config_state) und erreichen das alle Pakete des Referenzrechners installiert sind, dafür hätte ich etwas anzubieten, dass ich zur Arbeiterleichterung programmiert habe und auch regelmäßig für das Ausrollen von PC-Pools nutze.
Es hätte lediglich zwei Einschränkungen:
  1. Es wird nicht darauf geprüft, ob auf dem Ziel-Client und dessen Depot die Properties bzw. das Paket mit diesen Eigenschaften vorhanden ist (da wir keine weiteren Depotserver haben - könnte aber aus einem der anderen Extension wie 40_admin_tasks.conf entnommen werden.) Solange man sicherstellt, dass auf den Depots, die selben Paketversionen installiert sind, oder nur ein Depot vorhanden ist, ist das also keine wirkliche Einschränkung!
    EDIT: habe ich kurz eingebaut - mangels Depotserver kann ich das aber nicht testen...
  2. Es werden keine Deinstallationen durchgeführt (ein Paket, dass der Zielrechner mehr hat, als der Referenz-Client). Da ich das hauptsächlich für PC-Pools nutzen und lediglich einen Referenz-PC auf viele duplizieren wollen, war das nie eine Anforderung.
Folgend der Code der Extension. Der Inhalt muss dem Backend (wie die diversen Extensions von UIB selbst) als Datei unter /etc/opsi/backendManager/extend.d/ bekannt gemacht werden. Ich habe den Inhalt unter dem Pfad: /etc/opsi/backendManager/extend.d/99_custom_extensions.conf. Den Code sollten auch nicht-Programmierer (aber OPSI-API/CLI-Nutzern) nachvollziehen können und somit sicherstellen können, dass ich hier keinen Blödsinn verbreiten will. Über diverse Typos und/oder den Programmierstiel bitte ich einfach hinwegzusehen :roll: - für (Verbesserungs)Vorschläge bin ich offen.

Der Inhalt:

Code: Alles auswählen

# copy to /etc/opsi/backendManager/extend.d/99_custom_extensions.conf
def custom_hostCopyProperties(self, refHostId, targetHostOrGroupId, doSetup, dryRun=False):
    """
    Change the ip address of a host (The backend has a get, but no set/update method...)
    """
    # TODO:
    # + ClientToDepot only for reference and current user of loop? (many vs. one api call with much unused data) -> only for used clientIds
    # + Check für product if depot of ref and current client differs - cache result
    # + Thow exception at the end if cache is not empty (with reasons)
    # - Use _getClientsOnDepotByHostGroup from opsiconfd\docker\files\etc\opsi\backendManager\extend.d\40_groupActions.conf?!
    # - Why UIB use mostly self._backend.FUNC instead of self.FUNC directly? It's the parent backend... Faster? Non overridden version by Extensions (possible?)?
    from OPSI.Object import ProductOnClient, ProductPropertyState
    from OPSI.Types import forceActionRequest, forceProductId, forceBool, forceHostId
    from functools import lru_cache
    import pprint

    refHostId = forceHostId(refHostId)
    doSetup = forceBool(doSetup)
    # backend = self
    # logger = None
    # dryRun = False

    client_ref = self.host_getObjects(type='OpsiClient', id=refHostId)
    if len(client_ref) != 1:
        # logger.error("Referenace host not found or not unique.")
        from OPSI.Exceptions import BackendMissingDataError
        raise BackendMissingDataError("Reference host with id {0!r} not found".format(refHostId))

    clients_target = []
    if self.host_getObjects(type='OpsiClient', id=targetHostOrGroupId):
        logger.debug("Found host '{}' as OpsiClient".format(targetHostOrGroupId))
        clients_target.append(targetHostOrGroupId)
    elif self.group_getObjects(type='HostGroup', id=targetHostOrGroupId):
        logger.debug("Found host '{}' as OpsiClient".format(targetHostOrGroupId))
        clients_target = [ x.objectId for x in self.objectToGroup_getObjects(groupType="HostGroup", groupId=targetHostOrGroupId) if not x.objectId == refHostId ]
    else:
        # logger.error("Target client/group id not found")
        from OPSI.Exceptions import BackendMissingDataError
        raise BackendMissingDataError("Target host or group with id {0!r} not found".format(targetHostOrGroupId))

    clientToDepot = {
        m['clientId']: m['depotId']
        for m in self.configState_getClientToDepotserver(clientIds=clients_target+[refHostId])
    }
    depotOfReferenceClient = clientToDepot[refHostId]

    _failedSetups = {}
    @lru_cache()
    def depotHasCurrentProduct(backend, reference_depotid, depotid, productid):
        def _add_to_cache(cache, depotid, productid, status):
            if depotid in cache:
                cache[depotid].append(f"{productid} ({status})")
            else:
                cache[depotid] = [f"{productid} ({status})"]
        if depotid == reference_depotid:
            return (True, "")
        prodOnDepotOfClient = backend.productOnDepot_getObjects(productId=productid, depotId=depotid)
        if not prodOnDepotOfClient:
            _add_to_cache(_failedSetups, depotid, productid, "not installed")
            return (False, "missing")
        prodOnDepotOfReference = backend.productOnDepot_getObjects(productId=productid, depotId=reference_depotid)
        pod_target = prodOnDepotOfClient[0] if prodOnDepotOfClient else None # could a productOnDepot exists which is NOT registered as product?!
        pod_reference = prodOnDepotOfReference[0] if prodOnDepotOfReference else None # possible to be none? state without productOnDepot?!
        if pod_target and pod_reference and not (pod_target.packageVersion == pod_reference.packageVersion
                        and pod_target.productVersion == pod_reference.productVersion):
            _add_to_cache(_failedSetups, depotid, productid, "other version installed")
            return (False, "wrong version")
        return (True, "")

    @lru_cache(maxsize=50)
    def getProduct(id):
        prod = self.product_getObjects(id=id)
        return prod[0] if prod else None

    exclude_products = ["opsi-client-agent", "opsi-winst", "opsi-script"] # will be installed by netboot product or automatically by other process
    for clientId in clients_target:
        logger.debug("Processing client \"{}\"".format(clientId))

        productOnClients = []
        if doSetup:
            # Set actionrequest "setup" if product not already installed on client
            logger.debug("Processing products on client...")
            for productOnClient in self.productOnClient_getObjects(clientId=refHostId): #, installationStatus="installed"):
                if productOnClient.installationStatus == "installed" or \
                    productOnClient.actionRequest == "setup":
                    # Skip exluded products
                    if productOnClient.productId in exclude_products: # check only if installed - faster
                        continue
                    # Skip products without setup script (like opsi-winst)
                    prod = getProduct(productOnClient.productId)
                    if prod and not prod.getSetupScript():
                        continue
                    # Check if product is available on depot of current client
                    depotOfClient = clientToDepot[clientId] if clientId in clientToDepot else None # TODO: fail possible?
                    if depotOfClient != depotOfReferenceClient:
                        # Check if product on client depot exists and has current version
                        r = depotHasCurrentProduct(self, depotOfReferenceClient, depotOfClient, productOnClient.productId)
                        if not r[0]:
                            if r[1] == "missing":
                                logger.debug("Product '{}' not installed on depot {} of client {}".format(
                                productOnClient.productId, depotOfClient, clientId))
                            elif r[1] == "wrong version":
                                logger.debug("Product '{}' on depot {} of client {} newer/older than reference".format(
                                    productOnClient.productId, depotOfClient, clientId))
                            continue

                    state_on_client = self.productOnClient_getObjects(clientId=clientId, productId=productOnClient.productId, productType = productOnClient.productType)
                    if not state_on_client or state_on_client[0].installationStatus != "installed":
                        productOnClient_new = ProductOnClient(
                                productId          = productOnClient.productId,
                                productType        = productOnClient.productType,
                                clientId           = clientId,
                                actionRequest      = "setup"
                            )
                        logger.debug("Create productOnClient/setup action request for id '{}' (status not installed)".format(productOnClient.productId))
                        productOnClients.append(productOnClient_new)

        logger.debug("Processing product property states...")
        productPropertyStates = []
        for productPropertyState in self.productPropertyState_getObjects(objectId=refHostId):
            productPropertyState.setObjectId(clientId)
            productPropertyStates.append(productPropertyState)
            # logger.debug("Create productPropertyState {!r}".format(productPropertyState))
            logger.debug("Create productPropertyState for id:'{}'/property:'{}'".format(productPropertyState.productId,productPropertyState.propertyId))
            if doSetup:
                # Skip exluded products
                if productPropertyState.productId in exclude_products:
                    continue
                # Skip if product missing or not in current version on client depot - TODO: Could be a new productid (not installed on ref or already installed on target) - own check required
                depotOfClient = clientToDepot[clientId]
                if depotOfClient != depotOfReferenceClient:
                    # Check if product on client depot exists and has current version
                    r = depotHasCurrentProduct(self, depotOfReferenceClient, depotOfClient, productPropertyState.productId)
                    if not r[0]:
                        if r[1] == "missing":
                            logger.debug("Product '{}' not installed on depot {} of client {}".format(
                            productOnClient.productId, depotOfClient, clientId))
                        elif r[1] == "wrong version":
                            logger.debug("Product '{}' on depot {} of client {} newer/older than reference".format(
                                productOnClient.productId, depotOfClient, clientId))
                        continue
                # Set actionrequest "setup" if a property of an installed product will be changed (must be installed on ref!)
                state_on_client = self.productPropertyState_getObjects(objectId=clientId, productId=productPropertyState.productId, propertyId=productPropertyState.propertyId)
                if state_on_client and len(state_on_client) > 0:
                    state_on_client = state_on_client[0]
                prod_on_client = self.productOnClient_getObjects(clientId=refHostId, productId=productPropertyState.productId)
                isInstalledOnRef = prod_on_client and len(prod_on_client) > 0 and prod_on_client[0].installationStatus == "installed"
                isAlreadyOnSetup = any([True for x in productOnClients if x.productId == productPropertyState.productId])
                if isInstalledOnRef and str(state_on_client) != str(productPropertyState) and not isAlreadyOnSetup:
                    # Get product type from product definition
                    prod = getProduct(productPropertyState.productId)
                    # Skip products without setup script (like opsi-winst)
                    if prod and not prod.getSetupScript():
                        continue
                    # Skip if product type not found
                    prod_type = prod.getType() if prod else None
                    if not prod_type:
                        logger.warning("Could not get product type of product '{}'".format(productPropertyState.productId))
                        continue
                    productOnClient_new = ProductOnClient(
                            # installationStatus = productOnClient_on_c.installationStatus, # 'installed', 'not_installed', 'unknown'
                            # actionProgress     = u"", # freier text - ersetzt gesamten report
                            # lastAction         = u"setup", # text in Klammern beim report (nur wenn kein progress)
                            productId          = productPropertyState.productId,
                            productType        = prod_type,
                            clientId           = clientId,
                            actionRequest      = "setup"
                        )
                    logger.debug("Create productOnClient/setup action request for id '{}' (property changed and not already registered for reinstall)".format(productPropertyState.productId))
                    productOnClients.append(productOnClient_new)

        logger.debug("Processing config states...")
        configStates = []
        for configState in self.configState_getObjects(objectId=refHostId):
            if configState.configId == 'clientconfig.depot.id': # do not change depot id!
                continue
            configState.setObjectId(clientId)
            configStates.append(configState)
            # logger.debug("Create configState {!r}".format(configState))
            logger.debug("Create configState for configId:'{}'".format(configState.configId))

        # ConfigStates first - opsi-linux-bootimage.append could be set and is required to be set before productOnClient for netboot products
        if configStates:
            logger.debug("Updating config states...")
            if dryRun:
                    logger.warning("Running in mode 'dry-run' - will update/insert following objects:\n{!s}".format(pprint.pformat(configStates)))
            else:
                self.configState_createObjects(configStates)
        # ProductPropertyStates next - properties could be used for netboot products!
        if productPropertyStates:
            logger.debug("Updating product property states...")
            if dryRun:
                    logger.warning("Running in mode 'dry-run' - will update/insert following objects:\n{!s}".format(pprint.pformat(productPropertyStates)))
            else:
                self.productPropertyState_createObjects(productPropertyStates)
        # ProductOnClients (action request) as last step
        if productOnClients:
            logger.debug("Updating products on client...")
            if dryRun:
                    logger.warning("Running in mode 'dry-run' - will update/insert following objects:\n{!s}".format(pprint.pformat(productOnClients)))
            else:
                self.productOnClient_updateObjects(productOnClients)

    if len(_failedSetups) > 0:
        from OPSI.Exceptions import OpsiError
        msg = '\n'.join( (f'{k}: {", ".join(v)}' for k,v in _failedSetups.items()) )
        raise OpsiError("Could not copy all states - one or more depots have missing products or other versions installed:\n{0!s}".format(msg))
EDIT1: Optimierungen (nur genutze Clients bei Depots abfragen, product_getObjects cachen)
EDIT2: Depot check hinzugefügt
EDIT3: Fehler beim Check behoben und Prüfung gegen Referenz-Depot anstatt gegen product
EDIT4: Klasse durch innere Funktion mit lru_cache ersetzt (kleiner, einfacher)
EDIT5: Es werden auch gesetzte ActionRequests (nur "Setup") kopiert, nicht nur bereits installierte Produkte

Aufgerufen werden kann es dann wie gewohnt per opsi-admin. Folgend ein paar Beispiele, die die verschiedenen Möglichkeiten aufgreifen:

Code: Alles auswählen

# Properties (Hostparameter und Pakteigenschaften) von einen Client auf einen anderen Client kopieren OHNE Setup von Paketen
opsi-admin -dc method custom_hostCopyProperties REFERENZ_ID ZIEL_ID no

# Properties und Paketinstallationen übernehmen (letztere Parameter steuert die productOnClient Erstellung, wenn das Product auf der Referenz-Maschine installiert ist und auf dem Zielrechner nicht ODER sich beim Kopieren eine Property auf dem Zielrechner ändert und somit eine Neuinstallation gefordert wird).
opsi-admin -dc method custom_hostCopyProperties REFERENZ_ID ZIEL_ID yes

# Vorheriges - jedoch auf alle Client einer Gruppe und nicht nur auf eine ID. Dazu den Namen (die id) der Gruppe angeben - der letzte Parameter ist wieder mit no/yes möglich
# (Der Referenz-PC darf selbst Bestandteil der Zielgruppe sein und wird lediglich übersprungen)
opsi-admin -dc method custom_hostCopyProperties REFERENZ_ID ZIEL_GRUPPE no

# Explizite Beispiele:
opsi-admin -dc method custom_hostCopyProperties refhost.domain.tld targethost.domain.tld yes
opsi-admin -dc method custom_hostCopyProperties refhost.domain.tld pc-pool-01 yes
Zuletzt geändert von mattiasmab am 16 Apr 2022, 19:12, insgesamt 1-mal geändert.
Benutzeravatar
n.doerrer
uib-Team
Beiträge: 107
Registriert: 23 Okt 2020, 16:11

Re: 2 Kleine vielleicht nützliche Funktionsideen

Beitrag von n.doerrer »

zu Punkt 1:
Ist aktuell schon möglich, wenn auch etwas aufwändig. Man kann eine Produktgruppe definieren, die nur aus den aktuell gewünschten Paketen besteht und dann für ein Event diesen ConfigState setzen:
opsiclientd.<event_name>.include_product_group_ids : <Die angelegte Gruppe>

Das ist gewissermaßen ein whitelisting der Produktgruppe, sodass alle nicht enthaltenen Pakete nicht mitinstalliert werden.
Nachher müsste man das include_product_group_ids natürlich wieder rausnehmen.

Ist aufwändig, aber skriptbar mit opsi-admin Befehlen.
Antworten