summaryrefslogtreecommitdiffstats
path: root/acf2/modules/network.lua
diff options
context:
space:
mode:
Diffstat (limited to 'acf2/modules/network.lua')
-rw-r--r--acf2/modules/network.lua270
1 files changed, 270 insertions, 0 deletions
diff --git a/acf2/modules/network.lua b/acf2/modules/network.lua
new file mode 100644
index 0000000..32b9d20
--- /dev/null
+++ b/acf2/modules/network.lua
@@ -0,0 +1,270 @@
+--[[
+Copyright (c) 2012-2014 Kaarle Ritvanen
+See LICENSE file for license details
+--]]
+
+local M = require('acf2.model')
+
+local posix = require('posix')
+
+
+local Host = M.new()
+Host.address = M.net.IPAddress{addr='ipaddr'}
+Host.canonical = M.String{ui_name='Canonical name'}
+Host.alias = M.Set{
+ type=M.String, addr='alias/#', ui_name='Aliases', ui_member='Alias'
+}
+
+local Resolv = M.new()
+Resolv.servers = M.List{
+ type=M.net.IPAddress, addr='nameserver/#', widget='inline'
+}
+Resolv.search_domains = M.List{
+ type=M.String, addr='search/domain/#', widget='inline'
+}
+
+
+local iface_aug_addr = '/augeas/etc/network/interfaces'
+local iface_sys_dir = '/sys/class/net'
+local vlan_pattern = '^(.+)%.(%d+)$'
+
+for _, trigger in ipairs{
+ {phase='pre', action='stop'}, {phase='post', action='start'}
+} do
+ M.trigger(
+ trigger.phase,
+ iface_aug_addr,
+ function() os.execute('rc-service networking '..trigger.action) end
+ )
+end
+
+M.defer(iface_aug_addr)
+
+
+-- TODO allow multiple addresses of same family
+
+local IPv4 = M.new()
+IPv4.method = M.String{
+ required=true,
+ default='manual',
+ choice={{'dhcp', 'DHCP'}, {'loopback', enabled=false}, 'manual', 'static'}
+}
+IPv4.address = M.net.IPv4Address{
+ condition={method='static'}, required=true, cidr=true, mask_addr='netmask'
+}
+IPv4.gateway = M.net.IPv4Address{condition={method='static'}}
+
+
+local IPv6 = M.new()
+IPv6.method = M.String{
+ required=true,
+ choice={{'loopback', enabled=false}, 'manual', 'static'},
+ default='manual'
+}
+IPv6.address = M.net.IPv6Address{
+ condition={method='static'}, required=true, cidr=true, mask_addr='netmask'
+}
+IPv6.gateway = M.net.IPv6Address{condition={method='static'}}
+
+
+
+local Interface = M.new()
+
+function Interface:validate()
+ if self.status == 'attached' then
+ for _, version in ipairs{4, 6} do
+ self['ipv'..version].method =
+ self.class == 'loopback' and 'loopback' or 'manual'
+ end
+ end
+end
+
+function Interface:is_removable() return self.class == 'logical' end
+
+function Interface:auto_set()
+ for _, set in M.node.ipairs(self:fetch('../../enabled-ifaces')) do
+ if M.node.contains(set, self) then return set end
+ end
+end
+
+function Interface:auto_vlan_tag()
+ local name = M.node.name(self)
+ local _, tag = name:match(vlan_pattern)
+ if tag then return tag end
+ return name:match('^vlan(%d+)$')
+end
+
+Interface.enabled = M.Boolean{
+ compute=function(self, iface) return iface:auto_set() and true or false end,
+ store=function(self, iface, value)
+ local set = iface:auto_set()
+ if value and not set then
+ M.node.insert(iface:fetch('../../enabled-ifaces/1'), iface)
+ elseif not value and set then set[iface] = nil end
+ end
+}
+
+Interface.class = M.String{
+ compute=function(self, iface, txn)
+ local name = M.node.name(iface)
+ if name == 'lo' then return 'loopback' end
+
+ local saddr = M.path.rawjoin('/files', iface_sys_dir, name, '.')
+ if not txn:get(saddr) then return 'logical' end
+
+ for _, addr in ipairs{
+ {saddr, 'bonding'},
+ {saddr, 'bridge'},
+ {'/files/proc/net/vlan', name}
+ } do
+ if txn:get(M.path.join(unpack(addr))) then
+ return 'logical'
+ end
+ end
+ return 'physical'
+ end,
+ choice={'loopback', 'logical', 'physical'}
+}
+
+Interface.type = M.String{
+ condition={class='logical'},
+ compute=function(self, iface)
+ if #iface.slaves > 0 then return 'bond' end
+ if #iface.ports > 0 then return 'bridge' end
+ if iface.vlan_tag then return 'vlan' end
+ end,
+ editable=function(self, iface) return not iface:auto_vlan_tag() end,
+ required=true,
+ choice={'bond', 'bridge', {'vlan', 'VLAN'}}
+}
+
+Interface.status = M.String{
+ visible=false,
+ compute=function(self, obj)
+ if obj.class == 'loopback' then return 'attached' end
+
+ for _, iface in M.node.pairs(M.node.parent(obj)) do
+ if (
+ iface.type == 'bond' and M.node.contains(iface.slaves, obj)
+ ) or (
+ iface.type == 'bridge' and M.node.contains(iface.ports, obj)
+ ) then
+ return 'attached'
+ end
+
+ if iface.type == 'vlan' and iface.trunk == obj then
+ return 'configured'
+ end
+ end
+
+ for _, version in ipairs{4, 6} do
+ if obj['ipv'..version].method ~= 'manual' then
+ return 'configured'
+ end
+ end
+
+ return 'detached'
+ end
+}
+
+Interface.slaves = M.Set{
+ condition={type='bond'},
+ type=M.Reference{scope='../..', filter={status='detached'}},
+ required=true,
+ addr='@family/link/@method/none/bond-slaves'
+}
+Interface.ports = M.Set{
+ condition={type='bridge'},
+ type=M.Reference{scope='../..', filter={status='detached'}},
+ required=true,
+ addr='@family/link/@method/none/bridge-ports'
+}
+-- TODO do not allow VLAN creation for non-existent interfaces
+Interface.trunk = M.Reference{
+ condition={type='vlan'},
+ compute=function(self, iface)
+ local trunk = M.node.name(iface):match(vlan_pattern)
+ if trunk and iface:fetch('..')[trunk] then return trunk end
+ end,
+ required=true,
+ scope='..',
+ filter={status={'detached', 'configured'}},
+ addr='@family/link/@method/none/vlan-raw-device'
+}
+-- TODO ensure that (trunk, tag) is unique
+Interface.vlan_tag = M.Integer{
+ condition={type='vlan'},
+ compute='auto_vlan_tag',
+ required=true,
+ min=0,
+ max=4095,
+ addr='@family/link/@method/none/vlan-id',
+ ui_name='VLAN tag'
+}
+
+Interface.ipv4 = M.Model{
+ model=IPv4,
+ condition={status={'detached', 'configured'}},
+ create=true,
+ addr='@family/inet',
+ ui_name='IPv4 configuration',
+ widget='inline'
+}
+Interface.ipv6 = M.Model{
+ model=IPv6,
+ condition={status={'detached', 'configured'}},
+ create=true,
+ addr='@family/inet6',
+ ui_name='IPv6 configuration',
+ widget='inline'
+}
+
+Interface.stats = M.Collection{
+ type=M.Number{editable=false},
+ editable=false,
+ addr=function(path)
+ return M.path.join(
+ '/files/sys/class/net', M.path.name(path), 'statistics'
+ )
+ end,
+ ui_name='Statistics',
+ ui_member='',
+ widget='inline'
+}
+
+
+local Net = M.new()
+Net.host_name = M.String{addr='/augeas/etc/hostname/hostname'}
+Net.hosts = M.List{type=Host, addr='/augeas/etc/hosts', layout='tabular'}
+Net.resolver = M.Model{
+ model=Resolv, addr='/augeas/etc/resolv.conf', ui_name='DNS resolver'
+}
+
+Net.enabled_ifaces = M.List{
+ type=M.Set{type=M.Reference{scope='../../interfaces', on_delete='set-null'}},
+ visible=false,
+ addr=iface_aug_addr..'/auto/#'
+}
+Net.interfaces = M.Collection{
+ type=Interface, addr=iface_aug_addr..'/iface/@', widget='inline'
+}
+
+M.register('net', Net, {ui_name='Network'})
+M.permission.defaults('/net')
+
+return function(txn)
+ local ifaces = txn:fetch('/net/interfaces')
+ for _, name in ipairs(posix.dir(iface_sys_dir)) do
+ if not ifaces[name] and posix.stat(
+ M.path.join(iface_sys_dir, name), 'type'
+ ) == 'link' then
+ ifaces[name] = {}
+ if ifaces[name].class == 'logical' then ifaces[name] = nil
+ else
+ for _, version in ipairs{4, 6} do
+ ifaces[name]['ipv'..version].method = 'manual'
+ end
+ end
+ end
+ end
+ end