diff --git a/ansible_collections/f5networks/f5_modules/tests/README.md b/ansible_collections/f5networks/f5_modules/tests/README.md new file mode 100644 index 0000000..a2a07f7 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/tests/README.md @@ -0,0 +1,94 @@ +# Ansible Collection - F5Networks.bigip_module + +In this fork we have placed all the custom modules that we have developed. +We did this beacuse the ones in the well known collection had not enough details. +We decided to develop the same modules but we added all the parameters presents in the device. +All the modules are GitOps oriented. + +Available modules +------------------- + +bigip_profile_client_ssl_module : Module made to manage the profile client ssl. It accept in parameter a variable named "clientssl_params" containing all the informations +for the profile. The state of the profile ("present" = creation or update, "absent" = delete) is defined in the git file +bigip_profile_persistence_hash : Module made to manage the profile persistence Hash. +bigip_profile_protocol_sslserver_module : Module made to manage the profile server ssl. It accept in parameter a variable named "sslserver_params" containing all the +informations for the profile. The state of the profile ("present" = creation or update, "absent" = delete) is defined in the git file +bigip_profile_protocol_tcp_module : Module made to manage the profile TCP. It accept in parameter a variable named "tcp_params" containing all the +informations for the profile. The state of the profile ("present" = creation or update, "absent" = delete) is defined in the git file +bigip_profile_protocol_udp_module : Module made to manage the profile UDP. It accept in parameter a variable named "udp_params" containing all the +informations for the profile. The state of the profile ("present" = creation or update, "absent" = delete) is defined in the git file +bigip_virtual_server_module : Module made to manage the Virtual server in the device. It accept in parameter a variable named "vip_params" containing all the +informations for the profile. The state of the profile ("present" = creation or update, "absent" = delete) is defined in the git file + +Examples: +--------- + +- Managing a client ssl profile: + - name: Client SSl profile creation/suppression/update + F5Networks.bigip_modules.bigip_profiles_client_ssl_module: + ip : "1.1.1.4" + username : "{{my_username}}" + password : "{{my_password}}" + clientssl_params : "{{profile_client_ssl_params_from_git_file}}" + +- Managing a server ssl profile: + - name: Server SSl profile creation/suppression/update + F5Networks.bigip_modules.bigip_profiles_server_ssl_module: + ip : "1.1.1.4" + username : "{{my_username}}" + password : "{{my_password}}" + sslserver_params : "{{profile_server_ssl_params_from_git_file}}" + +- Managing a profile persistence hash: + - name: profile hash management + F5Networks.bigip_modules.bigip_profile_persistence_hash: + description : "this profile is a test" + ip : "1.1.1.4" + username : "{{my_username}}" + password : "{{my_password}}" + hash_name :"{{hash_profile_test}}" + partition : "{{hash_profile_partition_on_the_device}} + hash_algorithm : "{{default or CARP}}" + hash_length : "{{length_of_hash_as_integer}}" + hash_buffer_limit : "{{max_size_of_the_hash_buffer}}" + hash_offset : "{{hash_offset_size}}" + irule : "{{name_of_the_irule}}" + timeout : "{{timeout_as_integer}}" + state : "{{profile_state: asbent, present}}" + +- Managing a profile tcp: + - name: tcp profile managment + F5Networks.bigip_modules.bigip_profile_protocol_tcp_module: + ip : "1.1.1.4" + username : "{{my_username}}" + password : "{{my_password}}" + tcp_params : "{{profile_tcp_params_from_git_file}}" + +- Managing a profile udp: + - name: udp profile managment + F5Networks.bigip_modules.bigip_profile_protocol_udp_module: + ip : "1.1.1.4" + username : "{{my_username}}" + password : "{{my_password}}" + udp_params : "{{profile_udp_params_from_git_file}}" + +- Managing a virtual server: + - name: virtual server managment + F5Networks.bigip_modules.bigip_virtual_server_module: + ip : "1.1.1.4" + username : "{{my_username}}" + password : "{{my_password}}" + vip_params : "{{virtual_server_params_from_git_file}}" + + +License +------- + +BSD + +Author Information +------------------ + +Benjamin Boussereau (MAIF external) +Theo Laurent (MAIF trainee) + diff --git a/ansible_collections/f5networks/f5_modules/tests/bigip_profile_client_ssl_module.py b/ansible_collections/f5networks/f5_modules/tests/bigip_profile_client_ssl_module.py new file mode 100644 index 0000000..e05c73b --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/tests/bigip_profile_client_ssl_module.py @@ -0,0 +1,303 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_client_ssl_module + +short_description: Ce module permet la modification, suppression ou création d'un profile client ssl + +description: + - Se connecte en ssh via paramiko + - Envoie la ou les commandes sur le device et retourne les lignes affichées par l'équipement. + +version_added: '0.1.0' + +authors: + - Benjamin Boussereau et Théo Laurent(@Theo_Laurent) + +options: + ip: + description: + - l'ip de l'équipement sur lequel on souhaite intervenir. + type: str + required: true + + username: + description: + - le login utilisé pour se connecter sur le device. + type: str + required: true + + password: + description: + - le password utilisé pour se connecter sur le device. + type: str + required: true + + clientssl_params: + description: + - dictionnaire qui contient les informations du profile clientssl telles qu'écrites dans le fichier. + type: dict + required: true + +''' + +EXAMPLES = r''' +- name: Manage the client ssl profile + F5Networks.f5-ansible-f5modules.module_bigip_profile_client_ssl_module:: + ip = "1.1.1.4" + username = "myadmin" + password = "mypass" + clientssl_params = "{{clientssl_params}}" +''' + + +import paramiko +import sys +import os +import time +import re +from ansible.module_utils.basic import AnsibleModule + +# Foction responsable de l'envoie des commandes ssh sur le server +def fn_ssh(cmdx, server, connection_port, user, pwd): + # initialisation de la variable de sortie + output = "" + # reservation du socket avec le module paramiko SSHClient + ssh=paramiko.SSHClient() + # deifinition de la politique de gestion des clé publique des hosts + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + # lancement de la connexion ssh sur le server + ssh.connect(server,connection_port,user,pwd,allow_agent=False,look_for_keys=False) + # invocation du shell interractif + shell = ssh.invoke_shell() + #tempo + time.sleep(0.5) + # pour chaque commande de la liste fournie en paramètre divisée sur les retours à la ligne nous lançons cette commande et plaçons le retour du shell dans une variable + for cm in cmdx.split("\n"): + cm = cm + "\n" + shell.send(cm) + time.sleep(0.5) + output += shell.recv(65535).decode('utf-8') + # traitement des résultats pour obtenir une liste lisible + resp = output.split("\r\n") + # fermeture du shell et du ssh afin de libérer le socket + shell.close() + ssh.close() + # renvoie de la valeur obtenue + return(resp) + +# Fonction de création d'un profile clientssl +def fn_creation(clientssl_params, username, password, ip): + # Initialisation de la commande principale + cmd = "tmsh\n" + cmda = "" + # Construction des blocs de commande à partir des paramètres clientssl + for key in clientssl_params: + # On ignore les clés contenant "state" + if "state" in key: + pass + # Commande pour changer de partition + elif key.strip() == "partition": + cmd1 = "cd ../" + clientssl_params[key].strip() + "\n" + # Commande de création du profil clientssl + elif key.strip() == "name": + cmd2 = "ltm profile client-ssl \ncreate " + clientssl_params['name'].strip() + # Ajout des autres paramètres à la commande pour la création + else: + cmda = cmda + " " + str(key).strip() + " " + str(clientssl_params[key]).strip() + # commande final + cmd = cmd + cmd1 + cmd2 + cmda + return(cmd) + +# Fonction pour générer une commande de modification d’un profil clientssl existant +def fn_define_cmd_to_modif(conf_to_modify,clientssl_params): + # Début de la commande de modification (tmsh + le profile clientssl + le nom du profile qu'on veut modif) + cmd_mod = "tmsh\nltm profile client-ssl\nmodify " + str(clientssl_params['name']) + # Ajout des paramètres à modifier + for modif in conf_to_modify: + cmd_mod = cmd_mod + " " + modif + return(cmd_mod) + + +# Fonction pour comparer la configuration existante et celle souhaitée dans clientssls.yml +def fn_compare_conf(clientssl_params, clientssl_infos_from_box): + to_modify = [] # Liste des paramètres à modifier + # Parcours des lignes de configuration une à une récupérées depuis le boitier + new_pos = 0 + for clientssl in range(0,len(clientssl_infos_from_box),1): + # On ignore toutes les lignes qui possent problème (message inutil, champs inéxistant dans la GUI) + if "END" in clientssl_infos_from_box[clientssl] or "}" in clientssl_infos_from_box[clientssl] or "mpclientssl-debug" in clientssl_infos_from_box[clientssl] or "description" in clientssl_infos_from_box[clientssl].strip() or "defaults-from" in clientssl_infos_from_box[clientssl].strip() or "app-service" in clientssl_infos_from_box[clientssl].strip() or clientssl_infos_from_box[clientssl] == '' or "Last" in clientssl_infos_from_box[clientssl] or "@" in clientssl_infos_from_box[clientssl] or "ltm" in clientssl_infos_from_box[clientssl]: + pass + # ensuite si la ligne contient le mot entre double quote + elif "cert-key-chain" in clientssl_infos_from_box[clientssl]: + # on initialise une variable string qui recevra le contenu du paramètre dans la condition + key_chain_params = "" + # on lance une boucle for en partant de la ligne précédente sur la longueur de la liste, tant que la ligne ne contient pas "cert-lifespan" + i = clientssl + new_line = "" + while "cert-lifespan" not in clientssl_infos_from_box[i].strip(): + if "less" in clientssl_infos_from_box[i].strip(): + line = clientssl_infos_from_box[i].strip().split(' ') + for x in range(1, len(line),1): + if line[x] == "" or "%" in line[x]: + pass + else: + if new_line == "" : + new_line = line[x] + else: + new_line = new_line + " " + line[x] + if key_chain_params == "": + key_chain_params = new_line + else: + key_chain_params = key_chain_params + " " + new_line + else: + # on ajoute la ligne à la string initialisés plus haut + if key_chain_params == "": + key_chain_params = clientssl_infos_from_box[i].replace(',','').strip() + else: + key_chain_params = key_chain_params + " " + clientssl_infos_from_box[i].replace(',','').strip() + i += 1 + # on récupère la position de la ligne à sur laquelle la boucle s'est stoppé et on ajoute 1 pour avoir la ligne contenant "cert-lifespan" + new_pos = i + 1 + # si le contenu obtenu est identique à celui présent dans le fichier pour la clé "cert-key-chain" alors on ne fait rien + compare_chain = "cert-key-chain" + clientssl_params["cert-key-chain"].replace(" ","") + if key_chain_params.replace(" ","") == compare_chain: + pass + else: + # sinon on construit la var data suivante et on l'ajoute à la liste to_modify + data = "old = " + key_chain_params + ", new = cert-key-chain " + clientssl_params["cert-key-chain"] + to_modify.append(data) + # si la valeur de clientssl est inférieur à la valeur de la ligne obtenu dans la condition ci-dessus, alors clientssl prend cette valeur + elif clientssl < new_pos: + clientssl = new_pos + else: + # Traitement des lignes contenant "less" + if "less" in clientssl_infos_from_box[clientssl]: + # On découpe la ligne en utilisant les espaces multiples comme séparateurs + new_line = re.split(" +",clientssl_infos_from_box[clientssl]) + # On récupère le nom du champ et sa valeur depuis la ligne + clientssl_field_from_box = new_line[2].strip() + clientssl_field_valued_from_box = new_line[3].strip() + try: + # On récupère la valeur attendue depuis clientssl_params via la découpe de la ligne juste au dessus + clientssl_field_from_file = clientssl_params[clientssl_field_from_box] + # Si les deux valeurs sont identique on pass + if str(clientssl_field_valued_from_box) == str(clientssl_field_from_file): + pass + else: + # Si la valeur est différente, on ajoute à la liste des modifications + data = clientssl_field_from_box + " " + str(clientssl_params[clientssl_field_from_box]) + to_modify.append(data) + # Si le champ n'existe pas dans clientssl_params, on ignore + except KeyError: + pass + # Traitement des autres lignes + else: + # On découpe la ligne en utilisant seulement un espace comme séparateurs + clientssl_field_from_box = clientssl_infos_from_box[clientssl].strip().split(' ')[0].strip() + clientssl_field_valued_from_box = clientssl_infos_from_box[clientssl].strip().split(' ')[1].strip() + try: + # On récupère la valeur attendue depuis clientssl_params via la découpe de la ligne juste au dessus + clientssl_field_from_file = clientssl_params[clientssl_field_from_box] + # Si les deux valeurs sont identique on pass + if str(clientssl_field_valued_from_box) == str(clientssl_field_from_file): + pass + else: + # Si la valeur est différente, on ajoute à la liste des modifications + data = clientssl_field_from_box + " " + str(clientssl_params[clientssl_field_from_box]) + to_modify.append(data) + # Si le champ n'existe pas dans clientssl_params, on ignore + except KeyError: + pass + # Retourne la liste des paramètres à modifier + return(to_modify) + + + + +# Fonction principale chargée de créer, modifier ou supprimer une VIP sur un F5 +def main(): + #### On commence par définir le module Ansible et ses paramètres + module = AnsibleModule( + argument_spec=dict( + ip = dict(required=True, type='str'), + username = dict(required=True, type='str'), + password = dict(required=True, type='str', no_log=True), + clientssl_params = dict(required=True, type='dict') + ) + ) + port = 22 + ip = module.params.get('ip') + username = module.params.get('username') + password = module.params.get('password') + clientssl_params = module.params.get('clientssl_params') + # maintenant que nous avons récupéré les informations passées en argument, nous allons observer le contenu de clientssl_params + # pour cela, nous lançons une boucle for sur le contenu de la variable et observons l'état du profil clientssl. + cmd = "tmsh\ncd ../" + clientssl_params['partition'] + "\nlist ltm profile client-ssl " + clientssl_params['name'] + " all-properties\n \n \n \n" + # Récupération des informations du boîtier via notre fonction SSH + clientssl_infos_from_box = fn_ssh(cmd, ip, port, username, password) + #module.exit_json(changed=True, message="Le profile clientssl a ete créé", resultat=clientssl_infos_from_box) + not_found = 0 + # Parcours des lignes de configuration une à une récupérées depuis le boîtier + for i in range(0, len(clientssl_infos_from_box),1): + # Si le profil n'éxiste pas sur le boîtier, alors on ajout 1 + if "not found" in clientssl_infos_from_box[i]: + not_found = 1 + # Sinon, on ne fait rien + else: + pass + # Traitement des paramètres du profil clientssl + for param in clientssl_params: + cmd = "" + # Gestion de l'état du profil (présent ou absent) + if param == "state": + if clientssl_params[param] != "absent": + # si le status du profil clientssl dans le fichier est différent de absent alors nous récupérons les informations de ce dernier sur le # Parcours des lignes de configuration une à une récupérées depuis le boîtier + if not_found == 1: + # Si le profil n'existe pas, on effectue les actions suivantes : + creation_profile = fn_creation(clientssl_params, username, password, ip) + # Appel de notre fonction de création + action_creation = fn_ssh(creation_profile, ip, port, username, password) + # Application sur le boîtier via SSH + module.exit_json(changed=True, message="Le profile clientssl a ete créé", resultat=action_creation) + + else: + # Comparaison de la configuration actuelle avec celle souhaitée + conf_to_modify = fn_compare_conf(clientssl_params, clientssl_infos_from_box) + # Ajout des éléments à modifier si des différences existent + if conf_to_modify != []: + # Si des différences sont détectées, on génère les commandes de modification via notre fonction + cmd_to_modify = fn_define_cmd_to_modif(conf_to_modify,clientssl_params) + modification_profile = fn_ssh(cmd_to_modify, ip, port, username, password) + # Application sur le boîtier via SSH + module.exit_json(changed=True, message="Le profile clientssl a ete mis à jour", resultat=modification_profile) + else: + module.exit_json(changed=False, message="Le profile clientssl sur le boitier est identique à celui du fichier") + else: + # vérifier si le profil existe, si oui, lancer la suppression + if "not found" in clientssl_infos_from_box: + module.exit_json(changed=False, message="Le profile n'existe pas donc nous ne faisons rien") + else: + # commande de suppression + suppr_cmd = "tmsh\ncd ../" + clientssl_params['partition'] + "\nltm profile client-ssl\ndelete " + clientssl_params['name'] + "\n" + # appel fonction fn_ssh avec les commandes de suppression + suppression_profile = fn_ssh(suppr_cmd, ip, port, username, password) + module.exit_json(changed=True, message="Le profile clientssl a été supprimé", resultat=suppression_profile) + + # Gestion des des paramètres spécifiques + elif param in ['alert-timeout', 'handshake-timeout', 'renegotiate-period', 'renegotiate-size', 'renegotiate-max-record-delay', 'max-active-handshakes', 'max-aggregate-renegotiation-per-minute']: + # Liste de tous les champs spécifiques + if clientssl_params[param] != "immediate" or clientssl_params[param] != "indefinite": + # Si les paramètre on une valeur différente de "immediate" ou de "indefinite" + clientssl_params[param] = int(clientssl_params[param]) + # On force la valeur en int au lieux de str car spécify + else: + pass + # si les champs on comme valeur "immediate" ou "indefinite" on ignore + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ansible_collections/f5networks/f5_modules/tests/bigip_profile_persistence_hash.py b/ansible_collections/f5networks/f5_modules/tests/bigip_profile_persistence_hash.py new file mode 100644 index 0000000..fb84745 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/tests/bigip_profile_persistence_hash.py @@ -0,0 +1,239 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: module_bigip_persistence_hash + +short_description: Ce module permet la MAJ (creation/Suppression/modification d'une persitence type cookie-session hash) + +description: + - Se connecte en ssh via paramiko + - Envoie les commandes de configuration du prolie persistence cookie-session hash. + - Affiche les actions réalisées + +version_added: '0.1.0' + +authors: + - Benjamin Boussereau (@Benjamin-Boussereau) + - Theo Laurent (@Theo_Laurent) + +options: + ip: + description: + - l'ip de l'équipement sur lequel on souhaite intervenir. + type: str + required: true + + username: + description: + - le login utilisé pour se connecter sur le device. + type: str + required: true + + password: + description: + - le password utilisé pour se connecter sur le device. + type: str + required: true + + hash_name: + description: + - le nom du persistence profile + type: str + required: true + + partition: + description: + - la partition sur laquelle se trouve le profile + type: str + required: true + + hash_algorithm: + description: + - l'alogithme de chiffrement utilisé par le profile pour générer le hash (default_value = default) + type: bool + required: false + + hash_length: + description: + - la longueur du hash à configurer (default: 0) + type: int + required: false + + hash_offset: + description: + - by default 0 + type: int + required: false + + irule: + description: + - la règle irule sur laquelle se base le profile pour calculer le hash (default: None) + type: str + required: false + + timeout: + description: + - temps max de connexion avant de relancer le calcul du hash (default: 180) + type: int + required: false +''' + +EXAMPLES = r''' +- name: configure un profile persistence hash + F5Networks.f5-ansible-f5modules.module_bigip_persistence_hash: + state: "present" + ip: "ip_du_f5" + username: "toto" + password: "secret" + hash_name: "test_hash" + partition: "Admin" + hash_algorithm: "myAlgo" + hash_buffer_limit: "64634" + hash_length: "128" + hash_offset: "10" + timeout = "300" + irule = "None" +''' + + +import paramiko +import sys +import os +import time +from ansible.module_utils.basic import AnsibleModule + +# Foction responsable de l'envoie des commandes ssh sur le server +def fn_ssh(cmdx, server, connection_port, user, pwd): + # initialisation de la variable de sortie + output = "" + # reservation du socket avec le module paramiko SSHClient + ssh=paramiko.SSHClient() + # deifinition de la politique de gestion des clé publique des hosts + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + # lancement de la connexion ssh sur le server + ssh.connect(server,connection_port,user,pwd,allow_agent=False,look_for_keys=False) + # invocation du shell interractif + shell = ssh.invoke_shell() + #tempo + time.sleep(0.5) + # pour chaque commande de la liste fournie en paramètre divisée sur les retours à la ligne nous lançons cette commande et plaçons le retour du shell dans une variable + for cm in cmdx.split("\n"): + cm = cm + "\n" + shell.send(cm) + time.sleep(0.5) + output += shell.recv(65535).decode('utf-8') + # traitement des résultats pour obtenir une liste lisible + resp = output.split("\r\n") + # fermeture du shell et du ssh afin de libérer le socket + shell.close() + ssh.close() + # renvoie de la valeur obtenue + return(resp) + + +# fonction principale chargé de créer, modifier ou supprimer une persistence de type hash sur un F5 +def main(): + # initialisation des paramètre attendu par le module et de leur type ainsi que de leur valeur par defaut + module = AnsibleModule( + argument_spec=dict( + description = dict(required=True, type='str'), + ip = dict(required=True, type='str'), + username = dict(required=True, type='str'), + password = dict(required=True, type='str', no_log=True), + hash_name = dict(required=True, type='str'), + partition = dict(required=True, type='str', default="Common"), + hash_algorithm = dict(required=False, type='str', default="default"), + hash_length = dict(required=False, type='int', default=0), + hash_buffer_limit = dict(required=False, type='int', default=0), + hash_offset = dict(required=False, type='int',default=0), + irule = dict(required=False, type='str',default=None), + timeout = dict(required=False, type='int', default=180), + state = dict(required=False, type='str'), + no_log = dict(type='bool', required=False, default=False) + ) + ) + # on récupère les informations fournies dans le module et on les place dans des variables + port = 22 + description = module.params.get('description') + ip = module.params.get('ip') + username = module.params.get('username') + password = module.params.get('password') + hash_name = module.params.get('hash_name') + partition = module.params.get('partition') + hash_algorithm = module.params.get('hash_algorithm') + hash_length = module.params.get('hash_length') + hash_buffer_limit = module.params.get('hash_buffer_limit') + hash_offset = module.params.get('hash_offset') + irule = module.params.get('irule') + timeout = module.params.get('timeout') + state = module.params.get('state') + # on place les premières commandes de bases dans les deux var suivantes + cmd0 = "tmsh\ncd ../" + partition + "\n" + cmd1 = "ltm persistence hash\n" + # ensuite, si le status du profile est "present" nous commençons par vérifier son existence sur le boitier + if state == "present": + cmd2 = "list "+ hash_name + "\n" + cmd= cmd0 + cmd1 + cmd2 + profile_existence = fn_ssh(cmd, ip, port, username, password) + # s'il n'existe pas, nous lançons les commandes de création + if "not found" in profile_existence: + cmd2 = "create " + hash_name+ " defaults-from hash description "+ description + " hash-buffer-limit " + str(hash_buffer_limit) +" hash-length " + str(hash_length)+ " hash-offset "+str(hash_offset)+ " hash-algorithm " + hash_algorithm+ " timeout "+str(timeout) + " rule " + irule +"\n" + cmd = cmd0 + cmd1 + cmd2 + # sinon, nous récupérons sa configuration et, pour chaque élément vérifions si sa valeur est égale ou non à celle du fichier source + # si ce n'est pas le cas nous plaçons ce champs de configuration dans la liste "to_modify" + else: + to_modify = [] + for line in profile_existence: + if "description" in line: + if line.split("description")[1].strip() != description: + data = " description " + description + to_modify.append(data) + elif "hash-buffer-limit" in line: + if line.split("hash-buffer-limit")[1].strip() != hash_buffer_limit: + data = " hash-buffer-limit " + hash_buffer_limit + to_modify.append(data) + elif "hash-length" in line: + if line.split("hash-length")[1].strip() != hash_length: + data = " hash-length " + hash_length + to_modify.append(data) + elif "hash-offset" in line: + if line.split("hash-offset")[1].strip() != hash_offset: + data = " hash-offset " + hash_offset + to_modify.append(data) + elif "hash-algorithm" in line: + if line.split("hash-algorithm")[1].strip() != hash_algorithm: + data = " hash-algorithm " + hash_algorithm + to_modify.append(data) + elif "timeout" in line: + if line.split("timeout")[1].strip() != timeout: + data = " timeout " + timeout + to_modify.append(data) + elif "rule" in line: + if line.split("rule")[1].strip() != irule: + data = " rule " + irule + to_modify.append(data) + # si la liste suivante est vide, nous quittons le module en precisant le message suivant car le hash est identique au fichier source + if to_modify == []: + module.exit_json(changed=False, message="pas de modifications à apporter au hash") + # sinon nous allons lancer les commandes de modifications sur le boitier + else: + cmd2 = "modify " + hash_name + for i in range(0, len(to_modify), 1): + cmd2 = cmd2 + to_modify[i] + cmd2 = cmd2 + "\n" + cmd = cmd0 + cmd1 + cmd2 + action_on_profile = fn_ssh(cmd, ip, port, username, password) + # sinon si le status du profile sur le fichier source est "absent" nous lançons les commandes de suppression. + elif state == "absent": + cmd2 = "delete " + hash_name + "\n" + cmd = cmd0 + cmd1 + cmd2 + action_on_profile = fn_ssh(cmd, ip, port, username, password) + # enfin nous quittons le module en retournant les actions réalisées sur le F5 + module.exit_json(changed=True, resultat=action_on_profile) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ansible_collections/f5networks/f5_modules/tests/bigip_profile_protocol_sslserver_module.py b/ansible_collections/f5networks/f5_modules/tests/bigip_profile_protocol_sslserver_module.py new file mode 100644 index 0000000..a0909dc --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/tests/bigip_profile_protocol_sslserver_module.py @@ -0,0 +1,259 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_protocol_sslserver_module + +short_description: Ce module permet la modification, suppression ou création d'une vip + +description: + - Se connecte en ssh via paramiko + - Envoie la ou les commandes sur le device et retourne les lignes affichées par l'équipement. + +version_added: '0.1.0' + +authors: + - Theo Laurent (@Theo_Laurent) + - Benjamin Boussereau (@Benjamin-Boussereau) + +options: + ip: + description: + - l'ip de l'équipement sur lequel on souhaite intervenir. + type: str + required: true + + username: + description: + - le login utilisé pour se connecter sur le device. + type: str + required: true + + password: + description: + - le password utilisé pour se connecter sur le device. + type: str + required: true + + sslserver_params: + description: + - dictionnaire qui contient les informations du profile sslserver telles qu'écrites dans le fichier. + type: dict + required: true + +''' + +EXAMPLES = r''' +- name: manage the server_ssl profile + F5Networks.f5-ansible-f5modules.module_bigip_profile_protocol_sslserver_module:: + ip = "1.1.1.4" + username = "myadmin" + password = "mypass" + sslserver_params = "{{ssl_server_param}}" +''' + + +import paramiko +import sys +import os +import time +import re +from ansible.module_utils.basic import AnsibleModule + +# Foction responsable de l'envoie des commandes ssh sur le server +def fn_ssh(cmdx, server, connection_port, user, pwd): + # initialisation de la variable de sortie + output = "" + # reservation du socket avec le module paramiko SSHClient + ssh=paramiko.SSHClient() + # deifinition de la politique de gestion des clé publique des hosts + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + # lancement de la connexion ssh sur le server + ssh.connect(server,connection_port,user,pwd,allow_agent=False,look_for_keys=False) + # invocation du shell interractif + shell = ssh.invoke_shell() + #tempo + time.sleep(0.5) + # pour chaque commande de la liste fournie en paramètre divisée sur les retours à la ligne nous lançons cette commande et plaçons le retour du shell dans une variable + for cm in cmdx.split("\n"): + cm = cm + "\n" + shell.send(cm) + time.sleep(0.5) + output += shell.recv(65535).decode('utf-8') + # traitement des résultats pour obtenir une liste lisible + resp = output.split("\r\n") + # fermeture du shell et du ssh afin de libérer le socket + shell.close() + ssh.close() + # renvoie de la valeur obtenue + return(resp) + +# Fonction de création d'un profile sslserver +def fn_creation(sslserver_params, username, password, ip): + # Initialisation de la commande principale + cmd = "tmsh\n" + cmda = "" + # Construction des blocs de commande à partir des paramètres sslserver + for key in sslserver_params: + # On ignore les clés contenant "state" + if "state" in key: + pass + # Commande pour changer de partition + elif key.strip() == "partition": + cmd1 = "cd ../" + sslserver_params[key].strip() + "\n" + # Commande de création du profil sslserver + elif key.strip() == "name": + cmd2 = "ltm profile sslserver \ncreate " + sslserver_params['name'].strip() + # Ajout des autres paramètres à la commande pour la création + else: + cmda = cmda + " " + str(key).strip() + " " + str(sslserver_params[key]).strip() + # commande final + cmd = cmd + cmd1 + cmd2 + cmda + return(cmd) + +# Fonction pour générer une commande de modification d’un profil sslserver existant +def fn_define_cmd_to_modif(conf_to_modify,sslserver_params): + # Début de la commande de modification (tmsh + le profile sslserver + le nom du profile qu'on veut modif) + cmd_mod = "tmsh\nltm profile sslserver\nmodify " + str(sslserver_params['name']) + # Ajout des paramètres à modifier + for modif in conf_to_modify: + cmd_mod = cmd_mod + " " + modif + return(cmd_mod) + + +# Fonction pour comparer la configuration existante et celle souhaitée dans sslservers.yml +def fn_compare_conf(sslserver_params, sslserver_infos_from_box): + to_modify = [] # Liste des paramètres à modifier + # Parcours des lignes de configuration une à une récupérées depuis le boitier + for sslserver in range(0,len(sslserver_infos_from_box),1): + # On ignore toutes les lignes qui possent problème (message inutil, champs inéxistant dans la GUI) + if "END" in sslserver_infos_from_box[sslserver] or "}" in sslserver_infos_from_box[sslserver] or "app-service" in sslserver_infos_from_box[sslserver].strip() or "defaults-from" in sslserver_infos_from_box[sslserver].strip() or "description" in sslserver_infos_from_box[sslserver].strip() or sslserver_infos_from_box[sslserver] == '' or "Last" in sslserver_infos_from_box[sslserver] or "@" in sslserver_infos_from_box[sslserver] or "ltm" in sslserver_infos_from_box[sslserver]: + pass + else: + # Traitement des lignes contenant "less" + if "less" in sslserver_infos_from_box[sslserver]: + # On découpe la ligne en utilisant les espaces multiples comme séparateurs + new_line = re.split(" +",sslserver_infos_from_box[sslserver]) + # On récupère le nom du champ et sa valeur depuis la ligne + sslserver_field_from_box = new_line[2].strip() + sslserver_field_valued_from_box = new_line[3].strip() + try: + # On récupère la valeur attendue depuis sslserver_params via la découpe de la ligne juste au dessus + sslserver_field_from_file = sslserver_params[sslserver_field_from_box] + # Si les deux valeurs sont identique on pass + if str(sslserver_field_valued_from_box) == str(sslserver_field_from_file): + pass + else: + # Si la valeur est différente, on ajoute à la liste des modifications + data = sslserver_field_from_box + " " + str(sslserver_params[sslserver_field_from_box]) + to_modify.append(data) + # Si le champ n'existe pas dans sslserver_params, on ignore + except KeyError: + pass + # Traitement des autres lignes + else: + # On découpe la ligne en utilisant seulement un espace comme séparateurs + sslserver_field_from_box = sslserver_infos_from_box[sslserver].strip().split(' ')[0].strip() + sslserver_field_valued_from_box = sslserver_infos_from_box[sslserver].strip().split(' ')[1].strip() + try: + # On récupère la valeur attendue depuis sslserver_params via la découpe de la ligne juste au dessus + sslserver_field_from_file = sslserver_params[sslserver_field_from_box] + # Si les deux valeurs sont identique on pass + if str(sslserver_field_valued_from_box) == str(sslserver_field_from_file): + pass + else: + # Si la valeur est différente, on ajoute à la liste des modifications + data = sslserver_field_from_box + " " + str(sslserver_params[sslserver_field_from_box]) + to_modify.append(data) + # Si le champ n'existe pas dans sslserver_params, on ignore + except KeyError: + pass + # Retourne la liste des paramètres à modifier + return(to_modify) + + + + +# Fonction principale chargée de créer, modifier ou supprimer une VIP sur un F5 +def main(): + #### On commence par définir le module Ansible et ses paramètres + module = AnsibleModule( + argument_spec=dict( + ip = dict(required=True, type='str'), + username = dict(required=True, type='str'), + password = dict(required=True, type='str', no_log=True), + sslserver_params = dict(required=True, type='dict') + ) + ) + port = 22 + ip = module.params.get('ip') + username = module.params.get('username') + password = module.params.get('password') + sslserver_params = module.params.get('sslserver_params') + # maintenant que nous avons récupéré les informations passées en argument, nous allons observer le contenu de sslserver_params + # pour cela, nous lançons une boucle for sur le contenu de la variable et observons l'état du profil sslserver. + cmd = "tmsh\ncd ../" + sslserver_params['partition'] + "\nlist ltm profile sslserver " + sslserver_params['name'] + " all-properties\n \n \n \n" + # Récupération des informations du boîtier via notre fonction SSH + sslserver_infos_from_box = fn_ssh(cmd, ip, port, username, password) + not_found = 0 + # Parcours des lignes de configuration une à une récupérées depuis le boîtier + for i in range(0, len(sslserver_infos_from_box),1): + # Si le profil n'éxiste pas sur le boîtier, alors on ajout 1 + if "not found" in sslserver_infos_from_box[i]: + not_found = 1 + # Sinon, on ne fait rien + else: + pass + # Traitement des paramètres du profil sslserver + for param in sslserver_params: + cmd = "" + # Gestion de l'état du profil (présent ou absent) + if param == "state": + if sslserver_params[param] != "absent": + # si le status du profil sslserver dans le fichier est différent de absent alors nous récupérons les informations de ce dernier sur le # Parcours des lignes de configuration une à une récupérées depuis le boîtier + if not_found == 1: + # Si le profil n'existe pas, on effectue les actions suivantes : + creation_profile = fn_creation(sslserver_params, username, password, ip) + # Appel de notre fonction de création + action_creation = fn_ssh(creation_profile, ip, port, username, password) + # Application sur le boîtier via SSH + module.exit_json(changed=True, message="Le profile sslserver a ete créé", resultat=action_creation) + else: + # Comparaison de la configuration actuelle avec celle souhaitée + conf_to_modify = fn_compare_conf(sslserver_params, sslserver_infos_from_box) + # Ajout des éléments à modifier si des différences existent + if conf_to_modify != []: + # Si des différences sont détectées, on génère les commandes de modification via notre fonction + cmd_to_modify = fn_define_cmd_to_modif(conf_to_modify,sslserver_params) + modification_profile = fn_ssh(cmd_to_modify, ip, port, username, password) + # Application sur le boîtier via SSH + module.exit_json(changed=True, message="Le profile sslserver a ete mis à jour", resultat=modification_profile) + else: + module.exit_json(changed=False, message="Le profile sslserver sur le boitier est identique à celui du fichier") + else: + # vérifier si le profil existe, si oui, lancer la suppression + if "not found" in sslserver_infos_from_box: + module.exit_json(changed=False, message="Le profile n'existe pas donc nous ne faisons rien") + else: + # commande de suppression + suppr_cmd = "tmsh\ncd ../" + sslserver_params['partition'] + "\nltm profile sslserver\ndelete " + sslserver_params['name'] + "\n" + # appel fonction fn_ssh avec les commandes de suppression + suppression_profile = fn_ssh(suppr_cmd, ip, port, username, password) + module.exit_json(changed=False, message="Le profile sslserver a été supprimé", resultat=suppression_profile) + + # Gestion des des paramètres spécifiques + elif param in ['idle-timeout', 'ip-tos-to-client', 'link-qos-to-client']: + # Liste de tous les champs spécifiques + if sslserver_params[param] != "immediate" or sslserver_params[param] != "indefinite": + # Si les paramètre on une valeur différente de "immediate" ou de "indefinite" + sslserver_params[param] = int(sslserver_params[param]) + # On force la valeur en int au lieux de str car spécify + else: + pass + # si les champs on comme valeur "immediate" ou "indefinite" on ignore + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ansible_collections/f5networks/f5_modules/tests/bigip_profile_protocol_tcp_module.py b/ansible_collections/f5networks/f5_modules/tests/bigip_profile_protocol_tcp_module.py new file mode 100644 index 0000000..b983264 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/tests/bigip_profile_protocol_tcp_module.py @@ -0,0 +1,259 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_protocol_TCP_module + +short_description: Ce module permet la modification, suppression ou création d'une vip + +description: + - Se connecte en ssh via paramiko + - Envoie la ou les commandes sur le device et retourne les lignes affichées par l'équipement. + +version_added: '0.1.0' + +authors: + - Theo Laurent (@Theo_Laurent) + - Benjamin Boussereau (@Benjamin-Boussereau) + +options: + ip: + description: + - l'ip de l'équipement sur lequel on souhaite intervenir. + type: str + required: true + + username: + description: + - le login utilisé pour se connecter sur le device. + type: str + required: true + + password: + description: + - le password utilisé pour se connecter sur le device. + type: str + required: true + + tcp_params: + description: + - dictionnaire qui contient les informations du profile TCP telles qu'écrites dans le fichier. + type: dict + required: true + +''' + +EXAMPLES = r''' +- name: Manage the profile protocol tcp + F5Networks.f5-ansible-f5modules.module_bigip_profile_protocol_tcp_module: + ip = "1.1.1.4" + username = "myadmin" + password = "mypass" + tcp_params = "{{tcp_params}}" +''' + + +import paramiko +import sys +import os +import time +import re +from ansible.module_utils.basic import AnsibleModule + +# Foction responsable de l'envoie des commandes ssh sur le server +def fn_ssh(cmdx, server, connection_port, user, pwd): + # initialisation de la variable de sortie + output = "" + # reservation du socket avec le module paramiko SSHClient + ssh=paramiko.SSHClient() + # deifinition de la politique de gestion des clé publique des hosts + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + # lancement de la connexion ssh sur le server + ssh.connect(server,connection_port,user,pwd,allow_agent=False,look_for_keys=False) + # invocation du shell interractif + shell = ssh.invoke_shell() + #tempo + time.sleep(0.5) + # pour chaque commande de la liste fournie en paramètre divisée sur les retours à la ligne nous lançons cette commande et plaçons le retour du shell dans une variable + for cm in cmdx.split("\n"): + cm = cm + "\n" + shell.send(cm) + time.sleep(0.5) + output += shell.recv(65535).decode('utf-8') + # traitement des résultats pour obtenir une liste lisible + resp = output.split("\r\n") + # fermeture du shell et du ssh afin de libérer le socket + shell.close() + ssh.close() + # renvoie de la valeur obtenue + return(resp) + +# Fonction de création d'un profile tcp +def fn_creation(tcp_params, username, password, ip): + # Initialisation de la commande principale + cmd = "tmsh\n" + cmda = "" + # Construction des blocs de commande à partir des paramètres TCP + for key in tcp_params: + # On ignore les clés contenant "state" + if "state" in key: + pass + # Commande pour changer de partition + elif key.strip() == "partition": + cmd1 = "cd ../" + tcp_params[key].strip() + "\n" + # Commande de création du profil TCP + elif key.strip() == "name": + cmd2 = "ltm profile tcp \ncreate " + tcp_params['name'].strip() + # Ajout des autres paramètres à la commande pour la création + else: + cmda = cmda + " " + str(key).strip() + " " + str(tcp_params[key]).strip() + # commande final + cmd = cmd + cmd1 + cmd2 + cmda + return(cmd) + +# Fonction pour générer une commande de modification d’un profil TCP existant +def fn_define_cmd_to_modif(conf_to_modify,tcp_params): + # Début de la commande de modification (tmsh + le profile tcp + le nom du profile qu'on veut modif) + cmd_mod = "tmsh\nltm profile tcp\nmodify " + str(tcp_params['name']) + # Ajout des paramètres à modifier + for modif in conf_to_modify: + cmd_mod = cmd_mod + " " + modif + return(cmd_mod) + + +# Fonction pour comparer la configuration existante et celle souhaitée dans tcps.yml +def fn_compare_conf(tcp_params, tcp_infos_from_box): + to_modify = [] # Liste des paramètres à modifier + # Parcours des lignes de configuration une à une récupérées depuis le boitier + for tcp in range(0,len(tcp_infos_from_box),1): + # On ignore toutes les lignes qui possent problème (message inutil, champs inéxistant dans la GUI) + if "END" in tcp_infos_from_box[tcp] or "}" in tcp_infos_from_box[tcp] or "mptcp-debug" in tcp_infos_from_box[tcp] or "description" in tcp_infos_from_box[tcp].strip() or "defaults-from" in tcp_infos_from_box[tcp].strip() or "app-service" in tcp_infos_from_box[tcp].strip() or tcp_infos_from_box[tcp] == '' or "Last" in tcp_infos_from_box[tcp] or "@" in tcp_infos_from_box[tcp] or "ltm" in tcp_infos_from_box[tcp]: + pass + else: + # Traitement des lignes contenant "less" + if "less" in tcp_infos_from_box[tcp]: + # On découpe la ligne en utilisant les espaces multiples comme séparateurs + new_line = re.split(" +",tcp_infos_from_box[tcp]) + # On récupère le nom du champ et sa valeur depuis la ligne + tcp_field_from_box = new_line[2].strip() + tcp_field_valued_from_box = new_line[3].strip() + try: + # On récupère la valeur attendue depuis tcp_params via la découpe de la ligne juste au dessus + tcp_field_from_file = tcp_params[tcp_field_from_box] + # Si les deux valeurs sont identique on pass + if str(tcp_field_valued_from_box) == str(tcp_field_from_file): + pass + else: + # Si la valeur est différente, on ajoute à la liste des modifications + data = tcp_field_from_box + " " + str(tcp_params[tcp_field_from_box]) + to_modify.append(data) + # Si le champ n'existe pas dans tcp_params, on ignore + except KeyError: + pass + # Traitement des autres lignes + else: + # On découpe la ligne en utilisant seulement un espace comme séparateurs + tcp_field_from_box = tcp_infos_from_box[tcp].strip().split(' ')[0].strip() + tcp_field_valued_from_box = tcp_infos_from_box[tcp].strip().split(' ')[1].strip() + try: + # On récupère la valeur attendue depuis tcp_params via la découpe de la ligne juste au dessus + tcp_field_from_file = tcp_params[tcp_field_from_box] + # Si les deux valeurs sont identique on pass + if str(tcp_field_valued_from_box) == str(tcp_field_from_file): + pass + else: + # Si la valeur est différente, on ajoute à la liste des modifications + data = tcp_field_from_box + " " + str(tcp_params[tcp_field_from_box]) + to_modify.append(data) + # Si le champ n'existe pas dans tcp_params, on ignore + except KeyError: + pass + # Retourne la liste des paramètres à modifier + return(to_modify) + + + + +# Fonction principale chargée de créer, modifier ou supprimer une VIP sur un F5 +def main(): + #### On commence par définir le module Ansible et ses paramètres + module = AnsibleModule( + argument_spec=dict( + ip = dict(required=True, type='str'), + username = dict(required=True, type='str'), + password = dict(required=True, type='str', no_log=True), + tcp_params = dict(required=True, type='dict') + ) + ) + port = 22 + ip = module.params.get('ip') + username = module.params.get('username') + password = module.params.get('password') + tcp_params = module.params.get('tcp_params') + # maintenant que nous avons récupéré les informations passées en argument, nous allons observer le contenu de tcp_params + # pour cela, nous lançons une boucle for sur le contenu de la variable et observons l'état du profil tcp. + cmd = "tmsh\ncd ../" + tcp_params['partition'] + "\nlist ltm profile tcp " + tcp_params['name'] + " all-properties\n \n \n \n" + # Récupération des informations du boîtier via notre fonction SSH + tcp_infos_from_box = fn_ssh(cmd, ip, port, username, password) + not_found = 0 + # Parcours des lignes de configuration une à une récupérées depuis le boîtier + for i in range(0, len(tcp_infos_from_box),1): + # Si le profil n'éxiste pas sur le boîtier, alors on ajout 1 + if "not found" in tcp_infos_from_box[i]: + not_found = 1 + # Sinon, on ne fait rien + else: + pass + # Traitement des paramètres du profil TCP + for param in tcp_params: + cmd = "" + # Gestion de l'état du profil (présent ou absent) + if param == "state": + if tcp_params[param] != "absent": + # si le status du profil tcp dans le fichier est différent de absent alors nous récupérons les informations de ce dernier sur le # Parcours des lignes de configuration une à une récupérées depuis le boîtier + if not_found == 1: + # Si le profil n'existe pas, on effectue les actions suivantes : + creation_profile = fn_creation(tcp_params, username, password, ip) + # Appel de notre fonction de création + action_creation = fn_ssh(creation_profile, ip, port, username, password) + # Application sur le boîtier via SSH + module.exit_json(changed=True, message="Le profile tcp a ete créé", resultat=action_creation) + else: + # Comparaison de la configuration actuelle avec celle souhaitée + conf_to_modify = fn_compare_conf(tcp_params, tcp_infos_from_box) + # Ajout des éléments à modifier si des différences existent + if conf_to_modify != []: + # Si des différences sont détectées, on génère les commandes de modification via notre fonction + cmd_to_modify = fn_define_cmd_to_modif(conf_to_modify,tcp_params) + modification_profile = fn_ssh(cmd_to_modify, ip, port, username, password) + # Application sur le boîtier via SSH + module.exit_json(changed=True, message="Le profile tcp a ete mis à jour", resultat=modification_profile) + else: + module.exit_json(changed=False, message="Le profile tcp sur le boitier est identique à celui du fichier") + else: + # vérifier si le profil existe, si oui, lancer la suppression + if "not found" in tcp_infos_from_box: + module.exit_json(changed=False, message="Le profile n'existe pas donc nous ne faisons rien") + else: + # commande de suppression + suppr_cmd = "tmsh\ncd ../" + tcp_params['partition'] + "\nltm profile tcp\ndelete " + tcp_params['name'] + "\n" + # appel fonction fn_ssh avec les commandes de suppression + suppression_profile = fn_ssh(suppr_cmd, ip, port, username, password) + module.exit_json(changed=False, message="Le profile tcp a été supprimé", resultat=suppression_profile) + + # Gestion des des paramètres spécifiques + elif param in ['close-wait-timeout', 'fin-wait-timeout', 'fin-wait-2-timeout', 'idle-timeout', 'keep-alive-interval', 'time-wait-timeout', 'zero-window-timeout', 'ip-tos-to-client', 'link-qos-to-client']: + # Liste de tous les champs spécifiques + if tcp_params[param] != "immediate" or tcp_params[param] != "indefinite": + # Si les paramètre on une valeur différente de "immediate" ou de "indefinite" + tcp_params[param] = int(tcp_params[param]) + # On force la valeur en int au lieux de str car spécify + else: + pass + # si les champs on comme valeur "immediate" ou "indefinite" on ignore + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ansible_collections/f5networks/f5_modules/tests/bigip_profile_protocol_udp_module.py b/ansible_collections/f5networks/f5_modules/tests/bigip_profile_protocol_udp_module.py new file mode 100644 index 0000000..b2ece27 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/tests/bigip_profile_protocol_udp_module.py @@ -0,0 +1,259 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_protocol_udp_module + +short_description: Ce module permet la modification, suppression ou création d'une vip + +description: + - Se connecte en ssh via paramiko + - Envoie la ou les commandes sur le device et retourne les lignes affichées par l'équipement. + +version_added: '0.1.0' + +authors: + - Theo Laurent (@Theo_Laurent) + - Benjamin Boussereau (@Benjamin-Boussereau) + +options: + ip: + description: + - l'ip de l'équipement sur lequel on souhaite intervenir. + type: str + required: true + + username: + description: + - le login utilisé pour se connecter sur le device. + type: str + required: true + + password: + description: + - le password utilisé pour se connecter sur le device. + type: str + required: true + + udp_params: + description: + - dictionnaire qui contient les informations du profile udp telles qu'écrites dans le fichier. + type: dict + required: true + +''' + +EXAMPLES = r''' +- name: Manage the profile protocol udp + F5Networks.f5-ansible-f5modules.module_bigip_profile_protocol_ucp_module: + ip = "1.1.1.4" + username = "myadmin" + password = "mypass" + udp_params = "{{udp_params}}" +''' + + +import paramiko +import sys +import os +import time +import re +from ansible.module_utils.basic import AnsibleModule + +# Foction responsable de l'envoie des commandes ssh sur le server +def fn_ssh(cmdx, server, connection_port, user, pwd): + # initialisation de la variable de sortie + output = "" + # reservation du socket avec le module paramiko SSHClient + ssh=paramiko.SSHClient() + # deifinition de la politique de gestion des clé publique des hosts + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + # lancement de la connexion ssh sur le server + ssh.connect(server,connection_port,user,pwd,allow_agent=False,look_for_keys=False) + # invocation du shell interractif + shell = ssh.invoke_shell() + #tempo + time.sleep(0.5) + # pour chaque commande de la liste fournie en paramètre divisée sur les retours à la ligne nous lançons cette commande et plaçons le retour du shell dans une variable + for cm in cmdx.split("\n"): + cm = cm + "\n" + shell.send(cm) + time.sleep(0.5) + output += shell.recv(65535).decode('utf-8') + # traitement des résultats pour obtenir une liste lisible + resp = output.split("\r\n") + # fermeture du shell et du ssh afin de libérer le socket + shell.close() + ssh.close() + # renvoie de la valeur obtenue + return(resp) + +# Fonction de création d'un profile udp +def fn_creation(udp_params, username, password, ip): + # Initialisation de la commande principale + cmd = "tmsh\n" + cmda = "" + # Construction des blocs de commande à partir des paramètres udp + for key in udp_params: + # On ignore les clés contenant "state" + if "state" in key: + pass + # Commande pour changer de partition + elif key.strip() == "partition": + cmd1 = "cd ../" + udp_params[key].strip() + "\n" + # Commande de création du profil udp + elif key.strip() == "name": + cmd2 = "ltm profile udp \ncreate " + udp_params['name'].strip() + # Ajout des autres paramètres à la commande pour la création + else: + cmda = cmda + " " + str(key).strip() + " " + str(udp_params[key]).strip() + # commande final + cmd = cmd + cmd1 + cmd2 + cmda + return(cmd) + +# Fonction pour générer une commande de modification d’un profil udp existant +def fn_define_cmd_to_modif(conf_to_modify,udp_params): + # Début de la commande de modification (tmsh + le profile udp + le nom du profile qu'on veut modif) + cmd_mod = "tmsh\nltm profile udp\nmodify " + str(udp_params['name']) + # Ajout des paramètres à modifier + for modif in conf_to_modify: + cmd_mod = cmd_mod + " " + modif + return(cmd_mod) + + +# Fonction pour comparer la configuration existante et celle souhaitée dans udps.yml +def fn_compare_conf(udp_params, udp_infos_from_box): + to_modify = [] # Liste des paramètres à modifier + # Parcours des lignes de configuration une à une récupérées depuis le boitier + for udp in range(0,len(udp_infos_from_box),1): + # On ignore toutes les lignes qui possent problème (message inutil, champs inéxistant dans la GUI) + if "END" in udp_infos_from_box[udp] or "}" in udp_infos_from_box[udp] or "app-service" in udp_infos_from_box[udp].strip() or "defaults-from" in udp_infos_from_box[udp].strip() or "ip-ttl-v4" in udp_infos_from_box[udp].strip() or "ip-ttl-v6" in udp_infos_from_box[udp].strip() or "no-checksum" in udp_infos_from_box[udp].strip() or "description" in udp_infos_from_box[udp].strip() or udp_infos_from_box[udp] == '' or "Last" in udp_infos_from_box[udp] or "@" in udp_infos_from_box[udp] or "ltm" in udp_infos_from_box[udp]: + pass + else: + # Traitement des lignes contenant "less" + if "less" in udp_infos_from_box[udp]: + # On découpe la ligne en utilisant les espaces multiples comme séparateurs + new_line = re.split(" +",udp_infos_from_box[udp]) + # On récupère le nom du champ et sa valeur depuis la ligne + udp_field_from_box = new_line[2].strip() + udp_field_valued_from_box = new_line[3].strip() + try: + # On récupère la valeur attendue depuis udp_params via la découpe de la ligne juste au dessus + udp_field_from_file = udp_params[udp_field_from_box] + # Si les deux valeurs sont identique on pass + if str(udp_field_valued_from_box) == str(udp_field_from_file): + pass + else: + # Si la valeur est différente, on ajoute à la liste des modifications + data = udp_field_from_box + " " + str(udp_params[udp_field_from_box]) + to_modify.append(data) + # Si le champ n'existe pas dans udp_params, on ignore + except KeyError: + pass + # Traitement des autres lignes + else: + # On découpe la ligne en utilisant seulement un espace comme séparateurs + udp_field_from_box = udp_infos_from_box[udp].strip().split(' ')[0].strip() + udp_field_valued_from_box = udp_infos_from_box[udp].strip().split(' ')[1].strip() + try: + # On récupère la valeur attendue depuis udp_params via la découpe de la ligne juste au dessus + udp_field_from_file = udp_params[udp_field_from_box] + # Si les deux valeurs sont identique on pass + if str(udp_field_valued_from_box) == str(udp_field_from_file): + pass + else: + # Si la valeur est différente, on ajoute à la liste des modifications + data = udp_field_from_box + " " + str(udp_params[udp_field_from_box]) + to_modify.append(data) + # Si le champ n'existe pas dans udp_params, on ignore + except KeyError: + pass + # Retourne la liste des paramètres à modifier + return(to_modify) + + + + +# Fonction principale chargée de créer, modifier ou supprimer une VIP sur un F5 +def main(): + #### On commence par définir le module Ansible et ses paramètres + module = AnsibleModule( + argument_spec=dict( + ip = dict(required=True, type='str'), + username = dict(required=True, type='str'), + password = dict(required=True, type='str', no_log=True), + udp_params = dict(required=True, type='dict') + ) + ) + port = 22 + ip = module.params.get('ip') + username = module.params.get('username') + password = module.params.get('password') + udp_params = module.params.get('udp_params') + # maintenant que nous avons récupéré les informations passées en argument, nous allons observer le contenu de udp_params + # pour cela, nous lançons une boucle for sur le contenu de la variable et observons l'état du profil udp. + cmd = "tmsh\ncd ../" + udp_params['partition'] + "\nlist ltm profile udp " + udp_params['name'] + " all-properties\n \n \n \n" + # Récupération des informations du boîtier via notre fonction SSH + udp_infos_from_box = fn_ssh(cmd, ip, port, username, password) + not_found = 0 + # Parcours des lignes de configuration une à une récupérées depuis le boîtier + for i in range(0, len(udp_infos_from_box),1): + # Si le profil n'éxiste pas sur le boîtier, alors on ajout 1 + if "not found" in udp_infos_from_box[i]: + not_found = 1 + # Sinon, on ne fait rien + else: + pass + # Traitement des paramètres du profil udp + for param in udp_params: + cmd = "" + # Gestion de l'état du profil (présent ou absent) + if param == "state": + if udp_params[param] != "absent": + # si le status du profil udp dans le fichier est différent de absent alors nous récupérons les informations de ce dernier sur le # Parcours des lignes de configuration une à une récupérées depuis le boîtier + if not_found == 1: + # Si le profil n'existe pas, on effectue les actions suivantes : + creation_profile = fn_creation(udp_params, username, password, ip) + # Appel de notre fonction de création + action_creation = fn_ssh(creation_profile, ip, port, username, password) + # Application sur le boîtier via SSH + module.exit_json(changed=True, message="Le profile udp a ete créé", resultat=action_creation) + else: + # Comparaison de la configuration actuelle avec celle souhaitée + conf_to_modify = fn_compare_conf(udp_params, udp_infos_from_box) + # Ajout des éléments à modifier si des différences existent + if conf_to_modify != []: + # Si des différences sont détectées, on génère les commandes de modification via notre fonction + cmd_to_modify = fn_define_cmd_to_modif(conf_to_modify,udp_params) + modification_profile = fn_ssh(cmd_to_modify, ip, port, username, password) + # Application sur le boîtier via SSH + module.exit_json(changed=True, message="Le profile udp a ete mis à jour", resultat=modification_profile) + else: + module.exit_json(changed=False, message="Le profile udp sur le boitier est identique à celui du fichier") + else: + # vérifier si le profil existe, si oui, lancer la suppression + if "not found" in udp_infos_from_box: + module.exit_json(changed=False, message="Le profile n'existe pas donc nous ne faisons rien") + else: + # commande de suppression + suppr_cmd = "tmsh\ncd ../" + udp_params['partition'] + "\nltm profile udp\ndelete " + udp_params['name'] + "\n" + # appel fonction fn_ssh avec les commandes de suppression + suppression_profile = fn_ssh(suppr_cmd, ip, port, username, password) + module.exit_json(changed=False, message="Le profile udp a été supprimé", resultat=suppression_profile) + + # Gestion des des paramètres spécifiques + elif param in ['idle-timeout', 'ip-tos-to-client', 'link-qos-to-client']: + # Liste de tous les champs spécifiques + if udp_params[param] != "immediate" or udp_params[param] != "indefinite": + # Si les paramètre on une valeur différente de "immediate" ou de "indefinite" + udp_params[param] = int(udp_params[param]) + # On force la valeur en int au lieux de str car spécify + else: + pass + # si les champs on comme valeur "immediate" ou "indefinite" on ignore + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ansible_collections/f5networks/f5_modules/tests/bigip_virtual_server_module.py b/ansible_collections/f5networks/f5_modules/tests/bigip_virtual_server_module.py new file mode 100644 index 0000000..c39d3d6 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/tests/bigip_virtual_server_module.py @@ -0,0 +1,291 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_virtual_server_module + +short_description: Ce module permet la modification, suppression ou création d'une vip + +description: + - Se connecte en ssh via paramiko + - Envoie la ou les commandes sur le device et retourne les lignes affichées par l'équipement. + +version_added: '0.1.0' + +authors: + - Benjamin Boussereau (@Benjamin-Boussereau) + - Theo Laurent (@Theo_Laurent) + +options: + ip: + description: + - l'ip de l'équipement sur lequel on souhaite intervenir. + type: str + required: true + + username: + description: + - le login utilisé pour se connecter sur le device. + type: str + required: true + + password: + description: + - le password utilisé pour se connecter sur le device. + type: str + required: true + + vip_params: + description: + - dictionnaire qui contient les informations de la vip telles qu'écrites dans le fichier. + type: dict + required: true + +''' + +EXAMPLES = r''' +- name: Manage the virtual server + F5Networks.f5-ansible-f5modules.module_bigip_virtual_server_module: + ip = "1.1.1.4" + username = "myadmin" + password = "mypass" + vip_params = "{{vip_params}}" +''' + + +import paramiko +import sys +import os +import time +import re +from ansible.module_utils.basic import AnsibleModule + +# Foction responsable de l'envoie des commandes ssh sur le server +def fn_ssh(cmdx, server, connection_port, user, pwd): + # initialisation de la variable de sortie + output = "" + # reservation du socket avec le module paramiko SSHClient + ssh=paramiko.SSHClient() + # deifinition de la politique de gestion des clé publique des hosts + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + # lancement de la connexion ssh sur le server + ssh.connect(server,connection_port,user,pwd,allow_agent=False,look_for_keys=False) + # invocation du shell interractif + shell = ssh.invoke_shell() + #tempo + time.sleep(0.5) + # pour chaque commande de la liste fournie en paramètre divisée sur les retours à la ligne nous lançons cette commande et plaçons le retour du shell dans une variable + for cm in cmdx.split("\n"): + cm = cm + "\n" + shell.send(cm) + time.sleep(0.5) + output += shell.recv(65535).decode('utf-8') + # traitement des résultats pour obtenir une liste lisible + resp = output.split("\r\n") + # fermeture du shell et du ssh afin de libérer le socket + shell.close() + ssh.close() + # renvoie de la valeur obtenue + return(resp) + + +# fonction responsable de la construction de la commande à envoyé pour créer une vip +def fn_define_cmd_and_send_it(vip, user, pwd, server): + # on commence par initialiser la var cmd. + cmd = "tmsh\n" + cmdx = "" + output = [] + # ensuite, on va lancer la boucle + for v in vip: + print(v) + if "state" in v or "syn-cookie-status" in v: + pass + elif v.strip() == "partition": + cmd1 = "cd ../"+ vip[v].strip() + "\n" + elif v.strip()=="name": + cmd2 = "ltm virtual\ncreate " + vip[v].strip() + elif v.strip() == "profiles": + cmdp = " profiles add " + str(vip[v]).strip().replace("'","") + elif v.strip() == "source-address-translation": + cmds = " source-address-translation " + str(vip[v]).strip().replace("'","") + elif v.strip()=="vlans-disabled" or v.strip()=="vlans-enabled": + cmdv = " " + str(v).strip() + else: + cmdx = cmdx + " " + str(v).strip() + " " + str(vip[v]).strip() + cmd = cmd + cmd1 + cmd2 + cmdp + cmds + cmdv + cmdx + return(cmd) + + +# définition de la fonction permettant la construction des commandes de modification +def fn_cmd_to_modif(list_modif, vip_name, partition): + cmd_mod = "tmsh\nltm virtual\ncd ../"+partition+"\nmodify " + str(vip_name) + for modif in list_modif: + cmd_mod = cmd_mod + " " + modif + return(cmd_mod) + + +# cette focntion à pour objectif de comparer la conf de la vip sur le boitier à sa conf dans le fichier +# elle retourne soit une liste contenant les actions de création et le chiffre 1 comme suit : 1,vip_creation +# soit elle retourne une liste contenant tous les champs à modifier sur le boitier. (nommée to_modify) +def fn_verif_vip_conf(conf_vip_box, conf_vip_file, username, password, ip, port): + to_modify = [] + # pour chaque ligne retournée par la commande ssh + new_pos = 0 + new_pos_2 = 0 + for pos in range(0,len(conf_vip_box),1): + # si la ligne contient "not found", nous lançons les actions de création car cela indique que la VIP n'existe pas + if "not found" in conf_vip_box[pos]: + action_on_vip = fn_define_cmd_and_send_it(conf_vip_file, username, password, ip) + vip_creation = fn_ssh(action_on_vip,ip, port, username,password) + data = [1, vip_creation] + return(data) + # sinon si profiles est dans la ligne + elif conf_vip_box[pos].strip().split(' ')[0] == "profiles" : + profile_params = "" + profile_pos = pos + i = pos + # nous lançons une autre boucle sur le contenu obtenu sur le boitier en commençant à l'index + # de la ligne comprenant "profiles" + # tant que la ligne ne contient pas rate-class, nous ajoutons le contenu de la ligne dans la var + while "rate-class" not in conf_vip_box[i]: + profile_params = profile_params + conf_vip_box[i].strip() + last_pos = i + i += 1 + # ensuite nous récupérons l'index de la dernière ligne et ajoutons 1 de sorte de recommencer à la ligne + # contenant "rate-class" + new_pos = i + 1 + # maintenant si le contenu récupérer sur le boitier est identique à celui du fichier pour le champs profile + # nous ne faison rien, sinon nous ajoutons le champs du fichiers à la var to_modify + if profile_params.replace('profiles','').strip() == conf_vip_file['profiles']: + continue + else: + data = "profiles replace " + conf_vip_file['profiles'] + to_modify.append(data) + # si la valeur de la position en cours est inférieur à celle définie dans la boucle précédente, nous lui donnons cette valeur + elif pos < new_pos: + pos = new_pos + # Dans le même cas que pour le champs profiles, nous vérifions si la ligne contient le mot suivant + elif conf_vip_box[pos].strip().split(' ')[0] == "source-address-translation" : + snat_params = "" + profile_pos = pos + i = pos + # si oui, nous suivons les mêmes étapes que pour profiles + while "source-port" not in conf_vip_box[i]: + snat_params = snat_params + conf_vip_box[i].strip() + last_pos = i + i += 1 + new_pos_2 = i + 1 + if snat_params == conf_vip_file['source-address-translation']: + continue + else: + data = "source-address-translation " + conf_vip_file['source-address-translation'] + to_modify.append(data) + # dans ce cas, si la postion dans la liste à laquelle nous sommes rendues est comprise entre la new_pos obtenue + # dans la boucle profiles et la nouvelle pos obtenu dans la boucle juste au dessus, nous lui donnons la plus grande + elif new_pos