Caso d'uso: Automazione dell'applicazione di patch al sistema operativo mediante un'istanza in hosting automatico

Come organizzazione, desidero automatizzare l'applicazione di patch al sistema operativo per le risorse DBNode di Oracle Base Database Service utilizzando un'istanza self-hosted con Fleet Application Management.

Questo caso d'uso descrive un esempio in cui è necessario applicare patch al sistema operativo per le risorse Oracle Base Database Service. Poiché Oracle Base Database Service non fornisce il supporto nativo per l'applicazione delle patch al sistema operativo, è possibile utilizzare la funzione di istanza auto-ospitata in Fleet Application Management per automatizzare l'applicazione delle patch al sistema operativo sulle risorse DBNode e mantenere aggiornati i sistemi.

Per informazioni sulle istanze in hosting automatico, vedere Istanze in hosting automatico in Fleet Application Management.

Eseguire i passi riportati di seguito per automatizzare l'applicazione di patch al sistema operativo per le risorse DBNode di Oracle Base Database Service utilizzando la funzione di istanza self-hosted in Fleet Application Management.

1. Creare e impostare l'istanza di computazione

Crea un'istanza di computazione self-hosted in Oracle Cloud Infrastructure.

  1. Accedere alla console e connettersi con le credenziali.
  2. Passare alla pagina Istanze di computazione e avviare il workflow Crea istanza.
    • Apri il menu di navigazione e seleziona Computazione. In Computazione, selezionare Istanze.
    • Selezionare Crea istanza.
    • Nome: immettere un nome per l'istanza, ad esempio Self-Hosted-Instance1.
    • Compartimento: selezionare il compartimento in cui si desidera creare l'istanza.
    • Immagine: selezionare un'immagine Oracle Linux (ad esempio, VM.Standard.E4.Flex).
      Nota

      Selezionare una forma di computazione con risorse sufficienti (ad esempio, 2 OCPU, 8 GB di RAM) per gestire l'elaborazione per tenancy di grandi dimensioni. Per ulteriori informazioni, vedere Forme di computazione.
    • Networking: utilizzare una rete cloud virtuale (VCN) esistente o crearne una nuova. Assicurarsi che l'istanza disponga di un indirizzo IP pubblico o sia accessibile (ad esempio, utilizzando un host Bastion).
    • Chiavi SSH: aggiungere la chiave SSH pubblica o generare nuove chiavi per l'accesso sicuro.
    • Selezionare Crea.
  3. Verificare l'istanza: dopo la creazione, verificare che lo stato dell'istanza sia In esecuzione in OCI Console.
  4. Recupera l'indirizzo IP privato dall'istanza di computazione.
    Nella pagina elenco Istanze di computazione, selezionare la nuova istanza e prendere nota dell'indirizzo IP privato.
  5. Connettersi utilizzando SSH.
    • Aprire un terminale sul computer locale.
    • Utilizzare il comando SSH con l'indirizzo IP privato indicato di seguito.

      ssh -i <your-key.pem> opc@instance-ip

      Sostituire your-key.pem e instance-ip con il file e l'indirizzo.

    • Usare un host Bastion o Cloud Shell per connettersi se l'istanza si trova in una subnet privata.
  6. Verificare la connessione.
    Dopo essersi collegati, verificare di aver visualizzato il prompt di Oracle Linux (ad esempio, [opc@Self-Hosted-Instance1 ~]$).
  7. Aggiornare il sistema.
    Eseguire il comando seguente per verificare che il sistema sia aggiornato: sudo yum update -y

2. Abilita autenticazione principal istanza per l'istanza

Impostare l'autenticazione del principal dell'istanza per consentire all'istanza di accedere alle credenziali dell'account SSH e alle chiavi private per la destinazione DBNodes.

  1. Passare alla pagina Gruppi dinamici e avviare il flusso Crea gruppo dinamico.
    • Aprire il menu di navigazione e selezionare Identità e sicurezza. In Identità selezionare Domini.
    • In Domini selezionare il dominio pertinente.
    • Nella pagina dei dettagli selezionare la scheda Gruppi dinamici, quindi selezionare Crea gruppo dinamico.
    • Immettere un nome (ad esempio, FAMS-SelfHost-Scheduler-Mgmt-DG) e una descrizione (ad esempio, un gruppo dinamico per l'istanza creata).
    • Aggiungere una regola di corrispondenza per includere l'istanza in base all'OCID:

      instance.id = 'ocid1.instance.oc1..<your-instance-ocid>'
      Nota

      Per trovare l'OCID dell'istanza, andare alla pagina della lista Istanze di computazione, selezionare l'istanza e copiare l'OCID.
    • Selezionare Crea.
  2. Aggiungere i criteri per il gruppo dinamico.
    • Aprire il menu di navigazione e selezionare Identità e sicurezza. In Identità selezionare Criteri.
    • Selezionare Crea criterio.
      • Immettere un nome (ad esempio, FAMS-SelfHost-Scheduler-Mgmt-DG) e una descrizione (ad esempio, il principal dell'istanza dello script di applicazione patch del sistema operativo).
      • Selezionare il compartimento radice o un altro compartimento, se necessario.
      • Aggiungere le istruzioni criterio riportate di seguito.

        Allow dynamic-group FAMS-SelfHost-Scheduler-Mgmt-DG to {VAULT_READ, SECRET_BUNDLE_READ, OBJECT_INSPECT, OBJECT_READ} in tenancy where any {target.compartment.name in ('Services-Comp1', 'Services-Comp2')}
        Allow dynamic-group FAMS-SelfHost-Scheduler-Mgmt-DG to read database-family in tenancy where any {target.compartment.name in ('Services-Comp1', 'Services-Comp2')}
        Allow dynamic-group FAMS-SelfHost-Scheduler-Mgmt-DG to read vnic in tenancy where any {target.compartment.name in ('Services-Comp1', 'Services-Comp2')}
        Allow dynamic-group FAMS-SelfHost-Scheduler-Mgmt-DG to {FAMS_SCHEDULE_JOB_UPDATE} in tenancy
      • Selezionare Crea.
    Nota

    Verificare l'OCID dell'istanza nelle regole del gruppo dinamico per assicurarsi che l'istanza disponga delle autorizzazioni necessarie. Senza questi criteri, lo script di applicazione delle patch al sistema operativo DBNode non riesce con errori di autorizzazione quando si accede alle API OCI.

3. Crea segreti

Creare un segreto per l'account SSH (opc) ssh_private_key:

4. Preparare lo script di applicazione patch del sistema operativo DBNode nell'istanza

  1. Utilizzare SSH per connettersi all'istanza creata in precedenza.
  2. Copiare lo script di applicazione delle patch del sistema operativo DBNode, ad esempio Script di applicazione delle patch del sistema operativo DBNode di esempio.

    Caricare il file di script (ad esempio, run_os_patching.py) nell'istanza utilizzando scp:

    scp -i your-key.pem run_os_patching.py opc@instance-ip:/home/opc
    sudo mv /home/opc/run_os_patching.py /root/fams_os_patching/run_os_patching.py
  3. Impostare un ambiente:
    • Installare le dipendenze Python.
    • Configurare le credenziali richieste (ad esempio, Jira).
  4. Eseguire il test dello script:
    python3 /root/fams_os_patching/run_os_patching.py
  5. Osservare l'output per verificare l'avanzamento e verificare la presenza di errori.

5. Eseguire lo script utilizzando l'istanza in hosting automatico con un runbook

Eseguire lo script di applicazione delle patch al sistema operativo DBNode utilizzando un'istanza self-hosted in un runbook. Il processo prevede la configurazione dell'istanza, la definizione di un runbook e il monitoraggio del processo.

  1. Assegnare l'istanza creata (Self-Hosted-Instance1) come istanza self-hosted in Fleet Application Management.
    Aggiungere l'istanza come istanza self-hosted selezionandola dalla lista di istanze di computazione. Vedere Creazione di un'istanza in hosting automatico.

    Confermare che l'istanza sia collegata e visibile in Fleet Application Management.

  2. Crea e configura una flotta per l'istanza.
    • Creare una flotta (ad esempio, fams_db-os-patching) e aggiungere l'istanza self-hosted come risorsa. Non è necessario aggiungere prodotti. Vedere Creazione di una Fleet.
    • Assicurarsi che la flotta si trovi nel compartimento appropriato e che sia impostata sul tipo di ambiente Production, se applicabile.
  3. Creare un runbook per l'istanza self-hosted.
    • Creare un runbook, ad esempio fams_os_dbaas_patching_test_runbook, che utilizza l'applicazione di patch come operazione del ciclo di vita. Vedere Creazione di un runbook.
    • Aggiungere un task al runbook per eseguire lo script della shell (ad esempio, /root/fams_os_patching/run_os_dbaas_patching.py) sull'istanza self-hosted:

      sh -c '. /root/fams_os_patching/os_dbaas/bin/activate; set -eu; dbsystemname=""; for arg in "$@"; do case "$arg" in dbsystemname=*) dbsystemname="${arg#dbsystemname=}";; esac; done; : "${dbsystemname:?dbsystemname not provided}"; echo "dbsystemname=${dbsystemname}"; exec python3 /root/fams_os_patching/run_os_dbaas_patching.py --display-name "${dbsystemname}" --option precheck' sh "$@"
    • Salvare il runbook.
  4. Creare un processo di runbook per la flotta utilizzando il runbook.
    • Pianificare o attivare il processo runbook per eseguire lo script di applicazione delle patch del sistema operativo DBNode. Vedere Elaborazione di un runbook.
    • Selezionare la flotta (ad esempio, fams_db-os-patching) dalla pagina della lista Fleets. Vedere Ricerca dei dettagli di una flotta.
    • Creare un processo di job o runbook. Selezionare il runbook appropriato, ad esempio fams_os_dbaas_patching_test_runbook, e l'operazione del ciclo di vita. Vedere Elaborazione di un runbook.
    • Pianificare o eseguire il job immediatamente.
  5. Monitorare i log del processo del runbook.
    • Controllare i log in Fleet Application Management per confermare che lo script sia stato eseguito correttamente. Vedere Recupero dei dettagli del log dei processi del runbook per una flotta.
    • Selezionare il processo del runbook e visualizzarne i log per i messaggi di avanzamento ed errore (ad esempio, l'avanzamento del processo di applicazione delle patch al sistema operativo).

Esempio di script di applicazione delle patch al sistema operativo DBNode

Di seguito è riportato uno script di esempio per applicare le patch al sistema operativo DBNode. Specificare il database display name e selezionare il file options da eseguire, ad esempio il controllo preliminare o l'aggiornamento.

def main():
    """Main function to orchestrate the DBaaS patching process."""
    args = parse_arguments()
    logger.info(f"Starting script with arguments: display-name={args.display_name}, option={args.option}")
    print(f"Starting script with arguments: display-name={args.display_name}, option={args.option}")

    # Get tenancy and region
    tenancy_id, region = get_tenancy_and_region()

    # Initialize OCI clients
    db_client, compute_client, virtual_network_client, identity_client, secrets_client = initialize_oci_clients(region)

    # Get all compartments
    try:
        compartments = oci.pagination.list_call_get_all_results(identity_client.list_compartments, tenancy_id).data
        logger.info(f"Retrieved {len(compartments)} compartments")        
    except oci.exceptions.ServiceError as e:
        logger.error(f"Failed to list compartments: {str(e)}. Exiting program.")
        //handle exception and exit

    # Get DB System by display name
    db_system, compartment_id = get_db_system_by_display_name(db_client, compartments, args.display_name)
    if not db_system:
        //handle exception and exit

    # Get DB Nodes
    db_nodes = get_db_nodes(db_client, compartment_id, db_system.id)
    if not db_nodes:
        //handle exception and exit

    # Retrieve secret for SSH
    secret_id = //handle fetching secrets from vault if required either from arguments or a suitable mechanism 
    private_key_content = get_secret_content(secrets_client, secret_id)
    if not private_key_content:
        //handle exception and exit

    # Process each DB Node
    for node in db_nodes:
        node_ip = get_node_ip(virtual_network_client, node.vnic_id)
        if not node_ip:
            //handle exception and exit

        # Initialize SSH client example using any suitable library based on your use case
        ssh_client = paramiko.SSHClient()
        ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        try:
            logger.info(f"Connecting to {node_ip} as <user>")
            private_key_file = StringIO(private_key_content)
            private_key = paramiko.RSAKey.from_private_key(private_key_file)
            ssh_client.connect(node_ip, username=user, pkey=private_key)
            logger.info(f"Connected to {node_ip}")            
        except Exception as e:
            //handle exception and exit

        # Check DCS agent status and attempt to restart if down
        //handle agent check if required
        ...
		
		# Determine storage type to check if ASM is used if required
        is_asm = identify_storage_type(ssh_client,command)

        # Perform precheck or update
        if args.option == "precheck":
            if not os_update_precheck(ssh_client, node_ip, is_asm):
                //handle exception and exit
            logger.info(f"OS update precheck completed successfully on {node_ip}")            
        elif args.option == "update":
            if not os_update_precheck(ssh_client, node_ip, is_asm):
                //handle exception and exit
            if not os_update(ssh_client, node_ip, is_asm, secrets_client, secret_id):
                //handle exception and exit
            logger.info(f"OS update completed successfully on {node_ip}")            

        ssh_client.close()

def os_update(ssh_client, node_ip, is_asm, secrets_client, secret_id):
    # Pre-patching checks
    if is_asm:
        # Check grid user permissions
        output, error = execute_ssh_command(ssh_client, command, user, sudo=<yes/no>, sudo_user=user)
        if error:
            //handle exception and exit

        # Check CRS status
        output, error = execute_ssh_command(ssh_client, command, user, sudo=<yes/no>)
        if error or output != "<expected outcome>":
            //handle exception and exit
        logger.info(f"CRS is online on {node_ip}")        

        # Check DB processes
        output, error = execute_ssh_command(ssh_client, command, user, sudo=<yes/no>)
        if error or int(output) <= <expected outcome>:
            //handle exception and exit
        logger.info(f"DB services are up on {node_ip} with {output} processes")
        
    else:
        # Check DB processes
        output, error = execute_ssh_command(ssh_client, command, user, sudo=<yes/no>, sudo_user=user)
        if error or int(output) <= <expected outcome>:
            //handle exception and exit
        logger.info(f"DB services are up on {node_ip} with {output} processes")

        # Check alert log for startup
        output, error = execute_ssh_command(ssh_client, command, user, sudo=<yes/no>, sudo_user=user)
        if error or int(output) <= <expected outcome>:
            //handle exception and exit
        logger.info(f"Database startup confirmed in alert log on {node_ip}")

    # Kernel control check
    output, error = execute_ssh_command(ssh_client, command, user, sudo=<yes/no>, sudo_user=user)
    if error:
        //handle exception and exit
    kernel = output
    if "<kernel version 1>" in kernel:
        repo_file = "<version suitable repo>"
    elif "<kernel version 2>" in kernel:
        logger.warning(f"Node {node_ip} is running a version, which is end of life. Skipping OS patching.")
        return False
    else:
        repo_file = "<version suitable repo>"


    # Start OS patching
    output, error = execute_ssh_command(ssh_client, command, user, sudo=<yes/no>)
    if error:
        //handle exception and exit
    if not output:
        logger.error(f"No output from dbcli update server on {node_ip}, cannot proceed. Exiting program.")
        //handle exception and exit

    logger.info(f"dbcli update output: {output}")
    
    try:
        job_data = json.loads(output)
        job_id = job_data.get('jobId')
        if not job_id:
            logger.error(f"No jobId found in dbcli update server output on {node_ip}. Exiting program.")
            //handle exception and exit
        logger.info(f"Update Job ID: {job_id}")
        
    except json.JSONDecodeError:
        //handle exception and exit

    # Monitor job status every 5 minutes for up to 3 hours
    start_time = time.time()
    timeout = 10800  # 3 hours in seconds
    polling_interval = 300  # 5 minutes in seconds
    while True:
        output, error = execute_ssh_command(ssh_client, command, user, sudo=<yes/no>)
        if error:
            logger.error(f"Failed to check job status for {job_id} on {node_ip}: {error}. Exiting program.")
            //handle exception and exit
        if not output:
            logger.error(f"No output from dbcli describe job for {job_id} on {node_ip}. Exiting program.")
            //handle exception and exit
        logger.info(f"Job {job_id} status output: {output}")
        
        try:
            job_data = json.loads(output)
            status = job_data.get('status')
            if not status:
                logger.error(f"No status found in dbcli describe-job output for {job_id} on {node_ip}. Exiting program.")
                //handle exception and exit
            logger.info(f"Job {job_id} status: {status}")
            if status == "Success":
                logger.info(f"OS patching job {job_id} completed successfully on {node_ip}")                
                break
            elif status == "Failure":
                logger.error(f"OS patching job {job_id} failed on {node_ip}. Exiting program.")
                //handle exception and exit
            elif status in ["Running", "InProgress", "In_Progress"]:
                elapsed = time.time() - start_time
                if elapsed > timeout:
                    logger.error(f"OS patching job {job_id} timed out after 3 hours on {node_ip}. Exiting program.")
                    //handle exception and exit
                logger.info(f"Job {job_id} still {status}, checking again in 5 minutes")                
                time.sleep(polling_interval)
            else:
                logger.error(f"Unexpected job status for {job_id} on {node_ip}: {status}. Exiting program.")
                //handle exception and exit
        except json.JSONDecodeError:
            //handle exception and exit

    # Shutdown CRS/DB before reboot
    if is_asm:
        output, error = execute_ssh_command(ssh_client, command, user, sudo=<yes/no>)
        logger.info(f"Pre-reboot CRS status output (as root): {output}")
        if output == <expected outcome>:
            logger.info(f"CRS is up, shutting down CRS on {node_ip} as root")
            if error:
                //handle exception and exit
            time.sleep(120)
        else:
            logger.info(f"CRS is already down on {node_ip}, proceeding with reboot")
            
    else:
        output, error = execute_ssh_command(ssh_client, command, user, sudo=<yes/no>, sudo_user=user)
        logger.info(f"Pre-reboot database processes output: {output}")
        print(f"Pre-reboot database processes output: {output}")
        if output == <expected outcome>:
            logger.info(f"Database is up, shutting down database on {node_ip}")
            if error:
                //handle exception and exit
            output, error = execute_ssh_command(ssh_client, command, user, sudo=<yes/no>, sudo_user=user) # check trace log
            if output != <expected outcome>:
                logger.error(f"Database shutdown incomplete on {node_ip}, expected 'Shutting down instance' in alert log. Exiting program.")
                //handle exception and exit
            time.sleep(120)
        else:
            logger.info(f"Database is already down on {node_ip}, proceeding with reboot")            

    # Reboot the server
    output, error = execute_ssh_command(ssh_client, command, user, sudo=<yes/no>)
    if error:
        //handle exception and exit
    logger.info(f"Initiated reboot on {node_ip}")    
    time.sleep(120)  # Wait for reboot to initiate

    # Check host status with fresh SSH client
    start_time = time.time()
    timeout = 1440  # 24 minutes in seconds
    new_ssh_client = None
    while True:
        new_ssh_client = paramiko.SSHClient()
        new_ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        try:
            //attempt connecting SSH to ensure its online
        except Exception as e:
            //handle exception and exit
        elapsed = time.time() - start_time
        if elapsed > timeout:
            logger.error(f"Node {node_ip} failed to come online after {timeout} seconds. Exiting program.")
            //handle exception and exit
        logger.info(f"{node_ip} not up yet. Waiting 30 seconds...")
        time.sleep(30)

    # Post-reboot wait and checks with new SSH client
    

    # Perform post-reboot service startup if needed
    ...

    # Perform post-reboot checks if required
    ...

    logger.info(f"OS update completed successfully on {node_ip}")
    new_ssh_client.close()
    return True

Per ulteriori informazioni sui comandi DBCLI (Database Command Line Interface), consultare il riferimento a Oracle Database CLI.