mirror of
https://github.com/xcat2/confluent.git
synced 2026-07-31 18:19:44 +00:00
Implement confluent2dnsmasqdhcp
confleunt2dnsmasqdhcp creates static DHCP entries for dnsmasq for nodes with defined net.*.hwaddr.
This commit is contained in:
@@ -0,0 +1,488 @@
|
||||
#!/usr/bin/python3
|
||||
# Generate a dnsmasq static DHCP configuration (a file under /etc/dnsmasq.d)
|
||||
# from the confluent attribute database. One "dhcp-host=" reservation is
|
||||
# emitted for every network of every node that has a hardware (MAC) address
|
||||
# defined (net.hwaddr, net.<iface>.hwaddr, net.bmc.hwaddr, ...).
|
||||
|
||||
import argparse
|
||||
import ipaddress
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
TARGET = '/etc/dnsmasq.d/confluent-dhcp.conf'
|
||||
TAGPREFIX = 'cdhcp'
|
||||
# Invocation flags that do not affect the generated content; excluded from the
|
||||
# recorded "Regenerate:" line and from the settings-changed comparison.
|
||||
NONCONFIG_ARGS = {'-y', '--yes', '-n', '--stdout'}
|
||||
|
||||
|
||||
def split_list(val):
|
||||
"""Split a confluent comma/whitespace delimited attribute value."""
|
||||
if not val:
|
||||
return []
|
||||
return [v for v in re.split(r'[\s,]+', val.strip()) if v]
|
||||
|
||||
|
||||
def config_args(argv):
|
||||
"""Join the invocation arguments that affect the generated content."""
|
||||
return ' '.join(a for a in argv if a not in NONCONFIG_ARGS)
|
||||
|
||||
|
||||
def parse_net_attrib(attrib):
|
||||
"""Split a net.* attribute into (network_name, field).
|
||||
|
||||
The field is always the final dotted component; everything between
|
||||
'net.' and the field is the network name, which may itself span
|
||||
several dotted components.
|
||||
"""
|
||||
parts = attrib.split('.')
|
||||
field = parts[-1]
|
||||
if len(parts) == 2:
|
||||
currnet = None
|
||||
else:
|
||||
currnet = '.'.join(parts[1:-1])
|
||||
return currnet, field
|
||||
|
||||
|
||||
def subnet_sort_key(netaddr):
|
||||
try:
|
||||
return tuple(int(o) for o in netaddr.split('.'))
|
||||
except ValueError:
|
||||
return (0, 0, 0, 0)
|
||||
|
||||
|
||||
def local_ip_for_network(network):
|
||||
"""Return this host's source address for `network`, but only when that
|
||||
address is itself within `network` (i.e. the host is directly attached);
|
||||
otherwise None. Uses a connected UDP socket, which performs a routing
|
||||
lookup without sending any packet."""
|
||||
family = socket.AF_INET6 if network.version == 6 else socket.AF_INET
|
||||
try:
|
||||
target = str(next(network.hosts()))
|
||||
except StopIteration:
|
||||
target = str(network.network_address)
|
||||
s = socket.socket(family, socket.SOCK_DGRAM)
|
||||
try:
|
||||
s.connect((target, 9))
|
||||
local = s.getsockname()[0]
|
||||
except OSError:
|
||||
return None
|
||||
finally:
|
||||
s.close()
|
||||
local = local.split('%', 1)[0] # drop any IPv6 zone id
|
||||
try:
|
||||
if ipaddress.ip_address(local) in network:
|
||||
return local
|
||||
except ValueError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def existing_regen_args(targetpath):
|
||||
"""Return the content arguments recorded in an existing config's
|
||||
'Regenerate:' header line, or None if not present/readable."""
|
||||
try:
|
||||
with open(targetpath) as cfg:
|
||||
for line in cfg:
|
||||
if line.startswith('# Regenerate:'):
|
||||
rest = line[len('# Regenerate:'):].strip()
|
||||
marker = 'confluent2dnsmasqdhcp'
|
||||
if rest.startswith(marker):
|
||||
return rest[len(marker):].strip()
|
||||
return rest
|
||||
if not line.startswith('#'):
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(
|
||||
description='Create /etc/dnsmasq.d static DHCP reservations for a '
|
||||
'noderange from the confluent database')
|
||||
ap.add_argument('noderange',
|
||||
help='Noderange to generate DHCP reservations for')
|
||||
ap.add_argument('--target', default=TARGET, metavar='PATH',
|
||||
help='Output configuration file (default: %(default)s).')
|
||||
ap.add_argument('--tag-prefix', default=TAGPREFIX, metavar='PREFIX',
|
||||
help='Prefix for generated dnsmasq tag names '
|
||||
'(default: %(default)s).')
|
||||
ap.add_argument('--no-bind-dynamic', dest='emit_bind_dynamic',
|
||||
action='store_false',
|
||||
help="Do not emit the 'bind-dynamic' line "
|
||||
'(it is emitted by default).')
|
||||
ap.add_argument('--no-listen-address', dest='detect_listen',
|
||||
action='store_false',
|
||||
help='Do not autodetect and emit a listen-address line for '
|
||||
"this host's address on each managed subnet. By "
|
||||
'default they are emitted together with localhost '
|
||||
'(127.0.0.1 and ::1).')
|
||||
ap.add_argument('--no-range', dest='emit_range', action='store_false',
|
||||
help='Do not emit dhcp-range lines (manage ranges '
|
||||
'elsewhere). Note: dnsmasq will not answer DHCP on a '
|
||||
'subnet that has no covering dhcp-range.')
|
||||
ap.add_argument('--lease', default='24h', metavar='TIME',
|
||||
help='Lease time for emitted dhcp-range lines '
|
||||
'(default: 24h; e.g. 1h, 24h, infinite). '
|
||||
'Pass an empty string to omit it.')
|
||||
ap.add_argument('-n', '--stdout', action='store_true',
|
||||
help='Write the configuration to stdout instead of '
|
||||
'writing the target file.')
|
||||
ap.add_argument('-y', '--yes', dest='assume_yes', action='store_true',
|
||||
help='Do not prompt for confirmation when an existing '
|
||||
'config was generated with different settings.')
|
||||
ap.add_argument('--allow-empty', action='store_true',
|
||||
help='Write the file even if no reservations were '
|
||||
'produced (default: refuse, to avoid clobbering an '
|
||||
'existing config on an empty/failed read).')
|
||||
|
||||
# Optional extra data injected into the DHCP reply, sourced from the
|
||||
# confluent database. All are OFF by default and scoped per-subnet.
|
||||
grp = ap.add_argument_group(
|
||||
'optional DHCP reply data (sourced from confluent, off by default)')
|
||||
grp.add_argument('--all-options', action='store_true',
|
||||
help='Enable every optional reply datum listed below.')
|
||||
grp.add_argument('--gateway', '--router', dest='opt_gateway',
|
||||
action='store_true',
|
||||
help='router, option 3, from net.<net>.ipv4_gateway')
|
||||
grp.add_argument('--dns', dest='opt_dns', action='store_true',
|
||||
help='dns-server, option 6, from dns.servers')
|
||||
grp.add_argument('--domain', dest='opt_domain', action='store_true',
|
||||
help='domain-name, option 15, from dns.domain')
|
||||
grp.add_argument('--ntp', dest='opt_ntp', action='store_true',
|
||||
help='ntp-server, option 42, from ntp.servers')
|
||||
grp.add_argument('--mtu', dest='opt_mtu', action='store_true',
|
||||
help='interface-mtu, option 26, from net.<net>.mtu')
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.all_options:
|
||||
args.opt_gateway = True
|
||||
args.opt_dns = True
|
||||
args.opt_domain = True
|
||||
args.opt_ntp = True
|
||||
args.opt_mtu = True
|
||||
|
||||
c = client.Command()
|
||||
|
||||
# Node level attributes
|
||||
domainbynode = {}
|
||||
dnsbynode = {}
|
||||
ntpbynode = {}
|
||||
# Per (node, net) network attributes: nets[node][net] = {field: value}
|
||||
nets = {}
|
||||
|
||||
read_error = False
|
||||
for ent in c.read('/noderange/{0}/attributes/current'.format(
|
||||
args.noderange)):
|
||||
if 'error' in ent:
|
||||
sys.stderr.write(ent['error'] + '\n')
|
||||
read_error = True
|
||||
continue
|
||||
ent = ent.get('databynode', {})
|
||||
for node in ent:
|
||||
for attrib in ent[node]:
|
||||
val = ent[node][attrib]
|
||||
if not isinstance(val, dict):
|
||||
continue
|
||||
val = val.get('value', None)
|
||||
if val in (None, ''):
|
||||
continue
|
||||
if attrib == 'dns.domain':
|
||||
domainbynode[node] = val
|
||||
elif attrib == 'dns.servers':
|
||||
dnsbynode[node] = val
|
||||
elif attrib == 'ntp.servers':
|
||||
ntpbynode[node] = val
|
||||
elif attrib.startswith('net.'):
|
||||
currnet, field = parse_net_attrib(attrib)
|
||||
if field not in ('hwaddr', 'ipv4_address',
|
||||
'ipv4_gateway', 'hostname', 'mtu'):
|
||||
continue
|
||||
nets.setdefault(node, {}).setdefault(currnet, {})
|
||||
nets[node][currnet][field] = val
|
||||
|
||||
# Build reservation entries and aggregate per-subnet information.
|
||||
hostentries = [] # {subnetkey, macs, ip, hostname}
|
||||
subnets = {} # subnetkey (netaddr, prefixlen) -> aggregate dict
|
||||
|
||||
for node in sorted(nets):
|
||||
netnames = sorted(nets[node], key=lambda n: (n is not None, n or ''))
|
||||
for currnet in netnames:
|
||||
slot = nets[node][currnet]
|
||||
hwaddr = slot.get('hwaddr')
|
||||
if not hwaddr:
|
||||
continue
|
||||
label = currnet if currnet else '(primary)'
|
||||
netprefix = (currnet + '.') if currnet else ''
|
||||
addr = slot.get('ipv4_address')
|
||||
if not addr:
|
||||
sys.stderr.write(
|
||||
'{0}: net {1} has a hwaddr but no net.{2}ipv4_address; '
|
||||
'skipping\n'.format(node, label, netprefix))
|
||||
continue
|
||||
ip = addr.split('/', 1)[0]
|
||||
|
||||
# Reservation hostname: first token of the per-net hostname,
|
||||
# else the node name for the primary (unnamed) network, else
|
||||
# omit it (named net with no hostname).
|
||||
hostname = None
|
||||
if slot.get('hostname'):
|
||||
hostname = split_list(slot['hostname'])[0]
|
||||
elif currnet is None:
|
||||
hostname = node
|
||||
|
||||
# Derive the subnet (needed for dhcp-range and for scoping
|
||||
# the optional dhcp-option tags).
|
||||
subnetkey = None
|
||||
if '/' in addr:
|
||||
try:
|
||||
iface = ipaddress.ip_interface(addr)
|
||||
except ValueError as e:
|
||||
sys.stderr.write(
|
||||
'{0}: net {1} has invalid address {2} ({3}); '
|
||||
'skipping\n'.format(node, label, addr, e))
|
||||
continue
|
||||
network = iface.network
|
||||
subnetkey = (str(network.network_address), network.prefixlen)
|
||||
sub = subnets.get(subnetkey)
|
||||
if sub is None:
|
||||
tag = '{0}_{1}_{2}'.format(
|
||||
args.tag_prefix,
|
||||
str(network.network_address).replace('.', '_'),
|
||||
network.prefixlen)
|
||||
sub = {'netmask': str(network.netmask), 'tag': tag,
|
||||
'gateway': set(), 'dns': set(), 'domain': set(),
|
||||
'ntp': set(), 'mtu': set()}
|
||||
subnets[subnetkey] = sub
|
||||
if slot.get('ipv4_gateway'):
|
||||
sub['gateway'].add(slot['ipv4_gateway'])
|
||||
if node in dnsbynode:
|
||||
sub['dns'].add(dnsbynode[node])
|
||||
if node in domainbynode:
|
||||
sub['domain'].add(domainbynode[node])
|
||||
if node in ntpbynode:
|
||||
sub['ntp'].add(ntpbynode[node])
|
||||
if slot.get('mtu'):
|
||||
sub['mtu'].add(slot['mtu'])
|
||||
elif args.emit_range:
|
||||
sys.stderr.write(
|
||||
'{0}: net {1} address {2} has no /prefix; cannot derive a '
|
||||
'subnet for dhcp-range (add a prefix or use --no-range). '
|
||||
'Emitting the dhcp-host anyway.\n'.format(
|
||||
node, label, addr))
|
||||
|
||||
hostentries.append({'subnetkey': subnetkey,
|
||||
'macs': split_list(hwaddr),
|
||||
'ip': ip, 'hostname': hostname})
|
||||
|
||||
# Resolve optional per-subnet options from the aggregated values.
|
||||
def resolve(values, what, subnetkey):
|
||||
if not values:
|
||||
return None
|
||||
if len(values) > 1:
|
||||
chosen = sorted(values)[0]
|
||||
sys.stderr.write(
|
||||
'subnet {0}/{1}: inconsistent {2} across nodes {3}; using '
|
||||
'{4}\n'.format(subnetkey[0], subnetkey[1], what,
|
||||
sorted(values), chosen))
|
||||
return chosen
|
||||
return next(iter(values))
|
||||
|
||||
optionlines = {} # subnetkey -> [ 'option:...,value', ... ]
|
||||
for subnetkey, sub in subnets.items():
|
||||
lines = []
|
||||
if args.opt_gateway:
|
||||
gw = resolve(sub['gateway'], 'net.ipv4_gateway', subnetkey)
|
||||
if gw:
|
||||
lines.append('option:router,{0}'.format(gw))
|
||||
if args.opt_dns:
|
||||
dv = resolve(sub['dns'], 'dns.servers', subnetkey)
|
||||
if dv:
|
||||
lines.append('option:dns-server,{0}'.format(
|
||||
','.join(split_list(dv))))
|
||||
if args.opt_domain:
|
||||
dom = resolve(sub['domain'], 'dns.domain', subnetkey)
|
||||
if dom:
|
||||
lines.append('option:domain-name,{0}'.format(dom))
|
||||
if args.opt_ntp:
|
||||
nv = resolve(sub['ntp'], 'ntp.servers', subnetkey)
|
||||
if nv:
|
||||
lines.append('option:ntp-server,{0}'.format(
|
||||
','.join(split_list(nv))))
|
||||
if args.opt_mtu:
|
||||
mv = resolve(sub['mtu'], 'net.mtu', subnetkey)
|
||||
if mv:
|
||||
lines.append('option:mtu,{0}'.format(mv))
|
||||
if lines:
|
||||
optionlines[subnetkey] = lines
|
||||
|
||||
orderedsubnets = sorted(subnets,
|
||||
key=lambda k: subnet_sort_key(k[0]) + (k[1],))
|
||||
|
||||
# Render.
|
||||
out = ['# Managed by confluent2dnsmasqdhcp -- DO NOT EDIT BY HAND.',
|
||||
'# Regenerate: confluent2dnsmasqdhcp {0}'.format(
|
||||
config_args(sys.argv[1:])),
|
||||
'# Generated {0}'.format(time.strftime('%Y-%m-%d %H:%M:%S %z')),
|
||||
'']
|
||||
if args.emit_bind_dynamic:
|
||||
out.append('# Allow confluent and dnsmasq to share the same network '
|
||||
'for DHCP')
|
||||
out.append('bind-dynamic')
|
||||
out.append('')
|
||||
|
||||
detected = []
|
||||
if args.detect_listen:
|
||||
for subnetkey in orderedsubnets:
|
||||
netaddr, prefixlen = subnetkey
|
||||
try:
|
||||
net = ipaddress.ip_network('{0}/{1}'.format(netaddr, prefixlen))
|
||||
except ValueError:
|
||||
continue
|
||||
lip = local_ip_for_network(net)
|
||||
if lip and lip not in detected:
|
||||
detected.append(lip)
|
||||
out.append('# Listen on localhost plus this host\'s address on each '
|
||||
'managed subnet,')
|
||||
out.append('# so dnsmasq serves these networks alongside confluent '
|
||||
'and bind-dynamic')
|
||||
out.append('# (a listen-address line also overrides local-service in '
|
||||
'dnsmasq.conf).')
|
||||
out.append('listen-address=127.0.0.1')
|
||||
out.append('listen-address=::1')
|
||||
for addr in detected:
|
||||
out.append('listen-address={0}'.format(addr))
|
||||
out.append('')
|
||||
|
||||
hosts_by_subnet = {}
|
||||
nosubnet_hosts = []
|
||||
for e in hostentries:
|
||||
fields = list(e['macs'])
|
||||
if e['subnetkey'] in optionlines:
|
||||
fields.append('set:{0}'.format(subnets[e['subnetkey']]['tag']))
|
||||
fields.append(e['ip'])
|
||||
if e['hostname']:
|
||||
fields.append(e['hostname'])
|
||||
line = 'dhcp-host={0}'.format(','.join(fields))
|
||||
if e['subnetkey'] is None:
|
||||
nosubnet_hosts.append(line)
|
||||
else:
|
||||
hosts_by_subnet.setdefault(e['subnetkey'], []).append(line)
|
||||
|
||||
for subnetkey in orderedsubnets:
|
||||
sub = subnets[subnetkey]
|
||||
netaddr, prefixlen = subnetkey
|
||||
out.append('# subnet {0}/{1}'.format(netaddr, prefixlen))
|
||||
if args.emit_range:
|
||||
rangefields = [netaddr, 'static', sub['netmask']]
|
||||
if args.lease:
|
||||
rangefields.append(args.lease)
|
||||
out.append('dhcp-range={0}'.format(','.join(rangefields)))
|
||||
for optline in optionlines.get(subnetkey, []):
|
||||
out.append('dhcp-option=tag:{0},{1}'.format(sub['tag'], optline))
|
||||
for line in sorted(hosts_by_subnet.get(subnetkey, [])):
|
||||
out.append(line)
|
||||
out.append('')
|
||||
|
||||
if nosubnet_hosts:
|
||||
out.append('# reservations with no derivable subnet (address had no '
|
||||
'/prefix)')
|
||||
out.extend(sorted(nosubnet_hosts))
|
||||
out.append('')
|
||||
|
||||
text = '\n'.join(out).rstrip('\n') + '\n'
|
||||
|
||||
# Warnings about configuration choices that affect coexistence with
|
||||
# confluent's own DHCP service.
|
||||
if args.detect_listen and subnets and not detected:
|
||||
sys.stderr.write(
|
||||
'WARNING: no local address was found on any managed subnet, so '
|
||||
'dnsmasq will only listen on localhost. Run this on the dnsmasq '
|
||||
'host, or set interface/listen-address manually.\n')
|
||||
if not args.detect_listen:
|
||||
sys.stderr.write(
|
||||
'WARNING: listen-address autodetection disabled. For dnsmasq to '
|
||||
'serve these networks together with confluent and bind-dynamic, '
|
||||
'set interface, except-interface or listen-address manually, or '
|
||||
'disable local-service=host in dnsmasq.conf.\n')
|
||||
if not args.emit_bind_dynamic:
|
||||
sys.stderr.write(
|
||||
'WARNING: bind-dynamic not emitted; this can conflict with '
|
||||
"confluent's DHCP unless bind-dynamic is set elsewhere in the "
|
||||
'dnsmasq configuration.\n')
|
||||
|
||||
if args.stdout:
|
||||
sys.stdout.write(text)
|
||||
return
|
||||
|
||||
if not hostentries and not args.allow_empty:
|
||||
sys.stderr.write(
|
||||
'No DHCP reservations were generated (no networks with both a '
|
||||
'hwaddr and an ipv4_address were found){0}. Refusing to overwrite '
|
||||
'{1}; pass --allow-empty to override.\n'.format(
|
||||
' and there were read errors' if read_error else '',
|
||||
args.target))
|
||||
sys.exit(1)
|
||||
|
||||
# If a config already exists that was generated with different
|
||||
# content-affecting settings, confirm before overwriting it.
|
||||
if os.path.exists(args.target):
|
||||
prev = existing_regen_args(args.target)
|
||||
cur = config_args(sys.argv[1:])
|
||||
if prev is not None and prev != cur:
|
||||
sys.stderr.write(
|
||||
'Existing {0} was generated with different settings:\n'
|
||||
' old: confluent2dnsmasqdhcp {1}\n'
|
||||
' new: confluent2dnsmasqdhcp {2}\n'.format(
|
||||
args.target, prev or '(no arguments)',
|
||||
cur or '(no arguments)'))
|
||||
if not args.assume_yes:
|
||||
if not sys.stdin.isatty():
|
||||
sys.stderr.write(
|
||||
'Refusing to regenerate with changed settings '
|
||||
'non-interactively; pass -y/--yes to confirm.\n')
|
||||
sys.exit(1)
|
||||
try:
|
||||
resp = input('Are you sure you want to regenerate using the new settings? [y/N] ')
|
||||
except EOFError:
|
||||
resp = ''
|
||||
if resp.strip().lower() not in ('y', 'yes'):
|
||||
sys.stderr.write(
|
||||
'Aborted; {0} left unchanged.\n'.format(args.target))
|
||||
sys.exit(1)
|
||||
|
||||
targetdir = os.path.dirname(args.target)
|
||||
targetname = os.path.basename(args.target)
|
||||
if targetdir and not os.path.isdir(targetdir):
|
||||
os.makedirs(targetdir, exist_ok=True)
|
||||
# Write via a hidden temp file in the same directory, then rename: the
|
||||
# rename is atomic, and the dot-prefix keeps a dnsmasq conf-dir from
|
||||
# loading the temp file if it scans mid-write.
|
||||
tmp = os.path.join(targetdir, '.' + targetname + '.tmp')
|
||||
with open(tmp, 'w') as cfg:
|
||||
cfg.write(text)
|
||||
os.replace(tmp, args.target)
|
||||
sys.stderr.write('Wrote {0} reservation(s) across {1} subnet(s) to {2}\n'
|
||||
.format(len(hostentries), len(subnets), args.target))
|
||||
sys.stderr.write('Be sure to restart dnsmasq for the changes to take effect\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,238 @@
|
||||
confluent2dnsmasqdhcp(8) -- Generate dnsmasq static DHCP reservations for nodes
|
||||
===============================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`confluent2dnsmasqdhcp [options] <noderange>`
|
||||
`confluent2dnsmasqdhcp <noderange>`
|
||||
`confluent2dnsmasqdhcp --all-options <noderange>`
|
||||
`confluent2dnsmasqdhcp -n <noderange>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`confluent2dnsmasqdhcp` generates a dnsmasq configuration fragment of static
|
||||
DHCP reservations for a noderange, using the confluent database as the source
|
||||
of truth. Each `net.<network>.<attribute>` group is pulled together and one
|
||||
`dhcp-host=` reservation is emitted for **every network that has a hardware
|
||||
address** (`net.hwaddr`, `net.bmc.hwaddr`, `net.eth.mynetwork.hwaddr`, and so
|
||||
on; the network component may itself contain several dotted parts). Networks
|
||||
without a `hwaddr` (for example InfiniBand interfaces identified only by a
|
||||
GUID) are skipped.
|
||||
|
||||
For each reservation, `net.<network>.ipv4_address` supplies the reserved
|
||||
address. The `/prefixlen` suffix on that address is used to compute the
|
||||
enclosing subnet, and one `dhcp-range=<network>,static,<netmask>,<lease>` line
|
||||
is emitted per distinct subnet. dnsmasq will not answer DHCP requests on a
|
||||
subnet that has no covering `dhcp-range`, even when every address on it is
|
||||
statically reserved, so these ranges are emitted by default (see `--no-range`).
|
||||
|
||||
The reservation name is the first token of `net.<network>.hostname`, falling
|
||||
back to the node name itself for the primary (unnamed) network, and omitted for
|
||||
a named network that has no hostname. Only IPv4 node addresses are used for
|
||||
reservations and ranges.
|
||||
|
||||
By default the fragment begins with a `bind-dynamic` directive so that dnsmasq
|
||||
can bind DHCP to networks confluent is using, too (see `--no-bind-dynamic`).
|
||||
|
||||
Also by default, a `listen-address` line is emitted for `127.0.0.1`, for
|
||||
`::1`, and for this host's own address on each managed subnet (see
|
||||
`--no-listen-address`). The per-subnet addresses are detected from the host
|
||||
the command runs on, so run it on the dnsmasq host; only subnets the host is
|
||||
directly attached to are added. Besides telling dnsmasq where to listen, the
|
||||
presence of any `listen-address` line disables the default `local-service`
|
||||
(or `local-service=host`) restriction in `dnsmasq.conf` that would otherwise
|
||||
limit dnsmasq to localhost.
|
||||
|
||||
Additional data can optionally be added to the DHCP reply from the confluent
|
||||
database (see OPTIONS; all are off by default). Each such option is scoped to
|
||||
the subnet it belongs to using a dnsmasq tag: the `dhcp-host` lines for that
|
||||
subnet are given a `set:<tag>` and the corresponding `dhcp-option` lines a
|
||||
matching `tag:<tag>`. Values are aggregated per subnet; if two nodes on the
|
||||
same subnet disagree (for example two different gateways), a warning is printed
|
||||
and the first value is used.
|
||||
|
||||
The configuration is written to `/etc/dnsmasq.d/confluent-dhcp.conf` (override
|
||||
with `--target`) and regenerated on each run, replacing the file atomically.
|
||||
If the existing file was generated with different content-affecting arguments
|
||||
(the preview and confirmation flags `-n` and `-y` are ignored for this
|
||||
comparison), you are asked to confirm before it is overwritten; the prompt is
|
||||
skipped with `-y` and is refused when there is no controlling terminal. If a
|
||||
run produces no reservations at all (for example an empty noderange or a failed
|
||||
read), `confluent2dnsmasqdhcp` refuses to overwrite the existing file unless
|
||||
`--allow-empty` is given.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `--target PATH`:
|
||||
Path of the configuration file to write. Defaults to
|
||||
`/etc/dnsmasq.d/confluent-dhcp.conf`.
|
||||
|
||||
* `--tag-prefix PREFIX`:
|
||||
Prefix used when generating dnsmasq tag names for per-subnet options.
|
||||
Defaults to `cdhcp`, yielding tags such as `cdhcp_10_28_104_0_21`.
|
||||
|
||||
* `--no-bind-dynamic`:
|
||||
Do not emit the `bind-dynamic` line. It is emitted by default. Note that
|
||||
`bind-dynamic` is a global dnsmasq directive and must not be combined with
|
||||
a `bind-interfaces` directive set elsewhere. Without it, dnsmasq may conflict
|
||||
with confluent's own DHCP unless `bind-dynamic` is set elsewhere in the
|
||||
dnsmasq configuration; a warning is printed.
|
||||
|
||||
* `--no-listen-address`:
|
||||
Do not autodetect or emit `listen-address` lines. By default a
|
||||
`listen-address` line is emitted for `127.0.0.1`, for `::1`, and for this
|
||||
host's address on each managed subnet. With this flag you must set
|
||||
`interface`, `except-interface`, or `listen-address` yourself (or disable
|
||||
`local-service`/`local-service=host` in `dnsmasq.conf`) for dnsmasq to serve
|
||||
the cluster networks alongside confluent; a warning is printed as a reminder.
|
||||
|
||||
* `--no-range`:
|
||||
Do not emit `dhcp-range` lines, leaving range and subnet declarations to be
|
||||
managed elsewhere. Remember that dnsmasq will not serve DHCP on a subnet
|
||||
that has no covering `dhcp-range`. Per-subnet options are still scoped via
|
||||
host tags, so they keep working with a range you declare yourself.
|
||||
|
||||
* `--lease TIME`:
|
||||
Lease time applied to the emitted `dhcp-range` lines. Defaults to `24h`.
|
||||
Accepts any dnsmasq lease syntax (for example `1h`, `24h`, `infinite`).
|
||||
Pass an empty string to omit the lease field entirely.
|
||||
|
||||
* `-n`, `--stdout`:
|
||||
Write the configuration to standard output instead of the target file, which
|
||||
is left untouched. Useful for previewing.
|
||||
|
||||
* `-y`, `--yes`:
|
||||
Do not prompt for confirmation when the existing configuration was generated
|
||||
with different settings; regenerate anyway. Without it a settings change is
|
||||
confirmed interactively, and refused when there is no controlling terminal.
|
||||
|
||||
* `--allow-empty`:
|
||||
Write the file even when no reservations were produced. By default an empty
|
||||
result does not overwrite the existing file, to avoid discarding a good
|
||||
configuration after an empty or failed read.
|
||||
|
||||
* `--all-options`:
|
||||
Enable all of the optional reply-data options below at once.
|
||||
|
||||
* `--gateway`, `--router`:
|
||||
Add a router (DHCP option 3) per subnet, taken from
|
||||
`net.<network>.ipv4_gateway`.
|
||||
|
||||
* `--dns`:
|
||||
Add DNS servers (DHCP option 6) per subnet, taken from `dns.servers`.
|
||||
|
||||
* `--domain`:
|
||||
Add a domain name (DHCP option 15) per subnet, taken from `dns.domain`.
|
||||
|
||||
* `--ntp`:
|
||||
Add NTP servers (DHCP option 42) per subnet, taken from `ntp.servers`.
|
||||
|
||||
* `--mtu`:
|
||||
Add an interface MTU (DHCP option 26) per subnet, taken from
|
||||
`net.<network>.mtu`.
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show a help message and exit.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Generate reservations for a noderange and write the default file:
|
||||
`# confluent2dnsmasqdhcp everything`
|
||||
|
||||
* Preview the configuration for one node without writing anything:
|
||||
`# confluent2dnsmasqdhcp -n node01`
|
||||
|
||||
* Include all optional reply data (gateway, DNS, domain, NTP, MTU):
|
||||
`# confluent2dnsmasqdhcp --all-options everything`
|
||||
|
||||
* Include only the gateway, and use an eight hour lease:
|
||||
`# confluent2dnsmasqdhcp --gateway --lease 8h everything`
|
||||
|
||||
* Regenerate non-interactively (for example from cron) after changing options, without the confirmation prompt:
|
||||
`# confluent2dnsmasqdhcp --all-options -y everything`
|
||||
|
||||
* Emit only reservations (no ranges) to a custom file kept beside your own range and listen interface definitions:
|
||||
`# confluent2dnsmasqdhcp --no-listen-address --no-range --target /etc/dnsmasq.d/confluent-reservations.conf everything`
|
||||
|
||||
## GENERATED CONFIGURATION
|
||||
|
||||
Given the following confluent attributes for node `node01`:
|
||||
|
||||
node01: net.hwaddr: 10:ff:e0:af:af:f5
|
||||
node01: net.ipv4_address: 10.28.90.1/21
|
||||
node01: net.ipv4_gateway: 10.28.88.1
|
||||
node01: net.bmc.hwaddr: 10:ff:e0:a4:cf:b6
|
||||
node01: net.bmc.hostname: node01-bmc
|
||||
node01: net.bmc.ipv4_address: 10.28.106.1/21
|
||||
node01: net.bmc.ipv4_gateway: 10.28.104.1
|
||||
node01: net.ib0.hostname: node01-ib0
|
||||
node01: net.ib0.ipv4_address: 10.28.101.1/21
|
||||
|
||||
`confluent2dnsmasqdhcp node01`, run on a dnsmasq host whose own addresses on the
|
||||
two managed subnets are `10.28.88.250` and `10.28.104.250`, writes the file
|
||||
below. The `ib0` network is skipped because it has no `hwaddr`; the two
|
||||
remaining networks each yield a subnet (with its `dhcp-range`) and a
|
||||
reservation, and a `listen-address` line is emitted for localhost and for this
|
||||
host's address on each subnet:
|
||||
|
||||
# Managed by confluent2dnsmasqdhcp -- DO NOT EDIT BY HAND.
|
||||
# Regenerate: confluent2dnsmasqdhcp node01
|
||||
# Generated 2026-06-28 12:00:00 +0000
|
||||
|
||||
# Allow confluent and dnsmasq to share the same network for DHCP
|
||||
bind-dynamic
|
||||
|
||||
# Listen on localhost plus this host's address on each managed subnet,
|
||||
# so dnsmasq serves these networks alongside confluent and bind-dynamic
|
||||
# (a listen-address line also overrides local-service in dnsmasq.conf).
|
||||
listen-address=127.0.0.1
|
||||
listen-address=::1
|
||||
listen-address=10.28.88.250
|
||||
listen-address=10.28.104.250
|
||||
|
||||
# subnet 10.28.88.0/21
|
||||
dhcp-range=10.28.88.0,static,255.255.248.0,24h
|
||||
dhcp-host=10:ff:e0:af:af:f5,10.28.90.1,node01
|
||||
|
||||
# subnet 10.28.104.0/21
|
||||
dhcp-range=10.28.104.0,static,255.255.248.0,24h
|
||||
dhcp-host=10:ff:e0:a4:cf:b6,10.28.106.1,node01-bmc
|
||||
|
||||
Adding `--gateway` scopes a router option to each subnet through a tag, and the
|
||||
matching `dhcp-host` lines gain a `set:` tag so the option reaches them:
|
||||
|
||||
# subnet 10.28.88.0/21
|
||||
dhcp-range=10.28.88.0,static,255.255.248.0,24h
|
||||
dhcp-option=tag:cdhcp_10_28_88_0_21,option:router,10.28.88.1
|
||||
dhcp-host=10:ff:e0:af:af:f5,set:cdhcp_10_28_88_0_21,10.28.90.1,node01
|
||||
|
||||
# subnet 10.28.104.0/21
|
||||
dhcp-range=10.28.104.0,static,255.255.248.0,24h
|
||||
dhcp-option=tag:cdhcp_10_28_104_0_21,option:router,10.28.104.1
|
||||
dhcp-host=10:ff:e0:a4:cf:b6,set:cdhcp_10_28_104_0_21,10.28.106.1,node01-bmc
|
||||
|
||||
## NOTES
|
||||
|
||||
`bind-dynamic` is a global dnsmasq option; if it is set here it must not be
|
||||
contradicted by a `bind-interfaces` directive elsewhere in the configuration.
|
||||
|
||||
The autodetected `listen-address` lines double as the mechanism that relaxes
|
||||
dnsmasq's default `local-service`/`local-service=host` restriction (any
|
||||
`interface`, `except-interface`, or `listen-address` line does), which is what
|
||||
lets dnsmasq answer on the cluster networks instead of only localhost.
|
||||
Detection only covers subnets the host running the command is directly attached
|
||||
to, so run it there; with `--no-listen-address` you must supply `interface`,
|
||||
`except-interface`, or `listen-address` yourself, or disable `local-service`.
|
||||
|
||||
After (re)generating the file, validate it with `dnsmasq --test` and restart
|
||||
dnsmasq (for example `systemctl restart dnsmasq`); a reload (SIGHUP) does not
|
||||
re-read `listen-address` or interface bindings.
|
||||
|
||||
## FILES
|
||||
|
||||
* `/etc/dnsmasq.d/confluent-dhcp.conf`:
|
||||
Default output file (override with `--target`).
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodeattrib(8), noderange(5), dnsmasq(8)
|
||||
Reference in New Issue
Block a user