require 'puppet/util/network_device/base'
require 'puppet/util/network_device/mikrotik/facts'
class Puppet::Util::NetworkDevice::Mikrotik::Device < Puppet::Util::NetworkDevice::Base
def initialize(url)
Puppet.debug("Puppet::Util::NetDevice::Mikrotik::Device:initialize: #{url}")
super(url)
transport.default_prompt = /\[[^@\[\]]+@[^@\[\]]+\]\s>\s\z/n
ObjectSpace.define_finalizer(self, self.class.method(:disconnect).to_proc)
end
def connect
transport.connect unless @transport_connected
@transport_connected = true
end
def self.disconnect
transport.close
end
def command(cmd = nil)
connect
out = execute(cmd) if cmd
yield self if block_given?
#connect
out
end
def execute(cmd)
transport.command(cmd)
end
def facts
@facts ||= Puppet::Util::NetworkDevice::Mikrotik::Facts.new(transport)
facts = {}
command do |ng|
facts = @facts.retrieve
end
facts
end
def parse_firewall_addresslists(family)
lists = {}
lines = []
pos = ''
flag = ''
comment = ''
execute("/#{family} firewall address-list print detail without-paging\r").split(/[\n\r]+/).each do |l|
case l
when /^\s*(\d+)\s([X\s])\s;;;\s(.*)\s*$/
pos = $1
flag = $2
comment = $3
when /^\s*list=(\S+)\saddress=([^ ]+)\s*$/
lines << "#{pos} #{flag} comment=#{comment} list=#{$1} address=#{$2}"
when /.*/
lines << l
end
end
lines.each do |l|
case l
when /^\s*(\d+)\s([X\s])\slist=(\S+)\saddress=([^ ]+)\s*$/
name = $3
lists[name] = {:listname => name} unless lists[name]
lists[name]['pos'] = {} unless lists[name]['pos']
lists[name]['pos'][$1] = $4
newdis = ($2 == 'X') ? 'yes' : 'no'
lists[name][:disabled] = (lists[name][:disabled] and (newdis != lists[name][:disabled])) ? 'maybe' : newdis
(lists[name][:address] ||= []) << $4
lists[name][:address].sort!
when /^\s*(\d+)\s([X\s])\scomment=(.*)\slist=(\S+)\saddress=([^ ]+)\s*$/
name = $4
lists[name] = {:listname => name} unless lists[name]
lists[name]['pos'] = {} unless lists[name]['pos']
lists[name]['pos'][$1] = $5
newdis = ($2 == 'X') ? 'yes' : 'no'
lists[name][:disabled] = (lists[name][:disabled] and (newdis != lists[name][:disabled])) ? 'maybe' : newdis
lists[name][:comment] = $3
(lists[name][:address] ||= []) << $5
lists[name][:address].sort!
end
end
lists
end
def update_firewall_addresslist(family, listname, is = {}, should = {})
lists = parse_firewall_addresslists(family) || {}
if should[:ensure] == :absent
Puppet.info "Removing address list #{listname}"
cmd = "/#{family} firewall address-list remove "
cmd += lists[listname]['pos'].keys.sort_by(&:to_i).reverse.join(',')
Puppet.debug("update_#{family}_firewall_addresslist: #{cmd}")
execute("#{cmd}\r")
return
end
addr_is = [is[:address]].flatten.sort
addr_should = [should[:address]].flatten.sort
if lists[listname]
Puppet.info "Updating address list #{listname} (#{addr_should.join(',')})"
if (is[:comment] != should[:comment]) or (is[:disabled] != should[:disabled])
cmd = "/#{family} firewall address-list set"
cmd += " comment=\"#{should[:comment]}\"" unless is[:comment] == should[:comment]
cmd += " disabled=#{should[:disabled]}" unless is[:disabled] == should[:disabled]
cmd += " numbers=#{lists[listname]['pos'].keys.join(',')}"
Puppet.debug("update_#{family}_firewall_addresslist: #{cmd}")
execute("#{cmd}\r")
end
if !(addr_is == addr_should)
(addr_should - addr_is).each do |address|
cmd = "/#{family} firewall address-list add list=#{listname}"
cmd += " disabled=#{should[:disabled]}" if should[:disabled]
cmd += " comment=\"#{should[:comment]}\"" if should[:comment]
cmd += " address=#{address}"
Puppet.debug("update_#{family}_firewall_addresslist: #{cmd}")
execute("#{cmd}\r")
end
if (addr_is - addr_should).size > 0
cmd = "/#{family} firewall address-list remove "
cmd += lists[listname]['pos'].select{|k,v| (addr_is - addr_should).include? v}.collect{|x| x.first}.sort_by(&:to_i).reverse.join(',')
Puppet.debug("update_#{family}_firewall_addresslist: #{cmd}")
execute("#{cmd}\r")
end
end
else
Puppet.info "Creating address list #{listname} (#{addr_should.join(',')})"
addr_should.each do |address|
cmd = "/#{family} firewall address-list add list=#{listname}"
cmd += " disabled=#{should[:disabled]}" if should[:disabled]
cmd += " comment=\"#{should[:comment]}\"" if should[:comment]
cmd += " address=#{address}"
Puppet.debug("update_#{family}_firewall_addresslist: #{cmd}")
execute("#{cmd}\r")
end
end
end
def parse_items(cmd, flags_regex, prop_mapping, name_property=:name, array_properties=[])
items = {}
lines = []
lastline = ''
execute("#{cmd}\r").split(/[\n\r]+/).each do |l|
case l
when /^\s*(\d+)\s(#{flags_regex})\s;;;\s(.*)\s*$/
lines << lastline
lastline = "#{$1} #{$2} comment=\"#{$3}\""
when /^\s*(\d+)\s(#{flags_regex})\s(.*)\s*$/
lines << lastline
lastline = "#{$1} #{$2} #{$3}"
when /\[[^@\[\]]+@[^@\[\]]+\]\s>\s/
#transport.default_prompt
# ignore the prompt
lines << lastline
lastline = ''
when /.*/
lastline = "#{lastline} #{l}"
end
end
lines << lastline
lines.each do |l|
case l
when /^\s*(\d+)\s(#{flags_regex})\s+(.*?)\s*$/
item = {'pos' => $1}
flags = $2
properties = $3
# parse_flags with custom code, item is skipped if code does not return anything
o = yield(flags) if block_given?
next unless o or !block_given?
item.merge!(o) if o
until properties == '' do
break unless properties
case properties
when /^([a-z0-9-]+)="([^"]*)"\s*(.*?)\s*$/
propkey=$1
propval=$2
properties = $3
when /^([a-z0-9-]+)=([^ ]*)\s*(.*?)\s*$/
propkey=$1
propval=$2
properties = $3
when /^\s*$/
properties = ''
when /^[^=]+?(?:\s+(.*?))?\s*$/
properties = $1
end
next unless prop_mapping[propkey]
if array_properties.include? propkey
item[prop_mapping[propkey]] = propval.split(',')
else
item[prop_mapping[propkey]] = propval
end
end
next unless item[name_property]
items[item[name_property]] = item
end
end
items
end
def update_item(label, items, cmd_prefix, prop_mapping, name, is={}, should={}, name_property=:name)
if should[:ensure] == :absent
Puppet.info "Removing #{label} #{name}"
cmd = "#{cmd_prefix} remove #{items[name]['pos']}"
Puppet.debug("update_item(#{label}): #{cmd}")
execute("#{cmd}\r")
return
end
if items[name]
Puppet.info "Updating #{label} #{name}"
cmd = "#{cmd_prefix} set numbers=#{items[name]['pos']}"
else
Puppet.info "Creating #{label} #{name}"
cmd = "#{cmd_prefix} add"
end
should[name_property] = name unless should[name_property] # make sure we always have a name (defaults to name)
prop_mapping.each do |l,k|
next unless should[k]
next unless should[k] != is[k]
val = [should[k]].flatten.join(',')
val = "\"#{val}\"" if val =~/[\s=]/
cmd += " #{l}=#{val}"
end
Puppet.debug("update_item(#{label}): #{cmd}")
execute("#{cmd}\r")
end
VLAN_PROPERTIES = {
'name' => :name,
'arp' => :arp,
'comment' => :comment,
'disabled' => :disabled,
'interface' => :interface,
'l2mtu' => :l2mtu,
'mtu' => :mtu,
'use-service-tag' => :useservicetag,
'vlan-id' => :vlanid,
}
def parse_interface_vlans
parse_items('/interface vlan print detail without-paging', /[\sXRS]{2}/, VLAN_PROPERTIES) do |flags|
{ :disabled => (flags =~ /X/) ? 'yes' : 'no' }
end
end
def update_interface_vlan(name, is = {}, should = {})
update_item('VLAN', parse_interface_vlans(), '/interface vlan', VLAN_PROPERTIES, name, is, should)
end
ADDRESS_PROPERTIES = {
'address' => :address,
'advertise' => :advertise,
'comment' => :comment,
'disabled' => :disabled,
'eui-64' => :eui64,
'from-pool' => :frompool,
'interface' => :interface,
'network' => :network,
'netmask' => :netmask,
'broadcast' => :broadcast,
}
def parse_addresses(family)
parse_items("/#{family} address print detail without-paging", /[\sXID][GL]?/, ADDRESS_PROPERTIES, :address) do |flags|
{ :disabled => (flags =~ /X/) ? 'yes' : 'no' } unless flags =~ /D/ # ignore dynamic addresses
end
end
def update_address(family, address, is={}, should={})
update_item("#{family} address", parse_addresses(family), "/#{family} address", ADDRESS_PROPERTIES, address, is, should, :address)
end
ROUTE_PROPERTIES = {
'bgp-as-path' => :bgpaspath,
'bgp-atomic-aggregate' => :bgpatomicaggregate,
'bgp-communities' => :bgpcommunities,
'bgp-local-pref' => :bgplocalpref,
'bgp-med' => :bgpmed,
'bgp-origin' => :bgporigin,
'bgp-prepend' => :bgpprepend,
'check-gateway' => :checkgateway,
'comment' => :comment,
'disabled' => :disabled,
'distance' => :distance,
'dst-address' => :route,
'gateway' => :gateway,
'route-tag' => :routetag,
'scope' => :scope,
'target-scope' => :targetscope,
'type' => :type,
'pref-src' => :prefsrc,
'routing-mark' => :routingmark,
'vrf-interface' => :vrfinterface,
}
def parse_routes(family)
parse_items("/#{family} route print detail without-paging", /[\sXADCSrobmPUB]{4}/, ROUTE_PROPERTIES, :route, ['gateway','bgp-communities']) do |flags|
{
:disabled => (flags =~ /X/) ? 'yes' : 'no',
:type => (flags =~ /U/) ? 'unreachable' :
((flags =~ /P/) ? 'prohibit' :
((flags =~ /B/) ? 'blackhole' : 'unicast')),
} unless flags =~ /[DC]/ # ignore dynamic & connected routes
end
end
def update_route(family, route, is={}, should={})
update_item("#{family} route", parse_routes(family), "/#{family} route", ROUTE_PROPERTIES, route, is, should, :route)
end
TUNNEL_PROPERTIES = {
'name' => :name,
'comment' => :comment,
'disabled' => :disabled,
'local-address' => :localaddress,
'mtu' => :mtu,
'remote-address' => :remoteaddress,
}
def parse_interface_6to4s
parse_items('/interface 6to4 print detail without-paging', /[\sXR]{2}/, TUNNEL_PROPERTIES) do |flags|
{ :disabled => (flags =~ /X/) ? 'yes' : 'no' }
end
end
def update_interface_6to4(name, is = {}, should = {})
update_item('6to4 tunnel', parse_interface_6to4s(), '/interface 6to4', TUNNEL_PROPERTIES, name, is, should)
end
LEASE_PROPERTIES = {
'address' => :address,
'comment' => :comment,
'disabled' => :disabled,
'address-list' => :addresslist,
'always-broadcast' => :alwaysbroadcast,
'block-access' => :blockaccess,
'client-id' => :clientid,
'lease-time' => :leasetime,
'mac-address' => :macaddress,
'rate-limit' => :ratelimit,
'server' => :server,
'use-src-mac' => :usesrcmac,
}
def parse_ip_dhcpserver_leases
parse_items('/ip dhcp-server lease print detail without-paging', /[\sXRDB]/, LEASE_PROPERTIES, :address) do |flags|
{ :disabled => (flags =~ /X/) ? 'yes' : 'no' } unless flags =~ /D/ # ignore dynamic leases
end
end
def update_ip_dhcpserver_lease(address, is = {}, should = {})
update_item('DHCP lease', parse_ip_dhcpserver_leases(), '/ip dhcp-server lease', LEASE_PROPERTIES, address, is, should, :address)
end
OVPNCLIENT_PROPERTIES = {
'name' => :name,
'comment' => :comment,
'disabled' => :disabled,
'add-default-route' => :adddefaultroute,
'auth' => :auth,
'certificate' => :certificate,
'cipher' => :cipher,
'connect-to' => :connectto,
'mac-address' => :macaddress,
'max-mtu' => :maxmtu,
'mode' => :mode,
'password' => :password,
'port' => :port,
'profile' => :profile,
'user' => :user,
}
def parse_interface_ovpnclients
parse_items('/interface ovpn-client print detail without-paging', /[\sXR]{2}/, OVPNCLIENT_PROPERTIES, :name) do |flags|
{ :disabled => (flags =~ /X/) ? 'yes' : 'no' }
end
end
def update_interface_ovpnclient(name, is = {}, should = {})
update_item('OVPN client', parse_interface_ovpnclients(), '/interface ovpn-client', OVPNCLIENT_PROPERTIES, name, is, should, :name)
end
end