From 926bdd64090eba7f0318c036d5737524780622e6 Mon Sep 17 00:00:00 2001 From: Kaarle Ritvanen Date: Tue, 17 Dec 2013 16:56:44 +0200 Subject: net module: support for editing /etc/network/interfaces --- acf2/modules/net.lua | 240 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 239 insertions(+), 1 deletion(-) diff --git a/acf2/modules/net.lua b/acf2/modules/net.lua index f0da354..f3a9fc0 100644 --- a/acf2/modules/net.lua +++ b/acf2/modules/net.lua @@ -5,6 +5,9 @@ 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'} @@ -20,13 +23,248 @@ 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 -- cgit v1.2.3