Tag: automation

  • 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

  • UFiber v4 Python Client

    Yeah, I finally remembered to make a post about this. I know it will like as a copy-paste of the previous one, because, in fact it is.

    Ok, if you have been following the series, you should already know that I equally love and hate UFiber OLTs. They are affordable, deliver a lot of bang for the buck, and have an awful GUI.

    Well, the GUI is lovely on v4.

    Python in the middle

    I wrote a quick and dirty client which acts as a sort of middleware between the HTTP inteface of the OLT and you.

    It allows to provision non existing ONUs, GPON profiles, WiFi profiles, retrieve active ONU status and general configuration.

    Take a look to it on https://github.com/baldoarturo/ufiber-client-4, and feel free to contribute if you want to.

    How to help

    It would be awesome to have docs 😀

    Are you a pydoc master? Let’a add docstrings.

    Do you have an OLT for me to test? Ping me and we can set up a VPN.

    olt.py

    This is the core of the project. It uses the OLTClient class to provide a middleware between you and the HTTP interface of the OLT.

    Initialize a new OLTClient instance with:

    client = OLTClient(host='192.168.1.1', username='ubnt', password='ubnt', debug_level=logging.DEBUG)

    Required params are only host, and credentials.

    The initialization will handle the login for you, altough you can call the login() method manually.

    If the OLT is network reachable, and you have provided the right credentials, and the OLT GUI is alive and well, you should be ready to start.

    What changes on v4

    Well, UBNT got rid of the GPON profiles. 🙁

    This software is intented to give you an alternative by keeping profiles as JSON in the ./profiles folder.

    You can copy the template.json file and make your way using it as a starting point. It should be self descriptive.

    There is an schema.json which validates your profile before pushing changes into the OLT.

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

  • Quick Ansible control node with Vagrant

    Ansible is an IT automation tools which can configure systems, deploy files and software and orchestrate almost every possible task you can imagine.

    Unlike other configuration management tools like Chef or Puppet, Ansible is agent-less, and does all its magic over SSH. Forget about keeping up client daemons updated and additional certificates. Just use your existing SSH keys, or usernames and passwords.

    Due to it agent-less nature, we just need a host to initiate SSH sessions towards the managed hosts. This node is called a control node in the Ansible jargon.

    On this post, we’ll catch up with the basics of Vagrant and Ansible. The repository for this post is on https://github.com/baldoarturo/ansible-control-node.

    Download VirtualBox

    VirtualBox is a general-purpose full virtualizer for x86 hardware, targeted at server, desktop and embedded use. Get it from here.

    Get Vagrant

    We’ll use Vagrant to quickly spin up a control node, based on a ubuntu/xenial64 box. If you are not familiar with Vagrant, you just need to know it is a tool capable to provision VMs on different hypervisors, using a Vagrantfile a VM template.

    First, get Vagrant for your system here, https://www.vagrantup.com/downloads.html, and install it.

    Use Git

    In order to make everything easier, we’ll init a new git repository to keep track of all the configuration changes. Also, we can push this repository to a remote and clone from wherever we need it, making a portable Ansible control node.

    C:\Users\Arturo\Desktop\ansible-control-node>git init
    Initialized empty Git repository in C:/Users/Arturo/Desktop/ansible-control-node/.git/
    
    C:\Users\Arturo\Desktop\ansible-control-node>git config user.name "Arturo Baldo"
    
    C:\Users\Arturo\Desktop\ansible-control-node>git config user.email "[email protected]"
    
    C:\Users\Arturo\Desktop\ansible-control-node>

    Although everything can be done from a shell, I prefer to use the integrated terminal on VSCode, and make use of the universe of plugins. There is support for Ansible and Ansible Playbooks, Vagrant, a lot more!

    Also, even if this post uses a Windows system, everything is reproducible on Linux platforms because all the tools are multiplatform.

    Init the Vagrant environment

    The Vagrant CLI is self explanatory and easy to understand.

    C:\Users\Arturo\Desktop\ansible-control-node>vagrant
    Usage: vagrant [options] <command> [<args>]
    
        -v, --version                    Print the version and exit.
        -h, --help                       Print this help.
    
    Common commands:
         box             manages boxes: installation, removal, etc.
         cloud           manages everything related to Vagrant Cloud
         destroy         stops and deletes all traces of the vagrant machine
         global-status   outputs status Vagrant environments for this user
         halt            stops the vagrant machine
         help            shows the help for a subcommand
         init            initializes a new Vagrant environment by creating a Vagrantfile
         login
         package         packages a running vagrant environment into a box
         plugin          manages plugins: install, uninstall, update, etc.
         port            displays information about guest port mappings
         powershell      connects to machine via powershell remoting
         provision       provisions the vagrant machine
         push            deploys code in this environment to a configured destination
         rdp             connects to machine via RDP
         reload          restarts vagrant machine, loads new Vagrantfile configuration
         resume          resume a suspended vagrant machine
         snapshot        manages snapshots: saving, restoring, etc.
         ssh             connects to machine via SSH
         ssh-config      outputs OpenSSH valid configuration to connect to the machine
         status          outputs status of the vagrant machine
         suspend         suspends the machine
         up              starts and provisions the vagrant environment
         upload          upload to machine via communicator
         validate        validates the Vagrantfile
         version         prints current and latest Vagrant version
         winrm           executes commands on a machine via WinRM
         winrm-config    outputs WinRM configuration to connect to the machine

    First, run vagrant intit to initialize a new environment.

    C:\Users\Arturo\Desktop\ansible-control-node>vagrant init
    A `Vagrantfile` has been placed in this directory. You are now
    ready to `vagrant up` your first virtual environment! Please read
    the comments in the Vagrantfile as well as documentation on
    `vagrantup.com` for more information on using Vagrant.

    Wait! You have a new file on your git repository. Make it the first commit.

    C:\Users\Arturo\Desktop\ansible-control-node>git add .
    
    C:\Users\Arturo\Desktop\ansible-control-node>git commit -m "First commit"
    [master (root-commit) 1416f75] First commit
     1 file changed, 70 insertions(+)
     create mode 100644 Vagrantfile
    
    C:\Users\Arturo\Desktop\ansible-control-node>

    On the newly create Vagrantfile, we can set configuration and provisioning options for the VM.

    First, replace config.vm.box = "base" with config.vm.box = "ubuntu/xenial64" to use this box.

    If you want to see the VM, config vb.gui = true. By default this setting is commented out, so the VM will start in headless mode.

    Feel free to customize the VM memory by commenting out the vb.memory = "1024" setting and adjusting it to your needs.

    Once you are done, save your changes, commit, execute vagrant up, and watch Vagrant create a brand new VM for you.

    C:\Users\Arturo\Desktop\ansible-control-node>vagrant up
    Bringing machine 'default' up with 'virtualbox' provider...
    ==> default: Importing base box 'ubuntu/xenial64'...
    ==> default: Matching MAC address for NAT networking...
    ==> default: Checking if box 'ubuntu/xenial64' version '20191114.0.0' is up to date...
    ==> default: Setting the name of the VM: ansible-control-node_default_1574353915423_60685
    ==> default: Clearing any previously set network interfaces...
    ==> default: Preparing network interfaces based on configuration...
        default: Adapter 1: nat
    ==> default: Forwarding ports...
        default: 22 (guest) => 2222 (host) (adapter 1)
    ==> default: Running 'pre-boot' VM customizations...
    ==> default: Booting VM...
    ==> default: Waiting for machine to boot. This may take a few minutes...
        default: SSH address: 127.0.0.1:2222
        default: SSH username: vagrant
        default: SSH auth method: private key
        default: Warning: Connection reset. Retrying...
        default: Warning: Connection aborted. Retrying...
        default: Warning: Remote connection disconnect. Retrying...
        default: 
        default: Vagrant insecure key detected. Vagrant will automatically replace
        default: this with a newly generated keypair for better security.
        default: 
        default: Inserting generated public key within guest...
        default: Removing insecure key from the guest if it's present...
        default: Key inserted! Disconnecting and reconnecting using new SSH key...
    ==> default: Machine booted and ready!
    ==> default: Checking for guest additions in VM...
        default: The guest additions on this VM do not match the installed version of
        default: VirtualBox! In most cases this is fine, but in rare cases it can
        default: prevent things such as shared folders from working properly. If you see
        default: shared folder errors, please make sure the guest additions within the
        default: virtual machine match the version of VirtualBox you have installed on
        default: your host and reload your VM.
        default:
        default: Guest Additions Version: 5.1.38
        default: VirtualBox Version: 6.0
    ==> default: Mounting shared folders...
        default: /vagrant => C:/Users/Arturo/Desktop/ansible-control-node

    Well, how do we login into the new VM? Try vagrant ssh

    C:\Users\Arturo\Desktop\ansible-control-node>vagrant ssh
    Welcome to Ubuntu 16.04.6 LTS (GNU/Linux 4.4.0-169-generic x86_64)
    
     * Documentation:  https://help.ubuntu.com
     * Management:     https://landscape.canonical.com
     * Support:        https://ubuntu.com/advantage
    
    
    0 packages can be updated.
    0 updates are security updates.
    
    New release '18.04.3 LTS' available.
    Run 'do-release-upgrade' to upgrade to it.
    
    
    vagrant@ubuntu-xenial:~$ whoami
    vagrant
    vagrant@ubuntu-xenial:~$ 

    Type exit and you will return to your system prompt.

    You’ll see that you have new items on your directory. A .log file, with the output from Vagrant, and a .vagrant/ directory.

    To exclude them from the repository, create a .gitignore, with the following contents.

    .vagrant/
    *.log

    Save and commit.

    Provisioning the VM

    Return to the Vagrantfile, and add a section like this:

      config.vm.provision "shell", inline: <<-SHELL
        apt-get update
        apt-get install software-properties-common -y
        apt-add-repository --yes --update ppa:ansible/ansible
        apt-get install ansible -y
      SHELL

    This block instructs Vagrant to execute commands on the shell. First, the package list is updated, then the software-properties-common package is installed. To install Ansible, the ppa:ansible/ansible repository is added, and then Ansible is installed.

    Notice the usage of -y to avoid interactive prompts.

    Save the file, commit, and run vagrant provision. This will re-provision the VM with the new instructions from the Vagrantfile.

    Once Vagrant finishes, connect once again with vagrant ssh, and run ansible --version.

    vagrant@ubuntu-xenial:~$ ansible --version
    ansible 2.9.1
      config file = /etc/ansible/ansible.cfg
      configured module search path = [u'/home/vagrant/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
      ansible python module location = /usr/lib/python2.7/dist-packages/ansible
      executable location = /usr/bin/ansible
      python version = 2.7.12 (default, Oct  8 2019, 14:14:10) [GCC 5.4.0 20160609]

    That’s great! We have just installed Ansible on our new VM.

    Seek and Destroy

    You might wonder, where is this VM? Head to the VirtualBox Manager, and you’ll find it inside.

    Here it is!

    Without asking anyone.. kill the machine and delete it.

    Hard power off
    Delete all files

    Head back to the console, and you will see that the SSH session has died. Of course, this makes perfect sense as the VM no longer exists.

    vagrant@ubuntu-xenial:~$ Connection to 127.0.0.1 closed by remote host.
     Connection to 127.0.0.1 closed.

    If you try to run vagrant provision, it will fail, as there is no VM to provision.

    C:\Users\Arturo\Desktop\ansible-control-node>vagrant provision
    ==> default: VM not created. Moving on...

    But what happens if you run vagrant up once again? Surprise! The VM will be recreated with all the previous configuration settings.

    Using Ansible

    Well, the VM is ready, Ansible is installed.. now what? Let’s write an inventory file and see what can we do.

    I’ll create a simple file to connect to a testing docker instance on 192.168.85.253. No need to worry about ssh keys now, as we are testing, username and password will do the trick.

    vagrant@ubuntu-xenial:~$ cat docker
    [all:vars]
    ansible_connection=ssh
    ansible_user=MY_USERNAME
    ansible_password=MY_PASSWORD
    
    [docker]
    192.168.85.253

    The [all:vars] section configures the connection settings for all hosts in the file. The [docker] section contains the lists of hosts, in this case just 192.168.85.253.

    Let’s run: ansible -m ping -i docker all, where -m is the module to run, -i is the inventory file, and all is the section of the inventory file which contains the hosts. Notice this is a very special ping.

    vagrant@ubuntu-xenial:~$ ansible -m ping -i docker all
    [DEPRECATION WARNING]: Distribution Ubuntu 16.04 on host 192.168.15.253 should use /usr/bin/python3, but is using /usr/bin/python for backward compatibility with prior Ansible releases. A future Ansible release
     will default to using the discovered platform python for this host. See https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more information. This feature will be removed   
    in version 2.12. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.
    192.168.15.253 | SUCCESS => {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        }, 
        "changed": false, 
        "ping": "pong"
    }

    Whoa! That definetly doesn’t look like a ICMP ping. First, this is an old box about to be decomissioned, so it has been very unmantained. Ansible warns us about an old version of Python, and a old version of Ansible which was installed previously on the remote hosts.

    Now, try ansible -m setup -i docker all. Be aware this will throw a ton of data to you. The setup module gathers data about the host.

    What it does and how does it, is documented here.