Category: Ansible

  • Ansible Para Todos – Roles

    Breve introducción a roles en Ansible, y demo en vivo!

    Estamos en
    – YouTube https://www.youtube.com/@cceste
    – Instagram https://www.instagram.com/ccesteok
    – Facebook https://www.facebook.com/ccesteok
    – Discord https://discord.gg/ZWQVg7cgdR

    00:00 Intro y musiquita
    00:40 Bienvenida
    01:25 Qué son roles en Ansible
    02:24 Porqué usar roles en Ansible
    04:03 Ventajas de usar roles en Ansible
    05:26 Estructura de un rol
    08:01 Defaults y Vars
    08:40 Tasks
    09:21 Files
    10:15 Templates
    10:54 Handlers
    11:24 Meta
    11:58 Demo de rol en vivo
    32:44 Ansible Galaxy
    33:52 Ansible Galaxy en CLI
    34:11 Roles de Jeff Geerling
    35:46 Ejemplo AS3356 – Enviadas – Tráfico entrante
    35:01 Outro y musiquita

  • Convertite en un Ninja de Jinja

    Introducción a Jinja2, características principales, estructura de los templates
    Variables y expresiones, directivas de control
    Filtros y funciones, comentarios y bloques
    Laboratorio a medida que vamos avanzando

    Emitido en vivo 9 Jul, 2023
    https://youtube.com/live/RPvy5EvM5rQ?feature=share

    Estamos en
    – Instagram https://www.instagram.com/ccesteok
    – Facebook https://www.facebook.com/ccesteok
    – YouTube https://youtube.com/@UCRtmP5A74JvUoFdj6uJiLIQ
    – Discord https://discord.gg/ZWQVg7cgdR

    00:00 Intro y musiquita
    01:10 Bienvenida
    01:27 Jinja para programadores
    02:08 Jinja para administradores
    00:29 Historia de Jinja
    02:54 Templates o plantillas
    03:47 Variables y expresiones
    02:54 Templates o plantillas
    04:35 Directivas de control – If – For
    05:56 Integración con Python
    07:59 Uso de variables
    09:14 Uso de diccionarios
    11:59 Uso de listas
    12:49 Operaciones en plantillas
    14:09 Debug Jinja en VS Code
    15:07 Trabajando con un sólo dato
    15:46 Escribiendo en archivos
    17:32 Introducción a YAML
    19:57 YAML como estado deseado – Interfaces
    20:36 Consumiendo YAML
    26:14 Ordenando todo
    27:30 Espacios en blanco o whitespace
    31:20 YAML como estado deseado – Protocolos
    33:56 Filtros en plantillas
    37:48 Direcciones IP y j2ipaddr
    43:15 YAML como estado deseado – NGINX
    45:56 Reutilizando plantilas donde no se debe
    46:51 Inclusión y herencia de plantillas
    52:30 Fin y musiquita

  • Ansible para todos

    Ansible? Qué es eso??
    Automatización sencilla y rápida
    Arranquemos desde cero y aprovisionemos un servidor

    https://github.com/cceste/ansible-para-todos

    Emitido en vivo Apr 6, 2023
    https://youtube.com/live/ZiiKaQplHDk

  • j2ipaddr

    Jinja2 filters for IP addresses, the easy way

    Why

    On networking and network automation, we need to extract info about IP addresses as a combination of two values:

    • a host address
    • a subnet mask

    For 10.10.10.5/24, the host address is 10.10.10.5 and the subnet mask is 255.255.255.0, and its prefix length is 24.

    There is additional information we can infer from this single item, as its network address, broadcast address.

    Useful data for network engineers are wildcards or hostmasks, network size, class, type, and so on.

    Jinja2 provides several integrated filters to work with, however it can be complicated to use complex data types.

    Ansible provides a way to work this on its ansible.utils.ipaddr collection.

    However, probably you won’t need the entire Ansible package just to be able to use it.

    This package intends to provide a set of filters and handler to the Python 3 netaddr module, on a way that is hopefully easy and lightweight to use.

    What

    Included filters are the following:

    ip_address(addr)

    Returns an IP address for a combination of IP address and subnet mask

    ip_address('10.10.10.5/24')
    > 10.10.10.5
    {{ '10.10.10.5/24 | ip_address }}
    > 10.10.10.5

    ip_prefixlen(addr)

    Returns a prefix length for a combination of IP address and subnet mask

    ip_prefixlen('10.10.10.5/24')
    > 24
    {{ '10.10.10.5/24 | ip_prefixlen }}
    > 24

    ip_netmask(addr)

    Returns a subnet mask for a combination of IP address and subnet mask

    ip_netmask('10.10.10.5/24')
    > 255.255.255.0
    {{ '10.10.10.5/24 | ip_netmask }}
    > 255.255.255.0

    ip_hostmask(addr)

    Returns a wilcard or hostmask for a combination of IP address and subnet mask

    ip_hostmask('10.10.10.5/24')
    > 0.0.0.255
    {{ '10.10.10.5/24 | ip_hostmask }}
    > 0.0.0.255

    ip_wildcard(addr)

    Alias for ip_hostmask(addr)

    ip_wildcard('10.10.10.5/24')
    > 0.0.0.255
    {{ '10.10.10.5/24 | ip_wildcard }}
    > 0.0.0.255

    ip_network(addr)

    Returns a network address for a combination of IP address and subnet mask

    ip_network('10.10.10.5/24')
    > 10.10.10.0
    {{ '10.10.10.5/24 | ip_network_hosts_size }}
    > 10.10.10.0

    ip_broadcast(addr)

    Returns a broadcast address for a combination of IP address and subnet mask

    ip_broadcast('10.10.10.5/24')
    > 10.10.10.255
    {{ '10.10.10.5/24 | ip_broadcast }}
    > 10.10.10.255

    ip_network_hosts_size(addr)

    Returns the size of the subnet for a combination of IP address and subnet mask

    ip_network_hosts_size('10.10.10.5/24')
    > 255
    {{ '10.10.10.5/24 | ip_network_hosts_size }}
    > 255

    ip_network_first(addr)

    Returns the first usable address in network address for a combination of IP address and subnet mask

    ip_network('10.10.10.5/24')
    > 10.10.10.1
    {{ '10.10.10.5/24 | ip_network_hosts_size }}
    > 10.10.10.1

    ip_network_last(addr)

    Returns the last usable address in network address for a combination of IP address and subnet mask

    ip_network('10.10.10.5/24')
    > 10.10.10.254
    {{ '10.10.10.5/24 | ip_network_hosts_size }}
    > 10.10.10.254

    How

    Simply install with pip.

    $ pip install j2ipaddr

    To insert the filters on your Jinja2 processor, simply use the following syntax. The filter name can be changed by adjusting the dict key name.

    import jinja2
    import j2ipaddr.filters
    jinja2.filters.FILTERS['ip_prefixlen'] = filters.ip_prefixlen

    Or, probably an easier way, use the following one-liner to load all the filters into your Jinja2 filters

    import jinja2
    import j2ipaddr.filters
    jinja2.filters.FILTERS = {**jinja2.filters.FILTERS, **filters.load_all()}

    On your templates, you can do this as an example:

    Variables

    host:
      interfaces:
        Te1/0/1:
          ipv4_addresses:
            - 10.10.10.5/24

    Template

    router ospf 10
      network {{host.interfaces.Te1/0/1.ipv4_addresses[0] | ip_network }} {{host.interfaces.Te1/0/1.ipv4_addresses[0] | ip_wildcard  }} area 0.0.0.0

    The output would looks like this:

    router ospf 10
      network 10.0.0.0 0.0.0.255 area 0.0.0.0

    Where

    You can find this project on

  • Using RSYNC with Ansible

    The past week I found myself in a situation where I had to copy a directory to a remote SMB share, using it as a backup destination.

    I didn’t had a login to the remote server, just a share and credentials for it, so the easiest way to sync all the data was to use rsync.

    After I coded a small bash script to execute rsync, the business requirements changed, and this storage was indented to be used as an “offline” backup. Of course the best way to execute an offline copy is to set up an intermediate host, with the following steps:

    1. Mount a share from the intermediate server
    2. Copy the data to this share
    3. Unmount the share
    4. Mount the share in the destination server
    5. Copy the data from the share
    6. Unmount the share

    By using an intermediate server, the source host of the data and the backup destination are never directly connected, meaning that a compromised origin server has no way to directly compromise the destination server, in the worst case scenario.

    At the moment the intermediate server is waiting to be deployed, to I had to wrote a quick Ansible playbook to mount the remote share, copy the data, and unmount the share after the copy.

    Instead of running rsync for the first copy, I suggest to run a standard copy because there is nothing to compare on the destination, and we will save some time and bandwidth.

    An email notification was added to the playbook to get feedback about the synchronization result, as it was syncing about 1TB of data over a slow WAN link.

    ---
    - hosts: remote_server
      gather_facts: no
      become: yes
    
      tasks:
    
        - name: Mount external storage
          mount:
            src: //this_is_a_smb_path/on_another_server
            path: /srv/external
            state: mounted
            fstype: cifs
            opts: username=myuser,password=mypass
    
        - name: Rsync /srv/data to /srv/external
          synchronize:
            archive: yes
            compress: yes
            src: /srv/data
            dest: /srv/external
          delegate_to: remote_server
          register: sync
    
        - name: Unmount external storage
          mount:
            src: //this_is_a_smb_path/on_another_server
            path: /srv/external
            state: unmounted
    
        - name: Send e-mail
          mail:
            host: my.smtp.server
            port: 25
            subject: Ansible Backup Report
            body: "Backup status is {{ sync.rc }}"
            from: Ansible Backups <[email protected]>
            to:
            - [email protected]
    
  • Creating passwordless logins with Ansible

    What kind of users? Well, a special user called Ansible, which will use SSH keys to login into remote devices, allowing for full automation on playbooks.

    Creating a new key

    If you have been following the series, maybe you remember that we already created keys on the Juniper Junos SSH Keys post.

    To create a new key, let’s issue the ssh-keygen command as follows. The -f flag tells the output path, and the -C flags specifies a comment.

    $ ssh-keygen -f ansible.key -C ansible-login-passwordless

    This should output two files, ansible.key and ansible.key.pub.

    The public key should look something like this.

    $ cat ansible.key.pub 
    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCtJUPfzJY6vKqLUssPPQe+LD7qRmIPbVhb/1i4Qab7T0Vf3x+ItfJyV4Ej4FsnRSU8iMU8J5eIdcetGQfsmwIZAm8glB0T6En5F9lvq2Yd+3RKIvxM3UlrIH6EaRedhsRUyV96CHfIO2nVqS9dmFfgrOJMIOwfTWIiRDNczUPw7aqw0FExslw9ZC0FO/1A6hYgofkGLrdIu9gK/WkNg5BE1EUCYPqbDBEHnnhv3C33LqiSJZnXJyqu53qz+jlv+1LZxerNHuovMGZMkjQsBo2f3r9Gk/9HqBmT0rcLr5prm4CqqryJ3S9VyVVlF599BlqYMuMjj+fCj277R8kSnLxl ansible-login-passwordless

    Of course we need an inventory to use, which has the following content.

    $ cat inventory.yml 
    ---
    all:
      hosts:
        vars:
          ansible_ssh_user: ansible
          ansible_ssh_private_key_file: ansible.key
          ansible_python_interpreter: auto_silent
        hosts:
          localhost:
    

    This inventory only has one host, localhost, and uses three main variables:

    • ansible_ssh_user, which tell Ansible to use the user ansible
    • ansible_ssh_private_key_file, which indicates the key for this user
    • ansible_python_interpreter, just to avoid non needed logs

    The playbook will looks like this. Notice we don’t need to gather_facts here, and we will instruct ansible to use become to gain privileges on the destination host.

    ---
    - hosts: all
      become: yes
    
      tasks:
    
        - name: Make sure we have a "wheel" group
          group:
            name: wheel
            state: present
    
        - name: Allow 'wheel' group to have passwordless sudo
          lineinfile:
            dest: /etc/sudoers
            state: present
            regexp: '^%wheel'
            line: '%wheel ALL=(ALL) NOPASSWD: ALL'
            validate: 'visudo -cf %s'
            
        - name: Create "ansible" user
          user:
            name: ansible
            comment: Ansible Automation User
            groups: wheel
    
        - name: Add ssh key
          authorized_key:
            user: ansible
            state: present
            key: "{{ lookup('file', './ansible.key.pub') }}"
    

    First, we want to make sure there is a group called wheel which will group users with administrative privileges.

    Then, the /etc/sudoers file will be edited by allowing the wheel group to gain privileges, with a failsafe using a visudo validation.

    Once the group has been created, the new user will be created, and a SSH key will be added to it.

    It seems allright, but, how should we run the playbook, if the default user is ansible and this user does not exists yet? Let’s give it a try.

    $ ansible-playbook create-user.yml -i inventory.yml 
    
    PLAY [all] ************************************************************************
    
    TASK [Gathering Facts] ************************************************************
    fatal: [localhost]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: ansible@localhost: Permission denied (publickey,password).", "unreachable": true}
    
    PLAY RECAP ************************************************************************
    localhost                  : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
    

    It fails, as expected, because the ansible user does not exists in the host.

    Well, there is a way to provide a one-time password by connecting a as different user. You will need to install sshpass with your favourite package manager, like apt install sshpass.

    One installed, run the playbook once again with the following arguments:

    • -e “ansible_ssh_user=xxxxx”, where xxxxx is a valid user on the remote host
    • -kK, which tell Ansible to ask for a login and a sudo password
    $ ansible-playbook create-user.yml -i inventory.yml -e "ansible_ssh_user=arturo" -kK
    SSH password: 
    BECOME password[defaults to SSH password]: 
    
    PLAY [all] ************************************************************************
    
    TASK [Gathering Facts] ************************************************************
    ok: [localhost]
    
    TASK [Make sure we have a "wheel" group] ******************************************
    changed: [localhost]
    
    TASK [Allow 'wheel' group to have passwordless sudo] ******************************
    changed: [localhost]
    
    TASK [Create "ansible" user] ******************************************************
    changed: [localhost]
    
    TASK [Add ssh key] ****************************************************************
    changed: [localhost]
    
    PLAY RECAP ************************************************************************
    localhost                  : ok=5    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

    Awesome, we have sucessfully created a new user!

    Let’s try to connect using the ansible user with its key, as defined in the playbook.

    $ ansible -m ping -i inventory.yml all
    localhost | SUCCESS => {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        }, 
        "changed": false, 
        "ping": "pong"
    }
    

    Stay tuned for more automation using Ansible.

  • Ansible and Juniper – SSH Keys and Prompts

    On previous posts we’ve seen how to connect with Ansible using credentials stored in a inventory file, and using SSH keys for authentication.

    However, it isn’t a good idea to store credentials in plain text files, neither have to rebuild your inventory when you want to switch over to key authentication.

    A possible solution is to first ask for credentials, run a playbook to install the SSH key, and then use this key for authentication on later playbooks.

    You can find all the files for this post on the following repo.

    https://github.com/baldoarturo/ansible-ssh-keys

    Variable prompts

      vars_prompt:
        - name: "ansible_user"
          prompt: "Username"
          private: no
    

    The vars_prompt section is used to prompt the user for information, which is stored in variables. System variables can be populated, for example the ansible_user and ansible_password variables, allowing us to provide credentials to connect.

    Take a look to the new version of the uptime playbook.

    ---
    - hosts: all
      gather_facts: no
    
      vars_prompt:
        - name: "ansible_user"
          prompt: "Username"
          private: no
          unsafe: yes
    
        - name: "ansible_password"
          prompt: "Password"
          private: yes
          unsafe: yes
    
      tasks:
        - name: Get uptime
          junos_command:
            commands:
                - show system uptime
          register: uptime
        
        - name: Show uptime
          debug: var=uptime

    We’re prompting for the username and password on the vars_prompt section. The private settings indicates if the user input should appear on the screen. The unsafe option allows to enter special chars.

    The task to execute are:

    • Get system uptime via the junos_command module, with “show system uptime”
    • Print the result using debug

    And the new (and definitive) inventory looks like this now.

    all:
        hosts:
          "192.168.227.101":
        vars:
          ansible_connection: netconf
          ansible_network_os: junos
          ansible_ssh_private_key_file: juniper-hosts.key
          ansible_python_interpreter: auto_silent
    

    The ansible_python_interpreter variable is set to auto_silent just to avoid the warning about no Python interpreters on the remote end.

    Let’s give the playbook a run, trying to login with user and password. If you have not been following the Ansible series, let me tell you that there is an user admin with a password of Password$1 on the router. Note that the password won’t be seen on the screen.

    arturo@arturo-ThinkPad-L440:~/Desktop/ansible-01$ ansible-playbook junos-auth-with-key.yaml -i junos-hosts.yaml 
    Username: admin
    Password: 
    
    PLAY [all] ******************************************************************************************************************
    
    TASK [Get uptime] ***********************************************************************************************************
    ok: [192.168.227.101]
    
    TASK [Show uptime] **********************************************************************************************************
    ok: [192.168.227.101] => {
        "uptime": {
            "ansible_facts": {
                "discovered_interpreter_python": "/usr/bin/python"
            }, 
            "changed": false, 
            "failed": false, 
            "stdout": [
                "Current time: 2020-01-13 17:12:32 UTC\nSystem booted: 2020-01-13 14:55:46 UTC (02:16:46 ago)\nProtocols started: 2020-01-13 14:56:03 UTC (02:16:29 ago)\nLast configured: 2020-01-12 16:09:02 UTC (1d 01:03 ago) by admin\n 5:12PM  up 2:17, 2 users, load averages: 0.00, 0.00, 0.00"
            ], 
            "stdout_lines": [
                [
                    "Current time: 2020-01-13 17:12:32 UTC", 
                    "System booted: 2020-01-13 14:55:46 UTC (02:16:46 ago)", 
                    "Protocols started: 2020-01-13 14:56:03 UTC (02:16:29 ago)", 
                    "Last configured: 2020-01-12 16:09:02 UTC (1d 01:03 ago) by admin", 
                    " 5:12PM  up 2:17, 2 users, load averages: 0.00, 0.00, 0.00"
                ]
            ]
        }
    }
    
    PLAY RECAP ******************************************************************************************************************
    192.168.227.101            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

    Great, the prompts work.

    What if we try to login with the user ansible we configured on the previous post? This user has an SSH key installed on the router, and the local private key is on juniper-hosts.key.

    arturo@arturo-ThinkPad-L440:~/Desktop/ansible-01$ ansible-playbook junos-auth-with-key.yaml -i junos-hosts.yaml 
    Username: ansible
    Password: 
    
    PLAY [all] ******************************************************************************************************************
    
    TASK [Get uptime] ***********************************************************************************************************
    ok: [192.168.227.101]
    
    TASK [Show uptime] **********************************************************************************************************
    ok: [192.168.227.101] => {
        "uptime": {
            "ansible_facts": {
                "discovered_interpreter_python": "/usr/bin/python"
            }, 
            "changed": false, 
            "failed": false, 
            "stdout": [
                "Current time: 2020-01-13 17:32:05 UTC\nSystem booted: 2020-01-13 14:55:46 UTC (02:36:19 ago)\nProtocols started:
     2020-01-13 14:56:03 UTC (02:36:02 ago)\nLast configured: 2020-01-12 16:09:02 UTC (1d 01:23 ago) by admin\n 5:32PM  up 2:36, 
    1 user, load averages: 0.00, 0.01, 0.00"
            ], 
            "stdout_lines": [
                [
                    "Current time: 2020-01-13 17:32:05 UTC", 
                    "System booted: 2020-01-13 14:55:46 UTC (02:36:19 ago)", 
                    "Protocols started: 2020-01-13 14:56:03 UTC (02:36:02 ago)", 
                    "Last configured: 2020-01-12 16:09:02 UTC (1d 01:23 ago) by admin", 
                    " 5:32PM  up 2:36, 1 user, load averages: 0.00, 0.01, 0.00"
                ]
            ]
        }
    }
    
    PLAY RECAP ******************************************************************************************************************
    192.168.227.101            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

    Excellent, by using the user ansible without password, it will fallback to the key authentication.

  • Ansible and Juniper Junos – Using SSH Keys

    Previous posts introduced basics connection methods to manage Juniper devices using Ansible playbooks. The inventory files had sensitive information and credentials which should not be accessible to anyone.

    SSH and NETCONF over SSH requires client authentication, for example with and username and password, which could looks like this:

    admin> show configuration system login 
    user admin {
        uid 2000;
        class super-user;
        authentication {
            encrypted-password "$1$./TeE4CZ$uAMigDedlRuuJgcZx4hYk0"; ## SECRET-DATA
        }
    }
    

    If you are a frequent SSH user, maybe you are aware that there are other login methods besides using usernames and passwords. By using a key-pair, with public and private keys, a password is no longer needed. The public key is installed on the remote host, and the private key is kept on the control node.

    Although by using keys a password is no longer needed, a passphrase can be used with a key, adding an additional security factor to the connection. In fact, using SSH keys with passphrases is considered best practice. However, a private key with a passphrase is less useful for scheduled automation tasks because an operator may not be available to enter the passphrase at the scheduled time.

    Creating a Key Pair

    A key pair is a set of two cryptographic keys, a public one and a private one. The public key, as its name says, is the one we expose to the public. The private key, must be kept in a secure location.

    To create a key pair, lauch ssh-keygen on a new console and follow the prompts. This utility will create two files, which are the public and private keys. Use the -f flag to specify the destination of the output files.

    arturo@arturo-ThinkPad-L440:~/Desktop/ansible-01$ ssh-keygen -f ./juniper-hosts.key
    Generating public/private rsa key pair.
    Enter passphrase (empty for no passphrase): 
    Enter same passphrase again: 
    Your identification has been saved in ./juniper-hosts.key.
    Your public key has been saved in ./juniper-hosts.key.pub.
    The key fingerprint is:
    SHA256:Kc/MZ11dLlXpcrK9PKO4L6XzaTrdczaek1ydzaFTFXw arturo@arturo-ThinkPad-L440
    The key's randomart image is:
    +---[RSA 2048]----+
    |              ..+|
    |               oE|
    |              . =|
    |         .   o O.|
    |      . S     @.B|
    |       *   . * +=|
    |        = o = = +|
    |         o =.o.%+|
    |           +X=o+B|
    +----[SHA256]-----+
    arturo@arturo-ThinkPad-L440:~/Desktop/ansible-01$ ls 
    juniper-hosts.key  juniper-hosts.key.pub
    

    Now that we have created the key pair, let’s examine them to find out how a key looks like.

    This is the private key, which in fact is a plain text file with a RSA key inside it.

    arturo@arturo-ThinkPad-L440:~/Desktop/ansible-01$ cat juniper-hosts.key
    -----BEGIN RSA PRIVATE KEY-----
    MIIEpQIBAAKCAQEAn0XtdTPJxWHQHeWi8IwSOGtsgWwW3z86Z91edH0dBS6SWDzm
    seWshSTD2PdD8EU0mGac1V9+rWIJYIw0VZlpTeEKiNnS+1/feHxln+S0LLwn91E6
    fgVRt6kE/9uTv217Pa5eP5HLbPuSwPsVMwRgdCJL1pYIFagJLyrsakDWE0qDtBXZ
    0fendmUr6NrrCLofjHBkRASviJ9E4Vvx4YqrG9ak+2jX21g0LRNxZ/nFv2uHwVeI
    gibC+1JiZ/IGaBSNCovF131wb1AUyLm5Z5DNvM6hu2C6+cMTNNQ5fiBfgMpryboW
    4+MVCGJQ31EthyFEx0XPgjos/EcS9Pp436OppwIDAQABAoIBAQCVczEws4qV2oVF
    OG/fFSAXrr0e6ATCMHsmcLKrzaZIcX3CrEqwDNoICQp4cPRf5SBIDKkHElc0a/Ru
    ksCcvZnxCMQwy2vMkhaH4PoewaRLAbbiu2aOT4FxO3jEeA44JovowdAQCEcAmUMI
    L9GhkG7NKk1NKnSllYogpz81KGd3qw21sRqb1NTLAlYnE4KOhJz+GKmJV8NdAaRj
    zjkVeaLf3t/FCxPRhdAtoADkRQSS1KSCjU0hx0lDCmsdJzM8gFJykltzzBQJ9tZu
    voPZ0TkaIjrR7o8+Oez/pkvUSa1AJPhmH7l6P3RqHdSzMJGQraM1yuvBOTixbnkt
    lsQ26tZpAoGBAM/CbeNhJvGfl18kLLLLSsNb4femXXlBimo6TW4tOGdOunD6+fyt
    LiA0FMLpWfvAh++/yvWX/jee+E3uXkDLLfQVWqWBbfFqIhU4VOKLpMvE5sMQzkOc
    OyooZNR5hui9e2+eU5P2qND/MUVy4YrUBdHtK58Or/cqYT6sHA78e9OtAoGBAMRB
    Y8F79BaqYoH7x0lJf51A45U5rLzKom8eJ+aJujDx9RgDvCEWCZmZ53q6MjvNgNBp
    v5AZptCDn0nIfAVOn2hCmTIPs1IvaLgVfMtmufxzdM1aGdYF7wEu0u7DV+Dzspf2
    h9Q/9C4Or4YZ5oe25Qf5mkwb+xnnmTZCWZETC70jAoGALvskplp91/3i2RzxDq1y
    BqNsgfgZAyaTClqMz/Fh49qlxo66oSz4VUfxufHS618qXkjcuJTaY/GK7PSOU9Ce
    X6fEi9Cs7/60HmBSsbgqV/n6xPmz6w4VQv9HbdTdcRwIIcGH3NnWawyKM846uo4f
    ks0zJBDKMfZfbzC0V5840TECgYEAraoTZR6Tsw7ZFp6/DZoNZBEMknsz4OgK7vsn
    YbiUW0VwleyQKFMA8bwf+xkS5JqIF2TMT+5zD+a5KKhRHr0hEDiGqab9DofHScYx
    5SelAsEEJcdKP3qGsWxG2WNguz3K1vAf5/Ej2THDno4C0itE5la4dAr6m0S27i2u
    ZlMNOzMCgYEAr3/7pkN2LdUCVZYEjVMAW4YxSzi4R5JeixyidZwihVF4GqqqJ2Nl
    VaOQzleNYUg23QWBs/n6yz2iDQaQdKXNCcOwrsQYzkX0aMuEz+4iWXkDKcRIvGWt
    5H/7ShsKoXFsvwbjXkaTnirBMqAJh4jyv4R3oAqIy966zJ5K2vd0nT4=
    -----END RSA PRIVATE KEY-----

    And the public keys looks like this.

    arturo@arturo-ThinkPad-L440:~/Desktop/ansible-01$ cat juniper-hosts.key.pub 
    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCfRe11M8nFYdAd5aLwjBI4a2yBbBbfPzpn3V50fR0FLpJYPOax5ayFJMPY90PwRTSYZpzVX36tYglgjDRVmWlN4QqI2dL7X994fGWf5LQsvCf3UTp+BVG3qQT/25O/bXs9rl4/kcts+5LA+xUzBGB0IkvWlggVqAkvKuxqQNYTSoO0FdnR96d2ZSvo2usIuh+McGREBK+In0ThW/Hhiqsb1qT7aNfbWDQtE3Fn+cW/a4fBV4iCJsL7UmJn8gZoFI0Ki8XXfXBvUBTIublnkM28zqG7YLr5wxM01Dl+IF+AymvJuhbj4xUIYlDfUS2HIUTHRc+COiz8RxL0+njfo6mn arturo@arturo-ThinkPad-L440
    

    This keypair can authenticate an user which connects via SSH.

    Installing the public key on Junos

    We already know that a basic authentication schema on Junos looks like this.

    admin> show configuration system login 
    user admin {
        uid 2000;
        class super-user;
        authentication {
            encrypted-password "$1$kYNQ.bg0$4T3W7GAPuXwsX3nbbsRCb/"; ## SECRET-DATA
        }
    }
    

    The main idea of using SSH keys, is to avoid user interaction, by trusting the keys instead of a credentials combination.

    As seen above, the keys are plain text files. We need to install the public key on the Junos configuration, either configuring it manually, or using Ansible to configure it.

    Manually configuring the key

    I added a new user called ansible, set its class as super-user and configured its authentication as ssh-rsa.

    [edit]
    admin# show system login | display set                                                                                                                                 
    set system login user admin uid 2000
    set system login user admin class super-user
    set system login user admin authentication encrypted-password "$1$MExZQJdK$lLhnzSw.CLSMQg5bdIiws."
    set system login user ansible uid 2001
    set system login user ansible class super-user
    set system login user ansible authentication ssh-rsa "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCfRe11M8nFYdAd5aLwjBI4a2yBbBbfPzpn3V50fR0FLpJYPOax5ayFJMPY90PwRTSYZpzVX36tYglgjDRVmWlN4QqI2dL7X994fGWf5LQsvCf3UTp+BVG3qQT/25O/bXs9rl4/kcts+5LA+xUzBGB0IkvWlggVqAkvKuxqQNYTSoO0FdnR96d2ZSvo2usIuh+McGREBK+In0ThW/Hhiqsb1qT7aNfbWDQtE3Fn+cW/a4fBV4iCJsL7UmJn8gZoFI0Ki8XXfXBvUBTIublnkM28zqG7YLr5wxM01Dl+IF+AymvJuhbj4xUIYlDfUS2HIUTHRc+COiz8RxL0+njfo6mn arturo@arturo-ThinkPad-L440"

    The SSH public key is copied and pasted between double quotes.

    Using Ansible to configure the key

    Altough the key can be configured manually on the remote hosts, what if we have hundreds, or thousands of hosts to configure?

    The idea behind this series of posts is to use Ansible whenever possible, so, let’s write a quick playbook to automate the key configuration.

    rturo@arturo-ThinkPad-L440:~/Desktop/ansible-01$ cat junos-install-ssh-key.yaml 
    ---
    - hosts: all
      gather_facts: no
    
      vars:
        - auth_key: "{{lookup('file', '{{ key_file }}')}}"    
    
      tasks:
        - name: Install SSH key on remote host
          junos_config:
            lines:
              - set system login user ansible authentication ssh-rsa "{{ auth_key }}"
              - set system login user ansible class super-user
    

    The playbook starts as usual, matching all hosts in the inventory, and without gathering facts, just for the sake of speed.

    On vars, we are using the lookup plugin to read from a file and store its contents on a variable. Lookup can retrieve data from multiple sources, for example, take a secret from Hashicorp’s Vault. In this scenario, it will read a file which name is take from the key_file variable from the inventory.

    It is possible to set a fixed file name on the playbook, but by taking the filename as a variable from the inventory, it gives us more flexibility. We could have multiple keys and rotate them by just changing the file name on the inventory, or use different keys per host group, and still apply the playbook to the full inventory, while using the proper key for each group.

    The inventory for this playbook looks like the following. Notice the key_file variable which tells the playbook where to look for the key.

    arturo@arturo-ThinkPad-L440:~/Desktop/ansible-01$ cat junos-hosts.yaml 
    all:
        hosts:
          "192.168.227.101":
        vars:
          ansible_connection: netconf
          ansible_network_os: junos
          ansible_user: admin
          ansible_password: Password$1
          key_file: juniper-hosts.key.pub
    

    Running the playbook to install the key

    The current configuration of router logins is:

    admin> show configuration system login 
    user admin {
        uid 2000;
        class super-user;
        authentication {
            encrypted-password "$1$MExZQJdK$lLhnzSw.CLSMQg5bdIiws."; ## SECRET-DATA
        }
    }
    

    Let’s run the playbook to apply the new configuration which will create the ansible user, set ssh-rsa authentication for it, and set its class as super-user.

    arturo@arturo-ThinkPad-L440:~/Desktop/ansible-01$ ansible-playbook junos-install-ssh-key.yaml -i junos-hosts.yaml 
    
    PLAY [all] *****************************************************************************
    
    TASK [Install SSH key on remote host] **************************************************
    changed: [192.168.227.101]
    
    PLAY RECAP *****************************************************************************
    192.168.227.101            : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

    Ok, the playbook executed with no errors, and Ansible says there is 1 changed host, which is what we expected.

    Let’s check the router configuration again.

    admin> show configuration system login    
    user admin {
        uid 2000;
        class super-user;
        authentication {
            encrypted-password "$1$MExZQJdK$lLhnzSw.CLSMQg5bdIiws."; ## SECRET-DATA
        }
    }
    user ansible {
        uid 2001;
        class super-user;
        authentication {
            ssh-rsa "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCfRe11M8nFYdAd5aLwjBI4a2yBbBbfPzpn3V50fR0FLpJYPOax5ayFJMPY90PwRTSYZpzVX36tYglgjDRVmWlN4QqI2dL7X994fGWf5LQsvCf3UTp+BVG3qQT/25O/bXs9rl4/kcts+5LA+xUzBGB0IkvWlggVqAkvKuxqQNYTSoO0FdnR96d2ZSvo2usIuh+McGREBK+In0ThW/Hhiqsb1qT7aNfbWDQtE3Fn+cW/a4fBV4iCJsL7UmJn8gZoFI0Ki8XXfXBvUBTIublnkM28zqG7YLr5wxM01Dl+IF+AymvJuhbj4xUIYlDfUS2HIUTHRc+COiz8RxL0+njfo6mn arturo@arturo-ThinkPad-L440"; ## SECRET-DATA
        }
    }
    

    There is a new user called ansible, with all the parameters we specified. That’s great!

    Authenticating using keys

    I wrote another playbook to show the system uptime

    ---
    - hosts: all
      gather_facts: no
    
      tasks:
        - name: Get uptime
          junos_command:
            commands:
                - show system uptime
          register: uptime
        
        - name: Show uptime
          debug: var=uptime

    And our inventory now looks like this.

    all:
        hosts:
          "192.168.227.101":
        vars:
          ansible_connection: netconf
          ansible_network_os: junos
          ansible_user: ansible
          ansible_ssh_private_key_file: juniper-hosts.key
          ansible_python_interpreter: auto_silent
    

    There is no plain text password, but instead, by setting up the ansible_ssh_private_key_file variable, we are instructing Ansible to authenticate using the private key.

    arturo@arturo-ThinkPad-L440:~/Desktop/ansible-01$ ansible-playbook junos-auth-with-key.yaml -i junos-hosts-w-key.yaml 
    
    PLAY [all] *****************************************************************************
    
    TASK [Get uptime] **********************************************************************
    ok: [192.168.227.101]
    
    TASK [Show uptime] *********************************************************************
    ok: [192.168.227.101] => {
        "uptime": {
            "ansible_facts": {
                "discovered_interpreter_python": "/usr/bin/python"
            }, 
            "changed": false, 
            "failed": false, 
            "stdout": [
                "Current time: 2020-01-12 16:25:10 UTC\nSystem booted: 2020-01-12 13:42:06 UTC (02:43:04 ago)\nProtocols started: 2020-01-12 13:42:27 UTC (02:42:43 ago)\nLast configured: 2020-01-12 16:09:02 UTC (00:16:08 ago) by admin\n 4:25PM  up 2:43, 3 users, load averages: 0.08, 0.02, 0.01"
            ], 
            "stdout_lines": [
                [
                    "Current time: 2020-01-12 16:25:10 UTC", 
                    "System booted: 2020-01-12 13:42:06 UTC (02:43:04 ago)", 
                    "Protocols started: 2020-01-12 13:42:27 UTC (02:42:43 ago)", 
                    "Last configured: 2020-01-12 16:09:02 UTC (00:16:08 ago) by admin", 
                    " 4:25PM  up 2:43, 3 users, load averages: 0.08, 0.02, 0.01"
                ]
            ]
        }
    }
    
    PLAY RECAP *****************************************************************************
    192.168.227.101            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

    This is great, now Ansible authenticates using the SSH key. You maybe are thinking:

    “Do i have to edit my inventory every time i want to use keys?”

    The answer is, no, and in the next post we will set a interactive prompt to connect using user and password to run the first playbook, which will configure the key, and then we will run all the other playbooks connecting with this key.

    Stay tuned for more!

  • Ansible and Juniper Junos – Playbook Variables and Raw Commands

    As seen on previous posts, Ansible has a decent catalog of modules for Junos, allowing various operations like:

    • Manage layer 2 and layer 3 intefaces
    • Manage LAG and LACP
    • Configure VLANs and VRFs
    • And many others

    But, what happens if you want to automate an operation which doesn’t has its own dedicated module. For example, routing protocols.

    Enter junos_config

    As you might guess, the junos_config module allows us to execute arbitrary commands on the Junos CLI, using Ansible. This maybe doesn’t makes sense for a single host (or does it?), but when you have a dozen, a hundred, or thousands hosts with similar configuration, it is a life-saver.

    Using Ansible, you could update the NTP servers for your entire fleet, or just a group of hosts. Maybe you need to bulk update passwords, change routing protocols keys, deploy a new service, or any other repetitive task.

    Even if Ansible can help you with routine and repetitive tasks, another of its main features is idempotency. For Ansible it means after 1 run of a playbook to set things to a desired state, further runs of the same playbook should result in 0 changes. In simplest terms, idempotency means you can be sure of a consistent state in your environment. This can ensure consistent configuration across all the devices.

    Configuring OSPF in Junos using Ansible

    For this example scenario we’ll use two Juniper vSRX instances linked to each other via ge-0/0/1.

    The only commands to be entered from the router CLI are:

    • IP addresses for the management interface ge-0/0/0.
      • vSRX01 ge-0/0/0 192.168.100.101/24
      • vSRX02 ge-0/0/0 192.168.100.102/24
    • Enabling SSH, and NETCONF over SSH

    The objective is to use Ansible to configure the rest.

    • Create a new non-root user
      • As this is a lab environment, we won’t be using AAA, and the user will be authenticated locally with admin for the username, and Password$1 for the password.
    • Proper IP address for the interfaces
      • vSRX01 ge-0/0/1 will use 10.10.10.1/30
      • vSRX02 ge-0/0/1 will use 10.10.10.2/30
    • Establish a OSPF relation between them to exchange routes
      • Interfaces in area 0, no authentication for now.

    Cloud tap1 is connected to a host-only network of the GNS3 VM, in fact a tap inteface, in the 192.168.100.0/24 subnet. This keeps the management interfaces in a common subnet, which is also accessible to the physical machine.

    This is a reference configuration for vSRX01:

    admin@vSRX01# show | display set             
    set version 12.1X47-D20.7
    set system host-name vSRX01
    set system root-authentication encrypted-password "$1$V7oU.Wtz$ferb8fVB.FNBf/kqGSr.V1"
    set system login user admin uid 2000
    set system login user admin class super-user
    set system login user admin authentication encrypted-password "$1$tUJBllq0$XVWiCQYBv5H9KgEwK1Ovj0"
    set system services ssh
    set system services netconf ssh
    set system services web-management http interface ge-0/0/0.0
    set system syslog user * any emergency
    set system syslog file messages any any
    set system syslog file messages authorization info
    set system syslog file interactive-commands interactive-commands any
    set system license autoupdate url https://ae1.juniper.net/junos/key_retrieval
    set interfaces ge-0/0/0 unit 0 family inet address 192.168.100.101/24
    set interfaces ge-0/0/1 unit 0
    set security zones security-zone MGMT host-inbound-traffic system-services ssh
    set security zones security-zone MGMT host-inbound-traffic system-services ping
    set security zones security-zone MGMT host-inbound-traffic system-services snmp
    set security zones security-zone MGMT host-inbound-traffic system-services netconf
    set security zones security-zone MGMT interfaces ge-0/0/0.0
    set security zones security-zone BACKBONE host-inbound-traffic system-services ping
    set security zones security-zone BACKBONE host-inbound-traffic protocols ospf
    set security zones security-zone BACKBONE interfaces ge-0/0/1.0
    
    

    Using Inventory Variables

    A couple changes has been made to the previous inventory. Now we have two hosts entries on the all group, and each host has additional information below its definition.

    all:
      hosts:
        "192.168.100.101":
          ipv4_address: 10.10.10.1/24
        "192.168.101.102":
          ipv4_address: 10.10.10.2/24
      vars:
        ansible_connection: netconf
        ansible_network_os: junos
        ansible_user: admin
        ansible_password: Password$1

    The new field, ipv4_address, contains information that will be passed to the playbook. Inside the playbook, this change is reflected in the following fashion.

    ---
    - hosts: all
      gather_facts: no
    
      tasks:
    
      - name: Config ge-0/0/1
        junos_l3_interfaces:
          config:
            - name: ge-0/0/1
              ipv4:
                - address: "{{ ipv4_address }}"
          state: replaced

    Can you see the {{ ipv4_address }} field?

    The variable in brackets is assigned with the respective information from the inventory. This means, when Ansible executes the module on a specific host, the ipv4_address value will be as declared on the playbook.

    This is a very simple and unefficient method, because the interface is fixed on the playbook, and it could be assigned from the inventory file, but it’s enough for a first approach. Stay tuned for more scenarios with advanced methods.

    Another change it that the desired state of the configuration now is replaecd instead of merged. This is idempotency. No matter what you have under ge-0/0/1, Ansible will replace the configuration with what builds from the inventory and the module.

    arturo@arturo-ThinkPad-L440:~/Desktop/ansible$ ansible-playbook juniper.yml -i juniper-hosts.yml
    
    PLAY [all] ********************************************************************************************************************
    
    TASK [Config ge-0/0/1] ********************************************************************************************************
    [WARNING]: Platform linux on host 192.168.100.101 is using the discovered Python interpreter at /usr/bin/python, but future
    installation of another Python interpreter could change this. See
    https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more information.
    
    changed: [192.168.100.101]
    [WARNING]: Platform linux on host 192.168.100.102 is using the discovered Python interpreter at /usr/bin/python, but future
    installation of another Python interpreter could change this. See
    https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more information.
    
    changed: [192.168.100.102]
    
    TASK [Put ge-0/0/1 on OSPF area 0] ********************************************************************************************
    changed: [192.168.100.101]
    changed: [192.168.100.102]
    
    PLAY RECAP ********************************************************************************************************************
    192.168.100.101            : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
    192.168.100.102            : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

    Ok, we can see that Ansible identified needed changes on both hosts, so we can run the playbook now.

    arturo@arturo-ThinkPad-L440:~/Desktop/ansible$ ansible-playbook juniper.yml -i juniper-hosts.yml
    
    PLAY [all] ********************************************************************************************************************
    
    TASK [Config ge-0/0/1] ********************************************************************************************************
    [WARNING]:  statement not found
    
    [WARNING]: Platform linux on host 192.168.227.102 is using the discovered Python interpreter at /usr/bin/python, but future
    installation of another Python interpreter could change this. See
    https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more information.
    
    changed: [192.168.227.102]
    [WARNING]: Platform linux on host 192.168.227.101 is using the discovered Python interpreter at /usr/bin/python, but future
    installation of another Python interpreter could change this. See
    https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more information.
    
    changed: [192.168.227.101]
    
    PLAY RECAP ********************************************************************************************************************
    192.168.227.101            : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
    192.168.227.102            : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

    The output is the same, but the changes are being effective now. Go ahead and check the configuration on the routers.

    admin@VMX01> show configuration interfaces 
    ge-0/0/1 {
        unit 0 {
            family inet {
                address 10.10.10.1/24;
            }
            family inet6;
        }
    }
    fxp0 {
        unit 0 {
            family inet {
                address 192.168.227.101/24;
            }
        }
    }
    

    As expected, the ge-0/0/1 is configured now with the correct configuration.

    Now, try to add an additional dummy address to VMX1 ge-0/0/1.

    admin@VMX02# set interfaces ge-0/0/1 unit 0 family inet address 192.168.77.44/24
    
    admin@VMX02# commit
    
    admin@VMX02> show configuration interfaces ge-0/0/1
    ge-0/0/1 {
        unit 0 {
            family inet {
                address 10.10.10.2/24;
                address 192.168.77.44/24;
            }
            family inet6;
        }
    }
    

    Run the playbook once again, and see what happens.

    arturo@arturo-ThinkPad-L440:~/Desktop/ansible$ ansible-playbook juniper.yml -i juniper-hosts.yml
    
    PLAY [all] ********************************************************************************************************************
    
    TASK [Config ge-0/0/1] ********************************************************************************************************
    [WARNING]:  statement not found
    
    [WARNING]: Platform linux on host 192.168.227.101 is using the discovered Python interpreter at /usr/bin/python, but future
    installation of another Python interpreter could change this. See
    https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more information.
    
    ok: [192.168.227.101]
    [WARNING]: Platform linux on host 192.168.227.102 is using the discovered Python interpreter at /usr/bin/python, but future
    installation of another Python interpreter could change this. See
    https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more information.
    
    changed: [192.168.227.102]
    
    PLAY RECAP ********************************************************************************************************************
    192.168.227.101            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
    192.168.227.102            : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
    
    arturo@arturo-ThinkPad-L440:~/Desktop/ansible$ 

    Ansible tell us that one host has changed, so it is applying the configuration once again. This is idempotency stands for.

    Using junos_config

    I changed the inventory once again, and it looks like this now.

    all:
       hosts:
         "192.168.227.101":
           ipv4_address: 10.10.10.1/24
           ospf_area_0: set protocols ospf area 0.0.0.0 interface ge-0/0/1
         "192.168.227.102":
           ipv4_address: 10.10.10.2/24
           ospf_area_0: set protocols ospf area 0.0.0.0 interface ge-0/0/1
       vars:
         ansible_connection: netconf
         ansible_network_os: junos
         ansible_user: admin
         ansible_password: Password$1

    The ospf_area_0 item now is a raw Junos CLI command to run OSPF on area 0 on ge-0/0/1.

    The playbook now has a new task which uses the junos_config module to run the previous set command. Notice the lines parameter is a list, so it is possible to run an arbitrary number of configuration commands. There is also a confirm_commit parameter to apply the new configuration.

    ---
    - hosts: all
      gather_facts: no
    
      tasks:
    
      - name: Config ge-0/0/1
        junos_l3_interfaces:
          config:
            - name: ge-0/0/1
              ipv4:
                - address: "{{ ipv4_address }}"
          state: replaced
      
      - name: Put ge-0/0/1 on OSPF area 0
        junos_config: 
          lines:
            - "{{ ospf_area_0 }}"
          confirm_commit: yes

    Run this new playbook and check the results.

    arturo@arturo-ThinkPad-L440:~/Desktop/ansible$ ansible-playbook juniper.yml -i juniper-hosts.yml
    
    PLAY [all] ********************************************************************************************************************
    
    TASK [Config ge-0/0/1] ********************************************************************************************************
    [WARNING]: Platform linux on host 192.168.100.101 is using the discovered Python interpreter at /usr/bin/python, but future
    installation of another Python interpreter could change this. See
    https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more information.
    
    changed: [192.168.100.101]
    [WARNING]: Platform linux on host 192.168.100.102 is using the discovered Python interpreter at /usr/bin/python, but future
    installation of another Python interpreter could change this. See
    https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more information.
    
    changed: [192.168.100.102]
    
    TASK [Put ge-0/0/1 on OSPF area 0] ********************************************************************************************
    changed: [192.168.100.101]
    changed: [192.168.100.102]
    
    PLAY RECAP ********************************************************************************************************************
    192.168.100.101            : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
    192.168.100.102            : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

    Well, something has changed.

    admin@vSRX01> show interfaces terse | match ge- 
    ge-0/0/0                up    up  
    ge-0/0/0.0              up    up   inet     192.168.100.101/24
    ge-0/0/1                up    up  
    ge-0/0/1.0              up    up   inet     10.10.10.1/24   
    ge-0/0/2                up    up  
    ge-0/0/3                up    up
    
    admin@vSRX01> show ospf neighbor 
    Address          Interface              State     ID               Pri  Dead
    10.10.10.2       ge-0/0/1.0             Full      10.10.10.2       128    3

    Everything worked as expected. The interface is now configured, and OSPF is running on both ge-0/0/1.0 interfaces

  • Ansible and Juniper Junos – Interfaces

    Previously we had made our first incursions connecting an Ansible control node with a Juniper router. In this post, we’ll see how to retrieve information about the router interfaces, both layer 2 and layer 3, and configure new interfaces.

    The official Ansible modules reference will be your main guide for any additional information.
    https://docs.ansible.com/ansible/latest/modules/list_of_network_modules.html#junos

    If you are interested on this subject, but don’t have access to physical gear, most of it should work on virtual appliances like vMX, vQFX, which you can operate on a stand-alone mode or on a network environment like GNS3 or EVE-NG.

    Juniper vLabs will also give you an introduction to the Juniper platform.
    https://jlabs.juniper.net/vlabs/portal/index.page

    Layer 2 Interfaces

    A basic layer 2 interface configuration in Junos looks like this:

    ge-0/0/1 {
         description "L2 interface";
         speed 1g;
         unit 0 {
             family ethernet-switching {
                 interface-mode access;
                 vlan {
                     members vlan30;
                 }
             }
         }
     }

    This configuration can be written as an Ansible playbook like this:

    - name: "Replace provided configuration with device configuration"
      junos_l2_interfaces:
        config:
          - name: ge-0/0/1
            access:
              vlan: v30
        state: merged

    Currently, I do not have any EX series or QFX series to decomission and run tests against it, so stay tuned for any updates on this.

    The official module documentation is on https://docs.ansible.com/ansible/latest/modules/junos_l2_interfaces_module.html.

    Layer 3 Interfaces

    A basic layer 3 interface configuration in Junos looks like this:

    ge-0/0/1 {
         unit 0 {
             family inet {
                 address 192.168.1.10/24;
             }
         }
     }

    This configuration can be written as an Ansible playbook like the following, using the same format as the last post.

    ---
    - hosts: all
      gather_facts: no
    
      tasks:
    
      - name: Config ge-0/0/1
        junos_l3_interfaces:
          config:
            - name: ge-0/0/1
              ipv4:
                - address: 192.168.1.10/24
          state: merged

    Let’s run it and check the result.

    $ ansible-playbook juniper.yml -i juniper-hosts.yml
    
    PLAY [all] ********************************************************************************************************************
    
    TASK [Config ge-0/0/1] ********************************************************************************************************
    [WARNING]: Platform linux on host 192.168.15.220 is using the discovered Python interpreter at /usr/bin/python, but future
    installation of another Python interpreter could change this. See
    https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more information.
    
    changed: [192.168.15.220]
    
    PLAY RECAP ********************************************************************************************************************
    192.168.15.220             : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

    Did you noticed the changed output?
    What about the configuration on the router now?

    admin> show configuration interfaces
    ge-0/0/1 {
        unit 0 {
            family inet {
                address 192.168.1.10/24;
            }
        }
    }
    fxp0 {
        unit 0 {
            family inet {
                address 192.168.15.220/24;
            }
            family inet6;
        }
    }
    

    That’s awesome! We just configured and IP address on ge-0/0/1.

    How does Ansible knows what to replace, what to override, and what to delete?

    If you take a closer look to the playbook, you will see a line with state: merged. This is a module parameter that specifies the state of the router configuration after the module finishes its job.

    The possible values are:

    • merged
    • replaced
    • overriden
    • deleted

    In fact, the module matches whatever configuration you build on its parameters, applies a configuration action, and commits the result.

    The official module documentation is on https://docs.ansible.com/ansible/latest/modules/junos_l3_interfaces_module.html.