luci-mod-network: Add PXE Boot options tab under DHCP and DNS
[project/luci.git] / modules / luci-mod-network / htdocs / luci-static / resources / view / network / dhcp.js
1 'use strict';
2 'require view';
3 'require dom';
4 'require poll';
5 'require rpc';
6 'require uci';
7 'require form';
8 'require network';
9 'require validation';
10 'require tools.widgets as widgets';
11
12 var callHostHints, callDUIDHints, callDHCPLeases, CBILeaseStatus, CBILease6Status;
13
14 callHostHints = rpc.declare({
15 object: 'luci-rpc',
16 method: 'getHostHints',
17 expect: { '': {} }
18 });
19
20 callDUIDHints = rpc.declare({
21 object: 'luci-rpc',
22 method: 'getDUIDHints',
23 expect: { '': {} }
24 });
25
26 callDHCPLeases = rpc.declare({
27 object: 'luci-rpc',
28 method: 'getDHCPLeases',
29 expect: { '': {} }
30 });
31
32 CBILeaseStatus = form.DummyValue.extend({
33 renderWidget: function(section_id, option_id, cfgvalue) {
34 return E([
35 E('h4', _('Active DHCP Leases')),
36 E('table', { 'id': 'lease_status_table', 'class': 'table' }, [
37 E('tr', { 'class': 'tr table-titles' }, [
38 E('th', { 'class': 'th' }, _('Hostname')),
39 E('th', { 'class': 'th' }, _('IPv4 address')),
40 E('th', { 'class': 'th' }, _('MAC address')),
41 E('th', { 'class': 'th' }, _('Lease time remaining'))
42 ]),
43 E('tr', { 'class': 'tr placeholder' }, [
44 E('td', { 'class': 'td' }, E('em', _('Collecting data...')))
45 ])
46 ])
47 ]);
48 }
49 });
50
51 CBILease6Status = form.DummyValue.extend({
52 renderWidget: function(section_id, option_id, cfgvalue) {
53 return E([
54 E('h4', _('Active DHCPv6 Leases')),
55 E('table', { 'id': 'lease6_status_table', 'class': 'table' }, [
56 E('tr', { 'class': 'tr table-titles' }, [
57 E('th', { 'class': 'th' }, _('Host')),
58 E('th', { 'class': 'th' }, _('IPv6 address')),
59 E('th', { 'class': 'th' }, _('DUID')),
60 E('th', { 'class': 'th' }, _('Lease time remaining'))
61 ]),
62 E('tr', { 'class': 'tr placeholder' }, [
63 E('td', { 'class': 'td' }, E('em', _('Collecting data...')))
64 ])
65 ])
66 ]);
67 }
68 });
69
70 function calculateNetwork(addr, mask) {
71 addr = validation.parseIPv4(String(addr));
72
73 if (!isNaN(mask))
74 mask = validation.parseIPv4(network.prefixToMask(+mask));
75 else
76 mask = validation.parseIPv4(String(mask));
77
78 if (addr == null || mask == null)
79 return null;
80
81 return [
82 [
83 addr[0] & (mask[0] >>> 0 & 255),
84 addr[1] & (mask[1] >>> 0 & 255),
85 addr[2] & (mask[2] >>> 0 & 255),
86 addr[3] & (mask[3] >>> 0 & 255)
87 ].join('.'),
88 mask.join('.')
89 ];
90 }
91
92 function getDHCPPools() {
93 return uci.load('dhcp').then(function() {
94 let sections = uci.sections('dhcp', 'dhcp'),
95 tasks = [], pools = [];
96
97 for (var i = 0; i < sections.length; i++) {
98 if (sections[i].ignore == '1' || !sections[i].interface)
99 continue;
100
101 tasks.push(network.getNetwork(sections[i].interface).then(L.bind(function(section_id, net) {
102 var cidr = net ? (net.getIPAddrs()[0] || '').split('/') : null;
103
104 if (cidr && cidr.length == 2) {
105 var net_mask = calculateNetwork(cidr[0], cidr[1]);
106
107 pools.push({
108 section_id: section_id,
109 network: net_mask[0],
110 netmask: net_mask[1]
111 });
112 }
113 }, null, sections[i]['.name'])));
114 }
115
116 return Promise.all(tasks).then(function() {
117 return pools;
118 });
119 });
120 }
121
122 function validateHostname(sid, s) {
123 if (s == null || s == '')
124 return true;
125
126 if (s.length > 256)
127 return _('Expecting: %s').format(_('valid hostname'));
128
129 var labels = s.replace(/^\.+|\.$/g, '').split(/\./);
130
131 for (var i = 0; i < labels.length; i++)
132 if (!labels[i].match(/^[a-z0-9_](?:[a-z0-9-]{0,61}[a-z0-9])?$/i))
133 return _('Expecting: %s').format(_('valid hostname'));
134
135 return true;
136 }
137
138 function validateAddressList(sid, s) {
139 if (s == null || s == '')
140 return true;
141
142 var m = s.match(/^\/(.+)\/$/),
143 names = m ? m[1].split(/\//) : [ s ];
144
145 for (var i = 0; i < names.length; i++) {
146 var res = validateHostname(sid, names[i]);
147
148 if (res !== true)
149 return res;
150 }
151
152 return true;
153 }
154
155 function validateServerSpec(sid, s) {
156 if (s == null || s == '')
157 return true;
158
159 var m = s.match(/^(?:\/(.+)\/)?(.*)$/);
160 if (!m)
161 return _('Expecting: %s').format(_('valid hostname'));
162
163 var res = validateAddressList(sid, m[1]);
164 if (res !== true)
165 return res;
166
167 if (m[2] == '' || m[2] == '#')
168 return true;
169
170 // ipaddr%scopeid#srvport@source@interface#srcport
171
172 m = m[2].match(/^([0-9a-f:.]+)(?:%[^#@]+)?(?:#(\d+))?(?:@([0-9a-f:.]+)(?:@[^#]+)?(?:#(\d+))?)?$/);
173
174 if (!m)
175 return _('Expecting: %s').format(_('valid IP address'));
176
177 if (validation.parseIPv4(m[1])) {
178 if (m[3] != null && !validation.parseIPv4(m[3]))
179 return _('Expecting: %s').format(_('valid IPv4 address'));
180 }
181 else if (validation.parseIPv6(m[1])) {
182 if (m[3] != null && !validation.parseIPv6(m[3]))
183 return _('Expecting: %s').format(_('valid IPv6 address'));
184 }
185 else {
186 return _('Expecting: %s').format(_('valid IP address'));
187 }
188
189 if ((m[2] != null && +m[2] > 65535) || (m[4] != null && +m[4] > 65535))
190 return _('Expecting: %s').format(_('valid port value'));
191
192 return true;
193 }
194
195 function validateMACAddr(pools, sid, s) {
196 if (s == null || s == '')
197 return true;
198
199 var leases = uci.sections('dhcp', 'host'),
200 this_macs = L.toArray(s).map(function(m) { return m.toUpperCase() });
201
202 for (var i = 0; i < pools.length; i++) {
203 var this_net_mask = calculateNetwork(this.section.formvalue(sid, 'ip'), pools[i].netmask);
204
205 if (!this_net_mask)
206 continue;
207
208 for (var j = 0; j < leases.length; j++) {
209 if (leases[j]['.name'] == sid || !leases[j].ip)
210 continue;
211
212 var lease_net_mask = calculateNetwork(leases[j].ip, pools[i].netmask);
213
214 if (!lease_net_mask || this_net_mask[0] != lease_net_mask[0])
215 continue;
216
217 var lease_macs = L.toArray(leases[j].mac).map(function(m) { return m.toUpperCase() });
218
219 for (var k = 0; k < lease_macs.length; k++)
220 for (var l = 0; l < this_macs.length; l++)
221 if (lease_macs[k] == this_macs[l])
222 return _('The MAC address %h is already used by another static lease in the same DHCP pool').format(this_macs[l]);
223 }
224 }
225
226 return true;
227 }
228
229 return view.extend({
230 load: function() {
231 return Promise.all([
232 callHostHints(),
233 callDUIDHints(),
234 getDHCPPools()
235 ]);
236 },
237
238 render: function(hosts_duids_pools) {
239 var has_dhcpv6 = L.hasSystemFeature('dnsmasq', 'dhcpv6') || L.hasSystemFeature('odhcpd'),
240 hosts = hosts_duids_pools[0],
241 duids = hosts_duids_pools[1],
242 pools = hosts_duids_pools[2],
243 m, s, o, ss, so;
244
245 m = new form.Map('dhcp', _('DHCP and DNS'),
246 _('Dnsmasq is a lightweight <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> server and <abbr title="Domain Name System">DNS</abbr> forwarder.'));
247
248 s = m.section(form.TypedSection, 'dnsmasq');
249 s.anonymous = true;
250 s.addremove = false;
251
252 s.tab('general', _('General Settings'));
253 s.tab('files', _('Resolv and Hosts Files'));
254 s.tab('pxe_tftp', _('PXE/TFTP Settings'));
255 s.tab('advanced', _('Advanced Settings'));
256 s.tab('leases', _('Static Leases'));
257 s.tab('hosts', _('Hostnames'));
258 s.tab('ipsets', _('IP Sets'));
259
260 s.taboption('general', form.Flag, 'domainneeded',
261 _('Domain required'),
262 _('Do not forward DNS queries without dots or domain parts.'));
263
264 s.taboption('general', form.Flag, 'authoritative',
265 _('Authoritative'),
266 _('This is the only DHCP server in the local network.'));
267
268 s.taboption('general', form.Value, 'local',
269 _('Local server'),
270 _('Never forward matching domains and subdomains, resolve from DHCP or hosts files only.'));
271
272 s.taboption('general', form.Value, 'domain',
273 _('Local domain'),
274 _('Local domain suffix appended to DHCP names and hosts file entries.'));
275
276 o = s.taboption('general', form.Flag, 'logqueries',
277 _('Log queries'),
278 _('Write received DNS queries to syslog.'));
279 o.optional = true;
280
281 o = s.taboption('general', form.DynamicList, 'server',
282 _('DNS forwardings'),
283 _('List of upstream resolvers to forward queries to.'));
284 o.optional = true;
285 o.placeholder = '/example.org/10.1.2.3';
286 o.validate = validateServerSpec;
287
288 o = s.taboption('general', form.DynamicList, 'address',
289 _('Addresses'),
290 _('List of domains to force to an IP address.'));
291 o.optional = true;
292 o.placeholder = '/router.local/192.168.0.1';
293
294 o = s.taboption('general', form.DynamicList, 'ipset',
295 _('IP sets'),
296 _('List of IP sets to populate with the specified domain IPs.'));
297 o.optional = true;
298 o.placeholder = '/example.org/ipset,ipset6';
299
300 o = s.taboption('general', form.Flag, 'rebind_protection',
301 _('Rebind protection'),
302 _('Discard upstream RFC1918 responses.'));
303 o.rmempty = false;
304
305 o = s.taboption('general', form.Flag, 'rebind_localhost',
306 _('Allow localhost'),
307 _('Exempt <code>127.0.0.0/8</code> and <code>::1</code> from rebinding checks, e.g. for RBL services.'));
308 o.depends('rebind_protection', '1');
309
310 o = s.taboption('general', form.DynamicList, 'rebind_domain',
311 _('Domain whitelist'),
312 _('List of domains to allow RFC1918 responses for.'));
313 o.depends('rebind_protection', '1');
314 o.optional = true;
315 o.placeholder = 'ihost.netflix.com';
316 o.validate = validateAddressList;
317
318 o = s.taboption('general', form.Flag, 'localservice',
319 _('Local service only'),
320 _('Accept DNS queries only from hosts whose address is on a local subnet.'));
321 o.optional = false;
322 o.rmempty = false;
323
324 o = s.taboption('general', form.Flag, 'nonwildcard',
325 _('Non-wildcard'),
326 _('Bind dynamically to interfaces rather than wildcard address.'));
327 o.default = o.enabled;
328 o.optional = false;
329 o.rmempty = true;
330
331 o = s.taboption('general', form.DynamicList, 'interface',
332 _('Listen interfaces'),
333 _('Listen only on the specified interfaces, and loopback if not excluded explicitly.'));
334 o.optional = true;
335 o.placeholder = 'lan';
336
337 o = s.taboption('general', form.DynamicList, 'notinterface',
338 _('Exclude interfaces'),
339 _('Do not listen on the specified interfaces.'));
340 o.optional = true;
341 o.placeholder = 'loopback';
342
343 s.taboption('files', form.Flag, 'readethers',
344 _('Use <code>/etc/ethers</code>'),
345 _('Read <code>/etc/ethers</code> to configure the DHCP server.'));
346
347 s.taboption('files', form.Value, 'leasefile',
348 _('Lease file'),
349 _('File to store DHCP lease information.'));
350
351 o = s.taboption('files', form.Flag, 'noresolv',
352 _('Ignore resolv file'));
353 o.optional = true;
354
355 o = s.taboption('files', form.Value, 'resolvfile',
356 _('Resolv file'),
357 _('File with upstream resolvers.'));
358 o.depends('noresolv', '0');
359 o.placeholder = '/tmp/resolv.conf.d/resolv.conf.auto';
360 o.optional = true;
361
362 o = s.taboption('files', form.Flag, 'nohosts',
363 _('Ignore <code>/etc/hosts</code>'));
364 o.optional = true;
365
366 o = s.taboption('files', form.DynamicList, 'addnhosts',
367 _('Additional hosts files'));
368 o.optional = true;
369 o.placeholder = '/etc/dnsmasq.hosts';
370
371 o = s.taboption('advanced', form.Flag, 'quietdhcp',
372 _('Suppress logging'),
373 _('Suppress logging of the routine operation for the DHCP protocol.'));
374 o.optional = true;
375
376 o = s.taboption('advanced', form.Flag, 'sequential_ip',
377 _('Allocate IPs sequentially'),
378 _('Allocate IP addresses sequentially, starting from the lowest available address.'));
379 o.optional = true;
380
381 o = s.taboption('advanced', form.Flag, 'boguspriv',
382 _('Filter private'),
383 _('Do not forward reverse lookups for local networks.'));
384 o.default = o.enabled;
385
386 s.taboption('advanced', form.Flag, 'filterwin2k',
387 _('Filter useless'),
388 _('Do not forward queries that cannot be answered by public resolvers.'));
389
390 s.taboption('advanced', form.Flag, 'localise_queries',
391 _('Localise queries'),
392 _('Return answers to DNS queries matching the subnet from which the query was received if multiple IPs are available.'));
393
394 if (L.hasSystemFeature('dnsmasq', 'dnssec')) {
395 o = s.taboption('advanced', form.Flag, 'dnssec',
396 _('DNSSEC'),
397 _('Validate DNS replies and cache DNSSEC data, requires upstream to support DNSSEC.'));
398 o.optional = true;
399
400 o = s.taboption('advanced', form.Flag, 'dnsseccheckunsigned',
401 _('DNSSEC check unsigned'),
402 _('Verify unsigned domain responses really come from unsigned domains.'));
403 o.default = o.enabled;
404 o.optional = true;
405 }
406
407 s.taboption('advanced', form.Flag, 'expandhosts',
408 _('Expand hosts'),
409 _('Add local domain suffix to names served from hosts files.'));
410
411 s.taboption('advanced', form.Flag, 'nonegcache',
412 _('No negative cache'),
413 _('Do not cache negative replies, e.g. for non-existent domains.'));
414
415 o = s.taboption('advanced', form.Value, 'serversfile',
416 _('Additional servers file'),
417 _('File listing upstream resolvers, optionally domain-specific, e.g. <code>server=1.2.3.4</code>, <code>server=/domain/1.2.3.4</code>.'));
418 o.placeholder = '/etc/dnsmasq.servers';
419
420 o = s.taboption('advanced', form.Flag, 'strictorder',
421 _('Strict order'),
422 _('Upstream resolvers will be queried in the order of the resolv file.'));
423 o.optional = true;
424
425 o = s.taboption('advanced', form.Flag, 'allservers',
426 _('All servers'),
427 _('Query all available upstream resolvers.'));
428 o.optional = true;
429
430 o = s.taboption('advanced', form.DynamicList, 'bogusnxdomain',
431 _('IPs to override with NXDOMAIN'),
432 _('List of IP addresses to convert into NXDOMAIN responses.'));
433 o.optional = true;
434 o.placeholder = '64.94.110.11';
435
436 o = s.taboption('advanced', form.Value, 'port',
437 _('DNS server port'),
438 _('Listening port for inbound DNS queries.'));
439 o.optional = true;
440 o.datatype = 'port';
441 o.placeholder = 53;
442
443 o = s.taboption('advanced', form.Value, 'queryport',
444 _('DNS query port'),
445 _('Fixed source port for outbound DNS queries.'));
446 o.optional = true;
447 o.datatype = 'port';
448 o.placeholder = _('any');
449
450 o = s.taboption('advanced', form.Value, 'dhcpleasemax',
451 _('Max. DHCP leases'),
452 _('Maximum allowed number of active DHCP leases.'));
453 o.optional = true;
454 o.datatype = 'uinteger';
455 o.placeholder = _('unlimited');
456
457 o = s.taboption('advanced', form.Value, 'ednspacket_max',
458 _('Max. EDNS0 packet size'),
459 _('Maximum allowed size of EDNS0 UDP packets.'));
460 o.optional = true;
461 o.datatype = 'uinteger';
462 o.placeholder = 1280;
463
464 o = s.taboption('advanced', form.Value, 'dnsforwardmax',
465 _('Max. concurrent queries'),
466 _('Maximum allowed number of concurrent DNS queries.'));
467 o.optional = true;
468 o.datatype = 'uinteger';
469 o.placeholder = 150;
470
471 o = s.taboption('advanced', form.Value, 'cachesize',
472 _('Size of DNS query cache'),
473 _('Number of cached DNS entries, 10000 is maximum, 0 is no caching.'));
474 o.optional = true;
475 o.datatype = 'range(0,10000)';
476 o.placeholder = 150;
477
478 o = s.taboption('pxe_tftp', form.Flag, 'enable_tftp',
479 _('Enable TFTP server'),
480 _('Enable the built-in single-instance TFTP server.'));
481 o.optional = true;
482
483 o = s.taboption('pxe_tftp', form.Value, 'tftp_root',
484 _('TFTP server root'),
485 _('Root directory for files served via TFTP.' +
486 '<br><code>Enable TFTP server</code> and <code>TFTP server root</code> turn on the TFTP server and serve files from <code>TFTP server root</code>.'));
487 o.depends('enable_tftp', '1');
488 o.optional = true;
489 o.placeholder = '/';
490
491 o = s.taboption('pxe_tftp', form.Value, 'dhcp_boot',
492 _('Network boot image'),
493 _('Filename of the boot image advertised to clients.'));
494 o.depends('enable_tftp', '1');
495 o.optional = true;
496 o.placeholder = 'pxelinux.0';
497
498 /* PXE - https://openwrt.org/docs/guide-user/base-system/dhcp#booting_options */
499 o = s.taboption('pxe_tftp', form.SectionValue, '__pxe__', form.GridSection, 'boot', null,
500 _('Special <abbr title="Preboot eXecution Environment">PXE</abbr> boot options for Dnsmasq.'));
501 ss = o.subsection;
502 ss.addremove = true;
503 ss.anonymous = true;
504 ss.nodescriptions = true;
505
506 so = ss.option(form.Value, 'filename',
507 _('Filename'),
508 _('Host requests this filename from the boot server.'));
509 so.optional = false;
510 so.placeholder = 'pxelinux.0';
511
512 so = ss.option(form.Value, 'servername',
513 _('Server name'),
514 _('The hostname of the boot server'));
515 so.optional = false;
516 so.placeholder = 'myNAS';
517
518 so = ss.option(form.Value, 'serveraddress',
519 _('Server address'),
520 _('The IP address of the boot server'));
521 so.optional = false;
522 so.placeholder = '192.168.1.2';
523
524 so = ss.option(form.DynamicList, 'dhcp_option',
525 _('DHCP Options'),
526 _('Options for the Network-ID. (Note: needs also Network-ID.) E.g. "<code>42,192.168.1.4</code>" for NTP server, "<code>3,192.168.4.4</code>" for default route. <code>0.0.0.0</code> means "the address of the system running dnsmasq".'));
527 so.optional = true;
528 so.placeholder = '42,192.168.1.4';
529
530 so = ss.option(widgets.DeviceSelect, 'networkid',
531 _('Network-ID'),
532 _('Apply DHCP Options to this net. (Empty = all clients).'));
533 so.optional = true;
534 so.noaliases = true;
535
536 so = ss.option(form.Flag, 'force',
537 _('Force'),
538 _('Always send DHCP Options. Sometimes needed, with e.g. PXELinux.'));
539 so.optional = true;
540
541 so = ss.option(form.Value, 'instance',
542 _('Instance'),
543 _('Dnsmasq instance to which this boot section is bound. If unspecified, the section is valid for all dnsmasq instances.'));
544 so.optional = true;
545
546 Object.values(L.uci.sections('dhcp', 'dnsmasq')).forEach(function(val, index) {
547 so.value(index, '%s (Domain: %s, Local: %s)'.format(index, val.domain || '?', val.local || '?'));
548 });
549
550 o = s.taboption('hosts', form.SectionValue, '__hosts__', form.GridSection, 'domain', null,
551 _('Hostnames are used to bind a domain name to an IP address. This setting is redundant for hostnames already configured with static leases, but it can be useful to rebind an FQDN.'));
552
553 ss = o.subsection;
554
555 ss.addremove = true;
556 ss.anonymous = true;
557 ss.sortable = true;
558
559 so = ss.option(form.Value, 'name', _('Hostname'));
560 so.rmempty = false;
561 so.datatype = 'hostname';
562
563 so = ss.option(form.Value, 'ip', _('IP address'));
564 so.rmempty = false;
565 so.datatype = 'ipaddr';
566
567 var ipaddrs = {};
568
569 Object.keys(hosts).forEach(function(mac) {
570 var addrs = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4);
571
572 for (var i = 0; i < addrs.length; i++)
573 ipaddrs[addrs[i]] = hosts[mac].name || mac;
574 });
575
576 L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
577 so.value(ipv4, '%s (%s)'.format(ipv4, ipaddrs[ipv4]));
578 });
579
580 o = s.taboption('ipsets', form.SectionValue, '__ipsets__', form.GridSection, 'ipset', null,
581 _('List of IP sets to populate with the specified domain IPs.'));
582
583 ss = o.subsection;
584
585 ss.addremove = true;
586 ss.anonymous = true;
587 ss.sortable = true;
588
589 so = ss.option(form.DynamicList, 'name', _('IP set'));
590 so.rmempty = false;
591 so.datatype = 'string';
592
593 so = ss.option(form.DynamicList, 'domain', _('Domain'));
594 so.rmempty = false;
595 so.datatype = 'hostname';
596
597 o = s.taboption('leases', form.SectionValue, '__leases__', form.GridSection, 'host', null,
598 _('Static leases are used to assign fixed IP addresses and symbolic hostnames to DHCP clients. They are also required for non-dynamic interface configurations where only hosts with a corresponding lease are served.') + '<br />' +
599 _('Use the <em>Add</em> Button to add a new lease entry. The <em>MAC address</em> identifies the host, the <em>IPv4 address</em> specifies the fixed address to use, and the <em>Hostname</em> is assigned as a symbolic name to the requesting host. The optional <em>Lease time</em> can be used to set non-standard host-specific lease time, e.g. 12h, 3d or infinite.'));
600
601 ss = o.subsection;
602
603 ss.addremove = true;
604 ss.anonymous = true;
605 ss.sortable = true;
606
607 so = ss.option(form.Value, 'name', _('Hostname'));
608 so.validate = validateHostname;
609 so.rmempty = true;
610 so.write = function(section, value) {
611 uci.set('dhcp', section, 'name', value);
612 uci.set('dhcp', section, 'dns', '1');
613 };
614 so.remove = function(section) {
615 uci.unset('dhcp', section, 'name');
616 uci.unset('dhcp', section, 'dns');
617 };
618
619 so = ss.option(form.Value, 'mac', _('MAC address'));
620 so.datatype = 'list(macaddr)';
621 so.rmempty = true;
622 so.cfgvalue = function(section) {
623 var macs = L.toArray(uci.get('dhcp', section, 'mac')),
624 result = [];
625
626 for (var i = 0, mac; (mac = macs[i]) != null; i++)
627 if (/^([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2})$/.test(mac))
628 result.push('%02X:%02X:%02X:%02X:%02X:%02X'.format(
629 parseInt(RegExp.$1, 16), parseInt(RegExp.$2, 16),
630 parseInt(RegExp.$3, 16), parseInt(RegExp.$4, 16),
631 parseInt(RegExp.$5, 16), parseInt(RegExp.$6, 16)));
632
633 return result.length ? result.join(' ') : null;
634 };
635 so.renderWidget = function(section_id, option_index, cfgvalue) {
636 var node = form.Value.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]),
637 ipopt = this.section.children.filter(function(o) { return o.option == 'ip' })[0];
638
639 node.addEventListener('cbi-dropdown-change', L.bind(function(ipopt, section_id, ev) {
640 var mac = ev.detail.value.value;
641 if (mac == null || mac == '' || !hosts[mac])
642 return;
643
644 var iphint = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
645 if (iphint == null)
646 return;
647
648 var ip = ipopt.formvalue(section_id);
649 if (ip != null && ip != '')
650 return;
651
652 var node = ipopt.map.findElement('id', ipopt.cbid(section_id));
653 if (node)
654 dom.callClassMethod(node, 'setValue', iphint);
655 }, this, ipopt, section_id));
656
657 return node;
658 };
659 so.validate = validateMACAddr.bind(so, pools);
660 Object.keys(hosts).forEach(function(mac) {
661 var hint = hosts[mac].name || L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
662 so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
663 });
664
665 so = ss.option(form.Value, 'ip', _('IPv4 address'));
666 so.datatype = 'or(ip4addr,"ignore")';
667 so.validate = function(section, value) {
668 var m = this.section.formvalue(section, 'mac'),
669 n = this.section.formvalue(section, 'name');
670
671 if ((m == null || m == '') && (n == null || n == ''))
672 return _('One of hostname or MAC address must be specified!');
673
674 if (value == null || value == '' || value == 'ignore')
675 return true;
676
677 var leases = uci.sections('dhcp', 'host');
678
679 for (var i = 0; i < leases.length; i++)
680 if (leases[i]['.name'] != section && leases[i].ip == value)
681 return _('The IP address %h is already used by another static lease').format(value);
682
683 for (var i = 0; i < pools.length; i++) {
684 var net_mask = calculateNetwork(value, pools[i].netmask);
685
686 if (net_mask && net_mask[0] == pools[i].network)
687 return true;
688 }
689
690 return _('The IP address is outside of any DHCP pool address range');
691 };
692
693 L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
694 so.value(ipv4, ipaddrs[ipv4] ? '%s (%s)'.format(ipv4, ipaddrs[ipv4]) : ipv4);
695 });
696
697 so = ss.option(form.Value, 'leasetime', _('Lease time'));
698 so.rmempty = true;
699
700 so = ss.option(form.Value, 'duid', _('DUID'));
701 so.datatype = 'and(rangelength(20,36),hexstring)';
702 Object.keys(duids).forEach(function(duid) {
703 so.value(duid, '%s (%s)'.format(duid, duids[duid].hostname || duids[duid].macaddr || duids[duid].ip6addr || '?'));
704 });
705
706 so = ss.option(form.Value, 'hostid', _('IPv6 suffix (hex)'));
707
708 o = s.taboption('leases', CBILeaseStatus, '__status__');
709
710 if (has_dhcpv6)
711 o = s.taboption('leases', CBILease6Status, '__status6__');
712
713 return m.render().then(function(mapEl) {
714 poll.add(function() {
715 return callDHCPLeases().then(function(leaseinfo) {
716 var leases = Array.isArray(leaseinfo.dhcp_leases) ? leaseinfo.dhcp_leases : [],
717 leases6 = Array.isArray(leaseinfo.dhcp6_leases) ? leaseinfo.dhcp6_leases : [];
718
719 cbi_update_table(mapEl.querySelector('#lease_status_table'),
720 leases.map(function(lease) {
721 var exp;
722
723 if (lease.expires === false)
724 exp = E('em', _('unlimited'));
725 else if (lease.expires <= 0)
726 exp = E('em', _('expired'));
727 else
728 exp = '%t'.format(lease.expires);
729
730 return [
731 lease.hostname || '?',
732 lease.ipaddr,
733 lease.macaddr,
734 exp
735 ];
736 }),
737 E('em', _('There are no active leases')));
738
739 if (has_dhcpv6) {
740 cbi_update_table(mapEl.querySelector('#lease6_status_table'),
741 leases6.map(function(lease) {
742 var exp;
743
744 if (lease.expires === false)
745 exp = E('em', _('unlimited'));
746 else if (lease.expires <= 0)
747 exp = E('em', _('expired'));
748 else
749 exp = '%t'.format(lease.expires);
750
751 var hint = lease.macaddr ? hosts[lease.macaddr] : null,
752 name = hint ? (hint.name || L.toArray(hint.ipaddrs || hint.ipv4)[0] || L.toArray(hint.ip6addrs || hint.ipv6)[0]) : null,
753 host = null;
754
755 if (name && lease.hostname && lease.hostname != name && lease.ip6addr != name)
756 host = '%s (%s)'.format(lease.hostname, name);
757 else if (lease.hostname)
758 host = lease.hostname;
759 else if (name)
760 host = name;
761
762 return [
763 host || '-',
764 lease.ip6addrs ? lease.ip6addrs.join(' ') : lease.ip6addr,
765 lease.duid,
766 exp
767 ];
768 }),
769 E('em', _('There are no active leases')));
770 }
771 });
772 });
773
774 return mapEl;
775 });
776 }
777 });