PowerDNS (parte 3)
Esta parte solo te interesará si tienes un servidor DNS MyDNS y lo quieres migrar a PowerDNS. Aquí se explica superficialmente cuál es el funcionamiento de los procedimientos PL/SQL que intervienen en la conversión y la migración a una base de datos estructurada para PowerDNS.
El código en el repo de Github está en: mydns2pdns.sql
3. Migración
El método de migración por el que yo me he decantado es tener MyDNS y PowerDNS en la misma máquina compartiendo un gestor de base de datos de tipo MySQL.
Dada esta situación he desarrollado una serie de “procedimientos PLSQL” que serán los encargados de hacer el trabajo de traspasar las zonas de una base de datos a otra.
Teniendo en cuenta que MyDNS es un servidor en producción antes que nada lo que yo haría sería ajustar la configuración de MyDNS para que respondiera en un puerto no privilegiado, por ejemplo el 53010, y configuro una regla con iptables que redirija todo el tráfico del puerto 53 al 53010, algo como esto:
iptables -A PREROUTING -t nat -p tcp --dport 53 -j REDIRECT --to-ports 53010
El hecho de cambiar la configuración, reiniciar el servidor MyDNS y aplicar esa regla, se puede hacer prácticamente instantáneo mediante comandos BASH enlazados en función del estado de sus salidas.
(Cuando tenga un hueco lo hago y pongo un pequeño vídeo)
Con MyDNS escuchando en el 53010, ahora se podría poner PowerDNS en el 53001, y cuando estén los dos perfectamente escuchando en sus puertos, solo habría que insertar sobre la regla anterior esta otra con este comando:
iptables -I PREROUTING -t nat -p tcp --dport 53 -j REDIRECT --to-ports 53001
Ya estaría PowerDNS sirviendo peticiones.
Para evitar perder modificaciones mientras se hace todo esto, he dispuesto un trigger que se enciende después de modificar las tablas mydns.soa y mydns.rr, este trigger transfiere la zona completa a la base de datos de PDNS.
Para los que usen MyDNS en modo Maestro/Esclavo también he dispuesto el código PLSQL necesario para que se compruebe el serial de la tabla mydns.soa con el serial de la tabla pdns.records, si el serial es mayor en mydns, se transfiere la zona completa.
También he hecho los procedimientos para poner un serial de un dominio a 0 dado su domain_id de pdns.records, o ponerlos todos, ya que como expliqué anteriomente PowerDNS viene con la característica de autoserial.
Todo el código necesario está en mi repositorio de Github.
3.1. mydns2pdns
3.1.1. Procedimientos de migración
Voy a explicar en pseudo-código, un poco y sin entrar en muchos detalles, cómo funcionan y se conectan los procedimientos desarrollados para hacer la migración entre las bases de datos de MyDNS y PowerDNS.
mydns2pdns tiene que llevar como párametro un entero positivo indicando el número de zonas que se desean transferir, o bien si se le pasa 0, se transferirán todas.
Todas las zonas:
MariaDB [procedimientos]> call mydns2pdns(0);
Solo 100 zonas:
MariaDB [procedimientos]> call mydns2pdns(100);
El proceso completo sería este:
check_name_record: ENTRADAS: - nombre del registro con o sin origen implícito - origen del dominio SALIDAS: - nombre de registro con el dominio de destino de forma explícita - actuación sobre ese nombre de registro Este procedimiento añade los campos que vienen con ORIGIN implícito o los que son de tipo @ check_txt_record: ENTRADA: registro de tipo texto en formato MyDNS SALIDA: registro de tipo texto en formato PowerDNS Encierra el contenido del registro entre comillas check_content: ENTRADAS: - tipo de registro - origen del dominio - contenido del registro en formato MyDNS SALIDA: - contenido del registro en formato PowerDNS Determina cómo formatear el contenido del registro en función del tipo qué es check_if_zone: ENTRADA: - origen del dominio SALIDA: - Acción a realizar sobre el dominio de entrada Procedimiento que devuelve qué hacer sobre el dominio: - Saltarlo (0) - Clearlo y clonarlo (1) - Solo clonar sus registros (2) clone_records: ENTRADA: - id de zona en MyDNS - id de zona en PowerDNS - origen del dominio Recorre todos los registros del ese dominio en MyDNS: Para cada registro: Comprueba el nombre del registro → check_name_record Comprueba el contenido del registro → check_con tent Si el registro es correcto lo inserta en PowerDNS clone_soa: ENTRADAS: - id de zona en MyDNS - id de zona en PowerDNS SALIDAS: - origen del dominio Crea el content del SOA para el dominio en PowerDNS. Determina si el dominio debe estar activo en PowerDNS Inserta el SOA en la BBDD de PowerDNS insert_zone: ENTRADAS: - id del dominio en PowerDNS SI el dominio no existe en la tabla pdns.zones, lo crea insert_domain: ENTRADAS: - id de zona en MyDNS SALIDAS: - id de zona en PowerDNS Si no existe la zona en pdns.domains, lo crea clone_zone: ENTRADAS: - id de zona en MyDNS - origen del dominio Comprueba la zona → check_if_zone Si la comprobación devuelve distinto de 0: inserta el dominio → insert_domain insert la zona → insert_zone clona el SOA → clone_soa clona los registros → clone_records walk_domains: ENTRADA: - límite de dominios a clonar Recorre dominios en MyDNS hasta el límite pasado o todos si límite es 0 Para cada dominio: clonar dominio → clone_zone /* función auxiliar para su uso como depuración */ clone_pattern: ENTRADA: - patrón SQL Migra los dominios que coincidan con el patrón LIKE pasado como parámetro desde MyDNS
El funcionamiento es muy simple, ¿no?.
3.1.2. Triggers
También explico un poco la finalidad de cada trigger.
3.1.2.1. PDNS
insert_change_date || update_change_date: Para cada registro que se modifique on inserte en PDNS, actualiza su campo de fecha de modificación
3.1.2.2. MyDNS
Para estos triggers aprovecho el procedimiento ‘clone_zone’ que es lo suficiente autónomo como para determinar si clonar la zona o no, en función del serial.
insert_zone || update_zone: Si se cambia o inserta una zona en MyDNS, se actualiza en PowerDNS → clone_zone
3.2. Código completo
El código completo es este:
/* Procedimientos para migrar de MyDNS -> PowerDNS Código por: Manuel Alcocer Jiménez freenode.#birras: nashgul mail: manuel@alcocer.net */ USE procedimientos; -- Desactivo los TRIGGERS para ganar un poco de velocidad -- en la migración -- Al final se vuelven a activar SET @TRIGGER_CHECKS = FALSE; DELIMITER // /* Procedimiento para reiniciar la migración */ CREATE OR REPLACE PROCEDURE limpia() BEGIN DELETE FROM pdns.domains; ALTER TABLE pdns.domains AUTO_INCREMENT = 1; DELETE FROM pdns.zones; ALTER TABLE pdns.zones AUTO_INCREMENT = 1; DELETE FROM pdns.records; ALTER TABLE pdns.records AUTO_INCREMENT = 1; END // /* Procedimiento para quitar de forma condicionada * los puntos al final de los registros de tipo TEXTO * Devuelve 1 si considera que no se debe insertar por * estar mal formado */ CREATE OR REPLACE PROCEDURE check_name_record (p_origin VARCHAR(255), INOUT p_name VARCHAR(255), OUT p_act INTEGER) BEGIN SET p_act = 0; IF (LENGTH(p_name) = 1 AND p_name = '@') OR LENGTH(p_name) = 0 THEN SELECT CONCAT(p_origin, '.') INTO p_name; ELSEIF LENGTH(p_name) = 1 AND p_name = '.' THEN SET p_act = 1; END IF; IF SUBSTR(p_name, -1) != '.' THEN SELECT CONCAT(p_name, '.', p_origin, '.') INTO p_name; END IF; SELECT TRIM(TRAILING '.' FROM p_name) INTO p_name; END // /* los TXT necesitan ir encerrados entre comillas */ CREATE OR REPLACE PROCEDURE check_txt_record (INOUT p_content TEXT(16000)) BEGIN IF SUBSTR(p_content, 1, 1) != '\"' THEN SELECT CONCAT('\"', p_content) INTO p_content; END IF; IF SUBSTR(p_content, -1 ,1) != '\"' THEN SELECT CONCAT(p_content, '\"') INTO p_content; END IF; END // /* Procedimiento que chequea el registro content de PDNS */ CREATE Or REPLACE PROCEDURE check_content (p_type VARCHAR(10), p_origin VARCHAR(255), INOUT p_content TEXT(64000)) BEGIN DECLARE v_act INTEGER; IF p_type IN ('CNAME', 'NS', 'MX', 'PTR') THEN CALL check_name_record (p_origin, p_content, v_act); ELSEIF p_type = 'TXT' THEN CALL check_txt_record (p_content); END IF; END // /* Devuelve 0 si no existe la zona en la BBDD pdns */ CREATE OR REPLACE PROCEDURE check_if_zone (p_origin VARCHAR(255), OUT p_result INTEGER) BEGIN DECLARE v_mydns_serial INTEGER DEFAULT 0; DECLARE v_pdns_serial INTEGER DEFAULT 0; SELECT count(*) INTO p_result FROM pdns.domains WHERE name = TRIM(TRAILING '.' FROM p_origin); -- si existe en destino, comprueba serials IF p_result > 0 THEN SELECT serial INTO v_mydns_serial FROM mydns.soa WHERE origin = p_origin; SELECT IFNULL(REGEXP_REPLACE(content, '(\\S+\\s+){2}(\\S+\\s+).*', '\\2'), 0) INTO v_pdns_serial FROM pdns.records WHERE name = TRIM(TRAILING '.' FROM p_origin) AND type = 'SOA'; IF v_mydns_serial > v_pdns_serial THEN SET p_result = 2; ELSE SET p_result = 0; END IF; ELSE SET p_result = 1; END IF; END // /* Procedimiento que pasa los registros de un id de origen * a un domain_id de destino */ CREATE OR REPLACE PROCEDURE clone_records (p_zone_id INTEGER, p_domain_id INTEGER, p_origin VARCHAR(255)) BEGIN DECLARE v_name VARCHAR(255); DECLARE v_type VARCHAR(10); DECLARE v_content TEXT(64000); DECLARE v_prio INTEGER; DECLARE v_ttl INTEGER; DECLARE v_change_date INTEGER; DECLARE v_act INTEGER DEFAULT 0; DECLARE v_done INT DEFAULT FALSE; DECLARE c_records CURSOR FOR SELECT name, type, data, aux, ttl, UNIX_TIMESTAMP(NOW()) FROM mydns.rr WHERE zone = p_zone_id; DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE; OPEN c_records; get_records: LOOP FETCH c_records INTO v_name, v_type, v_content, v_prio, v_ttl, v_change_date; IF v_done THEN LEAVE get_records; END IF; CALL check_name_record (p_origin, v_name, v_act); CALL check_content (v_type, p_origin, v_content); IF v_act = 0 THEN INSERT INTO pdns.records (domain_id, name, type, content, ttl, prio, change_date) VALUES (p_domain_id, v_name, v_type, v_content, v_ttl, v_prio, v_change_date); END IF; END LOOP; CLOSE c_records; END // /* zoneid: identificador de zona en origen * zone_origin: dominio acabado en '.': example.org. */ /* Clonado del registro SOA */ CREATE OR REPLACE PROCEDURE clone_soa (p_zone_id INTEGER, p_domain_id INTEGER, OUT p_name VARCHAR(255)) BEGIN DECLARE v_content TEXT(64000); DECLARE v_ttl INTEGER; DECLARE v_change_date INTEGER; DECLARE v_disabled TINYINT(1) DEFAULT 0; DECLARE v_active VARCHAR(2); -- Primero me aseguro que en destino no existe ya ese dominio DELETE FROM pdns.records WHERE domain_id = p_domain_id; SELECT TRIM(TRAILING '.' FROM origin), CONCAT_WS(' ', TRIM(TRAILING '.' FROM ns), TRIM(TRAILING '.' FROM mbox), serial, refresh, retry, expire, minimum), ttl, active, UNIX_TIMESTAMP(NOW()) INTO p_name, v_content, v_ttl, v_active, v_change_date FROM mydns.soa WHERE id = p_zone_id; IF v_active = 'N' THEN SET v_disabled = 1; END IF; INSERT INTO pdns.records (domain_id, name, type, content, ttl, prio, change_date, disabled, auth) VALUES (p_domain_id, p_name, 'SOA', v_content, v_ttl, 0, v_change_date, v_disabled, 1); END // CREATE OR REPLACE PROCEDURE insert_zone (p_domain_id INTEGER) BEGIN DECLARE v_domain_id INTEGER; SELECT COUNT(*) INTO v_domain_id FROM pdns.zones WHERE domain_id = p_domain_id; IF v_domain_id = 0 THEN INSERT INTO pdns.zones (domain_id, owner) VALUES (p_domain_id, 1); END IF; END // /* Crea el registro de la zona correspondiente en la tabla DOMAINS */ CREATE OR REPLACE PROCEDURE insert_domain (p_zone_id INTEGER, OUT p_domain_id INTEGER) BEGIN DECLARE v_name VARCHAR(255); SELECT TRIM(TRAILING '.' FROM origin) INTO v_name FROM mydns.soa WHERE id = p_zone_id; SELECT COUNT(*) INTO p_domain_id FROM pdns.domains WHERE name = v_name; IF p_domain_id = 0 THEN INSERT INTO pdns.domains (name, type) VALUES (v_name, 'NATIVE'); END IF; SELECT id INTO p_domain_id FROM pdns.domains WHERE name = v_name; END // /* Dado un identificador de zona de origen, lo clona completo en PDNS */ CREATE OR REPLACE PROCEDURE clone_zone (p_zone_id INTEGER, p_origin VARCHAR(255)) BEGIN DECLARE v_domain_id INTEGER; DECLARE v_name VARCHAR(255); DECLARE v_result INTEGER DEFAULT 0; CALL check_if_zone(p_origin, v_result); IF v_result <> 0 THEN CALL insert_domain (p_zone_id, v_domain_id); CALL insert_zone (v_domain_id); CALL clone_soa (p_zone_id, v_domain_id, v_name); CALL clone_records (p_zone_id, v_domain_id, v_name); END IF; SET @result := v_result; END // /* Recorre todas las zonas de origen */ CREATE OR REPLACE PROCEDURE walk_domains (p_limit INT) BEGIN DECLARE v_zone_id INTEGER; DECLARE v_done INT DEFAULT FALSE; DECLARE v_origin VARCHAR(255); DECLARE v_num INTEGER DEFAULT 0; DECLARE c_domains CURSOR FOR SELECT id, origin FROM mydns.soa ORDER BY id; DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE; IF p_limit = 0 THEN SELECT COUNT(id) INTO p_limit FROM mydns.soa; END IF; OPEN c_domains; get_domains: LOOP SET v_zone_id = 0; FETCH c_domains INTO v_zone_id, v_origin; IF v_done OR v_num >= p_limit THEN LEAVE get_domains; END IF; SET v_num = v_num + 1; CALL clone_zone (v_zone_id, v_origin); IF @result = 0 THEN SELECT 'Se saltó', v_num, p_limit, v_origin; ELSE SELECT 'Se insertó', v_num, p_limit, v_origin; END IF; END LOOP; CLOSE c_domains; END // CREATE OR REPLACE PROCEDURE mydns2pdns (p_limit INTEGER) BEGIN CALL walk_domains(p_limit); END // /* clona los registros que cumplan con un patron dado en formato SQL LIKE */ CREATE OR REPLACE PROCEDURE clone_pattern (p_pattern VARCHAR(255)) BEGIN DECLARE v_done INTEGER DEFAULT FALSE; DECLARE total INTEGER DEFAULT 0; DECLARE v_id VARCHAR(255); DECLARE num INTEGER DEFAULT 0; DECLARE c_search CURSOR FOR SELECT id, ( SELECT COUNT(id) FROM mydns.soa WHERE origin LIKE p_pattern ) FROM mydns.soa WHERE origin LIKE p_pattern; DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE; OPEN c_search; walk_result: LOOP FETCH c_search INTO v_id, total; SET num = num + 1; IF v_done THEN LEAVE walk_result; END IF; CALL clone_zone(num, total, v_id); END LOOP; CLOSE c_search; END // DELIMITER ; USE pdns; DELIMITER // -- Trigger para tener actualizado el change_date en caso de inserción CREATE OR REPLACE TRIGGER insert_change_date BEFORE INSERT ON records FOR EACH ROW BEGIN DECLARE v_domtype VARCHAR(6); SELECT type INTO v_domtype FROM pdns.domains WHERE id = NEW.domain_id; IF v_domtype IN ('MASTER', 'NATIVE') THEN SET NEW.change_date = UNIX_TIMESTAMP(NOW()); END IF; END // -- Trigger para tener actualizado el change_date en caso de actualización CREATE OR REPLACE TRIGGER update_change_date BEFORE UPDATE ON records FOR EACH ROW BEGIN DECLARE v_domtype VARCHAR(6); SELECT type INTO v_domtype FROM pdns.domains WHERE id = NEW.domain_id; IF v_domtype IN ('MASTER', 'NATIVE') THEN SET NEW.change_date = UNIX_TIMESTAMP(NOW()); END IF; END // DELIMITER ; USE mydns; DELIMITER // CREATE OR REPLACE TRIGGER insert_zone AFTER INSERT ON soa FOR EACH ROW BEGIN CALL procedimientos.clone_zone(NEW.id, NEW.origin); END // CREATE OR REPLACE TRIGGER update_zone AFTER UPDATE ON soa FOR EACH ROW BEGIN CALL procedimientos.clone_zone(NEW.id, NEW.origin); END // DELIMITER ; -- Clona 25 registros -- call mydns2pdns(25); -- Clona todos registros -- call mydns2pdns(0); -- Vuelvo a activar los triggers SET @TRIGGER_CHECKS = TRUE; USE procedimientos;
Comentarios recientes