Cas d'utilisation : automatiser l'application de patches au système d'exploitation à l'aide d'une instance auto-hébergée

En tant qu'organisation, je veux automatiser l'application de patches de système d'exploitation pour les ressources Oracle Base Database Service DBNode à l'aide d'une instance auto-hébergée avec Fleet Application Management.

Ce cas d'emploi décrit un exemple dans lequel vous devez appliquer des patches au système d'exploitation pour les ressources Oracle Base Database Service. Etant donné qu'Oracle Base Database Service ne fournit pas de prise en charge native de l'application de patches au système d'exploitation, vous pouvez utiliser la fonctionnalité d'instance auto-hébergée dans Fleet Application Management pour automatiser l'application de patches au système d'exploitation sur les ressources DBNode et maintenir les systèmes à jour.

Pour plus d'informations sur les instances auto-hébergées, reportez-vous à Instances auto-hébergées dans Fleet Application Management.

Pour automatiser l'application de patches au système d'exploitation pour les ressources Oracle Base Database Service DBNode à l'aide de la fonctionnalité d'instance auto-hébergée dans Fleet Application Management, procédez comme suit :

1. Créer et configurer l'instance Compute

Créez une instance Compute auto-hébergée dans Oracle Cloud Infrastructure.

  1. Accédez à la console et connectez-vous avec vos informations d'identification.
  2. Accédez à la page Instances de calcul et lancez le workflow Créer une instance.
    • Ouvrez le menu de navigation et sélectionnez Compute. Sous Compute, sélectionnez Instances.
    • Sélectionnez Créer une instance.
    • Nom : entrez le nom de l'instance (par exemple, Self-Hosted-Instance1).
    • Compartiment : sélectionnez le compartiment vers lequel créer l'instance.
    • Image : sélectionnez une image Oracle Linux (par exemple, VM.Standard.E4.Flex).
      Remarque

      Sélectionnez une forme de calcul avec suffisamment de ressources (par exemple, 2 OCPU, 8 Go de RAM) pour gérer le traitement des locations volumineuses. Pour plus d'informations, reportez-vous à Formes de calcul.
    • Mise en réseau : utilisez un réseau cloud virtuel (VCN) existant ou créez-en un nouveau. Assurez-vous que l'instance dispose d'une adresse IP publique ou qu'elle est accessible (par exemple, en utilisant un bastion).
    • Clés SSH : ajoutez votre clé SSH publique ou générez de nouvelles clés pour un accès sécurisé.
    • Choisissez Créer.
  3. Vérifier l'instance : après la création, vérifiez que l'état de l'instance est En cours d'exécution dans la console OCI.
  4. Obtenez l'adresse IP privée à partir de l'instance Compute.
    Sur la page de liste Instances de calcul, sélectionnez la nouvelle instance et notez l'adresse IP privée.
  5. Connectez-vous via SSH.
    • Ouvrez un terminal sur l'ordinateur local.
    • Utilisez la commande SSH avec l'adresse IP privée que vous avez indiquée :

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

      Remplacez your-key.pem et instance-ip par votre fichier et votre adresse.

    • Utilisez un bastion ou Cloud Shell pour vous connecter si l'instance se trouve dans un sous-réseau privé.
  6. Vérifiez la connexion.
    Une fois connecté, vérifiez que l'invite Oracle Linux (par exemple, [opc@Self-Hosted-Instance1 ~]$) s'affiche.
  7. Mettez à jour le système.
    Exécutez la commande suivante pour vous assurer que le système est à jour : sudo yum update -y

2. Activer l'authentification du principal d'instance pour l'instance

Configurez l'authentification de principal d'instance afin de permettre à l'instance d'accéder aux informations d'identification de compte SSH et aux clés privées pour la cible DBNodes.

  1. Accédez à la page Groupes dynamiques et démarrez le flux Créer un groupe dynamique.
    • Ouvrez le menu de navigation et sélectionnez Identité et sécurité. Sous Identité, sélectionnez Domaines.
    • Dans Domaines, sélectionnez le domaine approprié.
    • Sur la page de détails, sélectionnez l'onglet Groupes dynamiques, puis Créer un groupe dynamique.
    • Entrez un nom (par exemple, FAMS-SelfHost-Scheduler-Mgmt-DG) et une description (par exemple, groupe dynamique pour l'instance que vous avez créée).
    • Ajoutez une règle de mise en correspondance pour inclure votre instance par OCID :

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

      Pour rechercher l'OCID d'instance, accédez à la page de liste Instances Compute, sélectionnez l'instance et copiez l'OCID.
    • Choisissez Créer.
  2. Ajoutez des stratégies pour le groupe dynamique.
    • Ouvrez le menu de navigation et sélectionnez Identité et sécurité. Sous Identité, sélectionnez Stratégies.
    • Sélectionnez Créer une règle.
      • Entrez un nom (par exemple, FAMS-SelfHost-Scheduler-Mgmt-DG) et une description (par exemple, Stratégie pour le principal d'instance de script d'application de patches de système d'exploitation).
      • Sélectionnez le compartiment racine ou un autre compartiment si nécessaire.
      • Ajoutez les instructions suivantes de stratégie :

        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
      • Choisissez Créer.
    Remarque

    Vérifiez l'OCID d'instance dans les règles de groupe dynamique pour vous assurer que l'instance dispose des droits d'accès requis. Sans ces stratégies, le script d'application de patches au système d'exploitation DBNode échoue avec des erreurs de droit d'accès lors de l'accès aux API OCI.

3. Créer des clés secrètes

Créez une clé secrète pour le compte SSH (opc) ssh_private_key :

4. Préparation du script d'application de patches au système d'exploitation DBNode sur l'instance

  1. Utilisez SSH pour vous connecter à l'instance que vous avez créée précédemment.
  2. Copiez le script d'application de patches au système d'exploitation DBNode (par exemple, Exemple de script d'application de patches au système d'exploitation DBNode).

    Téléchargez le fichier de script (par exemple, run_os_patching.py) vers l'instance à l'aide de 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. Paramétrer un environnement :
    • Installez les dépendances Python.
    • Configurez les informations d'identification requises (par exemple, Jira).
  4. Testez le script :
    python3 /root/fams_os_patching/run_os_patching.py
  5. Observez la sortie pour vérifier la progression et rechercher les erreurs.

5. Exécution du script à l'aide de l'instance auto-hébergée avec un guide d'exécution

Exécutez le script d'application de patches au système d'exploitation DBNode à l'aide d'une instance auto-hébergée dans un guide d'exploitation. Le traitement consiste à configurer l'instance, à définir un guide d'exécution et à surveiller le traitement.

  1. Affectez l'instance que vous avez créée (Self-Hosted-Instance1) en tant qu'instance auto-hébergée dans Fleet Application Management.
    Ajoutez l'instance en tant qu'instance auto-hébergée en la sélectionnant dans la liste des instances Compute. Reportez-vous à Création d'une instance auto-hébergée.

    Vérifiez que l'instance est attachée et visible dans Fleet Application Management.

  2. Créez et configurez un parc pour l'instance.
    • Créez un parc (par exemple, fams_db-os-patching) et ajoutez l'instance auto-hébergée en tant que ressource. Aucun produit n'a besoin d'être ajouté. Reportez-vous à Création d'un parc.
    • Assurez-vous que le parc se trouve dans le compartiment approprié et définissez le type d'environnement Production, le cas échéant.
  3. Créez un guide d'exploitation pour l'instance auto-hébergée.
    • Créez un guide d'exploitation (par exemple, fams_os_dbaas_patching_test_runbook) qui utilise l'application de patches comme opération de cycle de vie. Reportez-vous à Création d'un guide d'exploitation.
    • Ajoutez une tâche au dossier d'exploitation pour exécuter le script shell (par exemple, /root/fams_os_patching/run_os_dbaas_patching.py) sur l'instance auto-hébergée :

      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 "$@"
    • Enregistrez le guide d'exploitation.
  4. Créez un processus de guide d'exploitation pour le parc à l'aide du guide d'exploitation.
    • Programmez ou déclenchez le processus du dossier d'exploitation pour exécuter le script d'application de patches du système d'exploitation DBNode. Reportez-vous à Traitement d'un guide d'exploitation.
    • Sélectionnez le parc (par exemple, fams_db-os-patching) dans la page de liste Parcs. Reportez-vous à Obtention des détails d'un parc.
    • Créer un traitement de travail ou de dossier d'exploitation. Sélectionnez le guide d'exploitation approprié, tel que fams_os_dbaas_patching_test_runbook, et l'opération de cycle de vie. Reportez-vous à Traitement d'un guide d'exploitation.
    • Programmez ou exécutez le travail immédiatement.
  5. Surveiller les journaux de traitement du dossier d'exécution.
    • Vérifiez les journaux dans Fleet Application Management et confirmez que le script a été exécuté. Reportez-vous à Obtention des détails du journal de processus du guide d'exécution pour un parc.
    • Sélectionnez le travail de processus du guide d'exécution et affichez ses journaux pour les messages de progression et d'erreur (par exemple, la progression du travail d'application de patches au système d'exploitation).

Exemple de script d'application de patches au système d'exploitation DBNode

Voici un exemple de script permettant d'appliquer des patches au système d'exploitation DBNode. Indiquez la base de données display name et sélectionnez l'élément options à exécuter, par exemple la prévérification ou la mise à jour.

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

Pour plus d'informations sur les commandes DBCLI (Database Command Line Interface), reportez-vous au Guide de référence de l'interface de ligne de commande Oracle Database.