diff --git a/.travis.yml b/.travis.yml index 1a9511d380..4aa1fc9639 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,6 +26,8 @@ stages: - Test - Build - Integration + - name: E2E + if: type IN (cron) OR (type IN (push) AND branch IN (master, dev)) jobs: include: @@ -71,6 +73,17 @@ jobs: - make verify-binapi - make integration-tests + - stage: E2E + env: VPP_VERSION=1904 + script: + - make e2e-tests + - env: VPP_VERSION=1908 + script: + - make e2e-tests + - env: VPP_VERSION=1901 + script: + - make e2e-tests + notifications: slack: rooms: diff --git a/Makefile b/Makefile index 7cf7766eb3..759a4fd5d7 100644 --- a/Makefile +++ b/Makefile @@ -135,6 +135,10 @@ integration-tests: @echo "=> running integration tests" VPP_IMG=$(VPP_IMG) ./tests/integration/vpp_integration.sh +e2e-tests: + @echo "=> running end-to-end tests" + VPP_IMG=$(VPP_IMG) ./tests/e2e/run_e2e.sh + # ------------------------------- # Code generation # ------------------------------- diff --git a/api/configurator/configurator.pb.go b/api/configurator/configurator.pb.go index 9e04684643..d4baff1b97 100644 --- a/api/configurator/configurator.pb.go +++ b/api/configurator/configurator.pb.go @@ -8,6 +8,7 @@ import ( fmt "fmt" proto "github.com/gogo/protobuf/proto" linux "github.com/ligato/vpp-agent/api/models/linux" + netalloc "github.com/ligato/vpp-agent/api/models/netalloc" vpp "github.com/ligato/vpp-agent/api/models/vpp" grpc "google.golang.org/grpc" math "math" @@ -26,11 +27,12 @@ const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package // Config groups all supported config data into single message. type Config struct { - VppConfig *vpp.ConfigData `protobuf:"bytes,1,opt,name=vpp_config,json=vppConfig,proto3" json:"vpp_config,omitempty"` - LinuxConfig *linux.ConfigData `protobuf:"bytes,2,opt,name=linux_config,json=linuxConfig,proto3" json:"linux_config,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + VppConfig *vpp.ConfigData `protobuf:"bytes,1,opt,name=vpp_config,json=vppConfig,proto3" json:"vpp_config,omitempty"` + LinuxConfig *linux.ConfigData `protobuf:"bytes,2,opt,name=linux_config,json=linuxConfig,proto3" json:"linux_config,omitempty"` + NetallocConfig *netalloc.ConfigData `protobuf:"bytes,3,opt,name=netalloc_config,json=netallocConfig,proto3" json:"netalloc_config,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *Config) Reset() { *m = Config{} } @@ -71,6 +73,13 @@ func (m *Config) GetLinuxConfig() *linux.ConfigData { return nil } +func (m *Config) GetNetallocConfig() *netalloc.ConfigData { + if m != nil { + return m.NetallocConfig + } + return nil +} + // Notification groups all notification data into single message. type Notification struct { // Types that are valid to be assigned to Notification: @@ -608,40 +617,42 @@ func init() { func init() { proto.RegisterFile("configurator/configurator.proto", fileDescriptor_150898d063c79000) } var fileDescriptor_150898d063c79000 = []byte{ - // 520 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x94, 0x41, 0x6f, 0xd3, 0x30, - 0x14, 0xc7, 0xe9, 0x36, 0x85, 0xf1, 0x92, 0x6e, 0xad, 0xd7, 0x43, 0x17, 0x90, 0x06, 0xbe, 0xb0, - 0x03, 0xa4, 0x68, 0x70, 0x00, 0xaa, 0xed, 0xb0, 0x56, 0x1a, 0x5c, 0x90, 0x88, 0xc4, 0x85, 0x03, - 0x55, 0xd6, 0xb8, 0x25, 0x52, 0x6a, 0x7b, 0x8d, 0x53, 0x75, 0xdf, 0x87, 0xcf, 0xc6, 0xe7, 0x40, - 0xf6, 0x73, 0x54, 0xbb, 0x2a, 0x3d, 0x6c, 0xea, 0xfb, 0xbf, 0xff, 0xfb, 0xbd, 0x17, 0x3f, 0xcb, - 0x70, 0x31, 0x15, 0x7c, 0x56, 0xcc, 0xeb, 0x65, 0xa6, 0xc4, 0x72, 0xe0, 0x06, 0x89, 0x5c, 0x0a, - 0x25, 0x48, 0xe4, 0x6a, 0x71, 0x6f, 0x21, 0x72, 0x56, 0x56, 0x83, 0x95, 0x94, 0xfa, 0x0f, 0x3d, - 0x71, 0xdf, 0xaa, 0x65, 0xc1, 0xeb, 0x35, 0xfe, 0xc7, 0x0c, 0xe5, 0x10, 0x8c, 0x4c, 0x3d, 0x49, - 0x00, 0x56, 0x52, 0x4e, 0x90, 0xd6, 0x6f, 0xbd, 0x6c, 0x5d, 0x86, 0x57, 0xa7, 0x89, 0x66, 0xa0, - 0x61, 0x9c, 0xa9, 0x2c, 0x7d, 0xb6, 0x92, 0xd2, 0xfa, 0x3f, 0x40, 0x64, 0x40, 0x4d, 0xc5, 0x81, - 0xa9, 0xe8, 0x26, 0x48, 0x77, 0x6a, 0x42, 0xa3, 0xa0, 0x40, 0xff, 0xb4, 0x20, 0xfa, 0x26, 0x54, - 0x31, 0x2b, 0xa6, 0x99, 0x2a, 0x04, 0x27, 0x37, 0xd0, 0xd1, 0x6d, 0xb9, 0xa3, 0xd9, 0xe6, 0x5d, - 0xd3, 0xdc, 0x35, 0x7f, 0x79, 0x92, 0x9e, 0xae, 0xa4, 0xf4, 0xea, 0xc7, 0x40, 0x70, 0x0c, 0x8f, - 0x80, 0xc3, 0x9c, 0xd9, 0x61, 0xb6, 0x18, 0x5d, 0xa3, 0xba, 0xe2, 0xed, 0x09, 0x44, 0x6e, 0x3d, - 0xfd, 0x05, 0xed, 0x1f, 0x32, 0xcf, 0x14, 0x4b, 0xd9, 0x43, 0xcd, 0x2a, 0x45, 0xde, 0x40, 0x50, - 0x1b, 0xc1, 0x0e, 0xd7, 0x4b, 0xbc, 0x55, 0xe0, 0xd7, 0xa5, 0xd6, 0x43, 0x2e, 0x20, 0x9c, 0xd5, - 0x65, 0x39, 0x59, 0xb2, 0xea, 0x91, 0x4f, 0xcd, 0x34, 0xc7, 0x29, 0x68, 0x29, 0x35, 0x0a, 0xed, - 0xc0, 0x49, 0xc3, 0xaf, 0xa4, 0xe0, 0x15, 0xa3, 0xd7, 0xd0, 0x1e, 0xb3, 0x92, 0x79, 0x1d, 0x73, - 0x23, 0xec, 0xef, 0x88, 0x1e, 0x0d, 0x6c, 0xca, 0x2d, 0x30, 0x02, 0xb8, 0x63, 0xca, 0xd2, 0xe8, - 0x10, 0x42, 0x13, 0x61, 0x52, 0xc3, 0xbd, 0x45, 0xff, 0x07, 0x8e, 0x22, 0x6d, 0x43, 0x38, 0xae, - 0x17, 0xb2, 0x61, 0x7d, 0x84, 0x08, 0x43, 0x0b, 0xbb, 0x84, 0xa3, 0xbc, 0x5e, 0xc8, 0xbd, 0x28, - 0xe3, 0xa0, 0xaf, 0xe1, 0xcc, 0x3d, 0xf6, 0xe6, 0x53, 0x3b, 0x70, 0x58, 0xe4, 0x6b, 0x53, 0xdf, - 0x4e, 0xf5, 0x4f, 0xfa, 0x00, 0x3d, 0xdf, 0x68, 0x5b, 0x9d, 0xc3, 0x31, 0x67, 0x6b, 0x35, 0xd9, - 0xd8, 0x9f, 0xea, 0xf8, 0x6b, 0xbe, 0x26, 0x37, 0xfe, 0x0a, 0xed, 0x15, 0x88, 0xfd, 0x69, 0x3c, - 0xa8, 0xe7, 0xbf, 0xfa, 0x7b, 0x00, 0xd1, 0xc8, 0xf1, 0x92, 0xcf, 0x70, 0x78, 0xc7, 0x14, 0xe9, - 0xfb, 0x84, 0xcd, 0x99, 0xc6, 0xe7, 0x3b, 0x32, 0x76, 0xce, 0x11, 0x04, 0xb8, 0x5f, 0xf2, 0xdc, - 0x37, 0x79, 0xb7, 0x2a, 0x7e, 0xb1, 0x3b, 0xb9, 0x81, 0xe0, 0x4e, 0xb7, 0x21, 0xde, 0x45, 0xd9, - 0x86, 0xf8, 0xd7, 0x80, 0x5c, 0xc3, 0x91, 0x5e, 0x16, 0xd9, 0x1a, 0xd6, 0xd9, 0x67, 0x1c, 0xef, - 0x4a, 0xd9, 0xf2, 0xef, 0x10, 0x98, 0x33, 0x7b, 0x24, 0xaf, 0xf6, 0x9c, 0xa4, 0x05, 0xd1, 0x7d, - 0x16, 0x04, 0xbe, 0x6b, 0xdd, 0x0e, 0x7f, 0x7e, 0x9a, 0x17, 0xea, 0x77, 0x7d, 0x9f, 0x4c, 0xc5, - 0x62, 0x50, 0x16, 0xf3, 0x4c, 0x09, 0xfd, 0x56, 0xbd, 0xcd, 0xe6, 0x8c, 0xab, 0x41, 0x26, 0x0b, - 0xef, 0x99, 0x1b, 0xba, 0xc1, 0x7d, 0x60, 0x9e, 0xad, 0xf7, 0xff, 0x02, 0x00, 0x00, 0xff, 0xff, - 0x51, 0xa8, 0x29, 0xab, 0x17, 0x05, 0x00, 0x00, + // 553 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x54, 0xcf, 0x6f, 0xd3, 0x30, + 0x18, 0xa5, 0xeb, 0x14, 0xc6, 0x97, 0xf4, 0x97, 0xd7, 0x43, 0x17, 0x10, 0x03, 0x5f, 0xd8, 0x01, + 0x52, 0x34, 0x38, 0x00, 0x55, 0x77, 0x58, 0x2b, 0x0d, 0x2e, 0x48, 0x44, 0xe2, 0xc2, 0x81, 0x2a, + 0x4b, 0xdc, 0x12, 0x29, 0xb5, 0xbd, 0xc6, 0xa9, 0xba, 0xff, 0x87, 0x2b, 0xff, 0x16, 0x7f, 0x07, + 0xf2, 0x8f, 0xac, 0x76, 0x55, 0x7a, 0x68, 0x15, 0xbf, 0xef, 0xbd, 0xf7, 0x3d, 0xfb, 0xb3, 0x0c, + 0xe7, 0x29, 0xa3, 0xf3, 0x7c, 0x51, 0xad, 0x12, 0xc1, 0x56, 0x43, 0x7b, 0x11, 0xf1, 0x15, 0x13, + 0x0c, 0x05, 0x36, 0x16, 0xf6, 0x97, 0x2c, 0x23, 0x45, 0x39, 0x5c, 0x73, 0x2e, 0x7f, 0x9a, 0x13, + 0x0e, 0x0c, 0x5a, 0xe4, 0xb4, 0xda, 0xe8, 0x7f, 0x53, 0x79, 0x6e, 0x2a, 0x94, 0x88, 0xa4, 0x28, + 0x58, 0xfa, 0xf0, 0xa1, 0xeb, 0xf8, 0x4f, 0x03, 0xbc, 0x89, 0x6a, 0x80, 0x22, 0x80, 0x35, 0xe7, + 0x33, 0xdd, 0x6e, 0xd0, 0x78, 0xd1, 0xb8, 0xf0, 0x2f, 0x3b, 0x91, 0x6c, 0xa2, 0x09, 0xd3, 0x44, + 0x24, 0xf1, 0x93, 0x35, 0xe7, 0x86, 0xff, 0x1e, 0x02, 0xd5, 0xa9, 0x56, 0x1c, 0x29, 0x45, 0x2f, + 0xd2, 0xed, 0x2d, 0x8d, 0xaf, 0x10, 0xa3, 0x1a, 0x43, 0xa7, 0x8e, 0x50, 0x0b, 0x9b, 0x4a, 0xd8, + 0x8f, 0x1e, 0xa2, 0x59, 0xda, 0x76, 0x0d, 0x6a, 0x0c, 0xff, 0x6e, 0x40, 0xf0, 0x95, 0x89, 0x7c, + 0x9e, 0xa7, 0x89, 0xc8, 0x19, 0x45, 0x57, 0xd0, 0x95, 0xa9, 0xa9, 0x85, 0x99, 0xec, 0x3d, 0x95, + 0xdd, 0x26, 0x7f, 0x7e, 0x14, 0x77, 0xd6, 0x9c, 0x3b, 0xfa, 0x29, 0x20, 0xbd, 0x0b, 0xc7, 0x41, + 0xef, 0xe5, 0xd4, 0xec, 0x65, 0xc7, 0xa3, 0xa7, 0x50, 0x1b, 0xbc, 0x6e, 0x43, 0x60, 0xeb, 0xf1, + 0x4f, 0x68, 0x7d, 0xe7, 0x59, 0x22, 0x48, 0x4c, 0xee, 0x2a, 0x52, 0x0a, 0xf4, 0x1a, 0xbc, 0x4a, + 0x01, 0x26, 0x5c, 0x3f, 0x72, 0x46, 0xad, 0x77, 0x17, 0x1b, 0x0e, 0x3a, 0x07, 0x7f, 0x5e, 0x15, + 0xc5, 0x6c, 0x45, 0xca, 0x7b, 0x9a, 0xaa, 0x34, 0x27, 0x31, 0x48, 0x28, 0x56, 0x08, 0xee, 0x42, + 0xbb, 0xf6, 0x2f, 0x39, 0xa3, 0x25, 0xc1, 0x63, 0x68, 0x4d, 0x49, 0x41, 0x9c, 0x8e, 0x99, 0x02, + 0x0e, 0x77, 0xd4, 0x1c, 0x69, 0x58, 0xcb, 0x8d, 0x61, 0x00, 0x70, 0x43, 0x84, 0x71, 0xc3, 0x23, + 0xf0, 0xd5, 0x4a, 0x17, 0xa5, 0xb9, 0x73, 0x4f, 0xfe, 0x63, 0xae, 0x41, 0xdc, 0x02, 0x7f, 0x5a, + 0x2d, 0x79, 0xed, 0xf5, 0x01, 0x02, 0xbd, 0x34, 0x66, 0x17, 0x70, 0x9c, 0x55, 0x4b, 0x7e, 0xd0, + 0x4a, 0x31, 0xf0, 0x2b, 0x38, 0xb5, 0x8f, 0xbd, 0xde, 0x6a, 0x17, 0x9a, 0x79, 0xb6, 0x51, 0xfa, + 0x56, 0x2c, 0x3f, 0xf1, 0x1d, 0xf4, 0x5d, 0xa2, 0x69, 0x75, 0x06, 0x27, 0x94, 0x6c, 0xc4, 0x6c, + 0x4b, 0x7f, 0x2c, 0xd7, 0x5f, 0xb2, 0x0d, 0xba, 0x72, 0x47, 0x68, 0xae, 0x40, 0xe8, 0xa6, 0x71, + 0x4c, 0x1d, 0xfe, 0xe5, 0xdf, 0x23, 0x08, 0x26, 0x16, 0x17, 0x7d, 0x82, 0xe6, 0x0d, 0x11, 0x68, + 0xe0, 0x3a, 0x6c, 0xcf, 0x34, 0x3c, 0xdb, 0x53, 0x31, 0x39, 0x27, 0xe0, 0xe9, 0xf9, 0xa2, 0xa7, + 0x2e, 0xc9, 0xb9, 0x55, 0xe1, 0xb3, 0xfd, 0xc5, 0xad, 0x89, 0x9e, 0xe9, 0xae, 0x89, 0x73, 0x51, + 0x76, 0x4d, 0xdc, 0x6b, 0x80, 0xc6, 0x70, 0x2c, 0x87, 0x85, 0x76, 0xc2, 0x5a, 0xf3, 0x0c, 0xc3, + 0x7d, 0x25, 0x23, 0xff, 0x06, 0x9e, 0x3a, 0xb3, 0x7b, 0xf4, 0xf2, 0xc0, 0x49, 0x1a, 0x23, 0x7c, + 0x88, 0xa2, 0x0d, 0xdf, 0x36, 0xae, 0x47, 0x3f, 0x3e, 0x2e, 0x72, 0xf1, 0xab, 0xba, 0x8d, 0x52, + 0xb6, 0x1c, 0x16, 0xf9, 0x22, 0x11, 0x4c, 0xbe, 0x85, 0x6f, 0x92, 0x05, 0xa1, 0x62, 0x98, 0xf0, + 0xdc, 0x79, 0x46, 0x47, 0xf6, 0xe2, 0xd6, 0x53, 0xcf, 0xde, 0xbb, 0x7f, 0x01, 0x00, 0x00, 0xff, + 0xff, 0x85, 0x92, 0x14, 0x3e, 0x77, 0x05, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. diff --git a/api/configurator/configurator.proto b/api/configurator/configurator.proto index ac11df39d8..0fa8fa0212 100644 --- a/api/configurator/configurator.proto +++ b/api/configurator/configurator.proto @@ -6,11 +6,13 @@ option go_package = "github.com/ligato/vpp-agent/api/configurator;configurator"; import "models/vpp/vpp.proto"; import "models/linux/linux.proto"; +import "models/netalloc/netalloc.proto"; // Config groups all supported config data into single message. message Config { vpp.ConfigData vpp_config = 1; linux.ConfigData linux_config = 2; + netalloc.ConfigData netalloc_config = 3; } // Notification groups all notification data into single message. diff --git a/api/models/linux/interfaces/interface.pb.go b/api/models/linux/interfaces/interface.pb.go index 4018abbdbf..4494843478 100644 --- a/api/models/linux/interfaces/interface.pb.go +++ b/api/models/linux/interfaces/interface.pb.go @@ -85,22 +85,39 @@ func (VethLink_ChecksumOffloading) EnumDescriptor() ([]byte, []int) { } type Interface struct { - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Type Interface_Type `protobuf:"varint,2,opt,name=type,proto3,enum=linux.interfaces.Interface_Type" json:"type,omitempty"` - Namespace *namespace.NetNamespace `protobuf:"bytes,3,opt,name=namespace,proto3" json:"namespace,omitempty"` - HostIfName string `protobuf:"bytes,4,opt,name=host_if_name,json=hostIfName,proto3" json:"host_if_name,omitempty"` - Enabled bool `protobuf:"varint,5,opt,name=enabled,proto3" json:"enabled,omitempty"` - IpAddresses []string `protobuf:"bytes,6,rep,name=ip_addresses,json=ipAddresses,proto3" json:"ip_addresses,omitempty"` - PhysAddress string `protobuf:"bytes,7,opt,name=phys_address,json=physAddress,proto3" json:"phys_address,omitempty"` - Mtu uint32 `protobuf:"varint,8,opt,name=mtu,proto3" json:"mtu,omitempty"` + // Name is mandatory field representing logical name for the interface. + // It must be unique across all configured interfaces. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Type represents the type of interface and It must match with actual Link. + Type Interface_Type `protobuf:"varint,2,opt,name=type,proto3,enum=linux.interfaces.Interface_Type" json:"type,omitempty"` + // Namespace is a reference to a Linux network namespace where the interface + // should be put into. + Namespace *namespace.NetNamespace `protobuf:"bytes,3,opt,name=namespace,proto3" json:"namespace,omitempty"` + // Name of the interface in the host OS. If not set, the host name will be + // the same as the interface logical name. + HostIfName string `protobuf:"bytes,4,opt,name=host_if_name,json=hostIfName,proto3" json:"host_if_name,omitempty"` + // Enabled controls if the interface should be UP. + Enabled bool `protobuf:"varint,5,opt,name=enabled,proto3" json:"enabled,omitempty"` + // IPAddresses define list of IP addresses for the interface and must be + // defined in the following format: /. + // Interface IP address can be also allocated via netalloc plugin and + // referenced here, see: api/models/netalloc/netalloc.proto + IpAddresses []string `protobuf:"bytes,6,rep,name=ip_addresses,json=ipAddresses,proto3" json:"ip_addresses,omitempty"` + // PhysAddress represents physical address (MAC) of the interface. + // Random address will be assigned if left empty. + PhysAddress string `protobuf:"bytes,7,opt,name=phys_address,json=physAddress,proto3" json:"phys_address,omitempty"` + // MTU is the maximum transmission unit value. + Mtu uint32 `protobuf:"varint,8,opt,name=mtu,proto3" json:"mtu,omitempty"` // Types that are valid to be assigned to Link: // *Interface_Veth // *Interface_Tap - Link isInterface_Link `protobuf_oneof:"link"` - LinkOnly bool `protobuf:"varint,9,opt,name=link_only,json=linkOnly,proto3" json:"link_only,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Link isInterface_Link `protobuf_oneof:"link"` + // Configure/Resync link only. IP/MAC addresses are expected to be configured + // externally - i.e. by a different agent or manually via CLI. + LinkOnly bool `protobuf:"varint,9,opt,name=link_only,json=linkOnly,proto3" json:"link_only,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *Interface) Reset() { *m = Interface{} } @@ -304,8 +321,11 @@ func (*Interface) XXX_MessageName() string { } type VethLink struct { - PeerIfName string `protobuf:"bytes,1,opt,name=peer_if_name,json=peerIfName,proto3" json:"peer_if_name,omitempty"` + // Name of the VETH peer, i.e. other end of the linux veth (mandatory for VETH) + PeerIfName string `protobuf:"bytes,1,opt,name=peer_if_name,json=peerIfName,proto3" json:"peer_if_name,omitempty"` + // Checksum offloading - Rx side (enabled by default) RxChecksumOffloading VethLink_ChecksumOffloading `protobuf:"varint,2,opt,name=rx_checksum_offloading,json=rxChecksumOffloading,proto3,enum=linux.interfaces.VethLink_ChecksumOffloading" json:"rx_checksum_offloading,omitempty"` + // Checksum offloading - Tx side (enabled by default) TxChecksumOffloading VethLink_ChecksumOffloading `protobuf:"varint,3,opt,name=tx_checksum_offloading,json=txChecksumOffloading,proto3,enum=linux.interfaces.VethLink_ChecksumOffloading" json:"tx_checksum_offloading,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` @@ -362,6 +382,7 @@ func (*VethLink) XXX_MessageName() string { } type TapLink struct { + // Logical name of the VPP TAP interface (mandatory for TAP_TO_VPP) VppTapIfName string `protobuf:"bytes,1,opt,name=vpp_tap_if_name,json=vppTapIfName,proto3" json:"vpp_tap_if_name,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` diff --git a/api/models/linux/interfaces/interface.proto b/api/models/linux/interfaces/interface.proto index 8168d5c6bd..9904f03b70 100644 --- a/api/models/linux/interfaces/interface.proto +++ b/api/models/linux/interfaces/interface.proto @@ -13,44 +13,73 @@ message Interface { enum Type { UNDEFINED = 0; VETH = 1; - TAP_TO_VPP = 2; /* TAP created by VPP to have the Linux-side further configured */ + TAP_TO_VPP = 2; // TAP created by VPP to have the Linux-side further configured LOOPBACK = 3; EXISTING = 4; }; - string name = 1; /* Logical interface name unique across all configured interfaces (mandatory) */ - Type type = 2; /* Interface type (mandatory) */ + // Name is mandatory field representing logical name for the interface. + // It must be unique across all configured interfaces. + string name = 1; + // Type represents the type of interface and It must match with actual Link. + Type type = 2; + + // Namespace is a reference to a Linux network namespace where the interface + // should be put into. linux.namespace.NetNamespace namespace = 3; - string host_if_name = 4; /* Name of the interface in the host OS. If not set, the host name - is the same as the interface logical name. */ + + // Name of the interface in the host OS. If not set, the host name will be + // the same as the interface logical name. + string host_if_name = 4; + + // Enabled controls if the interface should be UP. bool enabled = 5; - repeated string ip_addresses = 6; /* IP addresses in the format / */ - string phys_address = 7; /* MAC address */ - uint32 mtu = 8; /* Maximum transmission unit value */ + + // IPAddresses define list of IP addresses for the interface and must be + // defined in the following format: /. + // Interface IP address can be also allocated via netalloc plugin and + // referenced here, see: api/models/netalloc/netalloc.proto + repeated string ip_addresses = 6; + + // PhysAddress represents physical address (MAC) of the interface. + // Random address will be assigned if left empty. + string phys_address = 7; + + /* MTU is the maximum transmission unit value. */ + uint32 mtu = 8; oneof link { - VethLink veth = 20; /* VETH-specific configuration */ - TapLink tap = 21; /* TAP_TO_VPP-specific configuration */ + // VETH-specific configuration + VethLink veth = 20; + + // TAP_TO_VPP-specific configuration + TapLink tap = 21; }; - bool link_only = 9; /* Configure/Resync link only. - IP/MAC addresses are expected to be configured - externally - i.e. by a different agent - or manually via CLI. */ + + // Configure/Resync link only. IP/MAC addresses are expected to be configured + // externally - i.e. by a different agent or manually via CLI. + bool link_only = 9; }; message VethLink { - string peer_if_name = 1; /* Name of the VETH peer, i.e. other end of the linux veth (mandatory for VETH) */ + // Name of the VETH peer, i.e. other end of the linux veth (mandatory for VETH) + string peer_if_name = 1; enum ChecksumOffloading { CHKSM_OFFLOAD_DEFAULT = 0; CHKSM_OFFLOAD_ENABLED = 1; CHKSM_OFFLOAD_DISABLED = 2; } - ChecksumOffloading rx_checksum_offloading = 2; /* checksum offloading - Rx side (enabled by default) */ - ChecksumOffloading tx_checksum_offloading = 3; /* checksum offloading - Tx side (enabled by default) */ + + // Checksum offloading - Rx side (enabled by default) + ChecksumOffloading rx_checksum_offloading = 2; + + // Checksum offloading - Tx side (enabled by default) + ChecksumOffloading tx_checksum_offloading = 3; }; message TapLink { - string vpp_tap_if_name = 1; /* Logical name of the VPP TAP interface (mandatory for TAP_TO_VPP) */ + // Logical name of the VPP TAP interface (mandatory for TAP_TO_VPP) + string vpp_tap_if_name = 1; }; diff --git a/api/models/linux/interfaces/keys.go b/api/models/linux/interfaces/keys.go index 63c2e69211..056d2c6769 100644 --- a/api/models/linux/interfaces/keys.go +++ b/api/models/linux/interfaces/keys.go @@ -15,11 +15,11 @@ package linux_interfaces import ( - "net" "strings" "github.com/gogo/protobuf/jsonpb" + "github.com/ligato/vpp-agent/api/models/netalloc" "github.com/ligato/vpp-agent/pkg/models" ) @@ -64,13 +64,18 @@ const ( /* Interface Address (derived) */ - // InterfaceAddressKeyPrefix is used as a common prefix for keys derived from + // interfaceAddressKeyPrefix is used as a common prefix for keys derived from // interfaces to represent assigned IP addresses. - InterfaceAddressKeyPrefix = "linux/interface/address/" + interfaceAddressKeyPrefix = "linux/interface/{iface}/address/" // interfaceAddressKeyTemplate is a template for (derived) key representing IP address // (incl. mask) assigned to a Linux interface (referenced by the logical name). - interfaceAddressKeyTemplate = InterfaceAddressKeyPrefix + "{ifName}/{addr}/{mask}" + interfaceAddressKeyTemplate = interfaceAddressKeyPrefix + "{address-source}/{address}" +) + +const ( + // InvalidKeyPart is used in key for parts which are invalid + InvalidKeyPart = "" ) /* Interface host-name (default ns only, notifications) */ @@ -117,43 +122,82 @@ func ParseInterfaceStateKey(key string) (ifName string, ifIsUp bool, isStateKey // InterfaceAddressPrefix returns longest-common prefix of keys representing // assigned IP addresses to a specific Linux interface. func InterfaceAddressPrefix(iface string) string { - return InterfaceAddressKeyPrefix + iface + "/" + if iface == "" { + iface = InvalidKeyPart + } + return strings.Replace(interfaceAddressKeyPrefix, "{iface}", iface, 1) } // InterfaceAddressKey returns key representing IP address assigned to Linux interface. -func InterfaceAddressKey(ifName string, address string) string { - var mask string - addrComps := strings.Split(address, "/") - addr := addrComps[0] - if len(addrComps) > 1 { - mask = addrComps[1] +func InterfaceAddressKey(iface string, address string, source netalloc.IPAddressSource) string { + if iface == "" { + iface = InvalidKeyPart + } + + src := source.String() + if src == "" { + src = InvalidKeyPart + } + if strings.HasPrefix(address, netalloc.AllocRefPrefix) { + src = netalloc.IPAddressSource_ALLOC_REF.String() } - key := strings.Replace(interfaceAddressKeyTemplate, "{ifName}", ifName, 1) - key = strings.Replace(key, "{addr}", addr, 1) - key = strings.Replace(key, "{mask}", mask, 1) + src = strings.ToLower(src) + + // construct key without validating the IP address + key := strings.Replace(interfaceAddressKeyTemplate, "{iface}", iface, 1) + key = strings.Replace(key, "{address-source}", src, 1) + key = strings.Replace(key, "{address}", address, 1) return key } // ParseInterfaceAddressKey parses interface address from key derived // from interface by InterfaceAddressKey(). -func ParseInterfaceAddressKey(key string) (ifName string, ipAddr net.IP, ipAddrNet *net.IPNet, invalidIP bool, isAddrKey bool) { - var err error - if strings.HasPrefix(key, InterfaceAddressKeyPrefix) { - keySuffix := strings.TrimPrefix(key, InterfaceAddressKeyPrefix) - keyComps := strings.Split(keySuffix, "/") - if len(keyComps) != 3 { - return "", nil, nil, true, true - } - ipAddr, ipAddrNet, err = net.ParseCIDR(keyComps[1] + "/" + keyComps[2]) - if err != nil { - return "", nil, nil, true, true +func ParseInterfaceAddressKey(key string) (iface, address string, source netalloc.IPAddressSource, invalidKey, isAddrKey bool) { + parts := strings.Split(key, "/") + if len(parts) < 4 || parts[0] != "linux" || parts[1] != "interface" { + return + } + + addrIdx := -1 + for idx, part := range parts { + if part == "address" { + addrIdx = idx + break } - ifName = keyComps[0] - isAddrKey = true - invalidIP = false + } + if addrIdx == -1 { + return + } + isAddrKey = true + + // parse interface name + iface = strings.Join(parts[2:addrIdx], "/") + if iface == "" { + iface = InvalidKeyPart + invalidKey = true + } + + // parse address type + if addrIdx == len(parts)-1 { + invalidKey = true + return + } + + // parse address source + src := strings.ToUpper(parts[addrIdx+1]) + srcInt, validSrc := netalloc.IPAddressSource_value[src] + if !validSrc { + invalidKey = true return } - return "", nil, nil, false, false + source = netalloc.IPAddressSource(srcInt) + + // return address as is (not parsed - this is done by the netalloc plugin) + address = strings.Join(parts[addrIdx+2:], "/") + if address == "" { + invalidKey = true + } + return } // MarshalJSON ensures that field of type 'oneOf' is correctly marshaled diff --git a/api/models/linux/interfaces/keys_test.go b/api/models/linux/interfaces/keys_test.go new file mode 100644 index 0000000000..d1e8251da1 --- /dev/null +++ b/api/models/linux/interfaces/keys_test.go @@ -0,0 +1,287 @@ +// Copyright (c) 2018 Cisco and/or its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package linux_interfaces + +import ( + "github.com/ligato/vpp-agent/api/models/netalloc" + "testing" +) + +func TestInterfaceAddressKey(t *testing.T) { + tests := []struct { + name string + iface string + address string + source netalloc.IPAddressSource + expectedKey string + }{ + { + name: "IPv4 address", + iface: "memif0", + address: "192.168.1.12/24", + source: netalloc.IPAddressSource_STATIC, + expectedKey: "linux/interface/memif0/address/static/192.168.1.12/24", + }, + { + name: "IPv4 address from DHCP", + iface: "memif0", + address: "192.168.1.12/24", + source: netalloc.IPAddressSource_FROM_DHCP, + expectedKey: "linux/interface/memif0/address/from_dhcp/192.168.1.12/24", + }, + { + name: "IPv6 address", + iface: "memif0", + address: "2001:db8::/32", + source: netalloc.IPAddressSource_STATIC, + expectedKey: "linux/interface/memif0/address/static/2001:db8::/32", + }, + { + name: "IPv6 address from DHCP", + iface: "memif0", + address: "2001:db8::/32", + source: netalloc.IPAddressSource_FROM_DHCP, + expectedKey: "linux/interface/memif0/address/from_dhcp/2001:db8::/32", + }, + { + name: "invalid interface", + iface: "", + address: "10.10.10.10/32", + source: netalloc.IPAddressSource_STATIC, + expectedKey: "linux/interface//address/static/10.10.10.10/32", + }, + { + name: "undefined source", + iface: "memif1", + address: "10.10.10.10/32", + expectedKey: "linux/interface/memif1/address/undefined_source/10.10.10.10/32", + }, + { + name: "invalid address", + iface: "tap0", + address: "invalid-addr", + source: netalloc.IPAddressSource_STATIC, + expectedKey: "linux/interface/tap0/address/static/invalid-addr", + }, + { + name: "missing mask", + iface: "tap1", + address: "10.10.10.10", + source: netalloc.IPAddressSource_STATIC, + expectedKey: "linux/interface/tap1/address/static/10.10.10.10", + }, + { + name: "empty address", + iface: "tap1", + address: "", + source: netalloc.IPAddressSource_STATIC, + expectedKey: "linux/interface/tap1/address/static/", + }, + { + name: "IPv4 address requested from netalloc", + iface: "memif0", + address: "alloc:net1", + source: netalloc.IPAddressSource_STATIC, + expectedKey: "linux/interface/memif0/address/alloc_ref/alloc:net1", + }, + { + name: "IPv6 address requested from netalloc", + iface: "memif0", + address: "alloc:net1/IPV6_ADDR", + source: netalloc.IPAddressSource_STATIC, + expectedKey: "linux/interface/memif0/address/alloc_ref/alloc:net1/IPV6_ADDR", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + key := InterfaceAddressKey(test.iface, test.address, test.source) + if key != test.expectedKey { + t.Errorf("failed for: iface=%s address=%s source=%s\n"+ + "expected key:\n\t%q\ngot key:\n\t%q", + test.iface, test.address, string(test.source), test.expectedKey, key) + } + }) + } +} + +func TestParseInterfaceAddressKey(t *testing.T) { + tests := []struct { + name string + key string + expectedIface string + expectedIfaceAddr string + expectedSource netalloc.IPAddressSource + expectedInvalidKey bool + expectedIsAddrKey bool + }{ + { + name: "IPv4 address", + key: "linux/interface/memif0/address/static/192.168.1.12/24", + expectedIface: "memif0", + expectedIfaceAddr: "192.168.1.12/24", + expectedSource: netalloc.IPAddressSource_STATIC, + expectedIsAddrKey: true, + }, + { + name: "IPv4 address from DHCP", + key: "linux/interface/memif0/address/from_dhcp/192.168.1.12/24", + expectedIface: "memif0", + expectedIfaceAddr: "192.168.1.12/24", + expectedSource: netalloc.IPAddressSource_FROM_DHCP, + expectedIsAddrKey: true, + }, + { + name: "IPv4 address requested from Netalloc", + key: "linux/interface/memif0/address/alloc_ref/alloc:net1", + expectedIface: "memif0", + expectedIfaceAddr: "alloc:net1", + expectedSource: netalloc.IPAddressSource_ALLOC_REF, + expectedIsAddrKey: true, + }, + { + name: "IPv6 address", + key: "linux/interface/tap1/address/static/2001:db8:85a3::8a2e:370:7334/48", + expectedIface: "tap1", + expectedIfaceAddr: "2001:db8:85a3::8a2e:370:7334/48", + expectedSource: netalloc.IPAddressSource_STATIC, + expectedIsAddrKey: true, + }, + { + name: "IPv6 address requested from netalloc", + key: "linux/interface/tap1/address/alloc_ref/alloc:net1/IPV6_ADDR", + expectedIface: "tap1", + expectedIfaceAddr: "alloc:net1/IPV6_ADDR", + expectedSource: netalloc.IPAddressSource_ALLOC_REF, + expectedIsAddrKey: true, + }, + { + name: "IPv6 address from DHCP", + key: "linux/interface/tap1/address/from_dhcp/2001:db8:85a3::8a2e:370:7334/48", + expectedIface: "tap1", + expectedIfaceAddr: "2001:db8:85a3::8a2e:370:7334/48", + expectedSource: netalloc.IPAddressSource_FROM_DHCP, + expectedIsAddrKey: true, + }, + { + name: "invalid interface", + key: "linux/interface//address/static/10.10.10.10/30", + expectedIface: "", + expectedIfaceAddr: "10.10.10.10/30", + expectedSource: netalloc.IPAddressSource_STATIC, + expectedIsAddrKey: true, + }, + { + name: "gbe interface", + key: "linux/interface/GigabitEthernet0/8/0/address/static/192.168.5.5/16", + expectedIface: "GigabitEthernet0/8/0", + expectedIfaceAddr: "192.168.5.5/16", + expectedSource: netalloc.IPAddressSource_STATIC, + expectedIsAddrKey: true, + }, + { + name: "missing interface", + key: "linux/interface//address/static/192.168.5.5/16", + expectedIface: "", + expectedIfaceAddr: "192.168.5.5/16", + expectedSource: netalloc.IPAddressSource_STATIC, + expectedInvalidKey: true, + expectedIsAddrKey: true, + }, + { + name: "missing interface (from DHCP)", + key: "linux/interface//address/from_dhcp/192.168.5.5/16", + expectedIface: "", + expectedIfaceAddr: "192.168.5.5/16", + expectedSource: netalloc.IPAddressSource_FROM_DHCP, + expectedInvalidKey: true, + expectedIsAddrKey: true, + }, + { + name: "missing IP", + key: "linux/interface/tap3/address/static/", + expectedIface: "tap3", + expectedIfaceAddr: "", + expectedSource: netalloc.IPAddressSource_STATIC, + expectedInvalidKey: true, + expectedIsAddrKey: true, + }, + { + name: "missing IP (from DHCP)", + key: "linux/interface/tap3/address/from_dhcp/", + expectedIface: "tap3", + expectedIfaceAddr: "", + expectedSource: netalloc.IPAddressSource_FROM_DHCP, + expectedInvalidKey: true, + expectedIsAddrKey: true, + }, + { + name: "missing IP for Gbe", + key: "linux/interface/Gbe0/1/2/address/static/", + expectedIface: "Gbe0/1/2", + expectedIfaceAddr: "", + expectedSource: netalloc.IPAddressSource_STATIC, + expectedInvalidKey: true, + expectedIsAddrKey: true, + }, + { + name: "not interface address key", + key: "linux/config/v2/interface/GigabitEthernet0/8/0", + expectedIface: "", + expectedIfaceAddr: "", + expectedIsAddrKey: false, + }, + { + name: "invalid address source", + key: "linux/interface/memif0/address//192.168.1.12/24", + expectedIface: "memif0", + expectedInvalidKey: true, + expectedIsAddrKey: true, + }, + { + name: "empty address source", + key: "linux/interface/memif0/address//192.168.1.12/24", + expectedIface: "memif0", + expectedInvalidKey: true, + expectedIsAddrKey: true, + }, + { + name: "missing address source", + key: "linux/interface/memif0/address/192.168.1.12/24", + expectedIface: "memif0", + expectedInvalidKey: true, + expectedIsAddrKey: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + iface, ipAddr, source, invalidKey, isAddrKey := ParseInterfaceAddressKey(test.key) + if isAddrKey != test.expectedIsAddrKey { + t.Errorf("expected isAddrKey: %v\tgot: %v", test.expectedIsAddrKey, isAddrKey) + } + if source != test.expectedSource { + t.Errorf("expected source: %v\tgot: %v", test.expectedSource, source) + } + if invalidKey != test.expectedInvalidKey { + t.Errorf("expected invalidKey: %v\tgot: %v", test.expectedInvalidKey, invalidKey) + } + if iface != test.expectedIface { + t.Errorf("expected iface: %s\tgot: %s", test.expectedIface, iface) + } + if ipAddr != test.expectedIfaceAddr { + t.Errorf("expected ipAddr: %s\tgot: %s", test.expectedIfaceAddr, ipAddr) + } + }) + } +} diff --git a/api/models/linux/l3/keys.go b/api/models/linux/l3/keys.go index 4d7f94644e..a3c5b0f857 100644 --- a/api/models/linux/l3/keys.go +++ b/api/models/linux/l3/keys.go @@ -15,8 +15,6 @@ package linux_l3 import ( - "net" - "strconv" "strings" "github.com/ligato/vpp-agent/pkg/models" @@ -37,7 +35,8 @@ var ( Version: "v2", Type: "route", }, models.WithNameTemplate( - `{{with ipnet .DstNetwork}}{{printf "%s/%d" .IP .MaskSize}}{{end}}/{{.OutgoingInterface}}`, + `{{with ipnet .DstNetwork}}{{printf "%s/%d" .IP .MaskSize}}`+ + `{{else}}{{.DstNetwork}}{{end}}/{{.OutgoingInterface}}`, )) ) @@ -64,14 +63,16 @@ const ( LinkLocalRouteKeyPrefix = "linux/link-local-route/" // staticLinkLocalRouteKeyTemplate is a template for key derived from link-local route. - linkLocalRouteKeyTemplate = LinkLocalRouteKeyPrefix + "{out-intf}/{dest-net}/{dest-mask}" + linkLocalRouteKeyTemplate = LinkLocalRouteKeyPrefix + "{out-iface}/dest-address/{dest-address}" ) /* Link-local Route (derived) */ // StaticLinkLocalRouteKey returns a derived key used to represent link-local route. func StaticLinkLocalRouteKey(dstAddr, outgoingInterface string) string { - return RouteKeyFromTemplate(linkLocalRouteKeyTemplate, dstAddr, outgoingInterface) + key := strings.Replace(linkLocalRouteKeyTemplate, "{dest-address}", dstAddr, 1) + key = strings.Replace(key, "{out-iface}", outgoingInterface, 1) + return key } // StaticLinkLocalRoutePrefix returns longest-common prefix of keys representing @@ -81,42 +82,18 @@ func StaticLinkLocalRoutePrefix(outgoingInterface string) string { } // ParseStaticLinkLocalRouteKey parses route attributes from a key derived from link-local route. -func ParseStaticLinkLocalRouteKey(key string) (dstNetAddr *net.IPNet, outgoingInterface string, isRouteKey bool) { - return parseRouteFromKeySuffix(key, LinkLocalRouteKeyPrefix, "invalid Linux link-local Route key: ") -} - -/* Route helpers */ +func ParseStaticLinkLocalRouteKey(key string) (dstAddr string, outgoingInterface string, isRouteKey bool) { + if strings.HasPrefix(key, LinkLocalRouteKeyPrefix) { + routeSuffix := strings.TrimPrefix(key, LinkLocalRouteKeyPrefix) + parts := strings.Split(routeSuffix, "/dest-address/") -// RouteKeyFromTemplate fills key template with route attributes. -func RouteKeyFromTemplate(template, dstAddr, outgoingInterface string) string { - _, dstNet, _ := net.ParseCIDR(dstAddr) - dstNetAddr := dstNet.IP.String() - dstNetMask, _ := dstNet.Mask.Size() - key := strings.Replace(template, "{dest-net}", dstNetAddr, 1) - key = strings.Replace(key, "{dest-mask}", strconv.Itoa(dstNetMask), 1) - key = strings.Replace(key, "{out-intf}", outgoingInterface, 1) - return key -} - -// parseRouteFromKeySuffix parses destination network and outgoing interface from a route key suffix. -func parseRouteFromKeySuffix(key, prefix, errPrefix string) (dstNetAddr *net.IPNet, outgoingInterface string, isRouteKey bool) { - var err error - if strings.HasPrefix(key, prefix) { - routeSuffix := strings.TrimPrefix(key, prefix) - routeComps := strings.Split(routeSuffix, "/") - - // beware: interface name may contain forward slashes - if len(routeComps) < 3 { - return nil, "", false + if len(parts) != 2 { + return "", "", false } - lastIdx := len(routeComps) - 1 - _, dstNetAddr, err = net.ParseCIDR(routeComps[lastIdx-1] + "/" + routeComps[lastIdx]) - if err != nil { - return nil, "", false - } - outgoingInterface = strings.Join(routeComps[:lastIdx-1], "/") + outgoingInterface = parts[0] + dstAddr = parts[1] isRouteKey = true return } - return nil, "", false -} + return "", "", false +} \ No newline at end of file diff --git a/api/models/linux/l3/keys_test.go b/api/models/linux/l3/keys_test.go new file mode 100644 index 0000000000..20b2e5debd --- /dev/null +++ b/api/models/linux/l3/keys_test.go @@ -0,0 +1,109 @@ +// Copyright (c) 2019 Cisco and/or its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package linux_l3 + +import ( + "testing" +) + +func TestRouteKey(t *testing.T) { + tests := []struct { + name string + outIface string + dstNetwork string + expectedKey string + }{ + { + name: "IPv4 dest address", + outIface: "memif1", + dstNetwork: "192.168.1.0/24", + expectedKey: "config/linux/l3/v2/route/192.168.1.0/24/memif1", + }, + { + name: "dest address obtained from netalloc", + outIface: "memif1", + dstNetwork: "alloc:net1/memif2", + expectedKey: "config/linux/l3/v2/route/alloc:net1/memif2/memif1", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + key := RouteKey(test.dstNetwork, test.outIface) + if key != test.expectedKey { + t.Errorf("failed for: outIface=%s dstNet=%s\n"+ + "expected key:\n\t%q\ngot key:\n\t%q", + test.outIface, test.dstNetwork, test.expectedKey, key) + } + }) + } +} + +func TestStaticLinkLocalRouteKey(t *testing.T) { + tests := []struct { + name string + dstAddr string + outIface string + expectedKey string + }{ + { + name: "IPv4 address via memif", + outIface: "memif0", + dstAddr: "192.168.1.12/24", + expectedKey: "linux/link-local-route/memif0/dest-address/192.168.1.12/24", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + key := StaticLinkLocalRouteKey(test.dstAddr, test.outIface) + if key != test.expectedKey { + t.Errorf("failed for: iface=%s address=%s\n"+ + "expected key:\n\t%q\ngot key:\n\t%q", + test.outIface, test.dstAddr, test.expectedKey, key) + } + }) + } +} + +func TestParseStaticLinkLocalRouteKey(t *testing.T) { + tests := []struct { + name string + key string + expectedIface string + expectedDstAddr string + expectedIsLinkLocalRouteKey bool + }{ + { + name: "IPv4 address via memif", + key: "linux/link-local-route/memif0/dest-address/192.168.1.12/24", + expectedIface: "memif0", + expectedDstAddr: "192.168.1.12/24", + expectedIsLinkLocalRouteKey: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + dstAddr, outIface, isLinkLocalRouteKey := ParseStaticLinkLocalRouteKey(test.key) + if isLinkLocalRouteKey != test.expectedIsLinkLocalRouteKey { + t.Errorf("expected isLinkLocalRouteKey: %v\tgot: %v", test.expectedIsLinkLocalRouteKey, isLinkLocalRouteKey) + } + if outIface != test.expectedIface { + t.Errorf("expected iface: %s\tgot: %s", test.expectedIface, outIface) + } + if dstAddr != test.expectedDstAddr { + t.Errorf("expected dstAddr: %s\tgot: %s", test.expectedDstAddr, dstAddr) + } + }) + } +} diff --git a/api/models/linux/l3/route.pb.go b/api/models/linux/l3/route.pb.go index 535a19052a..886c7e43b4 100644 --- a/api/models/linux/l3/route.pb.go +++ b/api/models/linux/l3/route.pb.go @@ -56,14 +56,23 @@ func (Route_Scope) EnumDescriptor() ([]byte, []int) { } type Route struct { - OutgoingInterface string `protobuf:"bytes,1,opt,name=outgoing_interface,json=outgoingInterface,proto3" json:"outgoing_interface,omitempty"` - Scope Route_Scope `protobuf:"varint,2,opt,name=scope,proto3,enum=linux.l3.Route_Scope" json:"scope,omitempty"` - DstNetwork string `protobuf:"bytes,3,opt,name=dst_network,json=dstNetwork,proto3" json:"dst_network,omitempty"` - GwAddr string `protobuf:"bytes,4,opt,name=gw_addr,json=gwAddr,proto3" json:"gw_addr,omitempty"` - Metric uint32 `protobuf:"varint,5,opt,name=metric,proto3" json:"metric,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + // Outgoing interface logical name (mandatory). + OutgoingInterface string `protobuf:"bytes,1,opt,name=outgoing_interface,json=outgoingInterface,proto3" json:"outgoing_interface,omitempty"` + // The scope of the area where the link is valid. + Scope Route_Scope `protobuf:"varint,2,opt,name=scope,proto3,enum=linux.l3.Route_Scope" json:"scope,omitempty"` + // Destination network address in the format
/ (mandatory) + // Address can be also allocated via netalloc plugin and referenced here, + // see: api/models/netalloc/netalloc.proto + DstNetwork string `protobuf:"bytes,3,opt,name=dst_network,json=dstNetwork,proto3" json:"dst_network,omitempty"` + // Gateway IP address (without mask, optional). + // Address can be also allocated via netalloc plugin and referenced here, + // see: api/models/netalloc/netalloc.proto + GwAddr string `protobuf:"bytes,4,opt,name=gw_addr,json=gwAddr,proto3" json:"gw_addr,omitempty"` + // routing metric (weight) + Metric uint32 `protobuf:"varint,5,opt,name=metric,proto3" json:"metric,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *Route) Reset() { *m = Route{} } diff --git a/api/models/linux/l3/route.proto b/api/models/linux/l3/route.proto index a8b0815a45..2216d60f1d 100644 --- a/api/models/linux/l3/route.proto +++ b/api/models/linux/l3/route.proto @@ -8,7 +8,8 @@ import "github.com/gogo/protobuf/gogoproto/gogo.proto"; option (gogoproto.messagename_all) = true; message Route { - string outgoing_interface = 1; /* outgoing interface logical name (mandatory) */ + // Outgoing interface logical name (mandatory). + string outgoing_interface = 1; enum Scope { UNDEFINED = 0; @@ -17,9 +18,19 @@ message Route { LINK = 3; HOST = 4; } - Scope scope = 2; /* the scope of the area where the link is valid */ + // The scope of the area where the link is valid. + Scope scope = 2; - string dst_network = 3; /* destination network address in the format
/ (mandatory) */ - string gw_addr = 4; /* gateway IP address */ - uint32 metric = 5; /* routing metric (weight) */ + // Destination network address in the format
/ (mandatory) + // Address can be also allocated via netalloc plugin and referenced here, + // see: api/models/netalloc/netalloc.proto + string dst_network = 3; + + // Gateway IP address (without mask, optional). + // Address can be also allocated via netalloc plugin and referenced here, + // see: api/models/netalloc/netalloc.proto + string gw_addr = 4; + + // routing metric (weight) + uint32 metric = 5; } diff --git a/api/models/netalloc/keys.go b/api/models/netalloc/keys.go new file mode 100644 index 0000000000..c932ef5c2b --- /dev/null +++ b/api/models/netalloc/keys.go @@ -0,0 +1,67 @@ +// Copyright (c) 2019 Cisco and/or its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package netalloc + +import ( + "net" + "strings" + + "github.com/ligato/vpp-agent/pkg/models" +) + +const ( + // ModuleName is the module name used for models of the netalloc plugin. + ModuleName = "netalloc" + + // AllocRefPrefix is a prefix added in front of references to allocated objects. + AllocRefPrefix = "alloc:" + + // AllocRefGWSuffix is a suffix added at the back of the reference when address + // of the default gateway is requested (instead of interface IP address). + AllocRefGWSuffix = "/GW" +) + +var ( + ModelIPAllocation = models.Register(&IPAllocation{}, models.Spec{ + Module: ModuleName, + Version: "v1", + Type: "ip", + }, models.WithNameTemplate( + "network/{{.NetworkName}}/interface/{{.InterfaceName}}", + )) +) + +const ( + /* neighbour gateway (derived) */ + + // neighGwKeyTemplate is a template for keys derived from IP allocations + // where GW is a neighbour of the interface (addresses are from the same + // IP network). + neighGwKeyTemplate = "netalloc/neigh-gw/network/{network}/interface/{iface}" +) + +// NeighGwKey returns a derived key used to represent IP allocation where +// GW is a neighbour of the interface (addresses are from the same IP network). +func NeighGwKey(network, iface string) string { + key := strings.Replace(neighGwKeyTemplate, "{network}", network, 1) + key = strings.Replace(key, "{iface}", iface, 1) + return key +} + +// IPAllocMetadata stores allocated IP address already parsed from string. +type IPAllocMetadata struct { + IfaceAddr *net.IPNet + GwAddr *net.IPNet +} diff --git a/api/models/netalloc/netalloc.pb.go b/api/models/netalloc/netalloc.pb.go new file mode 100644 index 0000000000..354697f6b7 --- /dev/null +++ b/api/models/netalloc/netalloc.pb.go @@ -0,0 +1,300 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: models/netalloc/netalloc.proto + +// Netalloc allows to disassociate topology from addressing in the network +// configuration. Instead of inserting specific IP/MAC addresses, VXLAN VNIs, etc., +// into the configuration data for interfaces, routes, ARPs and other network +// objects, the addresses can be symbolic references into the pool of allocated +// addresses known to the netalloc plugin. +// +// The ability to separate addresses from the rest of the network configuration +// is especially useful in scenarios where address allocations are provided +// externally, for example by another control-plane agent, IPAM tool or by CNI +// in containerized environments. +// +// But for now, only model for IP address allocations has been implemented. +// To allocate a new IP address, an instance of the proto message IPAllocation +// should be submitted into the vpp-agent through one of the supported NB +// transports (etcd, GRPC, ...) under the corresponding key. Network object which +// references (to-be or already) allocated address will have a dependency on the +// corresponding key-value instance of IPAllocation and will read and apply the +// address only once it is available. + +package netalloc + +import ( + fmt "fmt" + _ "github.com/gogo/protobuf/gogoproto" + proto "github.com/gogo/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package + +// IPAddressForm can be used in descriptors whose models reference allocated IP +// addresses, to ask for a specific form in which the address should applied. +type IPAddressForm int32 + +const ( + IPAddressForm_UNDEFINED_FORM IPAddressForm = 0 + // ADDR_ONLY = apply address without mask, e.g. 192.168.2.5 + IPAddressForm_ADDR_ONLY IPAddressForm = 1 + // ADDR_WITH_MASK = apply address including the mask of the network, + // e.g. 192.168.2.5/24 + IPAddressForm_ADDR_WITH_MASK IPAddressForm = 2 + // ADDR_NET = apply network implied by the address, + // e.g. for 192.168.2.10/24 apply 192.168.2.0/24 + IPAddressForm_ADDR_NET IPAddressForm = 3 + // SINGLE_ADDR_NET = apply address with an all-ones mask (i.e. /32 for IPv4, + // /128 for IPv6) + IPAddressForm_SINGLE_ADDR_NET IPAddressForm = 4 +) + +var IPAddressForm_name = map[int32]string{ + 0: "UNDEFINED_FORM", + 1: "ADDR_ONLY", + 2: "ADDR_WITH_MASK", + 3: "ADDR_NET", + 4: "SINGLE_ADDR_NET", +} + +var IPAddressForm_value = map[string]int32{ + "UNDEFINED_FORM": 0, + "ADDR_ONLY": 1, + "ADDR_WITH_MASK": 2, + "ADDR_NET": 3, + "SINGLE_ADDR_NET": 4, +} + +func (x IPAddressForm) String() string { + return proto.EnumName(IPAddressForm_name, int32(x)) +} + +func (IPAddressForm) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_871b6a8ed3ae2830, []int{0} +} + +// IPAddressSource can be used to remember the source of an IP address. +// (e.g. to distinguish allocated IP addresses from statically defined ones) +type IPAddressSource int32 + +const ( + IPAddressSource_UNDEFINED_SOURCE IPAddressSource = 0 + // STATIC is IP address statically assigned in the NB configuration. + IPAddressSource_STATIC IPAddressSource = 1 + // FROM_DHCP is set when IP address is obtained from DHCP. + IPAddressSource_FROM_DHCP IPAddressSource = 2 + // ALLOC_REF is a reference inside NB configuration to an allocated + // IP address. + IPAddressSource_ALLOC_REF IPAddressSource = 3 +) + +var IPAddressSource_name = map[int32]string{ + 0: "UNDEFINED_SOURCE", + 1: "STATIC", + 2: "FROM_DHCP", + 3: "ALLOC_REF", +} + +var IPAddressSource_value = map[string]int32{ + "UNDEFINED_SOURCE": 0, + "STATIC": 1, + "FROM_DHCP": 2, + "ALLOC_REF": 3, +} + +func (x IPAddressSource) String() string { + return proto.EnumName(IPAddressSource_name, int32(x)) +} + +func (IPAddressSource) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_871b6a8ed3ae2830, []int{1} +} + +// IPAllocation represents a single allocated IP address. +// +// To reference allocated address, instead of entering specific IP address +// for interface/route/ARP/..., use one of the following string templates +// prefixed with netalloc keyword "alloc" followed by colon: +// a) reference IP address allocated for an interface: +// "alloc:/" +// b) when interface is given (e.g. when asked for IP from interface model), +// interface_name can be omitted: +// "alloc:" +// c) reference default gateway IP address assigned to an interface: +// "alloc://GW" +// d) when asking for GW IP for interface which is given, interface_name +// can be omitted: +// "alloc:/GW" +type IPAllocation struct { + // NetworkName is some label assigned to the network where the IP address + // was assigned to the given interface. + // In theory, interface can have multiple IP adresses or there can be multiple + // address allocators and the network name allows to separate them. + // The network name is not allowed to contain forward slashes. + NetworkName string `protobuf:"bytes,1,opt,name=network_name,json=networkName,proto3" json:"network_name,omitempty"` + // InterfaceName is the logical VPP or Linux interface name for which the + // address is allocated. + InterfaceName string `protobuf:"bytes,2,opt,name=interface_name,json=interfaceName,proto3" json:"interface_name,omitempty"` + // Address is an IP addres allocated to the interface inside the given + // network. + // If the address is specified without a mask, the all-ones mask (/32 for + // IPv4, /128 for IPv6) will be assumed. + Address string `protobuf:"bytes,4,opt,name=address,proto3" json:"address,omitempty"` + // Gw is the address of the default gateway assigned to the interface in + // the given network. + // If the address is specified without a mask, then either: + // a) the mask of the
is used provided that GW IP falls into the + // same network IP range, or + // b) the all-ones mask is used otherwise + Gw string `protobuf:"bytes,5,opt,name=gw,proto3" json:"gw,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *IPAllocation) Reset() { *m = IPAllocation{} } +func (m *IPAllocation) String() string { return proto.CompactTextString(m) } +func (*IPAllocation) ProtoMessage() {} +func (*IPAllocation) Descriptor() ([]byte, []int) { + return fileDescriptor_871b6a8ed3ae2830, []int{0} +} +func (m *IPAllocation) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_IPAllocation.Unmarshal(m, b) +} +func (m *IPAllocation) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_IPAllocation.Marshal(b, m, deterministic) +} +func (m *IPAllocation) XXX_Merge(src proto.Message) { + xxx_messageInfo_IPAllocation.Merge(m, src) +} +func (m *IPAllocation) XXX_Size() int { + return xxx_messageInfo_IPAllocation.Size(m) +} +func (m *IPAllocation) XXX_DiscardUnknown() { + xxx_messageInfo_IPAllocation.DiscardUnknown(m) +} + +var xxx_messageInfo_IPAllocation proto.InternalMessageInfo + +func (m *IPAllocation) GetNetworkName() string { + if m != nil { + return m.NetworkName + } + return "" +} + +func (m *IPAllocation) GetInterfaceName() string { + if m != nil { + return m.InterfaceName + } + return "" +} + +func (m *IPAllocation) GetAddress() string { + if m != nil { + return m.Address + } + return "" +} + +func (m *IPAllocation) GetGw() string { + if m != nil { + return m.Gw + } + return "" +} + +func (*IPAllocation) XXX_MessageName() string { + return "netalloc.IPAllocation" +} + +// ConfigData wraps all configuration items exported by netalloc. +// TBD: MACs, VXLAN VNIs, memif IDs, etc. +type ConfigData struct { + IpAddresses []*IPAllocation `protobuf:"bytes,10,rep,name=ip_addresses,json=ipAddresses,proto3" json:"ip_addresses,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ConfigData) Reset() { *m = ConfigData{} } +func (m *ConfigData) String() string { return proto.CompactTextString(m) } +func (*ConfigData) ProtoMessage() {} +func (*ConfigData) Descriptor() ([]byte, []int) { + return fileDescriptor_871b6a8ed3ae2830, []int{1} +} +func (m *ConfigData) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ConfigData.Unmarshal(m, b) +} +func (m *ConfigData) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ConfigData.Marshal(b, m, deterministic) +} +func (m *ConfigData) XXX_Merge(src proto.Message) { + xxx_messageInfo_ConfigData.Merge(m, src) +} +func (m *ConfigData) XXX_Size() int { + return xxx_messageInfo_ConfigData.Size(m) +} +func (m *ConfigData) XXX_DiscardUnknown() { + xxx_messageInfo_ConfigData.DiscardUnknown(m) +} + +var xxx_messageInfo_ConfigData proto.InternalMessageInfo + +func (m *ConfigData) GetIpAddresses() []*IPAllocation { + if m != nil { + return m.IpAddresses + } + return nil +} + +func (*ConfigData) XXX_MessageName() string { + return "netalloc.ConfigData" +} +func init() { + proto.RegisterEnum("netalloc.IPAddressForm", IPAddressForm_name, IPAddressForm_value) + proto.RegisterEnum("netalloc.IPAddressSource", IPAddressSource_name, IPAddressSource_value) + proto.RegisterType((*IPAllocation)(nil), "netalloc.IPAllocation") + proto.RegisterType((*ConfigData)(nil), "netalloc.ConfigData") +} + +func init() { proto.RegisterFile("models/netalloc/netalloc.proto", fileDescriptor_871b6a8ed3ae2830) } + +var fileDescriptor_871b6a8ed3ae2830 = []byte{ + // 395 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x5c, 0x92, 0xcd, 0x6e, 0xd3, 0x40, + 0x10, 0xc7, 0x6b, 0xa7, 0x94, 0x76, 0xf2, 0xd1, 0xd5, 0x82, 0x90, 0xc5, 0xa1, 0x2a, 0x95, 0x90, + 0xaa, 0x4a, 0x8d, 0x25, 0x10, 0x07, 0x8e, 0xc6, 0x1f, 0xad, 0x85, 0x63, 0x07, 0xdb, 0x15, 0x82, + 0xcb, 0x6a, 0xe3, 0x6c, 0x96, 0x15, 0xb6, 0xd7, 0xb2, 0x37, 0xe4, 0xce, 0xd3, 0xf1, 0x1e, 0xbc, + 0x08, 0xca, 0x36, 0x31, 0x51, 0x6f, 0xf3, 0xfb, 0xfb, 0x37, 0x9a, 0xb1, 0x66, 0xe1, 0xa2, 0x92, + 0x4b, 0x56, 0x76, 0x76, 0xcd, 0x14, 0x2d, 0x4b, 0x59, 0xf4, 0xc5, 0xb4, 0x69, 0xa5, 0x92, 0xf8, + 0x74, 0xcf, 0xaf, 0x6f, 0xb9, 0x50, 0x3f, 0xd6, 0x8b, 0x69, 0x21, 0x2b, 0x9b, 0x4b, 0x2e, 0x6d, + 0x2d, 0x2c, 0xd6, 0x2b, 0x4d, 0x1a, 0x74, 0xf5, 0xd8, 0x78, 0xf5, 0xdb, 0x80, 0x51, 0x38, 0x77, + 0xb6, 0xad, 0x54, 0x09, 0x59, 0xe3, 0x37, 0x30, 0xaa, 0x99, 0xda, 0xc8, 0xf6, 0x27, 0xa9, 0x69, + 0xc5, 0x2c, 0xe3, 0xd2, 0xb8, 0x3e, 0x4b, 0x87, 0xbb, 0x2c, 0xa6, 0x15, 0xc3, 0x6f, 0x61, 0x22, + 0x6a, 0xc5, 0xda, 0x15, 0x2d, 0xd8, 0xa3, 0x64, 0x6a, 0x69, 0xdc, 0xa7, 0x5a, 0xb3, 0xe0, 0x39, + 0x5d, 0x2e, 0x5b, 0xd6, 0x75, 0xd6, 0xb1, 0xfe, 0xbe, 0x47, 0x3c, 0x01, 0x93, 0x6f, 0xac, 0x67, + 0x3a, 0x34, 0xf9, 0xe6, 0xea, 0x0e, 0xc0, 0x95, 0xf5, 0x4a, 0x70, 0x8f, 0x2a, 0x8a, 0x3f, 0xc2, + 0x48, 0x34, 0x64, 0xe7, 0xb2, 0xce, 0x82, 0xcb, 0xc1, 0xf5, 0xf0, 0xdd, 0xab, 0x69, 0xff, 0xcb, + 0x87, 0xfb, 0xa6, 0x43, 0xd1, 0x38, 0x7b, 0xf5, 0x46, 0xc0, 0x38, 0x9c, 0xef, 0x30, 0x90, 0x6d, + 0x85, 0x31, 0x4c, 0x1e, 0x62, 0xcf, 0x0f, 0xc2, 0xd8, 0xf7, 0x48, 0x90, 0xa4, 0x33, 0x74, 0x84, + 0xc7, 0x70, 0xe6, 0x78, 0x5e, 0x4a, 0x92, 0x38, 0xfa, 0x86, 0x8c, 0xad, 0xa2, 0xf1, 0x6b, 0x98, + 0xdf, 0x93, 0x99, 0x93, 0x7d, 0x46, 0x26, 0x1e, 0xc1, 0xa9, 0xce, 0x62, 0x3f, 0x47, 0x03, 0xfc, + 0x02, 0xce, 0xb3, 0x30, 0xbe, 0x8b, 0x7c, 0xd2, 0x87, 0xc7, 0x37, 0x5f, 0xe0, 0xbc, 0x1f, 0x95, + 0xc9, 0x75, 0x5b, 0x30, 0xfc, 0x12, 0xd0, 0xff, 0x61, 0x59, 0xf2, 0x90, 0xba, 0x3e, 0x3a, 0xc2, + 0x00, 0x27, 0x59, 0xee, 0xe4, 0xa1, 0x8b, 0x8c, 0xed, 0xe8, 0x20, 0x4d, 0x66, 0xc4, 0xbb, 0x77, + 0xe7, 0xc8, 0xd4, 0x9b, 0x44, 0x51, 0xe2, 0x92, 0xd4, 0x0f, 0xd0, 0xe0, 0xd3, 0x87, 0x3f, 0x7f, + 0x2f, 0x8c, 0xef, 0xf6, 0xc1, 0x01, 0x4b, 0xc1, 0xa9, 0x92, 0xf6, 0xaf, 0xa6, 0xb9, 0xa5, 0x9c, + 0xd5, 0xca, 0xa6, 0x8d, 0xb0, 0x9f, 0x3c, 0x85, 0xc5, 0x89, 0xbe, 0xe4, 0xfb, 0x7f, 0x01, 0x00, + 0x00, 0xff, 0xff, 0xd4, 0xfe, 0xe7, 0xce, 0x24, 0x02, 0x00, 0x00, +} diff --git a/api/models/netalloc/netalloc.proto b/api/models/netalloc/netalloc.proto new file mode 100644 index 0000000000..6b04973665 --- /dev/null +++ b/api/models/netalloc/netalloc.proto @@ -0,0 +1,111 @@ +syntax = "proto3"; + +// Netalloc allows to disassociate topology from addressing in the network +// configuration. Instead of inserting specific IP/MAC addresses, VXLAN VNIs, etc., +// into the configuration data for interfaces, routes, ARPs and other network +// objects, the addresses can be symbolic references into the pool of allocated +// addresses known to the netalloc plugin. +// +// The ability to separate addresses from the rest of the network configuration +// is especially useful in scenarios where address allocations are provided +// externally, for example by another control-plane agent, IPAM tool or by CNI +// in containerized environments. +// +// But for now, only model for IP address allocations has been implemented. +// To allocate a new IP address, an instance of the proto message IPAllocation +// should be submitted into the vpp-agent through one of the supported NB +// transports (etcd, GRPC, ...) under the corresponding key. Network object which +// references (to-be or already) allocated address will have a dependency on the +// corresponding key-value instance of IPAllocation and will read and apply the +// address only once it is available. +package netalloc; + +option go_package = "github.com/ligato/vpp-agent/api/models/netalloc"; + +import "github.com/gogo/protobuf/gogoproto/gogo.proto"; +option (gogoproto.messagename_all) = true; + +// IPAddressForm can be used in descriptors whose models reference allocated IP +// addresses, to ask for a specific form in which the address should applied. +enum IPAddressForm { + UNDEFINED_FORM = 0; + + // ADDR_ONLY = apply address without mask, e.g. 192.168.2.5 + ADDR_ONLY = 1; + + // ADDR_WITH_MASK = apply address including the mask of the network, + // e.g. 192.168.2.5/24 + ADDR_WITH_MASK = 2; + + // ADDR_NET = apply network implied by the address, + // e.g. for 192.168.2.10/24 apply 192.168.2.0/24 + ADDR_NET = 3; + + // SINGLE_ADDR_NET = apply address with an all-ones mask (i.e. /32 for IPv4, + // /128 for IPv6) + SINGLE_ADDR_NET = 4; +}; + +// IPAddressSource can be used to remember the source of an IP address. +// (e.g. to distinguish allocated IP addresses from statically defined ones) +enum IPAddressSource { + UNDEFINED_SOURCE = 0; + + // STATIC is IP address statically assigned in the NB configuration. + STATIC = 1; + + // FROM_DHCP is set when IP address is obtained from DHCP. + FROM_DHCP = 2; + + // ALLOC_REF is a reference inside NB configuration to an allocated + // IP address. + ALLOC_REF = 3; +} + +// IPAllocation represents a single allocated IP address. +// +// To reference allocated address, instead of entering specific IP address +// for interface/route/ARP/..., use one of the following string templates +// prefixed with netalloc keyword "alloc" followed by colon: +// a) reference IP address allocated for an interface: +// "alloc:/" +// b) when interface is given (e.g. when asked for IP from interface model), +// interface_name can be omitted: +// "alloc:" +// c) reference default gateway IP address assigned to an interface: +// "alloc://GW" +// d) when asking for GW IP for interface which is given, interface_name +// can be omitted: +// "alloc:/GW" +message IPAllocation { + // NetworkName is some label assigned to the network where the IP address + // was assigned to the given interface. + // In theory, interface can have multiple IP adresses or there can be multiple + // address allocators and the network name allows to separate them. + // The network name is not allowed to contain forward slashes. + string network_name = 1; + + // InterfaceName is the logical VPP or Linux interface name for which the + // address is allocated. + string interface_name = 2; + + // Address is an IP addres allocated to the interface inside the given + // network. + // If the address is specified without a mask, the all-ones mask (/32 for + // IPv4, /128 for IPv6) will be assumed. + string address = 4; + + // Gw is the address of the default gateway assigned to the interface in + // the given network. + // If the address is specified without a mask, then either: + // a) the mask of the
is used provided that GW IP falls into the + // same network IP range, or + // b) the all-ones mask is used otherwise + string gw = 5; +} + +// ConfigData wraps all configuration items exported by netalloc. +// TBD: MACs, VXLAN VNIs, memif IDs, etc. +message ConfigData { + repeated IPAllocation ip_addresses = 10; +} \ No newline at end of file diff --git a/api/models/vpp/interfaces/interface.pb.go b/api/models/vpp/interfaces/interface.pb.go index 5db5c1723b..ba00dd4629 100644 --- a/api/models/vpp/interfaces/interface.pb.go +++ b/api/models/vpp/interfaces/interface.pb.go @@ -286,15 +286,17 @@ type Interface struct { // Name is mandatory field representing logical name for the interface. // It must be unique across all configured interfaces. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Type represents the type of interface and It must match with actual Link. + // Type represents the type of interface and it must match with actual Link. Type Interface_Type `protobuf:"varint,2,opt,name=type,proto3,enum=vpp.interfaces.Interface_Type" json:"type,omitempty"` - // Enabled controls if the interface should be + // Enabled controls if the interface should be UP. Enabled bool `protobuf:"varint,3,opt,name=enabled,proto3" json:"enabled,omitempty"` // PhysAddress represents physical address (MAC) of the interface. // Random address will be assigned if left empty. PhysAddress string `protobuf:"bytes,4,opt,name=phys_address,json=physAddress,proto3" json:"phys_address,omitempty"` // IPAddresses define list of IP addresses for the interface and must be - // defined in the following format: / + // defined in the following format: /. + // Interface IP address can be also allocated via netalloc plugin and + // referenced here, see: api/models/netalloc/netalloc.proto IpAddresses []string `protobuf:"bytes,5,rep,name=ip_addresses,json=ipAddresses,proto3" json:"ip_addresses,omitempty"` // Vrf defines the ID of VRF table that the interface is assigned to. // The VRF table must be explicitely configured (see api/models/vpp/l3/vrf.proto). diff --git a/api/models/vpp/interfaces/interface.proto b/api/models/vpp/interfaces/interface.proto index 6d943cd4c6..eea2f0ce50 100644 --- a/api/models/vpp/interfaces/interface.proto +++ b/api/models/vpp/interfaces/interface.proto @@ -29,10 +29,10 @@ message Interface { // It must be unique across all configured interfaces. string name = 1; - // Type represents the type of interface and It must match with actual Link. + // Type represents the type of interface and it must match with actual Link. Type type = 2; - // Enabled controls if the interface should be + // Enabled controls if the interface should be UP. bool enabled = 3; // PhysAddress represents physical address (MAC) of the interface. @@ -40,7 +40,9 @@ message Interface { string phys_address = 4; // IPAddresses define list of IP addresses for the interface and must be - // defined in the following format: / + // defined in the following format: /. + // Interface IP address can be also allocated via netalloc plugin and + // referenced here, see: api/models/netalloc/netalloc.proto repeated string ip_addresses = 5; // Vrf defines the ID of VRF table that the interface is assigned to. diff --git a/api/models/vpp/interfaces/keys.go b/api/models/vpp/interfaces/keys.go index 51b2cb2040..9f36c8c016 100644 --- a/api/models/vpp/interfaces/keys.go +++ b/api/models/vpp/interfaces/keys.go @@ -15,12 +15,12 @@ package vpp_interfaces import ( - "net" "strconv" "strings" "github.com/gogo/protobuf/jsonpb" + "github.com/ligato/vpp-agent/api/models/netalloc" "github.com/ligato/vpp-agent/pkg/models" ) @@ -76,10 +76,7 @@ const ( // addressKeyTemplate is a template for (derived) key representing assigned // IP addresses to an interface. - addressKeyTemplate = addressKeyPrefix + "{address-type}/{address}" - - addressStatic = "static" - addressFromDHCP = "from-dhcp" + addressKeyTemplate = addressKeyPrefix + "{address-source}/{address}" ) /* Interface VRF (derived) */ @@ -200,27 +197,30 @@ func InterfaceAddressPrefix(iface string) string { } // InterfaceAddressKey returns key representing IP address assigned to VPP interface. -func InterfaceAddressKey(iface string, address string, fromDHCP bool) string { +func InterfaceAddressKey(iface string, address string, source netalloc.IPAddressSource) string { if iface == "" { iface = InvalidKeyPart } + src := source.String() + if src == "" { + src = InvalidKeyPart + } + if strings.HasPrefix(address, netalloc.AllocRefPrefix) { + src = netalloc.IPAddressSource_ALLOC_REF.String() + } + src = strings.ToLower(src) + // construct key without validating the IP address key := strings.Replace(addressKeyTemplate, "{iface}", iface, 1) - if fromDHCP { - key = strings.Replace(key, "{address-type}", addressFromDHCP, 1) - } else { - key = strings.Replace(key, "{address-type}", addressStatic, 1) - } + key = strings.Replace(key, "{address-source}", src, 1) key = strings.Replace(key, "{address}", address, 1) return key } // ParseInterfaceAddressKey parses interface address from key derived // from interface by InterfaceAddressKey(). -func ParseInterfaceAddressKey(key string) (iface string, ipAddr net.IP, ipAddrNet *net.IPNet, - fromDHCP, invalidIP, isAddrKey bool) { - +func ParseInterfaceAddressKey(key string) (iface, address string, source netalloc.IPAddressSource, invalidKey, isAddrKey bool) { parts := strings.Split(key, "/") if len(parts) < 4 || parts[0] != "vpp" || parts[1] != "interface" { return @@ -242,29 +242,29 @@ func ParseInterfaceAddressKey(key string) (iface string, ipAddr net.IP, ipAddrNe iface = strings.Join(parts[2:addrIdx], "/") if iface == "" { iface = InvalidKeyPart + invalidKey = true } // parse address type if addrIdx == len(parts)-1 { - invalidIP = true + invalidKey = true return } - switch parts[addrIdx+1] { - case addressStatic: - case addressFromDHCP: - fromDHCP = true - default: - invalidIP = true + + // parse address source + src := strings.ToUpper(parts[addrIdx+1]) + srcInt, validSrc := netalloc.IPAddressSource_value[src] + if !validSrc { + invalidKey = true return } + source = netalloc.IPAddressSource(srcInt) - // parse IP address - var err error - ipAddr, ipAddrNet, err = net.ParseCIDR(strings.Join(parts[addrIdx+2:], "/")) - if err != nil { - invalidIP = true + // return address as is (not parsed - this is done by the netalloc plugin) + address = strings.Join(parts[addrIdx+2:], "/") + if address == "" { + invalidKey = true } - return } diff --git a/api/models/vpp/interfaces/keys_test.go b/api/models/vpp/interfaces/keys_test.go index 19566cf767..7d8210d772 100644 --- a/api/models/vpp/interfaces/keys_test.go +++ b/api/models/vpp/interfaces/keys_test.go @@ -15,6 +15,7 @@ package vpp_interfaces import ( + "github.com/ligato/vpp-agent/api/models/netalloc" "testing" ) @@ -176,67 +177,93 @@ func TestInterfaceAddressKey(t *testing.T) { name string iface string address string - fromDHCP bool + source netalloc.IPAddressSource expectedKey string }{ { name: "IPv4 address", iface: "memif0", address: "192.168.1.12/24", + source: netalloc.IPAddressSource_STATIC, expectedKey: "vpp/interface/memif0/address/static/192.168.1.12/24", }, { name: "IPv4 address from DHCP", iface: "memif0", address: "192.168.1.12/24", - fromDHCP: true, - expectedKey: "vpp/interface/memif0/address/from-dhcp/192.168.1.12/24", + source: netalloc.IPAddressSource_FROM_DHCP, + expectedKey: "vpp/interface/memif0/address/from_dhcp/192.168.1.12/24", }, { name: "IPv6 address", iface: "memif0", address: "2001:db8::/32", + source: netalloc.IPAddressSource_STATIC, expectedKey: "vpp/interface/memif0/address/static/2001:db8::/32", }, { name: "IPv6 address from DHCP", iface: "memif0", address: "2001:db8::/32", - fromDHCP: true, - expectedKey: "vpp/interface/memif0/address/from-dhcp/2001:db8::/32", + source: netalloc.IPAddressSource_FROM_DHCP, + expectedKey: "vpp/interface/memif0/address/from_dhcp/2001:db8::/32", }, { name: "invalid interface", iface: "", address: "10.10.10.10/32", + source: netalloc.IPAddressSource_STATIC, expectedKey: "vpp/interface//address/static/10.10.10.10/32", }, + { + name: "undefined source", + iface: "memif1", + address: "10.10.10.10/32", + expectedKey: "vpp/interface/memif1/address/undefined_source/10.10.10.10/32", + }, { name: "invalid address", iface: "tap0", address: "invalid-addr", + source: netalloc.IPAddressSource_STATIC, expectedKey: "vpp/interface/tap0/address/static/invalid-addr", }, { name: "missing mask", iface: "tap1", address: "10.10.10.10", + source: netalloc.IPAddressSource_STATIC, expectedKey: "vpp/interface/tap1/address/static/10.10.10.10", }, { name: "empty address", iface: "tap1", address: "", + source: netalloc.IPAddressSource_STATIC, expectedKey: "vpp/interface/tap1/address/static/", }, + { + name: "IPv4 address requested from netalloc", + iface: "memif0", + address: "alloc:net1", + source: netalloc.IPAddressSource_STATIC, + expectedKey: "vpp/interface/memif0/address/alloc_ref/alloc:net1", + }, + { + name: "IPv6 address requested from netalloc", + iface: "memif0", + address: "alloc:net1/IPV6_ADDR", + source: netalloc.IPAddressSource_STATIC, + expectedKey: "vpp/interface/memif0/address/alloc_ref/alloc:net1/IPV6_ADDR", + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - key := InterfaceAddressKey(test.iface, test.address, test.fromDHCP) + key := InterfaceAddressKey(test.iface, test.address, test.source) if key != test.expectedKey { - t.Errorf("failed for: iface=%s address=%s\n"+ + t.Errorf("failed for: iface=%s address=%s source=%s\n"+ "expected key:\n\t%q\ngot key:\n\t%q", - test.iface, test.address, test.expectedKey, key) + test.iface, test.address, string(test.source), test.expectedKey, key) } }) } @@ -244,213 +271,169 @@ func TestInterfaceAddressKey(t *testing.T) { func TestParseInterfaceAddressKey(t *testing.T) { tests := []struct { - name string - key string - expectedIface string - expectedIfaceAddr string - expectedIfaceAddrNet string - expectedFromDHCP bool - expectedInvalidIP bool - expectedIsAddrKey bool + name string + key string + expectedIface string + expectedIfaceAddr string + expectedSource netalloc.IPAddressSource + expectedInvalidKey bool + expectedIsAddrKey bool }{ { - name: "IPv4 address", - key: "vpp/interface/memif0/address/static/192.168.1.12/24", - expectedIface: "memif0", - expectedIfaceAddr: "192.168.1.12", - expectedIfaceAddrNet: "192.168.1.0/24", - expectedIsAddrKey: true, + name: "IPv4 address", + key: "vpp/interface/memif0/address/static/192.168.1.12/24", + expectedIface: "memif0", + expectedIfaceAddr: "192.168.1.12/24", + expectedSource: netalloc.IPAddressSource_STATIC, + expectedIsAddrKey: true, }, { - name: "IPv4 address from DHCP", - key: "vpp/interface/memif0/address/from-dhcp/192.168.1.12/24", - expectedIface: "memif0", - expectedIfaceAddr: "192.168.1.12", - expectedIfaceAddrNet: "192.168.1.0/24", - expectedIsAddrKey: true, - expectedFromDHCP: true, + name: "IPv4 address from DHCP", + key: "vpp/interface/memif0/address/from_dhcp/192.168.1.12/24", + expectedIface: "memif0", + expectedIfaceAddr: "192.168.1.12/24", + expectedSource: netalloc.IPAddressSource_FROM_DHCP, + expectedIsAddrKey: true, }, { - name: "IPv6 address", - key: "vpp/interface/tap1/address/static/2001:db8:85a3::8a2e:370:7334/48", - expectedIface: "tap1", - expectedIfaceAddr: "2001:db8:85a3::8a2e:370:7334", - expectedIfaceAddrNet: "2001:db8:85a3::/48", - expectedIsAddrKey: true, + name: "IPv4 address requested from Netalloc", + key: "vpp/interface/memif0/address/alloc_ref/alloc:net1", + expectedIface: "memif0", + expectedIfaceAddr: "alloc:net1", + expectedSource: netalloc.IPAddressSource_ALLOC_REF, + expectedIsAddrKey: true, }, { - name: "IPv6 address", - key: "vpp/interface/tap1/address/from-dhcp/2001:db8:85a3::8a2e:370:7334/48", - expectedIface: "tap1", - expectedIfaceAddr: "2001:db8:85a3::8a2e:370:7334", - expectedIfaceAddrNet: "2001:db8:85a3::/48", - expectedIsAddrKey: true, - expectedFromDHCP: true, + name: "IPv6 address", + key: "vpp/interface/tap1/address/static/2001:db8:85a3::8a2e:370:7334/48", + expectedIface: "tap1", + expectedIfaceAddr: "2001:db8:85a3::8a2e:370:7334/48", + expectedSource: netalloc.IPAddressSource_STATIC, + expectedIsAddrKey: true, }, { - name: "invalid interface", - key: "vpp/interface//address/static/10.10.10.10/30", - expectedIface: "", - expectedIfaceAddr: "10.10.10.10", - expectedIfaceAddrNet: "10.10.10.8/30", - expectedIsAddrKey: true, + name: "IPv6 address requested from netalloc", + key: "vpp/interface/tap1/address/alloc_ref/alloc:net1/IPV6_ADDR", + expectedIface: "tap1", + expectedIfaceAddr: "alloc:net1/IPV6_ADDR", + expectedSource: netalloc.IPAddressSource_ALLOC_REF, + expectedIsAddrKey: true, }, { - name: "gbe interface", - key: "vpp/interface/GigabitEthernet0/8/0/address/static/192.168.5.5/16", - expectedIface: "GigabitEthernet0/8/0", - expectedIfaceAddr: "192.168.5.5", - expectedIfaceAddrNet: "192.168.0.0/16", - expectedIsAddrKey: true, - }, - { - name: "missing interface", - key: "vpp/interface//address/static/192.168.5.5/16", - expectedIface: "", - expectedIfaceAddr: "192.168.5.5", - expectedIfaceAddrNet: "192.168.0.0/16", - expectedIsAddrKey: true, - }, - { - name: "missing interface (from DHCP)", - key: "vpp/interface//address/from-dhcp/192.168.5.5/16", - expectedIface: "", - expectedIfaceAddr: "192.168.5.5", - expectedIfaceAddrNet: "192.168.0.0/16", - expectedIsAddrKey: true, - expectedFromDHCP: true, - }, - { - name: "not valid IP (missing mask)", - key: "vpp/interface/tap3/address/static/192.168.5.5", - expectedIface: "tap3", - expectedIfaceAddr: "", - expectedIfaceAddrNet: "", - expectedInvalidIP: true, - expectedIsAddrKey: true, - }, - { - name: "not valid IP (missing mask, from DHCP)", - key: "vpp/interface/tap3/address/from-dhcp/192.168.5.5", - expectedIface: "tap3", - expectedIfaceAddr: "", - expectedIfaceAddrNet: "", - expectedInvalidIP: true, - expectedIsAddrKey: true, - expectedFromDHCP: true, - }, - { - name: "not valid IP for Gbe (missing mask)", - key: "vpp/interface/Gbe0/1/2/address/static/192.168.5.5", - expectedIface: "Gbe0/1/2", - expectedIfaceAddr: "", - expectedIfaceAddrNet: "", - expectedInvalidIP: true, - expectedIsAddrKey: true, - }, - { - name: "not valid IP (missing address and mask)", - key: "vpp/interface/tap3/address/static/", - expectedIface: "tap3", - expectedIfaceAddr: "", - expectedIfaceAddrNet: "", - expectedInvalidIP: true, - expectedIsAddrKey: true, - }, - { - name: "not valid IP (missing address and mask, from DHCP)", - key: "vpp/interface/tap3/address/from-dhcp/", - expectedIface: "tap3", - expectedIfaceAddr: "", - expectedIfaceAddrNet: "", - expectedInvalidIP: true, - expectedIsAddrKey: true, - expectedFromDHCP: true, - }, - { - name: "not valid IP for Gbe (missing address and mask)", - key: "vpp/interface/Gbe0/1/2/address/static/", - expectedIface: "Gbe0/1/2", - expectedIfaceAddr: "", - expectedIfaceAddrNet: "", - expectedInvalidIP: true, - expectedIsAddrKey: true, - }, - { - name: "not interface address key", - key: "vpp/config/v2/interface/GigabitEthernet0/8/0", - expectedIface: "", - expectedIfaceAddr: "", - expectedIfaceAddrNet: "", - expectedIsAddrKey: false, - }, - { - name: "invalid address", - key: "vpp/interface/tap3/2/1/address/static//32", - expectedIface: "tap3/2/1", - expectedIfaceAddr: "", - expectedIfaceAddrNet: "", - expectedInvalidIP: true, - expectedIsAddrKey: true, - }, - { - name: "invalid mask", - key: "vpp/interface/tap3/address/static/10.10.10.10/invalid", - expectedIface: "tap3", - expectedIfaceAddr: "", - expectedIfaceAddrNet: "", - expectedInvalidIP: true, - expectedIsAddrKey: true, - }, - { - name: "invalid address type", - key: "vpp/interface/memif0/address//192.168.1.12/24", - expectedIface: "memif0", - expectedInvalidIP: true, - expectedIsAddrKey: true, + name: "IPv6 address from DHCP", + key: "vpp/interface/tap1/address/from_dhcp/2001:db8:85a3::8a2e:370:7334/48", + expectedIface: "tap1", + expectedIfaceAddr: "2001:db8:85a3::8a2e:370:7334/48", + expectedSource: netalloc.IPAddressSource_FROM_DHCP, + expectedIsAddrKey: true, }, { - name: "empty address type", - key: "vpp/interface/memif0/address//192.168.1.12/24", - expectedIface: "memif0", - expectedInvalidIP: true, - expectedIsAddrKey: true, + name: "invalid interface", + key: "vpp/interface//address/static/10.10.10.10/30", + expectedIface: "", + expectedIfaceAddr: "10.10.10.10/30", + expectedSource: netalloc.IPAddressSource_STATIC, + expectedIsAddrKey: true, }, { - name: "missing address type", - key: "vpp/interface/memif0/address/192.168.1.12/24", - expectedIface: "memif0", - expectedInvalidIP: true, - expectedIsAddrKey: true, + name: "gbe interface", + key: "vpp/interface/GigabitEthernet0/8/0/address/static/192.168.5.5/16", + expectedIface: "GigabitEthernet0/8/0", + expectedIfaceAddr: "192.168.5.5/16", + expectedSource: netalloc.IPAddressSource_STATIC, + expectedIsAddrKey: true, + }, + { + name: "missing interface", + key: "vpp/interface//address/static/192.168.5.5/16", + expectedIface: "", + expectedIfaceAddr: "192.168.5.5/16", + expectedSource: netalloc.IPAddressSource_STATIC, + expectedInvalidKey: true, + expectedIsAddrKey: true, + }, + { + name: "missing interface (from DHCP)", + key: "vpp/interface//address/from_dhcp/192.168.5.5/16", + expectedIface: "", + expectedIfaceAddr: "192.168.5.5/16", + expectedSource: netalloc.IPAddressSource_FROM_DHCP, + expectedInvalidKey: true, + expectedIsAddrKey: true, + }, + { + name: "missing IP", + key: "vpp/interface/tap3/address/static/", + expectedIface: "tap3", + expectedIfaceAddr: "", + expectedSource: netalloc.IPAddressSource_STATIC, + expectedInvalidKey: true, + expectedIsAddrKey: true, + }, + { + name: "missing IP (from DHCP)", + key: "vpp/interface/tap3/address/from_dhcp/", + expectedIface: "tap3", + expectedIfaceAddr: "", + expectedSource: netalloc.IPAddressSource_FROM_DHCP, + expectedInvalidKey: true, + expectedIsAddrKey: true, + }, + { + name: "missing IP for Gbe", + key: "vpp/interface/Gbe0/1/2/address/static/", + expectedIface: "Gbe0/1/2", + expectedIfaceAddr: "", + expectedSource: netalloc.IPAddressSource_STATIC, + expectedInvalidKey: true, + expectedIsAddrKey: true, + }, + { + name: "not interface address key", + key: "vpp/config/v2/interface/GigabitEthernet0/8/0", + expectedIface: "", + expectedIfaceAddr: "", + expectedIsAddrKey: false, + }, + { + name: "invalid address source", + key: "vpp/interface/memif0/address//192.168.1.12/24", + expectedIface: "memif0", + expectedInvalidKey: true, + expectedIsAddrKey: true, + }, + { + name: "empty address source", + key: "vpp/interface/memif0/address//192.168.1.12/24", + expectedIface: "memif0", + expectedInvalidKey: true, + expectedIsAddrKey: true, + }, + { + name: "missing address source", + key: "vpp/interface/memif0/address/192.168.1.12/24", + expectedIface: "memif0", + expectedInvalidKey: true, + expectedIsAddrKey: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - iface, ipAddr, ipAddrNet, fromDHCP, invalidIP, isAddrKey := ParseInterfaceAddressKey(test.key) - var ipAddrStr, ipAddrNetStr string - if ipAddr != nil { - ipAddrStr = ipAddr.String() - } - if ipAddrNet != nil { - ipAddrNetStr = ipAddrNet.String() - } + iface, ipAddr, source, invalidKey, isAddrKey := ParseInterfaceAddressKey(test.key) if isAddrKey != test.expectedIsAddrKey { t.Errorf("expected isAddrKey: %v\tgot: %v", test.expectedIsAddrKey, isAddrKey) } - if fromDHCP != test.expectedFromDHCP { - t.Errorf("expected fromDHCP: %v\tgot: %v", test.expectedFromDHCP, fromDHCP) + if source != test.expectedSource { + t.Errorf("expected source: %v\tgot: %v", test.expectedSource, source) } - if invalidIP != test.expectedInvalidIP { - t.Errorf("expected invalidIP: %v\tgot: %v", test.expectedInvalidIP, invalidIP) + if invalidKey != test.expectedInvalidKey { + t.Errorf("expected invalidKey: %v\tgot: %v", test.expectedInvalidKey, invalidKey) } if iface != test.expectedIface { t.Errorf("expected iface: %s\tgot: %s", test.expectedIface, iface) } - if ipAddrStr != test.expectedIfaceAddr { - t.Errorf("expected ipAddr: %s\tgot: %s", test.expectedIface, ipAddrStr) - } - if ipAddrNetStr != test.expectedIfaceAddrNet { - t.Errorf("expected ipAddrNet: %s\tgot: %s", test.expectedIfaceAddrNet, ipAddrNetStr) + if ipAddr != test.expectedIfaceAddr { + t.Errorf("expected ipAddr: %s\tgot: %s", test.expectedIfaceAddr, ipAddr) } }) } diff --git a/api/models/vpp/l3/keys.go b/api/models/vpp/l3/keys.go index dde4662154..86acb20b4c 100644 --- a/api/models/vpp/l3/keys.go +++ b/api/models/vpp/l3/keys.go @@ -16,7 +16,6 @@ package vpp_l3 import ( "fmt" - "strconv" "strings" "github.com/ligato/vpp-agent/pkg/models" @@ -41,7 +40,8 @@ var ( }, models.WithNameTemplate( `{{if .OutgoingInterface}}{{printf "if/%s/" .OutgoingInterface}}{{end}}`+ `vrf/{{.VrfId}}/`+ - `{{with ipnet .DstNetwork}}{{printf "dst/%s/%d/" .IP .MaskSize}}{{end}}`+ + `{{with ipnet .DstNetwork}}{{printf "dst/%s/%d/" .IP .MaskSize}}`+ + `{{else}}{{printf "dst/%s/" .DstNetwork}}{{end}}` + `{{if .NextHopAddr}}gw/{{.NextHopAddr}}{{end}}`, )) @@ -144,17 +144,38 @@ func RouteVrfPrefix(vrf uint32) string { } // ParseRouteKey parses VRF label and route address from a route key. -func ParseRouteKey(key string) (vrfIndex string, dstNetAddr string, dstNetMask int, nextHopAddr string, isRouteKey bool) { +func ParseRouteKey(key string) (outIface, vrfIndex, dstNet, nextHopAddr string, isRouteKey bool) { if routeKey := strings.TrimPrefix(key, ModelRoute.KeyPrefix()); routeKey != key { + var foundVrf, foundDst bool keyParts := strings.Split(routeKey, "/") - if len(keyParts) >= 7 && - keyParts[0] == "vrf" && - keyParts[2] == "dst" && - keyParts[5] == "gw" { - if mask, err := strconv.Atoi(keyParts[4]); err == nil { - return keyParts[1], keyParts[3], mask, keyParts[6], true - } + outIface, _ = getRouteKeyItem(keyParts, "if", "vrf") + vrfIndex, foundVrf = getRouteKeyItem(keyParts, "vrf", "dst") + dstNet, foundDst = getRouteKeyItem(keyParts, "dst", "gw") + nextHopAddr, _ = getRouteKeyItem(keyParts, "gw", "") + if foundDst && foundVrf { + isRouteKey = true + return } } - return "", "", 0, "", false + return "", "", "", "", false } + +func getRouteKeyItem(items []string, itemLabel, nextItemLabel string) (value string, found bool) { + begin := len(items) + end := len(items) + for i, item := range items { + if item == itemLabel { + begin = i+1 + } + if nextItemLabel != "" && item == nextItemLabel { + end = i + break + } + } + if begin < end { + value = strings.Join(items[begin:end], "/") + value = strings.TrimSuffix(value, "/") + return value, true + } + return "", false +} \ No newline at end of file diff --git a/api/models/vpp/l3/keys_test.go b/api/models/vpp/l3/keys_test.go index 3309bbd34e..d2d6b9091a 100644 --- a/api/models/vpp/l3/keys_test.go +++ b/api/models/vpp/l3/keys_test.go @@ -137,9 +137,9 @@ func TestParseRouteKey(t *testing.T) { name string routeKey string expectedIsRouteKey bool + expectedOutIface string expectedVrfIndex string - expectedDstNetAddr string - expectedDstNetMask int + expectedDstNet string expectedNextHopAddr string }{ { @@ -147,8 +147,16 @@ func TestParseRouteKey(t *testing.T) { routeKey: "config/vpp/v2/route/vrf/0/dst/10.10.0.0/16/gw/0.0.0.0", expectedIsRouteKey: true, expectedVrfIndex: "0", - expectedDstNetAddr: "10.10.0.0", - expectedDstNetMask: 16, + expectedDstNet: "10.10.0.0/16", + expectedNextHopAddr: "0.0.0.0", + }, + { + name: "route-ipv4 with interface", + routeKey: "config/vpp/v2/route/if/Gbe0/8/0/vrf/0/dst/10.10.0.0/16/gw/0.0.0.0", + expectedIsRouteKey: true, + expectedOutIface: "Gbe0/8/0", + expectedVrfIndex: "0", + expectedDstNet: "10.10.0.0/16", expectedNextHopAddr: "0.0.0.0", }, { @@ -156,14 +164,15 @@ func TestParseRouteKey(t *testing.T) { routeKey: "config/vpp/v2/route/vrf/0/dst/2001:db8::/32/gw/::", expectedIsRouteKey: true, expectedVrfIndex: "0", - expectedDstNetAddr: "2001:db8::", - expectedDstNetMask: 32, + expectedDstNet: "2001:db8::/32", expectedNextHopAddr: "::", }, { - name: "invalid-key", + name: "undefined interface and GW", routeKey: "config/vpp/v2/route/vrf/0/dst/2001:db8::/32/", - expectedIsRouteKey: false, + expectedIsRouteKey: true, + expectedVrfIndex: "0", + expectedDstNet: "2001:db8::/32", }, { name: "invalid-key-missing-dst", @@ -174,12 +183,12 @@ func TestParseRouteKey(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { RegisterTestingT(t) - vrfIndex, dstNetAddr, dstNetMask, nextHopAddr, isRouteKey := ParseRouteKey(test.routeKey) + outIface, vrfIndex, dstNet, nextHopAddr, isRouteKey := ParseRouteKey(test.routeKey) Expect(isRouteKey).To(BeEquivalentTo(test.expectedIsRouteKey), "Route/Non-route key should be properly detected") if isRouteKey { + Expect(outIface).To(BeEquivalentTo(test.expectedOutIface), "outgoing interface should be properly extracted by parsing route key") Expect(vrfIndex).To(BeEquivalentTo(test.expectedVrfIndex), "VRF should be properly extracted by parsing route key") - Expect(dstNetAddr).To(BeEquivalentTo(test.expectedDstNetAddr), "Destination network address should be properly extracted by parsing route key") - Expect(dstNetMask).To(BeEquivalentTo(test.expectedDstNetMask), "Destination network mask should be properly extracted by parsing route key") + Expect(dstNet).To(BeEquivalentTo(test.expectedDstNet), "Destination network should be properly extracted by parsing route key") Expect(nextHopAddr).To(BeEquivalentTo(test.expectedNextHopAddr), "Next hop address should be properly extracted by parsing route key") } }) diff --git a/client/remoteclient/grpc_client.go b/client/remoteclient/grpc_client.go index 6e1c4f13d4..f6e915d11a 100644 --- a/client/remoteclient/grpc_client.go +++ b/client/remoteclient/grpc_client.go @@ -157,6 +157,7 @@ func (r *setConfigRequest) Delete(items ...proto.Message) client.ChangeRequest { r.err = err return r } + item.Data = nil // delete r.req.Updates = append(r.req.Updates, &api.UpdateItem{ Item: item, }) diff --git a/cmd/vpp-agent/app/vpp_agent.go b/cmd/vpp-agent/app/vpp_agent.go index 2503a7fb44..632c631152 100644 --- a/cmd/vpp-agent/app/vpp_agent.go +++ b/cmd/vpp-agent/app/vpp_agent.go @@ -25,6 +25,7 @@ import ( "github.com/ligato/cn-infra/db/keyval/redis" "github.com/ligato/cn-infra/health/probe" "github.com/ligato/cn-infra/health/statuscheck" + "github.com/ligato/cn-infra/infra" "github.com/ligato/cn-infra/logging/logmanager" "github.com/ligato/cn-infra/messaging/kafka" @@ -33,6 +34,7 @@ import ( linux_iptablesplugin "github.com/ligato/vpp-agent/plugins/linux/iptablesplugin" linux_l3plugin "github.com/ligato/vpp-agent/plugins/linux/l3plugin" linux_nsplugin "github.com/ligato/vpp-agent/plugins/linux/nsplugin" + "github.com/ligato/vpp-agent/plugins/netalloc" "github.com/ligato/vpp-agent/plugins/orchestrator" "github.com/ligato/vpp-agent/plugins/restapi" "github.com/ligato/vpp-agent/plugins/telemetry" @@ -52,13 +54,15 @@ import ( // Note: the plugin itself is loaded after all its dependencies. It means that the VPP plugin is first in the list // despite it needs to be loaded after the linux plugin. type VPPAgent struct { + infra.PluginName LogManager *logmanager.Plugin - // VPP & Linux are first to ensure that + // VPP & Linux (and other plugins with descriptors) are first to ensure that // all their descriptors are registered to KVScheduler // before orchestrator that starts watch for their NB key prefixes. VPP Linux + Netalloc *netalloc.Plugin Orchestrator *orchestrator.Plugin @@ -69,6 +73,7 @@ type VPPAgent struct { Configurator *configurator.Plugin RESTAPI *restapi.Plugin Probe *probe.Plugin + StatusCheck *statuscheck.Plugin Telemetry *telemetry.Plugin } @@ -123,6 +128,7 @@ func New() *VPPAgent { linux := DefaultLinux() return &VPPAgent{ + PluginName: "VPPAgent", LogManager: &logmanager.DefaultPlugin, Orchestrator: &orchestrator.DefaultPlugin, ETCDDataSync: etcdDataSync, @@ -130,23 +136,28 @@ func New() *VPPAgent { RedisDataSync: redisDataSync, VPP: vpp, Linux: linux, + Netalloc: &netalloc.DefaultPlugin, Configurator: &configurator.DefaultPlugin, RESTAPI: &restapi.DefaultPlugin, Probe: &probe.DefaultPlugin, + StatusCheck: &statuscheck.DefaultPlugin, Telemetry: &telemetry.DefaultPlugin, } } // Init initializes main plugin. -func (VPPAgent) Init() error { +func (a *VPPAgent) Init() error { + a.StatusCheck.Register(a.PluginName, nil) + a.StatusCheck.ReportStateChange(a.PluginName, statuscheck.Init, nil) return nil } // AfterInit executes resync. -func (VPPAgent) AfterInit() error { +func (a *VPPAgent) AfterInit() error { // manually start resync after all plugins started resync.DefaultPlugin.DoResync() //orchestrator.DefaultPlugin.InitialSync() + a.StatusCheck.ReportStateChange(a.PluginName, statuscheck.OK, nil) return nil } @@ -155,11 +166,6 @@ func (VPPAgent) Close() error { return nil } -// String returns name of the plugin. -func (VPPAgent) String() string { - return "VPPAgent" -} - // VPP contains all VPP plugins. type VPP struct { ABFPlugin *abfplugin.ABFPlugin diff --git a/examples/kvscheduler/netalloc/main.go b/examples/kvscheduler/netalloc/main.go new file mode 100644 index 0000000000..94073a9630 --- /dev/null +++ b/examples/kvscheduler/netalloc/main.go @@ -0,0 +1,329 @@ +// Copyright (c) 2018 Cisco and/or its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "log" + "time" + "context" + + "github.com/ligato/cn-infra/agent" + "github.com/ligato/vpp-agent/client" + "github.com/ligato/vpp-agent/plugins/orchestrator" + + "github.com/ligato/vpp-agent/api/models/linux/interfaces" + "github.com/ligato/vpp-agent/api/models/linux/l3" + linux_ns "github.com/ligato/vpp-agent/api/models/linux/namespace" + "github.com/ligato/vpp-agent/api/models/netalloc" + "github.com/ligato/vpp-agent/api/models/vpp/interfaces" + linux_ifplugin "github.com/ligato/vpp-agent/plugins/linux/ifplugin" + linux_l3plugin "github.com/ligato/vpp-agent/plugins/linux/l3plugin" + linux_nsplugin "github.com/ligato/vpp-agent/plugins/linux/nsplugin" + vpp_ifplugin "github.com/ligato/vpp-agent/plugins/vpp/ifplugin" +) + +/* + This example demonstrates netalloc plugin (topology disassociated from the addressing). +*/ + +func main() { + // Set inter-dependency between VPP & Linux plugins + vpp_ifplugin.DefaultPlugin.LinuxIfPlugin = &linux_ifplugin.DefaultPlugin + vpp_ifplugin.DefaultPlugin.NsPlugin = &linux_nsplugin.DefaultPlugin + linux_ifplugin.DefaultPlugin.VppIfPlugin = &vpp_ifplugin.DefaultPlugin + + ep := &ExamplePlugin{ + Orchestrator: &orchestrator.DefaultPlugin, + LinuxIfPlugin: &linux_ifplugin.DefaultPlugin, + LinuxL3Plugin: &linux_l3plugin.DefaultPlugin, + VPPIfPlugin: &vpp_ifplugin.DefaultPlugin, + } + + a := agent.NewAgent( + agent.AllPlugins(ep), + ) + if err := a.Run(); err != nil { + log.Fatal(err) + } +} + +// ExamplePlugin is the main plugin which +// handles resync and changes in this example. +type ExamplePlugin struct { + LinuxIfPlugin *linux_ifplugin.IfPlugin + LinuxL3Plugin *linux_l3plugin.L3Plugin + VPPIfPlugin *vpp_ifplugin.IfPlugin + Orchestrator *orchestrator.Plugin +} + +// String returns plugin name +func (p *ExamplePlugin) String() string { + return "netalloc-example" +} + +// Init handles initialization phase. +func (p *ExamplePlugin) Init() error { + return nil +} + +// AfterInit handles phase after initialization. +func (p *ExamplePlugin) AfterInit() error { + go demonstrateNetalloc() + return nil +} + +// Close cleans up the resources. +func (p *ExamplePlugin) Close() error { + return nil +} + +func demonstrateNetalloc() { + // initial resync + time.Sleep(time.Second) + fmt.Println("=== RESYNC ===") + + err := client.LocalClient.ResyncConfig( + // addresses + veth1Addr, afpacketAddr, linuxTapAddr, vppTapAddr, + // topology + veth2, veth1, linuxTap, arpForVeth1, arpForLinuxTap, + linkRouteToMs1, routeToMs1, linkRouteToMs2, routeToMs2, + afpacket, vppTap) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println("=== CHANGE ===") + time.Sleep(time.Second * 5) + err = client.LocalClient.ChangeRequest(). + Delete(veth1Addr). + Send(context.Background()) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println("=== CHANGE (revert of the previous change) ===") + time.Sleep(time.Second * 5) + err = client.LocalClient.ChangeRequest(). + Update(veth1Addr). + Send(context.Background()) + if err != nil { + fmt.Println(err) + return + } +} + +const ( + networkName = "example-net" + + veth1LogicalName = "myVETH1" + veth1IPAddr = "10.11.1.1" + veth1IPAddr2 = "10.11.1.10" + + veth2LogicalName = "myVETH2" + veth2HostName = "veth2" + + afPacketLogicalName = "myAFPacket" + afPacketIPAddr = "10.11.1.2" + afPacketHWAddr = "a7:35:45:55:65:75" + + vppTapLogicalName = "myVPPTap" + vppTapIPAddr = "10.11.2.2" + vppTapHwAddr = "b3:12:12:45:A7:B7" + + linuxTapLogicalName = "myLinuxTAP" + + linuxTapIPAddr = "10.11.2.1" + linuxTapHwAddr = "88:88:88:88:88:88" + + microserviceNetMask = "/30" + mycroservice1 = "microservice1" + mycroservice2 = "microservice2" + + mycroservice2Mtu = 1700 + + routeMetric = 50 +) + +// ADRESSING + +var ( + veth1Addr = &netalloc.IPAllocation{ + NetworkName: networkName, + InterfaceName: veth1LogicalName, + Address: veth1IPAddr + microserviceNetMask, + Gw: vppTapIPAddr, + } + + veth1Addr2 = &netalloc.IPAllocation{ + NetworkName: networkName, + InterfaceName: veth1LogicalName, + Address: veth1IPAddr2 + microserviceNetMask, + Gw: vppTapIPAddr, + } + + afpacketAddr = &netalloc.IPAllocation{ + NetworkName: networkName, + InterfaceName: afPacketLogicalName, + Address: afPacketIPAddr + microserviceNetMask, + } + + linuxTapAddr = &netalloc.IPAllocation{ + NetworkName: networkName, + InterfaceName: linuxTapLogicalName, + Address: linuxTapIPAddr + microserviceNetMask, + Gw: afPacketIPAddr, + } + + vppTapAddr = &netalloc.IPAllocation{ + NetworkName: networkName, + InterfaceName: vppTapLogicalName, + Address: vppTapIPAddr + microserviceNetMask, + } +) + +// TOPOLOGY + +var ( + /* microservice1 <-> VPP */ + veth1 = &linux_interfaces.Interface{ + Name: veth1LogicalName, + Type: linux_interfaces.Interface_VETH, + Enabled: true, + PhysAddress: "66:66:66:66:66:66", + IpAddresses: []string{ + "alloc:" + networkName, + }, + Mtu: 1800, + HostIfName: "veth1", + Link: &linux_interfaces.Interface_Veth{ + Veth: &linux_interfaces.VethLink{PeerIfName: veth2LogicalName}, + }, + Namespace: &linux_ns.NetNamespace{ + Type: linux_ns.NetNamespace_MICROSERVICE, + Reference: mycroservice1, + }, + } + + arpForVeth1 = &linux_l3.ARPEntry{ + Interface: veth1LogicalName, + IpAddress: "alloc:" + networkName + "/" + vppTapLogicalName, + HwAddress: vppTapHwAddr, + } + + linkRouteToMs2 = &linux_l3.Route{ + OutgoingInterface: veth1LogicalName, + Scope: linux_l3.Route_LINK, + DstNetwork: "alloc:" + networkName + "/" + veth1LogicalName + "/GW", + } + + routeToMs2 = &linux_l3.Route{ + OutgoingInterface: veth1LogicalName, + Scope: linux_l3.Route_GLOBAL, + DstNetwork: "alloc:" + networkName + "/" + linuxTapLogicalName, + GwAddr: "alloc:" + networkName + "/GW", + Metric: routeMetric, + } + + veth2 = &linux_interfaces.Interface{ + Name: veth2LogicalName, + Type: linux_interfaces.Interface_VETH, + Enabled: true, + Mtu: 1800, + HostIfName: veth2HostName, + Link: &linux_interfaces.Interface_Veth{ + Veth: &linux_interfaces.VethLink{PeerIfName: veth1LogicalName}, + }, + } + + afpacket = &vpp_interfaces.Interface{ + Name: afPacketLogicalName, + Type: vpp_interfaces.Interface_AF_PACKET, + Enabled: true, + PhysAddress: afPacketHWAddr, + IpAddresses: []string{ + "alloc:" + networkName, + }, + Mtu: 1800, + Link: &vpp_interfaces.Interface_Afpacket{ + Afpacket: &vpp_interfaces.AfpacketLink{ + HostIfName: veth2HostName, + }, + }, + } + + /* microservice2 <-> VPP */ + + linuxTap = &linux_interfaces.Interface{ + Name: linuxTapLogicalName, + Type: linux_interfaces.Interface_TAP_TO_VPP, + Enabled: true, + PhysAddress: linuxTapHwAddr, + IpAddresses: []string{ + "alloc:" + networkName, + }, + Mtu: mycroservice2Mtu, + HostIfName: "tap_to_vpp", + Link: &linux_interfaces.Interface_Tap{ + Tap: &linux_interfaces.TapLink{ + VppTapIfName: vppTapLogicalName, + }, + }, + Namespace: &linux_ns.NetNamespace{ + Type: linux_ns.NetNamespace_MICROSERVICE, + Reference: mycroservice2, + }, + } + + vppTap = &vpp_interfaces.Interface{ + Name: vppTapLogicalName, + Type: vpp_interfaces.Interface_TAP, + Enabled: true, + PhysAddress: vppTapHwAddr, + IpAddresses: []string{ + "alloc:" + networkName, + }, + Mtu: mycroservice2Mtu, + Link: &vpp_interfaces.Interface_Tap{ + Tap: &vpp_interfaces.TapLink{ + Version: 2, + ToMicroservice: mycroservice2, + }, + }, + } + + arpForLinuxTap = &linux_l3.ARPEntry{ + Interface: linuxTapLogicalName, + IpAddress: "alloc:" + networkName + "/" + afPacketLogicalName, + HwAddress: afPacketHWAddr, + } + + linkRouteToMs1 = &linux_l3.Route{ + OutgoingInterface: linuxTapLogicalName, + Scope: linux_l3.Route_LINK, + DstNetwork: "alloc:" + networkName + "/" + linuxTapLogicalName + "/GW", + } + + routeToMs1 = &linux_l3.Route{ + OutgoingInterface: linuxTapLogicalName, + Scope: linux_l3.Route_GLOBAL, + DstNetwork: "alloc:" + networkName + "/" + veth1LogicalName, + GwAddr: "alloc:" + networkName + "/GW", + Metric: routeMetric, + } +) diff --git a/pkg/models/spec.go b/pkg/models/spec.go index 8dde544da1..4bdc648ab6 100644 --- a/pkg/models/spec.go +++ b/pkg/models/spec.go @@ -247,17 +247,23 @@ var funcMap = template.FuncMap{ return "IPv4" }, "ipnet": func(s string) map[string]interface{} { + if strings.HasPrefix(s, "alloc:") { + // reference to IP address allocated via netalloc + return nil + } _, ipNet, err := net.ParseCIDR(s) if err != nil { return map[string]interface{}{ "IP": "", "MaskSize": 0, + "AllocRef": "", } } maskSize, _ := ipNet.Mask.Size() return map[string]interface{}{ "IP": ipNet.IP.String(), "MaskSize": maskSize, + "AllocRef": "", } }, } diff --git a/plugins/configurator/configurator.go b/plugins/configurator/configurator.go index 70224f2a5a..4823611081 100644 --- a/plugins/configurator/configurator.go +++ b/plugins/configurator/configurator.go @@ -24,6 +24,7 @@ import ( rpc "github.com/ligato/vpp-agent/api/configurator" "github.com/ligato/vpp-agent/api/models/linux" + "github.com/ligato/vpp-agent/api/models/netalloc" "github.com/ligato/vpp-agent/api/models/vpp" "github.com/ligato/vpp-agent/pkg/models" "github.com/ligato/vpp-agent/pkg/util" @@ -117,7 +118,8 @@ func (svc *configuratorServer) Delete(ctx context.Context, req *rpc.DeleteReques func newConfig() *rpc.Config { return &rpc.Config{ - LinuxConfig: &linux.ConfigData{}, - VppConfig: &vpp.ConfigData{}, + LinuxConfig: &linux.ConfigData{}, + VppConfig: &vpp.ConfigData{}, + NetallocConfig: &netalloc.ConfigData{}, } } diff --git a/plugins/configurator/options.go b/plugins/configurator/options.go index 329f71257c..43aad95ace 100644 --- a/plugins/configurator/options.go +++ b/plugins/configurator/options.go @@ -17,6 +17,7 @@ package configurator import ( "github.com/ligato/cn-infra/rpc/grpc" "github.com/ligato/vpp-agent/plugins/govppmux" + "github.com/ligato/vpp-agent/plugins/netalloc" "github.com/ligato/vpp-agent/plugins/orchestrator" "github.com/ligato/vpp-agent/plugins/vpp/aclplugin" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin" @@ -35,6 +36,7 @@ func NewPlugin(opts ...Option) *Plugin { p.GRPCServer = &grpc.DefaultPlugin p.Dispatch = &orchestrator.DefaultPlugin p.GoVppmux = &govppmux.DefaultPlugin + p.AddrAlloc = &netalloc.DefaultPlugin p.VPPACLPlugin = &aclplugin.DefaultPlugin p.VPPIfPlugin = &ifplugin.DefaultPlugin p.VPPL2Plugin = &l2plugin.DefaultPlugin diff --git a/plugins/configurator/plugin.go b/plugins/configurator/plugin.go index 7e41704968..34f1a139fb 100644 --- a/plugins/configurator/plugin.go +++ b/plugins/configurator/plugin.go @@ -26,6 +26,7 @@ import ( "github.com/ligato/vpp-agent/plugins/govppmux" iflinuxcalls "github.com/ligato/vpp-agent/plugins/linux/ifplugin/linuxcalls" l3linuxcalls "github.com/ligato/vpp-agent/plugins/linux/l3plugin/linuxcalls" + "github.com/ligato/vpp-agent/plugins/netalloc" "github.com/ligato/vpp-agent/plugins/orchestrator" abfvppcalls "github.com/ligato/vpp-agent/plugins/vpp/abfplugin/vppcalls" "github.com/ligato/vpp-agent/plugins/vpp/aclplugin" @@ -57,6 +58,7 @@ type Deps struct { GRPCServer grpc.Server Dispatch orchestrator.Dispatcher GoVppmux govppmux.StatsAPI + AddrAlloc netalloc.AddressAllocator VPPACLPlugin aclplugin.API VPPIfPlugin ifplugin.API VPPL2Plugin *l2plugin.L2Plugin @@ -122,7 +124,7 @@ func (p *Plugin) initHandlers() (err error) { if p.configurator.l2Handler == nil { p.Log.Info("VPP L2 handler is not available, it will be skipped") } - p.configurator.l3Handler = l3vppcalls.CompatibleL3VppHandler(p.vppChan, ifIndexes, vrfIndexes, p.Log) + p.configurator.l3Handler = l3vppcalls.CompatibleL3VppHandler(p.vppChan, ifIndexes, vrfIndexes, p.AddrAlloc, p.Log) if p.configurator.l3Handler == nil { p.Log.Info("VPP L3 handler is not available, it will be skipped") } diff --git a/plugins/kvscheduler/api/txn_record.go b/plugins/kvscheduler/api/txn_record.go index 76bb2e38a5..f6d75a3634 100644 --- a/plugins/kvscheduler/api/txn_record.go +++ b/plugins/kvscheduler/api/txn_record.go @@ -19,8 +19,6 @@ import ( "strings" "time" - "github.com/gogo/protobuf/proto" - "github.com/ligato/vpp-agent/plugins/kvscheduler/internal/utils" ) @@ -84,8 +82,8 @@ type RecordedTxnOp struct { Key string // changes - PrevValue proto.Message - NewValue proto.Message + PrevValue *utils.RecordedProtoMessage + NewValue *utils.RecordedProtoMessage PrevState ValueState NewState ValueState PrevErr error @@ -103,7 +101,7 @@ type RecordedTxnOp struct { // RecordedKVPair is used to record key-value pair. type RecordedKVPair struct { Key string - Value proto.Message + Value *utils.RecordedProtoMessage Origin ValueOrigin } diff --git a/plugins/kvscheduler/internal/utils/conversions.go b/plugins/kvscheduler/internal/utils/conversions.go index 7fadea00c5..a209f37d26 100644 --- a/plugins/kvscheduler/internal/utils/conversions.go +++ b/plugins/kvscheduler/internal/utils/conversions.go @@ -21,12 +21,16 @@ import ( // ProtoToString converts proto message to string. func ProtoToString(msg proto.Message) string { + if recProto, wrapped := msg.(*RecordedProtoMessage); wrapped { + if recProto != nil { + msg = recProto.Message + } else { + msg = nil + } + } if msg == nil { return "" } - if recProto, wrapped := msg.(*RecordedProtoMessage); wrapped { - msg = recProto.Message - } if _, isEmpty := msg.(*prototypes.Empty); isEmpty { return "" } diff --git a/plugins/kvscheduler/internal/utils/record.go b/plugins/kvscheduler/internal/utils/record.go index 1df18b5277..e8dc3d4660 100644 --- a/plugins/kvscheduler/internal/utils/record.go +++ b/plugins/kvscheduler/internal/utils/record.go @@ -15,6 +15,10 @@ package utils import ( + "encoding/json" + "fmt" + "reflect" + "github.com/gogo/protobuf/jsonpb" "github.com/gogo/protobuf/proto" ) @@ -23,27 +27,79 @@ import ( // REST API. type RecordedProtoMessage struct { proto.Message + ProtoMsgName string +} + +// ProtoWithName is used to marshall proto message data alongside the proto +// message name. +type ProtoWithName struct { + ProtoMsgName string + ProtoMsgData string } // MarshalJSON marshalls proto message using the marshaller from jsonpb. // The jsonpb package produces a different output than the standard "encoding/json" // package, which does not operate correctly on protocol buffers. func (p *RecordedProtoMessage) MarshalJSON() ([]byte, error) { - marshaller := &jsonpb.Marshaler{} - str, err := marshaller.MarshalToString(p.Message) + var ( + msgName string + msgData string + err error + ) + if p != nil { + msgName = proto.MessageName(p.Message) + marshaller := &jsonpb.Marshaler{} + msgData, err = marshaller.MarshalToString(p.Message) + if err != nil { + return nil, err + } + } + pwn, err := json.Marshal( + ProtoWithName{ProtoMsgName: msgName, ProtoMsgData: msgData}) if err != nil { return nil, err } - return []byte(str), nil + fmt.Printf("MARSHALLED: %s\n", string(pwn)) + return pwn, nil +} + +// UnmarshalJSON un-marshalls proto message using the marshaller from jsonpb. +// The jsonpb package produces a different output than the standard "encoding/json" +// package, which does not operate correctly on protocol buffers. +func (p *RecordedProtoMessage) UnmarshalJSON(data []byte) error { + fmt.Printf("UNMARSHALL: %s\n", string(data)) + pwn := ProtoWithName{} + err := json.Unmarshal(data, &pwn) + if err != nil { + return err + } + p.ProtoMsgName = pwn.ProtoMsgName + if p.ProtoMsgName == "" { + return nil + } + msgType := proto.MessageType(pwn.ProtoMsgName) + if msgType == nil { + return fmt.Errorf("unknown proto message: %s", p.ProtoMsgName) + } + msg := reflect.New(msgType.Elem()).Interface().(proto.Message) + err = jsonpb.UnmarshalString(pwn.ProtoMsgData, msg) + if err != nil { + return err + } + p.Message = msg + return nil } // RecordProtoMessage prepares proto message for recording and potential // access via REST API. // Note: no need to clone the message - once un-marshalled, the content is never // changed (otherwise it would break prev-new value comparisons). -func RecordProtoMessage(msg proto.Message) proto.Message { +func RecordProtoMessage(msg proto.Message) *RecordedProtoMessage { if msg == nil { return nil } - return &RecordedProtoMessage{Message: msg} + return &RecordedProtoMessage{ + Message: msg, + ProtoMsgName: proto.MessageName(msg), + } } diff --git a/plugins/kvscheduler/rest.go b/plugins/kvscheduler/rest.go index 5498b56e94..916f39af17 100644 --- a/plugins/kvscheduler/rest.go +++ b/plugins/kvscheduler/rest.go @@ -62,7 +62,7 @@ const ( // keyTimelineURL is URL used to obtain timeline of value changes for a given key. keyTimelineURL = urlPrefix + "key-timeline" - // keyArg is the name of the argument used to define key for "key-timeline" API. + // keyArg is the name of the argument used to define key for "key-timeline" and "status" API. keyArg = "key" // graphSnapshotURL is URL used to obtain graph snapshot from a given point in time. @@ -355,16 +355,17 @@ func (s *Scheduler) downstreamResyncPostHandler(formatter *render.Render) http.H if retry { ctx = kvs.WithRetryDefault(ctx) } - _, err := s.StartNBTransaction().Commit(ctx) + seqNum, err := s.StartNBTransaction().Commit(ctx) if err != nil { s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) return } - s.logError(formatter.Text(w, http.StatusOK, "SB was successfully synchronized with KVScheduler\n")) + txn := s.GetRecordedTransaction(seqNum) + s.logError(formatter.JSON(w, http.StatusOK, txn)) } } -func parseDumpAndStatusCommonArgs(args url.Values) (descriptor, keyPrefix string, err error) { +func parseDumpAndStatusCommonArgs(args url.Values) (descriptor, keyPrefix, key string, err error) { // parse optional *descriptor* argument descriptors, withDescriptor := args[descriptorArg] if withDescriptor && len(descriptors) != 1 { @@ -384,6 +385,16 @@ func parseDumpAndStatusCommonArgs(args url.Values) (descriptor, keyPrefix string if withKeyPrefix { keyPrefix = keyPrefixes[0] } + + // parse optional *key* argument + keys, withKey := args[keyArg] + if withKey && len(keys) != 1 { + err = errors.New("key argument listed more than once") + return + } + if withKey { + key = keys[0] + } return } @@ -392,7 +403,7 @@ func (s *Scheduler) dumpGetHandler(formatter *render.Render) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { args := req.URL.Query() - descriptor, keyPrefix, err := parseDumpAndStatusCommonArgs(args) + descriptor, keyPrefix, _, err := parseDumpAndStatusCommonArgs(args) if err != nil { s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) return @@ -448,7 +459,7 @@ func (s *Scheduler) statusGetHandler(formatter *render.Render) http.HandlerFunc return func(w http.ResponseWriter, req *http.Request) { args := req.URL.Query() - descriptor, keyPrefix, err := parseDumpAndStatusCommonArgs(args) + descriptor, keyPrefix, key, err := parseDumpAndStatusCommonArgs(args) if err != nil { s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) return @@ -457,13 +468,19 @@ func (s *Scheduler) statusGetHandler(formatter *render.Render) http.HandlerFunc graphR := s.graph.Read() defer graphR.Release() + if key != "" { + singleStatus := getValueStatus(graphR.GetNode(key), key) + s.logError(formatter.JSON(w, http.StatusOK, singleStatus)) + return + } + if descriptor == "" && keyPrefix != "" { descriptor = s.getDescriptorForKeyPrefix(keyPrefix) if descriptor == "" { err = errors.New("unknown key prefix") + s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) + return } - s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) - return } var nodes []graph.Node diff --git a/plugins/kvscheduler/txn_record.go b/plugins/kvscheduler/txn_record.go index e9ff24e666..14377bf2f3 100644 --- a/plugins/kvscheduler/txn_record.go +++ b/plugins/kvscheduler/txn_record.go @@ -22,8 +22,6 @@ import ( "strings" "time" - "github.com/gogo/protobuf/proto" - kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" "github.com/ligato/vpp-agent/plugins/kvscheduler/internal/graph" "github.com/ligato/vpp-agent/plugins/kvscheduler/internal/utils" @@ -78,7 +76,7 @@ func (s *Scheduler) GetRecordedTransaction(SeqNum uint64) (txn *kvs.RecordedTxn) // preRecordTxnOp prepares txn operation record - fills attributes that we can even // before executing the operation. func (s *Scheduler) preRecordTxnOp(args *applyValueArgs, node graph.Node) *kvs.RecordedTxnOp { - var prevValue proto.Message + var prevValue *utils.RecordedProtoMessage if getNodeState(node) != kvs.ValueState_REMOVED { prevValue = utils.RecordProtoMessage(node.GetValue()) } diff --git a/plugins/linux/ifplugin/descriptor/adapter/interface.go b/plugins/linux/ifplugin/descriptor/adapter/interface.go index 1553e75e42..a7d1912daf 100644 --- a/plugins/linux/ifplugin/descriptor/adapter/interface.go +++ b/plugins/linux/ifplugin/descriptor/adapter/interface.go @@ -4,8 +4,8 @@ package adapter import ( "github.com/gogo/protobuf/proto" - . "github.com/ligato/vpp-agent/plugins/kvscheduler/api" "github.com/ligato/vpp-agent/api/models/linux/interfaces" + . "github.com/ligato/vpp-agent/plugins/kvscheduler/api" "github.com/ligato/vpp-agent/plugins/linux/ifplugin/ifaceidx" ) diff --git a/plugins/linux/ifplugin/descriptor/interface.go b/plugins/linux/ifplugin/descriptor/interface.go index 73a30eefc3..592d5142dc 100644 --- a/plugins/linux/ifplugin/descriptor/interface.go +++ b/plugins/linux/ifplugin/descriptor/interface.go @@ -34,6 +34,7 @@ import ( interfaces "github.com/ligato/vpp-agent/api/models/linux/interfaces" namespace "github.com/ligato/vpp-agent/api/models/linux/namespace" + netalloc_api "github.com/ligato/vpp-agent/api/models/netalloc" vpp_intf "github.com/ligato/vpp-agent/api/models/vpp/interfaces" "github.com/ligato/vpp-agent/pkg/models" kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" @@ -43,6 +44,8 @@ import ( "github.com/ligato/vpp-agent/plugins/linux/nsplugin" nsdescriptor "github.com/ligato/vpp-agent/plugins/linux/nsplugin/descriptor" nslinuxcalls "github.com/ligato/vpp-agent/plugins/linux/nsplugin/linuxcalls" + "github.com/ligato/vpp-agent/plugins/netalloc" + netalloc_descr "github.com/ligato/vpp-agent/plugins/netalloc/descriptor" vpp_ifaceidx "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/ifaceidx" ) @@ -127,6 +130,7 @@ type InterfaceDescriptor struct { nsPlugin nsplugin.API vppIfPlugin VPPIfPluginAPI scheduler kvs.KVScheduler + addrAlloc netalloc.AddressAllocator // runtime intfIndex ifaceidx.LinuxIfMetadataIndex @@ -145,8 +149,8 @@ type VPPIfPluginAPI interface { // NewInterfaceDescriptor creates a new instance of the Interface descriptor. func NewInterfaceDescriptor( scheduler kvs.KVScheduler, serviceLabel servicelabel.ReaderAPI, nsPlugin nsplugin.API, - vppIfPlugin VPPIfPluginAPI, ifHandler iflinuxcalls.NetlinkAPI, log logging.PluginLogger, - goRoutinesCnt int) (descr *kvs.KVDescriptor, ctx *InterfaceDescriptor) { + vppIfPlugin VPPIfPluginAPI, addrAlloc netalloc.AddressAllocator, ifHandler iflinuxcalls.NetlinkAPI, + log logging.PluginLogger, goRoutinesCnt int) (descr *kvs.KVDescriptor, ctx *InterfaceDescriptor) { // descriptor context ctx = &InterfaceDescriptor{ @@ -154,30 +158,34 @@ func NewInterfaceDescriptor( ifHandler: ifHandler, nsPlugin: nsPlugin, vppIfPlugin: vppIfPlugin, + addrAlloc: addrAlloc, serviceLabel: serviceLabel, goRoutinesCnt: goRoutinesCnt, log: log.NewLogger("if-descriptor"), } typedDescr := &adapter.InterfaceDescriptor{ - Name: InterfaceDescriptorName, - NBKeyPrefix: interfaces.ModelInterface.KeyPrefix(), - ValueTypeName: interfaces.ModelInterface.ProtoName(), - KeySelector: interfaces.ModelInterface.IsKeyValid, - KeyLabel: interfaces.ModelInterface.StripKeyPrefix, - ValueComparator: ctx.EquivalentInterfaces, - WithMetadata: true, - MetadataMapFactory: ctx.MetadataFactory, - Validate: ctx.Validate, - Create: ctx.Create, - Delete: ctx.Delete, - Update: ctx.Update, - UpdateWithRecreate: ctx.UpdateWithRecreate, - Retrieve: ctx.Retrieve, - IsRetriableFailure: ctx.IsRetriableFailure, - DerivedValues: ctx.DerivedValues, - Dependencies: ctx.Dependencies, - RetrieveDependencies: []string{nsdescriptor.MicroserviceDescriptorName}, + Name: InterfaceDescriptorName, + NBKeyPrefix: interfaces.ModelInterface.KeyPrefix(), + ValueTypeName: interfaces.ModelInterface.ProtoName(), + KeySelector: interfaces.ModelInterface.IsKeyValid, + KeyLabel: interfaces.ModelInterface.StripKeyPrefix, + ValueComparator: ctx.EquivalentInterfaces, + WithMetadata: true, + MetadataMapFactory: ctx.MetadataFactory, + Validate: ctx.Validate, + Create: ctx.Create, + Delete: ctx.Delete, + Update: ctx.Update, + UpdateWithRecreate: ctx.UpdateWithRecreate, + Retrieve: ctx.Retrieve, + IsRetriableFailure: ctx.IsRetriableFailure, + DerivedValues: ctx.DerivedValues, + Dependencies: ctx.Dependencies, + RetrieveDependencies: []string{ + // refresh the pool of allocated IP addresses first + netalloc_descr.IPAllocDescriptorName, + nsdescriptor.MicroserviceDescriptorName}, } descr = adapter.NewInterfaceDescriptor(typedDescr) return @@ -249,14 +257,6 @@ func (d *InterfaceDescriptor) Validate(key string, linuxIf *interfaces.Interface return kvs.NewInvalidValueError(ErrInterfaceWithoutName, "name") } - // validate IP addresses - for _, a := range linuxIf.GetIpAddresses() { - // TODO: perhaps we could assume default mask if there isnt one? - if _, _, err := net.ParseCIDR(a); err != nil { - return kvs.NewInvalidValueError(ErrInvalidIPWithMask, "ip_addresses") - } - } - // validate namespace if ns := linuxIf.GetNamespace(); ns != nil { if ns.GetType() == namespace.NetNamespace_UNDEFINED || ns.GetReference() == "" { @@ -608,7 +608,7 @@ func (d *InterfaceDescriptor) DerivedValues(key string, linuxIf *interfaces.Inte // IP addresses for _, ipAddr := range linuxIf.IpAddresses { derValues = append(derValues, kvs.KeyValuePair{ - Key: interfaces.InterfaceAddressKey(linuxIf.Name, ipAddr), + Key: interfaces.InterfaceAddressKey(linuxIf.Name, ipAddr, netalloc_api.IPAddressSource_STATIC), Value: &prototypes.Empty{}, }) } @@ -779,6 +779,15 @@ func (d *InterfaceDescriptor) Retrieve(correlate []adapter.InterfaceKVWithMetada values = append(values, kv) } + // correlate IP addresses with netalloc references from the expected config + for _, kv := range values { + if expCfg, hasExpCfg := ifCfg[kv.Value.Name]; hasExpCfg { + kv.Value.IpAddresses = d.addrAlloc.CorrelateRetrievedIPs( + expCfg.IpAddresses, kv.Value.IpAddresses, + kv.Value.Name, netalloc_api.IPAddressForm_ADDR_WITH_MASK) + } + } + return values, nil } diff --git a/plugins/linux/ifplugin/descriptor/interface_address.go b/plugins/linux/ifplugin/descriptor/interface_address.go index ba492f21ad..acb9118dc9 100644 --- a/plugins/linux/ifplugin/descriptor/interface_address.go +++ b/plugins/linux/ifplugin/descriptor/interface_address.go @@ -23,11 +23,13 @@ import ( "github.com/pkg/errors" interfaces "github.com/ligato/vpp-agent/api/models/linux/interfaces" + netalloc_api "github.com/ligato/vpp-agent/api/models/netalloc" kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" "github.com/ligato/vpp-agent/plugins/linux/ifplugin/ifaceidx" iflinuxcalls "github.com/ligato/vpp-agent/plugins/linux/ifplugin/linuxcalls" "github.com/ligato/vpp-agent/plugins/linux/nsplugin" nslinuxcalls "github.com/ligato/vpp-agent/plugins/linux/nsplugin/linuxcalls" + "github.com/ligato/vpp-agent/plugins/netalloc" ) const ( @@ -44,16 +46,18 @@ type InterfaceAddressDescriptor struct { log logging.Logger ifHandler iflinuxcalls.NetlinkAPI nsPlugin nsplugin.API + addrAlloc netalloc.AddressAllocator intfIndex ifaceidx.LinuxIfMetadataIndex } // NewInterfaceAddressDescriptor creates a new instance of InterfaceAddressDescriptor. -func NewInterfaceAddressDescriptor(nsPlugin nsplugin.API, ifHandler iflinuxcalls.NetlinkAPI, - log logging.PluginLogger) (descr *kvs.KVDescriptor, ctx *InterfaceAddressDescriptor) { +func NewInterfaceAddressDescriptor(nsPlugin nsplugin.API, addrAlloc netalloc.AddressAllocator, + ifHandler iflinuxcalls.NetlinkAPI, log logging.PluginLogger) (descr *kvs.KVDescriptor, ctx *InterfaceAddressDescriptor) { ctx = &InterfaceAddressDescriptor{ ifHandler: ifHandler, nsPlugin: nsPlugin, + addrAlloc: addrAlloc, log: log.NewLogger("interface-address-descriptor"), } descr = &kvs.KVDescriptor{ @@ -62,6 +66,7 @@ func NewInterfaceAddressDescriptor(nsPlugin nsplugin.API, ifHandler iflinuxcalls Validate: ctx.Validate, Create: ctx.Create, Delete: ctx.Delete, + Dependencies: ctx.Dependencies, } return } @@ -73,25 +78,27 @@ func (d *InterfaceAddressDescriptor) SetInterfaceIndex(intfIndex ifaceidx.LinuxI } // IsInterfaceVrfKey returns true if the key represents assignment of an IP address -// to a Linux interface. +// to a Linux interface (that needs to be applied). KVs representing addresses +// already allocated from netalloc plugin are excluded. func (d *InterfaceAddressDescriptor) IsInterfaceAddressKey(key string) bool { - _, _, _, _, isAddrKey := interfaces.ParseInterfaceAddressKey(key) - return isAddrKey + _, _, source, _, isAddrKey := interfaces.ParseInterfaceAddressKey(key) + return isAddrKey && + (source == netalloc_api.IPAddressSource_STATIC || source == netalloc_api.IPAddressSource_ALLOC_REF) } // Validate validates IP address to be assigned to an interface. func (d *InterfaceAddressDescriptor) Validate(key string, emptyVal proto.Message) (err error) { - _, _, _, invalidIP, _ := interfaces.ParseInterfaceAddressKey(key) - if invalidIP { - return errors.New("invalid IP address") + iface, addr, _, invalidKey, _ := interfaces.ParseInterfaceAddressKey(key) + if invalidKey { + return errors.New("invalid key") } - return nil + + return d.addrAlloc.ValidateIPAddress(addr, iface, "ip_addresses", netalloc.GwRefUnexpected) } // Create assigns IP address to an interface. func (d *InterfaceAddressDescriptor) Create(key string, emptyVal proto.Message) (metadata kvs.Metadata, err error) { - iface, ipAddr, ipAddrNet, _, _ := interfaces.ParseInterfaceAddressKey(key) - ipAddrNet.IP = ipAddr + iface, addr, _, _, _ := interfaces.ParseInterfaceAddressKey(key) ifMeta, found := d.intfIndex.LookupByName(iface) if !found { @@ -100,6 +107,12 @@ func (d *InterfaceAddressDescriptor) Create(key string, emptyVal proto.Message) return nil, err } + ipAddr, err := d.addrAlloc.GetOrParseIPAddress(addr, iface, netalloc_api.IPAddressForm_ADDR_WITH_MASK) + if err != nil { + d.log.Error(err) + return nil, err + } + // switch to the namespace with the interface nsCtx := nslinuxcalls.NewNamespaceMgmtCtx() revert, err := d.nsPlugin.SwitchToNamespace(nsCtx, ifMeta.Namespace) @@ -109,29 +122,31 @@ func (d *InterfaceAddressDescriptor) Create(key string, emptyVal proto.Message) } defer revert() - // Enabled IPv6 for loopback "lo" and the interface being configured - for _, iface := range [2]string{"lo", ifMeta.HostIfName} { - ipv6SysctlValueName := fmt.Sprintf(DisableIPv6SysctlTemplate, iface) + if ipAddr.IP.To4() == nil { + // Enable IPv6 for loopback "lo" and the interface being configured + for _, iface := range [2]string{"lo", ifMeta.HostIfName} { + ipv6SysctlValueName := fmt.Sprintf(DisableIPv6SysctlTemplate, iface) + + // Read current sysctl value + value, err := getSysctl(ipv6SysctlValueName) + if err != nil || value == "0" { + if err != nil { + d.log.Warnf("could not read sysctl value for %v: %v", + ifMeta.HostIfName, err) + } + continue + } - // Read current sysctl value - value, err := getSysctl(ipv6SysctlValueName) - if err != nil || value == "0" { + // Write sysctl to enable IPv6 + _, err = setSysctl(ipv6SysctlValueName, "0") if err != nil { - d.log.Warnf("could not read sysctl value for %v: %v", - ifMeta.HostIfName, err) + return nil, fmt.Errorf("failed to enable IPv6 (%s=%s): %v", + ipv6SysctlValueName, value, err) } - continue - } - - // Write sysctl to enable IPv6 - _, err = setSysctl(ipv6SysctlValueName, "0") - if err != nil { - return nil, fmt.Errorf("failed to enable IPv6 (%s=%s): %v", - ipv6SysctlValueName, value, err) } } - err = d.ifHandler.AddInterfaceIP(ifMeta.HostIfName, ipAddrNet) + err = d.ifHandler.AddInterfaceIP(ifMeta.HostIfName, ipAddr) // an attempt to add already assigned IP is not considered as error if err == syscall.EEXIST { @@ -142,8 +157,7 @@ func (d *InterfaceAddressDescriptor) Create(key string, emptyVal proto.Message) // Delete unassigns IP address from an interface. func (d *InterfaceAddressDescriptor) Delete(key string, emptyVal proto.Message, metadata kvs.Metadata) (err error) { - iface, ipAddr, ipAddrNet, _, _ := interfaces.ParseInterfaceAddressKey(key) - ipAddrNet.IP = ipAddr + iface, addr, _, _, _ := interfaces.ParseInterfaceAddressKey(key) ifMeta, found := d.intfIndex.LookupByName(iface) if !found { @@ -152,6 +166,12 @@ func (d *InterfaceAddressDescriptor) Delete(key string, emptyVal proto.Message, return err } + ipAddr, err := d.addrAlloc.GetOrParseIPAddress(addr, iface, netalloc_api.IPAddressForm_ADDR_WITH_MASK) + if err != nil { + d.log.Error(err) + return err + } + // switch to the namespace with the interface nsCtx := nslinuxcalls.NewNamespaceMgmtCtx() revert, err := d.nsPlugin.SwitchToNamespace(nsCtx, ifMeta.Namespace) @@ -161,6 +181,17 @@ func (d *InterfaceAddressDescriptor) Delete(key string, emptyVal proto.Message, } defer revert() - err = d.ifHandler.DelInterfaceIP(ifMeta.HostIfName, ipAddrNet) + err = d.ifHandler.DelInterfaceIP(ifMeta.HostIfName, ipAddr) return err } + +// Dependencies mentions potential allocation of the IP address as dependency. +func (d *InterfaceAddressDescriptor) Dependencies(key string, emptyVal proto.Message) (deps []kvs.Dependency) { + iface, addr, _, _, _ := interfaces.ParseInterfaceAddressKey(key) + allocDep, hasAllocDep := d.addrAlloc.GetAddressAllocDep(addr, iface, "") + if hasAllocDep { + deps = append(deps, allocDep) + } + + return deps +} diff --git a/plugins/linux/ifplugin/ifaceidx/ifaceidx.go b/plugins/linux/ifplugin/ifaceidx/ifaceidx.go index 80f702c94f..0a055c4768 100644 --- a/plugins/linux/ifplugin/ifaceidx/ifaceidx.go +++ b/plugins/linux/ifplugin/ifaceidx/ifaceidx.go @@ -16,6 +16,7 @@ package ifaceidx import ( "time" + "github.com/gogo/protobuf/proto" "github.com/ligato/cn-infra/idxmap" "github.com/ligato/cn-infra/idxmap/mem" @@ -39,6 +40,13 @@ type LinuxIfMetadataIndex interface { // and both set to empty values. LookupByVPPTap(vppTapName string) (name string, metadata *LinuxIfMetadata, exists bool) + // LookupByHostName retrieves a previously configured Linux interface + // by the host interface name inside the given namespace. + // If there is no such interface, is returned as *false* with + // and both set to empty values. + LookupByHostName(hostname string, ns *linux_namespace.NetNamespace) (name string, + metadata *LinuxIfMetadata, exists bool) + // ListAllInterfaces returns slice of names of all interfaces in the mapping. ListAllInterfaces() (names []string) @@ -80,6 +88,10 @@ const ( // tapVPPNameIndexKey is used as a secondary key used to search TAP_TO_VPP // interface by the logical name of the VPP-side of the TAP. tapVPPNameIndexKey = "tap-vpp-name" + + // hostNameIndexKey is used as a secondary key used to search Linux + // interfaces by the host interface name. + hostNameIndexKey = "host-iface-name" ) // NewLinuxIfIndex creates a new instance implementing LinuxIfMetadataIndexRW. @@ -121,6 +133,27 @@ func (ifmx *linuxIfMetadataIndex) LookupByVPPTap(vppTapName string) (name string return } +// LookupByHostName retrieves a previously configured Linux interface +// by the host interface name inside the given namespace. +// If there is no such interface, is returned as *false* with +// and both set to empty values. +func (ifmx *linuxIfMetadataIndex) LookupByHostName(hostname string, ns *linux_namespace.NetNamespace) ( + name string, metadata *LinuxIfMetadata, exists bool) { + + res := ifmx.ListNames(hostNameIndexKey, hostname) + for _, iface := range res { + untypedMeta, found := ifmx.GetValue(iface) + if found { + if ifMeta, ok := untypedMeta.(*LinuxIfMetadata); ok { + if proto.Equal(ns, ifMeta.Namespace) { + return iface, ifMeta, true + } + } + } + } + return +} + // ListAllInterfaces returns slice of names of all interfaces in the mapping. func (ifmx *linuxIfMetadataIndex) ListAllInterfaces() (names []string) { return ifmx.ListAllNames() @@ -159,5 +192,7 @@ func indexMetadata(metaData interface{}) map[string][]string { if ifMeta.VPPTapName != "" { indexes[tapVPPNameIndexKey] = []string{ifMeta.VPPTapName} } + indexes[hostNameIndexKey] = []string{ifMeta.HostIfName} + return indexes } diff --git a/plugins/linux/ifplugin/ifplugin.go b/plugins/linux/ifplugin/ifplugin.go index 56e849c64a..0fcecf880b 100644 --- a/plugins/linux/ifplugin/ifplugin.go +++ b/plugins/linux/ifplugin/ifplugin.go @@ -27,6 +27,7 @@ import ( "github.com/ligato/vpp-agent/plugins/linux/ifplugin/ifaceidx" "github.com/ligato/vpp-agent/plugins/linux/ifplugin/linuxcalls" "github.com/ligato/vpp-agent/plugins/linux/nsplugin" + "github.com/ligato/vpp-agent/plugins/netalloc" ) const ( @@ -60,6 +61,7 @@ type Deps struct { ServiceLabel servicelabel.ReaderAPI KVScheduler kvs.KVScheduler NsPlugin nsplugin.API + AddrAlloc netalloc.AddressAllocator VppIfPlugin descriptor.VPPIfPluginAPI /* mandatory if TAP_TO_VPP interfaces are used */ } @@ -90,7 +92,7 @@ func (p *IfPlugin) Init() error { // init & register descriptors var ifDescriptor *kvs.KVDescriptor ifDescriptor, p.ifDescriptor = descriptor.NewInterfaceDescriptor(p.KVScheduler, - p.ServiceLabel, p.NsPlugin, p.VppIfPlugin, p.ifHandler, p.Log, config.GoRoutinesCnt) + p.ServiceLabel, p.NsPlugin, p.VppIfPlugin, p.AddrAlloc, p.ifHandler, p.Log, config.GoRoutinesCnt) err = p.Deps.KVScheduler.RegisterKVDescriptor(ifDescriptor) if err != nil { return err @@ -98,7 +100,7 @@ func (p *IfPlugin) Init() error { var addrDescriptor *kvs.KVDescriptor addrDescriptor, p.ifAddrDescriptor = descriptor.NewInterfaceAddressDescriptor(p.NsPlugin, - p.ifHandler, p.Log) + p.AddrAlloc, p.ifHandler, p.Log) err = p.Deps.KVScheduler.RegisterKVDescriptor(addrDescriptor) if err != nil { return err diff --git a/plugins/linux/ifplugin/options.go b/plugins/linux/ifplugin/options.go index 2f52c41fc1..b27f4a20ba 100644 --- a/plugins/linux/ifplugin/options.go +++ b/plugins/linux/ifplugin/options.go @@ -6,6 +6,7 @@ import ( "github.com/ligato/cn-infra/servicelabel" "github.com/ligato/vpp-agent/plugins/kvscheduler" "github.com/ligato/vpp-agent/plugins/linux/nsplugin" + "github.com/ligato/vpp-agent/plugins/netalloc" ) // DefaultPlugin is a default instance of IfPlugin. @@ -18,6 +19,7 @@ func NewPlugin(opts ...Option) *IfPlugin { p.PluginName = "linux-ifplugin" p.KVScheduler = &kvscheduler.DefaultPlugin p.NsPlugin = &nsplugin.DefaultPlugin + p.AddrAlloc = &netalloc.DefaultPlugin p.ServiceLabel = &servicelabel.DefaultPlugin for _, o := range opts { diff --git a/plugins/linux/l3plugin/descriptor/arp.go b/plugins/linux/l3plugin/descriptor/arp.go index 307a17791c..4e355d7634 100644 --- a/plugins/linux/l3plugin/descriptor/arp.go +++ b/plugins/linux/l3plugin/descriptor/arp.go @@ -26,12 +26,15 @@ import ( ifmodel "github.com/ligato/vpp-agent/api/models/linux/interfaces" l3 "github.com/ligato/vpp-agent/api/models/linux/l3" + netalloc_api "github.com/ligato/vpp-agent/api/models/netalloc" "github.com/ligato/vpp-agent/plugins/linux/ifplugin" ifdescriptor "github.com/ligato/vpp-agent/plugins/linux/ifplugin/descriptor" "github.com/ligato/vpp-agent/plugins/linux/l3plugin/descriptor/adapter" l3linuxcalls "github.com/ligato/vpp-agent/plugins/linux/l3plugin/linuxcalls" "github.com/ligato/vpp-agent/plugins/linux/nsplugin" nslinuxcalls "github.com/ligato/vpp-agent/plugins/linux/nsplugin/linuxcalls" + "github.com/ligato/vpp-agent/plugins/netalloc" + netalloc_descr "github.com/ligato/vpp-agent/plugins/netalloc/descriptor" ) const ( @@ -53,9 +56,6 @@ var ( // interface reference. ErrARPWithoutInterface = errors.New("Linux ARP entry defined without interface reference") - // ErrARPWithoutIP is returned when Linux ARP configuration is missing IP address. - ErrARPWithoutIP = errors.New("Linux ARP entry defined without IP address") - // ErrARPWithInvalidIP is returned when Linux ARP configuration contains IP address that cannot be parsed. ErrARPWithInvalidIP = errors.New("Linux ARP entry defined with invalid IP address") @@ -73,6 +73,7 @@ type ARPDescriptor struct { l3Handler l3linuxcalls.NetlinkAPI ifPlugin ifplugin.API nsPlugin nsplugin.API + addrAlloc netalloc.AddressAllocator scheduler kvs.KVScheduler // parallelization of the Retrieve operation @@ -81,53 +82,45 @@ type ARPDescriptor struct { // NewARPDescriptor creates a new instance of the ARP descriptor. func NewARPDescriptor( - scheduler kvs.KVScheduler, ifPlugin ifplugin.API, nsPlugin nsplugin.API, - l3Handler l3linuxcalls.NetlinkAPI, log logging.PluginLogger, goRoutinesCnt int) *ARPDescriptor { + scheduler kvs.KVScheduler, ifPlugin ifplugin.API, nsPlugin nsplugin.API, addrAlloc netalloc.AddressAllocator, + l3Handler l3linuxcalls.NetlinkAPI, log logging.PluginLogger, goRoutinesCnt int) *kvs.KVDescriptor { - return &ARPDescriptor{ + ctx := &ARPDescriptor{ scheduler: scheduler, l3Handler: l3Handler, ifPlugin: ifPlugin, nsPlugin: nsPlugin, + addrAlloc: addrAlloc, goRoutinesCnt: goRoutinesCnt, log: log.NewLogger("arp-descriptor"), } -} -// GetDescriptor returns descriptor suitable for registration (via adapter) with -// the KVScheduler. -func (d *ARPDescriptor) GetDescriptor() *adapter.ARPDescriptor { - return &adapter.ARPDescriptor{ + typedDescr := &adapter.ARPDescriptor{ Name: ARPDescriptorName, NBKeyPrefix: l3.ModelARPEntry.KeyPrefix(), ValueTypeName: l3.ModelARPEntry.ProtoName(), KeySelector: l3.ModelARPEntry.IsKeyValid, KeyLabel: l3.ModelARPEntry.StripKeyPrefix, - ValueComparator: d.EquivalentARPs, - Validate: d.Validate, - Create: d.Create, - Delete: d.Delete, - Update: d.Update, - Retrieve: d.Retrieve, - Dependencies: d.Dependencies, - RetrieveDependencies: []string{ifdescriptor.InterfaceDescriptorName}, + ValueComparator: ctx.EquivalentARPs, + Validate: ctx.Validate, + Create: ctx.Create, + Delete: ctx.Delete, + Update: ctx.Update, + Retrieve: ctx.Retrieve, + Dependencies: ctx.Dependencies, + RetrieveDependencies: []string{ + netalloc_descr.IPAllocDescriptorName, + ifdescriptor.InterfaceDescriptorName}, } + return adapter.NewARPDescriptor(typedDescr) } // EquivalentARPs is case-insensitive comparison function for l3.LinuxARPEntry. +// Only MAC addresses are compared - interface and IP address are part of the key +// which is already given to be the same for the two values. func (d *ARPDescriptor) EquivalentARPs(key string, oldArp, NewArp *l3.ARPEntry) bool { - // interfaces compared as usually: - if oldArp.Interface != NewArp.Interface { - return false - } - // compare MAC addresses case-insensitively - if strings.ToLower(oldArp.HwAddress) != strings.ToLower(NewArp.HwAddress) { - return false - } - - // compare IP addresses converted to net.IPNet - return equalAddrs(oldArp.IpAddress, NewArp.IpAddress) + return strings.ToLower(oldArp.HwAddress) == strings.ToLower(NewArp.HwAddress) } // Validate validates ARP entry configuration. @@ -135,13 +128,10 @@ func (d *ARPDescriptor) Validate(key string, arp *l3.ARPEntry) (err error) { if arp.Interface == "" { return kvs.NewInvalidValueError(ErrARPWithoutInterface, "interface") } - if arp.IpAddress == "" { - return kvs.NewInvalidValueError(ErrARPWithoutIP, "ip_address") - } if arp.HwAddress == "" { return kvs.NewInvalidValueError(ErrARPWithoutHwAddr, "hw_address") } - return nil + return d.addrAlloc.ValidateIPAddress(arp.IpAddress, "", "ip_address", netalloc.GWRefAllowed) } // Create creates ARP entry. @@ -180,13 +170,13 @@ func (d *ARPDescriptor) updateARPEntry(arp *l3.ARPEntry, actionName string, acti neigh.LinkIndex = ifMeta.LinuxIfIndex // set IP address - ipAddr := net.ParseIP(arp.IpAddress) - if ipAddr == nil { - err = ErrARPWithInvalidIP + ipAddr, err := d.addrAlloc.GetOrParseIPAddress(arp.IpAddress, "", + netalloc_api.IPAddressForm_ADDR_ONLY) + if err != nil { d.log.Error(err) return err } - neigh.IP = ipAddr + neigh.IP = ipAddr.IP // set MAC address mac, err := net.ParseMAC(arp.HwAddress) @@ -229,11 +219,11 @@ func (d *ARPDescriptor) updateARPEntry(arp *l3.ARPEntry, actionName string, acti } // Dependencies lists dependencies for a Linux ARP entry. -func (d *ARPDescriptor) Dependencies(key string, arp *l3.ARPEntry) []kvs.Dependency { +func (d *ARPDescriptor) Dependencies(key string, arp *l3.ARPEntry) (deps []kvs.Dependency) { // the associated interface must exist, but also must be UP and have at least // one IP address assigned (to be in the L3 mode) if arp.Interface != "" { - return []kvs.Dependency{ + deps = []kvs.Dependency{ { Label: arpInterfaceDep, Key: ifmodel.InterfaceStateKey(arp.Interface, true), @@ -246,7 +236,12 @@ func (d *ARPDescriptor) Dependencies(key string, arp *l3.ARPEntry) []kvs.Depende }, } } - return nil + // if IP is only a symlink to netalloc address pool, then wait for it to be allocated first + allocDep, hasAllocDep := d.addrAlloc.GetAddressAllocDep(arp.IpAddress, "", "") + if hasAllocDep { + deps = append(deps, allocDep) + } + return deps } // retrievedARPs is used as the return value sent via channel by retrieveARPs(). @@ -258,6 +253,15 @@ type retrievedARPs struct { // Retrieve returns all ARP entries associated with interfaces managed by this agent. func (d *ARPDescriptor) Retrieve(correlate []adapter.ARPKVWithMetadata) ([]adapter.ARPKVWithMetadata, error) { var values []adapter.ARPKVWithMetadata + + hwLabel := func(arp *l3.ARPEntry) string { + return arp.Interface + "/" + strings.ToLower(arp.HwAddress) + } + expCfg := make(map[string]*l3.ARPEntry) // Interface+MAC -> expected ARP config + for _, kv := range correlate { + expCfg[hwLabel(kv.Value)] = kv.Value + } + interfaces := d.ifPlugin.GetInterfaceIndex().ListAllInterfaces() goRoutinesCnt := len(interfaces) / minWorkForGoRoutine if goRoutinesCnt == 0 { @@ -283,7 +287,17 @@ func (d *ARPDescriptor) Retrieve(correlate []adapter.ARPKVWithMetadata) ([]adapt if retrieved.err != nil { return values, retrieved.err } - values = append(values, retrieved.arps...) + // correlate IP addresses with netalloc references (if any) from the expected config + for _, arp := range retrieved.arps { + if expCfg, hasExpCfg := expCfg[hwLabel(arp.Value)]; hasExpCfg { + arp.Value.IpAddress = d.addrAlloc.CorrelateRetrievedIPs( + []string{expCfg.IpAddress}, []string{arp.Value.IpAddress}, + "", netalloc_api.IPAddressForm_ADDR_ONLY)[0] + // recreate key in case the IP address was replaced with a netalloc link + arp.Key = l3.ArpKey(arp.Value.Interface, arp.Value.IpAddress) + } + values = append(values, arp) + } } return values, nil diff --git a/plugins/linux/l3plugin/descriptor/route.go b/plugins/linux/l3plugin/descriptor/route.go index f5ef5cb4b3..ed29b28105 100644 --- a/plugins/linux/l3plugin/descriptor/route.go +++ b/plugins/linux/l3plugin/descriptor/route.go @@ -19,6 +19,7 @@ import ( "net" "strings" + "github.com/gogo/protobuf/proto" prototypes "github.com/gogo/protobuf/types" "github.com/pkg/errors" "github.com/vishvananda/netlink" @@ -28,6 +29,8 @@ import ( ifmodel "github.com/ligato/vpp-agent/api/models/linux/interfaces" "github.com/ligato/vpp-agent/api/models/linux/l3" + netalloc_api "github.com/ligato/vpp-agent/api/models/netalloc" + "github.com/ligato/vpp-agent/pkg/models" kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" "github.com/ligato/vpp-agent/plugins/linux/ifplugin" ifdescriptor "github.com/ligato/vpp-agent/plugins/linux/ifplugin/descriptor" @@ -35,6 +38,8 @@ import ( l3linuxcalls "github.com/ligato/vpp-agent/plugins/linux/l3plugin/linuxcalls" "github.com/ligato/vpp-agent/plugins/linux/nsplugin" nslinuxcalls "github.com/ligato/vpp-agent/plugins/linux/nsplugin/linuxcalls" + "github.com/ligato/vpp-agent/plugins/netalloc" + netalloc_descr "github.com/ligato/vpp-agent/plugins/netalloc/descriptor" ) const ( @@ -49,6 +54,7 @@ const ( routeOutInterfaceDep = "outgoing-interface-is-up" routeOutInterfaceIPAddrDep = "outgoing-interface-has-ip-address" routeGwReachabilityDep = "gw-reachable" + allocatedAddrAttached = "allocated-addr-attached" ) // A list of non-retriable errors: @@ -57,20 +63,9 @@ var ( // outgoing interface reference. ErrRouteWithoutInterface = errors.New("Linux Route defined without outgoing interface reference") - // ErrRouteWithoutDestination is returned when Linux Route configuration is missing destination network. - ErrRouteWithoutDestination = errors.New("Linux Route defined without destination network") - // ErrRouteWithUndefinedScope is returned when Linux Route is configured without scope. ErrRouteWithUndefinedScope = errors.New("Linux Route defined without scope") - // ErrRouteWithInvalidDst is returned when Linux Route configuration contains destination - // network that cannot be parsed. - ErrRouteWithInvalidDst = errors.New("Linux Route defined with invalid destination network") - - // ErrRouteWithInvalidGW is returned when Linux Route configuration contains gateway - // address that cannot be parsed. - ErrRouteWithInvalidGw = errors.New("Linux Route defined with invalid GW address") - // ErrRouteLinkWithGw is returned when link-local Linux route has gateway address // specified - it shouldn't be since destination is already neighbour by definition. ErrRouteLinkWithGw = errors.New("Link-local Linux Route was defined with non-empty GW address") @@ -82,6 +77,7 @@ type RouteDescriptor struct { l3Handler l3linuxcalls.NetlinkAPI ifPlugin ifplugin.API nsPlugin nsplugin.API + addrAlloc netalloc.AddressAllocator scheduler kvs.KVScheduler // parallelization of the Retrieve operation @@ -90,38 +86,37 @@ type RouteDescriptor struct { // NewRouteDescriptor creates a new instance of the Route descriptor. func NewRouteDescriptor( - scheduler kvs.KVScheduler, ifPlugin ifplugin.API, nsPlugin nsplugin.API, - l3Handler l3linuxcalls.NetlinkAPI, log logging.PluginLogger, goRoutinesCnt int) *RouteDescriptor { + scheduler kvs.KVScheduler, ifPlugin ifplugin.API, nsPlugin nsplugin.API, addrAlloc netalloc.AddressAllocator, + l3Handler l3linuxcalls.NetlinkAPI, log logging.PluginLogger, goRoutinesCnt int) *kvs.KVDescriptor { - return &RouteDescriptor{ + ctx := &RouteDescriptor{ scheduler: scheduler, l3Handler: l3Handler, ifPlugin: ifPlugin, nsPlugin: nsPlugin, + addrAlloc: addrAlloc, goRoutinesCnt: goRoutinesCnt, log: log.NewLogger("route-descriptor"), } -} - -// GetDescriptor returns descriptor suitable for registration (via adapter) with -// the KVScheduler. -func (d *RouteDescriptor) GetDescriptor() *adapter.RouteDescriptor { - return &adapter.RouteDescriptor{ - Name: RouteDescriptorName, - NBKeyPrefix: linux_l3.ModelRoute.KeyPrefix(), - ValueTypeName: linux_l3.ModelRoute.ProtoName(), - KeySelector: linux_l3.ModelRoute.IsKeyValid, - KeyLabel: linux_l3.ModelRoute.StripKeyPrefix, - ValueComparator: d.EquivalentRoutes, - Validate: d.Validate, - Create: d.Create, - Delete: d.Delete, - Update: d.Update, - Retrieve: d.Retrieve, - DerivedValues: d.DerivedValues, - Dependencies: d.Dependencies, - RetrieveDependencies: []string{ifdescriptor.InterfaceDescriptorName}, + typedDescr := &adapter.RouteDescriptor{ + Name: RouteDescriptorName, + NBKeyPrefix: linux_l3.ModelRoute.KeyPrefix(), + ValueTypeName: linux_l3.ModelRoute.ProtoName(), + KeySelector: linux_l3.ModelRoute.IsKeyValid, + KeyLabel: linux_l3.ModelRoute.StripKeyPrefix, + ValueComparator: ctx.EquivalentRoutes, + Validate: ctx.Validate, + Create: ctx.Create, + Delete: ctx.Delete, + Update: ctx.Update, + Retrieve: ctx.Retrieve, + DerivedValues: ctx.DerivedValues, + Dependencies: ctx.Dependencies, + RetrieveDependencies: []string{ + netalloc_descr.IPAllocDescriptorName, + ifdescriptor.InterfaceDescriptorName}, } + return adapter.NewRouteDescriptor(typedDescr) } // EquivalentRoutes is case-insensitive comparison function for l3.LinuxRoute. @@ -145,13 +140,16 @@ func (d *RouteDescriptor) Validate(key string, route *linux_l3.Route) (err error if route.OutgoingInterface == "" { return kvs.NewInvalidValueError(ErrRouteWithoutInterface, "outgoing_interface") } - if route.DstNetwork == "" { - return kvs.NewInvalidValueError(ErrRouteWithoutDestination, "dst_network") - } if route.Scope == linux_l3.Route_LINK && route.GwAddr != "" { return kvs.NewInvalidValueError(ErrRouteLinkWithGw, "scope", "gw_addr") } - return nil + err = d.addrAlloc.ValidateIPAddress(route.DstNetwork, "", "dst_network", + netalloc.GWRefAllowed) + if err != nil { + return err + } + return d.addrAlloc.ValidateIPAddress(getGwAddr(route), route.OutgoingInterface, + "gw_addr", netalloc.GWRefRequired) } // Create adds Linux route. @@ -190,9 +188,9 @@ func (d *RouteDescriptor) updateRoute(route *linux_l3.Route, actionName string, netlinkRoute.LinkIndex = ifMeta.LinuxIfIndex // set destination network - _, dstNet, err := net.ParseCIDR(route.DstNetwork) + dstNet, err := d.addrAlloc.GetOrParseIPAddress(route.DstNetwork, "", + netalloc_api.IPAddressForm_ADDR_NET) if err != nil { - err = ErrRouteWithInvalidDst d.log.Error(err) return err } @@ -200,13 +198,13 @@ func (d *RouteDescriptor) updateRoute(route *linux_l3.Route, actionName string, // set gateway address if route.GwAddr != "" { - gwAddr := net.ParseIP(route.GwAddr) - if gwAddr == nil { - err = ErrRouteWithInvalidGw + gwAddr, err := d.addrAlloc.GetOrParseIPAddress(route.GwAddr, route.OutgoingInterface, + netalloc_api.IPAddressForm_ADDR_ONLY) + if err != nil { d.log.Error(err) return err } - netlinkRoute.Gw = gwAddr + netlinkRoute.Gw = gwAddr.IP } // set route scope @@ -251,9 +249,41 @@ func (d *RouteDescriptor) Dependencies(key string, route *linux_l3.Route) []kvs. Key: ifmodel.InterfaceStateKey(route.OutgoingInterface, true), }) } + // if destination network is netalloc reference, then the address must be allocated first + allocDep, hasAllocDep := d.addrAlloc.GetAddressAllocDep(route.DstNetwork, "", + "dst_network-") + if hasAllocDep { + dependencies = append(dependencies, allocDep) + } + // if GW is netalloc reference, then the address must be allocated first + allocDep, hasAllocDep = d.addrAlloc.GetAddressAllocDep(route.GwAddr, route.OutgoingInterface, + "gw_addr-") + if hasAllocDep { + dependencies = append(dependencies, allocDep) + } // GW must be routable - gwAddr := net.ParseIP(getGwAddr(route)) - if gwAddr != nil && !gwAddr.IsUnspecified() { + network, iface, _, isRef, _ := d.addrAlloc.ParseAddressAllocRef(route.GwAddr, route.OutgoingInterface) + if isRef { + // GW is netalloc reference + dependencies = append(dependencies, kvs.Dependency{ + Label: routeGwReachabilityDep, + AnyOf: kvs.AnyOfDependency{ + KeyPrefixes: []string{ + netalloc_api.NeighGwKey(network, iface), + linux_l3.StaticLinkLocalRouteKey( + d.addrAlloc.CreateAddressAllocRef(network, iface, true), + route.OutgoingInterface), + }, + }, + }) + dependencies = append(dependencies, kvs.Dependency{ + Label: allocatedAddrAttached, + Key: ifmodel.InterfaceAddressKey( + route.OutgoingInterface, d.addrAlloc.CreateAddressAllocRef(network, "", false), + netalloc_api.IPAddressSource_ALLOC_REF), + }) + } else if gwAddr := net.ParseIP(getGwAddr(route)); gwAddr != nil && !gwAddr.IsUnspecified() { + // GW is not netalloc reference but an actual IP dependencies = append(dependencies, kvs.Dependency{ Label: routeGwReachabilityDep, AnyOf: kvs.AnyOfDependency{ @@ -263,15 +293,20 @@ func (d *RouteDescriptor) Dependencies(key string, route *linux_l3.Route) []kvs. }, KeySelector: func(key string) bool { dstAddr, ifName, isRouteKey := linux_l3.ParseStaticLinkLocalRouteKey(key) - if isRouteKey && ifName == route.OutgoingInterface && dstAddr.Contains(gwAddr) { - // GW address is neighbour as told by another link-local route - return true + if isRouteKey && ifName == route.OutgoingInterface { + if _, dstNet, err := net.ParseCIDR(dstAddr); err == nil && dstNet.Contains(gwAddr) { + // GW address is neighbour as told by another link-local route + return true + } + return false } - ifName, _, network, invalidIP, isAddrKey := ifmodel.ParseInterfaceAddressKey(key) - if isAddrKey && !invalidIP && ifName == route.OutgoingInterface && network.Contains(gwAddr) { - // GW address is inside the local network of the outgoing interface - // as given by the assigned IP address - return true + ifName, address, source, _, isAddrKey := ifmodel.ParseInterfaceAddressKey(key) + if isAddrKey && source != netalloc_api.IPAddressSource_ALLOC_REF { + if _, network, err := net.ParseCIDR(address); err == nil && network.Contains(gwAddr) { + // GW address is inside the local network of the outgoing interface + // as given by the assigned IP address + return true + } } return false }, @@ -314,6 +349,31 @@ type retrievedRoutes struct { // Retrieve returns all routes associated with interfaces managed by this agent. func (d *RouteDescriptor) Retrieve(correlate []adapter.RouteKVWithMetadata) ([]adapter.RouteKVWithMetadata, error) { var values []adapter.RouteKVWithMetadata + + // prepare expected configuration with de-referenced netalloc links + nbCfg := make(map[string]*linux_l3.Route) + expCfg := make(map[string]*linux_l3.Route) + for _, kv := range correlate { + dstNetwork := kv.Value.DstNetwork + parsed, err := d.addrAlloc.GetOrParseIPAddress(kv.Value.DstNetwork, + "", netalloc_api.IPAddressForm_ADDR_NET) + if err == nil { + dstNetwork = parsed.String() + } + gwAddr := kv.Value.GwAddr + parsed, err = d.addrAlloc.GetOrParseIPAddress(getGwAddr(kv.Value), + kv.Value.OutgoingInterface, netalloc_api.IPAddressForm_ADDR_ONLY) + if err == nil { + gwAddr = parsed.IP.String() + } + route := proto.Clone(kv.Value).(*linux_l3.Route) + route.DstNetwork = dstNetwork + route.GwAddr = gwAddr + key := models.Key(route) + expCfg[key] = route + nbCfg[key] = kv.Value + } + interfaces := d.ifPlugin.GetInterfaceIndex().ListAllInterfaces() goRoutinesCnt := len(interfaces) / minWorkForGoRoutine if goRoutinesCnt == 0 { @@ -339,7 +399,18 @@ func (d *RouteDescriptor) Retrieve(correlate []adapter.RouteKVWithMetadata) ([]a if retrieved.err != nil { return values, retrieved.err } - values = append(values, retrieved.routes...) + // correlate with the expected configuration + for _, route := range retrieved.routes { + key := linux_l3.RouteKey(route.Value.DstNetwork, route.Value.OutgoingInterface) + if expCfg, hasExpCfg := expCfg[key]; hasExpCfg { + if d.EquivalentRoutes(key, route.Value, expCfg) { + route.Value = nbCfg[key] + // recreate the key in case the dest. IP was replaced with netalloc link + route.Key = models.Key(route.Value) + } + } + values = append(values, route) + } } return values, nil @@ -457,6 +528,9 @@ func rtScopeFromNetlinkToNB(scope netlink.Scope) (linux_l3.Route_Scope, error) { // equalAddrs compares two IP addresses for equality. func equalAddrs(addr1, addr2 string) bool { + if strings.HasPrefix(addr1, netalloc_api.AllocRefPrefix) { + return addr1 == addr2 + } a1 := net.ParseIP(addr1) a2 := net.ParseIP(addr2) if a1 == nil || a2 == nil { @@ -468,6 +542,9 @@ func equalAddrs(addr1, addr2 string) bool { // equalNetworks compares two IP networks for equality. func equalNetworks(net1, net2 string) bool { + if strings.HasPrefix(net1, netalloc_api.AllocRefPrefix) { + return net1 == net2 + } _, n1, err1 := net.ParseCIDR(net1) _, n2, err2 := net.ParseCIDR(net2) if err1 != nil || err2 != nil { diff --git a/plugins/linux/l3plugin/l3plugin.go b/plugins/linux/l3plugin/l3plugin.go index dbcbf4983c..14627570c9 100644 --- a/plugins/linux/l3plugin/l3plugin.go +++ b/plugins/linux/l3plugin/l3plugin.go @@ -23,9 +23,9 @@ import ( "github.com/ligato/vpp-agent/plugins/linux/ifplugin" "github.com/ligato/vpp-agent/plugins/linux/l3plugin/descriptor" - "github.com/ligato/vpp-agent/plugins/linux/l3plugin/descriptor/adapter" "github.com/ligato/vpp-agent/plugins/linux/l3plugin/linuxcalls" "github.com/ligato/vpp-agent/plugins/linux/nsplugin" + "github.com/ligato/vpp-agent/plugins/netalloc" ) const ( @@ -55,6 +55,7 @@ type Deps struct { KVScheduler kvs.KVScheduler NsPlugin nsplugin.API IfPlugin ifplugin.API + AddrAlloc netalloc.AddressAllocator } // Config holds the l3plugin configuration. @@ -81,11 +82,11 @@ func (p *L3Plugin) Init() error { p.l3Handler = linuxcalls.NewNetLinkHandler() // init & register descriptors - arpDescriptor := adapter.NewARPDescriptor(descriptor.NewARPDescriptor( - p.KVScheduler, p.IfPlugin, p.NsPlugin, p.l3Handler, p.Log, config.GoRoutinesCnt).GetDescriptor()) + arpDescriptor := descriptor.NewARPDescriptor( + p.KVScheduler, p.IfPlugin, p.NsPlugin, p.AddrAlloc, p.l3Handler, p.Log, config.GoRoutinesCnt) - routeDescriptor := adapter.NewRouteDescriptor(descriptor.NewRouteDescriptor( - p.KVScheduler, p.IfPlugin, p.NsPlugin, p.l3Handler, p.Log, config.GoRoutinesCnt).GetDescriptor()) + routeDescriptor := descriptor.NewRouteDescriptor( + p.KVScheduler, p.IfPlugin, p.NsPlugin, p.AddrAlloc, p.l3Handler, p.Log, config.GoRoutinesCnt) err = p.Deps.KVScheduler.RegisterKVDescriptor(arpDescriptor) if err != nil { diff --git a/plugins/linux/l3plugin/options.go b/plugins/linux/l3plugin/options.go index 5f56034250..dc38460a00 100644 --- a/plugins/linux/l3plugin/options.go +++ b/plugins/linux/l3plugin/options.go @@ -6,6 +6,7 @@ import ( "github.com/ligato/vpp-agent/plugins/kvscheduler" "github.com/ligato/vpp-agent/plugins/linux/ifplugin" "github.com/ligato/vpp-agent/plugins/linux/nsplugin" + "github.com/ligato/vpp-agent/plugins/netalloc" ) // DefaultPlugin is a default instance of IfPlugin. @@ -18,6 +19,7 @@ func NewPlugin(opts ...Option) *L3Plugin { p.PluginName = "linux-l3plugin" p.KVScheduler = &kvscheduler.DefaultPlugin p.NsPlugin = &nsplugin.DefaultPlugin + p.AddrAlloc = &netalloc.DefaultPlugin p.IfPlugin = &ifplugin.DefaultPlugin for _, o := range opts { diff --git a/plugins/netalloc/descriptor/adapter/ipalloc.go b/plugins/netalloc/descriptor/adapter/ipalloc.go new file mode 100644 index 0000000000..1835dca548 --- /dev/null +++ b/plugins/netalloc/descriptor/adapter/ipalloc.go @@ -0,0 +1,233 @@ +// Code generated by adapter-generator. DO NOT EDIT. + +package adapter + +import ( + "github.com/gogo/protobuf/proto" + . "github.com/ligato/vpp-agent/plugins/kvscheduler/api" + "github.com/ligato/vpp-agent/api/models/netalloc" +) + +////////// type-safe key-value pair with metadata ////////// + +type IPAllocKVWithMetadata struct { + Key string + Value *netalloc.IPAllocation + Metadata *netalloc.IPAllocMetadata + Origin ValueOrigin +} + +////////// type-safe Descriptor structure ////////// + +type IPAllocDescriptor struct { + Name string + KeySelector KeySelector + ValueTypeName string + KeyLabel func(key string) string + ValueComparator func(key string, oldValue, newValue *netalloc.IPAllocation) bool + NBKeyPrefix string + WithMetadata bool + MetadataMapFactory MetadataMapFactory + Validate func(key string, value *netalloc.IPAllocation) error + Create func(key string, value *netalloc.IPAllocation) (metadata *netalloc.IPAllocMetadata, err error) + Delete func(key string, value *netalloc.IPAllocation, metadata *netalloc.IPAllocMetadata) error + Update func(key string, oldValue, newValue *netalloc.IPAllocation, oldMetadata *netalloc.IPAllocMetadata) (newMetadata *netalloc.IPAllocMetadata, err error) + UpdateWithRecreate func(key string, oldValue, newValue *netalloc.IPAllocation, metadata *netalloc.IPAllocMetadata) bool + Retrieve func(correlate []IPAllocKVWithMetadata) ([]IPAllocKVWithMetadata, error) + IsRetriableFailure func(err error) bool + DerivedValues func(key string, value *netalloc.IPAllocation) []KeyValuePair + Dependencies func(key string, value *netalloc.IPAllocation) []Dependency + RetrieveDependencies []string /* descriptor name */ +} + +////////// Descriptor adapter ////////// + +type IPAllocDescriptorAdapter struct { + descriptor *IPAllocDescriptor +} + +func NewIPAllocDescriptor(typedDescriptor *IPAllocDescriptor) *KVDescriptor { + adapter := &IPAllocDescriptorAdapter{descriptor: typedDescriptor} + descriptor := &KVDescriptor{ + Name: typedDescriptor.Name, + KeySelector: typedDescriptor.KeySelector, + ValueTypeName: typedDescriptor.ValueTypeName, + KeyLabel: typedDescriptor.KeyLabel, + NBKeyPrefix: typedDescriptor.NBKeyPrefix, + WithMetadata: typedDescriptor.WithMetadata, + MetadataMapFactory: typedDescriptor.MetadataMapFactory, + IsRetriableFailure: typedDescriptor.IsRetriableFailure, + RetrieveDependencies: typedDescriptor.RetrieveDependencies, + } + if typedDescriptor.ValueComparator != nil { + descriptor.ValueComparator = adapter.ValueComparator + } + if typedDescriptor.Validate != nil { + descriptor.Validate = adapter.Validate + } + if typedDescriptor.Create != nil { + descriptor.Create = adapter.Create + } + if typedDescriptor.Delete != nil { + descriptor.Delete = adapter.Delete + } + if typedDescriptor.Update != nil { + descriptor.Update = adapter.Update + } + if typedDescriptor.UpdateWithRecreate != nil { + descriptor.UpdateWithRecreate = adapter.UpdateWithRecreate + } + if typedDescriptor.Retrieve != nil { + descriptor.Retrieve = adapter.Retrieve + } + if typedDescriptor.Dependencies != nil { + descriptor.Dependencies = adapter.Dependencies + } + if typedDescriptor.DerivedValues != nil { + descriptor.DerivedValues = adapter.DerivedValues + } + return descriptor +} + +func (da *IPAllocDescriptorAdapter) ValueComparator(key string, oldValue, newValue proto.Message) bool { + typedOldValue, err1 := castIPAllocValue(key, oldValue) + typedNewValue, err2 := castIPAllocValue(key, newValue) + if err1 != nil || err2 != nil { + return false + } + return da.descriptor.ValueComparator(key, typedOldValue, typedNewValue) +} + +func (da *IPAllocDescriptorAdapter) Validate(key string, value proto.Message) (err error) { + typedValue, err := castIPAllocValue(key, value) + if err != nil { + return err + } + return da.descriptor.Validate(key, typedValue) +} + +func (da *IPAllocDescriptorAdapter) Create(key string, value proto.Message) (metadata Metadata, err error) { + typedValue, err := castIPAllocValue(key, value) + if err != nil { + return nil, err + } + return da.descriptor.Create(key, typedValue) +} + +func (da *IPAllocDescriptorAdapter) Update(key string, oldValue, newValue proto.Message, oldMetadata Metadata) (newMetadata Metadata, err error) { + oldTypedValue, err := castIPAllocValue(key, oldValue) + if err != nil { + return nil, err + } + newTypedValue, err := castIPAllocValue(key, newValue) + if err != nil { + return nil, err + } + typedOldMetadata, err := castIPAllocMetadata(key, oldMetadata) + if err != nil { + return nil, err + } + return da.descriptor.Update(key, oldTypedValue, newTypedValue, typedOldMetadata) +} + +func (da *IPAllocDescriptorAdapter) Delete(key string, value proto.Message, metadata Metadata) error { + typedValue, err := castIPAllocValue(key, value) + if err != nil { + return err + } + typedMetadata, err := castIPAllocMetadata(key, metadata) + if err != nil { + return err + } + return da.descriptor.Delete(key, typedValue, typedMetadata) +} + +func (da *IPAllocDescriptorAdapter) UpdateWithRecreate(key string, oldValue, newValue proto.Message, metadata Metadata) bool { + oldTypedValue, err := castIPAllocValue(key, oldValue) + if err != nil { + return true + } + newTypedValue, err := castIPAllocValue(key, newValue) + if err != nil { + return true + } + typedMetadata, err := castIPAllocMetadata(key, metadata) + if err != nil { + return true + } + return da.descriptor.UpdateWithRecreate(key, oldTypedValue, newTypedValue, typedMetadata) +} + +func (da *IPAllocDescriptorAdapter) Retrieve(correlate []KVWithMetadata) ([]KVWithMetadata, error) { + var correlateWithType []IPAllocKVWithMetadata + for _, kvpair := range correlate { + typedValue, err := castIPAllocValue(kvpair.Key, kvpair.Value) + if err != nil { + continue + } + typedMetadata, err := castIPAllocMetadata(kvpair.Key, kvpair.Metadata) + if err != nil { + continue + } + correlateWithType = append(correlateWithType, + IPAllocKVWithMetadata{ + Key: kvpair.Key, + Value: typedValue, + Metadata: typedMetadata, + Origin: kvpair.Origin, + }) + } + + typedValues, err := da.descriptor.Retrieve(correlateWithType) + if err != nil { + return nil, err + } + var values []KVWithMetadata + for _, typedKVWithMetadata := range typedValues { + kvWithMetadata := KVWithMetadata{ + Key: typedKVWithMetadata.Key, + Metadata: typedKVWithMetadata.Metadata, + Origin: typedKVWithMetadata.Origin, + } + kvWithMetadata.Value = typedKVWithMetadata.Value + values = append(values, kvWithMetadata) + } + return values, err +} + +func (da *IPAllocDescriptorAdapter) DerivedValues(key string, value proto.Message) []KeyValuePair { + typedValue, err := castIPAllocValue(key, value) + if err != nil { + return nil + } + return da.descriptor.DerivedValues(key, typedValue) +} + +func (da *IPAllocDescriptorAdapter) Dependencies(key string, value proto.Message) []Dependency { + typedValue, err := castIPAllocValue(key, value) + if err != nil { + return nil + } + return da.descriptor.Dependencies(key, typedValue) +} + +////////// Helper methods ////////// + +func castIPAllocValue(key string, value proto.Message) (*netalloc.IPAllocation, error) { + typedValue, ok := value.(*netalloc.IPAllocation) + if !ok { + return nil, ErrInvalidValueType(key, value) + } + return typedValue, nil +} + +func castIPAllocMetadata(key string, metadata Metadata) (*netalloc.IPAllocMetadata, error) { + if metadata == nil { + return nil, nil + } + typedMetadata, ok := metadata.(*netalloc.IPAllocMetadata) + if !ok { + return nil, ErrInvalidMetadataType(key) + } + return typedMetadata, nil +} diff --git a/plugins/netalloc/descriptor/ip_alloc.go b/plugins/netalloc/descriptor/ip_alloc.go new file mode 100644 index 0000000000..cec67de800 --- /dev/null +++ b/plugins/netalloc/descriptor/ip_alloc.go @@ -0,0 +1,122 @@ +// Copyright (c) 2019 Cisco and/or its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package descriptor + +import ( + "net" + + "github.com/ligato/cn-infra/logging" + prototypes "github.com/gogo/protobuf/types" + + "github.com/ligato/vpp-agent/api/models/netalloc" + kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" + "github.com/ligato/vpp-agent/plugins/netalloc/descriptor/adapter" + "github.com/ligato/vpp-agent/plugins/netalloc/utils" +) + +const ( + // IPAllocDescriptorName is the name of the descriptor for allocating + // IP addresses. + IPAllocDescriptorName = "netalloc-ip-address" +) + +// IPAllocDescriptor just validates and parses allocated IP addresses. +type IPAllocDescriptor struct { + log logging.Logger +} + +// NewAddrAllocDescriptor creates a new instance of IPAllocDescriptor. +func NewAddrAllocDescriptor(log logging.PluginLogger) (descr *kvs.KVDescriptor) { + ctx := &IPAllocDescriptor{ + log: log.NewLogger("ip-address-alloc-descriptor"), + } + typedDescr := &adapter.IPAllocDescriptor{ + Name: IPAllocDescriptorName, + NBKeyPrefix: netalloc.ModelIPAllocation.KeyPrefix(), + ValueTypeName: netalloc.ModelIPAllocation.ProtoName(), + KeySelector: netalloc.ModelIPAllocation.IsKeyValid, + KeyLabel: netalloc.ModelIPAllocation.StripKeyPrefix, + WithMetadata: true, + Validate: ctx.Validate, + Create: ctx.Create, + Delete: ctx.Delete, + Retrieve: ctx.Retrieve, + DerivedValues: ctx.DerivedValues, + } + descr = adapter.NewIPAllocDescriptor(typedDescr) + return +} + +// Validate checks if the address can be parsed. +func (d *IPAllocDescriptor) Validate(key string, addrAlloc *netalloc.IPAllocation) (err error) { + _, _, err = d.parseAddr(addrAlloc) + return err +} + +// Create parses the address and stores it into the metadata. +func (d *IPAllocDescriptor) Create(key string, addrAlloc *netalloc.IPAllocation) (metadata *netalloc.IPAllocMetadata, err error) { + metadata, _, err = d.parseAddr(addrAlloc) + return +} + +// Delete is NOOP. +func (d *IPAllocDescriptor) Delete(key string, addrAlloc *netalloc.IPAllocation, metadata *netalloc.IPAllocMetadata) (err error) { + return err +} + +// DerivedValues derives "neighbour-gateway" key if GW is a neighbour of the interface +// (addresses are from the same IP network). +func (d *IPAllocDescriptor) DerivedValues(key string, addrAlloc *netalloc.IPAllocation) (derValues []kvs.KeyValuePair) { + _, neighGw, _ := d.parseAddr(addrAlloc) + if neighGw { + derValues = append(derValues, kvs.KeyValuePair{ + Key: netalloc.NeighGwKey(addrAlloc.NetworkName, addrAlloc.InterfaceName), + Value: &prototypes.Empty{}, + }) + } + return derValues +} + +// Retrieve always returns what is expected to exists since Create doesn't really change +// anything in SB. +func (d *IPAllocDescriptor) Retrieve(correlate []adapter.IPAllocKVWithMetadata) (valid []adapter.IPAllocKVWithMetadata, err error) { + for _, addrAlloc := range correlate { + if meta, _, err := d.parseAddr(addrAlloc.Value); err == nil { + valid = append(valid, adapter.IPAllocKVWithMetadata{ + Key: addrAlloc.Key, + Value: addrAlloc.Value, + Metadata: meta, + Origin: kvs.FromNB, + }) + } + } + return valid, nil +} + +// parseAddr tries to parse the allocated address. +func (d *IPAllocDescriptor) parseAddr(addrAlloc *netalloc.IPAllocation) (parsed *netalloc.IPAllocMetadata, neighGw bool, err error) { + ifaceAddr, _, err := utils.ParseIPAddr(addrAlloc.Address, nil) + if err != nil { + return nil, false, err + } + var gwAddr *net.IPNet + if addrAlloc.Gw != "" { + gwAddr, neighGw, err = utils.ParseIPAddr(addrAlloc.Gw, ifaceAddr) + if err != nil { + return nil, false, err + } + } + return &netalloc.IPAllocMetadata{IfaceAddr: ifaceAddr, GwAddr: gwAddr}, neighGw,nil +} diff --git a/plugins/netalloc/mock/mock_netplugin.go b/plugins/netalloc/mock/mock_netplugin.go new file mode 100644 index 0000000000..fd803567f4 --- /dev/null +++ b/plugins/netalloc/mock/mock_netplugin.go @@ -0,0 +1,131 @@ +package mock + +import ( + "errors" + "net" + + "github.com/ligato/vpp-agent/api/models/netalloc" + "github.com/ligato/vpp-agent/pkg/models" + kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" + "github.com/ligato/vpp-agent/plugins/netalloc/utils" + plugin "github.com/ligato/vpp-agent/plugins/netalloc" +) + +// NetAlloc is a mock version of the netplugin, suitable for unit testing. +type NetAlloc struct { + realNetAlloc *plugin.Plugin + allocated map[string]*netalloc.IPAllocMetadata // allocation name -> parsed address +} + +// NewMockNetAlloc is a constructor for mock netalloc plugin. +func NewMockNetAlloc() *NetAlloc { + return &NetAlloc{ + realNetAlloc: &plugin.Plugin{}, + allocated: make(map[string]*netalloc.IPAllocMetadata), + } +} + +// Allocate simulates allocation of an IP address. +func (p *NetAlloc) Allocate(network, ifaceName, address, gw string) { + var ( + gwAddr *net.IPNet + err error + ) + addrAlloc := &netalloc.IPAllocation{ + NetworkName: network, + InterfaceName: ifaceName, + } + allocName := models.Name(addrAlloc) + + ifaceAddr, _, err := utils.ParseIPAddr(address, nil) + if err != nil { + panic(err) + } + if gw != "" { + gwAddr, _, err = utils.ParseIPAddr(gw, ifaceAddr) + if err != nil { + panic(err) + } + } + p.allocated[allocName] = &netalloc.IPAllocMetadata{IfaceAddr: ifaceAddr, GwAddr: gwAddr} +} + +// Deallocate simulates de-allocation of an IP address. +func (p *NetAlloc) Deallocate(network, ifaceName string) { + addrAlloc := &netalloc.IPAllocation{ + NetworkName: network, + InterfaceName: ifaceName, + } + allocName := models.Name(addrAlloc) + delete(p.allocated, allocName) +} + +// CreateAddressAllocRef creates reference to an allocated IP address. +func (p *NetAlloc) CreateAddressAllocRef(network, iface string, getGW bool) string { + return p.realNetAlloc.CreateAddressAllocRef(network, iface, getGW) +} + +// ParseAddressAllocRef parses reference to an allocated IP address. +func (p *NetAlloc) ParseAddressAllocRef(addrAllocRef, expIface string) ( + network, iface string, isGW, isRef bool, err error) { + return p.realNetAlloc.ParseAddressAllocRef(addrAllocRef, expIface) +} + +// GetAddressAllocDep is not implemented here. +func (p *NetAlloc) GetAddressAllocDep(addrOrAllocRef, ifaceName, depLabelPrefix string) ( + dep kvs.Dependency, hasAllocDep bool) { + return kvs.Dependency{}, false +} + +// ValidateIPAddress checks validity of address reference or, if +// already contains an actual IP address, it tries to parse it. +func (p *NetAlloc) ValidateIPAddress(addrOrAllocRef, ifaceName, fieldName string, gwCheck plugin.GwValidityCheck) error { + return p.realNetAlloc.ValidateIPAddress(addrOrAllocRef, ifaceName, fieldName, gwCheck) +} + +// GetOrParseIPAddress tries to get allocated interface (or GW) IP address +// referenced by in the requested form. But if the string +// contains/ an actual IP address instead of a reference, the address is parsed +// using methods from the net package and returned in the requested form. +// For ADDR_ONLY address form, the returned will have the mask unset +// and the IP address should be accessed as .IP +func (p *NetAlloc) GetOrParseIPAddress(addrOrAllocRef string, ifaceName string, + addrForm netalloc.IPAddressForm) (addr *net.IPNet, err error) { + + network, iface, getGW, isRef, err := utils.ParseAddrAllocRef(addrOrAllocRef, ifaceName) + if isRef && err != nil { + return nil, err + } + + if isRef { + // de-reference + allocName := models.Name(&netalloc.IPAllocation{ + NetworkName: network, + InterfaceName: iface, + }) + allocation, found := p.allocated[allocName] + if !found { + return nil, errors.New("address is not allocated") + } + if getGW { + if allocation.GwAddr == nil { + return nil, errors.New("gw address is not defined") + } + return utils.GetIPAddrInGivenForm(allocation.GwAddr, addrForm), nil + } + return utils.GetIPAddrInGivenForm(allocation.IfaceAddr, addrForm), nil + } + + // try to parse the address + ipAddr, _, err := utils.ParseIPAddr(addrOrAllocRef, nil) + if err != nil { + return nil, err + } + return utils.GetIPAddrInGivenForm(ipAddr, addrForm), nil +} + +// CorrelateRetrievedIPs is not implemented here. +func (p *NetAlloc) CorrelateRetrievedIPs(expAddrsOrRefs []string, retrievedAddrs []string, + ifaceName string, addrForm netalloc.IPAddressForm) (correlated []string) { + return retrievedAddrs +} diff --git a/plugins/netalloc/netalloc_api.go b/plugins/netalloc/netalloc_api.go new file mode 100644 index 0000000000..6f012a70fb --- /dev/null +++ b/plugins/netalloc/netalloc_api.go @@ -0,0 +1,134 @@ +package netalloc + +import ( + "net" + + "github.com/ligato/vpp-agent/api/models/netalloc" + kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" +) + + +// GwValidityCheck is used in ValidateIPAddress to tell if a GW reference is (un)expected/required. +type GwValidityCheck int +const ( + // GWRefAllowed is used when it doesn't matter if reference points to interface + // address or GW address. + GWRefAllowed GwValidityCheck = iota + // GWRefAllowed is used when an IP address reference should point to GW address. + GWRefRequired + // GwRefUnexpected is used when an IP address reference should not point to GW address. + GwRefUnexpected +) + +// AddressAllocator provides methods for descriptors of other plugins to reference +// and obtain allocated addresses. +// +// For example, if a model of some configuration item contains field IpAddress +// (of type string) which could reference allocated IP address (assigned to non +// pre-determined interface) and should be applied without mask, the descriptor +// for that item would implement some of the methods as follows: +// +// func (d *Descriptor) Validate(key string, intf *mymodel.MyModel) error { +// err := d.netallocPlugin.ValidateIPAddress(item.IpAddress, "", "IpAddress") +// if err != nil { +// return err +// } +// } +// +// func (d *Descriptor) Dependencies(key string, item *mymodel.MyModel) (dependencies []kvs.Dependency) { +// // Note: it is actually preferred to derive the IP address into a separate key-value +// // pair and assign the allocation dependency to it rather than to the entire configuration item +// dep, hasAllocDep := d.netallocPlugin.GetAddressAllocDep(item.IpAddress, "", "") +// if hasAllocDep { +// dependencies = append(dependencies, dep) +// } +// // ... +// } +// +// func (d *Descriptor) Create(key string, item *mymodel.MyModel) (metadata interface{}, err error) { +// addr, err := d.netallocPlugin.GetOrParseIPAddress(item.IpAddress, "", netalloc.ADDR_ONLY) +// if err != nil { +// d.log.Error(err) +// return nil, err +// } +// fmt.Printf("Assign IP address: %v", addr.IP) +// ... +// } +// +// func (d *Descriptor) Delete(key string, item *mymodel.MyModel) (err error) { +// addr, err := d.netallocPlugin.GetOrParseIPAddress(item.IpAddress, "", netalloc.ADDR_ONLY) +// if err != nil { +// d.log.Error(err) +// return nil, err +// } +// fmt.Printf("Un-assign IP address: %v", addr.IP) +// ... +// } +// +// func (d *Descriptor) Update(key string, oldItem, newItem *mymodel.MyModel, oldMetadata interface{}) (newMetadata interface{}, err error) { { +// prevAddr, err := d.netallocPlugin.GetOrParseIPAddress(oldItem.IpAddress, "", netalloc.ADDR_ONLY) +// if err != nil { +// d.log.Error(err) +// return nil, err +// } +// newAddr, err := d.netallocPlugin.GetOrParseIPAddress(newItem.IpAddress, "", netalloc.ADDR_ONLY) +// if err != nil { +// d.log.Error(err) +// return nil, err +// } +// fmt.Printf("Changing assigned IP address from %v to %v", prevAddr.IP, newAddr.IP) +// ... +// } +// +// func (d *Descriptor) Retrieve(correlate []adapter.MyModelKVWithMetadata) (retrieved []adapter.MyModelKVWithMetadata, err error) { +// // Retrieve instances of mymodel.MyModel ... +// // Use CorrelateRetrievedIPs to replace actual IP address with reference if it was used. +// for _, item := range retrieved { +// // get expected item configuration ... (store to expCfg) +// item.IpAddress = d.netallocPlugin.CorrelateRetrievedIPs( +// []string{expCfg.IpAddress}, []string{item.IpAddress}, "", netalloc.ADDR_ONLY)[0] +// } +// } +// +// Also don't forget to include netalloc descriptors in the list of "RetrieveDependencies" +// (for IP allocations, the descriptor name is stored in the constant IPAllocDescriptorName +// defined in plugins/netalloc/descriptor) +type AddressAllocator interface { + // CreateAddressAllocRef creates reference to an allocated IP address. + CreateAddressAllocRef(network, iface string, getGW bool) string + + // ParseAddressAllocRef parses reference to an allocated IP address. + ParseAddressAllocRef(addrAllocRef, expIface string) ( + network, iface string, isGW, isRef bool, err error) + + // GetAddressAllocDep reads what can be potentially a reference to an allocated + // IP address. If is indeed a reference, the function returns + // the corresponding dependency to be passed further into KVScheduler + // from the descriptor. Otherwise is returned as false, and + // should be an actual address and not a reference. + GetAddressAllocDep(addrOrAllocRef, expIface, depLabelPrefix string) ( + dep kvs.Dependency, hasAllocDep bool) + + // ValidateIPAddress checks validity of address reference or, if + // already contains an actual IP address, it tries to parse it. + ValidateIPAddress(addrOrAllocRef, expIface, fieldName string, gwCheck GwValidityCheck) error + + // GetOrParseIPAddress tries to get allocated interface (or GW) IP address + // referenced by in the requested form. But if the string + // contains/ an actual IP address instead of a reference, the address is parsed + // using methods from the net package and returned in the requested form. + // For ADDR_ONLY address form, the returned will have the mask unset + // and the IP address should be accessed as .IP + GetOrParseIPAddress(addrOrAllocRef string, expIface string, addrForm netalloc.IPAddressForm) ( + addr *net.IPNet, err error) + + // CorrelateRetrievedIPs should be used in Retrieve to correlate one or group + // of (model-wise indistinguishable) retrieved interface or GW IP addresses + // with the expected configuration. The method will replace retrieved addresses + // with the corresponding allocation references from the expected configuration + // if there are any. + // The method returns one IP address or address-allocation reference for every + // address from . + CorrelateRetrievedIPs(expAddrsOrRefs []string, retrievedAddrs []string, expIface string, + addrForm netalloc.IPAddressForm) []string +} diff --git a/plugins/netalloc/netalloc_plugin.go b/plugins/netalloc/netalloc_plugin.go new file mode 100644 index 0000000000..38bbccc975 --- /dev/null +++ b/plugins/netalloc/netalloc_plugin.go @@ -0,0 +1,232 @@ +// Copyright (c) 2019 Cisco and/or its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:generate descriptor-adapter --descriptor-name IPAlloc --value-type *netalloc.IPAllocation --meta-type *netalloc.IPAllocMetadata --import "github.com/ligato/vpp-agent/api/models/netalloc" --output-dir "descriptor" + +package netalloc + +import ( + "bytes" + "errors" + "fmt" + "net" + + "github.com/ligato/cn-infra/infra" + + "github.com/ligato/cn-infra/idxmap" + "github.com/ligato/vpp-agent/api/models/netalloc" + "github.com/ligato/vpp-agent/pkg/models" + kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" + "github.com/ligato/vpp-agent/plugins/netalloc/descriptor" + "github.com/ligato/vpp-agent/plugins/netalloc/utils" +) + +// Plugin implements network allocation features. +// For more information, please refer to the netalloc proto model: api/models/netalloc/netalloc.proto +type Plugin struct { + Deps + + // IP address allocation + ipAllocDescriptor *kvs.KVDescriptor + ipIndex idxmap.NamedMapping +} + +// Deps lists dependencies of the netalloc plugin. +type Deps struct { + infra.PluginDeps + KVScheduler kvs.KVScheduler +} + +// Init initializes netalloc descriptors. +func (p *Plugin) Init() error { + // init & register descriptors + p.ipAllocDescriptor = descriptor.NewAddrAllocDescriptor(p.Log) + err := p.Deps.KVScheduler.RegisterKVDescriptor(p.ipAllocDescriptor) + if err != nil { + return err + } + + // obtain map with metadata of allocated addresses + p.ipIndex = p.KVScheduler.GetMetadataMap(descriptor.IPAllocDescriptorName) + if p.ipIndex == nil { + return errors.New("missing index with metadata of allocated addresses") + } + return nil +} + +// Close does nothing. +func (p *Plugin) Close() error { + return nil +} + +// CreateAddressAllocRef creates reference to an allocated IP address. +func (p *Plugin) CreateAddressAllocRef(network, iface string, getGW bool) string { + ref := netalloc.AllocRefPrefix + network + if iface != "" { + ref += "/" + iface + } + if getGW { + ref += netalloc.AllocRefGWSuffix + } + return ref +} + +// ParseAddressAllocRef parses reference to an allocated IP address. +func (p *Plugin) ParseAddressAllocRef(addrAllocRef, expIface string) ( + network, iface string, isGW, isRef bool, err error) { + return utils.ParseAddrAllocRef(addrAllocRef, expIface) +} + +// GetAddressAllocDep reads what can be potentially a reference to an allocated +// IP address. If is indeed a reference, the function returns +// the corresponding dependency to be passed further into KVScheduler +// from the descriptor. Otherwise is returned as false, and +// should be an actual address and not a reference. +func (p *Plugin) GetAddressAllocDep(addrOrAllocRef, ifaceName, depLabelPrefix string) ( + dep kvs.Dependency, hasAllocDep bool) { + + network, iface, _, isRef, err := utils.ParseAddrAllocRef(addrOrAllocRef, ifaceName) + if !isRef || err != nil { + return kvs.Dependency{}, false + } + + return kvs.Dependency{ + Label: depLabelPrefix + addrOrAllocRef, + Key: models.Key(&netalloc.IPAllocation{ + NetworkName: network, + InterfaceName: iface, + }), + }, true +} + +// ValidateIPAddress checks validity of address reference or, if +// already contains an actual IP address, it tries to parse it. +func (p *Plugin) ValidateIPAddress(addrOrAllocRef, ifaceName, fieldName string, gwCheck GwValidityCheck) error { + _, _, isGW, isRef, err := utils.ParseAddrAllocRef(addrOrAllocRef, ifaceName) + if !isRef { + _, _, err = utils.ParseIPAddr(addrOrAllocRef, nil) + } else if err == nil { + switch gwCheck { + case GWRefRequired: + if !isGW { + err = errors.New("expected GW address reference") + } + case GwRefUnexpected: + if isGW { + err = errors.New("expected non-GW address reference") + } + } + } + if err != nil { + if fieldName != "" { + return kvs.NewInvalidValueError(err, fieldName) + } else { + return kvs.NewInvalidValueError(err) + } + } + return nil +} + +// GetOrParseIPAddress tries to get allocated interface (or GW) IP address +// referenced by in the requested form. But if the string +// contains/ an actual IP address instead of a reference, the address is parsed +// using methods from the net package and returned in the requested form. +// For ADDR_ONLY address form, the returned will have the mask unset +// and the IP address should be accessed as .IP +func (p *Plugin) GetOrParseIPAddress(addrOrAllocRef string, ifaceName string, + addrForm netalloc.IPAddressForm) (addr *net.IPNet, err error) { + + network, iface, getGW, isRef, err := utils.ParseAddrAllocRef(addrOrAllocRef, ifaceName) + if isRef && err != nil { + return nil, err + } + + if isRef { + // reference to allocated IP address + allocName := models.Name(&netalloc.IPAllocation{ + NetworkName: network, + InterfaceName: iface, + }) + allocVal, found := p.ipIndex.GetValue(allocName) + if !found { + return nil, fmt.Errorf("failed to find metadata for IP address allocation '%s'", + allocName) + } + allocMeta, ok := allocVal.(*netalloc.IPAllocMetadata) + if !ok { + return nil, fmt.Errorf("invalid type of metadata stored for IP address allocation '%s'", + allocName) + } + if getGW { + if allocMeta.GwAddr == nil { + return nil, fmt.Errorf("gw address is not defined for IP address allocation '%s'", + allocName) + } + return utils.GetIPAddrInGivenForm(allocMeta.GwAddr, addrForm), nil + } + return utils.GetIPAddrInGivenForm(allocMeta.IfaceAddr, addrForm), nil + } + + // not a reference - try to parse the address + ipAddr, _, err := utils.ParseIPAddr(addrOrAllocRef, nil) + if err != nil { + return nil, err + } + return utils.GetIPAddrInGivenForm(ipAddr, addrForm), nil +} + +// CorrelateRetrievedIPs should be used in Retrieve to correlate one or group +// of (model-wise indistinguishable) retrieved interface or GW IP addresses +// with the expected configuration. The method will replace retrieved addresses +// with the corresponding allocation references from the expected configuration +// if there are any. +// The method returns one IP address or address-allocation reference for every +// address from . +func (p *Plugin) CorrelateRetrievedIPs(expAddrsOrRefs []string, retrievedAddrs []string, + ifaceName string, addrForm netalloc.IPAddressForm) (correlated []string) { + + expParsed := make([]*net.IPNet, len(expAddrsOrRefs)) + for i, addr := range expAddrsOrRefs { + expParsed[i], _ = p.GetOrParseIPAddress(addr, ifaceName, addrForm) + } + + for _, addr := range retrievedAddrs { + ipAddr, _, err := utils.ParseIPAddr(addr, nil) + if err != nil { + // invalid - do not try to correlate, just return as is + correlated = append(correlated, addr) + continue + } + var addrCorrelated bool + for i, expAddr := range expParsed { + if expAddr == nil { + continue + } + if bytes.Equal(ipAddr.IP, expAddr.IP) { + if addrForm == netalloc.IPAddressForm_ADDR_ONLY || + bytes.Equal(ipAddr.Mask, expAddr.Mask) { + // found match in the expected configuration + correlated = append(correlated, expAddrsOrRefs[i]) + addrCorrelated = true + break + } + } + } + if !addrCorrelated { + // couldn't find match in the expected configuration, just return as is + correlated = append(correlated, addr) + } + } + return correlated +} diff --git a/plugins/netalloc/options.go b/plugins/netalloc/options.go new file mode 100644 index 0000000000..e9013ff310 --- /dev/null +++ b/plugins/netalloc/options.go @@ -0,0 +1,37 @@ +package netalloc + +import ( + "github.com/ligato/cn-infra/logging" + "github.com/ligato/vpp-agent/plugins/kvscheduler" +) + +// DefaultPlugin is a default instance of netalloc plugin. +var DefaultPlugin = *NewPlugin() + +// NewPlugin creates a new Plugin with the provides Options +func NewPlugin(opts ...Option) *Plugin { + p := &Plugin{} + + p.PluginName = "netalloc" + p.KVScheduler = &kvscheduler.DefaultPlugin + + for _, o := range opts { + o(p) + } + + if p.Log == nil { + p.Log = logging.ForPlugin(p.String()) + } + + return p +} + +// Option is a function that can be used in NewPlugin to customize Plugin. +type Option func(plugin *Plugin) + +// UseDeps returns Option that can inject custom dependencies. +func UseDeps(f func(*Deps)) Option { + return func(p *Plugin) { + f(&p.Deps) + } +} diff --git a/plugins/netalloc/utils/netalloc_utils.go b/plugins/netalloc/utils/netalloc_utils.go new file mode 100644 index 0000000000..12f060a799 --- /dev/null +++ b/plugins/netalloc/utils/netalloc_utils.go @@ -0,0 +1,131 @@ +package utils + +import ( + "fmt" + "net" + "strings" + + "github.com/ligato/vpp-agent/api/models/netalloc" +) + +// ParseIPAddr parses IP address from string. +func ParseIPAddr(addr string, expNet *net.IPNet) (ipNet *net.IPNet, fromExpNet bool, err error) { + if expNet != nil { + expNet = &net.IPNet{IP: expNet.IP.Mask(expNet.Mask), Mask: expNet.Mask} + } + + if strings.Contains(addr, "/") { + // IP with mask + ip, ipNet, err := net.ParseCIDR(addr) + if err != nil { + return nil, false, err + } + if ip.To4() != nil { + ip = ip.To4() + } + ipNet.IP = ip + if expNet != nil { + fromExpNet = expNet.Contains(ip) + } + return ipNet, fromExpNet, nil + } + + // IP without mask + ip := net.ParseIP(addr) + if ip == nil { + return nil, false, fmt.Errorf("invalid IP address: %s", addr) + } + if ip.To4() != nil { + ip = ip.To4() + } + if expNet != nil { + if expNet.Contains(ip) { + // IP address from the expected network + return &net.IPNet{IP: ip, Mask: expNet.Mask}, true,nil + } + } + + // use all-ones mask + defaultIpv4Mask := net.CIDRMask(32, 32) + defaultIpv6Mask := net.CIDRMask(128, 128) + + if ip.To4() != nil { + // IPv4 address + return &net.IPNet{IP: ip.To4(), Mask: defaultIpv4Mask}, false, nil + } + + // IPv6 address + return &net.IPNet{IP: ip, Mask: defaultIpv6Mask}, false,nil +} + +// ParseAddrAllocRef parses reference to allocated address. +func ParseAddrAllocRef(addrAllocRef, expIface string) ( + network, iface string, isGW, isRef bool, err error) { + + if !strings.HasPrefix(addrAllocRef, netalloc.AllocRefPrefix) { + isRef = false + return + } + + isRef = true + addrAllocRef = strings.TrimPrefix(addrAllocRef, netalloc.AllocRefPrefix) + if strings.HasSuffix(addrAllocRef, netalloc.AllocRefGWSuffix) { + addrAllocRef = strings.TrimSuffix(addrAllocRef, netalloc.AllocRefGWSuffix) + isGW = true + } + + // parse network name + parts := strings.SplitN(addrAllocRef, "/", 2) + network = parts[0] + if network == "" { + err = fmt.Errorf("address allocation reference with empty network name: %s", + addrAllocRef) + return + } + + // parse interface name + if len(parts) == 2 { + iface = parts[1] + if expIface != "" && iface != expIface { + err = fmt.Errorf("expected different interface name in the address allocation "+ + "reference: %s (expected=%s vs. actual=%s)", addrAllocRef, expIface, iface) + return + } + } else { + if expIface == "" { + err = fmt.Errorf("missing interface name in the address allocation reference: %s", + addrAllocRef) + return + } else { + iface = expIface + } + } + return +} + +// GetIPAddrInGivenForm returns IP address in the requested form. +func GetIPAddrInGivenForm(addr *net.IPNet, form netalloc.IPAddressForm) *net.IPNet { + switch form { + case netalloc.IPAddressForm_UNDEFINED_FORM: + return addr + case netalloc.IPAddressForm_ADDR_ONLY: + zeroMaskIpv4 := net.CIDRMask(0, 32) + zeroMaskIpv6 := net.CIDRMask(0, 128) + if addr.IP.To4() != nil { + return &net.IPNet{IP: addr.IP, Mask: zeroMaskIpv4} + } + return &net.IPNet{IP: addr.IP, Mask: zeroMaskIpv6} + case netalloc.IPAddressForm_ADDR_WITH_MASK: + return addr + case netalloc.IPAddressForm_ADDR_NET: + return &net.IPNet{IP: addr.IP.Mask(addr.Mask), Mask: addr.Mask} + case netalloc.IPAddressForm_SINGLE_ADDR_NET: + allOnesIpv4 := net.CIDRMask(32, 32) + allOnesIpv6 := net.CIDRMask(128, 128) + if addr.IP.To4() != nil { + return &net.IPNet{IP: addr.IP, Mask: allOnesIpv4} + } + return &net.IPNet{IP: addr.IP, Mask: allOnesIpv6} + } + return addr +} diff --git a/plugins/orchestrator/genericmanager.go b/plugins/orchestrator/genericmanager.go index e935de57f8..5efc2b17a9 100644 --- a/plugins/orchestrator/genericmanager.go +++ b/plugins/orchestrator/genericmanager.go @@ -94,6 +94,7 @@ func (s *genericManagerSvc) SetConfig(ctx context.Context, req *api.SetConfigReq if req.OverwriteAll { ctx = kvs.WithResync(ctx, kvs.FullResync, true) } + ctx = kvs.WithRetryDefault(ctx) results, err := s.dispatch.PushData(ctx, kvPairs) if err != nil { st := status.New(codes.FailedPrecondition, err.Error()) diff --git a/plugins/restapi/options.go b/plugins/restapi/options.go index c07df891aa..2152384f92 100644 --- a/plugins/restapi/options.go +++ b/plugins/restapi/options.go @@ -19,6 +19,7 @@ import ( "github.com/ligato/vpp-agent/plugins/vpp/l3plugin" "github.com/ligato/vpp-agent/plugins/govppmux" + "github.com/ligato/vpp-agent/plugins/netalloc" "github.com/ligato/vpp-agent/plugins/vpp/aclplugin" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin" "github.com/ligato/vpp-agent/plugins/vpp/l2plugin" @@ -34,6 +35,7 @@ func NewPlugin(opts ...Option) *Plugin { p.PluginName = "restpapi" p.HTTPHandlers = &rest.DefaultPlugin p.GoVppmux = &govppmux.DefaultPlugin + p.AddrAlloc = &netalloc.DefaultPlugin p.VPPACLPlugin = &aclplugin.DefaultPlugin p.VPPIfPlugin = &ifplugin.DefaultPlugin p.VPPL2Plugin = &l2plugin.DefaultPlugin diff --git a/plugins/restapi/plugin_restapi.go b/plugins/restapi/plugin_restapi.go index 74f5c392c2..aaeb3f93cc 100644 --- a/plugins/restapi/plugin_restapi.go +++ b/plugins/restapi/plugin_restapi.go @@ -28,6 +28,7 @@ import ( vpevppcalls "github.com/ligato/vpp-agent/plugins/govppmux/vppcalls" iflinuxcalls "github.com/ligato/vpp-agent/plugins/linux/ifplugin/linuxcalls" l3linuxcalls "github.com/ligato/vpp-agent/plugins/linux/l3plugin/linuxcalls" + "github.com/ligato/vpp-agent/plugins/netalloc" "github.com/ligato/vpp-agent/plugins/restapi/resturl" telemetryvppcalls "github.com/ligato/vpp-agent/plugins/telemetry/vppcalls" abfvppcalls "github.com/ligato/vpp-agent/plugins/vpp/abfplugin/vppcalls" @@ -84,6 +85,7 @@ type Deps struct { infra.PluginDeps HTTPHandlers rest.HTTPHandlers GoVppmux govppmux.StatsAPI + AddrAlloc netalloc.AddressAllocator VPPACLPlugin aclplugin.API VPPIfPlugin ifplugin.API VPPL2Plugin *l2plugin.L2Plugin @@ -134,7 +136,7 @@ func (p *Plugin) Init() (err error) { if p.l2Handler == nil { p.Log.Info("VPP L2 handler is not available, it will be skipped") } - p.l3Handler = l3vppcalls.CompatibleL3VppHandler(p.vppChan, ifIndexes, vrfIndexes, p.Log) + p.l3Handler = l3vppcalls.CompatibleL3VppHandler(p.vppChan, ifIndexes, vrfIndexes, p.AddrAlloc, p.Log) if p.l3Handler == nil { p.Log.Info("VPP L3 handler is not available, it will be skipped") } diff --git a/plugins/vpp/ifplugin/descriptor/dhcp.go b/plugins/vpp/ifplugin/descriptor/dhcp.go index 4f7703f1f2..ec2845be0d 100644 --- a/plugins/vpp/ifplugin/descriptor/dhcp.go +++ b/plugins/vpp/ifplugin/descriptor/dhcp.go @@ -24,6 +24,7 @@ import ( "github.com/ligato/cn-infra/logging" "github.com/pkg/errors" + "github.com/ligato/vpp-agent/api/models/netalloc" interfaces "github.com/ligato/vpp-agent/api/models/vpp/interfaces" kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/ifaceidx" @@ -202,7 +203,8 @@ func (d *DHCPDescriptor) DerivedValues(key string, dhcpData proto.Message) (derV if ok && dhcpLease.HostIpAddress != "" { return []kvs.KeyValuePair{ { - Key: interfaces.InterfaceAddressKey(dhcpLease.InterfaceName, dhcpLease.HostIpAddress, true), + Key: interfaces.InterfaceAddressKey(dhcpLease.InterfaceName, dhcpLease.HostIpAddress, + netalloc.IPAddressSource_FROM_DHCP), Value: &prototypes.Empty{}, }, } diff --git a/plugins/vpp/ifplugin/descriptor/interface.go b/plugins/vpp/ifplugin/descriptor/interface.go index 4bd9d259da..94e37dd14a 100644 --- a/plugins/vpp/ifplugin/descriptor/interface.go +++ b/plugins/vpp/ifplugin/descriptor/interface.go @@ -28,12 +28,15 @@ import ( linux_intf "github.com/ligato/vpp-agent/api/models/linux/interfaces" linux_ns "github.com/ligato/vpp-agent/api/models/linux/namespace" + netalloc_api "github.com/ligato/vpp-agent/api/models/netalloc" interfaces "github.com/ligato/vpp-agent/api/models/vpp/interfaces" l3 "github.com/ligato/vpp-agent/api/models/vpp/l3" kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" linux_ifdescriptor "github.com/ligato/vpp-agent/plugins/linux/ifplugin/descriptor" linux_ifaceidx "github.com/ligato/vpp-agent/plugins/linux/ifplugin/ifaceidx" "github.com/ligato/vpp-agent/plugins/linux/nsplugin" + "github.com/ligato/vpp-agent/plugins/netalloc" + netalloc_descr "github.com/ligato/vpp-agent/plugins/netalloc/descriptor" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/descriptor/adapter" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/ifaceidx" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/vppcalls" @@ -128,6 +131,7 @@ type InterfaceDescriptor struct { // dependencies log logging.Logger ifHandler vppcalls.InterfaceVppAPI + addrAlloc netalloc.AddressAllocator // optional dependencies, provide if AFPacket and/or TAP+TAP_TO_VPP interfaces are used linuxIfPlugin LinuxPluginAPI @@ -157,13 +161,14 @@ type NetlinkAPI interface { } // NewInterfaceDescriptor creates a new instance of the Interface descriptor. -func NewInterfaceDescriptor(ifHandler vppcalls.InterfaceVppAPI, defaultMtu uint32, - linuxIfHandler NetlinkAPI, linuxIfPlugin LinuxPluginAPI, nsPlugin nsplugin.API, +func NewInterfaceDescriptor(ifHandler vppcalls.InterfaceVppAPI, addrAlloc netalloc.AddressAllocator, + defaultMtu uint32, linuxIfHandler NetlinkAPI, linuxIfPlugin LinuxPluginAPI, nsPlugin nsplugin.API, log logging.PluginLogger) (descr *kvs.KVDescriptor, ctx *InterfaceDescriptor) { // descriptor context ctx = &InterfaceDescriptor{ ifHandler: ifHandler, + addrAlloc: addrAlloc, defaultMtu: defaultMtu, linuxIfPlugin: linuxIfPlugin, linuxIfHandler: linuxIfHandler, @@ -192,8 +197,11 @@ func NewInterfaceDescriptor(ifHandler vppcalls.InterfaceVppAPI, defaultMtu uint3 Retrieve: ctx.Retrieve, Dependencies: ctx.Dependencies, DerivedValues: ctx.DerivedValues, - // If Linux-IfPlugin is loaded, dump it first. - RetrieveDependencies: []string{linux_ifdescriptor.InterfaceDescriptorName}, + RetrieveDependencies: []string{ + // refresh the pool of allocated IP addresses first + netalloc_descr.IPAllocDescriptorName, + // If Linux-IfPlugin is loaded, dump it first. + linux_ifdescriptor.InterfaceDescriptorName}, } descr = adapter.NewInterfaceDescriptor(typedDescr) return @@ -523,8 +531,14 @@ func (d *InterfaceDescriptor) Dependencies(key string, intf *interfaces.Interfac AnyOf: kvs.AnyOfDependency{ KeyPrefixes: []string{interfaces.InterfaceAddressPrefix(vxlanMulticast)}, KeySelector: func(key string) bool { - _, ifaceAddr, _, _, _, _ := interfaces.ParseInterfaceAddressKey(key) - return ifaceAddr != nil && ifaceAddr.IsMulticast() + _, ifaceAddr, source, _, _ := interfaces.ParseInterfaceAddressKey(key) + if source != netalloc_api.IPAddressSource_ALLOC_REF { + ip, _, err := net.ParseCIDR(ifaceAddr) + return err == nil && ip.IsMulticast() + } + // TODO: handle the case when multicast IP address is allocated + // via netalloc (too specific to bother until really needed) + return false }, }, }) @@ -596,7 +610,7 @@ func (d *InterfaceDescriptor) DerivedValues(key string, intf *interfaces.Interfa // IP addresses for _, ipAddr := range intf.IpAddresses { derValues = append(derValues, kvs.KeyValuePair{ - Key: interfaces.InterfaceAddressKey(intf.Name, ipAddr, false), + Key: interfaces.InterfaceAddressKey(intf.Name, ipAddr, netalloc_api.IPAddressSource_STATIC), Value: &prototypes.Empty{}, }) } @@ -783,6 +797,11 @@ func equalStringSets(set1, set2 []string) bool { // contains IPv4 and/or IPv6 type addresses func getIPAddressVersions(ipAddrs []string) (hasIPv4, hasIPv6 bool) { for _, ip := range ipAddrs { + if strings.HasPrefix(ip, netalloc_api.AllocRefPrefix) { + // TODO: figure out how to define VRF-related dependencies with netalloc'd addresses + // - for now assume it is only used with IPv4 + hasIPv4 = true + } if strings.Contains(ip, ":") { hasIPv6 = true } else { diff --git a/plugins/vpp/ifplugin/descriptor/interface_address.go b/plugins/vpp/ifplugin/descriptor/interface_address.go index 1137749d50..9eaf9544a0 100644 --- a/plugins/vpp/ifplugin/descriptor/interface_address.go +++ b/plugins/vpp/ifplugin/descriptor/interface_address.go @@ -19,8 +19,10 @@ import ( "github.com/ligato/cn-infra/logging" "github.com/pkg/errors" + netalloc_api "github.com/ligato/vpp-agent/api/models/netalloc" interfaces "github.com/ligato/vpp-agent/api/models/vpp/interfaces" kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" + "github.com/ligato/vpp-agent/plugins/netalloc" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/ifaceidx" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/vppcalls" ) @@ -39,15 +41,17 @@ type InterfaceAddressDescriptor struct { log logging.Logger ifHandler vppcalls.InterfaceVppAPI ifIndex ifaceidx.IfaceMetadataIndex + addrAlloc netalloc.AddressAllocator } // NewInterfaceAddressDescriptor creates a new instance of InterfaceAddressDescriptor. -func NewInterfaceAddressDescriptor(ifHandler vppcalls.InterfaceVppAPI, ifIndex ifaceidx.IfaceMetadataIndex, - log logging.PluginLogger) *kvs.KVDescriptor { +func NewInterfaceAddressDescriptor(ifHandler vppcalls.InterfaceVppAPI, addrAlloc netalloc.AddressAllocator, + ifIndex ifaceidx.IfaceMetadataIndex, log logging.PluginLogger) *kvs.KVDescriptor { descrCtx := &InterfaceAddressDescriptor{ ifHandler: ifHandler, ifIndex: ifIndex, + addrAlloc: addrAlloc, log: log.NewLogger("interface-address-descriptor"), } return &kvs.KVDescriptor{ @@ -61,25 +65,28 @@ func NewInterfaceAddressDescriptor(ifHandler vppcalls.InterfaceVppAPI, ifIndex i } // IsInterfaceVrfKey returns true if the key represents assignment of an IP address -// to a VPP interface. +// to a VPP interface (that needs to be applied). KVs representing addresses +// already allocated from netalloc plugin or obtained from a DHCP server are +// excluded. func (d *InterfaceAddressDescriptor) IsInterfaceAddressKey(key string) bool { - _, _, _, fromDHCP, _, isAddrKey := interfaces.ParseInterfaceAddressKey(key) - return isAddrKey && !fromDHCP + _, _, source, _, isAddrKey := interfaces.ParseInterfaceAddressKey(key) + return isAddrKey && + (source == netalloc_api.IPAddressSource_STATIC || source == netalloc_api.IPAddressSource_ALLOC_REF) } // Validate validates IP address to be assigned to an interface. func (d *InterfaceAddressDescriptor) Validate(key string, emptyVal proto.Message) (err error) { - _, _, _, _, invalidIP, _ := interfaces.ParseInterfaceAddressKey(key) - if invalidIP { - return errors.New("invalid IP address") + iface, addr, _, invalidKey, _ := interfaces.ParseInterfaceAddressKey(key) + if invalidKey { + return errors.New("invalid key") } - return nil + + return d.addrAlloc.ValidateIPAddress(addr, iface, "ip_addresses", netalloc.GwRefUnexpected) } // Create assigns IP address to an interface. func (d *InterfaceAddressDescriptor) Create(key string, emptyVal proto.Message) (metadata kvs.Metadata, err error) { - iface, ipAddr, ipAddrNet, _, _, _ := interfaces.ParseInterfaceAddressKey(key) - ipAddrNet.IP = ipAddr + iface, addr, _, _, _ := interfaces.ParseInterfaceAddressKey(key) ifMeta, found := d.ifIndex.LookupByName(iface) if !found { @@ -88,18 +95,19 @@ func (d *InterfaceAddressDescriptor) Create(key string, emptyVal proto.Message) return nil, err } - err = d.ifHandler.AddInterfaceIP(ifMeta.SwIfIndex, ipAddrNet) + ipAddr, err := d.addrAlloc.GetOrParseIPAddress(addr, iface, netalloc_api.IPAddressForm_ADDR_WITH_MASK) + if err != nil { + d.log.Error(err) + return nil, err + } + + err = d.ifHandler.AddInterfaceIP(ifMeta.SwIfIndex, ipAddr) return nil, err } // Delete unassigns IP address from an interface. func (d *InterfaceAddressDescriptor) Delete(key string, emptyVal proto.Message, metadata kvs.Metadata) (err error) { - iface, ipAddr, ipAddrNet, _, _, _ := interfaces.ParseInterfaceAddressKey(key) - ipAddrNet.IP = ipAddr - - if ipAddr.IsLinkLocalUnicast() { - return nil - } + iface, addr, _, _, _ := interfaces.ParseInterfaceAddressKey(key) ifMeta, found := d.ifIndex.LookupByName(iface) if !found { @@ -108,17 +116,35 @@ func (d *InterfaceAddressDescriptor) Delete(key string, emptyVal proto.Message, return err } - err = d.ifHandler.DelInterfaceIP(ifMeta.SwIfIndex, ipAddrNet) + ipAddr, err := d.addrAlloc.GetOrParseIPAddress(addr, iface, netalloc_api.IPAddressForm_ADDR_WITH_MASK) + if err != nil { + d.log.Error(err) + return err + } + + if ipAddr.IP.IsLinkLocalUnicast() { + return nil + } + + err = d.ifHandler.DelInterfaceIP(ifMeta.SwIfIndex, ipAddr) return err } -// Dependencies lists assignment of the interface into the VRF table as the only dependency. +// Dependencies lists assignment of the interface into the VRF table and potential +// allocation of the IP address as dependencies. func (d *InterfaceAddressDescriptor) Dependencies(key string, emptyVal proto.Message) []kvs.Dependency { - iface, _, _, _, _, _ := interfaces.ParseInterfaceAddressKey(key) - return []kvs.Dependency{{ + iface, addr, _, _, _ := interfaces.ParseInterfaceAddressKey(key) + deps := []kvs.Dependency{{ Label: interfaceInVrfDep, AnyOf: kvs.AnyOfDependency{ KeyPrefixes: []string{interfaces.InterfaceVrfKeyPrefix(iface)}, }, }} -} + + allocDep, hasAllocDep := d.addrAlloc.GetAddressAllocDep(addr, iface, "") + if hasAllocDep { + deps = append(deps, allocDep) + } + + return deps +} \ No newline at end of file diff --git a/plugins/vpp/ifplugin/descriptor/interface_crud.go b/plugins/vpp/ifplugin/descriptor/interface_crud.go index 50d7794934..16b0de3ce1 100644 --- a/plugins/vpp/ifplugin/descriptor/interface_crud.go +++ b/plugins/vpp/ifplugin/descriptor/interface_crud.go @@ -3,6 +3,7 @@ package descriptor import ( "github.com/pkg/errors" + "github.com/ligato/vpp-agent/api/models/netalloc" interfaces "github.com/ligato/vpp-agent/api/models/vpp/interfaces" "github.com/ligato/vpp-agent/pkg/models" kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" @@ -454,6 +455,11 @@ func (d *InterfaceDescriptor) Retrieve(correlate []adapter.InterfaceKVWithMetada if len(expCfg.GetRxModes()) == 0 { intf.Interface.RxModes = []*interfaces.Interface_RxMode{} } + + // correlate references to allocated IP addresses + intf.Interface.IpAddresses = d.addrAlloc.CorrelateRetrievedIPs( + expCfg.IpAddresses, intf.Interface.IpAddresses, + intf.Interface.Name, netalloc.IPAddressForm_ADDR_WITH_MASK) } // verify links between VPP and Linux side diff --git a/plugins/vpp/ifplugin/descriptor/interface_with_address.go b/plugins/vpp/ifplugin/descriptor/interface_with_address.go index ceac826f81..d44adc741c 100644 --- a/plugins/vpp/ifplugin/descriptor/interface_with_address.go +++ b/plugins/vpp/ifplugin/descriptor/interface_with_address.go @@ -34,14 +34,14 @@ const ( // InterfaceWithAddrDescriptor assigns property key-value pairs to interfaces // with at least one IP address. type InterfaceWithAddrDescriptor struct { - log logging.Logger + log logging.Logger } // NewInterfaceWithAddrDescriptor creates a new instance of InterfaceWithAddrDescriptor. func NewInterfaceWithAddrDescriptor(log logging.PluginLogger) *kvs.KVDescriptor { descrCtx := &InterfaceWithAddrDescriptor{ - log: log.NewLogger("interface-has-address-descriptor"), + log: log.NewLogger("interface-has-address-descriptor"), } return &kvs.KVDescriptor{ Name: InterfaceWithAddressDescriptorName, @@ -82,4 +82,3 @@ func (d *InterfaceWithAddrDescriptor) Dependencies(key string, emptyVal proto.Me }, } } - diff --git a/plugins/vpp/ifplugin/ifaceidx/ifaceidx.go b/plugins/vpp/ifplugin/ifaceidx/ifaceidx.go index 58c2d1da3a..d5c723a58a 100644 --- a/plugins/vpp/ifplugin/ifaceidx/ifaceidx.go +++ b/plugins/vpp/ifplugin/ifaceidx/ifaceidx.go @@ -61,7 +61,7 @@ type IfaceMetadataIndexRW interface { type IfaceMetadata struct { SwIfIndex uint32 Vrf uint32 - IPAddresses []string + IPAddresses []string // TODO: update from interfaceAddress descriptor with real IPs (not netalloc links) TAPHostIfName string /* host interface name set for the Linux-side of the TAP interface; empty for non-TAPs */ } diff --git a/plugins/vpp/ifplugin/ifplugin.go b/plugins/vpp/ifplugin/ifplugin.go index 1b69ea383d..bbdcb141a0 100644 --- a/plugins/vpp/ifplugin/ifplugin.go +++ b/plugins/vpp/ifplugin/ifplugin.go @@ -40,6 +40,7 @@ import ( kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" linux_ifcalls "github.com/ligato/vpp-agent/plugins/linux/ifplugin/linuxcalls" "github.com/ligato/vpp-agent/plugins/linux/nsplugin" + "github.com/ligato/vpp-agent/plugins/netalloc" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/descriptor" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/ifaceidx" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/vppcalls" @@ -92,6 +93,7 @@ type Deps struct { infra.PluginDeps KVScheduler kvs.KVScheduler GoVppmux govppmux.StatsAPI + AddrAlloc netalloc.AddressAllocator /* LinuxIfPlugin and NsPlugin deps are optional, but they are required if AFPacket or TAP+TAP_TO_VPP interfaces are used. */ @@ -137,7 +139,7 @@ func (p *IfPlugin) Init() (err error) { // -> base interface descriptor ifaceDescriptor, ifaceDescrCtx := descriptor.NewInterfaceDescriptor(p.ifHandler, - p.defaultMtu, p.linuxIfHandler, p.LinuxIfPlugin, p.NsPlugin, p.Log) + p.AddrAlloc, p.defaultMtu, p.linuxIfHandler, p.LinuxIfPlugin, p.NsPlugin, p.Log) err = p.KVScheduler.RegisterKVDescriptor(ifaceDescriptor) if err != nil { return err @@ -162,7 +164,7 @@ func (p *IfPlugin) Init() (err error) { rxModeDescriptor := descriptor.NewRxModeDescriptor(p.ifHandler, p.intfIndex, p.Log) rxPlacementDescriptor := descriptor.NewRxPlacementDescriptor(p.ifHandler, p.intfIndex, p.Log) - addrDescriptor := descriptor.NewInterfaceAddressDescriptor(p.ifHandler, p.intfIndex, p.Log) + addrDescriptor := descriptor.NewInterfaceAddressDescriptor(p.ifHandler, p.AddrAlloc, p.intfIndex, p.Log) unIfDescriptor := descriptor.NewUnnumberedIfDescriptor(p.ifHandler, p.intfIndex, p.Log) bondIfDescriptor, _ := descriptor.NewBondedInterfaceDescriptor(p.ifHandler, p.intfIndex, p.Log) vrfDescriptor := descriptor.NewInterfaceVrfDescriptor(p.ifHandler, p.intfIndex, p.Log) diff --git a/plugins/vpp/ifplugin/options.go b/plugins/vpp/ifplugin/options.go index eada75ef9f..0e32bdfb01 100644 --- a/plugins/vpp/ifplugin/options.go +++ b/plugins/vpp/ifplugin/options.go @@ -20,6 +20,7 @@ import ( "github.com/ligato/cn-infra/logging" "github.com/ligato/vpp-agent/plugins/govppmux" "github.com/ligato/vpp-agent/plugins/kvscheduler" + "github.com/ligato/vpp-agent/plugins/netalloc" ) // DefaultPlugin is a default instance of IfPlugin. @@ -33,6 +34,7 @@ func NewPlugin(opts ...Option) *IfPlugin { p.StatusCheck = &statuscheck.DefaultPlugin p.KVScheduler = &kvscheduler.DefaultPlugin p.GoVppmux = &govppmux.DefaultPlugin + p.AddrAlloc = &netalloc.DefaultPlugin for _, o := range opts { o(p) diff --git a/plugins/vpp/l3plugin/descriptor/route.go b/plugins/vpp/l3plugin/descriptor/route.go index b138951a08..538285493f 100644 --- a/plugins/vpp/l3plugin/descriptor/route.go +++ b/plugins/vpp/l3plugin/descriptor/route.go @@ -20,14 +20,18 @@ import ( "net" "strings" + "github.com/gogo/protobuf/proto" "github.com/pkg/errors" "github.com/ligato/cn-infra/logging" "github.com/ligato/cn-infra/utils/addrs" + netalloc_api "github.com/ligato/vpp-agent/api/models/netalloc" interfaces "github.com/ligato/vpp-agent/api/models/vpp/interfaces" l3 "github.com/ligato/vpp-agent/api/models/vpp/l3" "github.com/ligato/vpp-agent/pkg/models" kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" + "github.com/ligato/vpp-agent/plugins/netalloc" + netalloc_descr "github.com/ligato/vpp-agent/plugins/netalloc/descriptor" ifdescriptor "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/descriptor" "github.com/ligato/vpp-agent/plugins/vpp/l3plugin/descriptor/adapter" "github.com/ligato/vpp-agent/plugins/vpp/l3plugin/vppcalls" @@ -50,29 +54,35 @@ const ( type RouteDescriptor struct { log logging.Logger routeHandler vppcalls.RouteVppAPI + addrAlloc netalloc.AddressAllocator } // NewRouteDescriptor creates a new instance of the Route descriptor. func NewRouteDescriptor( - routeHandler vppcalls.RouteVppAPI, log logging.PluginLogger) *kvs.KVDescriptor { + routeHandler vppcalls.RouteVppAPI, addrAlloc netalloc.AddressAllocator, + log logging.PluginLogger) *kvs.KVDescriptor { ctx := &RouteDescriptor{ routeHandler: routeHandler, + addrAlloc: addrAlloc, log: log.NewLogger("static-route-descriptor"), } typedDescr := &adapter.RouteDescriptor{ - Name: RouteDescriptorName, - NBKeyPrefix: l3.ModelRoute.KeyPrefix(), - ValueTypeName: l3.ModelRoute.ProtoName(), - KeySelector: l3.ModelRoute.IsKeyValid, - ValueComparator: ctx.EquivalentRoutes, - Validate: ctx.Validate, - Create: ctx.Create, - Delete: ctx.Delete, - Retrieve: ctx.Retrieve, - Dependencies: ctx.Dependencies, - RetrieveDependencies: []string{ifdescriptor.InterfaceDescriptorName, VrfTableDescriptorName}, + Name: RouteDescriptorName, + NBKeyPrefix: l3.ModelRoute.KeyPrefix(), + ValueTypeName: l3.ModelRoute.ProtoName(), + KeySelector: l3.ModelRoute.IsKeyValid, + ValueComparator: ctx.EquivalentRoutes, + Validate: ctx.Validate, + Create: ctx.Create, + Delete: ctx.Delete, + Retrieve: ctx.Retrieve, + Dependencies: ctx.Dependencies, + RetrieveDependencies: []string{ + netalloc_descr.IPAllocDescriptorName, + ifdescriptor.InterfaceDescriptorName, + VrfTableDescriptorName}, } return adapter.NewRouteDescriptor(typedDescr) } @@ -103,16 +113,28 @@ func (d *RouteDescriptor) EquivalentRoutes(key string, oldRoute, newRoute *l3.Ro // Validate validates VPP static route configuration. func (d *RouteDescriptor) Validate(key string, route *l3.Route) (err error) { - // validation destination network - _, ipNet, err := net.ParseCIDR(route.DstNetwork) + // validate destination network + err = d.addrAlloc.ValidateIPAddress(route.DstNetwork, "", "dst_network", + netalloc.GWRefAllowed) if err != nil { - return kvs.NewInvalidValueError(err, "dst_network") + return err + } + + // validate next hop address (GW) + err = d.addrAlloc.ValidateIPAddress(getGwAddr(route), route.OutgoingInterface, + "gw_addr", netalloc.GWRefRequired) + if err != nil { + return err } // validate IP network implied by the IP and prefix length - if strings.ToLower(ipNet.String()) != strings.ToLower(route.DstNetwork) { - e := fmt.Errorf("DstNetwork (%s) must represent IP network (%s)", route.DstNetwork, ipNet.String()) - return kvs.NewInvalidValueError(e, "dst_network") + if !strings.HasPrefix(route.DstNetwork, netalloc_api.AllocRefPrefix) { + _, ipNet, _ := net.ParseCIDR(route.DstNetwork) + if strings.ToLower(ipNet.String()) != strings.ToLower(route.DstNetwork) { + e := fmt.Errorf("DstNetwork (%s) must represent IP network (%s)", + route.DstNetwork, ipNet.String()) + return kvs.NewInvalidValueError(e, "dst_network") + } } // TODO: validate mix of IP versions? @@ -144,6 +166,30 @@ func (d *RouteDescriptor) Delete(key string, route *l3.Route, metadata interface func (d *RouteDescriptor) Retrieve(correlate []adapter.RouteKVWithMetadata) ( retrieved []adapter.RouteKVWithMetadata, err error, ) { + // prepare expected configuration with de-referenced netalloc links + nbCfg := make(map[string]*l3.Route) + expCfg := make(map[string]*l3.Route) + for _, kv := range correlate { + dstNetwork := kv.Value.DstNetwork + parsed, err := d.addrAlloc.GetOrParseIPAddress(kv.Value.DstNetwork, + "", netalloc_api.IPAddressForm_ADDR_NET) + if err == nil { + dstNetwork = parsed.String() + } + nextHop := kv.Value.NextHopAddr + parsed, err = d.addrAlloc.GetOrParseIPAddress(getGwAddr(kv.Value), + kv.Value.OutgoingInterface, netalloc_api.IPAddressForm_ADDR_ONLY) + if err == nil { + nextHop = parsed.IP.String() + } + route := proto.Clone(kv.Value).(*l3.Route) + route.DstNetwork = dstNetwork + route.NextHopAddr = nextHop + key := models.Key(route) + expCfg[key] = route + nbCfg[key] = kv.Value + } + // Retrieve VPP route configuration routes, err := d.routeHandler.DumpRoutes() if err != nil { @@ -151,10 +197,24 @@ func (d *RouteDescriptor) Retrieve(correlate []adapter.RouteKVWithMetadata) ( } for _, route := range routes { + key := models.Key(route.Route) + value := route.Route + origin := kvs.UnknownOrigin + + // correlate with the expected configuration + if expCfg, hasExpCfg := expCfg[key]; hasExpCfg { + if d.EquivalentRoutes(key, value, expCfg) { + value = nbCfg[key] + // recreate the key in case the dest. IP or GW IP were replaced with netalloc link + key = models.Key(value) + origin = kvs.FromNB + } + } + retrieved = append(retrieved, adapter.RouteKVWithMetadata{ - Key: models.Key(route.Route), - Value: route.Route, - Origin: kvs.UnknownOrigin, + Key: key, + Value: value, + Origin: origin, }) } @@ -191,12 +251,29 @@ func (d *RouteDescriptor) Dependencies(key string, route *l3.Route) []kvs.Depend }) } + // if destination network is netalloc reference, then the address must be allocated first + allocDep, hasAllocDep := d.addrAlloc.GetAddressAllocDep(route.DstNetwork, + "", "dst_network-") + if hasAllocDep { + dependencies = append(dependencies, allocDep) + } + // if GW is netalloc reference, then the address must be allocated first + allocDep, hasAllocDep = d.addrAlloc.GetAddressAllocDep(route.NextHopAddr, + route.OutgoingInterface, "gw_addr-") + if hasAllocDep { + dependencies = append(dependencies, allocDep) + } + // TODO: perhaps check GW routability return dependencies } // equalAddrs compares two IP addresses for equality. func equalAddrs(addr1, addr2 string) bool { + if strings.HasPrefix(addr1, netalloc_api.AllocRefPrefix) || + strings.HasPrefix(addr1, netalloc_api.AllocRefPrefix) { + return addr1 == addr2 + } a1 := net.ParseIP(addr1) a2 := net.ParseIP(addr2) if a1 == nil || a2 == nil { @@ -213,12 +290,15 @@ func getGwAddr(route *l3.Route) string { return route.GetNextHopAddr() } // return zero address - _, dstIPNet, err := net.ParseCIDR(route.GetDstNetwork()) - if err != nil { - return "" - } - if dstIPNet.IP.To4() == nil { - return net.IPv6zero.String() + // - with netalloc'd destination network, just assume it is for IPv4 + if !strings.HasPrefix(route.GetDstNetwork(), netalloc_api.AllocRefPrefix) { + _, dstIPNet, err := net.ParseCIDR(route.GetDstNetwork()) + if err != nil { + return "" + } + if dstIPNet.IP.To4() == nil { + return net.IPv6zero.String() + } } return net.IPv4zero.String() } @@ -233,6 +313,10 @@ func getWeight(route *l3.Route) uint32 { // equalNetworks compares two IP networks for equality. func equalNetworks(net1, net2 string) bool { + if strings.HasPrefix(net1, netalloc_api.AllocRefPrefix) || + strings.HasPrefix(net2, netalloc_api.AllocRefPrefix) { + return net1 == net2 + } _, n1, err1 := net.ParseCIDR(net1) _, n2, err2 := net.ParseCIDR(net2) if err1 != nil || err2 != nil { diff --git a/plugins/vpp/l3plugin/l3plugin.go b/plugins/vpp/l3plugin/l3plugin.go index b1eabaaba0..57edaba029 100644 --- a/plugins/vpp/l3plugin/l3plugin.go +++ b/plugins/vpp/l3plugin/l3plugin.go @@ -34,6 +34,7 @@ import ( "github.com/ligato/vpp-agent/plugins/vpp/l3plugin/descriptor" "github.com/ligato/vpp-agent/plugins/vpp/l3plugin/vppcalls" "github.com/ligato/vpp-agent/plugins/vpp/l3plugin/vrfidx" + "github.com/ligato/vpp-agent/plugins/netalloc" _ "github.com/ligato/vpp-agent/plugins/vpp/l3plugin/vppcalls/vpp1901" _ "github.com/ligato/vpp-agent/plugins/vpp/l3plugin/vppcalls/vpp1904" @@ -65,6 +66,7 @@ type Deps struct { KVScheduler kvs.KVScheduler GoVppmux govppmux.API IfPlugin ifplugin.API + AddrAlloc netalloc.AddressAllocator StatusCheck statuscheck.PluginStatusWriter // optional } @@ -78,9 +80,10 @@ func (p *L3Plugin) Init() error { } // init handlers - p.l3Handler = vppcalls.CompatibleL3VppHandler(p.vppCh, p.IfPlugin.GetInterfaceIndex(), p.vrfIndex, p.Log) + p.l3Handler = vppcalls.CompatibleL3VppHandler(p.vppCh, p.IfPlugin.GetInterfaceIndex(), + p.vrfIndex, p.AddrAlloc, p.Log) if p.l3Handler == nil { - return errors.Errorf("could not find compatible L2VppHandler") + return errors.Errorf("could not find compatible L3VppHandler") } // init and register VRF descriptor @@ -95,11 +98,12 @@ func (p *L3Plugin) Init() error { return errors.New("missing index with VRF metadata") } - // set l3 handler again since it was nil before - p.l3Handler = vppcalls.CompatibleL3VppHandler(p.vppCh, p.IfPlugin.GetInterfaceIndex(), p.vrfIndex, p.Log) + // set l3 handler again since VRF index was nil before + p.l3Handler = vppcalls.CompatibleL3VppHandler(p.vppCh, p.IfPlugin.GetInterfaceIndex(), + p.vrfIndex, p.AddrAlloc, p.Log) // init & register descriptors - routeDescriptor := descriptor.NewRouteDescriptor(p.l3Handler, p.Log) + routeDescriptor := descriptor.NewRouteDescriptor(p.l3Handler, p.AddrAlloc, p.Log) arpDescriptor := descriptor.NewArpDescriptor(p.KVScheduler, p.l3Handler, p.Log) proxyArpDescriptor := descriptor.NewProxyArpDescriptor(p.KVScheduler, p.l3Handler, p.Log) proxyArpIfaceDescriptor := descriptor.NewProxyArpInterfaceDescriptor(p.KVScheduler, p.l3Handler, p.Log) diff --git a/plugins/vpp/l3plugin/options.go b/plugins/vpp/l3plugin/options.go index fbf6ab0971..7884c0e97f 100644 --- a/plugins/vpp/l3plugin/options.go +++ b/plugins/vpp/l3plugin/options.go @@ -20,6 +20,7 @@ import ( "github.com/ligato/vpp-agent/plugins/govppmux" "github.com/ligato/vpp-agent/plugins/kvscheduler" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin" + "github.com/ligato/vpp-agent/plugins/netalloc" ) // DefaultPlugin is a default instance of IfPlugin. @@ -34,6 +35,7 @@ func NewPlugin(opts ...Option) *L3Plugin { p.KVScheduler = &kvscheduler.DefaultPlugin p.GoVppmux = &govppmux.DefaultPlugin p.IfPlugin = &ifplugin.DefaultPlugin + p.AddrAlloc = &netalloc.DefaultPlugin for _, o := range opts { o(p) diff --git a/plugins/vpp/l3plugin/vppcalls/l3_vppcalls.go b/plugins/vpp/l3plugin/vppcalls/l3_vppcalls.go index 39dee54d6e..f2affea926 100644 --- a/plugins/vpp/l3plugin/vppcalls/l3_vppcalls.go +++ b/plugins/vpp/l3plugin/vppcalls/l3_vppcalls.go @@ -18,6 +18,7 @@ import ( govppapi "git.fd.io/govpp.git/api" "github.com/ligato/cn-infra/logging" "github.com/ligato/vpp-agent/plugins/vpp/l3plugin/vrfidx" + "github.com/ligato/vpp-agent/plugins/netalloc" l3 "github.com/ligato/vpp-agent/api/models/vpp/l3" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/ifaceidx" @@ -200,13 +201,15 @@ var Versions = map[string]HandlerVersion{} type HandlerVersion struct { Msgs []govppapi.Message - New func(govppapi.Channel, ifaceidx.IfaceMetadataIndex, vrfidx.VRFMetadataIndex, logging.Logger) L3VppAPI + New func(govppapi.Channel, ifaceidx.IfaceMetadataIndex, vrfidx.VRFMetadataIndex, + netalloc.AddressAllocator, logging.Logger) L3VppAPI } func CompatibleL3VppHandler( ch govppapi.Channel, ifIdx ifaceidx.IfaceMetadataIndex, vrfIdx vrfidx.VRFMetadataIndex, + addrAlloc netalloc.AddressAllocator, log logging.Logger, ) L3VppAPI { for ver, h := range Versions { @@ -215,7 +218,7 @@ func CompatibleL3VppHandler( continue } log.Debug("found compatible version:", ver) - return h.New(ch, ifIdx, vrfIdx, log) + return h.New(ch, ifIdx, vrfIdx, addrAlloc, log) } panic("no compatible version available") } diff --git a/plugins/vpp/l3plugin/vppcalls/vpp1901/route_dump_test.go b/plugins/vpp/l3plugin/vppcalls/vpp1901/route_dump_test.go index bcf791dc88..595d207f15 100644 --- a/plugins/vpp/l3plugin/vppcalls/vpp1901/route_dump_test.go +++ b/plugins/vpp/l3plugin/vppcalls/vpp1901/route_dump_test.go @@ -18,6 +18,7 @@ import ( "testing" "github.com/ligato/cn-infra/logging/logrus" + netallock_mock "github.com/ligato/vpp-agent/plugins/netalloc/mock" "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1901/ip" "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1901/vpe" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/ifaceidx" @@ -30,7 +31,8 @@ func TestDumpStaticRoutes(t *testing.T) { ctx := vppcallmock.SetupTestCtx(t) defer ctx.TeardownTestCtx() ifIndexes := ifaceidx.NewIfaceIndex(logrus.NewLogger("test"), "test") - l3handler := NewRouteVppHandler(ctx.MockChannel, ifIndexes, logrus.DefaultLogger()) + l3handler := NewRouteVppHandler(ctx.MockChannel, ifIndexes, netallock_mock.NewMockNetAlloc(), + logrus.DefaultLogger()) ifIndexes.Put("if1", &ifaceidx.IfaceMetadata{SwIfIndex: 1}) ifIndexes.Put("if2", &ifaceidx.IfaceMetadata{SwIfIndex: 2}) diff --git a/plugins/vpp/l3plugin/vppcalls/vpp1901/route_vppcalls.go b/plugins/vpp/l3plugin/vppcalls/vpp1901/route_vppcalls.go index 7f07e4abe4..172d68d245 100644 --- a/plugins/vpp/l3plugin/vppcalls/vpp1901/route_vppcalls.go +++ b/plugins/vpp/l3plugin/vppcalls/vpp1901/route_vppcalls.go @@ -17,8 +17,8 @@ package vpp1901 import ( "net" - "github.com/ligato/cn-infra/utils/addrs" "github.com/ligato/vpp-agent/api/models/vpp/l3" + "github.com/ligato/vpp-agent/api/models/netalloc" "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1901/ip" "github.com/pkg/errors" ) @@ -47,12 +47,25 @@ func (h *RouteHandler) vppAddDelRoute(route *vpp_l3.Route, rtIfIdx uint32, delet } // Destination address (route set identifier) - parsedDstIP, isIpv6, err := addrs.ParseIPWithPrefix(route.DstNetwork) + parsedDstIP, err := h.addrAlloc.GetOrParseIPAddress(route.DstNetwork, + "", netalloc.IPAddressForm_ADDR_NET) if err != nil { return err } - parsedNextHopIP := net.ParseIP(route.NextHopAddr) + isIpv6 := parsedDstIP.IP.To4() == nil prefix, _ := parsedDstIP.Mask.Size() + + // Next Hop IP address + var parsedNextHopIP net.IP + if route.NextHopAddr != "" { + nextHopIPNet, err := h.addrAlloc.GetOrParseIPAddress(route.NextHopAddr, + route.OutgoingInterface, netalloc.IPAddressForm_ADDR_ONLY) + if err != nil { + return err + } + parsedNextHopIP = nextHopIPNet.IP + } + if isIpv6 { req.IsIPv6 = 1 req.DstAddress = []byte(parsedDstIP.IP.To16()) diff --git a/plugins/vpp/l3plugin/vppcalls/vpp1901/route_vppcalls_test.go b/plugins/vpp/l3plugin/vppcalls/vpp1901/route_vppcalls_test.go index fd5feda368..429f46d5f9 100644 --- a/plugins/vpp/l3plugin/vppcalls/vpp1901/route_vppcalls_test.go +++ b/plugins/vpp/l3plugin/vppcalls/vpp1901/route_vppcalls_test.go @@ -19,6 +19,7 @@ import ( "github.com/ligato/cn-infra/logging/logrus" l3 "github.com/ligato/vpp-agent/api/models/vpp/l3" + netallock_mock "github.com/ligato/vpp-agent/plugins/netalloc/mock" "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1901/ip" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/ifaceidx" ifvppcalls "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/vppcalls" @@ -89,6 +90,6 @@ func routeTestSetup(t *testing.T) (*vppcallmock.TestCtx, ifvppcalls.InterfaceVpp ifIndexes.Put("iface1", &ifaceidx.IfaceMetadata{ SwIfIndex: 1, }) - rtHandler := vpp1901.NewRouteVppHandler(ctx.MockChannel, ifIndexes, log) + rtHandler := vpp1901.NewRouteVppHandler(ctx.MockChannel, ifIndexes, netallock_mock.NewMockNetAlloc(), log) return ctx, ifHandler, rtHandler } diff --git a/plugins/vpp/l3plugin/vppcalls/vpp1901/vppcalls_handlers.go b/plugins/vpp/l3plugin/vppcalls/vpp1901/vppcalls_handlers.go index 3b0283333e..3dd0b7d73e 100644 --- a/plugins/vpp/l3plugin/vppcalls/vpp1901/vppcalls_handlers.go +++ b/plugins/vpp/l3plugin/vppcalls/vpp1901/vppcalls_handlers.go @@ -23,6 +23,7 @@ import ( "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1901/dhcp" "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1901/ip" "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1901/vpe" + "github.com/ligato/vpp-agent/plugins/netalloc" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/ifaceidx" "github.com/ligato/vpp-agent/plugins/vpp/l3plugin/vppcalls" "github.com/ligato/vpp-agent/plugins/vpp/l3plugin/vrfidx" @@ -36,9 +37,9 @@ func init() { vppcalls.Versions["vpp1901"] = vppcalls.HandlerVersion{ Msgs: msgs, - New: func(ch govppapi.Channel, ifIdx ifaceidx.IfaceMetadataIndex, vrfIdx vrfidx.VRFMetadataIndex, log logging.Logger, - ) vppcalls.L3VppAPI { - return NewL3VppHandler(ch, ifIdx, log) + New: func(ch govppapi.Channel, ifIdx ifaceidx.IfaceMetadataIndex, + vrfIdx vrfidx.VRFMetadataIndex, addrAlloc netalloc.AddressAllocator, log logging.Logger) vppcalls.L3VppAPI { + return NewL3VppHandler(ch, ifIdx, addrAlloc, log) }, } } @@ -53,12 +54,12 @@ type L3VppHandler struct { } func NewL3VppHandler( - ch govppapi.Channel, ifIdx ifaceidx.IfaceMetadataIndex, log logging.Logger, + ch govppapi.Channel, ifIdx ifaceidx.IfaceMetadataIndex, addrAlloc netalloc.AddressAllocator, log logging.Logger, ) *L3VppHandler { return &L3VppHandler{ ArpVppHandler: NewArpVppHandler(ch, ifIdx, log), ProxyArpVppHandler: NewProxyArpVppHandler(ch, ifIdx, log), - RouteHandler: NewRouteVppHandler(ch, ifIdx, log), + RouteHandler: NewRouteVppHandler(ch, ifIdx, addrAlloc, log), IPNeighHandler: NewIPNeighVppHandler(ch, log), VrfTableHandler: NewVrfTableVppHandler(ch, log), DHCPProxyHandler: NewDHCPProxyHandler(ch, log), @@ -83,6 +84,7 @@ type ProxyArpVppHandler struct { type RouteHandler struct { callsChannel govppapi.Channel ifIndexes ifaceidx.IfaceMetadataIndex + addrAlloc netalloc.AddressAllocator log logging.Logger } @@ -141,13 +143,15 @@ func NewProxyArpVppHandler(callsChan govppapi.Channel, ifIndexes ifaceidx.IfaceM } // NewRouteVppHandler creates new instance of route vppcalls handler -func NewRouteVppHandler(callsChan govppapi.Channel, ifIndexes ifaceidx.IfaceMetadataIndex, log logging.Logger) *RouteHandler { +func NewRouteVppHandler(callsChan govppapi.Channel, ifIndexes ifaceidx.IfaceMetadataIndex, + addrAlloc netalloc.AddressAllocator, log logging.Logger) *RouteHandler { if log == nil { log = logrus.NewLogger("route-handler") } return &RouteHandler{ callsChannel: callsChan, ifIndexes: ifIndexes, + addrAlloc: addrAlloc, log: log, } } diff --git a/plugins/vpp/l3plugin/vppcalls/vpp1904/route_dump_test.go b/plugins/vpp/l3plugin/vppcalls/vpp1904/route_dump_test.go index 3de48d55c7..ebc8c27ff6 100644 --- a/plugins/vpp/l3plugin/vppcalls/vpp1904/route_dump_test.go +++ b/plugins/vpp/l3plugin/vppcalls/vpp1904/route_dump_test.go @@ -18,6 +18,7 @@ import ( "testing" "github.com/ligato/cn-infra/logging/logrus" + netallock_mock "github.com/ligato/vpp-agent/plugins/netalloc/mock" "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1904/ip" "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1904/vpe" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/ifaceidx" @@ -30,7 +31,8 @@ func TestDumpStaticRoutes(t *testing.T) { ctx := vppcallmock.SetupTestCtx(t) defer ctx.TeardownTestCtx() ifIndexes := ifaceidx.NewIfaceIndex(logrus.NewLogger("test"), "test") - l3handler := NewRouteVppHandler(ctx.MockChannel, ifIndexes, logrus.DefaultLogger()) + l3handler := NewRouteVppHandler(ctx.MockChannel, ifIndexes, netallock_mock.NewMockNetAlloc(), + logrus.DefaultLogger()) ifIndexes.Put("if1", &ifaceidx.IfaceMetadata{SwIfIndex: 1}) ifIndexes.Put("if2", &ifaceidx.IfaceMetadata{SwIfIndex: 2}) diff --git a/plugins/vpp/l3plugin/vppcalls/vpp1904/route_vppcalls.go b/plugins/vpp/l3plugin/vppcalls/vpp1904/route_vppcalls.go index 6efc8c94e4..9d769e961e 100644 --- a/plugins/vpp/l3plugin/vppcalls/vpp1904/route_vppcalls.go +++ b/plugins/vpp/l3plugin/vppcalls/vpp1904/route_vppcalls.go @@ -17,8 +17,8 @@ package vpp1904 import ( "net" - "github.com/ligato/cn-infra/utils/addrs" - vpp_l3 "github.com/ligato/vpp-agent/api/models/vpp/l3" + "github.com/ligato/vpp-agent/api/models/netalloc" + "github.com/ligato/vpp-agent/api/models/vpp/l3" "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1904/ip" "github.com/pkg/errors" ) @@ -47,12 +47,25 @@ func (h *RouteHandler) vppAddDelRoute(route *vpp_l3.Route, rtIfIdx uint32, delet } // Destination address (route set identifier) - parsedDstIP, isIpv6, err := addrs.ParseIPWithPrefix(route.DstNetwork) + parsedDstIP, err := h.addrAlloc.GetOrParseIPAddress(route.DstNetwork, + "", netalloc.IPAddressForm_ADDR_NET) if err != nil { return err } - parsedNextHopIP := net.ParseIP(route.NextHopAddr) + isIpv6 := parsedDstIP.IP.To4() == nil prefix, _ := parsedDstIP.Mask.Size() + + // Next Hop IP address + var parsedNextHopIP net.IP + if route.NextHopAddr != "" { + nextHopIPNet, err := h.addrAlloc.GetOrParseIPAddress(route.NextHopAddr, + route.OutgoingInterface, netalloc.IPAddressForm_ADDR_ONLY) + if err != nil { + return err + } + parsedNextHopIP = nextHopIPNet.IP + } + if isIpv6 { req.IsIPv6 = 1 req.DstAddress = []byte(parsedDstIP.IP.To16()) @@ -124,4 +137,4 @@ func (h *RouteHandler) getRouteSwIfIndex(ifName string) (swIfIdx uint32, err err swIfIdx = meta.SwIfIndex } return -} \ No newline at end of file +} diff --git a/plugins/vpp/l3plugin/vppcalls/vpp1904/route_vppcalls_test.go b/plugins/vpp/l3plugin/vppcalls/vpp1904/route_vppcalls_test.go index eb0c0ae756..0856ef031f 100644 --- a/plugins/vpp/l3plugin/vppcalls/vpp1904/route_vppcalls_test.go +++ b/plugins/vpp/l3plugin/vppcalls/vpp1904/route_vppcalls_test.go @@ -23,6 +23,7 @@ import ( "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/ifaceidx" ifvppcalls "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/vppcalls" ifvpp1904 "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/vppcalls/vpp1904" + netallock_mock "github.com/ligato/vpp-agent/plugins/netalloc/mock" "github.com/ligato/vpp-agent/plugins/vpp/l3plugin/vppcalls" "github.com/ligato/vpp-agent/plugins/vpp/l3plugin/vppcalls/vpp1904" "github.com/ligato/vpp-agent/plugins/vpp/vppcallmock" @@ -89,6 +90,6 @@ func routeTestSetup(t *testing.T) (*vppcallmock.TestCtx, ifvppcalls.InterfaceVpp ifIndexes.Put("iface1", &ifaceidx.IfaceMetadata{ SwIfIndex: 1, }) - rtHandler := vpp1904.NewRouteVppHandler(ctx.MockChannel, ifIndexes, log) + rtHandler := vpp1904.NewRouteVppHandler(ctx.MockChannel, ifIndexes, netallock_mock.NewMockNetAlloc(), log) return ctx, ifHandler, rtHandler } diff --git a/plugins/vpp/l3plugin/vppcalls/vpp1904/vppcalls_handlers.go b/plugins/vpp/l3plugin/vppcalls/vpp1904/vppcalls_handlers.go index e80698a8a4..38293c1c0b 100644 --- a/plugins/vpp/l3plugin/vppcalls/vpp1904/vppcalls_handlers.go +++ b/plugins/vpp/l3plugin/vppcalls/vpp1904/vppcalls_handlers.go @@ -28,6 +28,7 @@ import ( vpevppcalls "github.com/ligato/vpp-agent/plugins/govppmux/vppcalls" "github.com/ligato/vpp-agent/plugins/govppmux/vppcalls/vpp1904" + "github.com/ligato/vpp-agent/plugins/netalloc" "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1904/ip" "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1904/vpe" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/ifaceidx" @@ -42,9 +43,10 @@ func init() { vppcalls.Versions["vpp1904"] = vppcalls.HandlerVersion{ Msgs: msgs, - New: func(ch govppapi.Channel, ifIdx ifaceidx.IfaceMetadataIndex, vrfIdx vrfidx.VRFMetadataIndex, log logging.Logger, + New: func(ch govppapi.Channel, ifIdx ifaceidx.IfaceMetadataIndex, vrfIdx vrfidx.VRFMetadataIndex, + addrAlloc netalloc.AddressAllocator, log logging.Logger, ) vppcalls.L3VppAPI { - return NewL3VppHandler(ch, ifIdx, log) + return NewL3VppHandler(ch, ifIdx, addrAlloc, log) }, } } @@ -59,12 +61,12 @@ type L3VppHandler struct { } func NewL3VppHandler( - ch govppapi.Channel, ifIdx ifaceidx.IfaceMetadataIndex, log logging.Logger, + ch govppapi.Channel, ifIdx ifaceidx.IfaceMetadataIndex, addrAlloc netalloc.AddressAllocator, log logging.Logger, ) *L3VppHandler { return &L3VppHandler{ ArpVppHandler: NewArpVppHandler(ch, ifIdx, log), ProxyArpVppHandler: NewProxyArpVppHandler(ch, ifIdx, log), - RouteHandler: NewRouteVppHandler(ch, ifIdx, log), + RouteHandler: NewRouteVppHandler(ch, ifIdx, addrAlloc, log), IPNeighHandler: NewIPNeighVppHandler(ch, log), VrfTableHandler: NewVrfTableVppHandler(ch, log), DHCPProxyHandler: NewDHCPProxyHandler(ch, log), @@ -95,6 +97,7 @@ type ProxyArpVppHandler struct { type RouteHandler struct { callsChannel govppapi.Channel ifIndexes ifaceidx.IfaceMetadataIndex + addrAlloc netalloc.AddressAllocator log logging.Logger } @@ -136,13 +139,15 @@ func NewProxyArpVppHandler(callsChan govppapi.Channel, ifIndexes ifaceidx.IfaceM } // NewRouteVppHandler creates new instance of route vppcalls handler -func NewRouteVppHandler(callsChan govppapi.Channel, ifIndexes ifaceidx.IfaceMetadataIndex, log logging.Logger) *RouteHandler { +func NewRouteVppHandler(callsChan govppapi.Channel, ifIndexes ifaceidx.IfaceMetadataIndex, + addrAlloc netalloc.AddressAllocator, log logging.Logger) *RouteHandler { if log == nil { log = logrus.NewLogger("route-handler") } return &RouteHandler{ callsChannel: callsChan, ifIndexes: ifIndexes, + addrAlloc: addrAlloc, log: log, } } diff --git a/plugins/vpp/l3plugin/vppcalls/vpp1908/route_dump_test.go b/plugins/vpp/l3plugin/vppcalls/vpp1908/route_dump_test.go index 0b9923d1b0..f0e80433d0 100644 --- a/plugins/vpp/l3plugin/vppcalls/vpp1908/route_dump_test.go +++ b/plugins/vpp/l3plugin/vppcalls/vpp1908/route_dump_test.go @@ -21,6 +21,7 @@ import ( "github.com/ligato/vpp-agent/plugins/vpp/l3plugin/vrfidx" "github.com/ligato/cn-infra/logging/logrus" + netallock_mock "github.com/ligato/vpp-agent/plugins/netalloc/mock" "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1908/ip" "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1908/vpe" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/ifaceidx" @@ -34,7 +35,8 @@ func TestDumpStaticRoutes(t *testing.T) { defer ctx.TeardownTestCtx() ifIndexes := ifaceidx.NewIfaceIndex(logrus.NewLogger("test-if"), "test-if") vrfIndexes := vrfidx.NewVRFIndex(logrus.NewLogger("test-vrf"), "test-vrf") - l3handler := NewRouteVppHandler(ctx.MockChannel, ifIndexes, vrfIndexes, logrus.DefaultLogger()) + l3handler := NewRouteVppHandler(ctx.MockChannel, ifIndexes, vrfIndexes, netallock_mock.NewMockNetAlloc(), + logrus.DefaultLogger()) vrfIndexes.Put("vrf1-ipv4", &vrfidx.VRFMetadata{Index: 0, Protocol: vpp_l3.VrfTable_IPV4}) vrfIndexes.Put("vrf1-ipv6", &vrfidx.VRFMetadata{Index: 0, Protocol: vpp_l3.VrfTable_IPV6}) diff --git a/plugins/vpp/l3plugin/vppcalls/vpp1908/route_vppcalls.go b/plugins/vpp/l3plugin/vppcalls/vpp1908/route_vppcalls.go index 860e9ae7e4..e539598dd4 100644 --- a/plugins/vpp/l3plugin/vppcalls/vpp1908/route_vppcalls.go +++ b/plugins/vpp/l3plugin/vppcalls/vpp1908/route_vppcalls.go @@ -17,6 +17,7 @@ package vpp1908 import ( "net" + "github.com/ligato/vpp-agent/api/models/netalloc" vpp_l3 "github.com/ligato/vpp-agent/api/models/vpp/l3" "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1908/ip" "github.com/pkg/errors" @@ -53,7 +54,14 @@ func (h *RouteHandler) vppAddDelRoute(route *vpp_l3.Route, rtIfIdx uint32, delet Weight: uint8(route.Weight), Preference: uint8(route.Preference), } - fibPath.Nh, fibPath.Proto = setFibPathNhAndProto(route.NextHopAddr) + if route.NextHopAddr != "" { + nextHop, err := h.addrAlloc.GetOrParseIPAddress(route.NextHopAddr, + route.OutgoingInterface, netalloc.IPAddressForm_ADDR_ONLY) + if err != nil { + return err + } + fibPath.Nh, fibPath.Proto = setFibPathNhAndProto(nextHop.IP) + } // VRF/Other route parameters based on type if route.Type == vpp_l3.Route_INTER_VRF { @@ -66,10 +74,12 @@ func (h *RouteHandler) vppAddDelRoute(route *vpp_l3.Route, rtIfIdx uint32, delet fibPath.TableID = route.VrfId } // Destination address - prefix, err := networkToPrefix(route.DstNetwork) + dstNet, err := h.addrAlloc.GetOrParseIPAddress(route.DstNetwork, + "", netalloc.IPAddressForm_ADDR_NET) if err != nil { return err } + prefix := networkToPrefix(dstNet) req.Route = ip.IPRoute{ TableID: route.VrfId, @@ -107,18 +117,14 @@ func (h *RouteHandler) VppDelRoute(route *vpp_l3.Route) error { return h.vppAddDelRoute(route, swIfIdx, true) } -func setFibPathNhAndProto(ipStr string) (nh ip.FibPathNh, proto ip.FibPathNhProto) { - netIP := net.ParseIP(ipStr) - if netIP == nil { - return - } +func setFibPathNhAndProto(netIP net.IP) (nh ip.FibPathNh, proto ip.FibPathNhProto) { var ipData [16]byte if netIP.To4() == nil { proto = ip.FIB_API_PATH_NH_PROTO_IP6 copy(ipData[:], netIP[:]) } else { proto = ip.FIB_API_PATH_NH_PROTO_IP4 - copy(ipData[:], netIP[12:]) + copy(ipData[:], netIP.To4()[:]) } return ip.FibPathNh{ Address: ip.AddressUnion{ diff --git a/plugins/vpp/l3plugin/vppcalls/vpp1908/route_vppcalls_test.go b/plugins/vpp/l3plugin/vppcalls/vpp1908/route_vppcalls_test.go index a53dd180de..53f6e3b228 100644 --- a/plugins/vpp/l3plugin/vppcalls/vpp1908/route_vppcalls_test.go +++ b/plugins/vpp/l3plugin/vppcalls/vpp1908/route_vppcalls_test.go @@ -21,6 +21,7 @@ import ( "github.com/ligato/cn-infra/logging/logrus" l3 "github.com/ligato/vpp-agent/api/models/vpp/l3" + netallock_mock "github.com/ligato/vpp-agent/plugins/netalloc/mock" "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1908/ip" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/ifaceidx" ifvppcalls "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/vppcalls" @@ -92,6 +93,6 @@ func routeTestSetup(t *testing.T) (*vppcallmock.TestCtx, ifvppcalls.InterfaceVpp ifIndexes.Put("iface1", &ifaceidx.IfaceMetadata{ SwIfIndex: 1, }) - rtHandler := vpp1908.NewRouteVppHandler(ctx.MockChannel, ifIndexes, vrfIndexes, log) + rtHandler := vpp1908.NewRouteVppHandler(ctx.MockChannel, ifIndexes, vrfIndexes, netallock_mock.NewMockNetAlloc(), log) return ctx, ifHandler, rtHandler } diff --git a/plugins/vpp/l3plugin/vppcalls/vpp1908/vppcalls_handlers.go b/plugins/vpp/l3plugin/vppcalls/vpp1908/vppcalls_handlers.go index 33fa9964d9..14c2d066f0 100644 --- a/plugins/vpp/l3plugin/vppcalls/vpp1908/vppcalls_handlers.go +++ b/plugins/vpp/l3plugin/vppcalls/vpp1908/vppcalls_handlers.go @@ -20,8 +20,6 @@ import ( "github.com/ligato/vpp-agent/plugins/vpp/l3plugin/vrfidx" - "github.com/ligato/cn-infra/utils/addrs" - "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1908/dhcp" govppapi "git.fd.io/govpp.git/api" @@ -30,6 +28,7 @@ import ( vpevppcalls "github.com/ligato/vpp-agent/plugins/govppmux/vppcalls" "github.com/ligato/vpp-agent/plugins/govppmux/vppcalls/vpp1908" + "github.com/ligato/vpp-agent/plugins/netalloc" "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1908/ip" "github.com/ligato/vpp-agent/plugins/vpp/binapi/vpp1908/vpe" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/ifaceidx" @@ -44,9 +43,10 @@ func init() { vppcalls.Versions["vpp1908"] = vppcalls.HandlerVersion{ Msgs: msgs, - New: func(ch govppapi.Channel, ifIdx ifaceidx.IfaceMetadataIndex, vrfIdx vrfidx.VRFMetadataIndex, log logging.Logger, + New: func(ch govppapi.Channel, ifIdx ifaceidx.IfaceMetadataIndex, + vrfIdx vrfidx.VRFMetadataIndex, addrAlloc netalloc.AddressAllocator, log logging.Logger, ) vppcalls.L3VppAPI { - return NewL3VppHandler(ch, ifIdx, vrfIdx, log) + return NewL3VppHandler(ch, ifIdx, vrfIdx, addrAlloc, log) }, } } @@ -61,12 +61,13 @@ type L3VppHandler struct { } func NewL3VppHandler( - ch govppapi.Channel, ifIdx ifaceidx.IfaceMetadataIndex, vrfIdx vrfidx.VRFMetadataIndex, log logging.Logger, + ch govppapi.Channel, ifIdx ifaceidx.IfaceMetadataIndex, vrfIdx vrfidx.VRFMetadataIndex, + addrAlloc netalloc.AddressAllocator, log logging.Logger, ) *L3VppHandler { return &L3VppHandler{ ArpVppHandler: NewArpVppHandler(ch, ifIdx, log), ProxyArpVppHandler: NewProxyArpVppHandler(ch, ifIdx, log), - RouteHandler: NewRouteVppHandler(ch, ifIdx, vrfIdx, log), + RouteHandler: NewRouteVppHandler(ch, ifIdx, vrfIdx, addrAlloc, log), IPNeighHandler: NewIPNeighVppHandler(ch, log), VrfTableHandler: NewVrfTableVppHandler(ch, log), DHCPProxyHandler: NewDHCPProxyHandler(ch, log), @@ -98,6 +99,7 @@ type RouteHandler struct { callsChannel govppapi.Channel ifIndexes ifaceidx.IfaceMetadataIndex vrfIndexes vrfidx.VRFMetadataIndex + addrAlloc netalloc.AddressAllocator log logging.Logger } @@ -139,7 +141,8 @@ func NewProxyArpVppHandler(callsChan govppapi.Channel, ifIndexes ifaceidx.IfaceM } // NewRouteVppHandler creates new instance of route vppcalls handler -func NewRouteVppHandler(callsChan govppapi.Channel, ifIndexes ifaceidx.IfaceMetadataIndex, vrfIdx vrfidx.VRFMetadataIndex, log logging.Logger) *RouteHandler { +func NewRouteVppHandler(callsChan govppapi.Channel, ifIndexes ifaceidx.IfaceMetadataIndex, + vrfIdx vrfidx.VRFMetadataIndex, addrAlloc netalloc.AddressAllocator, log logging.Logger) *RouteHandler { if log == nil { log = logrus.NewLogger("route-handler") } @@ -147,6 +150,7 @@ func NewRouteVppHandler(callsChan govppapi.Channel, ifIndexes ifaceidx.IfaceMeta callsChannel: callsChan, ifIndexes: ifIndexes, vrfIndexes: vrfIdx, + addrAlloc: addrAlloc, log: log, } } @@ -204,28 +208,24 @@ func ipToAddress(ipstr string) (addr ip.Address, err error) { return } -func networkToPrefix(dstNetwork string) (ip.Prefix, error) { - netIP, isIPv6, err := addrs.ParseIPWithPrefix(dstNetwork) - if err != nil { - return ip.Prefix{}, err - } +func networkToPrefix(dstNetwork *net.IPNet) ip.Prefix { var addr ip.Address - if isIPv6 { + if dstNetwork.IP.To4() == nil { addr.Af = ip.ADDRESS_IP6 var ip6addr ip.IP6Address - copy(ip6addr[:], netIP.IP.To16()) + copy(ip6addr[:], dstNetwork.IP.To16()) addr.Un.SetIP6(ip6addr) } else { addr.Af = ip.ADDRESS_IP4 var ip4addr ip.IP4Address - copy(ip4addr[:], netIP.IP.To4()) + copy(ip4addr[:], dstNetwork.IP.To4()) addr.Un.SetIP4(ip4addr) } - mask, _ := netIP.Mask.Size() + mask, _ := dstNetwork.Mask.Size() return ip.Prefix{ Address: addr, Len: uint8(mask), - }, nil + } } func uintToBool(value uint8) bool { diff --git a/plugins/vpp/srplugin/descriptor/localsid.go b/plugins/vpp/srplugin/descriptor/localsid.go index fd255e1a7b..bf3dc2db2c 100644 --- a/plugins/vpp/srplugin/descriptor/localsid.go +++ b/plugins/vpp/srplugin/descriptor/localsid.go @@ -15,7 +15,6 @@ package descriptor import ( - "fmt" "net" "reflect" "strings" @@ -296,11 +295,10 @@ func ParseIPv4(str string) (net.IP, error) { } func isRouteDstIpv6(key string) (bool, error) { - _, dstNetAddr, dstNetMask, _, isRouteKey := vpp_l3.ParseRouteKey(key) + _, _, dstNet, _, isRouteKey := vpp_l3.ParseRouteKey(key) if !isRouteKey { return false, errors.Errorf("Key %v is not route key", key) } - dstNet := fmt.Sprintf("%s/%d", dstNetAddr, dstNetMask) _, isIPv6, err := addrs.ParseIPWithPrefix(dstNet) return isIPv6, err } diff --git a/tests/e2e/000_initial_test.go b/tests/e2e/000_initial_test.go new file mode 100644 index 0000000000..060f037ee9 --- /dev/null +++ b/tests/e2e/000_initial_test.go @@ -0,0 +1,46 @@ +// Copyright (c) 2019 Cisco and/or its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2e + +import ( + "testing" + + . "github.com/onsi/gomega" + + ns "github.com/ligato/vpp-agent/api/models/linux/namespace" + kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" +) + +func TestAgentInSync(t *testing.T) { + ctx := setupE2E(t) + defer ctx.teardownE2E() + Expect(ctx.agentInSync()).To(BeTrue()) +} + +func TestStartStopMicroservice(t *testing.T) { + ctx := setupE2E(t) + defer ctx.teardownE2E() + + const msName = "microservice1" + key := ns.MicroserviceKey(msNamePrefix + msName) + msState := func() kvs.ValueState { + return ctx.getValueStateByKey(key) + } + + ctx.startMicroservice(msName) + Eventually(msState, msUpdateTimeout).Should(Equal(kvs.ValueState_OBTAINED)) + ctx.stopMicroservice(msName) + Eventually(msState, msUpdateTimeout).Should(Equal(kvs.ValueState_NONEXISTENT)) +} \ No newline at end of file diff --git a/tests/e2e/010_interfaces_test.go b/tests/e2e/010_interfaces_test.go new file mode 100644 index 0000000000..d74579fefc --- /dev/null +++ b/tests/e2e/010_interfaces_test.go @@ -0,0 +1,284 @@ +// Copyright (c) 2019 Cisco and/or its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2e + +import ( + "context" + "testing" + "time" + + . "github.com/onsi/gomega" + + "github.com/ligato/vpp-agent/api/models/linux/interfaces" + "github.com/ligato/vpp-agent/api/models/linux/namespace" + "github.com/ligato/vpp-agent/api/models/vpp/interfaces" + kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" +) + +// TODO: running downstream resync in-between restarts/re-creates seems to break stuff (for now commented out) + +const ( + msUpdateTimeout = time.Second * 6 + recreateTimeout = time.Second * 6 +) + +// connect VPP with a microservice via TAP interface +func TestTapInterfaceConn(t *testing.T) { + ctx := setupE2E(t) + defer ctx.teardownE2E() + + const ( + vppTapName = "vpp-tap" + linuxTapName = "linux-tap" + linuxTapHostname = "tap" + vppTapIP = "192.168.1.1" + linuxTapIP = "192.168.1.2" + netMask = "/30" + msName = "microservice1" + ) + + vppTap := &vpp_interfaces.Interface{ + Name: vppTapName, + Type: vpp_interfaces.Interface_TAP, + Enabled: true, + IpAddresses: []string{vppTapIP + netMask}, + Link: &vpp_interfaces.Interface_Tap{ + Tap: &vpp_interfaces.TapLink{ + Version: 2, + ToMicroservice: msNamePrefix + msName, + }, + }, + } + linuxTap := &linux_interfaces.Interface{ + Name: linuxTapName, + Type: linux_interfaces.Interface_TAP_TO_VPP, + Enabled: true, + IpAddresses: []string{linuxTapIP + netMask}, + HostIfName: linuxTapHostname, + Link: &linux_interfaces.Interface_Tap{ + Tap: &linux_interfaces.TapLink{ + VppTapIfName: vppTapName, + }, + }, + Namespace: &linux_namespace.NetNamespace{ + Type: linux_namespace.NetNamespace_MICROSERVICE, + Reference: msNamePrefix + msName, + }, + } + + ctx.startMicroservice(msName) + req := ctx.grpcClient.ChangeRequest() + err := req.Update( + vppTap, + linuxTap, + ).Send(context.Background()) + Expect(err).To(BeNil()) + + Eventually(ctx.getValueStateClb(vppTap), msUpdateTimeout).Should(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(linuxTap)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.pingFromVPP(linuxTapIP)).To(BeNil()) + Expect(ctx.pingFromMs(msName, vppTapIP)).To(BeNil()) + + // restart microservice twice + for i := 0; i < 2; i++ { + ctx.stopMicroservice(msName) + Eventually(ctx.getValueStateClb(vppTap), msUpdateTimeout).Should(Equal(kvs.ValueState_PENDING)) + Eventually(ctx.getValueStateClb(linuxTap), msUpdateTimeout).Should(Equal(kvs.ValueState_PENDING)) + Expect(ctx.pingFromVPP(linuxTapIP)).ToNot(BeNil()) + + //Expect(ctx.agentInSync()).To(BeTrue()) + + ctx.startMicroservice(msName) + Eventually(ctx.getValueStateClb(vppTap), msUpdateTimeout).Should(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(linuxTap)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.pingFromVPP(linuxTapIP)).To(BeNil()) + Expect(ctx.pingFromMs(msName, vppTapIP)).To(BeNil()) + + //Expect(ctx.agentInSync()).To(BeTrue()) + } + + // re-create VPP TAP + req = ctx.grpcClient.ChangeRequest() + err = req.Delete( + vppTap, + ).Send(context.Background()) + Expect(err).To(BeNil()) + + Expect(ctx.pingFromVPP(linuxTapIP)).ToNot(BeNil()) + Expect(ctx.pingFromMs(msName, vppTapIP)).ToNot(BeNil()) + + req = ctx.grpcClient.ChangeRequest() + err = req.Update( + vppTap, + ).Send(context.Background()) + Expect(err).To(BeNil()) + + Eventually(ctx.pingFromVPPClb(linuxTapIP), recreateTimeout).Should(BeNil()) + Expect(ctx.pingFromMs(msName, vppTapIP)).To(BeNil()) + //Expect(ctx.agentInSync()).To(BeTrue()) + + // re-create Linux TAP + req = ctx.grpcClient.ChangeRequest() + err = req.Delete( + linuxTap, + ).Send(context.Background()) + Expect(err).To(BeNil()) + + Expect(ctx.pingFromVPP(linuxTapIP)).ToNot(BeNil()) + Expect(ctx.pingFromMs(msName, vppTapIP)).ToNot(BeNil()) + + req = ctx.grpcClient.ChangeRequest() + err = req.Update( + linuxTap, + ).Send(context.Background()) + Expect(err).To(BeNil()) + + Eventually(ctx.pingFromVPPClb(linuxTapIP), recreateTimeout).Should(BeNil()) + Expect(ctx.pingFromMs(msName, vppTapIP)).To(BeNil()) + //Expect(ctx.agentInSync()).To(BeTrue()) +} + +// connect VPP with a microservice via AF-PACKET + VETH interfaces +func TestAfPacketInterfaceConn(t *testing.T) { + ctx := setupE2E(t) + defer ctx.teardownE2E() + + const ( + afPacketName = "vpp-afpacket" + veth1Name = "linux-veth1" + veth2Name = "linux-veth2" + veth1Hostname = "veth1" + veth2Hostname = "veth2" + afPacketIP = "192.168.1.1" + veth2IP = "192.168.1.2" + netMask = "/30" + msName = "microservice1" + ) + + afPacket := &vpp_interfaces.Interface{ + Name: afPacketName, + Type: vpp_interfaces.Interface_AF_PACKET, + Enabled: true, + IpAddresses: []string{afPacketIP + netMask}, + Link: &vpp_interfaces.Interface_Afpacket{ + Afpacket: &vpp_interfaces.AfpacketLink{ + HostIfName: veth1Hostname, + }, + }, + } + veth1 := &linux_interfaces.Interface{ + Name: veth1Name, + Type: linux_interfaces.Interface_VETH, + Enabled: true, + HostIfName: veth1Hostname, + Link: &linux_interfaces.Interface_Veth{ + Veth: &linux_interfaces.VethLink{ + PeerIfName: veth2Name, + }, + }, + } + veth2 := &linux_interfaces.Interface{ + Name: veth2Name, + Type: linux_interfaces.Interface_VETH, + Enabled: true, + HostIfName: veth2Hostname, + IpAddresses: []string{veth2IP + netMask}, + Link: &linux_interfaces.Interface_Veth{ + Veth: &linux_interfaces.VethLink{ + PeerIfName: veth1Name, + }, + }, + Namespace: &linux_namespace.NetNamespace{ + Type: linux_namespace.NetNamespace_MICROSERVICE, + Reference: msNamePrefix + msName, + }, + } + + ctx.startMicroservice(msName) + req := ctx.grpcClient.ChangeRequest() + err := req.Update( + afPacket, + veth1, + veth2, + ).Send(context.Background()) + Expect(err).To(BeNil()) + + Eventually(ctx.getValueStateClb(afPacket), msUpdateTimeout).Should(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(veth1)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(veth2)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.pingFromVPP(veth2IP)).To(BeNil()) + Expect(ctx.pingFromMs(msName, afPacketIP)).To(BeNil()) + + // restart microservice twice + for i := 0; i < 2; i++ { + ctx.stopMicroservice(msName) + Eventually(ctx.getValueStateClb(afPacket), msUpdateTimeout).Should(Equal(kvs.ValueState_PENDING)) + Eventually(ctx.getValueStateClb(veth1), msUpdateTimeout).Should(Equal(kvs.ValueState_PENDING)) + Eventually(ctx.getValueStateClb(veth2), msUpdateTimeout).Should(Equal(kvs.ValueState_PENDING)) + Expect(ctx.pingFromVPP(veth2IP)).ToNot(BeNil()) + + //Expect(ctx.agentInSync()).To(BeTrue()) + + ctx.startMicroservice(msName) + Eventually(ctx.getValueStateClb(afPacket), msUpdateTimeout).Should(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(veth1)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(veth2)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.pingFromVPP(veth2IP)).To(BeNil()) + Expect(ctx.pingFromMs(msName, afPacketIP)).To(BeNil()) + + //Expect(ctx.agentInSync()).To(BeTrue()) + } + + // re-create AF-PACKET + req = ctx.grpcClient.ChangeRequest() + err = req.Delete( + afPacket, + ).Send(context.Background()) + Expect(err).To(BeNil()) + + Expect(ctx.pingFromVPP(veth2IP)).ToNot(BeNil()) + Expect(ctx.pingFromMs(msName, afPacketIP)).ToNot(BeNil()) + + req = ctx.grpcClient.ChangeRequest() + err = req.Update( + afPacket, + ).Send(context.Background()) + Expect(err).To(BeNil()) + + Eventually(ctx.pingFromVPPClb(veth2IP), recreateTimeout).Should(BeNil()) + Expect(ctx.pingFromMs(msName, afPacketIP)).To(BeNil()) + //Expect(ctx.agentInSync()).To(BeTrue()) + + // re-create VETH + req = ctx.grpcClient.ChangeRequest() + err = req.Delete( + veth2, + ).Send(context.Background()) + Expect(err).To(BeNil()) + + Expect(ctx.pingFromVPP(veth2IP)).ToNot(BeNil()) + Expect(ctx.pingFromMs(msName, afPacketIP)).ToNot(BeNil()) + + req = ctx.grpcClient.ChangeRequest() + err = req.Update( + veth2, + ).Send(context.Background()) + Expect(err).To(BeNil()) + + Eventually(ctx.pingFromVPPClb(veth2IP), recreateTimeout).Should(BeNil()) + Expect(ctx.pingFromMs(msName, afPacketIP)).To(BeNil()) + //Expect(ctx.agentInSync()).To(BeTrue()) +} + diff --git a/tests/e2e/011_interface_link_only_test.go b/tests/e2e/011_interface_link_only_test.go new file mode 100644 index 0000000000..a400e74102 --- /dev/null +++ b/tests/e2e/011_interface_link_only_test.go @@ -0,0 +1,132 @@ +// Copyright (c) 2019 Cisco and/or its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2e + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + + "github.com/ligato/vpp-agent/api/models/linux/interfaces" + "github.com/ligato/vpp-agent/api/models/linux/namespace" + "github.com/ligato/vpp-agent/api/models/vpp/interfaces" + kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" + "github.com/ligato/vpp-agent/plugins/linux/ifplugin/linuxcalls" + "github.com/ligato/vpp-agent/plugins/netalloc/utils" +) + +// configure only link on the Linux side of the interface and leave addresses +// untouched during resync. +func TestLinkOnly(t *testing.T) { + ctx := setupE2E(t) + defer ctx.teardownE2E() + + const ( + vppTapName = "vpp-tap" + linuxTapName = "linux-tap" + linuxTapHostname = "tap" + vppTapIP = "192.168.1.1" + linuxTapIPIgnored = "192.168.1.2" + linuxTapIPExternal = "192.168.1.3" + linuxTapHwIgnored = "22:22:22:33:33:33" + linuxTapHwExternal = "44:44:44:55:55:55" + netMask = "/24" + msName = "microservice1" + ) + + vppTap := &vpp_interfaces.Interface{ + Name: vppTapName, + Type: vpp_interfaces.Interface_TAP, + Enabled: true, + IpAddresses: []string{vppTapIP + netMask}, + Link: &vpp_interfaces.Interface_Tap{ + Tap: &vpp_interfaces.TapLink{ + Version: 2, + ToMicroservice: msNamePrefix + msName, + }, + }, + } + linuxTap := &linux_interfaces.Interface{ + Name: linuxTapName, + LinkOnly: true, // <--- link only + Type: linux_interfaces.Interface_TAP_TO_VPP, + Enabled: true, + IpAddresses: []string{linuxTapIPIgnored + netMask}, + HostIfName: linuxTapHostname, + PhysAddress: linuxTapHwIgnored, + Link: &linux_interfaces.Interface_Tap{ + Tap: &linux_interfaces.TapLink{ + VppTapIfName: vppTapName, + }, + }, + Namespace: &linux_namespace.NetNamespace{ + Type: linux_namespace.NetNamespace_MICROSERVICE, + Reference: msNamePrefix + msName, + }, + } + + ms := ctx.startMicroservice(msName) + req := ctx.grpcClient.ChangeRequest() + err := req.Update( + vppTap, + linuxTap, + ).Send(context.Background()) + Expect(err).To(BeNil()) + + Eventually(ctx.getValueStateClb(vppTap), msUpdateTimeout).Should(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(linuxTap)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.pingFromVPP(linuxTapIPIgnored)).ToNot(BeNil()) // IP address was not set + + ifHandler := linuxcalls.NewNetLinkHandler() + hasIP := func(ipAddr string) bool { + addrs, err := ifHandler.GetAddressList(linuxTapHostname) + Expect(err).To(BeNil()) + for _, addr := range addrs { + if addr.IP.String() == ipAddr { + return true + } + } + return false + } + + leaveMs := ms.enterNetNs() + // agent didn't set IP address + Expect(hasIP(linuxTapIPIgnored)).To(BeFalse()) + + // set IP and MAC addresses from outside of the agent + ipAddr, _, err := utils.ParseIPAddr(linuxTapIPExternal+netMask, nil) + Expect(err).To(BeNil()) + err = ifHandler.AddInterfaceIP(linuxTapHostname, ipAddr) + Expect(err).To(BeNil()) + err = ifHandler.SetInterfaceMac(linuxTapHostname, linuxTapHwExternal) + Expect(err).To(BeNil()) + leaveMs() + + // run downstream resync + Expect(ctx.agentInSync()).To(BeTrue()) // everything in-sync even though the IP addr was added + leaveMs = ms.enterNetNs() + Expect(hasIP(linuxTapIPIgnored)).To(BeFalse()) + Expect(hasIP(linuxTapIPExternal)).To(BeTrue()) + link, err := ifHandler.GetLinkByName(linuxTapHostname) + Expect(err).To(BeNil()) + Expect(link).ToNot(BeNil()) + Expect(link.Attrs().HardwareAddr.String()).To(Equal(linuxTapHwExternal)) + leaveMs() + + // test with ping + Expect(ctx.pingFromVPP(linuxTapIPExternal)).To(BeNil()) + Expect(ctx.pingFromMs(msName, vppTapIP)).To(BeNil()) +} diff --git a/tests/e2e/020_netalloc_test.go b/tests/e2e/020_netalloc_test.go new file mode 100644 index 0000000000..1ef3b169a3 --- /dev/null +++ b/tests/e2e/020_netalloc_test.go @@ -0,0 +1,638 @@ +// Copyright (c) 2019 Cisco and/or its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2e + +import ( + "context" + "os" + "strings" + "testing" + + . "github.com/onsi/gomega" + + "github.com/ligato/vpp-agent/api/models/linux/interfaces" + "github.com/ligato/vpp-agent/api/models/linux/l3" + "github.com/ligato/vpp-agent/api/models/linux/namespace" + "github.com/ligato/vpp-agent/api/models/netalloc" + "github.com/ligato/vpp-agent/api/models/vpp/interfaces" + "github.com/ligato/vpp-agent/api/models/vpp/l3" + kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" +) + +// test IP address allocation using the netalloc plugin for VPP+Linux interfaces, +// Linux routes and Linux ARPs in a topology where the interface is a neighbour +// of the associated GW. +// +// topology + addressing: +// VPP loop (192.168.10.1/24) <--> VPP tap (192.168.11.1/24) <--> Linux tap (192.168.11.2/24) +// topology + addressing AFTER CHANGE: +// VPP loop (192.168.20.1/24) <--> VPP tap (192.168.12.1/24) <--> Linux tap (192.168.12.2/24) +func TestIPWithNeighGW(t *testing.T) { + ctx := setupE2E(t) + defer ctx.teardownE2E() + + const ( + networkName = "net1" + vppLoopName = "vpp-loop" + vppTapName = "vpp-tap" + linuxTapName = "linux-tap" + linuxTapHostname = "tap" + vppLoopIP = "192.168.10.1" + vppLoopIP2 = "192.168.20.1" + vppTapIP = "192.168.11.1" + vppTapIP2 = "192.168.12.1" + vppTapHw = "aa:aa:aa:bb:bb:bb" + linuxTapIP = "192.168.11.2" + linuxTapIP2 = "192.168.12.2" + linuxTapHw = "cc:cc:cc:dd:dd:dd" + netMask = "/24" + msName = "microservice1" + ) + + // ------- addresses: + + vppLoopAddr := &netalloc.IPAllocation{ + NetworkName: networkName, + InterfaceName: vppLoopName, + Address: vppLoopIP + netMask, + } + + vppTapAddr := &netalloc.IPAllocation{ + NetworkName: networkName, + InterfaceName: vppTapName, + Address: vppTapIP + netMask, + Gw: linuxTapIP, + } + + linuxTapAddr := &netalloc.IPAllocation{ + NetworkName: networkName, + InterfaceName: linuxTapName, + Address: linuxTapIP + netMask, + Gw: vppTapIP, + } + + // ------- network items: + + vppLoop := &vpp_interfaces.Interface{ + Name: vppLoopName, + Type: vpp_interfaces.Interface_SOFTWARE_LOOPBACK, + Enabled: true, + IpAddresses: []string{"alloc:" + networkName}, + } + + vppTap := &vpp_interfaces.Interface{ + Name: vppTapName, + Type: vpp_interfaces.Interface_TAP, + Enabled: true, + IpAddresses: []string{"alloc:" + networkName}, + PhysAddress: vppTapHw, + Link: &vpp_interfaces.Interface_Tap{ + Tap: &vpp_interfaces.TapLink{ + Version: 2, + ToMicroservice: msNamePrefix + msName, + }, + }, + } + + linuxTap := &linux_interfaces.Interface{ + Name: linuxTapName, + Type: linux_interfaces.Interface_TAP_TO_VPP, + Enabled: true, + IpAddresses: []string{"alloc:" + networkName}, + HostIfName: linuxTapHostname, + PhysAddress: linuxTapHw, + Link: &linux_interfaces.Interface_Tap{ + Tap: &linux_interfaces.TapLink{ + VppTapIfName: vppTapName, + }, + }, + Namespace: &linux_namespace.NetNamespace{ + Type: linux_namespace.NetNamespace_MICROSERVICE, + Reference: msNamePrefix + msName, + }, + } + + linuxArp := &linux_l3.ARPEntry{ + Interface: linuxTapName, + IpAddress: "alloc:" + networkName + "/" + vppTapName, + HwAddress: vppTapHw, + } + + linuxRoute := &linux_l3.Route{ + OutgoingInterface: linuxTapName, + Scope: linux_l3.Route_GLOBAL, + DstNetwork: "alloc:" + networkName + "/" + vppLoopName, + GwAddr: "alloc:" + networkName + "/GW", + } + + ctx.startMicroservice(msName) + req := ctx.grpcClient.ChangeRequest() + err := req.Update( + vppLoopAddr, vppTapAddr, linuxTapAddr, + vppLoop, vppTap, linuxTap, + linuxArp, linuxRoute, + ).Send(context.Background()) + Expect(err).To(BeNil()) + + checkItemsAreConfigured := func(msRestart, withLoopAddr bool) { + // configured immediately: + if withLoopAddr { + Expect(ctx.getValueState(vppLoopAddr)).To(Equal(kvs.ValueState_CONFIGURED)) + } + Expect(ctx.getValueState(vppTapAddr)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(linuxTapAddr)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(vppLoop)).To(Equal(kvs.ValueState_CONFIGURED)) + // the rest depends on the microservice + if msRestart { + Eventually(ctx.getValueStateClb(vppTap), msUpdateTimeout).Should(Equal(kvs.ValueState_CONFIGURED)) + } else { + Expect(ctx.getValueState(vppTap)).To(Equal(kvs.ValueState_CONFIGURED)) + } + Expect(ctx.getValueState(linuxTap)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(linuxArp)).To(Equal(kvs.ValueState_CONFIGURED)) + if withLoopAddr { + Expect(ctx.getValueState(linuxRoute)).To(Equal(kvs.ValueState_CONFIGURED)) + } else { + Expect(ctx.getValueState(linuxRoute)).To(Equal(kvs.ValueState_PENDING)) + } + } + checkItemsAreConfigured(true, true) + + // check connection with ping + Expect(ctx.pingFromVPP(linuxTapIP)).To(BeNil()) + Expect(ctx.pingFromMs(msName, vppLoopIP)).To(BeNil()) + Expect(ctx.agentInSync()).To(BeTrue()) + + // restart microservice + ctx.stopMicroservice(msName) + ctx.startMicroservice(msName) + checkItemsAreConfigured(true, true) + + // check connection with ping (few packets will get lost before tables are refreshed) + ctx.pingFromVPP(linuxTapIP) + Expect(ctx.pingFromVPP(linuxTapIP)).To(BeNil()) + Expect(ctx.pingFromMs(msName, vppLoopIP)).To(BeNil()) + Expect(ctx.agentInSync()).To(BeTrue()) + + // change IP addresses - the network items should be re-created + vppLoopAddr.Address = vppLoopIP2 + netMask + vppTapAddr.Address = vppTapIP2 + netMask + vppTapAddr.Gw = linuxTapIP2 + linuxTapAddr.Address = linuxTapIP2 + netMask + linuxTapAddr.Gw = vppTapIP2 + + req = ctx.grpcClient.ChangeRequest() + err = req.Update( + vppLoopAddr, vppTapAddr, linuxTapAddr, + ).Send(context.Background()) + Expect(err).To(BeNil()) + checkItemsAreConfigured(false, true) + + // check connection with ping + Expect(ctx.pingFromVPP(linuxTapIP)).ToNot(BeNil()) + + Expect(ctx.pingFromMs(msName, vppLoopIP)).ToNot(BeNil()) + Expect(ctx.pingFromVPP(linuxTapIP2)).To(BeNil()) + Expect(ctx.pingFromMs(msName, vppLoopIP2)).To(BeNil()) + Expect(ctx.agentInSync()).To(BeTrue()) + + // de-allocate loopback IP - the connection should not work anymore + req = ctx.grpcClient.ChangeRequest() + err = req.Delete( + vppLoopAddr, + ).Send(context.Background()) + Expect(err).To(BeNil()) + + // loopback is still created but without IP and route is pending + checkItemsAreConfigured(false, false) + + // can ping linux TAP from VPP, but cannot ping loopback + Expect(ctx.pingFromVPP(linuxTapIP2)).To(BeNil()) + Expect(ctx.pingFromMs(msName, vppLoopIP2)).ToNot(BeNil()) + + // TODO: not in-sync - the list of IP addresses is updated in the metadata + // - we need to figure out how to get rid of this and how to solve VRF-related + // dependencies with netalloc'd IP addresses + //Expect(ctx.agentInSync()).To(BeTrue()) +} + +// test IP address allocation using the netalloc plugin for VPP+Linux interfaces, +// Linux routes and Linux ARPs in a topology where the interface is NOT a neighbour +// of the associated GW. +// +// topology + addressing (note the single-host network mask for Linux TAP): +// VPP loop (192.168.10.1/24) <--> VPP tap (192.168.11.1/24) <--> Linux tap (192.168.11.2/32) +// topology + addressing AFTER CHANGE: +// VPP loop (192.168.20.1/24) <--> VPP tap (192.168.12.1/24) <--> Linux tap (192.168.12.2/32) +func TestIPWithNonLocalGW(t *testing.T) { + ctx := setupE2E(t) + defer ctx.teardownE2E() + + const ( + networkName = "net1" + vppLoopName = "vpp-loop" + vppTapName = "vpp-tap" + linuxTapName = "linux-tap" + linuxTapHostname = "tap" + vppLoopIP = "192.168.10.1" + vppLoopIP2 = "192.168.20.1" + vppTapIP = "192.168.11.1" + vppTapIP2 = "192.168.12.1" + vppTapHw = "aa:aa:aa:bb:bb:bb" + linuxTapIP = "192.168.11.2" + linuxTapIP2 = "192.168.12.2" + linuxTapHw = "cc:cc:cc:dd:dd:dd" + vppNetMask = "/24" + linuxNetMask = "/32" + msName = "microservice1" + ) + + // ------- addresses: + + vppLoopAddr := &netalloc.IPAllocation{ + NetworkName: networkName, + InterfaceName: vppLoopName, + Address: vppLoopIP + vppNetMask, + } + + vppTapAddr := &netalloc.IPAllocation{ + NetworkName: networkName, + InterfaceName: vppTapName, + Address: vppTapIP + vppNetMask, + Gw: linuxTapIP, + } + + linuxTapAddr := &netalloc.IPAllocation{ + NetworkName: networkName, + InterfaceName: linuxTapName, + Address: linuxTapIP + linuxNetMask, + Gw: vppTapIP, + } + + // ------- network items: + + vppLoop := &vpp_interfaces.Interface{ + Name: vppLoopName, + Type: vpp_interfaces.Interface_SOFTWARE_LOOPBACK, + Enabled: true, + IpAddresses: []string{"alloc:" + networkName}, + } + + vppTap := &vpp_interfaces.Interface{ + Name: vppTapName, + Type: vpp_interfaces.Interface_TAP, + Enabled: true, + IpAddresses: []string{"alloc:" + networkName}, + PhysAddress: vppTapHw, + Link: &vpp_interfaces.Interface_Tap{ + Tap: &vpp_interfaces.TapLink{ + Version: 2, + ToMicroservice: msNamePrefix + msName, + }, + }, + } + + linuxTap := &linux_interfaces.Interface{ + Name: linuxTapName, + Type: linux_interfaces.Interface_TAP_TO_VPP, + Enabled: true, + IpAddresses: []string{"alloc:" + networkName}, + HostIfName: linuxTapHostname, + PhysAddress: linuxTapHw, + Link: &linux_interfaces.Interface_Tap{ + Tap: &linux_interfaces.TapLink{ + VppTapIfName: vppTapName, + }, + }, + Namespace: &linux_namespace.NetNamespace{ + Type: linux_namespace.NetNamespace_MICROSERVICE, + Reference: msNamePrefix + msName, + }, + } + + linuxArp := &linux_l3.ARPEntry{ + Interface: linuxTapName, + IpAddress: "alloc:" + networkName + "/" + vppTapName, + HwAddress: vppTapHw, + } + + linuxRoute := &linux_l3.Route{ + OutgoingInterface: linuxTapName, + Scope: linux_l3.Route_GLOBAL, + DstNetwork: "alloc:" + networkName + "/" + vppLoopName, + GwAddr: "alloc:" + networkName + "/GW", + } + + // link route is necessary to route the GW of the linux TAP interface + linuxLinkRoute := &linux_l3.Route{ + OutgoingInterface: linuxTapName, + Scope: linux_l3.Route_LINK, + DstNetwork: "alloc:" + networkName + "/" + linuxTapName + "/GW", + } + + ctx.startMicroservice(msName) + req := ctx.grpcClient.ChangeRequest() + err := req.Update( + vppLoopAddr, vppTapAddr, linuxTapAddr, + vppLoop, vppTap, linuxTap, + linuxArp, linuxRoute, linuxLinkRoute, + ).Send(context.Background()) + Expect(err).To(BeNil()) + + checkItemsAreConfigured := func(msRestart, withLinkRoute bool) { + // configured immediately: + Expect(ctx.getValueState(vppLoopAddr)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(vppTapAddr)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(linuxTapAddr)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(vppLoop)).To(Equal(kvs.ValueState_CONFIGURED)) + // the rest depends on the microservice + if msRestart { + Eventually(ctx.getValueStateClb(vppTap), msUpdateTimeout).Should(Equal(kvs.ValueState_CONFIGURED)) + } else { + Expect(ctx.getValueState(vppTap)).To(Equal(kvs.ValueState_CONFIGURED)) + } + Expect(ctx.getValueState(linuxTap)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(linuxArp)).To(Equal(kvs.ValueState_CONFIGURED)) + if withLinkRoute { + Expect(ctx.getValueState(linuxRoute)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(linuxLinkRoute)).To(Equal(kvs.ValueState_CONFIGURED)) + } else { + Expect(ctx.getValueState(linuxRoute)).To(Equal(kvs.ValueState_PENDING)) + } + } + checkItemsAreConfigured(true, true) + + // check connection with ping + Expect(ctx.pingFromVPP(linuxTapIP)).To(BeNil()) + Expect(ctx.pingFromMs(msName, vppLoopIP)).To(BeNil()) + Expect(ctx.agentInSync()).To(BeTrue()) + + // restart microservice + ctx.stopMicroservice(msName) + ctx.startMicroservice(msName) + checkItemsAreConfigured(true, true) + + // check connection with ping (few packets will get lost before tables are refreshed) + ctx.pingFromVPP(linuxTapIP) + Expect(ctx.pingFromVPP(linuxTapIP)).To(BeNil()) + Expect(ctx.pingFromMs(msName, vppLoopIP)).To(BeNil()) + Expect(ctx.agentInSync()).To(BeTrue()) + + // change IP addresses - the network items should be re-created + vppLoopAddr.Address = vppLoopIP2 + vppNetMask + vppTapAddr.Address = vppTapIP2 + vppNetMask + vppTapAddr.Gw = linuxTapIP2 + linuxTapAddr.Address = linuxTapIP2 + linuxNetMask + linuxTapAddr.Gw = vppTapIP2 + + req = ctx.grpcClient.ChangeRequest() + err = req.Update( + vppLoopAddr, vppTapAddr, linuxTapAddr, + ).Send(context.Background()) + Expect(err).To(BeNil()) + checkItemsAreConfigured(false, true) + + // check connection with ping + Expect(ctx.pingFromVPP(linuxTapIP)).ToNot(BeNil()) + + Expect(ctx.pingFromMs(msName, vppLoopIP)).ToNot(BeNil()) + Expect(ctx.pingFromVPP(linuxTapIP2)).To(BeNil()) + Expect(ctx.pingFromMs(msName, vppLoopIP2)).To(BeNil()) + Expect(ctx.agentInSync()).To(BeTrue()) + + // remove link route - this should make the GW for linux TAP non-routable + req = ctx.grpcClient.ChangeRequest() + err = req.Delete( + linuxLinkRoute, + ).Send(context.Background()) + Expect(err).To(BeNil()) + + // the route to VPP is pending + checkItemsAreConfigured(false, false) + + // cannot ping anymore from any of the sides + Expect(ctx.pingFromVPP(linuxTapIP2)).ToNot(BeNil()) + Expect(ctx.pingFromMs(msName, vppLoopIP2)).ToNot(BeNil()) + Expect(ctx.agentInSync()).To(BeTrue()) +} + +// test IP address allocation using the netalloc plugin for VPP routes mainly. +// +// topology + addressing: +// VPP tap (192.168.11.1/24) <--> Linux tap (192.168.11.2/24) <--> Linux loop (192.168.20.1/24, 10.10.10.10/32) +// +// topology + addressing AFTER CHANGE: +// VPP tap (192.168.12.1/24) <--> Linux tap (192.168.12.2/24) <--> Linux loop (192.168.30.1/24, 10.10.10.10/32) +func TestVPPRoutesWithNetalloc(t *testing.T) { + ctx := setupE2E(t) + defer ctx.teardownE2E() + + const ( + network1Name = "net1" + network2Name = "net2" + vppTapName = "vpp-tap" + linuxTapName = "linux-tap" + linuxTapHostname = "tap" + linuxLoopName = "linux-loop" + vppTapIP = "192.168.11.1" + vppTapIP2 = "192.168.12.1" + linuxTapIP = "192.168.11.2" + linuxTapIP2 = "192.168.12.2" + linuxLoopNet1IP = "192.168.20.1" + linuxLoopNet1IP2 = "192.168.30.1" + linuxLoopNet2IP = "10.10.10.10" + net1Mask = "/24" + net2Mask = "/32" + msName = "microservice1" + ) + + // ------- addresses: + + vppTapAddr := &netalloc.IPAllocation{ + NetworkName: network1Name, + InterfaceName: vppTapName, + Address: vppTapIP + net1Mask, + Gw: linuxTapIP, + } + + linuxTapAddr := &netalloc.IPAllocation{ + NetworkName: network1Name, + InterfaceName: linuxTapName, + Address: linuxTapIP + net1Mask, + Gw: vppTapIP, + } + + linuxLoopNet1Addr := &netalloc.IPAllocation{ + NetworkName: network1Name, + InterfaceName: linuxLoopName, + Address: linuxLoopNet1IP + net1Mask, + } + + linuxLoopNet2Addr := &netalloc.IPAllocation{ + NetworkName: network2Name, + InterfaceName: linuxLoopName, + Address: linuxLoopNet2IP + net2Mask, + } + + // ------- network items: + + vppTap := &vpp_interfaces.Interface{ + Name: vppTapName, + Type: vpp_interfaces.Interface_TAP, + Enabled: true, + IpAddresses: []string{"alloc:" + network1Name}, + Link: &vpp_interfaces.Interface_Tap{ + Tap: &vpp_interfaces.TapLink{ + Version: 2, + ToMicroservice: msNamePrefix + msName, + }, + }, + } + + linuxTap := &linux_interfaces.Interface{ + Name: linuxTapName, + Type: linux_interfaces.Interface_TAP_TO_VPP, + Enabled: true, + IpAddresses: []string{"alloc:" + network1Name}, + HostIfName: linuxTapHostname, + Link: &linux_interfaces.Interface_Tap{ + Tap: &linux_interfaces.TapLink{ + VppTapIfName: vppTapName, + }, + }, + Namespace: &linux_namespace.NetNamespace{ + Type: linux_namespace.NetNamespace_MICROSERVICE, + Reference: msNamePrefix + msName, + }, + } + + linuxLoop := &linux_interfaces.Interface{ + Name: linuxLoopName, + Type: linux_interfaces.Interface_LOOPBACK, + Enabled: true, + IpAddresses: []string{ + "127.0.0.1/8", "alloc:" + network1Name, "alloc:" + network2Name}, + Namespace: &linux_namespace.NetNamespace{ + Type: linux_namespace.NetNamespace_MICROSERVICE, + Reference: msNamePrefix + msName, + }, + } + + vppRouteLoopNet1 := &vpp_l3.Route{ + OutgoingInterface: vppTapName, + DstNetwork: "alloc:" + network1Name + "/" + linuxLoopName, + NextHopAddr: "alloc:" + network1Name + "/GW", + } + + vppRouteLoopNet2 := &vpp_l3.Route{ + OutgoingInterface: vppTapName, + DstNetwork: "alloc:" + network2Name + "/" + linuxLoopName, + NextHopAddr: "alloc:" + network1Name + "/GW", + } + + ctx.startMicroservice(msName) + req := ctx.grpcClient.ChangeRequest() + err := req.Update( + vppTapAddr, linuxTapAddr, linuxLoopNet1Addr, linuxLoopNet2Addr, + vppTap, linuxTap, linuxLoop, + vppRouteLoopNet1, vppRouteLoopNet2, + ).Send(context.Background()) + Expect(err).To(BeNil()) + + checkItemsAreConfigured := func(msRestart, withLoopNet2Addr bool) { + // configured immediately: + if withLoopNet2Addr { + Expect(ctx.getValueState(linuxLoopNet2Addr)).To(Equal(kvs.ValueState_CONFIGURED)) + } + Expect(ctx.getValueState(vppTapAddr)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(linuxTapAddr)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(linuxLoopNet1Addr)).To(Equal(kvs.ValueState_CONFIGURED)) + // the rest depends on the microservice + if msRestart { + Eventually(ctx.getValueStateClb(vppTap), msUpdateTimeout).Should(Equal(kvs.ValueState_CONFIGURED)) + } else { + Expect(ctx.getValueState(vppTap)).To(Equal(kvs.ValueState_CONFIGURED)) + } + Expect(ctx.getValueState(linuxTap)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(linuxLoop)).To(Equal(kvs.ValueState_CONFIGURED)) + Expect(ctx.getValueState(vppRouteLoopNet1)).To(Equal(kvs.ValueState_CONFIGURED)) + if withLoopNet2Addr { + Expect(ctx.getValueState(vppRouteLoopNet2)).To(Equal(kvs.ValueState_CONFIGURED)) + } else { + Expect(ctx.getValueState(vppRouteLoopNet2)).To(Equal(kvs.ValueState_PENDING)) + } + } + checkItemsAreConfigured(true, true) + + // check connection with ping + Expect(ctx.pingFromVPP(linuxLoopNet1IP)).To(BeNil()) + Expect(ctx.pingFromVPP(linuxLoopNet2IP)).To(BeNil()) + Expect(ctx.agentInSync()).To(BeTrue()) + + if strings.HasSuffix(os.Getenv("VPP_IMG"), "19.08") { + // TODO: we cannot handle microservice restarts in 19.08 since the TAP interface + // attached to the stopped microservice is automatically removed, but not the + // associated routes and it is not possible to remove/update them + // Waiting for reaction from VPP dev... + return + } + + // restart microservice + ctx.stopMicroservice(msName) + ctx.startMicroservice(msName) + checkItemsAreConfigured(true, true) + + // check connection with ping (few packets will get lost before tables are refreshed) + ctx.pingFromVPP(linuxLoopNet1IP) + ctx.pingFromVPP(linuxLoopNet2IP) + Expect(ctx.pingFromVPP(linuxLoopNet1IP)).To(BeNil()) + Expect(ctx.pingFromVPP(linuxLoopNet2IP)).To(BeNil()) + Expect(ctx.agentInSync()).To(BeTrue()) + + // change IP addresses - the network items should be re-created + linuxLoopNet1Addr.Address = linuxLoopNet1IP2 + net1Mask + vppTapAddr.Address = vppTapIP2 + net1Mask + vppTapAddr.Gw = linuxTapIP2 + linuxTapAddr.Address = linuxTapIP2 + net1Mask + linuxTapAddr.Gw = vppTapIP2 + + req = ctx.grpcClient.ChangeRequest() + err = req.Update( + linuxLoopNet1Addr, vppTapAddr, linuxTapAddr, + ).Send(context.Background()) + Expect(err).To(BeNil()) + checkItemsAreConfigured(false, true) + + // check connection with ping + Expect(ctx.pingFromVPP(linuxLoopNet1IP)).ToNot(BeNil()) + Expect(ctx.pingFromVPP(linuxLoopNet1IP2)).To(BeNil()) + Expect(ctx.pingFromVPP(linuxLoopNet2IP)).To(BeNil()) + Expect(ctx.agentInSync()).To(BeTrue()) + + // de-allocate loopback IP in net2 - the connection to that IP should not work anymore + req = ctx.grpcClient.ChangeRequest() + err = req.Delete( + linuxLoopNet2Addr, + ).Send(context.Background()) + Expect(err).To(BeNil()) + + // loopback is still created but without IP and route is pending + checkItemsAreConfigured(false, false) + + // can ping loop1, but cannot ping loop2 + Expect(ctx.pingFromVPP(linuxLoopNet1IP2)).To(BeNil()) + Expect(ctx.pingFromVPP(linuxLoopNet2IP)).ToNot(BeNil()) + Expect(ctx.agentInSync()).To(BeTrue()) +} diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go new file mode 100644 index 0000000000..273254f3a8 --- /dev/null +++ b/tests/e2e/e2e_test.go @@ -0,0 +1,422 @@ +// Copyright (c) 2018 Cisco and/or its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2e + +import ( + "bytes" + "flag" + "fmt" + "log" + "net/url" + "os" + "os/exec" + "regexp" + "strings" + "syscall" + "testing" + "time" + + "encoding/json" + "github.com/fsouza/go-dockerclient" + "github.com/gogo/protobuf/proto" + "github.com/ligato/cn-infra/health/probe" + "github.com/mitchellh/go-ps" + . "github.com/onsi/gomega" + "google.golang.org/grpc" + + "github.com/ligato/cn-infra/health/statuscheck/model/status" + "github.com/ligato/vpp-agent/api/genericmanager" + "github.com/ligato/vpp-agent/client" + "github.com/ligato/vpp-agent/client/remoteclient" + "github.com/ligato/vpp-agent/cmd/agentctl/utils" + "github.com/ligato/vpp-agent/pkg/models" + kvs "github.com/ligato/vpp-agent/plugins/kvscheduler/api" + nslinuxcalls "github.com/ligato/vpp-agent/plugins/linux/nsplugin/linuxcalls" +) + +var ( + vppPath = flag.String("vpp-path", "/usr/bin/vpp", "VPP program path") + vppConfig = flag.String("vpp-config", "", "VPP config file") + vppSockAddr = flag.String("vpp-sock-addr", "", "VPP binapi socket address") + agentHTTPPort = flag.Int("agent-http-port", 9191, "VPP-Agent HTTP port") + agentGrpcPort = flag.Int("agent-grpc-port", 9111, "VPP-Agent GRPC port") + + vppPingRegexp = regexp.MustCompile("Statistics: ([0-9]+) sent, ([0-9]+) received, ([0-9]+)% packet loss") +) + +const ( + agentInitTimeout = time.Second * 15 + processExitTimeout = time.Second * 3 + + vppConf = ` + unix { + nodaemon + cli-listen /run/vpp/cli.sock + cli-no-pager + log /tmp/vpp.log + full-coredump + } + api-trace { + on + } + socksvr { + default + } + statseg { + default + per-node-counters on + } + plugins { + plugin dpdk_plugin.so { disable } + }` +) + +func init() { + log.SetFlags(log.Lmicroseconds | log.Lshortfile) + flag.Parse() +} + +type testCtx struct { + t *testing.T + VPP *exec.Cmd + agent *exec.Cmd + dockerClient *docker.Client + microservices map[string]*microservice + nsCalls nslinuxcalls.NetworkNamespaceAPI + httpClient *utils.HTTPClient + grpcConn *grpc.ClientConn + grpcClient client.ConfigClient +} + +func setupE2E(t *testing.T) *testCtx { + if os.Getenv("TRAVIS") != "" { + t.Skip("skipping test for Travis") + } + RegisterTestingT(t) + + // connect to the docker daemon + dockerClient, err := docker.NewClientFromEnv() + if err != nil { + t.Fatalf("failed to get docker client instance from the environment variables: %v", err) + } + t.Logf("Using docker client endpoint: %+v", dockerClient.Endpoint()) + + // make sure there are no microservices left from the previous run + resetMicroservices(t, dockerClient) + + // check if VPP process is not running already + assertProcessNotRunning(t, "vpp", "vpp_main", *vppPath) + + // remove binapi files from previous run + if *vppSockAddr != "" { + removeFile(t, *vppSockAddr) + } + if err := os.Mkdir("/run/vpp", 0755); err != nil && !os.IsExist(err) { + t.Logf("mkdir failed: %v", err) + } + + // start VPP process + var vppArgs []string + if *vppConfig != "" { + vppArgs = []string{"-c", *vppConfig} + } else { + vppArgs = []string{vppConf} + } + vppCmd := startProcess(t, "VPP", *vppPath, vppArgs...) + + // start the agent + assertProcessNotRunning(t, "vpp_agent") + agentCmd := startProcess(t, "VPP-Agent", "/vpp-agent") + + // prepare HTTP client for access to REST API of the agent + httpAddr := fmt.Sprintf(":%d", *agentHTTPPort) + httpClient := utils.NewHTTPClient(httpAddr) + + waitUntilAgentReady(t, httpClient) + + // connect with agent via GRPC + grpcAddr := fmt.Sprintf(":%d", *agentGrpcPort) + grpcConn, err := grpc.Dial(grpcAddr, grpc.WithInsecure()) + if err != nil { + t.Fatalf("Failed to connect to VPP-agent via gRPC: %v", err) + } + grpcClient := remoteclient.NewClientGRPC(genericmanager.NewGenericManagerClient(grpcConn)) + + // run initial resync + syncAgent(t, httpClient) + + return &testCtx{ + t: t, + VPP: vppCmd, + dockerClient: dockerClient, + agent: agentCmd, + microservices: make(map[string]*microservice), + nsCalls: nslinuxcalls.NewSystemHandler(), + httpClient: httpClient, + grpcConn: grpcConn, + grpcClient: grpcClient, + } +} + +func (ctx *testCtx) teardownE2E() { + ctx.t.Logf("-----------------") + + // stop all microservices + for msName := range ctx.microservices { + ctx.stopMicroservice(msName) + } + + // close gRPC connection + ctx.grpcConn.Close() + + // terminate agent + stopProcess(ctx.t, ctx.agent, "VPP-Agent") + + // terminate VPP + stopProcess(ctx.t, ctx.VPP, "VPP") +} + +// syncAgent runs downstream resync and returns the list of executed operations. +func (ctx *testCtx) syncAgent() (executed kvs.RecordedTxnOps) { + return syncAgent(ctx.t, ctx.httpClient) +} + +// agentInSync checks if the agent NB config and the SB state (VPP+Linux) +// are in-sync. +func (ctx *testCtx) agentInSync() bool { + ops := ctx.syncAgent() + for _, op := range ops { + if !op.NOOP { + return false + } + } + return true +} + +func (ctx *testCtx) startMicroservice(msName string) (ms *microservice) { + ms = createMicroservice(ctx.t, msName, ctx.dockerClient, ctx.nsCalls) + ctx.microservices[msName] = ms + return ms +} + +func (ctx *testCtx) stopMicroservice(msName string) { + ms, found := ctx.microservices[msName] + if !found { + // bug inside a test + ctx.t.Fatalf("cannot stop unknown microservice '%s'", msName) + } + if err := ms.stop(); err != nil { + ctx.t.Fatalf("failed to stop microservice '%s': %v", msName, err) + } + delete(ctx.microservices, msName) +} + +// pingFromMs pings from the microservice +func (ctx *testCtx) pingFromMs(msName, dstAddress string) error { + ms, found := ctx.microservices[msName] + if !found { + // bug inside a test + ctx.t.Fatalf("cannot ping from unknown microservice '%s'", msName) + } + return ms.ping(dstAddress) +} + +// pingFromMsClb can be used to ping repeatedly inside the assertions "Eventually" +// and "Consistently" from Omega. +func (ctx *testCtx) pingFromMsClb(msName, dstAddress string) func() error { + return func() error { + return ctx.pingFromMs(msName, dstAddress) + } +} + +// pingFromVPP pings from inside the VPP. +func (ctx *testCtx) pingFromVPP(destAddress string) error { + var stdout bytes.Buffer + + // run ping on VPP using vppctl + cmd := exec.Command("vppctl", "ping", destAddress) + cmd.Stdout = &stdout + err := cmd.Run() + if err != nil { + return err + } + + // parse output + matches := vppPingRegexp.FindStringSubmatch(stdout.String()) + sent, recv, loss, err := parsePingOutput(stdout.String(), matches) + if err != nil { + return err + } + ctx.t.Logf("VPP ping %s: sent=%d, received=%d, loss=%d%%", + destAddress, sent, recv, loss) + + if sent == 0 || loss >= 50 { + return fmt.Errorf("failed to ping '%s': %s", destAddress, matches[0]) + } + return nil +} + +// pingFromVPPClb can be used to ping repeatedly inside the assertions "Eventually" +// and "Consistently" from Omega. +func (ctx *testCtx) pingFromVPPClb(destAddress string) func() error { + return func() error { + return ctx.pingFromVPP(destAddress) + } +} + +func (ctx *testCtx) testConnection(fromMs, toMs, dstAddr, listenAddr string, port uint16, udp bool) error { + // TODO (run nc client and server) + return nil +} + +func (ctx *testCtx) getValueState(value proto.Message) kvs.ValueState { + key := models.Key(value) + return ctx.getValueStateByKey(key) +} + +func (ctx *testCtx) getValueStateByKey(key string) kvs.ValueState { + q := fmt.Sprintf(`/scheduler/status?key=%s`, url.QueryEscape(key)) + resp, err := ctx.httpClient.GET(q) + if err != nil { + ctx.t.Fatalf("Request to obtain value status has failed: %v", err) + } + status := kvs.BaseValueStatus{} + if err := json.Unmarshal(resp, &status); err != nil { + ctx.t.Fatalf("Reply with value status cannot be decoded: %v", err) + } + if status.GetValue().GetKey() != key { + ctx.t.Fatalf("Received value status for unexpected key: %v", status) + } + return status.GetValue().GetState() +} + +// getValueStateClb can be used to repeatedly check value state inside the assertions +// "Eventually" and "Consistently" from Omega. +func (ctx *testCtx) getValueStateClb(value proto.Message) func() kvs.ValueState { + return func() kvs.ValueState { + return ctx.getValueState(value) + } +} + +func syncAgent(t *testing.T, httpClient *utils.HTTPClient) (executed kvs.RecordedTxnOps) { + resp, err := httpClient.POST("/scheduler/downstream-resync", struct{}{}) + if err != nil { + t.Fatalf("Downstream resync request has failed: %v", err) + } + txn := kvs.RecordedTxn{} + if err := json.Unmarshal(resp, &txn); err != nil { + t.Fatalf("Downstream resync reply cannot be decoded: %v", err) + } + if txn.Start.IsZero() { + t.Fatalf("Downstream resync returned empty transaction record: %v", txn) + } + return txn.Executed +} + +func waitUntilAgentReady(t *testing.T, httpClient *utils.HTTPClient) { + start := time.Now() + for { + select { + case <-time.After(100 * time.Millisecond): + if time.Since(start) > agentInitTimeout { + t.Fatalf("agent failed to initialize within the timeout period of %v", + agentInitTimeout) + } + resp, err := httpClient.GET("/readiness") + if err != nil { + continue + } + agentStatus := probe.ExposedStatus{} + if err := json.Unmarshal(resp, &agentStatus); err != nil { + t.Fatalf("Agent readiness reply cannot be decoded: %v", err) + } + if agentStatus, ok := agentStatus.PluginStatus["VPPAgent"]; ok { + if agentStatus.State == status.OperationalState_OK { + return + } + } + } + } +} + +func assertProcessNotRunning(t *testing.T, name string, aliases ...string) { + processes, err := ps.Processes() + if err != nil { + t.Fatalf("listing processes failed: %v", err) + } + for _, process := range processes { + proc := process.Executable() + if strings.Contains(proc, name) && process.Pid() != os.Getpid() { + t.Logf(" - found process: %+v", process) + } + aliases := append(aliases, name) + for _, alias := range aliases { + if alias == proc { + t.Fatalf("%s is already running (PID: %v)", name, process.Pid()) + } + } + } +} + +func startProcess(t *testing.T, name, path string, args ...string) *exec.Cmd { + cmd := exec.Command(path) + cmd.Args = append(cmd.Args, args...) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + + // ensure that process is killed when current process exits + cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL} + if err := cmd.Start(); err != nil { + t.Fatalf("starting %s failed: %v", name, err) + } + pid := uint32(cmd.Process.Pid) + t.Logf("%s start OK (PID: %v)", name, pid) + return cmd +} + +func stopProcess(t *testing.T, cmd *exec.Cmd, name string) { + // terminate process + if err := cmd.Process.Signal(syscall.SIGTERM); err != nil { + t.Fatalf("sending SIGTERM to %s failed: %v", name, err) + } + + // wait until process exits + exit := make(chan struct{}) + go func() { + if err := cmd.Wait(); err != nil { + t.Logf("%s process wait failed: %v", name, err) + } + close(exit) + }() + select { + case <-exit: + t.Logf("%s exit OK", name) + + case <-time.After(processExitTimeout): + t.Logf("%s exit timeout", name) + t.Logf("sending SIGKILL to %s..", name) + if err := cmd.Process.Signal(syscall.SIGKILL); err != nil { + t.Fatalf("sending SIGKILL to %s failed: %v", name, err) + } + } +} + +func removeFile(t *testing.T, path string) { + if err := os.Remove(path); err == nil { + t.Logf("removed file %q", path) + } else if !os.IsNotExist(err) { + t.Fatalf("removing file %q failed: %v", path, err) + } +} diff --git a/tests/e2e/grpc.conf b/tests/e2e/grpc.conf new file mode 100644 index 0000000000..cd0e70d17f --- /dev/null +++ b/tests/e2e/grpc.conf @@ -0,0 +1,19 @@ +# GRPC endpoint defines IP address and port (if tcp type) or unix domain socket file (if unix type). +endpoint: 127.0.0.1:9111 + +# If unix domain socket file is used for GRPC communication, permissions to the file can be set here. +# Permission value uses standard three-or-four number linux binary reference. +permission: 000 + +# If socket file exists in defined path, it is not removed by default, GRPC plugin tries to use it. +# Set the force removal flag to 'true' ensures that the socket file will be always re-created +force-socket-removal: false + +# Available socket types: tcp, tcp4, tcp6, unix, unixpacket. If not set, defaults to tcp. +network: tcp + +# Maximum message size in bytes for inbound mesages. If not set, GRPC uses the default 4MB. +#max-msg-size: 4096 + +# Limit of server streams to each server transport. +max-concurrent-streams: 0 diff --git a/tests/e2e/microservice.go b/tests/e2e/microservice.go new file mode 100644 index 0000000000..f13936a57d --- /dev/null +++ b/tests/e2e/microservice.go @@ -0,0 +1,211 @@ +package e2e + +import ( + "bytes" + "fmt" + "regexp" + "strconv" + "testing" + "runtime" + + "github.com/fsouza/go-dockerclient" + "github.com/vishvananda/netns" + + nslinuxcalls "github.com/ligato/vpp-agent/plugins/linux/nsplugin/linuxcalls" +) + +const ( + msImage = "busybox" + msImageTag = "1.31" + msStopTimeout = 3 // seconds + msLabelKey = "e2e.test.ms" + msNamePrefix = "e2e-test-" +) + +var ( + linuxPingRegexp = regexp.MustCompile("\n([0-9]+) packets transmitted, ([0-9]+) packets received, ([0-9]+)% packet loss") +) + +type microservice struct { + t *testing.T + name string + dockerClient *docker.Client + container *docker.Container + nsCalls nslinuxcalls.NetworkNamespaceAPI +} + +func createMicroservice(t *testing.T, msName string, dockerClient *docker.Client, nsCalls nslinuxcalls.NetworkNamespaceAPI) *microservice { + container, err := dockerClient.CreateContainer(docker.CreateContainerOptions{ + Name: msNamePrefix + msName, + Config: &docker.Config{ + Env: []string{"MICROSERVICE_LABEL=" + msNamePrefix + msName}, + Image: msImage + ":" + msImageTag, + Cmd: []string{"tail", "-f", "/dev/null"}, + Labels: map[string]string{msLabelKey: msName}, + }, + HostConfig: &docker.HostConfig{ + // networking configured via VPP in E2E tests + NetworkMode: "none", + }, + }) + if err != nil { + t.Fatalf("failed to create microservice '%s': %v", msName, err) + } + err = dockerClient.StartContainer(container.ID, nil) + if err != nil { + t.Fatalf("failed to start microservice '%s': %v", msName, err) + } + container, err = dockerClient.InspectContainer(container.ID) + if err != nil { + t.Fatalf("failed to inspect microservice '%s': %v", msName, err) + } + return µservice{ + t: t, + name: msName, + container: container, + dockerClient: dockerClient, + nsCalls: nsCalls, + } +} + +func resetMicroservices(t *testing.T, dockerClient *docker.Client) { + // pull image for microservices + err := dockerClient.PullImage(docker.PullImageOptions{ + Repository: msImage, + Tag: msImageTag, + }, docker.AuthConfiguration{}) + if err != nil { + t.Fatalf("failed to pull image '%s:%s' for microservices: %v", msImage, msImageTag, err) + } + + // remove any running microservices prior to starting a new test + containers, err := dockerClient.ListContainers(docker.ListContainersOptions{ + All: true, + Filters: map[string][]string{ + "label": {msLabelKey}, + }, + }) + if err != nil { + t.Fatalf("failed to list existing microservices: %v", err) + } + for _, container := range containers { + err = dockerClient.RemoveContainer(docker.RemoveContainerOptions{ + ID: container.ID, + Force: true, + }) + if err != nil { + t.Fatalf("failed to remove existing microservices: %v", err) + } else { + t.Logf("removed existing microservice: %s", container.Labels[msLabelKey]) + } + } +} + +func (ms *microservice) stop() error { + err := ms.dockerClient.StopContainer(ms.container.ID, msStopTimeout) + if err != nil { + return err + } + return ms.dockerClient.RemoveContainer(docker.RemoveContainerOptions{ + ID: ms.container.ID, + Force: true, + }) +} + +// exec allows to execute command **inside** the microservice - i.e. not just +// inside the network namespace of the microservice, but inside the container +// as a whole. +func (ms *microservice) exec(cmdName string, args ...string) (output string, err error) { + execCtx, err := ms.dockerClient.CreateExec(docker.CreateExecOptions{ + AttachStdout: true, + Cmd: append([]string{cmdName}, args...), + Container: ms.container.ID, + }) + if err != nil { + ms.t.Fatalf("failed to create docker exec instance for ping: %v", err) + } + + var stdout bytes.Buffer + err = ms.dockerClient.StartExec(execCtx.ID, docker.StartExecOptions{ + OutputStream: &stdout, + }) + return stdout.String(), err +} + +// enterNetNs enters the **network** namespace of the microservice (other namespaces +// remain unchanged). Leave using the returned callback. +func (ms *microservice) enterNetNs() (exitNetNs func()) { + origns, err := netns.Get() + if err != nil { + ms.t.Fatalf("failed to obtain current network namespace: %v", err) + } + nsHandle, err := ms.nsCalls.GetNamespaceFromPid(ms.container.State.Pid) + if err != nil { + ms.t.Fatalf("failed to obtain handle for network namespace of microservice '%s': %v", + ms.name, err) + } + defer nsHandle.Close() + + runtime.LockOSThread() + err = ms.nsCalls.SetNamespace(nsHandle) + if err != nil { + ms.t.Fatalf("failed to enter network namespace of microservice '%s': %v", + ms.name, err) + } + return func() { + err = ms.nsCalls.SetNamespace(origns) + if err != nil { + ms.t.Fatalf("failed to return back to the original network namespace: %v", err) + } + origns.Close() + runtime.UnlockOSThread() + } +} + +// ping from inside of the microservice. +func (ms *microservice) ping(destAddress string, allowedLoss ...int) error { + stdout, err := ms.exec("ping", "-w", "4", destAddress) + if err != nil { + return err + } + + matches := linuxPingRegexp.FindStringSubmatch(stdout) + sent, recv, loss, err := parsePingOutput(stdout, matches) + if err != nil { + return err + } + ms.t.Logf("Linux ping %s: sent=%d, received=%d, loss=%d%%", + destAddress, sent, recv, loss) + + maxLoss := 49 // by default at least half of the packets should ge through + if len(allowedLoss) > 0 { + maxLoss = allowedLoss[0] + } + if sent == 0 || loss > maxLoss { + return fmt.Errorf("failed to ping '%s': %s", destAddress, matches[0]) + } + return nil +} + +func parsePingOutput(output string, matches []string) (sent int, recv int, loss int, err error) { + if len(matches) != 4 { + err = fmt.Errorf("unexpected output from ping: %s", output) + return + } + sent, err = strconv.Atoi(matches[1]) + if err != nil { + err = fmt.Errorf("failed to parse the sent packet count: %v", err) + return + } + recv, err = strconv.Atoi(matches[2]) + if err != nil { + err = fmt.Errorf("failed to parse the received packet count: %v", err) + return + } + loss, err = strconv.Atoi(matches[3]) + if err != nil { + err = fmt.Errorf("failed to parse the loss percentage: %v", err) + return + } + return +} diff --git a/tests/e2e/run_e2e.sh b/tests/e2e/run_e2e.sh new file mode 100755 index 0000000000..5cfcf60365 --- /dev/null +++ b/tests/e2e/run_e2e.sh @@ -0,0 +1,56 @@ +#!/bin/bash +set -eu + +# compile test +go test -c ./tests/e2e -o ./tests/e2e/e2e.test +go build -v -o ./tests/e2e/vpp-agent ./cmd/vpp-agent + +# start vpp image +cid=$(docker run -d -it \ + -v $(pwd)/tests/e2e/e2e.test:/e2e.test:ro \ + -v $(pwd)/tests/e2e/vpp-agent:/vpp-agent:ro \ + -v $(pwd)/tests/e2e/grpc.conf:/etc/grpc.conf:ro \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --label e2e.test="$*" \ + --pid="host" \ + --privileged \ + --env KVSCHEDULER_GRAPHDUMP=true \ + --env VPP_IMG="$VPP_IMG" \ + --env GRPC_CONFIG=/etc/grpc.conf \ + ${DOCKER_ARGS-} \ + "$VPP_IMG" bash) + + +on_exit() { + docker stop -t 2 "$cid" >/dev/null + docker rm "$cid" >/dev/null +} + +vppver=$(docker exec -i "$cid" dpkg-query -f '${Version}' -W vpp) + +trap 'on_exit' EXIT + +echo "=============================================================" +echo -e " E2E Test - \e[1;33m${vppver}\e[0m" +echo "=============================================================" + +# run e2e test +if docker exec -i "$cid" /e2e.test -test.v $*; then + echo >&2 "-------------------------------------------------------------" + echo >&2 -e " \e[32mPASSED\e[0m (took: ${SECONDS}s)" + echo >&2 "-------------------------------------------------------------" + exit 0 +else + res=$? + echo >&2 "-------------------------------------------------------------" + echo >&2 -e " \e[31mFAILED!\e[0m (exit code: $res)" + echo >&2 "-------------------------------------------------------------" + + # dump container logs + logs=$(docker logs --tail 10 "$cid") + if [[ -n "$logs" ]]; then + echo >&2 -e "\e[1;30m$logs\e[0m" + fi + + exit $res +fi diff --git a/tests/integration/vpp/020_routes_test.go b/tests/integration/vpp/020_routes_test.go index 7ade970bed..14030a76f5 100644 --- a/tests/integration/vpp/020_routes_test.go +++ b/tests/integration/vpp/020_routes_test.go @@ -21,6 +21,7 @@ import ( "github.com/ligato/cn-infra/logging/logrus" vpp_l3 "github.com/ligato/vpp-agent/api/models/vpp/l3" + netalloc_mock "github.com/ligato/vpp-agent/plugins/netalloc/mock" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/ifaceidx" ifplugin_vppcalls "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/vppcalls" _ "github.com/ligato/vpp-agent/plugins/vpp/l3plugin" @@ -37,7 +38,8 @@ func TestRoutes(t *testing.T) { vrfIndexes.Put("vrf1-ipv4", &vrfidx.VRFMetadata{Index: 0, Protocol: vpp_l3.VrfTable_IPV4}) vrfIndexes.Put("vrf1-ipv6", &vrfidx.VRFMetadata{Index: 0, Protocol: vpp_l3.VrfTable_IPV6}) - h := l3plugin_vppcalls.CompatibleL3VppHandler(ctx.vppBinapi, ifIndexes, vrfIndexes, logrus.NewLogger("test")) + h := l3plugin_vppcalls.CompatibleL3VppHandler(ctx.vppBinapi, ifIndexes, vrfIndexes, + netalloc_mock.NewMockNetAlloc(), logrus.NewLogger("test")) routes, err := h.DumpRoutes() if err != nil { @@ -86,7 +88,8 @@ func TestCRUDIPv4Route(t *testing.T) { vrfIndexes := vrfidx.NewVRFIndex(logrus.NewLogger("test-vrf"), "test-vrf") vrfIndexes.Put("vrf1-ipv4-vrf0", &vrfidx.VRFMetadata{Index: vrfMetaIdx, Protocol: vpp_l3.VrfTable_IPV4}) - h := l3plugin_vppcalls.CompatibleL3VppHandler(ctx.vppBinapi, ifIndexes, vrfIndexes, logrus.NewLogger("test")) + h := l3plugin_vppcalls.CompatibleL3VppHandler(ctx.vppBinapi, ifIndexes, vrfIndexes, + netalloc_mock.NewMockNetAlloc(), logrus.NewLogger("test")) routes, errx := h.DumpRoutes() if errx != nil { @@ -95,7 +98,7 @@ func TestCRUDIPv4Route(t *testing.T) { routesCnt := len(routes) t.Logf("%d routes dumped", routesCnt) - newRoute := vpp_l3.Route{VrfId: 0, DstNetwork: "192.168.10.21/24", NextHopAddr: "192.168.30.1", OutgoingInterface: ifName} + newRoute := vpp_l3.Route{VrfId: 0, DstNetwork: "192.168.10.0/24", NextHopAddr: "192.168.30.1", OutgoingInterface: ifName} err = h.VppAddRoute(&newRoute) if err != nil { t.Fatalf("adding route failed: %v", err) @@ -161,7 +164,7 @@ func TestCRUDIPv4Route(t *testing.T) { routesCnt = len(routes) t.Logf("%d routes dumped", routesCnt) - newRoute = vpp_l3.Route{VrfId: 2, DstNetwork: "192.168.10.21/24", NextHopAddr: "192.168.30.1", OutgoingInterface: ifName} + newRoute = vpp_l3.Route{VrfId: 2, DstNetwork: "192.168.10.0/24", NextHopAddr: "192.168.30.1", OutgoingInterface: ifName} err = h.VppAddRoute(&newRoute) if err != nil { t.Fatalf("adding route failed: %v", err) @@ -234,7 +237,8 @@ func TestCRUDIPv6Route(t *testing.T) { vrfIndexes := vrfidx.NewVRFIndex(logrus.NewLogger("test-vrf"), "test-vrf") vrfIndexes.Put("vrf1-ipv6-vrf0", &vrfidx.VRFMetadata{Index: vrfMetaIdx, Protocol: vpp_l3.VrfTable_IPV6}) - h := l3plugin_vppcalls.CompatibleL3VppHandler(ctx.vppBinapi, ifIndexes, vrfIndexes, logrus.NewLogger("test")) + h := l3plugin_vppcalls.CompatibleL3VppHandler(ctx.vppBinapi, ifIndexes, vrfIndexes, + netalloc_mock.NewMockNetAlloc(), logrus.NewLogger("test")) routes, errx := h.DumpRoutes() if errx != nil { diff --git a/tests/integration/vpp/030_arp_test.go b/tests/integration/vpp/030_arp_test.go index c6d0d0a8d2..0d0a3ea05f 100644 --- a/tests/integration/vpp/030_arp_test.go +++ b/tests/integration/vpp/030_arp_test.go @@ -20,6 +20,7 @@ import ( "testing" vpp_l3 "github.com/ligato/vpp-agent/api/models/vpp/l3" + netalloc_mock "github.com/ligato/vpp-agent/plugins/netalloc/mock" "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/ifaceidx" ifplugin_vppcalls "github.com/ligato/vpp-agent/plugins/vpp/ifplugin/vppcalls" _ "github.com/ligato/vpp-agent/plugins/vpp/l3plugin" @@ -45,7 +46,8 @@ func TestArp(t *testing.T) { vrfIndexes.Put("vrf1-ipv4", &vrfidx.VRFMetadata{Index: 0, Protocol: vpp_l3.VrfTable_IPV4}) vrfIndexes.Put("vrf1-ipv6", &vrfidx.VRFMetadata{Index: 0, Protocol: vpp_l3.VrfTable_IPV6}) - h := l3plugin_vppcalls.CompatibleL3VppHandler(ctx.vppBinapi, ifIndexes, vrfIndexes, logrus.NewLogger("test")) + h := l3plugin_vppcalls.CompatibleL3VppHandler(ctx.vppBinapi, ifIndexes, vrfIndexes, + netalloc_mock.NewMockNetAlloc(), logrus.NewLogger("test")) tests := []struct { name string