net/quagga: version 1.1.0 (BGP support and assorted fixes)

This commit is contained in:
Franco Fichtner 2017-04-30 13:12:21 +02:00
parent 87e559d5a9
commit e3f2425e80
16 changed files with 1056 additions and 9 deletions

View file

@ -1,7 +1,7 @@
PLUGIN_NAME= quagga
PLUGIN_VERSION= 1.0.0
PLUGIN_VERSION= 1.1.0
PLUGIN_COMMENT= Quagga Routing Suite
PLUGIN_DEPENDS= quagga
PLUGIN_DEPENDS= quagga ruby
PLUGIN_MAINTAINER= franz.fabian.94@gmail.com
.include "../../Mk/plugins.mk"

View file

@ -41,6 +41,7 @@ function quagga_services()
'stop' => array('quagga stop'),
),
'name' => 'quagga',
'pidfile' => '/var/run/quagga/zebra.pid'
);
}

View file

@ -0,0 +1,202 @@
<?php
namespace OPNsense\Quagga\Api;
use \OPNsense\Base\ApiControllerBase;
use \OPNsense\Quagga\BGP;
use \OPNsense\Core\Config;
use \OPNsense\Base\ApiMutableModelControllerBase;
use \OPNsense\Base\UIModelGrid;
/**
* Copyright (C) 2015 - 2017 Deciso B.V.
* Copyright (C) 2017 Fabian Franz
* Copyright (C) 2017 Michael Muenz
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
*/
class BgpController extends ApiMutableModelControllerBase
{
static protected $internalModelName = 'BGP';
static protected $internalModelClass = '\OPNsense\Quagga\BGP';
public function getAction()
{
// define list of configurable settings
$result = array();
if ($this->request->isGet()) {
$mdlBGP = new BGP();
$result['bgp'] = $mdlBGP->getNodes();
}
return $result;
}
public function setAction()
{
$result = array("result"=>"failed");
if ($this->request->isPost()) {
// load model and update with provided data
$mdlBGP = new BGP();
$mdlBGP->setNodes($this->request->getPost("bgp"));
// perform validation
$valMsgs = $mdlBGP->performValidation();
foreach ($valMsgs as $field => $msg) {
if (!array_key_exists("validations", $result)) {
$result["validations"] = array();
}
$result["validations"]["bgp.".$msg->getField()] = $msg->getMessage();
}
// serialize model to config and save
if ($valMsgs->count() == 0) {
$mdlBGP->serializeToConfig();
Config::getInstance()->save();
$result["result"] = "saved";
}
}
return $result;
}
public function searchNeighborAction()
{
$this->sessionClose();
$mdlBGP = $this->getModel();
$grid = new UIModelGrid($mdlBGP->neighbors->neighbor);
return $grid->fetchBindRequest(
$this->request,
array("enabled", "address", "remoteas", "updatesource", "nexthopself", "defaultoriginate" )
);
}
public function getNeighborAction($uuid = null)
{
$mdlBGP = $this->getModel();
if ($uuid != null) {
$node = $mdlBGP->getNodeByReference('neighbors.neighbor.' . $uuid);
if ($node != null) {
// return node
return array("neighbor" => $node->getNodes());
}
} else {
$node = $mdlBGP->neighbors->neighbor->add();
return array("neighbor" => $node->getNodes());
}
return array();
}
public function addNeighborAction()
{
$result = array("result" => "failed");
if ($this->request->isPost() && $this->request->hasPost("neighbor")) {
$result = array("result" => "failed", "validations" => array());
$mdlBGP = $this->getModel();
$node = $mdlBGP->neighbors->neighbor->Add();
$node->setNodes($this->request->getPost("neighbor"));
$valMsgs = $mdlBGP->performValidation();
foreach ($valMsgs as $field => $msg) {
$fieldnm = str_replace($node->__reference, "neighbor", $msg->getField());
$result["validations"][$fieldnm] = $msg->getMessage();
}
if (count($result['validations']) == 0) {
// save config if validated correctly
$mdlBGP->serializeToConfig();
Config::getInstance()->save();
$result["result"] = "saved";
}
}
return $result;
}
public function delNeighborAction($uuid)
{
$result = array("result" => "failed");
if ($this->request->isPost()) {
$mdlBGP = $this->getModel();
if ($uuid != null) {
if ($mdlBGP->neighbors->neighbor->del($uuid)) {
$mdlBGP->serializeToConfig();
Config::getInstance()->save();
$result['result'] = 'deleted';
} else {
$result['result'] = 'not found';
}
}
}
return $result;
}
public function setNeighborAction($uuid)
{
if ($this->request->isPost() && $this->request->hasPost("neighbor")) {
$mdlNeighbor = $this->getModel();
if ($uuid != null) {
$node = $mdlNeighbor->getNodeByReference('neighbors.neighbor.' . $uuid);
if ($node != null) {
$result = array("result" => "failed", "validations" => array());
$neighborInfo = $this->request->getPost("neighbor");
$node->setNodes($neighborInfo);
$valMsgs = $mdlNeighbor->performValidation();
foreach ($valMsgs as $field => $msg) {
$fieldnm = str_replace($node->__reference, "neighbor", $msg->getField());
$result["validations"][$fieldnm] = $msg->getMessage();
}
if (count($result['validations']) == 0) {
// save config if validated correctly
$mdlNeighbor->serializeToConfig();
Config::getInstance()->save();
$result = array("result" => "saved");
}
return $result;
}
}
}
return array("result" => "failed");
}
public function toggle_handler($uuid, $elements, $element)
{
$result = array("result" => "failed");
if ($this->request->isPost()) {
$mdlNeighbor = $this->getModel();
if ($uuid != null) {
$node = $mdlNeighbor->getNodeByReference($elements . '.'. $element .'.' . $uuid);
if ($node != null) {
if ($node->enabled->__toString() == "1") {
$result['result'] = "Disabled";
$node->enabled = "0";
} else {
$result['result'] = "Enabled";
$node->enabled = "1";
}
// if item has toggled, serialize to config and save
$mdlNeighbor->serializeToConfig();
Config::getInstance()->save();
}
}
}
return $result;
}
public function toggleNeighborAction($uuid)
{
return $this->toggle_handler($uuid, 'neighbors', 'neighbor');
}
}

View file

@ -150,6 +150,7 @@ class OspfsettingsController extends ApiMutableModelControllerBase
// save config if validated correctly
$mdlOSPF->serializeToConfig();
Config::getInstance()->save();
unset($result['validations']);
$result["result"] = "saved";
}
}
@ -174,6 +175,7 @@ class OspfsettingsController extends ApiMutableModelControllerBase
// save config if validated correctly
$mdlOSPF->serializeToConfig();
Config::getInstance()->save();
unset($result['validations']);
$result["result"] = "saved";
}
}

View file

@ -1,12 +1,35 @@
<?php
namespace OPNsense\Quagga;
/*
Copyright (C) 2017 Fabian Franz
Copyright (C) 2017 Michael Muenz
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
class BgpController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->view->title = gettext("BGP-Settings");
$this->view->generalForm = $this->getForm("bgp");
$this->view->title = gettext("BGP Settings");
$this->view->bgpForm = $this->getForm("bgp");
$this->view->formDialogEditBGPNeighbor = $this->getForm("dialogEditBGPNeighbor");
$this->view->pick('OPNsense/Quagga/bgp');
}
}

View file

@ -1,8 +1,30 @@
<form>
<field>
<id>routing.bgp.general.Enabled</id>
<id>bgp.enabled</id>
<label>enable</label>
<type>checkbox</type>
<help>This will activate the bgp service.</help>
</field>
<field>
<id>bgp.asnumber</id>
<label>BGP AS Number</label>
<type>text</type>
<hint>Your AS Number here</hint>
</field>
<field>
<id>bgp.networks</id>
<label>Network</label>
<style>tokenize</style>
<type>select_multiple</type>
<allownew>true</allownew>
<help>Select the network to advertise, you have to set a Null route via System -> Routes</help>
</field>
<field>
<id>bgp.redistribute</id>
<label>Route Redistribution</label>
<type>select_multiple</type>
<style>tokenize</style>
<help><![CDATA[Select other routing sources, which should be redistributed to the other nodes.]]></help>
<hint>Type or select a route source.</hint>
</field>
</form>

View file

@ -0,0 +1,35 @@
<form>
<field>
<id>neighbor.enabled</id>
<label>Enabled</label>
<type>checkbox</type>
</field>
<field>
<id>neighbor.address</id>
<label>Peer-IP</label>
<type>text</type>
<help>Specify the IP of your neighbor.</help>
</field>
<field>
<id>neighbor.remoteas</id>
<label>Remote AS</label>
<type>text</type>
<help>Neighbor AS.</help>
</field>
<field>
<id>neighbor.updatesource</id>
<label>Update-Source Interface</label>
<type>select_multiple</type>
<help>Physical name of the interface facing the peer</help>
</field>
<field>
<id>neighbor.nexthopself</id>
<label>Next-Hop-Self</label>
<type>checkbox</type>
</field>
<field>
<id>neighbor.defaultoriginate</id>
<label>Send Defaultroute</label>
<type>checkbox</type>
</field>
</form>

View file

@ -0,0 +1,30 @@
<?php
namespace OPNsense\Quagga;
use OPNsense\Base\BaseModel;
/*
Copyright (C) 2017 Fabian Franz
Copyright (C) 2017 Michael Muenz
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
class BGP extends BaseModel
{
}

View file

@ -0,0 +1,67 @@
<model>
<mount>//OPNsense/quagga/bgp</mount>
<description>BGP Routing configuration</description>
<items>
<enabled type="BooleanField">
<default>0</default>
<Required>Y</Required>
</enabled>
<asnumber type="TextField">
<default></default>
<Required>Y</Required>
</asnumber>
<networks type="CSVListField">
<default></default>
<Required>N</Required>
<mask>/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2},)*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2})$/</mask>
</networks>
<redistribute type="OptionField">
<Required>N</Required>
<multiple>Y</multiple>
<default></default>
<OptionValues>
<babel>Babel routing protocol (Babel)</babel>
<ospf>Open Shortest Path First (OSPF)</ospf>
<connected>Connected routes (directly attached subnet or host)</connected>
<isis>Intermediate System to Intermediate System (IS-IS)</isis>
<kernel>Kernel routes (not installed via the zebra RIB)</kernel>
<pim>Protocol Independent Multicast (PIM)</pim>
<rip>Routing Information Protocol (RIP)</rip>
<static>Statically configured routes</static>
</OptionValues>
</redistribute>
<neighbors>
<neighbor type="ArrayField">
<enabled type="BooleanField">
<default>1</default>
<Required>Y</Required>
</enabled>
<address type="TextField">
<default></default>
<Required>Y</Required>
<mask>/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/</mask>
</address>
<remoteas type="TextField">
<default></default>
<Required>Y</Required>
</remoteas>
<updatesource type="InterfaceField">
<default></default>
<Required>N</Required>
<multiple>N</multiple>
<filters>
<enable>/^(?!0).*$/</enable>
</filters>
</updatesource>
<nexthopself type="BooleanField">
<default>0</default>
<Required>N</Required>
</nexthopself>
<defaultoriginate type="BooleanField">
<default>0</default>
<Required>N</Required>
</defaultoriginate>
</neighbor>
</neighbors>
</items>
</model>

View file

@ -3,8 +3,8 @@
<General VisibleName="General" cssClass="fa fa-bolt fa-fw" url="/ui/quagga/general/index" order="1"/>
<RIP VisibleName="RIP" cssClass="fa fa-bolt fa-fw" url="/ui/quagga/rip/index" order="10" />
<OSPF VisibleName="OSPF" cssClass="fa fa-bolt fa-fw" url="/ui/quagga/ospf/index" order="20" />
<!--<ISIS VisibleName="IS-IS" cssClass="fa fa-bolt fa-fw" url="/ui/quagga/isis/index" order="30" />
<BGP VisibleName="BGPv4" cssClass="fa fa-bolt fa-fw" url="/ui/quagga/bgp/index" order="40" />-->
<!--<ISIS VisibleName="IS-IS" cssClass="fa fa-bolt fa-fw" url="/ui/quagga/isis/index" order="30" />-->
<BGP VisibleName="BGPv4" cssClass="fa fa-bolt fa-fw" url="/ui/quagga/bgp/index" order="40" />
</Routing>
</menu>

View file

@ -1 +1,112 @@
{{ partial("layout_partials/base_form",['fields':generalForm,'id':'frm_ospf_settings'])}}
{#
OPNsense® is Copyright © 2014 2017 by Deciso B.V.
Copyright (C) 2017 Fabian Franz
Copyright (C) 2017 Michael Muenz
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
#}
<!-- Navigation bar -->
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#general">{{ lang._('General') }}</a></li>
<li><a data-toggle="tab" href="#neighbors">{{ lang._('Neighbors') }}</a></li>
</ul>
<div class="tab-content content-box tab-content">
<div id="general" class="tab-pane fade in active">
<div class="content-box" style="padding-bottom: 1.5em;">
{{ partial("layout_partials/base_form",['fields':bgpForm,'id':'frm_bgp_settings'])}}
<hr />
<div class="col-md-12">
<button class="btn btn-primary" id="saveAct" type="button"><b>{{ lang._('Save') }}</b></button>
</div>
</div>
</div>
<div id="neighbors" class="tab-pane fade in">
<table id="grid-neighbors" class="table table-responsive" data-editDialog="DialogEditBGPNeighbor">
<thead>
<tr>
<th data-column-id="enabled" data-type="string" data-formatter="rowtoggle">{{ lang._('Enabled') }}</th>
<th data-column-id="address" data-type="string" data-visible="true">{{ lang._('Neighbor Address') }}</th>
<th data-column-id="remoteas" data-type="string" data-visible="true">{{ lang._('Remote AS') }}</th>
<th data-column-id="updatesource" data-type="string" data-visible="true">{{ lang._('Update Source Address') }}</th>
<th data-column-id="nexthopself" data-type="string" data-formatter="rowtoggle">{{ lang._('Next Hop Self') }}</th>
<th data-column-id="defaultoriginate" data-type="string" data-formatter="rowtoggle">{{ lang._('Default Originate') }}</th>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="commands" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td colspan="5"></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-default"><span class="fa fa-plus"></span></button>
<!-- <button data-action="deleteSelected" type="button" class="btn btn-xs btn-default"><span class="fa fa-trash-o"></span></button> -->
</td>
</tr>
</tfoot>
</table>
</div>
</div>
<script type="text/javascript">
$(document).ready(function() {
var data_get_map = {'frm_bgp_settings':"/api/quagga/bgp/get"};
mapDataToFormUI(data_get_map).done(function(data){
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
});
ajaxCall(url="/api/quagga/service/status", sendData={}, callback=function(data,status) {
updateServiceStatusUI(data['status']);
});
// link save button to API set action
$("#saveAct").click(function(){
saveFormToEndpoint(url="/api/quagga/bgp/set",formid='frm_bgp_settings',callback_ok=function(){
ajaxCall(url="/api/quagga/service/reconfigure", sendData={}, callback=function(data,status) {
ajaxCall(url="/api/quagga/service/status", sendData={}, callback=function(data,status) {
updateServiceStatusUI(data['status']);
});
});
});
});
$("#grid-neighbors").UIBootgrid(
{ 'search':'/api/quagga/bgp/searchNeighbor',
'get':'/api/quagga/bgp/getNeighbor/',
'set':'/api/quagga/bgp/setNeighbor/',
'add':'/api/quagga/bgp/addNeighbor/',
'del':'/api/quagga/bgp/delNeighbor/',
'toggle':'/api/quagga/bgp/toggleNeighbor/',
'options':{selection:false, multiSelect:false}
}
);
});
</script>
{{ partial("layout_partials/base_dialog",['fields':formDialogEditBGPNeighbor,'id':'DialogEditBGPNeighbor','label':lang._('Edit Neighbor')])}}

View file

@ -98,7 +98,7 @@ POSSIBILITY OF SUCH DAMAGE.
</tfoot>
</table>
</div>
</div>
<script type="text/javascript">

View file

@ -0,0 +1,480 @@
#!/usr/local/bin/ruby
=begin
Copyright 2017 Fabian Franz
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
=end
require 'json'
require 'shellwords'
require 'pp'
class VTYSH
def initialize(path = '/usr/local/bin/vtysh')
@path = path
end
def execute(param)
o = `#{@path} -c #{param.shellescape}`
raise "error" if o.length <= 2
raise "command error - command: #{param}" if o.include? "% Unknown command"
o
end
#def execute(param)
# fn = param.sub("show","sh").gsub(" ","_")
# File.read(fn)
#end
end
class QuaggaTableReader
attr_accessor :headers
def initialize(headers = [])
@headers = headers
end
def read_headline(line, start_without_header = false, start_without_header_name = 'status')
# get begin of header (number of the first char of the string)
header = line
header_offset = {}
header_offset[0] = start_without_header_name if start_without_header
@headers.map do |x|
header_offset[header.index(x)] = x.strip
end
# make ranges: this will make a range of the first char of the sting until
# the the char befor the next heading begins
ranges = []
0.upto (header_offset.keys.length - 2) do |i|
ranges << ((header_offset.keys[i])...(header_offset.keys[i + 1]))
end
# the last one has no next heading - this will go to the end of the line
ranges.push ((header_offset.keys.last)..-1) # path
@header_offset = header_offset
@ranges = ranges
nil
end
def read_entry(line, expand_fields = {})
raise "heading missing" unless @ranges
tmp = {}
return tmp unless line&.strip.length > 2
@ranges.each do |r|
# the string starts here
b = r.begin
# get the heading starting where the string starts
n = @header_offset[b]
# get the data or return an empty string
tmp[n] = line[r]&.strip || ""
end
# replace characters by the meaning
expand_fields.keys.each do |key|
tmp[key] = tmp[key].split("").map {|x| {dn: expand_fields[key][x], abb: x} } if tmp[key]
end
tmp
end
end
class General
def initialize(vtysh)
@vtysh = vtysh
end
def routes
lines = @vtysh.execute("show ip route").lines
# headers
meanings = {}
while (line = lines.shift.strip) != ''
line = line.gsub('Codes: ','')
line.split(",").each do |meaning|
short, long = meaning.strip.split(" - ")
meanings[short] = long
end
end
# you don't have to understand this regex ;)
entry_regex = /(\S+?)\s+?(\S+?)(?: \[(\d+)\/(\d+)\])? (?:via (\S+?)|is ([^,]+?)), ([^,\n]+)(?:, (\S+))?/
entries = []
while (line = lines.shift&.strip)
if line.length > 10
code, network, ad, metric, via, direct, interface, time = line.scan(entry_regex).first
code = code.split('').map {|c| {short: c, long: meanings[c]}}
entries << {code: code, network: (network || direct), ad: ad, metric: metric, interface: interface, time: time }
end
end
entries
end
end
class OSPF
def initialize(vtysh)
@vtysh = vtysh
end
def neighbors
qta = QuaggaTableReader.new(["Neighbor ID", "Pri State", "Dead Time", "Address", "Interface", "RXmtL", "RqstL", "DBsmL"])
lines = @vtysh.execute("show ip ospf neighbor").lines
lines.shift # empty line
data = []
qta.read_headline(lines.shift)
while (line = lines.shift) && (line.length > 2)
data << qta.read_entry(line)
end
data
end
def interface
lines = @vtysh.execute("show ip ospf interface").lines
interfaces = {}
current_if = ''
while line = lines.shift
next if line.strip.length <= 1
if line[0] != ' ' # we are in a heading
current_if = line.split(" ").first
interfaces[current_if] = {}
current_if = interfaces[current_if]
current_if[:enabled] = true
lines.shift
else
line.strip!
case line
when 'OSPF not enabled on this interface'
current_if[:enabled] = false
when /Internet Address ([^,]+?), Broadcast ([^,]+?), Area (.*)/
current_if[:address] = $1
current_if[:broadcast] = $2
current_if[:area] = $3
when /MTU mismatch detection:(.*)/
current_if[:mtu_mismatch_detection] = ($1 == 'enabled')
when /Router ID ([^,]+?), Network Type ([^,]+?), Cost: (\d+)/
current_if[:router_id] = $1
current_if[:network_type] = $2
current_if[:cost] = $3.to_i
when /Transmit Delay is (\d+) sec, State ([^,]+?), Priority (\d+)/
current_if[:transmit_delay] = $1.to_i
current_if[:state] = $2
current_if[:priority] = $3.to_i
when "No designated router on this network"
current_if[:designated_router] = nil
when /Designated Router \(ID\) ([^,]+?), Interface Address (.*)/
current_if[:designated_router] = $1
current_if[:designated_router_interface_address] = $2
when "No backup designated router on this network"
current_if[:backup_designated_router] = nil
when /Timer intervals configured, Hello (\d+)s, Dead (\d+)s, Wait (\d+)s, Retransmit (\d+)/
current_if[:intervals] = {hello: $1.to_i, dead: $2.to_i, wait: $3.to_i, retransmit: $4.to_i}
when /Multicast group memberships: (.*)/
current_if[:multicast_group_memberships] = $1.split(" ")
when /Hello due in ([\d\.]+|inactive)s?/
current_if[:hello_due_in] = $1 == 'inactive' ? $1 : $1.to_f
when /Neighbor Count is (\d+), Adjacent neighbor count is (\d+)/
current_if[:neighbor_count] = $1.to_i
current_if[:adjacent_neighbor_count] = $2.to_i
else
# make sure there is an array to write in
current_if[:unparsed] ||= []
current_if[:unparsed] << line
end
end
end
interfaces
end
def database
lines = @vtysh.execute("show ip ospf database").lines
db = {}
heading = ''
router = ''
router_link_states_area = ''
mode = :none
qta = nil
while line = lines.shift
next if line == ''
if line[0] == ' ' # heading
heading = line.strip
case heading
when /OSPF Router with ID \(([\.\d]+)\)/
router = $1
db[router] ||= {}
mode = :router
when /Router Link States \(Area ([\.\d]+)\)/
router_link_states_area = $1
db[router]['link_state_area'] ||= {}
db[router]['link_state_area'][$1] ||= []
mode = :link_state
qta = nil
when 'AS External Link States'
mode = :states
db[router]['external_states'] ||= []
qta = nil
else
$stderr.puts "unknown heading"
end
else
if qta == nil
case mode
when :link_state
qta = QuaggaTableReader.new(["Link ID", "ADV Router", "Age", "Seq#", "CkSum", "Link count"])
when :states
qta = QuaggaTableReader.new(["Link ID", "ADV Router", "Age", "Seq#", "CkSum", "Route\n"])
else
next
end
headline = lines.shift
qta.read_headline(headline,true)
else
entry = qta.read_entry(line)
case mode
when :link_state
db[router]['link_state_area'][router_link_states_area] << entry
when :states
db[router]['external_states'] << entry
end
end
end
# table
end
db
end
def route
lines = @vtysh.execute("show ip ospf route").lines
heading = ''
route = {}
last_line = []
while line = lines.shift
if line[0] == "=" #heading
heading = line.scan(/=* ([^=]*) =*/).first.first
route[heading] = []
else # data
case line.strip
when /N\s+([\d\.\/]+)\s+\[(\d+)\]\s+area:\s(.*)/
last_line = {network: $1, cost: $2.to_i, area: $3, type: 'N'}
route[heading] << last_line
when /N (E(?:\d+) (?:\S+))\s+\[([\d\/]+)\] tag: (\d+)/
last_line = {network: $1, cost: $2, tag: $3.to_i, type: 'N'}
route[heading] << last_line
when /(?:(directly attached) to|via ([^,]+),) (.*)/
last_line[:via] = $1 || $2
last_line[:via_interface] = $3
when /R\s+(\S+)\s+\[(\d+)\] area: ([^,]+)(, ASBR)/
last_line = {ip: $1, cost: $2.to_i, area: $3, asbr: (", ASBR" == $4), type: 'R'}
route[heading] << last_line
else
#puts line
end
end
end
route
end
def overview
lines = @vtysh.execute("show ip ospf").lines
overview = {rfc2328_conform: false, asbr: false}
while line = lines.shift&.strip
case line
when /OSPF Routing Process, Router ID: ([\d\.]+)/
overview[:router_id] = $1
when "This implementation conforms to RFC2328"
overview[:rfc2328_conform] = true
when /OpaqueCapability flag is (\S+)/
overview[:opaque_capability] = ($1 == 'enabled')
when /Initial SPF scheduling delay (\d+) millisec\(s\)/
overview[:initial_spf_scheduling_delay] = $1.to_i
when /(Min|Max)imum hold time between consecutive SPFs (\d+) millisec\(s\)/
overview[:hold_time] ||= {}
overview[:hold_time][$1.downcase] = $2.to_i
when "This router is an ASBR (injecting external routing information)"
overview[:asbr] = true
when /Number of external LSA (\d+). Checksum Sum ([x\d]+)/
overview[:external_lsa] = {count: $1.to_i, checksum: $2}
when /Number of opaque AS LSA (\d+). Checksum Sum ([x\d]+)/
overview[:opaque_as_lsa] = {count: $1.to_i, checksum: $2}
when /Refresh timer (\d+) secs/
overview[:refresh_timer] = $1.to_i
when /Number of areas attached to this router: (\d+)/
overview[:areas_attached_count] = $1.to_i
when /Hold time multiplier is currently (\d+)/
overview[:current_hold_time_multipier] = $1.to_i
when /RFC1583Compatibility flag is (\S+)/
overview[:rfc1583_compatibility] = ($1 == 'enabled')
when /SPF timer is (.*)/
overview[:spf_timer] = $1
when ""
break
else
# debug
#puts line
end
end
# general overview has ended - now the area overviews come
overview[:areas] = {}
current_area = {}
while line = lines.shift&.strip
case line
when /Area ID: (.*)/
current_area = {}
overview[:areas][$1] = current_area
when /Number of interfaces in this area: Total: (\d+), Active: (\d+)/
current_area[:interfaces] = {total: $1.to_i,active: $2.to_i}
when /Number of (router|network|summary) LSA (\d+). Checksum Sum ([\da-fx]+)/
current_area[:lsa] ||= {}
current_area[:lsa][$1] = {count: $2.to_i, checksum: $3}
when /Number of LSA (\d+)/
current_area[:lsa] ||= {}
current_area[:lsa][:count] = $1.to_i
when /Number of (opaque (?:area|link)|NSSA|ASBR summary) LSA (\d+). Checksum Sum ([\da-fx]+)/
current_area[:lsa] ||= {}
current_area[:lsa][$1] = {count: $2.to_i, checksum: $3}
when /Number of fully adjacent neighbors in this area: (\d+)/
current_area[:fully_adjacent_neighbour_count] = $1.to_i
when /SPF algorithm executed (\d) times/
current_area[:spf_exec_count] = $1.to_i
when "Area has no authentication"
current_area[:auth] = "none"
else
#puts line
end
end
overview
end
end
class BGP
def initialize(sh)
@vtysh = sh
end
def overview
output = @vtysh.execute('show ip bgp')
return {} if output.include? "No BGP process is configured"
return {} unless output.include? 'version' # we get an empty output if quagga is not running
output = output.split("\n")
bgp = {}
x,y = output.shift.scan(/.*?version is (\d+).*?ID is ([0-9\.]+).*/).first
bgp['table_version'] = x
bgp['local_router_id'] = y
# find out, what the status abbreviations mean
status_codes = {}
line = output.shift
line.split(":").last.strip.split(",").each do |x|
k,v = x.strip.split(" ")
status_codes[k] = v
end
while line.end_with? ","
line = output.shift
line.strip.split(",").each do |x|
k,v = x.strip.split(" ")
status_codes[k] = v
end
end
# same like before but for the origin codes
origin_codes = {}
output.shift.split(":").last.strip.split(",").each do |x|
k,v = x.strip.split(" - ")
origin_codes[k] = v
end
# drop empty line
output.shift
# read entries
bgp['output'] = []
qta = QuaggaTableReader.new(["Network", "Next Hop", "Metric", "LocPrf", "Weight", "Path"])
qta.read_headline(output.shift,true)
while line = output.shift&.strip
break if line == ''
data = qta.read_entry(line)
data['status'] = data['status'].split("").map {|x| {dn: status_codes[x], abb: x} }
data['Path'] = data['Path'].split("").map {|x| {dn: origin_codes[x], abb: x} }
bgp['output'] << data
end
bgp
end
end
require 'optparse'
options = {}
supported_sections = %w{general ospf}
OptionParser.new do |opts|
opts.banner = "Usage: #{__FILE__} -s section [section specific params]"
opts.on("-d", "--ospf-database") do |od|
options[:ospf_database] = od
end
opts.on("-r", "--ospf-route", 'print OSPF routing table') do |od|
options[:ospf_route] = od
end
opts.on("-i", "--ospf-interface", 'print OSPF interface information') do |od|
options[:ospf_interface] = od
end
opts.on("-n", "--ospf-neighbor", 'Print OSPF neighbors') do |od|
options[:ospf_neighbors] = od
end
opts.on("-o", "--ospf-overview", "Print OSPF summary") do |od|
options[:ospf_overview] = od
end
opts.on("-R", "--general-routes", "Print Routing Table") do |od|
options[:general_routes] = od
end
opts.on("-B", "--bgp-overview", "Print an overview of BGP") do |od|
options[:bgp_overview] = od
end
opts.on("-H", "--human-readable", "Print the output human readable (not json)") do |od|
options[:human_readable] = od
end
opts.on("-h", "--help", "Prints this help") do
puts opts
exit
end
end.parse!
# use the lib
sh = VTYSH.new
ospf = OSPF.new sh
bgp = BGP.new sh
general = General.new sh
result = {}
options.keys.each do |k|
# if it is true
if options[k]
begin
if k.to_s.include? 'ospf'
cmd = k.to_s.split('_').last
result[k] = ospf.send(cmd)
elsif k.to_s.include? 'general'
cmd = k.to_s.split('_').last
result[k] = general.send(cmd)
elsif k.to_s.include? 'bgp'
cmd = k.to_s.split('_').last
result[k] = bgp.send(cmd)
end
rescue # do nothing on an error
result[k] = "error"
#puts $!
end
end
end
# ospf.database, general.routes, ospf.interface, ospf.neighbors, ospf.route, ospf.overview
if options[:human_readable]
pp result
else
print result.to_json
end

View file

@ -27,3 +27,39 @@ command:/usr/local/etc/rc.d/quagga status;exit 0
parameters:
type:script_output
message:request quagga
[ospf-database]
command:/usr/local/opnsense/scripts/quagga/quagga.rb --ospf-database
parameters:
type:script_output
message: Shows the OSPF database
[ospf-route]
command:/usr/local/opnsense/scripts/quagga/quagga.rb --ospf-route
parameters:
type:script_output
message: print OSPF routing table
[ospf-interface]
command:/usr/local/opnsense/scripts/quagga/quagga.rb --ospf-interface
parameters:
type:script_output
message: print OSPF interface information
[ospf-neighbor]
command:/usr/local/opnsense/scripts/quagga/quagga.rb --ospf-neighbor
parameters:
type:script_output
message: Print OSPF neighbors
[ospf-overview]
command:/usr/local/opnsense/scripts/quagga/quagga.rb --ospf-overview
parameters:
type:script_output
message: Print OSPF summary
[general-routes]
command:/usr/local/opnsense/scripts/quagga/quagga.rb --general-routes
parameters:
type:script_output
message: Print Routing Table

View file

@ -1,3 +1,4 @@
bgpd.conf:/usr/local/etc/quagga/bgpd.conf
ospfd.conf:/usr/local/etc/quagga/ospfd.conf
ripd.conf:/usr/local/etc/quagga/ripd.conf
quagga:/etc/rc.conf.d/quagga

View file

@ -0,0 +1,37 @@
{% if helpers.exists('OPNsense.quagga.bgp.enabled') and OPNsense.quagga.bgp.enabled == '1' %}
{% from 'OPNsense/Macros/interface.macro' import physical_interface %}
!
! Zebra configuration saved from vty
! 2017/03/03 20:21:04
!
!
!
!
{% if helpers.exists('OPNsense.quagga.bgp.asnumber') and OPNsense.quagga.bgp.asnumber != '' %}
router bgp {{ OPNsense.quagga.bgp.asnumber }}
{% if helpers.exists('OPNsense.quagga.bgp.networks') %}
{% for network in OPNsense.quagga.bgp.networks.split(',') %}
network {{ network }}
{% endfor %}
{% endif %}
{% if helpers.exists('OPNsense.quagga.bgp.neighbors.neighbor') %}
{% for neighbor in helpers.toList('OPNsense.quagga.bgp.neighbors.neighbor') %}
{% if neighbor.enabled == '1' %}
neighbor {{ neighbor.address }} remote-as {{ neighbor.remoteas }}
{% if 'updatesource' in neighbor and neighbor.updatesource != '' %}
neighbor {{ neighbor.address }} update-source {{ physical_interface(neighbor.updatesource) }}
{% endif %}
{% if 'nexthopself' in neighbor and neighbor.nexthopself == '1' %}
neighbor {{ neighbor.address }} next-hop-self
{% endif %}
{% if 'defaultoriginate' in neighbor and neighbor.defaultoriginate == '1' %}
neighbor {{ neighbor.address }} default-originate
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
!
line vty
!
{% endif %}