diff --git a/ncm-network/src/main/pan/components/network/core-schema.pan b/ncm-network/src/main/pan/components/network/core-schema.pan index 6541ef10cb..f37cce2719 100644 --- a/ncm-network/src/main/pan/components/network/core-schema.pan +++ b/ncm-network/src/main/pan/components/network/core-schema.pan @@ -5,6 +5,41 @@ declaration template components/network/core-schema; include 'pan/types'; include 'quattor/functions/network'; +@documentation{ + IPv4/IPv6 prefix notation +} +function is_ip_prefix = { + if (ARGC != 1 || !is_string(ARGV[0])) { + error("Usage: is_interface_name(string)"); + }; + if (!is_ip(ARGV[0])) { + slash = index("/", ARGV[0]); + if (slash < 1) { + return(false); + }; + pfx = to_long(substr(ARGV[0], slash + 1)); + if (is_ipv4(substr(ARGV[0], 0, slash))) { + if (pfx < 1 || pfx > 32) { + return(false); + }; + return(true); + }; + if (is_ipv6(substr(ARGV[0], 0, slash))) { + if (pfx < 1 || pfx > 128) { + return(false); + }; + return(true); + }; + return(false); + }; + return(true); +}; + +@documentation{ + IPv4/IPv6 prefix notation +} +type type_ip_prefix = string with is_ip_prefix(SELF); + @documentation{ Route } @@ -14,6 +49,149 @@ type structure_route = { "gateway" ? type_ip }; +@documentation{ + Single byte +} +# TODO: This should likely be defined elsewhere +type type_byte = long with { SELF >= 0 && SELF <= 255; }; + +@documentation{ + Define the conversion from human-readable names to numeric values used by iproute and the kernel +} +# TODO: disallow overriding values which are hardcoded in the 'ip' tool +type structure_rt_table = { + "dsfield" ? type_byte{} + "protos" ? type_byte{} + "realms" ? type_byte{} + "scopes" ? type_byte{} + "tables" ? type_byte{} +}; + +@documentation{ + Type of the routing entry +} +type type_route_type = string with match(SELF, '^(local|broadcast|anycast|multicast|unicast|prohibit|unreachable|blackhole|throw|nop)$'); + +@documentation{ + Type Of Service (TOS), as defined in /etc/iproute2/rt_dsfield +} +type type_rt_dsfield = string with exists("/system/network/rt_tables/dsfield/" + SELF); + +@documentation{ + Routing protocol identifier - either a built-in value, or a value defined in /etc/iproute2/rt_protos +} +type type_rt_proto = string with { + # Accept the values hardcoded into the 'ip' tool as well as user-defined ones + match(SELF, '^(none|redirect|kernel|boot|static|gated|ra|mrt|zebra|bird|dnrouted|xorp|ntk|dhcp)$') || + exists("/system/network/rt_tables/protos/" + SELF); +}; + +@documentation{ + Route realm +} +# TODO: I cut some corners, and this type is usabe for verifying both the "realm REALMID" and the "realms FROMREALM/TOREALM" constructs +type type_rt_realm = string with { + if (exists("/system/network/rt_tables/realms/" + SELF)) { + return(true); + }; + slash = index("/", SELF); + if (slash < 1) { + return(false); + }; + exists("/system/network/rt_tables/realms/" + substr(SELF, 0, slash)) && + exists("/system/network/rt_tables/realms/" + substr(SELF, slash + 1)); +}; + +@documentation{ + Route scope +} +type type_rt_scope = string with { + # Accept the values hardcoded into the 'ip' tool as well as user-defined ones + match(SELF, '^(nowhere|host|link|site)$') || exists("/system/network/rt_tables/scopes/" + SELF); +}; + +@documentation{ + Reference to a routing table +} +type type_rt_table = string with { + # Accept the values hardcoded into the 'ip' tool as well as user-defined ones + match(SELF, '^(default|main|local)$') || exists("/system/network/rt_tables/tables/" + SELF); +}; + +@documentation{ + Routing-related time specification +} +# If no unit is specified, the default is jiffies +type type_rt_time = string with match(SELF, '^\d+(s|ms|us|ns|j)?$'); + +@documentation{ + Routing rule specification +} +type structure_policy_rule = { + "not" ? boolean # negate the rule + "from" ? type_ip_prefix + "to" ? type_ip_prefix + "priority" ? long # also known as "preference" or "order" + "tos" ? type_rt_dsfield + "fwmark" ? long + "fwmask" ? long # cmdline is: fwmark [/] + "realms" ? type_rt_realm + "table" ? type_rt_table # also known as "lookup" + "dev" ? valid_interface # also known as "iif" + "type" ? type_route_type + "goto" ? long +}; + +@documentation{ + Route next hop specification +} +type structure_nexthop = { + "via" ? type_ip + "dev" ? valid_interface + "weight" ? long + "onlink" ? boolean + "realms" ? type_rt_realm +}; + +type structure_policy_route = { + "to" ? string with { SELF == "default" || is_ip_prefix(SELF); } + "src" ? string + "via" ? type_ip + "from" ? type_ip_prefix + "tos" ? type_rt_dsfield # also known as "dsfield" + "priority" ? long # also known as "metric" or "preference" + "scope" ? type_rt_scope + "mtu" ? long + "lock_mtu" ? boolean + "hoplimit" ? long + "lock_hoplimit" ? boolean + "advmss" ? long + "lock_advmss" ? boolean + "reordering" ? long + "lock_reordering" ? boolean + "rtt" ? type_rt_time + "lock_rtt" ? boolean + "window" ? long + "lock_window" ? boolean + "cwnd" ? long + "lock_cwnd" ? boolean + "initcwnd" ? long + "lock_initcwnd" ? boolean + "rttvar" ? type_rt_time + "lock_rttvar" ? boolean + "rto_min" ? type_rt_time + "sstresh" ? long + "lock_sstresh" ? boolean + "realms" ? string + "onlink" ? boolean + "equalize" ? boolean + "nexthop" ? structure_nexthop[] + "protocol" ? string + "table" ? string + "dev" ? valid_interface # also known as "oif" + "type" ? type_route_type +}; + @documentation{ Interface alias } @@ -28,11 +206,24 @@ type structure_interface_alias = { Describes the bonding options for configuring channel bonding on EL5 and similar. } type structure_bonding_options = { - "mode" : long(0..6) + "ad_select" ? string with match(SELF, '^(\d+|stable|bandwidth|count)$') + "all_slaves_active" ? long + "arp_interval" ? long + "arp_ip_target" ? type_ip[] + "arp_validate" ? string with match(SELF, '^(\d+|none|active|backup|all)$') + "downdelay" ? long + "fail_over_mac" ? string with match(SELF, '^(\d+|none|active|follow)$') + "min_links" ? long + "use_carrier" ? long + "num_grat_arp" ? long + "num_unsol_na" ? long + "resend_igmp" ? long + "mode" : long(0..6) # TODO: allow mode to be given as a string "miimon" : long "updelay" ? long "downdelay" ? long "primary" ? valid_interface + "primary_reselect" ? string with match(SELF, '^(\d+|always|better|failure)$') "lacp_rate" ? long(0..1) "xmit_hash_policy" ? string with match (SELF, '^(0|1|2|layer(2|2\+3|3\+4))$') } with { @@ -125,6 +316,8 @@ type structure_interface = { "master" ? string "mtu" ? long "route" ? structure_route[] + "policy_rule" ? structure_policy_rule[] + "policy_route" ? structure_policy_route[] "aliases" ? structure_interface_alias{} "set_hwaddr" ? boolean "bridge" ? valid_interface diff --git a/ncm-network/src/main/pan/components/network/schema.pan b/ncm-network/src/main/pan/components/network/schema.pan index c8f229074c..8d17913fbc 100755 --- a/ncm-network/src/main/pan/components/network/schema.pan +++ b/ncm-network/src/main/pan/components/network/schema.pan @@ -10,6 +10,12 @@ include 'quattor/schema'; type component_network_type = { include structure_component + # Name of the auto-generated udev rule which needs to be removed to allow udev re-consider the interface names + "udev_rules_file" ? string + # E.g. "/usr/bin/udevadm trigger", although restricting the scope to just network devices should not hurt + "udev_trigger_command" ? string + # E.g. "/usr/bin/udevadm settle" + "udev_settle_command" ? string }; diff --git a/ncm-network/src/main/perl/network.pm b/ncm-network/src/main/perl/network.pm index a13eb99ae1..a524d709a0 100755 --- a/ncm-network/src/main/perl/network.pm +++ b/ncm-network/src/main/perl/network.pm @@ -14,7 +14,7 @@ use vars qw(@ISA $EC); @ISA = qw(NCM::Component); $EC=LC::Exception::Context->new->will_store_all; -use EDG::WP4::CCM::Element; +use EDG::WP4::CCM::Element qw(LIST); use EDG::WP4::CCM::Fetch qw(NOQUATTOR_EXITCODE); use NCM::Check; @@ -60,6 +60,52 @@ my %ethtool_option_map = ( my $ethtoolcmd = "/usr/sbin/ethtool"; +# These values are hardcoded in the 'ip' tool +my %rt_builtins = ( + "dsfield" => {}, + "protos" => { + 0 => "unspec", + 1 => "redirect", + 2 => "kernel", + 3 => "boot", + 4 => "static", + 8 => "gated", + 9 => "ra", + 10 => "mrt", + 11 => "zebra", + 12 => "bird", + 13 => "dnrouted", + 14 => "xorp", + 15 => "ntk", + 16 => "dhcp", + }, + "realms" => {}, + "scopes" => { + 0 => "global", + 255 => "nowhere", + 254 => "host", + 253 => "link", + 200 => "site", + }, + "tables" => { + 0 => "unspec", + 255 => "local", + 254 => "main", + 253 => "default", + }, +); + +my %mask_to_prefix = ( + "128.0.0.0" => 1, "192.0.0.0" => 2, "224.0.0.0" => 3, "240.0.0.0" => 4, + "248.0.0.0" => 5, "252.0.0.0" => 6, "254.0.0.0" => 7, "255.0.0.0" => 8, + "255.128.0.0" => 9, "255.192.0.0" => 10, "255.224.0.0" => 11, "255.240.0.0" => 12, + "255.248.0.0" => 13, "255.252.0.0" => 14, "255.254.0.0" => 15, "255.255.0.0" => 16, + "255.255.128.0" => 17, "255.255.192.0" => 18, "255.255.224.0" => 19, "255.255.240.0" => 20, + "255.255.248.0" => 21, "255.255.252.0" => 22, "255.255.254.0" => 23, "255.255.255.0" => 24, + "255.255.255.128" => 25, "255.255.255.192" => 26, "255.255.255.224" => 27, "255.255.255.240" => 28, + "255.255.255.248" => 29, "255.255.255.252" => 30, "255.255.255.254" => 31, "255.255.255.255" => 32 +); + use constant FAILED_SUFFIX => '-failed'; # gen_backup_filename: returns backup filename for given file @@ -186,7 +232,7 @@ sub Configure while ($iface->hasNextElement()) { $element = $iface->getNextElement(); $elementname = $element->getName(); - if ($elementname =~ m/route|aliases/) { + if ($elementname =~ m/^(route|aliases|policy_route|policy_rule)$/) { while ($element->hasNextElement()) { $el = $element->getNextElement(); ## number of route OR name of alias @@ -195,7 +241,11 @@ sub Configure while ($el->hasNextElement()) { $l = $el->getNextElement(); $ln = $l->getName(); - $tmp_el{$ln} = $l->getValue(); + if ($elementname eq 'policy_route' && $ln eq 'nexthop') { + $tmp_el{$ln} = $l->getTree(); + } else { + $tmp_el{$ln} = $l->getValue(); + } } if ($elementname =~ m/aliases/) { # overwrite the alias name @@ -263,12 +313,15 @@ sub Configure # read current config my $dir_pref="/etc/sysconfig/network-scripts"; opendir(DIR, $dir_pref); - # here's the reason why it only verifies eth, bond, bridge, usb and vlan - # devices. add regexp at will - my $dev_regexp='-((eth|seth|em|bond|br|ovirtmgmt|vlan|usb|ib|p\d+p|en(o|(p\d+)?s(?:\d+f)?(?:\d+d)?))\d+|enx[[:xdigit:]]{12})(\.\d+)?'; + my $dev_regexp='(?:ifcfg|route|rule)-(\w+\d*(\.\d+)?)'; # $1 is the device name foreach my $file (grep(/$dev_regexp/, readdir(DIR))) { my $msg; + + # Do not mess with the loopback interface + # TODO: aliases on the loopback could be handled by ncm-network + next if $file =~ m/^ifcfg-lo(:.*)?$/; + if ( -l "$dir_pref/$file" ) { # keep the links separate $exilinks{"$dir_pref/$file"} = readlink("$dir_pref/$file"); @@ -548,7 +601,7 @@ sub Configure # route config, interface based. # hey, where are the static routes? - if (exists($net{$iface}{route})) { + if (exists($net{$iface}{route}) && !exists($net{$iface}{policy_route})) { $file_name = "$dir_pref/route-$iface"; $exifiles{$file_name} = 1; $text = ""; @@ -570,6 +623,56 @@ sub Configure }; $exifiles{$file_name} = $self->file_dump($file_name, $text, FAILED_SUFFIX); $self->debug(3, "exifiles $file_name has value ".$exifiles{$file_name}); + } elsif (exists($net{$iface}{policy_route})) { + $file_name = "$dir_pref/route-$iface"; + $exifiles{$file_name} = 1; + $text=""; + + # We can't mix different syntaxes in the same file, so convert + # simple routes to the policy routing syntax + if (exists($net{$iface}{route})) { + foreach my $rt (sort keys %{$net{$iface}{route}}) { + my $route = {"dev" => $iface}; + + my $netmask = $net{$iface}{route}{$rt}{'netmask'}; + + if ($net{$iface}{route}{$rt}{'address'}) { + my $address = $net{$iface}{route}{$rt}{'address'}; + my $prefix = 32; + if ($net{$iface}{route}{$rt}{'netmask'}) { + my $netmask = $net{$iface}{route}{$rt}{'netmask'}; + if (exists($mask_to_prefix{$netmask})) { + $prefix = $mask_to_prefix{$netmask}; + } else { + $self->warn("Invalid netmask $netmask, skipping route"); + next; + } + } + $route->{'to'} = "$address/$prefix"; + } + if ($net{$iface}{route}{$rt}{'gateway'}) { + $route->{'via'} = $net{$iface}{route}{$rt}{'gateway'}; + } + $text .= generatePolicyRoute($route) . "\n"; + } + } + + foreach my $rt (sort keys %{$net{$iface}{policy_route}}) { + $text .= generatePolicyRoute($net{$iface}{policy_route}{$rt}) . "\n"; + } + $exifiles{$file_name} = $self->file_dump($file_name, $text, FAILED_SUFFIX); + $self->debug(3, "exifiles $file_name has value " . $exifiles{$file_name}); + } + # routing rules + if (exists($net{$iface}{policy_rule})) { + $file_name = "$dir_pref/rule-$iface"; + $exifiles{$file_name} = 1; + $text=""; + foreach my $rule (sort keys %{$net{$iface}{policy_rule}}) { + $text .= generatePolicyRule($net{$iface}{policy_rule}{$rule}) . "\n"; + } + $exifiles{$file_name} = $self->file_dump($file_name, $text, FAILED_SUFFIX); + $self->debug(3,"exifiles $file_name has value ".$exifiles{$file_name}); } # set up aliases for interfaces # on file per alias @@ -718,6 +821,25 @@ sub Configure $self->debug(3, "exifiles $file_name has value ".$exifiles{$file_name}); + ### + ### /etc/iproute2/rt_* + ### + + $path = $base_path; + foreach my $table (keys %rt_builtins) { + next unless $config->elementExists($path . "/rt_tables/" . $table); + + $file_name = "/etc/iproute2/rt_" . $table; + mk_bu($file_name); + $exifiles{$file_name} = -1; + my $element = $config->getElement($path . "/rt_tables/" . $table); + my $prefer_hex = $table eq 'dsfield'; + $text = updateRtTable($file_name, $element, $rt_builtins{$table}, $prefer_hex); + $exifiles{$file_name} = $self->file_dump($file_name, $text, FAILED_SUFFIX); + $self->debug(3, "exifiles $file_name has value " . $exifiles{$file_name}); + } + + # we now have a list with files and values. # for general network: separate? # for devices: create list of affected devices @@ -762,6 +884,8 @@ sub Configure } } elsif ($file eq "/etc/sysconfig/network") { # nothing needed + } elsif ($file =~ m!^/etc/iproute2/rt_!) { + # nothing needed } else { $self->error("Filename $file found that doesn't match the ", "regexp. Must be an error in ncm-network. ", @@ -809,20 +933,49 @@ sub Configure } else { foreach my $if (sort keys %ifdown) { # how do we actually know that the device was up? - # eg for non-existing device eth4: /sbin/ifdown eth4 --> usage: ifdown + # Do not try to down non-existing devices + next if (-d "/sys/class/net" && ! -e "/sys/class/net/$if"); push(@cmds, ["/sbin/ifdown", $if]); } } $self->runrun(@cmds); # replace modified/new files + my $was_change = 0; foreach my $file (keys %exifiles) { if (($exifiles{$file} == 1) || ($exifiles{$file} == 2)) { copy($self->gen_backup_filename($file).FAILED_SUFFIX,$file) || $self->error("replace modified/new files: can't copy ", $self->gen_backup_filename($file).FAILED_SUFFIX, " to $file. ($!)"); + $was_change = 1; } elsif ($exifiles{$file} == -1) { unlink($file) || $self->error("replace modified/new files: can't unlink $file. ($!)"); + $was_change = 1; + } + } + ## kick udev to rename devices if needed; do nothing if no files were changed + if ($was_change) { + # Remove the auto-generated rule file if present + if ($config->elementExists("/software/components/network/udev_rules_file")) { + my $file = $config->getElement("/software/components/network/udev_rules_file")->getValue(); + if (-f $file) { + $exifiles{$file} = -1; + mk_bu($file); + unlink($file); + } + else { + $exifiles{$file} = 2; + } + } + if ($config->elementExists("/software/components/network/udev_trigger_command")) { + my $cmd = $config->getElement("/software/components/network/udev_trigger_command")->getValue(); + $self->runrun([split(' ', $cmd)]); + # TODO: should the sleep be configurable? + sleep(2); + } + if ($config->elementExists("/software/components/network/udev_settle_command")) { + my $cmd = $config->getElement("/software/components/network/udev_settle_command")->getValue(); + $self->runrun([split(' ', $cmd)]); } } # ifup OR network start @@ -897,6 +1050,17 @@ sub Configure $self->error("Can't copy ".$self->gen_backup_filename($file)." to $file."); } } + ## kick udev to rename devices if needed + # Remove the auto-generated rule file if it was not present originally + if ($config->elementExists("/software/components/network/udev_trigger_command")) { + my $cmd = $config->getElement("/software/components/network/udev_trigger_command")->getValue(); + $self->runrun([split(' ', $cmd)]); + sleep(2); + } + if ($config->elementExists("/software/components/network/udev_settle_command")) { + my $cmd = $config->getElement("/software/components/network/udev_settle_command")->getValue(); + $self->runrun([split(' ', $cmd)]); + } # ifup OR network start $self->runrun([qw(/sbin/service network start)]); @@ -979,7 +1143,10 @@ sub Configure $output .= $self->runrun([qw(ls -ltr /etc/sysconfig/network-scripts)]); $output .= $self->runrun(["/sbin/ifconfig"]); + $output .= $self->runrun([qw(/sbin/ip addr list)]); $output .= $self->runrun([qw(/sbin/route -n)]); + $output .= $self->runrun([qw(/sbin/ip route list table all)]); + $output .= $self->runrun([qw(/sbin/ip rule list)]); # when brctl is missing, this would generate an error. # but it is harmless to skip the show command. @@ -1145,6 +1312,9 @@ sub Configure my @op; while (my ($k, $v) = each(%$opts)) { + if ($v->isType(LIST)) { + $v = join(",", @{$v}); + } push(@op, "$k=$v"); } @@ -1175,6 +1345,86 @@ sub Configure return "$st'" . join(' ', @op) . "'\n"; } + sub updateRtTable { + my ($filename, $element, $builtin, $prefer_hex) = @_; + + my $table = $element->getTree(); + + my %by_idx; + foreach my $name (keys %{$table}) { + $by_idx{$table->{$name}} = $name; + } + + my $text = ""; + my %seen; + if (-e $filename) { + open(FILE, $filename) or $self->error("can not open $filename"); + while (my $line = ) { + chomp $line; + if ($line =~ m/^(0x[[:xdigit:]]+|\d+)\s+(\S+)\s*$/) { + my $name = $2; + my $idx = $1; + $idx = hex($idx) if $idx =~ m/0x/; + + next unless (exists($by_idx{$idx}) && $name eq $by_idx{$idx}) or exists($builtin->{$idx}); + $seen{$idx} = 1; + } + $text .= $line . "\n"; + } + close FILE; + } + foreach my $name (sort keys %{$table}) { + next if exists($seen{$table->{$name}}); + if ($prefer_hex) { + $text .= sprintf("0x%02x\t", $table->{$name}) . $name . "\n"; + } else { + $text .= $table->{$name} . "\t" . $name . "\n"; + } + } + return $text; + } + + sub generatePolicyRoute { + my ($route) = @_; + my @args; + + foreach my $key (keys %{$route}) { + next if $key eq 'nexthop'; + next if $key =~ m/^lock_/; + my $locked = (exists($route->{'lock_' . $key}) && $route->{'lock_'. $key}); + push @args, $key; + push @args, "lock" if $locked; + push @args, $route->{$key}; + } + if (exists($route->{'nexthop'})) { + foreach my $nexthop (@{$route->{'nexthop'}}) { + push @args, 'nexthop'; + foreach my $key (keys %{$nexthop}) { + push @args, $key; + push @args, $nexthop->{$key}; + } + } + } + return join(' ', @args); + } + + sub generatePolicyRule { + my ($rule) = @_; + my @args; + + push @args, 'not' if exists($rule->{'not'}) && $rule->{'not'}; + foreach my $key (keys %{$rule}) { + next if $key eq 'fwmask' || $key eq 'not'; + my $value = $rule->{$key}; + if ($key eq 'fwmark') { + $value .= '/' . $rule->{'fwmask'} if exists($rule->{'fwmask'}); + } + push @args, $key; + push @args, $value; + } + return join(' ', @args); + } + # real end of Configure return 1;