#!/usr/bin/perl -Tw ###################################################################### # # Copyright (C) 2020 Network RADIUS # # $Id$ # ###################################################################### # # Helper script for populating IP pools with address entries. # # This script generates SQL output that is useful for populating an IP pool # for use with FreeRADIUS (and possibly other purposes). Alternatively, # if called with the -f option will directly operate on the database configured # within the FreeRADIUS configuration to update the IP pool table specified # # Note: Direct connection to databases is done using Perl DBI. You may need # to install the appropriate Perl DBD driver to enable this functionality. # Formatted SQL output is produced using Perl Template::Toolkit which # will need to be installed if this output is required. # # # Use with a single address range # ------------------------------- # # For basic use, arguments can be provided to this script that denote the ends # of a single IP (v4 or v6) address range together with the pool_name and # SQL dialect or a raddb directory from which the database config will be # read. # # If a raddb directory is specified, then the instance of the FreeRADIUS sql # module to be found in the config can be specified. It defaults to "sql". # # Optionally the number of IPs to sparsely populate the range with can be # provided. If the range is wider than a /16 then the population of the range # is capped at 65536 IPs, unless otherwise specified. # # In the case that a sparse range is defined, a file containing pre-existing # IP entries can be provided. The range will be populated with entries from # this file that fall within the range, prior to the remainder of the range # being populated with random address in the range. # # rlm_sqlippool_tool -p -s -e \ # -t (-d | -f [ -i ]) \ # [ -c ] [ -x ] # # Note: Sparse ranges are populated using a deterministic, pseudo-random # function. This allows pools to be trivially extended without having to # supply the existing contents using a file. If you require # less-predictable randomness or a different random sequence then remove # or modify the line calling srand(), below. # # # Use with multiple pools and address ranges # ------------------------------------------ # # For more complex us, the script allows a set of pool definitions to be # provided in a YAML file which describes a set of one or more pools, each # containing a set of one or more ranges. # # rlm_sqlippool_tool -y -t \ # ( -d | -f [ -i ] ) \ # [ -x ] # # The format for the YAML file is demonstrated by the following example: # # pool_with_a_single_contiguous_range: # - start: 192.0.2.3 # end: 192.0.2.250 # # pool_with_a_single_sparse_range: # - start: 10.10.10.0 # end: 10.10.20.255 # capacity: 200 # # pool_with_multiple_ranges: # - start: 10.10.10.1 # end: 10.10.10.253 # - start: 10.10.100.0 # end: 10.10.199.255 # capacity: 1000 # # v6_pool_with_contiguous_range: # - start: '2001:db8:1:2:3:4:5:10' # end: '2001:db8:1:2:3:4:5:7f' # # v6_pool_with_sparse_range: # - start: '2001:db8:1:2::' # end: '2001:db8:1:2:ffff:ffff:ffff:ffff' # capacity: 200 # # As with the basic use case, a file containing pre-existing IP entries can be # provided with which any sparse ranges will be populated ahead of any random # addresses. # # # Output # ------ # # The script returns SQL formatted appropriately for one of a number of # different SQL dialects. # # The SQL first creates a temporary table to insert the new pools into, # inserts the addresses, then removes any exisitng entries from the pool # table that do not exist in the new pool. Finally any new entries that # don't exist in the existing pool table are copied from the temporary # table. # # The SQL templates assume that the pool name will be in a field called # "pool_name" and the IP address in a field named "framedipaddress", # matching the default schema for ippools and DHCP ippools as shipped with # FreeRADIUS. # # # Examples # -------- # # rlm_sqlippool_tool -p main_pool -s 192.0.2.3 -e 192.0.2.249 \ # -d postgresql -t radippool # # Will create a pool from a full populated IPv4 range, i.e. all IPs in the # range available for allocation, with SQL output suitable for PostgreSQL # # rlm_sqlippool_tool -p main_pool -s 10.66.0.0 -e 10.66.255.255 -c 10000 \ # -d mysql -t radippool # # Will create a pool from a sparsely populated IPv4 range for a /16 # network (maximum of 65.536 addresses), populating the range with 10,000 # addreses. With SQL output suitable for MySQL. # The effective size of the pool can be increased in future by increasing # the capacity of the range with: # # rlm_sqlippool_tool -p main_pool -s 10.66.0.0 -e 10.66.255.255 -c 20000 \ # -d mysql -t radippool # # This generates the same initial set of 10,000 addresses as the previous # example but will create 20,000 addresses overall, unless the random seed # has been amended since the initial run. # # rlm_sqlippool_tool -p main_pool -s 2001:db8:1:2:: \ # -e 2001:db8:1:2:ffff:ffff:ffff:ffff -d mssql -t radippool # # Will create a pool from the IPv6 range 2001:db8:1:2::/64, initially # populating the range with 65536 (by default) addresses. # # rlm_sqlippool_tool -p main_pool -s 2001:db8:1:2:: \ # -e 2001:db8:1:2:ffff:ffff:ffff:ffff \ # -c 10000 -x existing_ips.txt -d mysql -t radippool # # Will create a pool using the same range as the previous example, but # this time the range will be populated with 10,000 addresses. The range # will be populated using lines extracted from the `existing_ips.txt` file # that represent IPs which fall within range. # # rlm_sqlippool_tool -y pool_defs.yml -d postgresql -t radippool \ # -x existing_ips.txt # # Will create one of more pools using the definitions found in the # pool_defs.yml YAML file. The pools will contain one or more ranges with # each of the ranges first being populated with entries from the # existing_ips.txt file that fall within the range, before being filled # with random addresses to the defined capacity. # use strict; use Net::IP qw/ip_bintoip ip_iptobin ip_bincomp ip_binadd ip_is_ipv4 ip_is_ipv6/; # # Option defaults # my $opts = { instance => 'sql', capacity => 65536 }; # # Parse the command line arguments # my $opt = ''; for (my $i = 0; $i <= $#ARGV; $i++) { if ($ARGV[$i] =~ m/^-(.)$/) { if ($1 eq 'p') { $opt = 'pool_name'; } elsif ($1 eq 's') { $opt = 'range_start'; } elsif ($1 eq 'e') { $opt = 'range_end'; } elsif ($1 eq 'c') { $opt = 'capacity'; } elsif ($1 eq 't') { $opt = 'table_name'; } elsif ($1 eq 'd') { $opt = 'dialect'; } elsif ($1 eq 'y') { $opt = 'yaml'; } elsif ($1 eq 'x') { $opt = 'entries'; } elsif ($1 eq 'f') { $opt = 'raddb_dir'; } elsif ($1 eq 'i') { $opt = 'instance'; } else { usage(); exit 1; } } else { if ($opt eq '') { usage(); exit 1; } else { $opts->{$opt} = $ARGV[$i] } } } # # If a raddb dir is set then we parse the mods-enabled config # if ($opts->{raddb_dir}) { my $found = 0; if (-d $opts->{raddb_dir}.'/mods-enabled') { opendir(my $dh, $opts->{raddb_dir}.'/mods-enabled') || die 'ERROR: Could not open directory '.$opts->{raddb_dir}.'/mods-enabled'; my @dir = grep { -f "$opts->{raddb_dir}/mods-enabled/$_" } readdir($dh); closedir($dh); my $instance = $opts->{instance}; foreach my $file (@dir) { open (my $fh, $opts->{raddb_dir}.'/mods-enabled/'.$file); my $level = 0; my $section = ''; my $subsection = ''; while (<$fh>) { if ($found) { $_ =~ s/#.*//; # Remove comments if ($_ =~ m/\s*([a-z_]+)\s*=\s*(.*)/) { my $param = $1; my $value = $2; $value =~ s/^"//; $value =~ s/"\s*$//; if ($level == 1) { $opts->{$param} = $value; } elsif ($level == 2) { $opts->{$section}->{$param} = $value; } elsif ($level == 3) { $opts->{$section}->{$subsection}->{$param} = $value; } } if ($_ =~ m/([a-z_]*)\s+\{/) { # Find nested sectinos $level++ ; if ($level == 2) { $section = $1; } elsif ($level == 3) { $subsection = $1; } } $level-- if ($_ =~ m/\s+\}/); # Close of nesting last if ($level == 0); # We've got to the end of the instance } if ($_ =~ m/\b$instance\s+\{/) { # We've found the specified SQL instance $found = 1; $level = 1; } } close ($fh); if ($found) { last; } } } else { die 'ERROR: Specified FreeRADIUS config directory does not contain mods-enabled'; } if ($found == 0) { die 'ERROR: SQL instance not found in FreeRADIUS config'; } } # # The SQL dialect and table name must be set # if ((!($opts->{dialect})) || (!($opts->{table_name}))) { usage(); exit 1; } if ($opts->{yaml}) { my $yaml_available = 0; eval { require YAML::XS; YAML::XS->import('LoadFile'); $yaml_available = 1; }; unless ($yaml_available) { die "ERROR: YAML is not available. Install the YAML::XS Perl module."; } process_yaml_file($opts); goto done; } if ((!($opts->{pool_name})) || (!($opts->{range_start})) || (!($opts->{range_end}))) { usage(); exit 1; } process_commandline($opts); done: exit 0; sub usage { print STDERR <<'EOF' Usage: rlm_sqlippool_tool -p -s -e -t (-d | -f [ -i ]) [ -c ] [ -x ] or: rlm_sqlippool_tool -y -t (-d | -f [ -i ]) [ -x ] EOF } sub process_commandline { my $opts = shift; $SIG{__DIE__} = sub { usage(); die(@_); }; (my $template, my $queries)=load_templates($opts->{table_name}); unless (defined $template->{$opts->{dialect}}) { print STDERR "Unknown dialect. Pick one of: "; print STDERR "$_ " foreach sort keys %{$template}; print STDERR "\n"; exit 1; } my @entries = (); @entries = load_entries($opts->{entries}) if ($opts->{entries}); @entries = handle_range($opts->{range_start}, $opts->{range_end}, $opts->{capacity}, @entries); if ($opts->{radius_db}) { &call_database($opts, $queries, @entries); } else { &output_sql($template->{$opts->{dialect}}, {ranges => [{pool_name => $opts->{pool_name}, ips => \@entries}], batchsize => 100, tablename => $opts->{table_name}}); } } sub process_yaml_file { my $opts = shift; unless (-r $opts->{yaml}) { die "ERROR: Cannot open for reading: $opts->{yaml}"; } my %pool_defs = %{LoadFile($opts->{yaml})}; (my $template, my $queries)=load_templates($opts->{table_name}); unless (defined $template->{$opts->{dialect}}) { print STDERR "Unknown dialect. Pick one of: "; print STDERR "$_ " foreach sort keys %{$template}; print STDERR "\n"; exit 1; } my @entries = (); @entries = load_entries($opts->{entries}) if ($opts->{entries}); my @ranges; foreach my $pool_name (sort keys %pool_defs) { foreach my $range (@{$pool_defs{$pool_name}}) { my $range_start = $range->{start}; my $range_end = $range->{end}; my $capacity = $range->{capacity}; my @ips = handle_range($range_start, $range_end, $capacity, @entries); push (@ranges, {pool_name => $pool_name, ips => \@ips}); } } if ($opts->{radius_db}) { &call_database($opts, $queries, @entries); } else { &output_sql($template->{$opts->{dialect}}, {ranges => \@ranges, batchsize => 100, tablename => $opts->{table_name}}); } } sub output_sql { my $template = shift(); my $vars = shift(); my $tt_available = 0; eval { require Template; $tt_available = 1; }; if ($tt_available) { my $tt=Template->new(); $tt->process(\$template, $vars) || die $tt->error(); } else { die "ERROR: Template Toolkit is not available. Install the Template Perl module."; } } sub call_database { my $opts = shift; my $queries = shift; my @entries = @_; my $dbi_avail = 0; eval { require DBI; $dbi_avail = 1; }; unless($dbi_avail) { die "ERROR: DBI is not available. Install the DBI Perl module."; } my $dsn; if ($opts->{dialect} eq 'mysql') { $dsn = "DBI:mysql:database=$opts->{radius_db};host=$opts->{server}"; if (defined($opts->{mysql}->{tls})) { $dsn .= ';mysql_ssl=1'; $dsn .= ';mysql_ssl_ca_file='.$opts->{mysql}->{tls}->{ca_file} if ($opts->{mysql}->{tls}->{ca_file}); $dsn .= ';mysql_ssl_ca_path='.$opts->{mysql}->{tls}->{ca_path} if ($opts->{mysql}->{tls}->{ca_path}); $dsn .= ';mysql_ssl_client_key='.$opts->{mysql}->{tls}->{private_key_file} if ($opts->{mysql}->{tls}->{private_key_file}); $dsn .= ';mysql_ssl_client_cert='.$opts->{mysql}->{tls}->{certificate_file} if ($opts->{mysql}->{tls}->{certificate_file}); $dsn .= ';mysql_ssl_cipher='.$opts->{mysql}->{tls}->{cipher} if ($opts->{mysql}->{tls}->{cipher}); } } elsif ($opts->{dialect} eq 'postgresql') { # Parse FreeRADIUS alternative connection string if ($opts->{radius_db} =~ m/host=(.+?)\b/) { $opts->{server} = $1; } if ($opts->{radius_db} =~ m/user=(.+?)\b/) { $opts->{login} = $1; } if ($opts->{radius_db} =~ m/password=(.+?)\b/) { $opts->{password} = $1; } if ($opts->{radius_db} =~ m/sslmode=(.+?)\b/) { $opts->{sslmode} = $1; } if ($opts->{radius_db} =~ m/dbname=(.+?)\b/) { $opts->{radius_db} = $1; } $dsn = "DBI:Pg:dbname=$opts->{radius_db};host=$opts->{server}"; # # DBD doesn't have all the options used by FreeRADIUS - just enable ssl if # FreeRADIUS has SSL options enabled # $dsn .= ';sslmode=prefer' if ($opts->{sslmode}); } elsif ($opts->{dialect} eq 'sqlite') { $dsn = "DBI:SQLite:dbname=$opts->{sqlite}->{filename}"; } elsif ($opts->{dialect} eq 'mssql') { if ($opts->{driver} eq 'rlm_sql_unixodbc') { $dsn = "DBI:ODBC:DSN=$opts->{server}"; } else { $dsn = "DBI:Sybase:server=$opts->{server};database=$opts->{radius_db}"; } } elsif ($opts->{dialect} eq 'oracle') { # Extract data from Oracle connection string as used by FreeRADIUS if ($opts->{radius_db} =~ m/HOST=(.+?)\)/) { $opts->{server} = $1; } if ($opts->{radius_db} =~ m/PORT=(.+?)\)/) { $opts->{port} =$1; } if ($opts->{radius_db} =~ m/SID=(.+?)\)/) { $opts->{sid} = $1; } $dsn = "DBI:Oracle:host=$opts->{server};sid=$opts->{sid}"; } else { $dsn = "DBI:$opts->{dialect}:database=$opts->{radius_db};host=$opts->{server}"; } $dsn .= ";port=$opts->{port}" if ($opts->{port}) && ($opts->{driver} ne 'rlm_sql_unixodbc'); # Read the results by running our query against the database my $dbh = DBI->connect($dsn, $opts->{login}, $opts->{password}) || die "Unable to connect to database"; foreach my $query (@{$queries->{$opts->{dialect}}->{pre}}) { $dbh->do($query); } my $sth = $dbh->prepare($queries->{$opts->{dialect}}->{insert}); foreach my $ip (@entries) { $sth->execute($opts->{pool_name}, $ip); } $sth->finish(); foreach my $query (@{$queries->{$opts->{dialect}}->{post}}) { $dbh->do($query); } $dbh->disconnect(); } sub load_entries { my $entries_file = shift; my @entries = (); unless (-r $entries_file) { die "ERROR: Cannot open for reading: $entries_file" } open(my $fh, "<", $entries_file) || die "Failed to open $entries_file"; while(<$fh>) { chomp; push @entries, $_; } return @entries; } sub handle_range { my $range_start = shift; my $range_end = shift; my $capacity = shift; my @entries = @_; unless (ip_is_ipv4($range_start) || ip_is_ipv6($range_start)) { die "ERROR: Incorrectly formatted IPv4/IPv6 address for range_start: $range_start"; } unless (ip_is_ipv4($range_end) || ip_is_ipv6($range_end)) { die "ERROR: Incorrectly formatted IPv4/IPv6 address for range_end: $range_end"; } my $ip_start = new Net::IP($range_start); my $ip_end = new Net::IP($range_end); my $ip_range = new Net::IP("$range_start - $range_end"); unless (defined $ip_range) { die "ERROR: The range defined by - is invalid: $range_start - $range_end"; } my $range_size = $ip_range->size; if ($range_size < $capacity) { $capacity = "$range_size"; warn 'WARNING: Insufficent IPs in the range. Will create '.$capacity.' entries.'; } # Prune the entries to only those within the specified range for (my $i = 0; $i <= $#entries; $i++) { my $version = ip_is_ipv4($entries[$i]) ? 4 : 6; my $binip = ip_iptobin($entries[$i],$version); if ($ip_start->version != $version || ip_bincomp($binip, 'lt', $ip_start->binip) == 1 || ip_bincomp($binip, 'gt', $ip_end->binip) == 1) { $entries[$i]=''; } } # # We use the sparse method if the number of entries available occupies < 80% of # the network range, otherwise we use a method that involves walking the # entire range. # srand(42); # Set the seed for the PRNG if ($capacity / "$range_size" > 0.9) { @entries = walk_fill($ip_start, $ip_end, $capacity, @entries); } elsif (length($range_size) > 9 || $capacity / "$range_size" < 0.8) { # From "BigInt" to FP @entries = sparse_fill($ip_start, $ip_end, $capacity, @entries); } else { @entries = dense_fill($ip_start, $ip_end, $ip_range, $capacity, @entries); } return @entries; } # # With this sparse fill method we randomly allocate within the scope of the # smallest enclosing network prefix, checking that we are within the given # range, retrying if we are outside or we hit a duplicate. # # This method can efficiently choose a small number of addresses relative to # the size of the range. It becomes slower as the population of a range nears # the range's limit since it is harder to choose a free address at random. # # It is useful for selecting a handful of addresses from an enourmous IPv6 /64 # network for example. # sub sparse_fill { my $ip_start = shift; my $ip_end = shift; my $capacity = shift; my @entries = @_; # Find the smallest network that encloses the given range my $version = $ip_start->version; ( $ip_start->binip ^ $ip_end->binip ) =~ /^\0*/; my $net_prefix = $+[0]; my $net_bits = substr($ip_start->binip, 0, $net_prefix); my $host_length = length($ip_start->binip) - $net_prefix; my %ips = (); my $i = 0; while ($i < $capacity) { # Use the given entries first my $rand_ip; my $given_lease = 0; shift @entries while $#entries >= 0 && $entries[0] eq ''; if ($#entries >= 0) { $rand_ip = ip_iptobin(shift @entries, $version); $given_lease = 1; } else { $rand_ip = $net_bits; $rand_ip .= [0..1]->[rand 2] for 1..$host_length; # Check that we are inside the given range next if ip_bincomp($rand_ip, 'lt', $ip_start->binip) == 1 || ip_bincomp($rand_ip, 'gt', $ip_end->binip) == 1; } next if defined $ips{$rand_ip}; $ips{$rand_ip} = $given_lease ? '=' : '+'; $i++; } return map { ip_bintoip($_, $version) } keys %ips; } # # With this dense fill method, after first selecting the given entries we walk # the network range picking IPs with evenly distributed probability. # # This method can efficiently choose a large number of addresses relative to # the size of a range, provided that the range isn't massive. It becomes # slower as the range size increases. # sub dense_fill { my $ip_start = shift; my $ip_end = shift; my $ip_range = shift; my $capacity = shift; my @entries = @_; my $version = $ip_start->version; my $one = ("0"x($version == 4 ? 31 : 127)) . '1'; my %ips = (); my $remaining_entries = $capacity; my $remaining_ips = $ip_range->size; my $ipbin = $ip_start->binip; while ($remaining_entries > 0 && (ip_bincomp($ipbin, 'le', $ip_end->binip) == 1)) { # Use the given entries first shift @entries while $#entries >= 0 && $entries[0] eq ''; if ($#entries >= 0) { $ips{ip_iptobin(shift @entries, $version)} = '='; $remaining_entries--; $remaining_ips--; next; } goto next_ip if defined $ips{$ipbin}; # Skip the IP that we have already selected by given entries, otherwise # randomly pick it if (!defined $ips{$ipbin} && (rand) <= $remaining_entries / "$remaining_ips") { # From "BigInt" to FP $ips{$ipbin} = '+'; $remaining_entries--; } $remaining_ips--; $ipbin = ip_binadd($ipbin,$one); } return map { ip_bintoip($_, $version) } keys %ips; } # # With this walk fill method we walk the IP range from the beginning # for as many IPs as are required # # It is useful for selecting a fully populated network. # sub walk_fill { my $ip_start = shift; my $ip_end = shift; my $capacity = shift; my @entries = @_; my $version = $ip_start->version; my $one = ("0"x($version == 4 ? 31 : 127)) . '1'; my %ips = (); my $remaining_entries = $capacity; my $ipbin = $ip_start->binip; # Sort existing IPs and remove any blank entries. Allows existing entries to be # matched quickly in the new pool my @sorted_entries = sort @entries; shift @sorted_entries while $#sorted_entries >= 0 && $sorted_entries[0] eq ''; # Walk through the IP range from the beginning while ($remaining_entries > 0 && (ip_bincomp($ipbin, 'le', $ip_end->binip) == 1)) { if ($#sorted_entries >= 0) { # If there are existing entries check if they match $ips{$ipbin} = (ip_bincomp($ipbin, 'eq', ip_iptobin($sorted_entries[0]) == 1) && shift(@sorted_entries) ? '=' : '+'); } else { $ips{$ipbin} = '+'; } $remaining_entries--; $ipbin = ip_binadd($ipbin,$one); } return map { ip_bintoip($_, $version) } keys %ips; } # # SQL dialect templates # sub load_templates { my $tablename = shift; my $template; my $queries; # # MySQL / MariaDB # $queries->{'mysql'}->{pre} = [ 'DROP TEMPORARY TABLE IF EXISTS '.$tablename.'_temp;', 'CREATE TEMPORARY TABLE '.$tablename.'_temp ( id int(11) unsigned NOT NULL auto_increment, pool_name varchar(30) NOT NULL, framedipaddress varchar(15) NOT NULL, PRIMARY KEY (id), KEY pool_name_framedipaddress (pool_name,framedipaddress) );' ]; $queries->{'mysql'}->{insert} = 'INSERT INTO '.$tablename.'_temp (pool_name,framedipaddress) VALUES (?, ?)'; $queries->{'mysql'}->{post} = [ 'START TRANSACTION;', 'DELETE r FROM '.$tablename.' r LEFT JOIN '.$tablename.'_temp t USING (pool_name,framedipaddress) WHERE t.id IS NULL;', 'INSERT INTO '.$tablename.' (pool_name,framedipaddress) SELECT pool_name,framedipaddress FROM '.$tablename.'_temp t WHERE NOT EXISTS ( SELECT * FROM '.$tablename.' r WHERE r.pool_name=t.pool_name AND r.framedipaddress=t.framedipaddress );', 'COMMIT;' ]; $template->{'mysql'} = join("\n", @{$queries->{'mysql'}->{pre}})."\n"; $template->{'mysql'} .= <<'END_mysql'; -- Populate the temporary table [%- FOREACH r IN ranges %] [%- FOREACH i IN r.ips %] [%- "\n\nINSERT INTO ${tablename}_temp (pool_name,framedipaddress) VALUES" IF loop.index % batchsize == 0 %] [%- IF (loop.index+1) % batchsize == 0 OR loop.last %] ('[% r.pool_name %]','[% i %]'); [%- ELSE %] ('[% r.pool_name %]','[% i %]'), [%- END %] [%- END %] [%- END %] END_mysql $template->{'mysql'} .= join("\n", @{$queries->{'mysql'}->{post}})."\n"; # # PostgreSQL # $queries->{'postgresql'}->{pre} = [ 'DROP TABLE IF EXISTS '.$tablename.'_temp;', 'CREATE TEMPORARY TABLE '.$tablename.'_temp ( pool_name varchar(64) NOT NULL, FramedIPAddress INET NOT NULL );', 'CREATE INDEX '.$tablename.'_temp_idx ON '.$tablename.'_temp USING btree (pool_name,FramedIPAddress);' ]; $queries->{'postgresql'}->{insert} = 'INSERT INTO '.$tablename.'_temp (pool_name,framedipaddress) VALUES (?, ?)'; $queries->{'postgresql'}->{post} = [ 'START TRANSACTION;', 'DELETE FROM '.$tablename.' r WHERE NOT EXISTS ( SELECT FROM '.$tablename.'_temp t WHERE t.pool_name = r.pool_name AND t.framedipaddress = r.framedipaddress );', 'INSERT INTO '.$tablename.' (pool_name,framedipaddress) SELECT pool_name,framedipaddress FROM '.$tablename.'_temp t WHERE NOT EXISTS ( SELECT * FROM '.$tablename.' r WHERE r.pool_name=t.pool_name AND r.framedipaddress=t.framedipaddress );', 'COMMIT;' ]; $template->{'postgresql'} = join("\n", @{$queries->{'postgresql'}->{pre}})."\n"; $template->{'postgresql'} .= <<'END_postgresql'; -- Populate the temporary table [%- FOREACH r IN ranges %] [%- FOREACH i IN r.ips %] [%- "\n\nINSERT INTO ${tablename}_temp (pool_name,framedipaddress) VALUES" IF loop.index % batchsize == 0 %] [%- IF (loop.index+1) % batchsize == 0 OR loop.last %] ('[% r.pool_name %]','[% i %]'); [%- ELSE %] ('[% r.pool_name %]','[% i %]'), [%- END %] [%- END %] [%- END %] END_postgresql $template->{'postgresql'} .= join("\n", @{$queries->{'postgresql'}->{post}})."\n"; # # Oracle # $queries->{'oracle'}->{pre} = [ 'CREATE TABLE '.$tablename.'_temp ( pool_name VARCHAR(30) NOT NULL, FramedIPAddress VARCHAR(15) NOT NULL )', 'CREATE INDEX '.$tablename.'_temp_idx ON '.$tablename.'_temp (pool_name,FramedIPAddress)' ]; $queries->{'oracle'}->{insert} = 'INSERT INTO '.$tablename.'_temp (pool_name,FramedIPAddress) VALUES (?, ?)'; $queries->{'oracle'}->{post} = [ 'DELETE FROM '.$tablename.' r WHERE NOT EXISTS (SELECT * FROM '.$tablename.'_temp t WHERE r.pool_name = t.pool_name AND r.framedipaddress = t.framedipaddress)', 'INSERT INTO '.$tablename.' (pool_name,framedipaddress) SELECT pool_name,framedipaddress FROM '.$tablename.'_temp t WHERE NOT EXISTS ( SELECT * FROM '.$tablename.' r WHERE r.pool_name=t.pool_name AND r.framedipaddress=t.framedipaddress )', 'DROP TABLE '.$tablename.'_temp', 'COMMIT' ]; $template->{'oracle'} = join(";\n", @{$queries->{'oracle'}->{pre}}).";\n"; $template->{'oracle'} .= <<'END_oracle'; -- Populate the temporary table [%- FOREACH r IN ranges %] [%- FOREACH i IN r.ips %] [%- "\nINSERT INTO ${tablename}_temp (pool_name,framedipaddress) VALUES " %]('[% r.pool_name %]','[% i %]'); [%- END %] [%- END %] END_oracle $template->{'oracle'} .= join(";\n", @{$queries->{'oracle'}->{post}})."\n"; # # SQLite # $queries->{'sqlite'}->{pre} = [ 'DROP TABLE IF EXISTS '.$tablename.'_temp;', 'CREATE TABLE '.$tablename.'_temp ( pool_name varchar(30) NOT NULL, framedipaddress varchar(15) NOT NULL );', 'CREATE INDEX '.$tablename.'_temp_idx ON '.$tablename.'_temp (pool_name,FramedIPAddress);' ]; $queries->{'sqlite'}->{insert} = 'INSERT INTO '.$tablename.'_temp (pool_name,framedipaddress) VALUES (?, ?)'; $queries->{'sqlite'}->{post} = [ 'BEGIN TRANSACTION;', 'DELETE FROM '.$tablename.' WHERE rowid IN ( SELECT r.rowid FROM '.$tablename.' r LEFT JOIN '.$tablename.'_temp t USING (pool_name,framedipaddress) WHERE t.rowid IS NULL);', 'INSERT INTO '.$tablename.' (pool_name,framedipaddress) SELECT pool_name,framedipaddress FROM '.$tablename.'_temp t WHERE NOT EXISTS ( SELECT * FROM '.$tablename.' r WHERE r.pool_name=t.pool_name AND r.framedipaddress=t.framedipaddress );', 'COMMIT;', 'DROP TABLE '.$tablename.'_temp;' ]; $template->{'sqlite'} = join("\n", @{$queries->{'sqlite'}->{pre}})."\n"; $template->{'sqlite'} .= <<'END_sqlite'; -- Populate the temporary table [%- FOREACH r IN ranges %] [%- FOREACH i IN r.ips %] [%- "\n\nINSERT INTO ${tablename}_temp (pool_name,framedipaddress) VALUES" IF loop.index % batchsize == 0 %] [%- IF (loop.index+1) % batchsize == 0 OR loop.last %] ('[% r.pool_name %]','[% i %]'); [%- ELSE %] ('[% r.pool_name %]','[% i %]'), [%- END %] [%- END %] [%- END %] END_sqlite $template->{'sqlite'} .= join("\n", @{$queries->{'sqlite'}->{post}})."\n"; # # MS SQL # $queries->{'mssql'}->{pre} = [ 'DROP TABLE IF EXISTS #'.$tablename.'_temp;', 'CREATE TABLE #'.$tablename.'_temp ( id int identity(1, 1) NOT NULL, pool_name varchar(30) NOT NULL, framedipaddress varchar(15) NOT NULL, PRIMARY KEY (id), );', 'CREATE INDEX pool_name_framedipaddress ON #'.$tablename.'_temp(pool_name, framedipaddress);' ]; $queries->{'mssql'}->{insert} = 'INSERT INTO #'.$tablename.'_temp (pool_name, framedipaddress) VALUES (?, ?)'; $queries->{'mssql'}->{post} = [ 'BEGIN TRAN;', 'DELETE r FROM '.$tablename.' r LEFT JOIN #'.$tablename.'_temp t ON r.pool_name = t.pool_name AND r.framedipaddress = t.framedipaddress WHERE t.id IS NULL;', 'INSERT INTO '.$tablename.' (pool_name,framedipaddress) SELECT pool_name,framedipaddress FROM #'.$tablename.'_temp t WHERE NOT EXISTS ( SELECT * FROM '.$tablename.' r WHERE r.pool_name=t.pool_name AND r.framedipaddress=t.framedipaddress );', 'COMMIT TRAN;' ]; $template->{'mssql'} = join("\nGO\n", @{$queries->{'mssql'}->{pre}})."\nGO\n"; $template->{'mssql'} .= <<'END_mssql'; -- Populate the temporary table [%- FOREACH r IN ranges %] [%- FOREACH i IN r.ips %] [%- "\n\nINSERT INTO #${tablename}_temp (pool_name,framedipaddress) VALUES" IF loop.index % batchsize == 0 %] [%- IF (loop.index+1) % batchsize == 0 OR loop.last %] ('[% r.pool_name %]','[% i %]'); GO [%- ELSE %] ('[% r.pool_name %]','[% i %]'), [%- END %] [%- END %] [% END %] END_mssql $template->{'mssql'} .= join("\n", @{$queries->{'mssql'}->{post}})."\n"; return ($template, $queries); }