Caso de uso: automatización de la aplicación de parches del sistema operativo mediante una instancia alojada

Como organización, quiero automatizar la aplicación de parches del sistema operativo para los recursos DBNode de Oracle Base Database Service mediante una instancia autoalojada con Fleet Application Management.

En este caso de uso se describe un ejemplo en el que necesita aplicar parches al sistema operativo para los recursos de Oracle Base Database Service. Debido a que Oracle Base Database Service no proporciona soporte nativo para la aplicación de parches del sistema operativo, puede utilizar la función de instancia autoalojada en Fleet Application Management para automatizar la aplicación de parches del sistema operativo en recursos DBNode y mantener los sistemas actualizados.

Para obtener información sobre las instancias autoalojadas, consulte Instancias autoalojadas en Fleet Application Management.

Realice los siguientes pasos para automatizar la aplicación de parches del sistema operativo para los recursos DBNode de Oracle Base Database Service mediante la función de instancia autoalojada en Fleet Application Management:

1. Creación y configuración de la instancia informática

Cree una instancia informática autoalojada en Oracle Cloud Infrastructure.

  1. Acceda a la consola e inicie sesión con sus credenciales.
  2. Navegue a la página de instancias informáticas e inicie el flujo de trabajo Crear instancia.
    • Abra el menú de navegación y seleccione Recursos informáticos. En Recursos informáticos, seleccione Instancias.
    • Seleccione Crear instancia.
    • Nombre: introduzca un nombre para la instancia (por ejemplo, Self-Hosted-Instance1).
    • Compartimento: seleccione el compartimento en la que desea crear la instancia.
    • Image: seleccione una imagen de Oracle Linux (por ejemplo, VM.Standard.E4.Flex).
      Nota

      Seleccione una unidad de computación con suficientes recursos (por ejemplo, 2 OCPU y 8 GB de RAM) para manejar el procesamiento de arrendamientos grandes. Para obtener más información, consulte Unidades de computación.
    • Red: utilice una red virtual en la nube (VCN) existente o cree una nueva. Asegúrese de que la instancia tiene una dirección IP pública o de que se puede acceder a ella (por ejemplo, mediante un host bastión).
    • Claves SSH: agregue la clave SSH pública o genere nuevas claves para un acceso seguro.
    • Seleccione Crear.
  3. Verificar instancia: después de crearla, confirme que el estado de la instancia es En ejecución en la consola de OCI.
  4. Obtenga la dirección IP privada de la instancia informática.
    En la página de lista Instancias informáticas, seleccione la nueva instancia y anote la dirección IP privada.
  5. Conéctese mediante SSH.
    • Abra un terminal en la máquina local.
    • Utilice el comando SSH con la dirección IP privada que anotó:

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

      Sustituya your-key.pem y instance-ip por su archivo y dirección.

    • Utilice un host bastión o Cloud Shell para conectarse si la instancia está en una subred privada.
  6. Verifique la conexión.
    Después de conectarse, verifique que ve la petición de datos de Oracle Linux (por ejemplo, [opc@Self-Hosted-Instance1 ~]$).
  7. Actualice el sistema.
    Ejecute el siguiente comando para asegurarse de que el sistema está actualizado: sudo yum update -y

2. Activar autenticación de principal de instancia para la instancia

Configure la autenticación del principal de instancia para permitir que la instancia acceda a las credenciales de la cuenta SSH y las claves privadas para el destino DBNodes.

  1. Vaya a la página Grupos dinámicos e inicie el flujo Crear grupo dinámico.
    • Abra el menú de navegación y seleccione Identidad y seguridad. En Identidad, seleccione Dominios.
    • En Dominios, seleccione el dominio correspondiente.
    • En la página de detalles, seleccione el separador Grupos dinámicos y, a continuación, seleccione Crear grupo dinámico.
    • Introduzca un nombre (por ejemplo, FAMS-SelfHost-Scheduler-Mgmt-DG) y una descripción (por ejemplo, grupo dinámico para la instancia que ha creado).
    • Agregue una regla de coincidencia para incluir la instancia por OCID:

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

      Para buscar el OCID de la instancia, vaya a la página de lista Instancias informáticas, seleccione la instancia y copie el OCID.
    • Seleccione Crear.
  2. Agregue políticas para el grupo dinámico.
    • Abra el menú de navegación y seleccione Identidad y seguridad. En Identidad, seleccione Políticas.
    • Seleccione Create policy.
      • Introduzca un nombre (por ejemplo, FAMS-SelfHost-Scheduler-Mgmt-DG) y una descripción (por ejemplo, Política para principal de instancia de script de aplicación de parches del sistema operativo).
      • Seleccione el compartimento raíz u otro compartimento si es necesario.
      • Agregue las siguientes sentencias de política:

        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
      • Seleccione Crear.
    Nota

    Verifique el OCID de la instancia en las reglas de grupo dinámico para asegurarse de que la instancia tiene los permisos necesarios. Sin estas políticas, el script de aplicación de parches del sistema operativo DBNode falla con errores de permiso al acceder a las API de OCI.

3. Crear secretos

Cree un secreto para la cuenta SSH (opc) ssh_private_key:

4. Preparación del script de aplicación de parches del sistema operativo DBNode en la instancia

  1. Utilice SSH para conectarse a la instancia que ha creado anteriormente.
  2. Copie la secuencia de comandos de aplicación de parches del sistema operativo DBNode (por ejemplo, Secuencia de comandos de aplicación de parches del sistema operativo DBNode).

    Cargue el archivo de script (por ejemplo, run_os_patching.py) en la instancia mediante 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. Configurar un entorno:
    • Instale las dependencias de Python.
    • Configure las credenciales necesarias (por ejemplo, Jira).
  4. Pruebe a ejecutar el script:
    python3 /root/fams_os_patching/run_os_patching.py
  5. Observe la salida para verificar el progreso y comprobar si hay errores.

5. Ejecución del script mediante la instancia alojada automáticamente con un libro de ejecución

Ejecute el script de aplicación de parches del sistema operativo DBNode mediante una instancia autoalojada en un libro de ejecución. El proceso implica configurar la instancia, definir un libro de ejecución y supervisar el proceso.

  1. Asigne la instancia que ha creado (Self-Hosted-Instance1) como instancia autoalojada en Fleet Application Management.
    Agregue la instancia como una instancia autoalojada seleccionándola de la lista de instancias informáticas. Consulte Creación de una instancia alojada.

    Confirme que la instancia está asociada y visible en Fleet Application Management.

  2. Cree y configure un conjunto para la instancia.
    • Cree un conjunto (por ejemplo, fams_db-os-patching) y agregue la instancia autoalojada como recurso. No es necesario agregar productos. Consulte Creación de un Conjunto.
    • Asegúrese de que el conjunto está en el compartimento adecuado y defina el tipo de entorno Production, si procede.
  3. Cree un libro de ejecución para la instancia autoalojada.
    • Cree un libro de ejecución, por ejemplo, fams_os_dbaas_patching_test_runbook, que utilice la aplicación de parches como operación de ciclo de vida. Consulte Creación de un Runbook.
    • Agregue una tarea al libro de ejecución para ejecutar el script de shell (por ejemplo, /root/fams_os_patching/run_os_dbaas_patching.py) en la instancia autoalojada:

      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 "$@"
    • Guarde el libro de ejecución.
  4. Cree un proceso de libro de ejecución para el conjunto mediante el libro de ejecución.
    • Programe o dispare el proceso de runbook para ejecutar el script de aplicación de parches del sistema operativo DBNode. Consulte Procesamiento de un Runbook.
    • Seleccione el conjunto (por ejemplo, fams_db-os-patching) en la página de lista Fleets. Consulte Obtención de detalles de un conjunto.
    • Creación de un proceso de job o runbook. Seleccione el libro de ejecución adecuado, como fams_os_dbaas_patching_test_runbook, y la operación de ciclo de vida. Consulte Procesamiento de un Runbook.
    • Programa o ejecuta el trabajo inmediatamente.
  5. Control de los registros de proceso del libro de ejecución.
    • Compruebe los logs de Fleet Application Management para confirmar que el script se ha ejecutado correctamente. Consulte Obtención de detalles de log de proceso de libro de ejecución para un conjunto.
    • Seleccione el trabajo de proceso del libro de ejecución y vea sus logs para ver los mensajes de progreso y error (por ejemplo, progreso del trabajo de aplicación de parches del sistema operativo).

Ejemplo de script de aplicación de parches del sistema operativo DBNode

A continuación, se muestra un script de ejemplo para aplicar un parche al sistema operativo DBNode. Especifique la base de datos display name y seleccione el options que desea realizar, como la comprobación previa o la actualización.

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

Para obtener más información sobre los comandos de la interfaz de línea de comandos de la base de datos (DBCLI), consulte la Referencia de la CLI de Oracle Database.