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

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,, 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 "Arturo Baldo"

C:\Users\Arturo\Desktop\ansible-control-node>git config ""


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.

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
     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
`` 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


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

First, replace = "base" with = "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:
    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: Vagrant insecure key detected. Vagrant will automatically replace
    default: this with a newly generated keypair for better security.
    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: 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:
 * Management:
 * Support:

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

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.


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

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 closed by remote host.
 Connection to 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 No need to worry about ssh keys now, as we are testing, username and password will do the trick.

vagrant@ubuntu-xenial:~$ cat docker


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

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


Introduction to NetOps

I am glad to have been invited to participate in the Introduction to NetOps course dictated by the ISOC. This course is a major step getting people involved in network automation and Unix/Linux/BSD technologies.

The Internet Society (ISOC) was founded in 1992 by a number of people involved with the Internet Engineering Task Force (IETF) and provides an organizational home for and financial support for the Internet standards process.

The Internet Society supports the work of the Internet Engineering Task Force (IETF) to create open standards for the Internet.

Books Projects

101 Labs – Linux LPIC1 is out

I am the Technical Editor for the new Paul Browning book, 101 Labs – Linux LPIC1.

Experts agree that we retain only 10% of what we read but 90% of what we do. Perhaps this explains why the global pass rate for most IT exams is a ghastly 40%.

101 Labs’ mission is to turn you into an IT expert by doing instead of reading. Using free software and free trials, the labs take you by the hand and walk you through every aspect of the protocols and technologies you will encounter in your IT career.

Configuration tips and tricks are shared with you as well as how to avoid the common mistakes many novice engineers make, which can quickly become career-ending.

You can get a free preview or buy the ebook below.

101 Labs – Linux LPIC1 takes you through the beginner exam syllabus, the LPI Linux Essentials in case you are a novice and want to learn the basics of Linux or simply just brush up your skills. You then tackle all the main LPIC1 topics broken into the two exams 101 and 102.

MikroTik Networking Projects

Upgrading a MikroTik CHR Cluster

I upgraded a CHR cluster with the main objectives of reduce costs, improve network redundancy and provide an easy administration for CHR instances. As explained in previous posts, CHR can be run on many popular hypervisors, and most users are having great success using Hyper-V Failover clusters or vSphere HA to provide highly available routers without depending on VRRP or other gateway redundancy protocols.

These virtual routers currently provide two main services besides routing for ISP customers. They act as PPPoE concentrator for FTTH users, and provide traffic shaping and policing depending on the customer service plan.

Server Hardware

For this node, I will use a 32 core Dell R730, with 32 GB of RAM, and a 500 GB RAID 10 storage. On future post, new hosts will be added to the cluster.

Unracking the server

Network Conectivity

This server comes with a 4 port Gigabit Ethernet NIC, which could be used without any issues with the ixgbe driver.

First idea was to use two ports in a LACP bundle, and the other two in separate port groups.

I had previous Netflow analysis where I saw a predictable traffic behavior, where most of the bandwidth usage was going from and to a CDN peer of the ISP network. Customers had a mix of public and private addresses of the Class B segment, and they were being moved to CG-NAT ranges. In other words, traffic from a specific set of addresses were going from and to a specific set of addresses.

Why not configure two port-channels, instead of using separate port groups? I tested and due to the nature of the IP addressing on the customer side of the routers, none of the available hashing modes for LACP allowed to achieve a decent distribution on both links of the port-channel.

So, for the purposes of this cluster, I added an Intel X520 dual SFP+ card, providing 20 Gbps conectivity to the CHR instances. Peak bandwidth usage was around 4200 Mbps, so this card is more than enough to allow for future grow.

Installing the Intel X520 NIC

The Intel X520 only supports Intel branded SFP modules, and this behavior can be tuned configuring the kernel module. However, for this particular scenario, where both ports will be connected to a top of rack Dell Force10 S4048-ON switch, I choosed to use DAC cables to keep things simple.

DAC cables on the switch
Connecting the server

The server is using ESXi 6.5 for the hypervisor. After booting, I noticed the NICs were being recognized as vmnic5 and vmnic6, but they were using the ixgbe driver and only establishing links at 1 Gbps.

I downloaded the ixgben driver which is provided by VMware itself here and uploaded it to ESXi via SFTP.

For all my SFTP needs, my tool of choice always is Bitwise SSH client.

Once uploaded, I installed the offline bundle with the following command line.

[root@esxi] esxcli software vib install -d "/complete/path/to/the/driver/bundle"

Then I followed the KB article to disable the native ixgbe driver and use the new one. First, I placed the host on maintenance mode, and then I executedthe following to disable the driver.

[root@esxi] esxcli system module set --enabled=false --module=ixgbe

After a reboot, the new ixgben driver was loaded, and the NICs were establishing links at 10 Gbps.

I added the new NICs to the previously created virtual switches, checked the correct assignments of port groups, and then migrated the VMs to this host.

MikroTik Networking Projects

Building a network on Entre Ríos

It is always nice to fly. I took two flights, the first one with a little stop at Aeroparque (AEP), and then a short one to Paraná city (PRA). The skies were just beautiful.

Travelling MDZ to AEP

My current company is establishing operations on Entre Rios province, where a we are initiating a brand-new ISP service for the towns of Crespo, Libertador San Martin, and Paraná City. This was the main task among another small consulting and assistance.

My first time seeing the mighty Paraná river

Connecting People

Service is provided with two providers, and BGP sessions must be established with both to announce a /24 prefix of our AS, and probably receive just a default route from the upstream. There is no need to use the full table- yet. Both providers has approximately the same AS-PATH.

We’ll use a MikroTik CCR1036-8G-2S+ as the border router. Although it has SFP+ ports to allow 10 Gbps operation, at the moment the links will be negotiated using SFP modules at 1000 Mbps.

Main customer will be directly connected to this router using copper at 1 Gbps. They are using a MikroTik CRS326-24G-2S+ for their edge router, which will be enough for their 100 Mbps service. They provide us co-location too, so I installed the core router on their shelter, which is backed up by dual A/C systems and dual UPS-rectifier systems.

The new router racked and powered up
We’ll have some mate while waiting for the upstream provider port to go into no shutdown

Once the upstream was go, I was able to see they were in fact sending us the full BGP table, which we don’t need yet, so a couple route filters were configured to use put only a default gateway on the main routing table. As the default route was configured as a static one, the route filter policy was as easy as discard all BGP inputs.

[rootmin@ROUTER-EDUC.AR-PARANA] > routing filter export 
# jun/18/2019 16:24:37 by RouterOS 6.42.6
/routing filter
add action=discard chain=dynamic-in protocol=bgp

On this site there was also an Ubiquiti AirFiber 11X wireless link to reach Libertador San Martín town. Both radios were previously installed but not configured, so I connected to the radio and the site and configured it as Master. We traveled to the remote end, configured the radio as Slave and it worked just fine. Ubiquiti is getting up to date with their firmwares and UI, and it has became pretty straight forward to get a link working, even for someone with little or none networking skills.

¿Do you think this ease-of-use is making the job easier for us, or is it the start point of a madness of wireless spectrum usage?

From this node at Libertador, we installed two single-mode fiber lines, one to connect the town Hospital and another for the town University. MikroTik CRS326-24G-2S+ switches were installed at each site to be used as CPEs.

All monitoring, reporting and backup systems were previously configured at our NOC, so that was ll for us on the site.

Watching cars go by

I also assisted a brand new urban surveillance camera installation on the entrance of the Raúl Uranga – Carlos Sylvestre Begnis Subfluvial Tunnel. The objective was to read license plates on this strategic points, which is one of the few exits outside the Paraná river, and the one which has the most vehicle traffic.

Previously we had selected a Hikvision DS-2CD4A26FWD-IZHS8/P (yep, that’s the model name) camera which was already installed by Policía of Entre Ríos technician. This camera was specifically designed for licence-plate recognition (LPR). It supports OCR on hardware and works in very low light conditions, as low as 0.0027 lux.

Traces of Paraná City

I stayed at Hotel Howard Johnson Plaza Resort & Casino Mayorazgo, and I encourage you to visit it. The rooms are lovely and the staff is excellent.

My view from the hotel room

Be sure to schedule time to walk on the Paraná river borders, visit the Martiniano Leguizamón historic town museum and enjoy yourself. This is a beautiful city.

Blue skies at Crespo, Entre Ríos
MikroTik Networking Projects

Using The Dude on MikroTik CHR

The Dude network monitor is RouterOS package intended to manage a network environment. It automatically scan all devices within specified subnets, draw and layout a network maps, monitor services, and alert you in case of problems.

Previous versions of The Dude were developed as Windows x86 software, but later versions went through a full rebuild, and now it is distributed as a RouterOS package. This comes handy as the same RouterOS instance can be linked to the network, eliminating the the need for additional VPNs on servers or gateways. Instead, all tunneling can be done inside the CHR instance.

The Windows versions also had a web GUI which was, awful. For all the new editions, you’ll need a software client available on

It will update itself whenever you connect to a newer RouterOS version. Just be sure to run it as administrator on W10.


Get the CHR package from

Once downloaded, upload it to the CHR instance via Winbox drag-and-drop, FTP client, or just download it from inside chr:

Downloading from CHR

Reboot the CHR instance, and you will find the new Dude menu inside Winbox.

New Dude menu

Head to Dude > Settings and tick Enabled to enable the server. A few folders will be created on the filesystem, and the server will be ready to accept connections on port 8291. The previous x86 based versions of Dude used port TCP/2210 or TCP/2211, but on this new integrated RouterOS package, all the management is handled on the same port as Winbox.

If you still don’t have the client, get it on

One you connect, the following window should appear by default. You can run a discover for multiple networks and let Dude map your network for you, but it will only disconver layer 3 adyancencies. In order to have complete control over the monitoring, I suggest to build your backbone manually and let the autodiscovery handle your management VLANs/VRFs.

MikroTik Networking

Introduction to MikroTik CHR

MikroTik Cloud Hosted Router (CHR) is a RouterOS version intended to be used as a virtual machine instance.

It runs on x86-64-bit architecture and can be deployed on most hypervisors such as:

  • VMWare, ESXi, Player and Workstation
  • Microsoft Hyper-V
  • Oracle VirtualBox
  • KVM‌
  • And others, like Xen, but I haven’t tested it yet

Some special requeriments apply depending on the subyacent hypervisor.


Network adapters must be vmxnet3 or E1000‌. Just use vmxnet3 to get the most. Disks must be IDE, VMware paravirtual SCSI, LSI Logic SAS or LSI Logic Parallel.


Network adapters must be Network adapter or Legacy Network adapter .Disks IDE or SCSI.


Virtio, E1000 or vmxnet3 NICs. IDE, Sata or Virtio disks.


Networking using E1000 or rtl8193, and disks with IDE, SATA, SCSI or SAS interfaces.


The CHR images have full RouterOS features enabled by default, but they use a different licensing model than other RouterOS versions.

Paid licenses


p1 (perpetual-1), which allows CHR to run indefinitely. It comes with a limit of 1Gbps upload per interface. All the rest of the features provided by CHR are available without restrictions. It can be upgraded p1 to p10 or p-unlimited.


p10 (perpetual-10), which also allows CHR to run indefinitely, with a 10Gbps upload limit per interface. All features are available without restrictions. It can be upgraded to p-unlimited.

p-unlimited (really?)

The p-unlimited (perpetual-unlimited) license level allows CHR to run indefinitely. It is the highest tier license and it has no enforced limitations.

Free licenses (yay!)

There are two ways to use and try CHR free of charge.


The free license level allows CHR to run indefinitely, with a limit of 1Mbps upload per interface. All the rest of the features have no restrictions. This level comes activated by default on all images.

60-day trial

Th p1/p10/pU licenses can be tested with a 60 days trial.

Cool. How can i try it?

The easiest way to spin up a working instance of CHR is using the OVA appliance provided by MikroTik.

Deployment on ESXi

Once downloaded, the OVA can be used to deploy a new instance. I’ll be using ESXi on this example. The OVA comes preconfigured with a single network adapter, but more interfaces can be added on a later stage.

Creating new VM from OVA template
Setting VM name, and uploading OVA file
I’ll use local storage for it
Thin provisioned disks, and a previously configured VM network
Review everything, and deploy

Initial Configuration

After the VM boots, log in via CLI with the default credentials:

  • Username: admin
  • Password: none

CHR comes with a free licence‌ by default, limited to 1Mbps upload limit. This is handy for lab purposes, or low traffic scenarios like stand-alone DHCP servers.

A DHCP client is enabled by default on the single existing ether1 interface. Use any of the following methods to find out the adquired address.

/ip dhcp-client print
/ip address print

Let’s get a trial licence. You will need the credentials for your MikroTik account. If you don’t have a MikroTik account, get one here.

The CHR instance will also need Internet access, so be sure to connect the virtual NIC to a VM network where it can make its way to the outside.

[admin@CHR] > sys license renew password=yourpassword level=

Level ::= p-unlimited | p1 | p10

Once you request a trial license, check the status with

[admin@CHR] > sys lic print
        system-id: 0ywIRMYrtGA
            level: p1
  next-renewal-at: may/05/2019 17:59:59
      deadline-at: jun/04/2019 17:59:59

We’ll install The Dude on the next post, and configure it for some custom monitoring.


Machine Learning – Weighted Train Data

Last post talked about an introduction to Machine Learning and how outcomes can be predicted using sklearn’s LogisticReggression.

Sometimes, the input data could require additional processing to prefer certain classes of information, that it considered more valuable or more representative to the outcome.

The LogisticRegression model allows to set the preference, or weight, at the time of being created, or later when being fitted.

The data used on the previous entry had four main classes: DRAFT, ACT, SLAST and FLAST. Once it is encoded and fitted, it can be selected by its index. I prefer to initialize some mnemonics selectors to ease the coding and make the entire code more human friendly.

x_columns_names = ['DRAFT', 'ACT', 'SLAST', 'FLAST']
y_columns_names = ['PREDICTION']

# Indexes for columns, used for weighting
ACT = 1

# Weights

The model can be initialized lated using the following method, where the class_weight parameter is used referencing the previous helpers.

model = LogisticRegression(
        ACT: ACT_WEIGHT,


Machine Learning – Classification and Regression Analysis

Machine Learning is the science and art of programming computers so they can learn from data.

For example, your spam filter is a Machine Learning program that can learn to flag spam given examples of spam emails (flagged by users, detected by other methods) and examples of regular (non-spam, also called “ham”) emails.

The examples that the system uses to learn are called the training set. The new ingested data is called the test set. The performance measure of the prediction model is called accuracy and it’s the objetive of this project.

The tools

To tackle this, Python (version 3) will be used, among the package scikit-learn. You can find more info about this package on the official page.

Supervised learning

In general, a learning problem considers a set of n samples of data and then tries to predict properties of unknown data. If each sample is more than a single number and, for instance, a multi-dimensional entry (aka multivariate data), it is said to have several attributes or features.

Supervised learning consists in learning the link between two datasets: the observed data X and an external variable y that we are trying to predict, usually called “target” or “labels”. Most often, y is a 1D array of length n_samples.

All supervised estimators in scikit-learn implement a fit(X, y) method to fit the model and a predict(X) method that, given unlabeled observations X, returns the predicted labels y.

If the prediction task is to classify the observations in a set of finite labels, in other words to “name” the objects observed, the task is said to be a classification task. On the other hand, if the goal is to predict a continuous target variable, it is said to be a regression task.

When doing classification in scikit-learn, y is a vector of integers or strings.

The Models

LinearRegression, in its simplest form, fits a linear model to the data set by adjusting a set of parameters in order to make the sum of the squared residuals of the model as small as possible.

LogisticRegression, which has a very counter-intuitive model, is a better choice when linear regression is not the right approach as it will give too much weight to data far from the decision frontier. A linear approach is to fit a sigmoid function or logistic function.


The Data

Data is presented on a CSV file. It has around 2500 rows, with 5 columns. Correct formatting and integrity of values cannot be assured, so additional processing will be needed. The sample file is like this.

The Code

We need three main libraries to start:

  • numpy, which basically is a N-dimensional array object. It also has tools for linear algebra, Fourier transforms and random numbers.
    It can be used as an efficient multi-dimensional container of generic data, where arbitrary data-types can be defined.
  • pandas, which provides high-performance and easy-to-use data structures and data analysis tools simple and efficient tools for data mining and data analysis
  • sklearn, the main machine learning library. It has capabilities for classification, regression, clustering, dimensionality reduction, model selection and data preprocessing.

A non essential, but useful library is matplotlib, to plot sets of data.

In order to provide data for sklearn models to work, it has to be encoded first. As the sample data has strings, or labels, a LabelEncoder is needed. Next, the prediction model is declared, where a LogisticRegression model is used.

The input data file path is also declared, in order to be loaded with pandas.read_csv().

import pandas as pd
import numpy as np
import matplotlib.pyplot as pyplot

from sklearn.preprocessing import LabelEncoder
from sklearn.linear_model import LogisticRegression

encoder = LabelEncoder()
model = LogisticRegression(
    solver='lbfgs', multi_class='multinomial', max_iter=5000)

# Input dataset
file = "sample_data.csv"

The CSV file can be loaded into a pandas dataframe in a single line. The library also provides a convenient method to remove any rows with missing values.

# Use pandas to load csv. Pandas can eat mixed data with numbers and strings
data = pd.read_csv(file, header=0, error_bad_lines=False)
# Remove missing values
data = data.dropna()

print("Valid data items : %s" % len(data))

Once loaded, the data needs to be encoded in order to be fitted into the prediction model. This is handled by the previously declared LabelEncoder. Once encoded, the x and y datasets are selected. The pandas library provides a way to drop entire labels from a dataframe, which allows to easily select data.

encoded_data = data.apply(encoder.fit_transform)
x = encoded_data.drop(columns=['PREDICTION'])
y = encoded_data.drop(columns=['DRAFT', 'ACT', 'SLAST', 'FLAST'])

The main objective is to test against different lengths of train and test data, to find out how much data provides the best accuracy. The lengths of data will be incremented in steps of 100 to get a broad variety of results.

length = 100
scores = []
lenghts = []
while length < len(x):
    x_train = x[:length]
    y_train = y[:length]
    x_test = x.sample(n=length)
    y_test = y.sample(n=length)
    print("Fitting model for %s training values" % length)
    trained =, y_train.values.ravel())
    score = model.score(x_test, y_test)
    print("Score for %s training values is %0.6f" % (length, score))
    length = length + 100

Finally, a plot is made with the accuracy scores.


Using Zabbix API for Custom Reports

Zabbix is an open source monitoring tool for diverse IT components, including networks, servers, virtual machines (VMs) and cloud services. It provides monitoring metrics, among others network utilization, CPU load and disk space consumption. Data can be collected in a agent-less fashion using SNMP, ICMP, or with an multi-platform agent, available for most operating systems.

Even when it is considered one of the best NMS on the market, its reporting capabilities are very limited. For example, this is an availability report created with PRTG.

Image result for prtg reports

And this is a Zabbix Report. There is no graphs, no data tables, and it is difficult to establish a defined time span for the data collection.

My client required an executive report with the following information.

  • Host / Service Name
  • Minimum SLA for ICMP echo request monitoring
  • Achieved SLA for ICMP echo request monitoring
  • Memory usage graph, if host is being SNMP-monitored
  • Main network interface graph, if host is being SNMP-monitored
  • And storage usage graph, also if the host is being SNMP-monitored

Using the Zabbix API

To do call the API, we need to send HTTP POST requests to the api_jsonrpc.php file located in the frontend directory. For example, if the Zabbix frontend is installed under, the HTTP request to call the apiinfo.version method may look like this:

Content-Type: application/json-rpc

The request must have the Content-Type header set to one of these values: application/json-rpc, application/json or application/jsonrequest.

Before access any data, it’s necessary to log in and obtain an authentication token. The user.login method is used for this.

    "jsonrpc": "2.0",
    "method": "user.login",
    "params": {
        "user": "Admin",
        "password": "zabbix"
    "id": 1,
    "auth": null

If the authentication request succeeds, the API response will look like this.

    "jsonrpc": "2.0",
    "result": "0424bd59b807674191e7d77572075f33",
    "id": 1

The result field is the authentication token, which will be sent on subsequent requests.

Instead of reinvent the wheel, let’s use a existing library to call the API.

Using jqzabbix jQuery plugin for the Zabbix API

GitHub user kodai provides a nice JavaScript client, in a form of a jQuery plugin. You can get it on

The usage is quite forward, first, include both jQuery and jqzabbix.js on your HTML file. I using Cloudflare to link jQuery.

<script src="">/script>
<script type="text/javascript" charset="utf-8" src="jqzabbix.js"></script>

An object has to be created to initialize the client. I prefer to set url, username, and password dynamically, with data provided by the end user, so no credentials are stored here.

server = new $.jqzabbix({
	url: url,  			// URL of Zabbix API
	username: user,   	// Zabbix login user name
	password: pass,  	// Zabbix login password
	basicauth: false,   // If you use basic authentication, set true for this option
	busername: '',      // User name for basic authentication
	bpassword: '',      // Password for basic authentication
	timeout: 5000,      // Request timeout (milli second)
	limit: 1000,        // Max data number for one request

As told before, the first step is to authenticate with the API, and save the authorization token. This is handled by the jqzabbix library by first making a request to get the API version, and then authenticating.


If the authentication procedure is completed properly, the API version and authentication ID are stored as properties of the server object. The userlogin() method allows to set callbacks for both success and error.

var success = function() { console.log('Success!'); }
var error = function() { console.error('Error!'); }

server.userLogin(null, success, error)

Once authenticated, the Zabbix API methods are called in the following fashion with the sendAjaxRequest method.

server.sendAjaxRequest(method, params, success, error)

Retrieving Hosts

I set a global array hosts to store the hosts information.
Another global array called SEARCH_GROUPS is used to define which hosts groups should considered on the API request. By setting the selectHosts parameter to true, the hosts on the host groups are retrieved too on the response.

On success, the result is stored on the hosts array, and the get_graphs function is called. If there is an error, the default error callback is fired.

hosts = [];
function get_hosts() {
	// Get hosts
			"selectHosts": true,
			"filter": {
				"name": SEARCH_GROUPS
		function (e) {
			e.result.forEach(group => {
				group.hosts.forEach(host => {

Retrieving Graphs

Previously, user defined graphs were configured on Zabbix, to match the client requeriments of specific information. All names for the graphs that should be included on the report were terminated the ” – Report” suffix.

This function retrieves all those graphs, and by setting the selectHosts to true, the hosts linked to each graph are retrieved too.

On success, the result is stored on the graphs array, and the render function is called. If there is an error, the default error callback is fired.

graphs = [];
function get_graphs() {
			"selectHosts": "*",
			"search": {
				name: "- Report"
		function (e) {
			graphs = e.result;

Retrieving Graphs Images Instead of Graph Data

By this time you should have noticed that the Zabbix API allows to retrieve values for the graphs, but no images. An additional PHP file will be stored with the HTML and JS files, as a helper to call the web interface by using php_curl.

You can get it on I made a couple modifications to it in order to pass username and password on the URL query, with parameters for the graph ID, the timespan, and the image dimensions.

// GraphImgByID v1.1 
// (c) Travis Mathis -
// It's free use it however you want.
// ChangeLog:
// 1/23/12 - Added width and height to GetGraph Function
// 23/7/13 - Zabbix 2.0 compatibility

$graph_id = filter_input(INPUT_GET,'id');
$period= filter_input(INPUT_GET,'period');
$width= filter_input(INPUT_GET,'width');
$height = filter_input(INPUT_GET,'height');
$user = filter_input(INPUT_GET,'user');
$pass = filter_input(INPUT_GET,'pass');

$z_server = 'zabbix_url'; //set your URL here
$z_user = $user;
$z_pass = $pass;
$z_img_path = "/usr/local/share/zabbix/custom_pages/tmp_images/";

$z_tmp_cookies = "";
$z_url_index = $z_server . "index.php";
$z_url_graph = $z_server . "chart2.php";
$z_url_api = $z_server . "api_jsonrpc.php";

// Zabbix 1.8
// $z_login_data  = "name=" .$z_user ."&password=" .$z_pass ."&enter=Enter";
// Zabbix 2.0
$z_login_data = array('name' => $z_user, 'password' => $z_pass, 'enter' => "Sign in");

function GraphImageById($graphid, $period = 3600, $width, $height) {
    global $z_server, $z_user, $z_pass, $z_tmp_cookies, $z_url_index, $z_url_graph, $z_url_api, $z_img_path, $z_login_data;
    // file names
    $filename_cookie = $z_tmp_cookies . "zabbix_cookie_" . $graphid . ".txt";
    $image_name = $z_img_path . "zabbix_graph_" . $graphid . ".png";

    //setup curl
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $z_url_index);
    curl_setopt($ch, CURLOPT_HEADER, false);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $z_login_data);
    curl_setopt($ch, CURLOPT_COOKIEJAR, $filename_cookie);
    curl_setopt($ch, CURLOPT_COOKIEFILE, $filename_cookie);
    // login
    // get graph
    curl_setopt($ch, CURLOPT_URL, $z_url_graph . "?graphid=" . $graphid . "&width=" . $width . "&height=" . $height . "&period=" . $period);
    $output = curl_exec($ch);
    // delete cookie
    header("Content-type: image/png");
      $fp = fopen($image_name, 'w');
      fwrite($fp, $output);
      header("Content-type: text/html");
    return $output;

echo GraphImageById($graph_id, $period, $width, $height);

Quick and Dirty Frontend

You should be able to customize this small frontend to your needs.


	<link rel="stylesheet" href="">
	<script src=""></script>
	<script src=""></script>
	<script src="jqzabbix.js"></script>
		.host-container {
			margin-bottom: 3em;
		@media print {
			.host-container {
				page-break-before: auto;
				page-break-after: auto;
				page-break-inside: avoid;
			img {
				display: block;
				page-break-before: auto;
				page-break-after: auto;
				page-break-inside: avoid;

	<div id="container" class="container">

		<div class="row" style="margin-bottom: 3em">
			<div class="col">
				<h2>Services and Availability Report</h2>
				<table id="table" class="bg-dark">
						<th>Host Name</th>
						<th class="is-text-center">Availibilty</th>
						<th class="is-text-center">Availabilty Status</th>
						<th class="is-text-center">Total Availability</th>

		<div id="template" style="display: none">
			<div class="host-container">
				<div class="row bg-dark">
					<div class="col-12">
						<span id="host-HOST_ID-name">Service Name</span>
				<div class="row bg-light">
					<div class="col-3">
					<div class="col-3">
						SLA Minimum
					<div class="col-3">
				<div class="row bg-primary">
					<div class="col-3">
						<span id="host-HOST_ID-status"></span>OK</span>
					<div class="col-3">
						<span id="host-HOST_ID-sla"></span>99.9%
					<div class="col-3">
						<span id="host-HOST_ID-sla-value"></span>100%
				<div class="row is-text-center" id="host-HOST_ID-graphs">


	<script src="ui.js"></script>




The final page is a complete report, including a briefing table which resumes the services status and SLA compliance.