1------------------------------------------------------------------------------
2-- Netmagis SQL functions
3------------------------------------------------------------------------------
4
5
6------------------------------------------------------------------------------
7-- Check a DHCP range against group permissions
8--
9-- Input:
10--   - $1 : idgrp
11--   - $2 : dhcp min
12--   - $3 : dhcp max
13-- Output:
14--   - true (all addresses in DHCP range are allowed) or false
15--
16-- History
17--    200?/??/?? : pda : design
18--
19
20
21-- check a DHCP range against group permissions
22CREATE OR REPLACE FUNCTION dns.check_dhcprange_grp (INTEGER, INET, INET)
23    RETURNS BOOLEAN AS '
24    set min {}
25    foreach o [split $2 "."] {
26	lappend min [format "%02x" $o]
27    }
28    set min [join $min ""]
29    set min [expr 0x$min]
30    set ipbin [expr 0x$min]
31
32    set max {}
33    foreach o [split $3 "."] {
34	lappend max [format "%02x" $o]
35    }
36    set max [join $max ""]
37    set max [expr 0x$max]
38
39    set r t
40    for {set ipbin $min} {$ipbin <= $max} {incr ipbin} {
41	# Prepare the new IP address
42	set ip {}
43	set o $ipbin
44	for {set i 0} {$i < 4} {incr i} {
45	    set ip [linsert $ip 0 [expr $o & 0xff]]
46	    set o [expr $o >> 8]
47	}
48	set ip [join $ip "."]
49
50	# Check validity
51	spi_exec "SELECT dns.check_ip_grp (''$ip'', $1) AS v"
52
53	if {! [string equal $v "t"]} then {
54	    set r f
55	    break
56	}
57    }
58    return $r
59    ' LANGUAGE pltcl ;
60
61
62------------------------------------------------------------------------------
63-- Classifies each IPv4 address in a network
64--
65-- Input:
66--   - net: network address
67--   - lim: limit on the number of addresses classified
68--   - grp: group id
69-- Output:
70--    - table with columns:
71--		addr  INET
72--		avail INTEGER (see below)
73--		fqdn  TEXT
74--
75-- Note: addresses are classified according to:
76--     0 : unavailable (broadcast addr, no right on addr, etc.)
77--     1 : not declared and not in a dhcp range
78--     2 : declared and not in a dhcp range
79--     3 : not declared and in a dhcp range
80--     4 : declared and in a dhcp range
81--   This function creates a temporary table (allip) which only exists
82--   during the postgresql session lifetime. This table is internal to
83--   the session (other sessions cannot see it).
84--   Since this function performs a sequential traversal of IP range,
85--   a limit value must be given to not overload the PostgreSQL engine.
86--
87-- History
88--    200?/??/?? : pda : design
89--
90
91CREATE OR REPLACE FUNCTION dns.mark_cidr (net CIDR, lim INTEGER, grp INTEGER)
92    RETURNS void AS $$
93    DECLARE
94	min INET ;
95	max INET ;
96	a INET ;
97    BEGIN
98	min := INET (HOST (net)) ;
99	max := INET (HOST (BROADCAST (net))) ;
100
101	IF max - min - 2 > lim THEN
102	    RAISE EXCEPTION 'Too many addresses' ;
103	END IF ;
104
105	-- All this exception machinery is here since we can't use :
106	--    DROP TABLE IF EXISTS allip ;
107	-- It raises a notice exception, which prevents
108	-- script "ajout" to function
109	BEGIN
110	    DROP TABLE allip ;
111	EXCEPTION
112	    WHEN OTHERS THEN -- nothing
113	END ;
114
115	CREATE TEMPORARY TABLE allip (
116	    addr INET,
117	    avail INTEGER,
118		-- 0 : unavailable (broadcast addr, no right on addr, etc.)
119		-- 1 : not declared and not in a dhcp range
120		-- 2 : declared and not in a dhcp range
121		-- 3 : not declared and in a dhcp range
122		-- 4 : declared and in a dhcp range
123	    fqdn TEXT		-- if 2 or 4, then fqdn else NULL
124	) ;
125
126	a := min ;
127	WHILE a <= max LOOP
128	    INSERT INTO allip VALUES (a, 1) ;
129	    a := a + 1 ;
130	END LOOP ;
131
132	UPDATE allip
133	    SET fqdn = rr.name || '.' || domain.name,
134		avail = 2
135	    FROM dns.rr_ip, dns.rr, dns.domain
136	    WHERE allip.addr = rr_ip.addr
137		AND rr_ip.idrr = rr.idrr
138		AND rr.iddom = domain.iddom
139		;
140
141	UPDATE allip
142	    SET avail = CASE
143			    WHEN avail = 1 THEN 3
144			    WHEN avail = 2 THEN 4
145			END
146	    FROM dns.dhcprange
147	    WHERE (avail = 1 OR avail = 2)
148		AND addr >= dhcprange.min
149		AND addr <= dhcprange.max
150	    ;
151
152	UPDATE allip SET avail = 0
153	    WHERE addr = min OR addr = max OR NOT dns.check_ip_grp (addr, grp) ;
154
155	RETURN ;
156
157    END ;
158    $$ LANGUAGE plpgsql ;
159
160------------------------------------------------------------------------------
161-- Search IPv4 address range for available blocks
162--
163-- Input:
164--   - net: network address
165--   - lim: limit on the number of addresses classified
166--   - grp: group id
167-- Output:
168--    - table with columns:
169--		a	INET		-- starting address
170--		n	INTEGER		-- number of addresses in block
171--
172-- Note: this is the PostgreSQL 8.3 version (the 8.4 version would have
173--   been more elegant)
174--
175-- History
176--    200?/??/?? : pda : design
177--
178
179DROP TYPE IF EXISTS iprange_t ;
180CREATE TYPE iprange_t AS (a INET, n INTEGER) ;
181
182CREATE OR REPLACE FUNCTION dns.ipranges (net CIDR, lim INTEGER, grp INTEGER)
183    RETURNS SETOF iprange_t AS $$
184    DECLARE
185	inarange BOOLEAN ;
186	r RECORD ;
187	q iprange_t%ROWTYPE ;
188    BEGIN
189	PERFORM dns.mark_cidr (net, lim, grp) ;
190	inarange := FALSE ;
191	FOR r IN (SELECT addr, avail FROM allip ORDER BY addr)
192	LOOP
193	    IF inarange THEN
194		-- (q.a, q.n) is already a valid range
195		IF r.avail = 1 THEN
196		    q.n := q.n + 1 ;
197		ELSE
198		    RETURN NEXT q ;
199		    inarange := FALSE ;
200		END IF ;
201	    ELSE
202		-- not inside a range
203		IF r.avail = 1 THEN
204		    -- start a new range (q.a, q.n)
205		    q.a := r.addr ;
206		    q.n := 1 ;
207		    inarange := TRUE ;
208		END IF ;
209	    END IF ;
210	END LOOP ;
211	IF inarange THEN
212	    RETURN NEXT q ;
213	END IF ;
214	DROP TABLE allip ;
215	RETURN ;
216    END ;
217    $$ LANGUAGE plpgsql ;
218
219------------------------------------------------------------------------------
220-- Set the generation flag for one or more zones. These functions
221-- are called from the corresponding trigger functions and set the
222-- generation flag for all modified zones.
223--
224-- Input:
225--   - $1: IPv4/v6 address or domain id or RR id
226--   - $2: view id
227-- Output:
228--   - an unused integer value, just to be able to call sum() on result
229--
230-- History
231--    2002/??/?? : pda/jean : design
232--
233
234-- called when an IPv4 address is modified ($1=addr, $2=idrr)
235CREATE OR REPLACE FUNCTION dns.gen_rev4 (INET, INTEGER)
236    RETURNS INTEGER AS $$
237    BEGIN
238	UPDATE dns.zone_reverse4 AS z SET gen = 1
239	    FROM dns.rr
240	    WHERE $1 <<= selection
241		AND rr.idrr = $2
242		AND z.idview = rr.idview ;
243	RETURN 1 ;
244    END ;
245    $$ LANGUAGE 'plpgsql' ;
246
247-- called when an IPv6 address is modified ($1=addr, $2=idrr)
248CREATE OR REPLACE FUNCTION dns.gen_rev6 (INET, INTEGER)
249    RETURNS INTEGER AS $$
250    BEGIN
251	UPDATE dns.zone_reverse6 AS z SET gen = 1
252	    FROM dns.rr
253	    WHERE $1 <<= selection
254		AND rr.idrr = $2
255		AND z.idview = rr.idview ;
256	RETURN 1 ;
257    END ;
258    $$ LANGUAGE 'plpgsql' ;
259
260-- ID of RR ($1=idrr)
261CREATE OR REPLACE FUNCTION dns.gen_norm_idrr (INTEGER)
262    RETURNS INTEGER AS $$
263    BEGIN
264	UPDATE dns.zone_forward SET gen = 1
265		WHERE (selection, idview) =
266			(
267			    SELECT domain.name, rr.idview
268				FROM dns.domain, dns.rr
269				WHERE rr.idrr = $1
270				    AND rr.iddom = domain.iddom
271			) ;
272	RETURN 1 ;
273    END ;
274    $$ LANGUAGE 'plpgsql' ;
275
276-- ID of RR ($1=iddom, $2=idview)
277CREATE OR REPLACE FUNCTION dns.gen_norm_iddom (INTEGER, INTEGER)
278    RETURNS INTEGER AS $$
279    BEGIN
280	UPDATE dns.zone_forward SET gen = 1
281		WHERE idview = $2
282		    AND selection = (
283			SELECT domain.name
284				FROM dns.domain
285				WHERE domain.iddom = $1
286			) ;
287	RETURN 1 ;
288    END ;
289    $$ LANGUAGE 'plpgsql' ;
290
291-- utility function for the mod_relay trigger function
292-- called when a mail relay is modified ($1=iddom, $2=idrr of mx)
293CREATE OR REPLACE FUNCTION dns.gen_relay (INTEGER, INTEGER)
294    RETURNS INTEGER AS $$
295    BEGIN
296	UPDATE dns.zone_forward SET gen = 1
297	    WHERE selection = ( SELECT name FROM dns.domain WHERE iddom = $1 )
298		AND idview = ( SELECT idview FROM dns.rr WHERE idrr = $2 )
299	    ;
300	RETURN 1 ;
301    END ;
302    $$ LANGUAGE 'plpgsql' ;
303
304------------------------------------------------------------------------------
305-- Set the DHCP generation flag for one or more views.
306--
307-- Input:
308--   - $1: RR id
309-- Output:
310--   - an unused integer value, just to be able to call sum() on result
311--
312-- History
313--    201?/??/?? : pda/jean : design
314--
315
316CREATE OR REPLACE FUNCTION dns.gen_dhcp (INTEGER)
317    RETURNS INTEGER AS $$
318    BEGIN
319	UPDATE dns.view SET gendhcp = 1
320	    FROM dns.rr
321		WHERE rr.idrr = $1
322		    AND rr.mac IS NOT NULL
323		    AND view.idview = rr.idview ;
324	RETURN 1 ;
325    END ;
326    $$ LANGUAGE 'plpgsql' ;
327
328------------------------------------------------------------------------------
329-- Trigger function called when an IP address is modified
330--
331-- History
332--    200?/??/?? : pda/jean : design
333--
334
335CREATE OR REPLACE FUNCTION dns.mod_ip ()
336    RETURNS trigger AS $$
337    BEGIN
338	IF TG_OP = 'INSERT'
339	THEN
340	    PERFORM sum (dns.gen_rev4 (NEW.addr, NEW.idrr)) ;
341	    PERFORM sum (dns.gen_rev6 (NEW.addr, NEW.idrr)) ;
342	    PERFORM sum (dns.gen_norm_idrr (NEW.idrr)) ;
343	    PERFORM sum (dns.gen_dhcp (NEW.idrr)) ;
344
345	END IF ;
346
347	IF TG_OP = 'UPDATE'
348	THEN
349	    PERFORM sum (dns.gen_rev4 (NEW.addr, NEW.idrr)) ;
350	    PERFORM sum (dns.gen_rev4 (OLD.addr, OLD.idrr)) ;
351	    PERFORM sum (dns.gen_rev6 (NEW.addr, NEW.idrr)) ;
352	    PERFORM sum (dns.gen_rev6 (OLD.addr, OLD.idrr)) ;
353	    PERFORM sum (dns.gen_norm_idrr (NEW.idrr)) ;
354	    PERFORM sum (dns.gen_norm_idrr (OLD.idrr)) ;
355	    PERFORM sum (dns.gen_dhcp (NEW.idrr)) ;
356	    PERFORM sum (dns.gen_dhcp (OLD.idrr)) ;
357	END IF ;
358
359	IF TG_OP = 'DELETE'
360	THEN
361	    PERFORM sum (dns.gen_rev4 (OLD.addr, OLD.idrr)) ;
362	    PERFORM sum (dns.gen_rev6 (OLD.addr, OLD.idrr)) ;
363	    PERFORM sum (dns.gen_norm_idrr (OLD.idrr)) ;
364	    PERFORM sum (dns.gen_dhcp (OLD.idrr)) ;
365	END IF ;
366
367	RETURN NEW ;
368    END ;
369    $$ LANGUAGE 'plpgsql' ;
370
371------------------------------------------------------------------------------
372-- Trigger function called when a CNAME or a MX is modified
373--
374-- History
375--    200?/??/?? : pda/jean : design
376--
377
378CREATE OR REPLACE FUNCTION dns.mod_mxcname ()
379    RETURNS trigger AS $$
380    BEGIN
381	IF TG_OP = 'INSERT'
382	THEN
383	    PERFORM sum (dns.gen_norm_idrr (NEW.idrr)) ;
384	END IF ;
385
386	IF TG_OP = 'UPDATE'
387	THEN
388	    PERFORM sum (dns.gen_norm_idrr (NEW.idrr)) ;
389	    PERFORM sum (dns.gen_norm_idrr (OLD.idrr)) ;
390	END IF ;
391
392	IF TG_OP = 'DELETE'
393	THEN
394	    PERFORM sum (dns.gen_norm_idrr (OLD.idrr)) ;
395	END IF ;
396
397	RETURN NEW ;
398    END ;
399    $$ LANGUAGE 'plpgsql' ;
400
401------------------------------------------------------------------------------
402-- Trigger function called when a RR is modified
403--
404-- History
405--    200?/??/?? : pda/jean : design
406--
407
408-- modify RR and reverse zones for all IP addresses
409CREATE OR REPLACE FUNCTION dns.mod_rr ()
410    RETURNS trigger AS $$
411    BEGIN
412	-- IF TG_OP = 'INSERT'
413	-- THEN
414	    -- no need to regenerate anything since no rr_* has
415	    -- been linked to this rr yet
416	-- END IF ;
417
418	IF TG_OP = 'UPDATE'
419	THEN
420	    PERFORM sum (dns.gen_norm_iddom (NEW.iddom, NEW.idview))
421		    ;
422	    PERFORM sum (dns.gen_norm_iddom (OLD.iddom, OLD.idview))
423		    ;
424	    PERFORM sum (dns.gen_rev4 (rr_ip.addr, NEW.idrr))
425		    FROM dns.rr_ip WHERE rr_ip.idrr = NEW.idrr ;
426	    PERFORM sum (dns.gen_rev6 (rr_ip.addr, NEW.idrr))
427		    FROM dns.rr_ip WHERE rr_ip.idrr = NEW.idrr ;
428	    PERFORM sum (dns.gen_dhcp (NEW.idrr))
429		    ;
430	    -- no need to regenerate reverse/dhcp for old rr since
431	    -- IP addresses did not change
432	END IF ;
433
434	-- IF TG_OP = 'DELETE'
435	-- THEN
436	    -- no need to regenerate anything since all rr_* have
437	    -- already been removed before
438	-- END IF ;
439
440	RETURN NEW ;
441    END ;
442    $$ LANGUAGE 'plpgsql' ;
443
444------------------------------------------------------------------------------
445-- Trigger function called when a mail relay is modified
446--
447-- History
448--    200?/??/?? : pda/jean : design
449--
450
451CREATE OR REPLACE FUNCTION dns.mod_relay ()
452    RETURNS trigger AS $$
453    BEGIN
454	IF TG_OP = 'INSERT'
455	THEN
456	    PERFORM sum (dns.gen_relay (NEW.iddom, NEW.mx)) ;
457	END IF ;
458
459	IF TG_OP = 'UPDATE'
460	THEN
461	    PERFORM sum (dns.gen_relay (NEW.iddom, NEW.mx)) ;
462	    PERFORM sum (dns.gen_relay (OLD.iddom, OLD.mx)) ;
463	END IF ;
464
465	IF TG_OP = 'DELETE'
466	THEN
467	    PERFORM sum (dns.gen_relay (OLD.iddom, OLD.mx)) ;
468	END IF ;
469
470	RETURN NEW ;
471    END ;
472    $$ LANGUAGE 'plpgsql' ;
473
474------------------------------------------------------------------------------
475-- Trigger function called when a zone is modified
476--
477-- History
478--    200?/??/?? : pda/jean : design
479--
480
481CREATE OR REPLACE FUNCTION dns.mod_zone ()
482    RETURNS TRIGGER AS $$
483    BEGIN
484	IF NEW.prologue <> OLD.prologue
485		OR NEW.rrsup <> OLD.rrsup
486		OR NEW.selection <> OLD.selection
487	THEN
488	    NEW.gen := 1 ;
489	END IF ;
490	RETURN NEW ;
491    END ;
492    $$ LANGUAGE 'plpgsql' ;
493
494------------------------------------------------------------------------------
495-- Trigger function called when a DHCP parameter (network, range or profile)
496-- is modified
497--
498-- History
499--    200?/??/?? : pda/jean : design
500--
501
502CREATE OR REPLACE FUNCTION dns.mod_dhcp ()
503    RETURNS TRIGGER AS $$
504    BEGIN
505	UPDATE dns.view SET gendhcp = 1 ;
506	RETURN NEW ;
507    END ;
508    $$ LANGUAGE 'plpgsql' ;
509
510------------------------------------------------------------------------------
511-- Check access rights to an IP address
512--
513-- Input:
514--   - $1: IPv4/v6 address to test
515--   - $2: group id or user id
516-- Output:
517--   - true if access is allowed
518--
519-- History
520--    2002/??/?? : pda/jean : design
521--
522
523CREATE OR REPLACE FUNCTION dns.check_ip_cor (INET, INTEGER)
524    RETURNS BOOLEAN AS $$
525    BEGIN
526	RETURN dns.check_ip_grp ($1, idgrp) FROM global.nmuser WHERE idcor = $2 ;
527    END ;
528    $$ LANGUAGE 'plpgsql' ;
529
530CREATE OR REPLACE FUNCTION dns.check_ip_grp (INET, INTEGER)
531    RETURNS BOOLEAN AS $$
532    BEGIN
533	RETURN ($1 <<= ANY (SELECT addr FROM dns.p_ip
534				WHERE allow_deny = 1 AND idgrp = $2)
535	    AND NOT $1 <<= ANY (SELECT addr FROM dns.p_ip
536				WHERE allow_deny = 0 AND idgrp = $2)
537	    ) ;
538    END ;
539    $$ LANGUAGE 'plpgsql' ;
540
541------------------------------------------------------------------------------
542-- Trigger function called when a vlan is modified
543--
544-- History
545--    200?/??/?? : pda/jean : design
546--
547
548CREATE OR REPLACE FUNCTION topo.mod_vlan ()
549    RETURNS trigger AS $$
550    BEGIN
551	INSERT INTO topo.modeq (eq) VALUES ('_vlan') ;
552	RETURN NEW ;
553    END ;
554    $$ LANGUAGE 'plpgsql' ;
555
556------------------------------------------------------------------------------
557-- Trigger function called when an equipment is modified
558--
559-- History
560--    200?/??/?? : pda/jean : design
561--
562
563CREATE OR REPLACE FUNCTION topo.mod_routerdb ()
564    RETURNS trigger AS $$
565    BEGIN
566	INSERT INTO topo.modeq (eq) VALUES ('_routerdb') ;
567	RETURN NEW ;
568    END ;
569    $$ LANGUAGE 'plpgsql' ;
570
571------------------------------------------------------------------------------
572-- Reduce a string to a soundex code in order to find approximate
573-- names
574--
575-- Input:
576--   - $1: string to reduce
577-- Output:
578--   - soundex
579--
580-- History
581--    200?/??/?? : pda : design
582--
583
584CREATE FUNCTION pgauth.soundex (TEXT)
585    RETURNS TEXT AS '
586	array set soundexFrenchCode {
587	    a 0 b 1 c 2 d 3 e 0 f 9 g 7 h 0 i 0 j 7 k 2 l 4 m 5
588	    n 5 o 0 p 1 q 2 r 6 s 8 t 3 u 0 v 9 w 9 x 8 y 0 z 8
589	}
590	set accentedFrenchMap {
591	    é e  ë e  ê e  è e   É E  Ë E  Ê E  È E
592	     ä a  â a  à a        Ä A  Â A  À A
593	     ï i  î i             Ï I  Î I
594	     ö o  ô o             Ö O  Ô O
595	     ü u  û u  ù u        Ü U  Û U  Ù U
596	     ç ss                 Ç SS
597	}
598	set key ""
599
600	# Map accented characters
601	set TempIn [string map $accentedFrenchMap $1]
602
603	# Only use alphabetic characters, so strip out all others
604	# also, soundex index uses only lower case chars, so force to lower
605
606	regsub -all {[^a-z]} [string tolower $TempIn] {} TempIn
607	if {$TempIn eq ""} then {
608	    return Z000
609	}
610	set last [string index $TempIn 0]
611	set key  [string toupper $last]
612	set last $soundexFrenchCode($last)
613
614	# Scan rest of string, stop at end of string or when the key is full
615
616	set count    1
617	set MaxIndex [string length $TempIn]
618
619	for {set index 1} {(($count < 4) && ($index < $MaxIndex))} {incr index } {
620	    set chcode $soundexFrenchCode([string index $TempIn $index])
621	    # Fold together adjacent letters sharing the same code
622	    if {$last ne $chcode} then {
623		set last $chcode
624		# Ignore code==0 letters except as separators
625		if {$last != 0} then {
626		    set key $key$last
627		    incr count
628		}
629	    }
630	}
631	return [string range ${key}0000 0 3]
632    ' LANGUAGE 'pltcl' WITH (isStrict) ;
633
634------------------------------------------------------------------------------
635-- Trigger function: computes soundex for name and first name
636-- each time a name or first name is modified.
637--
638-- History
639--    200?/??/?? : pda : design
640--
641
642CREATE FUNCTION pgauth.add_soundex ()
643    RETURNS TRIGGER AS '
644    BEGIN
645	NEW.phlast  := pgauth.SOUNDEX (NEW.lastname) ;
646	NEW.phfirst := pgauth.SOUNDEX (NEW.firstname) ;
647	RETURN NEW ;
648    END ;
649    ' LANGUAGE 'plpgsql' ;
650