Categories
Networking Projects Ubiquiti

Digging into Ubiquiti’s UFiber OLT

As some of you might know, currently I’m working as a network engineer on a medium size ISP. The company had a long history working as a WISP, and in later times they moved into FTTH, trying several vendors among the lead players of the industry.

As some of you might also know, Argentina has a history of economic meltdowns, currency devaluations and import restrictions. Considering this, the best solution to implement a network here is usually the one you can afford, which can provide the performance you need, and over all things, the one you will be able to keep buying in the future.

So, considering all these factors, when planning for a GPON network for a medium size operator…while trying to keep costs low for both the company and customers:

  • It really doesn’t matter if Calix supports XGS-PON technologies…
  • Or if Huawei gear is compatible with almost everything…
  • Or if Furukawa Electric has some great management software…

The real questions to ask were:

  • Can the company afford the OLTs, and the ONUs for the planned customer base?
  • Will they be in the market in the years to come?

Enter Ubiquiti UFiber

UFiber offers internet and telecom service providers a cost‑effective fiber optic delivery system for Triple Play Services (data, voice, IPTV/VoD) with speeds of up to 2.488 Gbps downstream and 1.244 Gbps upstream.

OLTs come with dual hot-swap power supplies, 4 and 8 PON ports versions. Every PON port supports 128 CPEs, 20 Km maximum range. The uplinks are two SFP+, which can with in LACP.

The ONUs options, at the time when I’m writing, are:

  • UFiber Nano – one PON (of course), one Gigabit Ethernet, a fancy LCD display. Passive PoE powered.
  • UFiber Loco – a PON, a Giga Eth, passive PoE powered or external micro USB power.
  • UFiber Wifi, like above, but with 4 Giga Ethernet ports, and a 802.11n interface.
  • UFiber Instant, a nice SFP ONU.

Ok, sounds nice. How do we manage them? There is a web GUI…

Hey, the GUI looks like EdgeOS !

Once logged in, the GUI has a nice dashboard which looks like this. And it crashes from time to time.

They tried

But this not EdgeOS, the OLT is a different product! Let’s ssh into it to get the real feel.

ssh admin@olt
The authenticity of host 'olt (olt)' can't be established.
ECDSA key fingerprint is SHA256:thnWRB2bImsdNuu1ar74GryFwv5r7PoHJsHhJOkHnCQ.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'olt' (ECDSA) to the list of known hosts.
Welcome to EdgeOS
By logging in, accessing, or using the Ubiquiti product, you
acknowledge that you have read and understood the Ubiquiti
License Agreement (available in the Web UI at, by default,
http://192.168.1.1) and agree to be bound by its terms.
admin@olt's password:
Linux olt 4.4.159+ #1 SMP Fri Feb 22 15:28:22 UTC 2019 mips
Welcome to EdgeOS
Last login: Tue May 26 15:50:16 2020 from 190.211.80.70
admin@olt:~$

Ok, this is definitely EdgeOS. So we got a fully featured command line interface, with commands similar to Juniper JunOS.

admin@olt:~$ show configuration | display set
-vbash: display: command not found

Ok, maybe the command line is not so-fully-featured. No worries, I’ll write Ansible playbooks to manage the OLTs anyway. Most of the configuration is Juniper-like, so all I need at this moment is find out how to configure GPON profiles, and provision ONUs.

admin@olt:~$ show configuration | match onu
admin@olt:~$ show configuration | match profile
admin@olt:~$ show configuration | match gpon
gpon {

There you are! Let’s go into configuration mode.

admin@olt:~$ configure
[edit]
admin@olt# show system gpon
isolation enable
mtu 1518
[edit]
admin@olt#

Well, this is awkward. There is nothing about GPON in the command line. Neither in the working configuration, and of course being Ubiquiti, there are no command line manuals.

Love the smell of undocumented commands in the morning

So, I saw two interesting lines before: Linux olt 4.4.15, and -vbash: display: command not found, which tells me this is Linux, not BSD as in Junos, and we have bash.

admin@olt:~$
Possible completions:
  add           Add an object to a service
  clear         Clear system information
  configure     Enter configure mode
  connect       Establish a connection
  copy          Copy data
  delete        Delete a file
  disconnect    Take down a connection
  generate      Generate an object
  initial-setup Enter initial configuration dialog
  no            Disable or reset operational variable
  ping          Send Internet Control Message Protocol (ICMP) echo request
  ping6         Send IPv6 Internet Control Message Protocol (ICMP) echo request
  reboot        Reboot the system
  release       Release specified variable
  rename        Re-name something.
  renew         Renew specified variable
  reset         Reset a service
  restart       Restart a service
  set           Set system or shell options
  show          Show system information
  shutdown      Shutdown the system
  telnet        Telnet to <hostname|IPv4 address>
  terminal      Control terminal behaviors
  traceroute    Track network path to <hostname|IPv4 address>
  traceroute6   Track network path to <hostname|IPv6 address>

No signs of bourne again shells in the horizon. Does my magic have any power here?

admin@olt:~$ sh
sh-4.4$ whoami
admin
sh-4.4$ sudo su
root@olt:/home/admin#

Finally a decent shell. Which world is this?

root@olt:~# uname -a
Linux olt 4.4.159+ #1 SMP Fri Feb 22 15:28:22 UTC 2019 mips GNU/Linux
root@olt:~# ls -l /etc/ | grep apt
drwxr-xr-x 6 root root 117 Feb 22 2019 apt

We have apt, so this is a Debian world. I checked on /etc/apt/ and there are no repositories, but I am sure I could run cowsay on this. But the fun can wait.

Where is my GPON configuration? It should say “onu” somewhere.

root@olt:/# grep -r "onu" / | more
grep: /proc/sys/net/ipv4/route/flush: Permission denied
/config/onu_config.json: "onu-policies": {
/config/onu_config.json: "onu-list": {
/config/onu_config.json: "onu-profiles": {
/home/admin/.history:show configuration | match onu
/home/admin/.history:show configuration | match onu
Binary file /lib/mipsel-linux-gnu/libbsd.so.0.8.3 matches
Binary file /lib/mipsel-linux-gnu/libnss_hesiod-2.24.so matches
Binary file /lib/udev/hwdb.bin matches
/lib/udev/hwdb.d/20-OUI.hwdb: ID_OUI_FROM_DATABASE=Monument Labs, Inc.
/lib/udev/hwdb.d/20-OUI.hwdb: ID_OUI_FROM_DATABASE=Optical Zonu Corporation
/lib/udev/hwdb.d/20-OUI.hwdb: ID_OUI_FROM_DATABASE=Presonus Corporation
/lib/udev/hwdb.d/20-usb-vendor-model.hwdb: ID_VENDOR_FROM_DATABASE=PreSonus Audio Electronics, Inc.
Binary file /opt/bcm68620/bcm68620_appl.bin matches
Binary file /opt/bcm68620/bcm_dev_ctrl_linux.ko matches
Binary file /opt/bcm68620/bcm_user_appl matches
/opt/vyatta/share/vyatta-cfg/templates/system/gpon/logging/module/node.def:syntax:expression: $VAR(@) in "main", "oltsys", "onu", "session", "events", "mon_th", "sdk"
/opt/vyatta/share/vyatta-cfg/templates/system/gpon/logging/module/node.def:allowed: echo main oltsys onu session events mon_th sdk

I bolded the interesting information.

  • There is a /config directory, which has a JSON file called onu_config.json
  • The operating system, is in fact, Vyatta.

If you are curious, this is the content of /config. We will dig deeper on the next article.

root@olt:/# ls -l /config
total 200
-rw-rw-r-- 1 root vyattacfg 3336 Jan 1 2015 2020
drwxrwsr-x 1 root vyattacfg 160 Feb 22 2019 auth
-rw-rw-r-- 1 root vyattacfg 3882 May 26 11:59 config.boot
-rw-r----- 1 root vyattacfg 2402 Dec 31 2014 config.boot.2015-01-01-0001.pre-migration
-rw-r----- 1 root vyattacfg 3151 Apr 13 2015 config.boot.2015-04-14-0130.pre-migration
-rw------- 1 root vyattacfg 187285 May 26 16:14 onu_config.json
drwxrwsr-x 1 root vyattacfg 232 Feb 22 2019 scripts
drwxr-sr-x 2 root vyattacfg 232 Dec 31 2014 snmp
drwxrwsr-x 1 root vyattacfg 160 Feb 22 2019 support
drwxr-xr-x 1 root root 160 Oct 29 2018 udapi-bridge
drwxrwsr-x 1 root vyattacfg 160 Feb 22 2019 user-data
drwxr-sr-x 3 www-data vyattacfg 224 Dec 31 2014 wizard
Categories
Projects

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 http://company.com/zabbix, the HTTP request to call the apiinfo.version method may look like this:

POST http://company.com/zabbix/api_jsonrpc.php HTTP/1.1
Content-Type: application/json-rpc
{
    "jsonrpc":"2.0",
    "method":"apiinfo.version",
    "id":1,
    "auth":null,
    "params":
        {
        }
}

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 https://github.com/kodai/jqzabbix.

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

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js">/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.

server.getApiVersion();
server.userLogin();

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
	server.sendAjaxRequest(
		"hostgroup.get",
		{
			"selectHosts": true,
			"filter": {
				"name": SEARCH_GROUPS
			}
		},
		function (e) {
			e.result.forEach(group => {
				group.hosts.forEach(host => {
					hosts.push(host);
				});
			});
			get_graphs();
		},
		error,
	);
}

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() {
	server.sendAjaxRequest(
		"graph.get",
		{
			"selectHosts": "*",
			"search": {
				name: "- Report"
			}
		},
		function (e) {
			graphs = e.result;
			render();
		},
		error
	)
}

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 https://zabbix.org/wiki/Get_Graph_Image_PHP. 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.

<?php
//////////
// GraphImgByID v1.1 
// (c) Travis Mathis - [email protected]
// 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
// ERROR REPORTING
error_reporting(E_ALL);
set_time_limit(1800);


$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');

//CONFIGURATION
$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/";

//NON CONFIGURABLE
$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
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
    curl_exec($ch);
    // get graph
    curl_setopt($ch, CURLOPT_URL, $z_url_graph . "?graphid=" . $graphid . "&width=" . $width . "&height=" . $height . "&period=" . $period);
    $output = curl_exec($ch);
    curl_close($ch);
    // delete cookie
    header("Content-type: image/png");
    unlink($filename_cookie);
    /*
      $fp = fopen($image_name, 'w');
      fwrite($fp, $output);
      fclose($fp);
      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.

<html>

<head>
	<link rel="stylesheet" href="https://unpkg.com/chota@latest">
	<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
	<script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
	<script src="jqzabbix.js"></script>
	<style>
		.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;
			}
		}
	</style>
</head>

<body>
	<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">
					<thead>
						<th>Host Name</th>
						<th>Target</th>
						<th class="is-text-center">Availibilty</th>
						<th class="is-text-center">Availabilty Status</th>
						<th class="is-text-center">Total Availability</th>
					</thead>
				</table>
			</div>
		</div>


		<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>
				</div>
				<div class="row bg-light">
					<div class="col-3">
						Status
					</div>
					<div class="col-3">
						SLA Minimum
					</div>
					<div class="col-3">
						SLA
					</div>
				</div>
				<div class="row bg-primary">
					<div class="col-3">
						<span id="host-HOST_ID-status"></span>OK</span>
					</div>
					<div class="col-3">
						<span id="host-HOST_ID-sla"></span>99.9%
					</div>
					<div class="col-3">
						<span id="host-HOST_ID-sla-value"></span>100%
					</div>
				</div>
				<div class="row is-text-center" id="host-HOST_ID-graphs">
				</div>
			</div>
		</div>

	</div>

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

</body>

</html>

Result

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