Tag: juniper

  • 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 – 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.

  • Ansible and Juniper Junos – First Steps

    On the previous post, I proposed a quick and dirty method to provision an Ansible control node, using Vagrant and VirtualBox. But, if you really want to spin the volume up to 11, the best is to work in a dedicated Linux machine.

    On this lab, I will be using Linux Mint, which is a Ubuntu derivate, but most examples will work on any Linux distribution.

    First, install Ansible if already don’t have it. There are two easy methods.

    • Using pip to install the ansible Python module. You’ll need a working installation of Python2 (altough the latests releases of Ansible works with Python 3 too). Also, pip has to be installed and on path.

      $ pip install ansible
    • Using apt by adding the Ansible ppa repository and offload all the work to the system package manager.

      $ sudo apt-add-repository ppa:ansible/ansible
      $ sudo apt-get update
      $ sudo apt install ansible

    I preffer the last one and leave apt handle all the job, because the ppa repository is usually up-to-date with the latest release.

    Once installed, check your setup with ansible --version or ansible localhost -m setup.

    Ansible manages Junos using NETFCONF over SSH. In order to be able to connect to Junos via Ansible, both SSH and NETCONF services has to be enabled on the remote host.

    SSH can be used for sending raw commands using the junos_command module, but NETCONF is definetly more versatile and supports the whose set of Ansible modules, which you can see here.

    To use NETCONF, you will need an additional Python module in your Ansible control node.

    $ pip install ncclient

    And to enable SSH and NETCONF in your Juniper host , do the following. Probably you already have SSH enabled on your router so you can ignore that setting.

    admin> edit  
    Entering configuration mode
    [edit]
    admin# set system services ssh 
    admin# set system services netconf ssh 
    [edit]
    admin# commit 
    commit complete

    Building the Ansible Inventory

    Ansible inventory files can use many formats, depending on the plugins you have. The two main formats are INI files, and YAML.

    INI-style Inventory

    [junos]
    192.168.15.194
    
    [junos:vars]
    ansible_connection=netconf
    ansible_network_os=junos
    ansible_user=admin
    ansible_password=Password$1

    YAML Inventory

    all:
      hosts:
        "192.168.15.194":
      vars:
        ansible_connection: netconf
        ansible_network_os: junos
        ansible_user: admin
        ansible_password: Password$1

    Both files represent the same set of information on a different style. Personally, I preffer the YAML notation because it is more human-friendly and readeable, and it allows you to learn YAML which is used on many other automation and orchestration tools.

    What does this means

    all:
      hosts:
        "192.168.15.194":

    all: stands for all hosts and its always required. All hosts on an Ansible inventory belongs to at least two groups, all and ungrouped. All groups of course, all hosts, and ungrouped contains all hosts which don’t belong to another specific group besides all.

    hosts: specifies the start position for the hosts. Hosts groups can be then written like:

    mendoza:
       hosts:
         "192.168.15.194":
         core.thisnetwork.net:

    Hosts groups can share a set of variables, which provide information for the Ansible modules. For this example, as the vars section is a direct children of the allgroup, these variables will apply to all hosts in the inventory.

    vars:
         ansible_connection: netconf
         ansible_network_os: junos
         ansible_user: admin
         ansible_password: Password$1

    The Junos modules for Ansible use the netconf connection, which uses SSH and NETCONF, so ensure to allow TCP/22 and TCP/830 on your rules.

    Although a fixed inventory file with manually added hosts can be enough for some users, on future entries we’ll set up a dynamic inventory calling a source of truth like Netbox, or a monitoring system like Zabbix.

    Testing our setup

    The real fun with Ansible is on ansible-playbook, but first let’s fire up an Ansible module to warm up your Ansible-fu.

    $ ansible -m junos_facts -i juniper-hosts.yml all
    • -m junos_facts instructs Ansible to use the junos_facts module
    • juniper-hosts.yml is our previously configured inventory file
    • all tells Ansible the group of hosts inside the inventory to use

    If everythig works allright, this will output a long JSON with a bunch of information about your device.

    192.168.15.195 | SUCCESS => {
        "ansible_facts": {
            "ansible_net_api": "netconf", 
            "ansible_net_filesystems": [
                "/dev/md0.uzip", 
                "devfs", 
                "/dev/gpt/junos", 
                "devfs", 
                "/dev/md1.uzip", 
                "/dev/md2.uzip", 
                "tmpfs", 
                "procfs", 
                "/dev/ada1s1e", 
                "/dev/ada1s1f", 
                "/dev/md3.uzip", 
                "/dev/md4.uzip", 
                "/dev/md5.uzip", 
                "/dev/md6.uzip", 
                "/dev/md7.uzip", 
                "/dev/md8.uzip", 
                "/dev/md9.uzip", 
                "/dev/md10.uzip", 
                "/dev/md11.uzip", 
                "/packages/mnt/junos-libs-compat32/usr/lib32", 
                "/packages/mnt/os-libs-compat32-10/usr/lib32", 
                "/packages/mnt/os-compat32/libexec", 
                "/var/jails/rest-api", 
                "/dev/md12", 
                "/dev/md13.uzip", 
                "/dev/md14.uzip", 
                "/dev/md15.uzip", 
                "/dev/md16.uzip", 
                "/dev/md17.uzip", 
                "/dev/md18.uzip", 
                "/dev/md19.uzip", 
                "/dev/md20.uzip", 
                "/dev/md21.uzip", 
                "/dev/md22.uzip", 
                "/dev/md23.uzip", 
                "/dev/md24.uzip", 
                "/dev/md25.uzip", 
                "/dev/md26.uzip", 
                "/dev/md27.uzip", 
                "/dev/md28.uzip", 
                "tmpfs", 
                "junosprocfs"
            ], 
            "ansible_net_gather_network_resources": [], 
            "ansible_net_gather_subset": [
                "hardware", 
                "default", 
                "interfaces"
            ], 
            "ansible_net_has_2RE": false, 
            "ansible_net_hostname": "None", 
            "ansible_net_interfaces": {
                ".local.": {
                    "admin-status": "up", 
                    "macaddress": "Unspecified", 
                    "mtu": "Unlimited", 
                    "oper-status": "up", 
                    "speed": "Unlimited", 
                    "type": "Loopback"
                }, 
                "cbp0": {
                    "admin-status": "up", 
                    "macaddress": "00:05:86:cc:c1:11", 
                    "mtu": "9192", 
                    "oper-status": "up", 
                    "speed": "Unspecified", 
                    "type": "Ethernet"
                }, 
                "demux0": {
                    "admin-status": "up", 
                    "macaddress": "Unspecified", 
                    "mtu": "9192", 
                    "oper-status": "up", 
                    "speed": "Unspecified", 
                    "type": "Software-Pseudo"
                }, 
                "dsc": {
                    "admin-status": "up", 
                    "macaddress": "Unspecified", 
                    "mtu": "Unlimited", 
                    "oper-status": "up", 
                    "speed": "Unspecified", 
                    "type": "Software-Pseudo"
                }, 
                "em1": {
                    "admin-status": "up", 
                    "macaddress": "0c:b8:15:cf:9b:01", 
                    "mtu": "1514", 
                    "oper-status": "up", 
                    "speed": "1000mbps", 
                    "type": "Ethernet"
                }, 
                "esi": {
                    "admin-status": "up", 
                    "macaddress": "Unspecified", 
                    "mtu": "Unlimited", 
                    "oper-status": "up", 
                    "speed": "Unlimited", 
                    "type": "Software-Pseudo"
                }, 
                "fxp0": {
                    "admin-status": "up", 
                    "macaddress": "0c:b8:15:cf:9b:00", 
                    "mtu": "1514", 
                    "oper-status": "down", 
                    "speed": "Unspecified", 
                    "type": "Ethernet"
                }, 
                "ge-0/0/0": {
                    "admin-status": "up", 
                    "macaddress": "0c:b8:15:f6:47:02", 
                    "mtu": "1514", 
                    "oper-status": "down", 
                    "speed": "1000mbps", 
                    "type": null
                }, 
                "ge-0/0/1": {
                    "admin-status": "up", 
                    "macaddress": "0c:b8:15:f6:47:03", 
                    "mtu": "1514", 
                    "oper-status": "down", 
                    "speed": "1000mbps", 
                    "type": null
                }, 
                "ge-0/0/2": {
                    "admin-status": "up", 
                    "macaddress": "0c:b8:15:f6:47:04", 
                    "mtu": "1514", 
                    "oper-status": "down", 
                    "speed": "1000mbps", 
                    "type": null
                }, 
                "ge-0/0/3": {
                    "admin-status": "up", 
                    "macaddress": "0c:b8:15:f6:47:05", 
                    "mtu": "1514", 
                    "oper-status": "down", 
                    "speed": "1000mbps", 
                    "type": null
                }, 
                "ge-0/0/4": {
                    "admin-status": "up", 
                    "macaddress": "0c:b8:15:f6:47:06", 
                    "mtu": "1514", 
                    "oper-status": "down", 
                    "speed": "1000mbps", 
                    "type": null
                }, 
                "ge-0/0/5": {
                    "admin-status": "up", 
                    "macaddress": "0c:b8:15:f6:47:07", 
                    "mtu": "1514", 
                    "oper-status": "down", 
                    "speed": "1000mbps", 
                    "type": null
                }, 
                "ge-0/0/6": {
                    "admin-status": "up", 
                    "macaddress": "0c:b8:15:f6:47:08", 
                    "mtu": "1514", 
                    "oper-status": "down", 
                    "speed": "1000mbps", 
                    "type": null
                }, 
                "ge-0/0/7": {
                    "admin-status": "up", 
                    "macaddress": "0c:b8:15:f6:47:09", 
                    "mtu": "1514", 
                    "oper-status": "down", 
                    "speed": "1000mbps", 
                    "type": null
                }, 
                "ge-0/0/8": {
                    "admin-status": "up", 
                    "macaddress": "0c:b8:15:f6:47:0a", 
                    "mtu": "1514", 
                    "oper-status": "down", 
                    "speed": "1000mbps", 
                    "type": null
                }, 
                "ge-0/0/9": {
                    "admin-status": "up", 
                    "macaddress": "0c:b8:15:f6:47:0b", 
                    "mtu": "1514", 
                    "oper-status": "up", 
                    "speed": "1000mbps", 
                    "type": null
                }, 
                "gre": {
                    "admin-status": "up", 
                    "macaddress": null, 
                    "mtu": "Unlimited", 
                    "oper-status": "up", 
                    "speed": "Unlimited", 
                    "type": "GRE"
                }, 
                "ipip": {
                    "admin-status": "up", 
                    "macaddress": null, 
                    "mtu": "Unlimited", 
                    "oper-status": "up", 
                    "speed": "Unlimited", 
                    "type": "IPIP"
                }, 
                "irb": {
                    "admin-status": "up", 
                    "macaddress": "00:05:86:cc:c8:f0", 
                    "mtu": "1514", 
                    "oper-status": "up", 
                    "speed": "Unspecified", 
                    "type": "Ethernet"
                }, 
                "jsrv": {
                    "admin-status": "up", 
                    "macaddress": "00:05:86:cc:c8:c0", 
                    "mtu": "1514", 
                    "oper-status": "up", 
                    "speed": "Unspecified", 
                    "type": "Ethernet"
                }, 
                "lc-0/0/0": {
                    "admin-status": "up", 
                    "macaddress": "Unspecified", 
                    "mtu": "0", 
                    "oper-status": "up", 
                    "speed": "800mbps", 
                    "type": "Unspecified"
                }, 
                "lo0": {
                    "admin-status": "up", 
                    "macaddress": "Unspecified", 
                    "mtu": "Unlimited", 
                    "oper-status": "up", 
                    "speed": "Unspecified", 
                    "type": "Loopback"
                }, 
                "lsi": {
                    "admin-status": "up", 
                    "macaddress": "Unspecified", 
                    "mtu": "Unlimited", 
                    "oper-status": "up", 
                    "speed": "Unlimited", 
                    "type": "Software-Pseudo"
                }, 
                "mtun": {
                    "admin-status": "up", 
                    "macaddress": null, 
                    "mtu": "Unlimited", 
                    "oper-status": "up", 
                    "speed": "Unlimited", 
                    "type": "Multicast-GRE"
                }, 
                "pfe-0/0/0": {
                    "admin-status": "up", 
                    "macaddress": "Unspecified", 
                    "mtu": "0", 
                    "oper-status": "up", 
                    "speed": "800mbps", 
                    "type": "Unspecified"
                }, 
                "pfh-0/0/0": {
                    "admin-status": "up", 
                    "macaddress": "Unspecified", 
                    "mtu": "0", 
                    "oper-status": "up", 
                    "speed": "800mbps", 
                    "type": "Unspecified"
                }, 
                "pimd": {
                    "admin-status": "up", 
                    "macaddress": null, 
                    "mtu": "Unlimited", 
                    "oper-status": "up", 
                    "speed": "Unlimited", 
                    "type": "PIMD"
                }, 
                "pime": {
                    "admin-status": "up", 
                    "macaddress": null, 
                    "mtu": "Unlimited", 
                    "oper-status": "up", 
                    "speed": "Unlimited", 
                    "type": "PIME"
                }, 
                "pip0": {
                    "admin-status": "up", 
                    "macaddress": "00:05:86:cc:c8:b0", 
                    "mtu": "9192", 
                    "oper-status": "up", 
                    "speed": "Unspecified", 
                    "type": "Ethernet"
                }, 
                "pp0": {
                    "admin-status": "up", 
                    "macaddress": "Unspecified", 
                    "mtu": "1532", 
                    "oper-status": "up", 
                    "speed": "Unspecified", 
                    "type": "PPPoE"
                }, 
                "rbeb": {
                    "admin-status": "up", 
                    "macaddress": "Unspecified", 
                    "mtu": "Unlimited", 
                    "oper-status": "up", 
                    "speed": "Unlimited", 
                    "type": "Software-Pseudo"
                }, 
                "tap": {
                    "admin-status": "up", 
                    "macaddress": "Unspecified", 
                    "mtu": "Unlimited", 
                    "oper-status": "up", 
                    "speed": "Unlimited", 
                    "type": "Software-Pseudo"
                }, 
                "vtep": {
                    "admin-status": "up", 
                    "macaddress": "Unspecified", 
                    "mtu": "Unlimited", 
                    "oper-status": "up", 
                    "speed": "Unlimited", 
                    "type": "Software-Pseudo"
                }
            }, 
            "ansible_net_memfree_mb": 179384, 
            "ansible_net_memtotal_mb": 2058336, 
            "ansible_net_model": "vmx", 
            "ansible_net_modules": [
                {
                    "name": "Midplane"
                }, 
                {
                    "description": "RE-VMX", 
                    "name": "Routing Engine 0"
                }, 
                {
                    "description": "VMX SCB", 
                    "name": "CB 0"
                }, 
                {
                    "description": "VMX SCB", 
                    "name": "CB 1"
                }, 
                {
                    "chassis_sub_module": null, 
                    "description": "Virtual FPC", 
                    "name": "FPC 0"
                }
            ], 
            "ansible_net_python_version": "2.7.15+", 
            "ansible_net_routing_engines": {
                "0": {
                    "cpu_background": "0", 
                    "cpu_background1": "0", 
                    "cpu_background2": "0", 
                    "cpu_background3": "0", 
                    "cpu_idle": "61", 
                    "cpu_idle1": "94", 
                    "cpu_idle2": "95", 
                    "cpu_idle3": "95", 
                    "cpu_interrupt": "2", 
                    "cpu_interrupt1": "1", 
                    "cpu_interrupt2": "1", 
                    "cpu_interrupt3": "1", 
                    "cpu_system": "27", 
                    "cpu_system1": "4", 
                    "cpu_system2": "3", 
                    "cpu_system3": "3", 
                    "cpu_user": "10", 
                    "cpu_user1": "1", 
                    "cpu_user2": "1", 
                    "cpu_user3": "1", 
                    "last_reboot_reason": "Router rebooted after a normal shutdown.", 
                    "load_average_fifteen": "0.62", 
                    "load_average_five": "0.73", 
                    "load_average_one": "0.75", 
                    "mastership_priority": "master (default)", 
                    "mastership_state": "master", 
                    "memory_buffer_utilization": "13", 
                    "memory_dram_size": "2010 MB", 
                    "memory_installed_size": "(2048 MB installed)", 
                    "model": "RE-VMX", 
                    "slot": "0", 
                    "start_time": "2019-11-26 12:06:10 UTC", 
                    "status": "OK", 
                    "up_time": "11 hours, 46 minutes, 19 seconds"
                }
            }, 
            "ansible_net_serialnum": "VM5DDBEA932E", 
            "ansible_net_system": "junos", 
            "ansible_net_version": "17.1R1.8", 
            "ansible_network_resources": {}, 
            "discovered_interpreter_python": "/usr/bin/python"
        }, 
        "changed": false
    }
    

    If you take a closer look, you’ll find out that I am using a vMX appliance, can you spot where is that information?

    What if we could use this and more information to gather more useful data about our router, and execute actions of provision new configuration depending on the gathered data?

    If Ansible modules are the tools in your workshop, playbooks are your instruction manuals, and your inventory of hosts are your raw material.

    The First Playbook

    Playbooks are expressed in YAML and are composed of one or more ‘plays’ in a list. The goal of a play is to map a group of hosts to some well defined roles, represented by things ansible calls tasks. At a basic level, a task is nothing more than a call to an ansible module.

    Go ahead and create a new file, juniper.yml or whatever you like, and put the following inside:

    ---
    - hosts: all
      gather_facts: no
    
      tasks:
    
      - name: Get Configuration
        junos_command:
          commands:
            - show configuration

    Going from top to bottom this playbook tells Ansible to:

    • hosts: all, use the all hosts from the inventory
    • gather_facts: no, don’t gather any facts for now. More on this later.
    • tasks:, this is the list of all the tasks I want you to do.
    • - name:, this is the name of the task. It starts with a - because it’s a list, even if it has only a single entry
    • junos_command:use this module for this task, like -m junos_command. This module, like most, supports a set of parameters which you can see here, and they are below!
    • commands: this is the list of commands to execute
    • - show configuration: this is one of the commands

    Now, run ansible-playbook juniper.yml -i juniper-hosts.yml. This will run your playbook, using all the hosts on the previously defined juniper-hosts.yml inventory.

    $ ansible-playbook juniper.yml -i juniper-hosts.yml
    
    PLAY [all] ***********************************************************************************************************************************************************************
    
    TASK [Get Configuration] *********************************************************************************************************************************************************
    [WARNING]: Platform linux on host 192.168.15.195 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.15.195]
    
    PLAY RECAP ***********************************************************************************************************************************************************************
    192.168.15.195             : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
    

    Well, that was fun but.. there is nothing useful here, besides a warning! In fact, Ansible connected sucessfully to the router, and retrieved the configuration. We didn’t instructed it to show the configuration to us.

    Modify your playbook so it looks like this:

    ---
    - hosts: all
      gather_facts: no
    
      tasks:
    
      - name: Get Configuration
        junos_command:
          commands:
            - show configuration
        register: config
    
      - name: Show Config
        debug: var=config

    We added an additional line to the first task, register: config, which saves the result of the task in a variable called config. This name can be whatever you like.

    There is also an additional task named Show Config, with a debug: var=config instruction, which outputs the config variable.

    Run the playbook once again like you did before, and check the result. Of course it will depend on your configuration. On my vMX, which is almost blank, and just using DHCP for a couple interfaces, the result was this.

    $ ansible-playbook juniper.yml -i juniper-hosts.yml
    
    PLAY [all] *****************************************************************************
    
    TASK [Get Configuration] ***************************************************************
    [WARNING]: Platform linux on host 192.168.15.195 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.15.195]
    
    TASK [Show Config] *********************************************************************
    ok: [192.168.15.195] => {
        "config": {
            "ansible_facts": {
                "discovered_interpreter_python": "/usr/bin/python"
            }, 
            "changed": false, 
            "failed": false, 
            "stdout": [
                "## Last changed: 2019-11-26 19:02:46 UTC\nversion 17.1R1.8;\nsystem {\n    root-authentication {\n        encrypted-password \"$6$5LBS/EfQ$tL9utW2Aj4T56SfJUxdnVaF/.RIbaZ65keFn1SbCgOTi6r.LDiGt3FvvoP2WuxuuosVtx0RobNk67obTMeNgF.\";\n    }\n    scripts {\n        inactive: language python;\n    }\n    login {\n        user admin {\n            uid 2000;\n            class super-user;\n            authentication {\n                encrypted-password \"$6$9YynK9hD$Is6rEd7WNnEYGF7q2MqQJoRU/9vGjkQv7Qig.V2WT1905ShVlow4LXKeATM5HR8F1vTwROz2gUpF7z7eCJruo1\";\n            }\n        }\n    }\n    services {\n        ssh;\n        netconf {\n            ssh;\n        }\n    }\n    syslog {\n        user * {\n            any emergency;\n        }\n        file messages {\n            any notice;\n            authorization info;\n        }\n        file interactive-commands {\n            interactive-commands any;\n        }\n    }\n}\ninterfaces {\n    ge-0/0/1 {\n        unit 0 {\n            family inet {\n                dhcp;\n            }\n        }\n    }\n    ge-0/0/9 {\n        unit 0 {\n            family inet {\n                dhcp;\n            }\n        }\n    }\n}"
            ], 
            "stdout_lines": [
                [
                    "## Last changed: 2019-11-26 19:02:46 UTC", 
                    "version 17.1R1.8;", 
                    "system {", 
                    "    root-authentication {", 
                    "        encrypted-password \"$6$5LBS/EfQ$tL9utW2Aj4T56SfJUxdnVaF/.RIbaZ65keFn1SbCgOTi6r.LDiGt3FvvoP2WuxuuosVtx0RobNk67obTMeNgF.\";", 
                    "    }", 
                    "    scripts {", 
                    "        inactive: language python;", 
                    "    }", 
                    "    login {", 
                    "        user admin {", 
                    "            uid 2000;", 
                    "            class super-user;", 
                    "            authentication {", 
                    "                encrypted-password \"$6$9YynK9hD$Is6rEd7WNnEYGF7q2MqQJoRU/9vGjkQv7Qig.V2WT1905ShVlow4LXKeATM5HR8F1vTwROz2gUpF7z7eCJruo1\";", 
                    "            }", 
                    "        }", 
                    "    }", 
                    "    services {", 
                    "        ssh;", 
                    "        netconf {", 
                    "            ssh;", 
                    "        }", 
                    "    }", 
                    "    syslog {", 
                    "        user * {", 
                    "            any emergency;", 
                    "        }", 
                    "        file messages {", 
                    "            any notice;", 
                    "            authorization info;", 
                    "        }", 
                    "        file interactive-commands {", 
                    "            interactive-commands any;", 
                    "        }", 
                    "    }", 
                    "}", 
                    "interfaces {", 
                    "    ge-0/0/1 {", 
                    "        unit 0 {", 
                    "            family inet {", 
                    "                dhcp;", 
                    "            }", 
                    "        }", 
                    "    }", 
                    "    ge-0/0/9 {", 
                    "        unit 0 {", 
                    "            family inet {", 
                    "                dhcp;", 
                    "            }", 
                    "        }", 
                    "    }", 
                    "}"
                ]
            ], 
            "warnings": [
                "Platform linux on host 192.168.15.195 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."
            ]
        }
    }
    
    PLAY RECAP *****************************************************************************
    192.168.15.195             : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  

    I know we all love show config | display set. Try adding display: set as an additional parameter of junos_command. It should look like this.

        junos_command:
          commands:
            - show configuration
          display: set
    

    Run the playbook again, and your output should be as expected if you ran it on the Junos CLI.

    Stay tuned for more posts to learn how to configure Juniper Junos using Ansible.