diff --git a/lib/nameserver/store/file.js b/lib/nameserver/store/file.js index 7088a0a..587a125 100644 --- a/lib/nameserver/store/file.js +++ b/lib/nameserver/store/file.js @@ -29,11 +29,7 @@ class NameserverRepoFile extends NameserverBase { if ([null, undefined].includes(r[f])) r[f] = '' } - // Ensure the nested export object is always present and well-formed. - // Unlike MySQL (which joins nt_nameserver_export_type), TOML stores the - // type name inline, so no translation is needed. if (!r.export || typeof r.export !== 'object') r.export = {} - if ([null, undefined].includes(r.export.type)) r.export.type = '' if ([null, undefined].includes(r.export.interval)) r.export.interval = 0 if ([null, undefined].includes(r.export.status)) r.export.status = '' r.export.serials = Boolean(r.export.serials) diff --git a/lib/nameserver/store/mysql.js b/lib/nameserver/store/mysql.js index 911cba5..8ef94ad 100644 --- a/lib/nameserver/store/mysql.js +++ b/lib/nameserver/store/mysql.js @@ -21,16 +21,7 @@ class Nameserver extends NameserverBase { if (g.length === 1) return g[0].id } - if (args.export.type) { - args = JSON.parse(JSON.stringify(args)) - const rows = await Mysql.execute( - ...Mysql.select('SELECT id FROM nt_nameserver_export_type', { - name: args.export.type, - }), - ) - args.export_type_id = rows[0].id - delete args.export.type - } + args = await resolveType(args) return await Mysql.execute(...Mysql.insert(`nt_nameserver`, mapToDbColumn(objectToDb(args), nsDbMap))) } @@ -58,13 +49,12 @@ class Nameserver extends NameserverBase { , ns.export_interval , ns.export_serials , ns.export_status - , ns.engine , ns.listen , ns.publisher , ns.transport , ns.dnssec , ns.deleted - , t.name AS export_type + , t.name AS type FROM nt_nameserver ns JOIN nt_nameserver_export_type t ON ns.export_type_id=t.id`, mapToDbColumn(args, nsDbMap), @@ -81,6 +71,7 @@ class Nameserver extends NameserverBase { async put(args) { if (!args.id) return false + args = await resolveType(args) const id = args.id delete args.id // Mysql.debug(1) @@ -111,6 +102,24 @@ class Nameserver extends NameserverBase { export default Nameserver +/** + * `type` is stored as the export_type_id foreign key, so a write has to resolve + * the name to its id. + */ +async function resolveType(argsIn) { + if (argsIn.type === undefined) return argsIn + + const args = JSON.parse(JSON.stringify(argsIn)) + const rows = await Mysql.execute( + ...Mysql.select('SELECT id FROM nt_nameserver_export_type', { name: args.type }), + ) + if (!rows.length) throw new Error(`unknown nameserver type: ${args.type}`) + + args.export_type_id = rows[0].id + delete args.type + return args +} + function dbToObject(rows) { for (const row of rows) { for (const f of ['description', 'address6', 'remote_login', 'datadir', 'logdir', 'export_status']) { @@ -128,9 +137,8 @@ function dbToObject(rows) { } if (row[f] === null || row[f] === undefined) delete row[f] } - if ([null, undefined, ''].includes(row.engine)) delete row.engine for (const f of ['export']) { - for (const p of ['type', 'interval', 'serials', 'status']) { + for (const p of ['interval', 'serials', 'status']) { if (![null, undefined].includes(row[`${f}_${p}`])) { if (row[f] === undefined) row[f] = {} row[f][p] = row[`${f}_${p}`] diff --git a/lib/nameserver/test/nameserver.json b/lib/nameserver/test/nameserver.json index 934ecbd..a97719a 100644 --- a/lib/nameserver/test/nameserver.json +++ b/lib/nameserver/test/nameserver.json @@ -8,11 +8,11 @@ "remote_login": "nsd", "logdir": "/foo", "datadir": "/bar", + "type": "nsd", "export": { "interval": 0, "serials": true, - "status": "last run:03-05 15:25
last cp :09-20 12:59", - "type": "nsd" + "status": "last run:03-05 15:25
last cp :09-20 12:59" }, "ttl": 3600 } diff --git a/lib/nameserver/test/runtime.js b/lib/nameserver/test/runtime.js index c2aef40..69b31d6 100644 --- a/lib/nameserver/test/runtime.js +++ b/lib/nameserver/test/runtime.js @@ -1,7 +1,8 @@ // The nameserver record carries the v3 runtime configuration the supervisor -// consumes (engine, listen, publisher, transport, dnssec). It has to survive a +// consumes (type, listen, publisher, transport, dnssec). It has to survive a // round-trip through whichever store is active: nested structures in the file -// stores, JSON columns under MySQL. +// stores, JSON columns under MySQL, and `type` through the export_type_id +// foreign key. import assert from 'node:assert/strict' import { describe, it, after, before } from 'node:test' @@ -13,8 +14,8 @@ const runtimeCase = { name: 'rt.ns.example.com.', address: '1.2.3.9', ttl: 3600, - export: { type: 'nsd', interval: 0, serials: true, status: '' }, - engine: 'native', + export: { interval: 0, serials: true, status: '' }, + type: 'native', listen: [ { address: '127.0.0.1', port: 5353, proto: 'udp' }, { address: '127.0.0.1', port: 5353, proto: 'tcp' }, @@ -41,10 +42,10 @@ describe('nameserver runtime config', () => { assert.deepEqual(ns.listen, runtimeCase.listen) }) - it('round-trips engine, publisher, transport and dnssec', async () => { + it('round-trips type, publisher, transport and dnssec', async () => { const [ns] = await Nameserver.get({ id: runtimeCase.id }) - assert.equal(ns.engine, 'native') + assert.equal(ns.type, 'native') assert.deepEqual(ns.publisher, runtimeCase.publisher) assert.deepEqual(ns.transport, runtimeCase.transport) assert.deepEqual(ns.dnssec, runtimeCase.dnssec) @@ -52,22 +53,34 @@ describe('nameserver runtime config', () => { it('updates runtime config in place', async () => { const listen = [{ address: '127.0.0.1', port: 5354, proto: 'udp' }] - assert.ok(await Nameserver.put({ id: runtimeCase.id, listen, engine: 'bind' })) + assert.ok(await Nameserver.put({ id: runtimeCase.id, listen, type: 'bind' })) const [ns] = await Nameserver.get({ id: runtimeCase.id }) assert.deepEqual(ns.listen, listen) - assert.equal(ns.engine, 'bind') + assert.equal(ns.type, 'bind') + }) + + // dynect and bind-nsupdate are 2.x export types with no v3 nameserver. An + // adopted record must still round-trip; refusing it would strand the record. + it('stores a type nothing here can build', async () => { + const legacy = { ...runtimeCase, id: 4244, name: 'dyn.ns.example.com.', type: 'dynect' } + await Nameserver.destroy({ id: legacy.id }) + await Nameserver.create(legacy) + + const [ns] = await Nameserver.get({ id: legacy.id }) + assert.equal(ns.type, 'dynect') + await Nameserver.destroy({ id: legacy.id }) }) it('omits runtime keys entirely for a legacy record that has none', async () => { const legacy = { ...runtimeCase, id: 4243, name: 'legacy.ns.example.com.' } - for (const k of ['engine', 'listen', 'publisher', 'transport', 'dnssec']) delete legacy[k] + for (const k of ['listen', 'publisher', 'transport', 'dnssec']) delete legacy[k] await Nameserver.destroy({ id: legacy.id }) await Nameserver.create(legacy) const [ns] = await Nameserver.get({ id: legacy.id }) - for (const k of ['engine', 'listen', 'publisher', 'transport', 'dnssec']) { + for (const k of ['listen', 'publisher', 'transport', 'dnssec']) { assert.equal(ns[k], undefined, `${k} should be absent, not null`) } await Nameserver.destroy({ id: legacy.id }) diff --git a/routes/test/nameserver.json b/routes/test/nameserver.json index a5366be..ce2ff0b 100644 --- a/routes/test/nameserver.json +++ b/routes/test/nameserver.json @@ -8,11 +8,11 @@ "remote_login": "nsd", "logdir": "/foo", "datadir": "/bar", + "type": "nsd", "export": { "interval": 0, "serials": true, - "status": "last run:03-05 15:25
last cp :09-20 12:59", - "type": "nsd" + "status": "last run:03-05 15:25
last cp :09-20 12:59" }, "ttl": 3600, "deleted": false diff --git a/sql/01_nt_group.sql b/sql/01_nt_group.sql index 4595ada..c7ed848 100644 --- a/sql/01_nt_group.sql +++ b/sql/01_nt_group.sql @@ -42,6 +42,10 @@ CREATE TABLE IF NOT EXISTS nt_group_subgroups( INSERT IGNORE INTO `nt_group` (`nt_group_id`, `parent_group_id`, `name`) VALUES (1,0,'NicTool'); -INSERT IGNORE INTO nt_group_log(nt_group_id, nt_user_id, action, timestamp, modified_group_id, parent_group_id) -VALUES - (1, 1, 'added', UNIX_TIMESTAMP(), 1, 0); + +# The log row carries no explicit id and nt_group_log has no unique key over +# these columns, guard on the row already being there. +INSERT INTO nt_group_log(nt_group_id, nt_user_id, action, timestamp, modified_group_id, parent_group_id) +SELECT 1, 1, 'added', UNIX_TIMESTAMP(), 1, 0 +FROM DUAL WHERE NOT EXISTS + (SELECT 1 FROM nt_group_log WHERE modified_group_id=1 AND action='added'); diff --git a/sql/04_nt_nameserver.sql b/sql/04_nt_nameserver.sql index 3e5a73a..d02916d 100644 --- a/sql/04_nt_nameserver.sql +++ b/sql/04_nt_nameserver.sql @@ -18,8 +18,8 @@ CREATE TABLE IF NOT EXISTS nt_nameserver( export_serials tinyint(1) UNSIGNED NOT NULL DEFAULT '1', export_status varchar(255) NULL DEFAULT NULL, -- v3 runtime configuration, driving the nameserver supervisor. Stored as - -- JSON because the shape is engine-specific and evolves with the engines. - engine VARCHAR(32) NULL DEFAULT NULL, + -- JSON because the shape varies by nameserver type and evolves with it. + -- Which software this is stays in export_type_id: one fact, one column. listen JSON NULL DEFAULT NULL, publisher JSON NULL DEFAULT NULL, transport JSON NULL DEFAULT NULL, @@ -72,21 +72,37 @@ VALUES (1,'djbdns','djbdns (tinydns & axfrdns)','cr.yp.to/djbdns.html'), (5,'bind-nsupdate','BIND (nsupdate protocol)',''), (6,'nsd','Name Server Daemon (NSD)','www.nlnetlabs.nl/projects/nsd/'), (7,'dynect','DynECT Standard DNS','dyn.com/managed-dns/'), - (8,'knot','Knot DNS','www.knot-dns.cz'); + (8,'knot','Knot DNS','www.knot-dns.cz'), + (9,'coredns','CoreDNS','coredns.io'), + (10,'native','NicTool (in-process)','nictool.com'); -INSERT IGNORE INTO nt_nameserver(nt_group_id, name, ttl, description, address, - export_type_id, logdir, datadir, export_interval) values (1,'ns1.example.com.',86400,'ns east', - '198.93.97.188','1','/etc/tinydns-ns1/log/main/', - '/etc/tinydns-ns1/root/',120); -INSERT IGNORE INTO nt_nameserver(nt_group_id, name, ttl, description, address, - export_type_id, logdir, datadir, export_interval) values (1,'ns2.example.com.',86400,'ns west', - '216.133.235.6','1','/etc/tinydns-ns2/log/main/','/etc/tinydns-ns2/root/',120); -INSERT IGNORE INTO nt_nameserver(nt_group_id, name, ttl, description, address, - export_type_id, logdir, datadir, export_interval) values (1,'ns3.example.com.',86400,'ns test', - '127.0.0.1','2','/var/log', '/etc/namedb/master/',120); -INSERT IGNORE INTO nt_nameserver_log(nt_group_id,nt_user_id, action, timestamp, nt_nameserver_id) VALUES (1,1,'added',UNIX_TIMESTAMP(), 1); -INSERT IGNORE INTO nt_nameserver_log(nt_group_id,nt_user_id, action, timestamp, nt_nameserver_id) VALUES (1,1,'added',UNIX_TIMESTAMP(), 2); -INSERT IGNORE INTO nt_nameserver_log(nt_group_id,nt_user_id, action, timestamp, nt_nameserver_id) VALUES (1,1,'added',UNIX_TIMESTAMP(), 3); +INSERT INTO nt_nameserver(nt_group_id, name, ttl, description, address, + export_type_id, logdir, datadir, export_interval) +SELECT 1,'ns1.example.com.',86400,'ns east','198.93.97.188','1', + '/etc/tinydns-ns1/log/main/','/etc/tinydns-ns1/root/',120 +FROM DUAL WHERE NOT EXISTS + (SELECT 1 FROM nt_nameserver WHERE name='ns1.example.com.'); + +INSERT INTO nt_nameserver(nt_group_id, name, ttl, description, address, + export_type_id, logdir, datadir, export_interval) +SELECT 1,'ns2.example.com.',86400,'ns west','216.133.235.6','1', + '/etc/tinydns-ns2/log/main/','/etc/tinydns-ns2/root/',120 +FROM DUAL WHERE NOT EXISTS + (SELECT 1 FROM nt_nameserver WHERE name='ns2.example.com.'); + +INSERT INTO nt_nameserver(nt_group_id, name, ttl, description, address, + export_type_id, logdir, datadir, export_interval) +SELECT 1,'ns3.example.com.',86400,'ns test','127.0.0.1','2', + '/var/log','/etc/namedb/master/',120 +FROM DUAL WHERE NOT EXISTS + (SELECT 1 FROM nt_nameserver WHERE name='ns3.example.com.'); + +INSERT INTO nt_nameserver_log(nt_group_id, nt_user_id, action, timestamp, nt_nameserver_id) +SELECT 1,1,'added',UNIX_TIMESTAMP(),ns.nt_nameserver_id +FROM nt_nameserver ns +WHERE ns.name IN ('ns1.example.com.','ns2.example.com.','ns3.example.com.') + AND NOT EXISTS (SELECT 1 FROM nt_nameserver_log l + WHERE l.nt_nameserver_id = ns.nt_nameserver_id AND l.action = 'added'); CREATE TABLE IF NOT EXISTS nt_nameserver_export_log( nt_nameserver_export_log_id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, diff --git a/sql/06_resource_records.sql b/sql/06_resource_records.sql index 60b93cc..fdbde03 100644 --- a/sql/06_resource_records.sql +++ b/sql/06_resource_records.sql @@ -17,7 +17,7 @@ VALUES (2,'NS','Name Server',1,1,0), (5,'CNAME','Canonical Name',1,1,0), (6,'SOA','Start Of Authority',0,0,0), - (12,'PTR','Pointer',1,0,0), + (12,'PTR','Pointer',1,1,0), (13,'HINFO','Host Info',0,0,1), (15,'MX','Mail Exchanger',0,1,0), (16,'TXT','Text',1,1,0), diff --git a/sql/upgrade/03_nameserver_runtime_columns.sql b/sql/upgrade/03_nameserver_runtime_columns.sql index 7946060..0e806d9 100644 --- a/sql/upgrade/03_nameserver_runtime_columns.sql +++ b/sql/upgrade/03_nameserver_runtime_columns.sql @@ -7,7 +7,6 @@ # errors with ER_DUP_FIELDNAME. That is safe to ignore. ALTER TABLE nt_nameserver - ADD COLUMN engine VARCHAR(32) NULL DEFAULT NULL, ADD COLUMN listen JSON NULL DEFAULT NULL, ADD COLUMN publisher JSON NULL DEFAULT NULL, ADD COLUMN transport JSON NULL DEFAULT NULL,