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

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

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

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

Effectuez les étapes suivantes pour automatiser l'application de correctifs au système d'exploitation pour les ressources DBNode d'Oracle Base Database Service à l'aide de la fonction d'instance auto-hébergée dans Fleet Application Management :

1. Créer et configurer l'instance de calcul

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

  1. Accédez à la console et connectez-vous avec vos données d'identification.
  2. Naviguez jusqu'à la page Instances de calcul et démarrez le flux de travail Créer une instance.
    • Ouvrez le menu de navigation et sélectionnez Calcul. Sous Calcul, sélectionnez Instances.
    • Sélectionnez Créer une instance.
    • Nom : Entrez un nom pour l'instance (par exemple, Self-Hosted-Instance1).
    • Compartiment : Sélectionnez le compartiment dans lequel créer l'instance.
    • Image : Sélectionnez une image Oracle Linux (par exemple, VM.Standard.E4.Flex).
      Note

      Sélectionnez une forme de calcul avec suffisamment de ressources (par exemple, 2 OCPU, 8 Go de mémoire vive) pour gérer le traitement des locations volumineuses. Pour plus d'informations, voir Formes de calcul.
    • Réseau : Utilisez un réseau en nuage virtuel (VCN) existant ou créez-en un nouveau. Assurez-vous que l'instance a une adresse IP publique ou qu'elle est accessible (par exemple, à l'aide d'un hôte bastion).
    • Clés SSH : Ajoutez votre clé SSH publique ou générez de nouvelles clés pour un accès sécurisé.
    • Sélectionnez Créer.
  3. Vérifiez 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 de calcul.
    Dans la page de liste Instances de calcul, sélectionnez la nouvelle instance et notez l'adresse IP privée.
  5. Connectez-vous à l'aide de SSH.
    • Ouvrez un terminal sur votre ordinateur local.
    • Utilisez la commande SSH avec l'adresse IP privée que vous avez notée :

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

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

    • Utilisez un hôte bastion ou Cloud Shell pour vous connecter si l'instance se trouve dans un sous-réseau privé.
  6. Vérifier la connexion.
    Après vous être connecté, vérifiez que vous voyez l'invite Oracle Linux (par exemple, [opc@Self-Hosted-Instance1 ~]$).
  7. Mettre à 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 de principal d'instance pour l'instance

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

  1. Naviguez jusqu'à 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é.
    • Dans 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 correspondance pour inclure votre instance par OCID :

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

      Pour trouver l'OCID de l'instance, allez à la page de liste Instances de calcul, sélectionnez votre instance et copiez l'OCID.
    • Sélectionnez Créer.
  2. Ajoutez des politiques pour le groupe dynamique.
    • Ouvrez le menu de navigation et sélectionnez Identité et sécurité. Sous identité, sélectionnez Politiques.
    • Sélectionnez Créer une politique.
      • Entrez un nom (par exemple, FAMS-SelfHost-Scheduler-Mgmt-DG) et une description (par exemple, Politique pour le principal d'instance de script d'application de correctifs au système d'exploitation).
      • Sélectionnez le compartiment racine ou un autre compartiment, si nécessaire.
      • Ajoutez les énoncés de politique suivants :

        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
      • Sélectionnez Créer.
    Note

    Vérifiez l'OCID de l'instance dans les règles de groupe dynamique pour vous assurer que l'instance dispose des autorisations requises. Sans ces politiques, le script d'application de correctifs au système d'exploitation DBNode échoue avec des erreurs d'autorisation 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éparer le script d'application de correctifs 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 correctifs au système d'exploitation DBNode (par exemple, Exemple de script d'application de correctifs au système d'exploitation DBNode).

    Chargez le fichier de script (par exemple, run_os_patching.py) dans 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. Configurer un environnement :
    • Installez les dépendances Python.
    • Configurez les données d'identification requises (par exemple, Jira).
  4. Testez l'exécution du script :
    python3 /root/fams_os_patching/run_os_patching.py
  5. Observez la sortie pour vérifier la progression et recherchez les erreurs.

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

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

  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 de calcul. Voir 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éer et configurer 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 ne doit être ajouté. Voir Création d'un parc.
    • Assurez-vous que le parc se trouve dans le compartiment approprié et réglez-le au type d'environnement Production, le cas échéant.
  3. Créez un dossier d'exploitation pour l'instance auto-hébergée.
    • Créez un dossier d'exploitation (par exemple, fams_os_dbaas_patching_test_runbook) qui utilise l'application de correctifs comme opération du cycle de vie. Voir Création d'un classeur d'exécution.
    • Ajoutez une tâche au dossier d'exploitation pour exécuter le script d'interpréteur de commandes (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 dossier d'exploitation.
  4. Créez un processus de dossier d'exploitation pour le parc à l'aide du dossier d'exploitation.
    • Programmez ou déclenchez le processus de dossier d'exploitation pour exécuter le script d'application de correctifs au système d'exploitation DBNode. Voir Traitement d'un classeur d'exécution.
    • Sélectionnez le parc (par exemple, fams_db-os-patching) dans la page de liste Flèches. Voir Obtention des détails d'un parc.
    • création d'un traitement de tâche ou de dossier d'exploitation; Sélectionnez le dossier d'exploitation approprié, par exemple fams_os_dbaas_patching_test_runbook) et l'opération de cycle de vie. Voir Traitement d'un classeur d'exécution.
    • Programmez ou exécutez le travail immédiatement.
  5. Surveillez les journaux de traitement du dossier d'exploitation.
    • Consultez les journaux dans Fleet Application Management pour vérifier que le script a été exécuté avec succès. Voir Obtention des détails du journal de processus du dossier d'exploitation pour un parc.
    • Sélectionnez la tâche de traitement du dossier d'exploitation et consultez ses journaux pour voir les messages de progression et d'erreur (par exemple, la progression de la tâche d'application de correctifs au système d'exploitation).

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

Voici un exemple de script pour appliquer des correctifs au système d'exploitation DBNode. Spécifiez la base de données display name et sélectionnez la valeur options à effectuer, par exemple Vérification préalable ou 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), voir Informations de référence sur l'interface de ligne de commande d'Oracle Database.