diff --git a/cns/api.go b/cns/api.go new file mode 100644 index 0000000000..57c0a2f0dc --- /dev/null +++ b/cns/api.go @@ -0,0 +1,106 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +package cns + +// Container Network Service remote API Contract +const ( + SetEnvironmentPath = "/network/environment" + CreateNetworkPath = "/network/create" + DeleteNetworkPath = "/network/delete" + ReserveIPAddressPath = "/network/ip/reserve" + ReleaseIPAddressPath = "/network/ip/release" + GetHostLocalIPPath = "/network/ip/hostlocal" + GetIPAddressUtilizationPath = "/network/ip/utilization" + GetUnhealthyIPAddressesPath = "/network/ipaddresses/unhealthy" + GetHealthReportPath = "/network/health" + V1Prefix = "/v0.1" +) + +// SetEnvironmentRequest describes the Request to set the environment in CNS. +type SetEnvironmentRequest struct { + Location string + NetworkType string +} + +// OverlayConfiguration describes configuration for all the nodes that are part of overlay. +type OverlayConfiguration struct { + NodeCount int + LocalNodeIP string + OverlaySubent Subnet + NodeConfig []NodeConfiguration +} + +// CreateNetworkRequest describes request to create the network. +type CreateNetworkRequest struct { + NetworkName string + OverlayConfiguration OverlayConfiguration +} + +// DeleteNetworkRequest describes request to delete the network. +type DeleteNetworkRequest struct { + NetworkName string +} + +// ReserveIPAddressRequest describes request to reserve an IP Address +type ReserveIPAddressRequest struct { + ReservationID string +} + +// ReserveIPAddressResponse describes response to reserve an IP address. +type ReserveIPAddressResponse struct { + Response Response + IPAddress string +} + +// ReleaseIPAddressRequest describes request to release an IP Address. +type ReleaseIPAddressRequest struct { + ReservationID string +} + +// IPAddressesUtilizationResponse describes response for ip address utilization. +type IPAddressesUtilizationResponse struct { + Response Response + Available int + Reserved int + Unhealthy int +} + +// GetIPAddressesResponse describes response containing requested ip addresses. +type GetIPAddressesResponse struct { + Response Response + IPAddresses []string +} + +// HostLocalIPAddressResponse describes reponse that returns the host local IP Address. +type HostLocalIPAddressResponse struct { + Response Response + IPAddress string +} + +// Subnet contains the ip address and the number of bits in prefix. +type Subnet struct { + IPAddress string + PrefixLength int +} + +// NodeConfiguration describes confguration for a node in overlay network. +type NodeConfiguration struct { + NodeIP string + NodeID string + NodeSubnet Subnet +} + +// Response describes generic response from CNS. +type Response struct { + ReturnCode int + Message string +} + +// OptionMap describes generic options that can be passed to CNS. +type OptionMap map[string]interface{} + +// Response to a failed request. +type errorResponse struct { + Err string +} diff --git a/cns/common/service.go b/cns/common/service.go new file mode 100644 index 0000000000..278c83aa19 --- /dev/null +++ b/cns/common/service.go @@ -0,0 +1,86 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +package common + +import ( + "errors" + + acn "github.com/Azure/azure-container-networking/common" + "github.com/Azure/azure-container-networking/log" + "github.com/Azure/azure-container-networking/store" +) + +// Service implements behavior common to all services. +type Service struct { + Name string + Version string + Options map[string]interface{} + ErrChan chan error + Store store.KeyValueStore +} + +// ServiceAPI defines base interface. +type ServiceAPI interface { + Start(*ServiceConfig) error + Stop() + GetOption(string) interface{} + SetOption(string, interface{}) +} + +// ServiceConfig specifies common configuration. +type ServiceConfig struct { + Name string + Version string + Listener *acn.Listener + ErrChan chan error + Store store.KeyValueStore +} + +// NewService creates a new Service object. +func NewService(name, version string, store store.KeyValueStore) (*Service, error) { + log.Debugf("[Azure CNS] Going to create a service object with name: %v. version: %v.", name, version) + + svc := &Service{ + Name: name, + Version: version, + Options: make(map[string]interface{}), + Store: store, + } + + log.Debugf("[Azure CNS] Finished creating service object with name: %v. version: %v.", name, version) + return svc, nil +} + +// Initialize initializes the service. +func (service *Service) Initialize(config *ServiceConfig) error { + if config == nil { + err := "[Azure CNS Errror] Initialize called with nil ServiceConfig." + log.Printf(err) + return errors.New(err) + } + + log.Debugf("[Azure CNS] Going to initialize the service: %+v with config: %+v.", service, config) + + service.ErrChan = config.ErrChan + service.Store = config.Store + service.Version = config.Version + + log.Debugf("[Azure CNS] nitialized service: %+v with config: %+v.", service, config) + + return nil +} + +// Uninitialize cleans up the service. +func (service *Service) Uninitialize() { +} + +// GetOption gets the option value for the given key. +func (service *Service) GetOption(key string) interface{} { + return service.Options[key] +} + +// SetOption sets the option value for the given key. +func (service *Service) SetOption(key string, value interface{}) { + service.Options[key] = value +} diff --git a/cns/dockerclient/api.go b/cns/dockerclient/api.go new file mode 100644 index 0000000000..e949f4b019 --- /dev/null +++ b/cns/dockerclient/api.go @@ -0,0 +1,33 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +package dockerclient + +const ( + createNetworkPath = "/networks/create" + inspectNetworkPath = "/networks/" +) + +// Config describes subnet/gateway for ipam. +type Config struct { + Subnet string +} + +// IPAM describes ipam details +type IPAM struct { + Driver string + Config []Config +} + +// NetworkConfiguration describes configuration for docker network create. +type NetworkConfiguration struct { + Name string + Driver string + IPAM IPAM + Internal bool +} + +// DockerErrorResponse defines the error response retunred by docker. +type DockerErrorResponse struct { + message string +} diff --git a/cns/dockerclient/dockerclient.go b/cns/dockerclient/dockerclient.go new file mode 100644 index 0000000000..d940f0b02c --- /dev/null +++ b/cns/dockerclient/dockerclient.go @@ -0,0 +1,156 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +package dockerclient + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + + "github.com/Azure/azure-container-networking/cns/imdsclient" + "github.com/Azure/azure-container-networking/log" +) + +const ( + defaultDockerConnectionURL = "http://127.0.0.1:2375" + defaultIpamPlugin = "azure-vnet" +) + +// DockerClient specifies a client to connect to docker. +type DockerClient struct { + connectionURL string + imdsClient *imdsclient.ImdsClient +} + +// NewDockerClient create a new docker client. +func NewDockerClient(url string) (*DockerClient, error) { + return &DockerClient{ + connectionURL: url, + imdsClient: &imdsclient.ImdsClient{}, + }, nil +} + +// NewDefaultDockerClient create a new docker client. +func NewDefaultDockerClient(imdsClient *imdsclient.ImdsClient) (*DockerClient, error) { + return &DockerClient{ + connectionURL: defaultDockerConnectionURL, + imdsClient: imdsClient, + }, nil +} + +// NetworkExists tries to retrieve a network from docker (if it exists). +func (dockerClient *DockerClient) NetworkExists(networkName string) error { + log.Printf("[Azure CNS] NetworkExists") + + res, err := http.Get( + dockerClient.connectionURL + inspectNetworkPath + networkName) + + if err != nil { + log.Printf("[Azure CNS] Error received from http Post for docker network inspect %v %v", networkName, err.Error()) + return err + } + + // network exists + if res.StatusCode == 200 { + log.Debugf("[Azure CNS] Network with name %v already exists. Docker return code: %v", networkName, res.StatusCode) + return nil + } + + // network not found + if res.StatusCode == 404 { + log.Debugf("[Azure CNS] Network with name %v does not exist. Docker return code: %v", networkName, res.StatusCode) + return fmt.Errorf("Network not found") + } + + return fmt.Errorf("Unknown return code from docker inspect %d", res.StatusCode) +} + +// CreateNetwork creates a network using docker network create. +func (dockerClient *DockerClient) CreateNetwork(networkName string) error { + log.Printf("[Azure CNS] CreateNetwork") + + primaryNic, err := dockerClient.imdsClient.GetPrimaryInterfaceInfoFromHost() + if err != nil { + return err + } + + config := &Config{ + Subnet: primaryNic.Subnet, + } + + configs := make([]Config, 1) + configs[0] = *config + ipamConfig := &IPAM{ + Driver: defaultIpamPlugin, + Config: configs, + } + + netConfig := &NetworkConfiguration{ + Name: networkName, + Driver: defaultNetworkPlugin, + IPAM: *ipamConfig, + Internal: true, + } + + log.Printf("[Azure CNS] Going to create network with config: %+v", netConfig) + + netConfigJSON := new(bytes.Buffer) + err = json.NewEncoder(netConfigJSON).Encode(netConfig) + if err != nil { + return err + } + + res, err := http.Post( + dockerClient.connectionURL+createNetworkPath, + "application/json; charset=utf-8", + netConfigJSON) + + if err != nil { + log.Printf("[Azure CNS] Error received from http Post for docker network create %v", networkName) + return err + } + if res.StatusCode != 201 { + var createNetworkResponse DockerErrorResponse + err = json.NewDecoder(res.Body).Decode(&createNetworkResponse) + var ermsg string + ermsg = "" + if err != nil { + ermsg = err.Error() + } + return fmt.Errorf("[Azure CNS] Create docker network failed with error code %v - %v - %v", + res.StatusCode, createNetworkResponse.message, ermsg) + } + + return nil +} + +// DeleteNetwork creates a network using docker network create. +func (dockerClient *DockerClient) DeleteNetwork(networkName string) error { + log.Printf("[Azure CNS] DeleteNetwork") + + url := dockerClient.connectionURL + inspectNetworkPath + networkName + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + log.Printf("[Azure CNS] Error received while creating http DELETE request for network delete %v %v", networkName, err.Error()) + return err + } + + req.Header.Set("Content-Type", "application/json; charset=utf-8") + client := &http.Client{} + res, err := client.Do(req) + + // network successfully deleted. + if res.StatusCode == 204 { + return nil + } + + // network not found. + if res.StatusCode == 404 { + return fmt.Errorf("[Azure CNS] Network not found %v", networkName) + } + + return fmt.Errorf("[Azure CNS] Unknown return code from docker delete network %v: ret = %d", + networkName, res.StatusCode) +} diff --git a/cns/dockerclient/dockerclient_linux.go b/cns/dockerclient/dockerclient_linux.go new file mode 100644 index 0000000000..18eadf5eee --- /dev/null +++ b/cns/dockerclient/dockerclient_linux.go @@ -0,0 +1,10 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +// +build linux + +package dockerclient + +const ( + defaultNetworkPlugin = "azure-vnet" +) diff --git a/cns/dockerclient/dockerclient_windows.go b/cns/dockerclient/dockerclient_windows.go new file mode 100644 index 0000000000..94ac90796d --- /dev/null +++ b/cns/dockerclient/dockerclient_windows.go @@ -0,0 +1,10 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +// +build windows + +package dockerclient + +const ( + defaultNetworkPlugin = "l2tunnel" +) diff --git a/cns/imdsclient/api.go b/cns/imdsclient/api.go new file mode 100644 index 0000000000..445461eab7 --- /dev/null +++ b/cns/imdsclient/api.go @@ -0,0 +1,47 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +package imdsclient + +import ( + "encoding/xml" +) + +const ( + hostQueryURL = "http://169.254.169.254/machine/plugins?comp=nmagent&type=getinterfaceinfov1" +) + +// ImdsClient cna be used to connect to VM Host agent in Azure. +type ImdsClient struct { + primaryInterface *InterfaceInfo +} + +// InterfaceInfo specifies the information about an interface as returned by Host Agent. +type InterfaceInfo struct { + Subnet string + Gateway string + IsPrimary bool + PrimaryIP string + SecondaryIPs []string +} + +// Azure host agent XML document format. +type xmlDocument struct { + XMLName xml.Name `xml:"Interfaces"` + Interface []struct { + XMLName xml.Name `xml:"Interface"` + MacAddress string `xml:"MacAddress,attr"` + IsPrimary bool `xml:"IsPrimary,attr"` + + IPSubnet []struct { + XMLName xml.Name `xml:"IPSubnet"` + Prefix string `xml:"Prefix,attr"` + + IPAddress []struct { + XMLName xml.Name `xml:"IPAddress"` + Address string `xml:"Address,attr"` + IsPrimary bool `xml:"IsPrimary,attr"` + } + } + } +} diff --git a/cns/imdsclient/imdsclient.go b/cns/imdsclient/imdsclient.go new file mode 100644 index 0000000000..2e1335f5cc --- /dev/null +++ b/cns/imdsclient/imdsclient.go @@ -0,0 +1,101 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +package imdsclient + +import ( + "encoding/xml" + "fmt" + "net/http" + "strings" + + "github.com/Azure/azure-container-networking/log" +) + +// GetPrimaryInterfaceInfoFromHost retrieves subnet and gateway of primary NIC from Host. +func (imdsClient *ImdsClient) GetPrimaryInterfaceInfoFromHost() (*InterfaceInfo, error) { + log.Printf("[Azure CNS] GetPrimaryInterfaceInfoFromHost") + + interfaceInfo := &InterfaceInfo{} + resp, err := http.Get(hostQueryURL) + if err != nil { + return nil, err + } + + log.Printf("[Azure CNS] Response received from NMAgent: %v", resp.Body) + + var doc xmlDocument + decoder := xml.NewDecoder(resp.Body) + err = decoder.Decode(&doc) + if err != nil { + return nil, err + } + + foundPrimaryInterface := false + + // For each interface. + for _, i := range doc.Interface { + // Find primary Interface. + if i.IsPrimary { + interfaceInfo.IsPrimary = true + + // Get the first subnet. + for _, s := range i.IPSubnet { + interfaceInfo.Subnet = s.Prefix + malformedSubnetError := fmt.Errorf("Malformed subnet received from host %s", s.Prefix) + + st := strings.Split(s.Prefix, "/") + if len(st) != 2 { + return nil, malformedSubnetError + } + + ip := strings.Split(st[0], ".") + if len(ip) != 4 { + return nil, malformedSubnetError + } + + interfaceInfo.Gateway = fmt.Sprintf("%s.%s.%s.1", ip[0], ip[1], ip[2]) + for _, ip := range s.IPAddress { + if ip.IsPrimary == true { + interfaceInfo.PrimaryIP = ip.Address + } + } + + imdsClient.primaryInterface = interfaceInfo + break + } + + foundPrimaryInterface = true + break + } + } + + var er error + er = nil + if !foundPrimaryInterface { + er = fmt.Errorf("Unable to find primary NIC") + } + + return interfaceInfo, er +} + +// GetPrimaryInterfaceInfoFromMemory retrieves subnet and gateway of primary NIC that is saved in memory. +func (imdsClient *ImdsClient) GetPrimaryInterfaceInfoFromMemory() (*InterfaceInfo, error) { + log.Printf("[Azure CNS] GetPrimaryInterfaceInfoFromMemory") + + var iface *InterfaceInfo + var err error + if imdsClient.primaryInterface == nil { + log.Debugf("Azure-CNS] Primary interface in memory does not exist. Will get it from Host.") + iface, err = imdsClient.GetPrimaryInterfaceInfoFromHost() + if err != nil { + log.Printf("[Azure-CNS] Unable to retrive primary interface info.") + } else { + log.Debugf("Azure-CNS] Primary interface received from HOST: %+v.", iface) + } + } else { + iface = imdsClient.primaryInterface + } + + return iface, err +} diff --git a/cns/ipamclient/ipamclient.go b/cns/ipamclient/ipamclient.go new file mode 100644 index 0000000000..0c14175221 --- /dev/null +++ b/cns/ipamclient/ipamclient.go @@ -0,0 +1,267 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +package ipamclient + +import ( + "bytes" + "encoding/json" + "net/http" + + "fmt" + + cnmIpam "github.com/Azure/azure-container-networking/cnm/ipam" + ipam "github.com/Azure/azure-container-networking/ipam" + "github.com/Azure/azure-container-networking/log" +) + +// IpamClient specifies a client to connect to Ipam Plugin. +type IpamClient struct { + connectionURL string +} + +// NewIpamClient create a new ipam client. +func NewIpamClient(url string) (*IpamClient, error) { + if url == "" { + url = defaultIpamPluginURL + } + return &IpamClient{ + connectionURL: url, + }, nil +} + +// GetAddressSpace request to get address space ID. +func (ic *IpamClient) GetAddressSpace() (string, error) { + log.Printf("[Azure CNS] GetAddressSpace Request") + + url := ic.connectionURL + cnmIpam.GetAddressSpacesPath + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + log.Printf("[Azure CNS] Error received while creating http GET request for AddressSpace %v", err.Error()) + return "", err + } + + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + client := &http.Client{} + + res, err := client.Do(req) + if res == nil { + return "", err + } + + if res.StatusCode == 200 { + var resp cnmIpam.GetDefaultAddressSpacesResponse + err := json.NewDecoder(res.Body).Decode(&resp) + if err != nil { + log.Printf("[Azure CNS] Error received while parsing GetAddressSpace response resp:%v err:%v", res.Body, err.Error()) + return "", err + } + + if resp.Err != "" { + log.Printf("[Azure CNS] GetAddressSpace received error response :%v", resp.Err) + return "", fmt.Errorf(resp.Err) + } + + return resp.LocalDefaultAddressSpace, nil + } + log.Printf("[Azure CNS] GetAddressSpace invalid http status code: %v err:%v", res.StatusCode, err.Error()) + return "", err +} + +// GetPoolID Request to get poolID. +func (ic *IpamClient) GetPoolID(asID, subnet string) (string, error) { + var body bytes.Buffer + log.Printf("[Azure CNS] GetPoolID Request") + + url := ic.connectionURL + cnmIpam.RequestPoolPath + + payload := &cnmIpam.RequestPoolRequest{ + AddressSpace: asID, + Pool: subnet, + } + + json.NewEncoder(&body).Encode(payload) + + req, err := http.NewRequest(http.MethodGet, url, &body) + if err != nil { + log.Printf("[Azure CNS] Error received while creating http GET request for GetPoolID asID: %v poolid: %v err:%v", asID, subnet, err.Error()) + return "", err + } + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + client := &http.Client{} + res, err := client.Do(req) + + if res == nil { + return "", err + } + + if res.StatusCode == 200 { + var resp cnmIpam.RequestPoolResponse + err := json.NewDecoder(res.Body).Decode(&resp) + if err != nil { + log.Printf("[Azure CNS] Error received while parsing GetPoolID response resp:%v err:%v", res.Body, err.Error()) + return "", err + } + + if resp.Err != "" { + log.Printf("[Azure CNS] GetPoolID received error response :%v", resp.Err) + return "", fmt.Errorf(resp.Err) + } + + return resp.PoolID, nil + } + log.Printf("[Azure CNS] GetPoolID invalid http status code: %v err:%v", res.StatusCode, err.Error()) + return "", err + +} + +// ReserveIPAddress request an Ip address for the reservation id. +func (ic *IpamClient) ReserveIPAddress(poolID string, reservationID string) (string, error) { + var body bytes.Buffer + log.Printf("[Azure CNS] ReserveIpAddress") + + url := ic.connectionURL + cnmIpam.RequestAddressPath + + payload := &cnmIpam.RequestAddressRequest{ + PoolID: poolID, + Address: "", + Options: make(map[string]string), + } + payload.Options[ipam.OptAddressID] = reservationID + json.NewEncoder(&body).Encode(payload) + + req, err := http.NewRequest(http.MethodGet, url, &body) + if err != nil { + log.Printf("[Azure CNS] Error received while creating http GET request for reserve IP resid: %v poolid: %v err:%v", reservationID, poolID, err.Error()) + return "", err + } + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + client := &http.Client{} + res, err := client.Do(req) + + if res == nil { + return "", err + } + + if res.StatusCode == 200 { + var reserveResp cnmIpam.RequestAddressResponse + + err = json.NewDecoder(res.Body).Decode(&reserveResp) + if err != nil { + log.Printf("[Azure CNS] Error received while parsing reserve response resp:%v err:%v", res.Body, err.Error()) + return "", err + } + + if reserveResp.Err != "" { + log.Printf("[Azure CNS] ReserveIP received error response :%v", reserveResp.Err) + return "", fmt.Errorf(reserveResp.Err) + } + + return reserveResp.Address, nil + } + + log.Printf("[Azure CNS] ReserveIp invalid http status code: %v err:%v", res.StatusCode, err.Error()) + return "", err +} + +// ReleaseIPAddress release an Ip address for the reservation id. +func (ic *IpamClient) ReleaseIPAddress(poolID string, reservationID string) error { + var body bytes.Buffer + log.Printf("[Azure CNS] ReleaseIpAddress") + + url := ic.connectionURL + cnmIpam.ReleaseAddressPath + + payload := &cnmIpam.ReleaseAddressRequest{ + PoolID: poolID, + Address: "", + Options: make(map[string]string), + } + + payload.Options[ipam.OptAddressID] = reservationID + + json.NewEncoder(&body).Encode(payload) + + req, err := http.NewRequest(http.MethodGet, url, &body) + if err != nil { + log.Printf("[Azure CNS] Error received while creating http GET request for ReleaseIP resid: %v poolid: %v err:%v", reservationID, poolID, err.Error()) + return err + } + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + client := &http.Client{} + res, err := client.Do(req) + + if res == nil { + return err + } + + if res.StatusCode == 200 { + var releaseResp cnmIpam.ReleaseAddressResponse + err := json.NewDecoder(res.Body).Decode(&releaseResp) + if err != nil { + log.Printf("[Azure CNS] Error received while parsing release response :%v err:%v", res.Body, err.Error()) + return err + } + + if releaseResp.Err != "" { + log.Printf("[Azure CNS] ReleaseIP received error response :%v", releaseResp.Err) + return fmt.Errorf(releaseResp.Err) + } + + return nil + } + log.Printf("[Azure CNS] ReleaseIP invalid http status code: %v err:%v", res.StatusCode, err.Error()) + return err + +} + +// GetIPAddressUtilization - returns number of available, reserved and unhealthy addresses list. +func (ic *IpamClient) GetIPAddressUtilization(poolID string) (int, int, []string, error) { + var body bytes.Buffer + log.Printf("[Azure CNS] GetIPAddressUtilization") + + url := ic.connectionURL + cnmIpam.GetPoolInfoPath + + payload := &cnmIpam.GetPoolInfoRequest{ + PoolID: poolID, + } + + json.NewEncoder(&body).Encode(payload) + + req, err := http.NewRequest(http.MethodGet, url, &body) + if err != nil { + log.Printf("[Azure CNS] Error received while creating http GET request for GetIPUtilization poolid: %v err:%v", poolID, err.Error()) + return 0, 0, nil, err + } + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + client := &http.Client{} + res, err := client.Do(req) + + if res == nil { + return 0, 0, nil, err + } + + if res.StatusCode == 200 { + var poolInfoResp cnmIpam.GetPoolInfoResponse + err := json.NewDecoder(res.Body).Decode(&poolInfoResp) + if err != nil { + log.Printf("[Azure CNS] Error received while parsing GetIPUtilization response :%v err:%v", res.Body, err.Error()) + return 0, 0, nil, err + } + + if poolInfoResp.Err != "" { + log.Printf("[Azure CNS] GetIPUtilization received error response :%v", poolInfoResp.Err) + return 0, 0, nil, fmt.Errorf(poolInfoResp.Err) + } + + return poolInfoResp.Capacity, poolInfoResp.Available, poolInfoResp.UnhealthyAddresses, nil + } + log.Printf("[Azure CNS] GetIPUtilization invalid http status code: %v err:%v", res.StatusCode, err.Error()) + return 0, 0, nil, err + +} diff --git a/cns/ipamclient/ipamclient_linux.go b/cns/ipamclient/ipamclient_linux.go new file mode 100644 index 0000000000..345be8e872 --- /dev/null +++ b/cns/ipamclient/ipamclient_linux.go @@ -0,0 +1,10 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +// +build linux + +package ipamclient + +const ( + ipamPluginURL = "unix:///run/docker/plugins/" +) diff --git a/cns/ipamclient/ipamclient_test.go b/cns/ipamclient/ipamclient_test.go new file mode 100644 index 0000000000..c890ae79be --- /dev/null +++ b/cns/ipamclient/ipamclient_test.go @@ -0,0 +1,229 @@ +package ipamclient + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/Azure/azure-container-networking/common" +) + +var mux *http.ServeMux +var ipamQueryUrl = "localhost:42424" +var ic *IpamClient + +// Wraps the test run with service setup and teardown. +func TestMain(m *testing.M) { + + // Create a fake IPAM plugin to handle requests from CNS plugin. + u, _ := url.Parse("tcp://" + ipamQueryUrl) + ipamAgent, err := common.NewListener(u) + if err != nil { + fmt.Printf("Failed to create agent, err:%v.\n", err) + return + } + ipamAgent.AddHandler(getAddressSpacesPath, handleIpamAsIDQuery) + ipamAgent.AddHandler(requestPoolPath, handlePoolIDQuery) + ipamAgent.AddHandler(reserveAddrPath, handleReserveIPQuery) + ipamAgent.AddHandler(releaseAddrPath, handleReleaseIPQuery) + ipamAgent.AddHandler(getPoolInfoPath, handleIPUtilizationQuery) + + err = ipamAgent.Start(make(chan error, 1)) + if err != nil { + fmt.Printf("Failed to start agent, err:%v.\n", err) + return + } + ic, err = NewIpamClient("http://" + ipamQueryUrl) + if err != nil { + fmt.Printf("Ipam client creation failed %+v", err) + } + + // Run tests. + exitCode := m.Run() + + ipamAgent.Stop() + + os.Exit(exitCode) + +} + +// Handles queries from GetAddressSpace. +func handleIpamAsIDQuery(w http.ResponseWriter, r *http.Request) { + var addressSpaceResp = "{\"LocalDefaultAddressSpace\": \"local\", \"GlobalDefaultAddressSpace\": \"global\"}" + w.Write([]byte(addressSpaceResp)) +} + +// Handles queries from GetPoolID +func handlePoolIDQuery(w http.ResponseWriter, r *http.Request) { + var requestPoolResp = "{\"PoolID\":\"10.0.0.0/16\", \"Pool\": \"\"}" + w.Write([]byte(requestPoolResp)) +} + +// Handles queries from ReserveIPAddress. +func handleReserveIPQuery(w http.ResponseWriter, r *http.Request) { + var reserveIPResp = "{\"Address\":\"10.0.0.2/16\"}" + w.Write([]byte(reserveIPResp)) +} + +// Handles queries from ReleaseIPAddress. +func handleReleaseIPQuery(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("{}")) +} + +// Handles queries from GetIPAddressUtiltization. +func handleIPUtilizationQuery(w http.ResponseWriter, r *http.Request) { + var ipUtilizationResp = "{\"Capacity\":10, \"Available\":7, \"UnhealthyAddresses\":[\"10.0.0.5\",\"10.0.0.6\",\"10.0.0.7\"]}" + w.Write([]byte(ipUtilizationResp)) +} + +// Decodes plugin's responses to test requests. +func decodeResponse(w *httptest.ResponseRecorder, response interface{}) error { + if w.Code != http.StatusOK { + return fmt.Errorf("Request failed with HTTP error %s", w.Code) + } + + if w.Body == nil { + return fmt.Errorf("Response body is empty") + } + + return json.NewDecoder(w.Body).Decode(&response) +} + +// Tests IpamClient GetAddressSpace function to get AddressSpaceID. +func TestAddressSpaces(t *testing.T) { + asID, err := ic.GetAddressSpace() + if err != nil { + t.Errorf("GetAddressSpace failed with %v\n", err) + return + } + + if asID != "local" { + t.Errorf("GetAddressSpace failed with invalid as id %s", asID) + } +} + +// Tests IpamClient GetPoolID function to get PoolID. +func TestGetPoolID(t *testing.T) { + subnet := "10.0.0.0/16" + + asID, err := ic.GetAddressSpace() + if err != nil { + t.Errorf("GetAddressSpace failed with %v\n", err) + return + } + + poolID, err := ic.GetPoolID(asID, subnet) + if err != nil { + t.Errorf("GetPoolID failed with %v\n", err) + return + } + + if poolID != "10.0.0.0/16" { + t.Errorf("GetPoolId failed with invalid pool id %s", poolID) + } +} + +// Tests IpamClient ReserveIPAddress function to request IP for ID. +func TestReserveIP(t *testing.T) { + subnet := "10.0.0.0/16" + + asID, err := ic.GetAddressSpace() + if err != nil { + t.Errorf("GetAddressSpace failed with %v\n", err) + return + } + + poolID, err := ic.GetPoolID(asID, subnet) + if err != nil { + t.Errorf("GetPoolID failed with %v\n", err) + return + } + + addr1, err := ic.ReserveIPAddress(poolID, "id1") + if err != nil { + t.Errorf("GetReserveIP failed with %v\n", err) + return + } + if addr1 != "10.0.0.2/16" { + t.Errorf("GetReserveIP returned ivnvalid IP %s\n", addr1) + return + } + addr2, err := ic.ReserveIPAddress(poolID, "id1") + if err != nil { + t.Errorf("GetReserveIP failed with %v\n", err) + return + } + if addr1 != addr2 { + t.Errorf("GetReserveIP with id returned ivnvalid IP1 %s IP2 %s\n", addr1, addr2) + return + } + +} + +// Tests IpamClient ReleaseIPAddress function to release IP associated with ID. +func TestReleaseIP(t *testing.T) { + subnet := "10.0.0.0/16" + + asID, err := ic.GetAddressSpace() + if err != nil { + t.Errorf("GetAddressSpace failed with %v\n", err) + return + } + + poolID, err := ic.GetPoolID(asID, subnet) + if err != nil { + t.Errorf("GetPoolID failed with %v\n", err) + return + } + + addr1, err := ic.ReserveIPAddress(poolID, "id1") + if err != nil { + t.Errorf("GetReserveIP failed with %v\n", err) + return + } + if addr1 != "10.0.0.2/16" { + t.Errorf("GetReserveIP returned ivnvalid IP %s\n", addr1) + return + } + + err = ic.ReleaseIPAddress(poolID, "id1") + if err != nil { + t.Errorf("Release reservation failed with %v\n", err) + return + } +} + +// Tests IpamClient GetIPAddressUtilization function to retrieve IP Utilization info. +func TestIPAddressUtilization(t *testing.T) { + subnet := "10.0.0.0/16" + + asID, err := ic.GetAddressSpace() + if err != nil { + t.Errorf("GetAddressSpace failed with %v\n", err) + return + } + + poolID, err := ic.GetPoolID(asID, subnet) + if err != nil { + t.Errorf("GetPoolID failed with %v\n", err) + return + } + + capacity, available, unhealthyAddrs, err := ic.GetIPAddressUtilization(poolID) + if err != nil { + t.Errorf("GetIPUtilization failed with %v\n", err) + return + } + + if capacity != 10 && available != 7 && len(unhealthyAddrs) == 3 { + t.Errorf("GetIPUtilization returned invalid either capacity %v / available %v count/ unhealthyaddrs %v \n", capacity, available, unhealthyAddrs) + return + } + + log.Printf("Capacity %v Available %v Unhealthy %v", capacity, available, unhealthyAddrs) +} diff --git a/cns/ipamclient/ipamclient_windows.go b/cns/ipamclient/ipamclient_windows.go new file mode 100644 index 0000000000..e39962aec0 --- /dev/null +++ b/cns/ipamclient/ipamclient_windows.go @@ -0,0 +1,10 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +// +build windows + +package ipamclient + +const ( + defaultIpamPluginURL = "http://localhost:48080" +) diff --git a/cns/restserver/api.go b/cns/restserver/api.go new file mode 100644 index 0000000000..5065050d0b --- /dev/null +++ b/cns/restserver/api.go @@ -0,0 +1,20 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +package restserver + +// Container Network Service remote API Contract. +const ( + Success = 0 + UnsupportedNetworkType = 1 + InvalidParameter = 2 + UnsupportedEnvironment = 3 + UnreachableHost = 4 + ReservationNotFound = 5 + MalformedSubnet = 8 + UnreachableDockerDaemon = 9 + UnspecifiedNetworkName = 10 + NotFound = 14 + AddressUnavailable = 15 + UnexpectedError = 99 +) diff --git a/cns/restserver/restserver.go b/cns/restserver/restserver.go new file mode 100644 index 0000000000..ce95aa3d0c --- /dev/null +++ b/cns/restserver/restserver.go @@ -0,0 +1,726 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +package restserver + +import ( + "fmt" + "net/http" + "time" + + "net" + + "github.com/Azure/azure-container-networking/cns" + "github.com/Azure/azure-container-networking/cns/common" + "github.com/Azure/azure-container-networking/cns/dockerclient" + "github.com/Azure/azure-container-networking/cns/imdsclient" + "github.com/Azure/azure-container-networking/cns/ipamclient" + "github.com/Azure/azure-container-networking/cns/routes" + "github.com/Azure/azure-container-networking/log" + "github.com/Azure/azure-container-networking/store" +) + +const ( + // Key against which CNS state is persisted. + storeKey = "ContainerNetworkService" +) + +// httpRestService represents http listener for CNS - Container Networking Service. +type httpRestService struct { + *cns.Service + dockerClient *dockerclient.DockerClient + imdsClient *imdsclient.ImdsClient + ipamClient *ipamclient.IpamClient + routingTable *routes.RoutingTable + store store.KeyValueStore + state httpRestServiceState +} + +// httpRestServiceState contains the state we would like to persist. +type httpRestServiceState struct { + Location string + NetworkType string + Initialized bool + TimeStamp time.Time +} + +// HTTPService describes the min API interface that every service should have. +type HTTPService interface { + common.ServiceAPI +} + +// NewHTTPRestService creates a new HTTP Service object. +func NewHTTPRestService(config *common.ServiceConfig) (HTTPService, error) { + service, err := cns.NewService(config.Name, config.Version, config.Store) + if err != nil { + return nil, err + } + + imdsClient := &imdsclient.ImdsClient{} + routingTable := &routes.RoutingTable{} + dc, err := dockerclient.NewDefaultDockerClient(imdsClient) + if err != nil { + return nil, err + } + + ic, err := ipamclient.NewIpamClient("") + if err != nil { + return nil, err + } + + return &httpRestService{ + Service: service, + store: service.Service.Store, + dockerClient: dc, + imdsClient: imdsClient, + ipamClient: ic, + routingTable: routingTable, + }, nil +} + +// Start starts the CNS listener. +func (service *httpRestService) Start(config *common.ServiceConfig) error { + + err := service.Initialize(config) + if err != nil { + log.Printf("[Azure CNS] Failed to initialize base service, err:%v.", err) + return err + } + + // Add handlers. + listener := service.Listener + // default handlers + listener.AddHandler(cns.SetEnvironmentPath, service.setEnvironment) + listener.AddHandler(cns.CreateNetworkPath, service.createNetwork) + listener.AddHandler(cns.DeleteNetworkPath, service.deleteNetwork) + listener.AddHandler(cns.ReserveIPAddressPath, service.reserveIPAddress) + listener.AddHandler(cns.ReleaseIPAddressPath, service.releaseIPAddress) + listener.AddHandler(cns.GetHostLocalIPPath, service.getHostLocalIP) + listener.AddHandler(cns.GetIPAddressUtilizationPath, service.getIPAddressUtilization) + listener.AddHandler(cns.GetUnhealthyIPAddressesPath, service.getUnhealthyIPAddresses) + + // handlers for v0.1 + listener.AddHandler(cns.V1Prefix+cns.SetEnvironmentPath, service.setEnvironment) + listener.AddHandler(cns.V1Prefix+cns.CreateNetworkPath, service.createNetwork) + listener.AddHandler(cns.V1Prefix+cns.DeleteNetworkPath, service.deleteNetwork) + listener.AddHandler(cns.V1Prefix+cns.ReserveIPAddressPath, service.reserveIPAddress) + listener.AddHandler(cns.V1Prefix+cns.ReleaseIPAddressPath, service.releaseIPAddress) + listener.AddHandler(cns.V1Prefix+cns.GetHostLocalIPPath, service.getHostLocalIP) + listener.AddHandler(cns.V1Prefix+cns.GetIPAddressUtilizationPath, service.getIPAddressUtilization) + listener.AddHandler(cns.V1Prefix+cns.GetUnhealthyIPAddressesPath, service.getUnhealthyIPAddresses) + + log.Printf("[Azure CNS] Listening.") + return nil +} + +// Stop stops the CNS. +func (service *httpRestService) Stop() { + service.Uninitialize() + log.Printf("[Azure CNS] Service stopped.") +} + +// Handles requests to set the environment type. +func (service *httpRestService) setEnvironment(w http.ResponseWriter, r *http.Request) { + log.Printf("[Azure CNS] setEnvironment") + + var req cns.SetEnvironmentRequest + err := service.Listener.Decode(w, r, &req) + log.Request(service.Name, &req, err) + + if err != nil { + return + } + + switch r.Method { + case "POST": + log.Printf("[Azure CNS] POST received for SetEnvironment.") + service.state.Location = req.Location + service.state.NetworkType = req.NetworkType + service.state.Initialized = true + service.saveState() + default: + } + + resp := &cns.Response{ReturnCode: 0} + err = service.Listener.Encode(w, &resp) + + log.Response(service.Name, resp, err) +} + +// Handles CreateNetwork requests. +func (service *httpRestService) createNetwork(w http.ResponseWriter, r *http.Request) { + log.Printf("[Azure CNS] createNetwork") + + var err error + returnCode := 0 + returnMessage := "" + + if service.state.Initialized { + var req cns.CreateNetworkRequest + err = service.Listener.Decode(w, r, &req) + log.Request(service.Name, &req, err) + + if err != nil { + returnMessage = fmt.Sprintf("[Azure CNS] Error. Unable to decode input request.") + returnCode = InvalidParameter + } else { + switch r.Method { + case "POST": + dc := service.dockerClient + rt := service.routingTable + err = dc.NetworkExists(req.NetworkName) + + // Network does not exist. + if err != nil { + switch service.state.NetworkType { + case "Underlay": + switch service.state.Location { + case "Azure": + log.Printf("[Azure CNS] Goign to create network with name %v.", req.NetworkName) + + err = rt.GetRoutingTable() + if err != nil { + // We should not fail the call to create network for this. + // This is because restoring routes is a fallback mechanism in case + // network driver is not behaving as expected. + // The responsibility to restore routes is with network driver. + log.Printf("[Azure CNS] Unable to get routing table from node, %+v.", err.Error()) + } + + err = dc.CreateNetwork(req.NetworkName) + if err != nil { + returnMessage = fmt.Sprintf("[Azure CNS] Error. CreateNetwork failed %v.", err.Error()) + returnCode = UnexpectedError + } + + err = rt.RestoreRoutingTable() + if err != nil { + log.Printf("[Azure CNS] Unable to restore routing table on node, %+v.", err.Error()) + } + + case "StandAlone": + returnMessage = fmt.Sprintf("[Azure CNS] Error. Underlay network is not supported in StandAlone environment. %v.", err.Error()) + returnCode = UnsupportedEnvironment + } + case "Overlay": + returnMessage = fmt.Sprintf("[Azure CNS] Error. Overlay support not yet available. %v.", err.Error()) + returnCode = UnsupportedEnvironment + } + } else { + returnMessage = fmt.Sprintf("[Azure CNS] Received a request to create an already existing network %v", req.NetworkName) + log.Printf(returnMessage) + } + + default: + returnMessage = "[Azure CNS] Error. CreateNetwork did not receive a POST." + returnCode = InvalidParameter + } + } + + } else { + returnMessage = fmt.Sprintf("[Azure CNS] Error. CNS is not yet initialized with environment.") + returnCode = UnsupportedEnvironment + } + + resp := &cns.Response{ + ReturnCode: returnCode, + Message: returnMessage, + } + + err = service.Listener.Encode(w, &resp) + + log.Response(service.Name, resp, err) +} + +// Handles DeleteNetwork requests. +func (service *httpRestService) deleteNetwork(w http.ResponseWriter, r *http.Request) { + log.Printf("[Azure CNS] deleteNetwork") + + var req cns.DeleteNetworkRequest + returnCode := 0 + returnMessage := "" + err := service.Listener.Decode(w, r, &req) + log.Request(service.Name, &req, err) + + if err != nil { + return + } + + switch r.Method { + case "POST": + dc := service.dockerClient + err := dc.NetworkExists(req.NetworkName) + + // Network does exist + if err == nil { + log.Printf("[Azure CNS] Goign to delete network with name %v.", req.NetworkName) + err := dc.DeleteNetwork(req.NetworkName) + if err != nil { + returnMessage = fmt.Sprintf("[Azure CNS] Error. DeleteNetwork failed %v.", err.Error()) + returnCode = UnexpectedError + } + } else { + if err == fmt.Errorf("Network not found") { + log.Printf("[Azure CNS] Received a request to delete network that does not exist: %v.", req.NetworkName) + } else { + returnCode = UnexpectedError + returnMessage = err.Error() + } + } + + default: + returnMessage = "[Azure CNS] Error. DeleteNetwork did not receive a POST." + returnCode = InvalidParameter + } + + resp := &cns.Response{ + ReturnCode: returnCode, + Message: returnMessage, + } + + err = service.Listener.Encode(w, &resp) + + log.Response(service.Name, resp, err) +} + +// Handles ip reservation requests. +func (service *httpRestService) reserveIPAddress(w http.ResponseWriter, r *http.Request) { + log.Printf("[Azure CNS] reserveIPAddress") + + var req cns.ReserveIPAddressRequest + returnMessage := "" + returnCode := 0 + addr := "" + address := "" + err := service.Listener.Decode(w, r, &req) + + log.Request(service.Name, &req, err) + + if err != nil { + return + } + + if req.ReservationID == "" { + returnCode = ReservationNotFound + returnMessage = fmt.Sprintf("[Azure CNS] Error. ReservationId is empty") + } + + switch r.Method { + case "POST": + ic := service.ipamClient + + ifInfo, err := service.imdsClient.GetPrimaryInterfaceInfoFromMemory() + if err != nil { + returnMessage = fmt.Sprintf("[Azure CNS] Error. GetPrimaryIfaceInfo failed %v", err.Error()) + returnCode = UnexpectedError + break + } + + asID, err := ic.GetAddressSpace() + if err != nil { + returnMessage = fmt.Sprintf("[Azure CNS] Error. GetAddressSpace failed %v", err.Error()) + returnCode = UnexpectedError + break + } + + poolID, err := ic.GetPoolID(asID, ifInfo.Subnet) + if err != nil { + returnMessage = fmt.Sprintf("[Azure CNS] Error. GetPoolID failed %v", err.Error()) + returnCode = UnexpectedError + break + } + + addr, err = ic.ReserveIPAddress(poolID, req.ReservationID) + if err != nil { + returnMessage = fmt.Sprintf("ReserveIpAddress failed with %+v", err.Error()) + returnCode = AddressUnavailable + break + } + + addressIP, _, err := net.ParseCIDR(addr) + if err != nil { + returnMessage = fmt.Sprintf("ParseCIDR failed with %+v", err.Error()) + returnCode = UnexpectedError + break + } + address = addressIP.String() + + default: + returnMessage = "[Azure CNS] Error. ReserveIP did not receive a POST." + returnCode = InvalidParameter + + } + + resp := cns.Response{ + ReturnCode: returnCode, + Message: returnMessage, + } + reserveResp := &cns.ReserveIPAddressResponse{Response: resp, IPAddress: address} + err = service.Listener.Encode(w, &reserveResp) + + log.Response(service.Name, reserveResp, err) +} + +// Handles release ip reservation requests. +func (service *httpRestService) releaseIPAddress(w http.ResponseWriter, r *http.Request) { + log.Printf("[Azure CNS] releaseIPAddress") + + var req cns.ReleaseIPAddressRequest + returnMessage := "" + returnCode := 0 + + err := service.Listener.Decode(w, r, &req) + log.Request(service.Name, &req, err) + + if err != nil { + return + } + + if req.ReservationID == "" { + returnCode = ReservationNotFound + returnMessage = fmt.Sprintf("[Azure CNS] Error. ReservationId is empty") + } + + switch r.Method { + case "POST": + ic := service.ipamClient + + ifInfo, err := service.imdsClient.GetPrimaryInterfaceInfoFromMemory() + if err != nil { + returnMessage = fmt.Sprintf("[Azure CNS] Error. GetPrimaryIfaceInfo failed %v", err.Error()) + returnCode = UnexpectedError + break + } + + asID, err := ic.GetAddressSpace() + if err != nil { + returnMessage = fmt.Sprintf("[Azure CNS] Error. GetAddressSpace failed %v", err.Error()) + returnCode = UnexpectedError + break + } + + poolID, err := ic.GetPoolID(asID, ifInfo.Subnet) + if err != nil { + returnMessage = fmt.Sprintf("[Azure CNS] Error. GetPoolID failed %v", err.Error()) + returnCode = UnexpectedError + break + } + + err = ic.ReleaseIPAddress(poolID, req.ReservationID) + if err != nil { + returnMessage = fmt.Sprintf("ReleaseIpAddress failed with %+v", err.Error()) + returnCode = ReservationNotFound + } + + default: + returnMessage = "[Azure CNS] Error. ReleaseIP did not receive a POST." + returnCode = InvalidParameter + } + + resp := cns.Response{ + ReturnCode: returnCode, + Message: returnMessage, + } + + err = service.Listener.Encode(w, &resp) + + log.Response(service.Name, resp, err) +} + +// Retrieves the host local ip address. Containers can talk to host using this IP address. +func (service *httpRestService) getHostLocalIP(w http.ResponseWriter, r *http.Request) { + log.Printf("[Azure CNS] getHostLocalIP") + log.Request(service.Name, "getHostLocalIP", nil) + + var found bool + var errmsg string + hostLocalIP := "0.0.0.0" + + if service.state.Initialized { + switch r.Method { + case "GET": + switch service.state.NetworkType { + case "Underlay": + if service.imdsClient != nil { + piface, err := service.imdsClient.GetPrimaryInterfaceInfoFromMemory() + if err == nil { + hostLocalIP = piface.PrimaryIP + found = true + } else { + log.Printf("[Azure-CNS] Received error from GetPrimaryInterfaceInfoFromMemory. err: %v", err.Error()) + } + } + + case "Overlay": + errmsg = "[Azure-CNS] Overlay is not yet supported." + } + + default: + errmsg = "[Azure-CNS] GetHostLocalIP API expects a GET." + } + } + + returnCode := 0 + if !found { + returnCode = NotFound + if errmsg == "" { + errmsg = "[Azure-CNS] Unable to get host local ip. Check if environment is initialized.." + } + } + + resp := cns.Response{ReturnCode: returnCode, Message: errmsg} + hostLocalIPResponse := &cns.HostLocalIPAddressResponse{ + Response: resp, + IPAddress: hostLocalIP, + } + + err := service.Listener.Encode(w, &hostLocalIPResponse) + + log.Response(service.Name, hostLocalIPResponse, err) +} + +// Handles ip address utilization requests. +func (service *httpRestService) getIPAddressUtilization(w http.ResponseWriter, r *http.Request) { + log.Printf("[Azure CNS] getIPAddressUtilization") + log.Request(service.Name, "getIPAddressUtilization", nil) + + returnMessage := "" + returnCode := 0 + capacity := 0 + available := 0 + var unhealthyAddrs []string + + switch r.Method { + case "GET": + ic := service.ipamClient + + ifInfo, err := service.imdsClient.GetPrimaryInterfaceInfoFromMemory() + if err != nil { + returnMessage = fmt.Sprintf("[Azure CNS] Error. GetPrimaryIfaceInfo failed %v", err.Error()) + returnCode = UnexpectedError + break + } + + asID, err := ic.GetAddressSpace() + if err != nil { + returnMessage = fmt.Sprintf("[Azure CNS] Error. GetAddressSpace failed %v", err.Error()) + returnCode = UnexpectedError + break + } + + poolID, err := ic.GetPoolID(asID, ifInfo.Subnet) + if err != nil { + returnMessage = fmt.Sprintf("[Azure CNS] Error. GetPoolID failed %v", err.Error()) + returnCode = UnexpectedError + break + } + + capacity, available, unhealthyAddrs, err = ic.GetIPAddressUtilization(poolID) + if err != nil { + returnMessage = fmt.Sprintf("[Azure CNS] Error. GetIPUtilization failed %v", err.Error()) + returnCode = UnexpectedError + break + } + log.Printf("Capacity %v Available %v UnhealthyAddrs %v", capacity, available, unhealthyAddrs) + + default: + returnMessage = "[Azure CNS] Error. GetIPUtilization did not receive a GET." + returnCode = InvalidParameter + } + + resp := cns.Response{ + ReturnCode: returnCode, + Message: returnMessage, + } + + utilResponse := &cns.IPAddressesUtilizationResponse{ + Response: resp, + Available: available, + Reserved: capacity - available, + Unhealthy: len(unhealthyAddrs), + } + + err := service.Listener.Encode(w, &utilResponse) + + log.Response(service.Name, utilResponse, err) +} + +// Handles retrieval of ip addresses that are available to be reserved from ipam driver. +func (service *httpRestService) getAvailableIPAddresses(w http.ResponseWriter, r *http.Request) { + log.Printf("[Azure CNS] getAvailableIPAddresses") + log.Request(service.Name, "getAvailableIPAddresses", nil) + + switch r.Method { + case "GET": + default: + } + + resp := cns.Response{ReturnCode: 0} + ipResp := &cns.GetIPAddressesResponse{Response: resp} + err := service.Listener.Encode(w, &ipResp) + + log.Response(service.Name, ipResp, err) +} + +// Handles retrieval of reserved ip addresses from ipam driver. +func (service *httpRestService) getReservedIPAddresses(w http.ResponseWriter, r *http.Request) { + log.Printf("[Azure CNS] getReservedIPAddresses") + log.Request(service.Name, "getReservedIPAddresses", nil) + + switch r.Method { + case "GET": + default: + } + + resp := cns.Response{ReturnCode: 0} + ipResp := &cns.GetIPAddressesResponse{Response: resp} + err := service.Listener.Encode(w, &ipResp) + + log.Response(service.Name, ipResp, err) +} + +// Handles retrieval of ghost ip addresses from ipam driver. +func (service *httpRestService) getUnhealthyIPAddresses(w http.ResponseWriter, r *http.Request) { + log.Printf("[Azure CNS] getUnhealthyIPAddresses") + log.Request(service.Name, "getUnhealthyIPAddresses", nil) + + returnMessage := "" + returnCode := 0 + capacity := 0 + available := 0 + var unhealthyAddrs []string + + switch r.Method { + case "GET": + ic := service.ipamClient + + ifInfo, err := service.imdsClient.GetPrimaryInterfaceInfoFromMemory() + if err != nil { + returnMessage = fmt.Sprintf("[Azure CNS] Error. GetPrimaryIfaceInfo failed %v", err.Error()) + returnCode = UnexpectedError + break + } + + asID, err := ic.GetAddressSpace() + if err != nil { + returnMessage = fmt.Sprintf("[Azure CNS] Error. GetAddressSpace failed %v", err.Error()) + returnCode = UnexpectedError + break + } + + poolID, err := ic.GetPoolID(asID, ifInfo.Subnet) + if err != nil { + returnMessage = fmt.Sprintf("[Azure CNS] Error. GetPoolID failed %v", err.Error()) + returnCode = UnexpectedError + break + } + + capacity, available, unhealthyAddrs, err = ic.GetIPAddressUtilization(poolID) + if err != nil { + returnMessage = fmt.Sprintf("[Azure CNS] Error. GetIPUtilization failed %v", err.Error()) + returnCode = UnexpectedError + break + } + log.Printf("Capacity %v Available %v UnhealthyAddrs %v", capacity, available, unhealthyAddrs) + + default: + returnMessage = "[Azure CNS] Error. GetUnhealthyIP did not receive a POST." + returnCode = InvalidParameter + } + + resp := cns.Response{ + ReturnCode: returnCode, + Message: returnMessage, + } + + ipResp := &cns.GetIPAddressesResponse{ + Response: resp, + IPAddresses: unhealthyAddrs, + } + + err := service.Listener.Encode(w, &ipResp) + + log.Response(service.Name, ipResp, err) +} + +// getAllIPAddresses retrieves all ip addresses from ipam driver. +func (service *httpRestService) getAllIPAddresses(w http.ResponseWriter, r *http.Request) { + log.Printf("[Azure CNS] getAllIPAddresses") + log.Request(service.Name, "getAllIPAddresses", nil) + + switch r.Method { + case "GET": + default: + } + + resp := cns.Response{ReturnCode: 0} + ipResp := &cns.GetIPAddressesResponse{Response: resp} + err := service.Listener.Encode(w, &ipResp) + + log.Response(service.Name, ipResp, err) +} + +// Handles health report requests. +func (service *httpRestService) getHealthReport(w http.ResponseWriter, r *http.Request) { + log.Printf("[Azure CNS] getHealthReport") + log.Request(service.Name, "getHealthReport", nil) + + switch r.Method { + case "GET": + default: + } + + resp := &cns.Response{ReturnCode: 0} + err := service.Listener.Encode(w, &resp) + + log.Response(service.Name, resp, err) +} + +// saveState writes CNS state to persistent store. +func (service *httpRestService) saveState() error { + log.Printf("[Azure CNS] saveState") + + // Skip if a store is not provided. + if service.store == nil { + log.Printf("[Azure CNS] store not initialized.") + return nil + } + + // Update time stamp. + service.state.TimeStamp = time.Now() + err := service.store.Write(storeKey, &service.state) + if err == nil { + log.Printf("[Azure CNS] State saved successfully.\n") + } else { + log.Printf("[Azure CNS] Failed to save state., err:%v\n", err) + } + + return err +} + +// restoreState restores CNS state from persistent store. +func (service *httpRestService) restoreState() error { + log.Printf("[Azure CNS] restoreState") + + // Skip if a store is not provided. + if service.store == nil { + log.Printf("[Azure CNS] store not initialized.") + return nil + } + + // Read any persisted state. + err := service.store.Read(storeKey, &service.state) + if err != nil { + if err == store.ErrKeyNotFound { + // Nothing to restore. + log.Printf("[Azure CNS] No state to restore.\n") + return nil + } + + log.Printf("[Azure CNS] Failed to restore state, err:%v\n", err) + return err + } + + log.Printf("[Azure CNS] Restored state, %+v\n", service.state) + return nil +} diff --git a/cns/restserver/restserver_test.go b/cns/restserver/restserver_test.go new file mode 100644 index 0000000000..9e9247862f --- /dev/null +++ b/cns/restserver/restserver_test.go @@ -0,0 +1,267 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +package restserver + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/Azure/azure-container-networking/cns" + "github.com/Azure/azure-container-networking/cns/common" +) + +var service HTTPService +var mux *http.ServeMux + +// Wraps the test run with service setup and teardown. +func TestMain(m *testing.M) { + var config common.ServiceConfig + var err error + + // Create the service. + service, err = NewHTTPRestService(&config) + if err != nil { + fmt.Printf("Failed to create CNS object %v\n", err) + os.Exit(1) + } + + // Configure test mode. + service.(*httpRestService).Name = "cns-test-server" + + // Start the service. + err = service.Start(&config) + if err != nil { + fmt.Printf("Failed to start CNS %v\n", err) + os.Exit(2) + } + + // Get the internal http mux as test hook. + mux = service.(*httpRestService).Listener.GetMux() + + // Run tests. + exitCode := m.Run() + + // Cleanup. + service.Stop() + + os.Exit(exitCode) +} + +// Decodes service's responses to test requests. +func decodeResponse(w *httptest.ResponseRecorder, response interface{}) error { + if w.Code != http.StatusOK { + return fmt.Errorf("Request failed with HTTP error %d", w.Code) + } + + if w.Body == nil { + return fmt.Errorf("Response body is empty") + } + + return json.NewDecoder(w.Body).Decode(&response) +} + +func setEnv(t *testing.T) *httptest.ResponseRecorder { + envRequest := cns.SetEnvironmentRequest{Location: "Azure", NetworkType: "Underlay"} + envRequestJSON := new(bytes.Buffer) + json.NewEncoder(envRequestJSON).Encode(envRequest) + + req, err := http.NewRequest(http.MethodPost, cns.V1Prefix+cns.SetEnvironmentPath, envRequestJSON) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + return w +} + +func TestSetEnvironment(t *testing.T) { + fmt.Println("Test: SetEnvironment") + + var resp cns.Response + w := setEnv(t) + + err := decodeResponse(w, &resp) + if err != nil || resp.ReturnCode != 0 { + t.Errorf("SetEnvironment failed with response %+v", resp) + } else { + fmt.Printf("SetEnvironment Responded with %+v\n", resp) + } +} + +// Tests CreateNetwork functionality. +func TestCreateNetwork(t *testing.T) { + fmt.Println("Test: CreateNetwork") + + var body bytes.Buffer + setEnv(t) + info := &cns.CreateNetworkRequest{ + NetworkName: "azurenet", + } + + json.NewEncoder(&body).Encode(info) + + req, err := http.NewRequest(http.MethodPost, cns.CreateNetworkPath, &body) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + var resp cns.Response + + err = decodeResponse(w, &resp) + if err != nil || resp.ReturnCode != 0 { + t.Errorf("CreateNetwork failed with response %+v", resp) + } else { + fmt.Printf("CreateNetwork Responded with %+v\n", resp) + } +} + +// Tests DeleteNetwork functionality. +func TestDeleteNetwork(t *testing.T) { + fmt.Println("Test: DeleteNetwork") + + var body bytes.Buffer + setEnv(t) + info := &cns.DeleteNetworkRequest{ + NetworkName: "azurenet", + } + + json.NewEncoder(&body).Encode(info) + + req, err := http.NewRequest(http.MethodPost, cns.DeleteNetworkPath, &body) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + var resp cns.Response + + err = decodeResponse(w, &resp) + if err != nil || resp.ReturnCode != 0 { + t.Errorf("DeleteNetwork failed with response %+v", resp) + } else { + fmt.Printf("DeleteNetwork Responded with %+v\n", resp) + } +} + +func TestReserveIPAddress(t *testing.T) { + fmt.Println("Test: ReserveIPAddress") + + reserveIPRequest := cns.ReserveIPAddressRequest{ReservationID: "ip01"} + reserveIPRequestJSON := new(bytes.Buffer) + json.NewEncoder(reserveIPRequestJSON).Encode(reserveIPRequest) + envRequest := cns.SetEnvironmentRequest{Location: "Azure", NetworkType: "Underlay"} + envRequestJSON := new(bytes.Buffer) + json.NewEncoder(envRequestJSON).Encode(envRequest) + + req, err := http.NewRequest(http.MethodPost, cns.ReserveIPAddressPath, envRequestJSON) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + var reserveIPAddressResponse cns.ReserveIPAddressResponse + + err = decodeResponse(w, &reserveIPAddressResponse) + if err != nil || reserveIPAddressResponse.Response.ReturnCode != 0 { + t.Errorf("SetEnvironment failed with response %+v", reserveIPAddressResponse) + } else { + fmt.Printf("SetEnvironment Responded with %+v\n", reserveIPAddressResponse) + } +} + +func TestReleaseIPAddress(t *testing.T) { + fmt.Println("Test: ReleaseIPAddress") + + releaseIPRequest := cns.ReleaseIPAddressRequest{ReservationID: "ip01"} + releaseIPAddressRequestJSON := new(bytes.Buffer) + json.NewEncoder(releaseIPAddressRequestJSON).Encode(releaseIPRequest) + + req, err := http.NewRequest(http.MethodPost, cns.ReleaseIPAddressPath, releaseIPAddressRequestJSON) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + var releaseIPAddressResponse cns.Response + + err = decodeResponse(w, &releaseIPAddressResponse) + if err != nil || releaseIPAddressResponse.ReturnCode != 0 { + t.Errorf("SetEnvironment failed with response %+v", releaseIPAddressResponse) + } else { + fmt.Printf("SetEnvironment Responded with %+v\n", releaseIPAddressResponse) + } +} + +func TestGetIPAddressUtilization(t *testing.T) { + fmt.Println("Test: GetIPAddressUtilization") + + req, err := http.NewRequest(http.MethodGet, cns.GetIPAddressUtilizationPath, nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + var iPAddressesUtilizationResponse cns.IPAddressesUtilizationResponse + + err = decodeResponse(w, &iPAddressesUtilizationResponse) + if err != nil || iPAddressesUtilizationResponse.Response.ReturnCode != 0 { + t.Errorf("GetIPAddressUtilization failed with response %+v", iPAddressesUtilizationResponse) + } else { + fmt.Printf("GetIPAddressUtilization Responded with %+v\n", iPAddressesUtilizationResponse) + } +} + +func TestGetHostLocalIP(t *testing.T) { + fmt.Println("Test: GetHostLocalIP") + + setEnv(t) + + req, err := http.NewRequest(http.MethodGet, cns.GetHostLocalIPPath, nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + var hostLocalIPAddressResponse cns.HostLocalIPAddressResponse + + err = decodeResponse(w, &hostLocalIPAddressResponse) + if err != nil || hostLocalIPAddressResponse.Response.ReturnCode != 0 { + t.Errorf("GetHostLocalIP failed with response %+v", hostLocalIPAddressResponse) + } else { + fmt.Printf("GetHostLocalIP Responded with %+v\n", hostLocalIPAddressResponse) + } +} + +func TestGetUnhealthyIPAddresses(t *testing.T) { + fmt.Println("Test: GetGhostIPAddresses") + + req, err := http.NewRequest(http.MethodGet, cns.GetUnhealthyIPAddressesPath, nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + var getIPAddressesResponse cns.GetIPAddressesResponse + + err = decodeResponse(w, &getIPAddressesResponse) + if err != nil || getIPAddressesResponse.Response.ReturnCode != 0 { + t.Errorf("GetUnhealthyIPAddresses failed with response %+v", getIPAddressesResponse) + } else { + fmt.Printf("GetUnhealthyIPAddresses Responded with %+v\n", getIPAddressesResponse) + } +} diff --git a/cns/routes/routes.go b/cns/routes/routes.go new file mode 100644 index 0000000000..ff758b51b1 --- /dev/null +++ b/cns/routes/routes.go @@ -0,0 +1,42 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +package routes + +import ( + "github.com/Azure/azure-container-networking/log" +) + +// Route describes a single route in the routing table. +type Route struct { + destination string + mask string + gateway string + metric string + ifaceIndex int +} + +// RoutingTable describes the routing table on the node. +type RoutingTable struct { + Routes []Route +} + +// GetRoutingTable retireves routing table in the node. +func (rt *RoutingTable) GetRoutingTable() error { + routes, err := getRoutes() + if err == nil { + rt.Routes = routes + } + + return err +} + +// RestoreRoutingTable pushes the saved route. +func (rt *RoutingTable) RestoreRoutingTable() error { + if rt.Routes == nil { + log.Printf("[Azure CNS] Nothing available in routing table to push") + return nil + } + + return putRoutes(rt.Routes) +} diff --git a/cns/routes/routes_linux.go b/cns/routes/routes_linux.go new file mode 100644 index 0000000000..ea043f7e46 --- /dev/null +++ b/cns/routes/routes_linux.go @@ -0,0 +1,26 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +// +build linux + +package routes + +import ( + "fmt" + "net" + "os/exec" + + "strings" + + "github.com/Azure/azure-container-networking/log" +) + +const () + +func getRoutes() ([]Route, error) { + return nil, nil +} + +func putRoutes(routes []Route) error { + return nil +} diff --git a/cns/routes/routes_test.go b/cns/routes/routes_test.go new file mode 100644 index 0000000000..1fa3226b51 --- /dev/null +++ b/cns/routes/routes_test.go @@ -0,0 +1,113 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +package routes + +import ( + "github.com/Azure/azure-container-networking/log" + "os" + "os/exec" + "testing" +) + +const ( + testDest = "159.254.169.254" + testMask = "255.255.255.255" + testGateway = "0.0.0.0" + testMetric = "12" +) + +// Wraps the test run. +func TestMain(m *testing.M) { + // Run tests. + exitCode := m.Run() + os.Exit(exitCode) +} + +func addTestRoute() error { + arg := []string{"/C", "route", "ADD", testDest, + "MASK", testMask, testGateway, "METRIC", testMetric} + log.Printf("[Azure CNS] Adding missing route: %v", arg) + + c := exec.Command("cmd", arg...) + bytes, err := c.Output() + if err == nil { + log.Printf("[Azure CNS] Successfully executed add route: %v\n%v", + arg, string(bytes)) + } else { + log.Printf("[Azure CNS] Failed to execute add route: %v\n%v\n%v", + arg, string(bytes), err.Error()) + return err + } + + return nil +} + +func deleteTestRoute() error { + args := []string{"/C", "route", "DELETE", testDest, "MASK", testMask, + testGateway, "METRIC", testMetric} + log.Printf("[Azure CNS] Deleting route: %v", args) + + c := exec.Command("cmd", args...) + bytes, err := c.Output() + if err == nil { + log.Printf("[Azure CNS] Successfully executed delete route: %v\n%v", + args, string(bytes)) + } else { + log.Printf("[Azure CNS] Failed to execute delete route: %v\n%v\n%v", + args, string(bytes), err.Error()) + return err + } + + return nil +} + +// TestPutRoutes tests if a missing route is properly restored or not. +func TestRestoreMissingRoute(t *testing.T) { + log.Printf("Test: PutMissingRoutes") + + err := addTestRoute() + if err != nil { + t.Errorf("add route failed %+v", err.Error()) + } + + rt := &RoutingTable{} + + // save routing table. + rt.GetRoutingTable() + cntr := 0 + for _, rt := range rt.Routes { + log.Printf("[]: Route[%d]: %+v", cntr, rt) + cntr++ + } + + // now delete the route so it goes missing. + err = deleteTestRoute() + if err != nil { + t.Errorf("delete route failed %+v", err.Error()) + } + + // now restore the deleted route. + rt.RestoreRoutingTable() + + // test if route was resotred or not. + rt.GetRoutingTable() + restored := false + for _, existingRoute := range rt.Routes { + log.Printf("Comapring %+v\n", existingRoute) + if existingRoute.destination == testDest && + existingRoute.gateway == testGateway && + existingRoute.mask == testMask { + restored = true + } + } + + if !restored { + t.Errorf("unable to restore missing route") + } else { + err = deleteTestRoute() + if err != nil { + t.Errorf("delete route failed %+v", err.Error()) + } + } +} diff --git a/cns/routes/routes_windows.go b/cns/routes/routes_windows.go new file mode 100644 index 0000000000..521577575b --- /dev/null +++ b/cns/routes/routes_windows.go @@ -0,0 +1,179 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +// +build windows + +package routes + +import ( + "fmt" + "net" + "os/exec" + "strings" + + "github.com/Azure/azure-container-networking/log" +) + +const ( + ipv4RoutingTableStart = "IPv4 Route Table" + activeRoutesStart = "Active Routes:" +) + +func getInterfaceByAddress(address string) (int, error) { + log.Printf("[Azure CNS] getInterfaceByAddress") + + var ifaces []net.Interface + log.Printf("[Azure CNS] Going to obtain interface for address %s", address) + ifaces, err := net.Interfaces() + if err != nil { + return -1, err + } + + for i := 0; i < len(ifaces); i++ { + log.Debugf("[Azure CNS] Going to check interface %v", ifaces[i].Name) + addrs, _ := ifaces[i].Addrs() + for _, addr := range addrs { + log.Debugf("[Azure CNS] ipAddress being compared input=%v %v\n", + address, addr.String()) + ip := strings.Split(addr.String(), "/") + if len(ip) != 2 { + return -1, fmt.Errorf("Malformed ip: %v", addr.String()) + } + if ip[0] == address { + return ifaces[i].Index, nil + } + } + } + + return -1, fmt.Errorf( + "[Azure CNS] Unable to determine interface index for address %s", + address) +} + +func getRoutes() ([]Route, error) { + log.Printf("[Azure CNS] getRoutes") + + c := exec.Command("cmd", "/C", "route", "print") + var routePrintOutput string + var routeCount int + bytes, err := c.Output() + if err == nil { + routePrintOutput = string(bytes) + log.Debugf("[Azure CNS] Printing Routing table \n %v\n", routePrintOutput) + } else { + log.Printf("Received error in printing routing table %v", err.Error()) + return nil, err + } + + routePrint := strings.Split(routePrintOutput, ipv4RoutingTableStart) + routeTable := strings.Split(routePrint[1], activeRoutesStart) + tokens := strings.Split( + strings.Split(routeTable[1], "Metric")[1], + "=") + table := tokens[0] + routes := strings.Split(table, "\r") + routeCount = len(routes) + log.Debugf("[Azure CNS] Recevied route count: %d", routeCount) + if routeCount == 0 { + return nil, nil + } + + localRoutes := make([]Route, routeCount) + cntr := 0 + truncated := 0 + for _, route := range routes { + if route != "" { + tokens := strings.Fields(route) + if len(tokens) != 5 { + log.Printf("[Azure CNS] Ignoring route %s", route) + truncated++ + } else { + log.Debugf("[Azure CNS] Parsing route: %s %s %s %s %s\n", + tokens[0], tokens[1], tokens[2], tokens[3], tokens[4]) + rt := Route{ + destination: tokens[0], + mask: tokens[1], + gateway: tokens[2], + metric: tokens[4], + } + + if rt.gateway == "On-link" { + rt.gateway = "0.0.0.0" + } + + index, err := getInterfaceByAddress(tokens[3]) + if err == nil { + rt.ifaceIndex = index + localRoutes[cntr] = rt + cntr++ + } else { + log.Printf("[Azure CNS] Error encountered while obtaining index. %v\n", err.Error()) + truncated++ + } + } + } + } + + if truncated == routeCount { + localRoutes = nil + } else { + localRoutes = localRoutes[0 : routeCount-truncated-1] + } + + return localRoutes, nil +} + +func containsRoute(routes []Route, route Route) (bool, error) { + log.Printf("[Azure CNS] containsRoute") + if routes == nil { + return false, nil + } + for _, existingRoute := range routes { + if existingRoute.destination == route.destination && + existingRoute.gateway == route.gateway && + existingRoute.ifaceIndex == route.ifaceIndex && + existingRoute.mask == route.mask { + return true, nil + } + } + return false, nil +} + +func putRoutes(routes []Route) error { + log.Printf("[Azure CNS] putRoutes") + + var err error + log.Printf("[Azure CNS] Going to get current routes") + currentRoutes, err := getRoutes() + if err != nil { + return err + } + + for _, route := range routes { + exists, err := containsRoute(currentRoutes, route) + if err == nil && !exists { + args := []string{"/C", "route", "ADD", + route.destination, + "MASK", + route.mask, + route.gateway, + "METRIC", + route.metric, + "IF", + fmt.Sprintf("%d", route.ifaceIndex)} + log.Printf("[Azure CNS] Adding missing route: %v", args) + + c := exec.Command("cmd", args...) + bytes, err := c.Output() + if err == nil { + log.Printf("[Azure CNS] Successfully executed add route: %v\n%v", args, string(bytes)) + } else { + log.Printf("[Azure CNS] Failed to execute add route: %v\n%v", args, string(bytes)) + } + } else { + log.Printf("[Azure CNS] Route already exists. skipping %+v", route) + } + } + + return err +} diff --git a/cns/service.go b/cns/service.go new file mode 100644 index 0000000000..62e8707913 --- /dev/null +++ b/cns/service.go @@ -0,0 +1,105 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +package cns + +import ( + "net/http" + "net/url" + + "github.com/Azure/azure-container-networking/cns/common" + acn "github.com/Azure/azure-container-networking/common" + "github.com/Azure/azure-container-networking/log" + "github.com/Azure/azure-container-networking/store" +) + +const ( + // Default CNS server URL. + defaultAPIServerURL = "tcp://localhost:10090" + genericData = "com.microsoft.azure.network.generic" +) + +// Service defines Container Networking Service. +type Service struct { + *common.Service + EndpointType string + Listener *acn.Listener +} + +// NewService creates a new Service object. +func NewService(name, version string, store store.KeyValueStore) (*Service, error) { + service, err := common.NewService(name, version, store) + + if err != nil { + return nil, err + } + + return &Service{ + Service: service, + }, nil +} + +// GetAPIServerURL returns the API server URL. +func (service *Service) getAPIServerURL() string { + urls, _ := service.GetOption(acn.OptAPIServerURL).(string) + if urls == "" { + urls = defaultAPIServerURL + } + + return urls +} + +// Initialize initializes the service and starts the listener. +func (service *Service) Initialize(config *common.ServiceConfig) error { + log.Debugf("[Azure CNS] Going to initialize a service with config: %+v", config) + + // Initialize the base service. + service.Service.Initialize(config) + + // Initialize the listener. + if config.Listener == nil { + // Fetch and parse the API server URL. + u, err := url.Parse(service.getAPIServerURL()) + if err != nil { + return err + } + + // Create the listener. + listener, err := acn.NewListener(u) + if err != nil { + return err + } + + // Start the listener. + err = listener.Start(config.ErrChan) + if err != nil { + return err + } + + config.Listener = listener + } + + service.Listener = config.Listener + + log.Debugf("[Azure CNS] Successfully initialized a service with config: %+v", config) + return nil +} + +// Uninitialize cleans up the plugin. +func (service *Service) Uninitialize() { + service.Listener.Stop() + service.Service.Uninitialize() +} + +// ParseOptions returns generic options from a libnetwork request. +func (service *Service) ParseOptions(options OptionMap) OptionMap { + opt, _ := options[genericData].(OptionMap) + return opt +} + +// SendErrorResponse sends and logs an error response. +func (service *Service) SendErrorResponse(w http.ResponseWriter, errMsg error) { + resp := errorResponse{errMsg.Error()} + err := service.Listener.Encode(w, &resp) + log.Response(service.Name, &resp, err) +} diff --git a/cns/service/main.go b/cns/service/main.go new file mode 100644 index 0000000000..99ec1e5937 --- /dev/null +++ b/cns/service/main.go @@ -0,0 +1,153 @@ +// Copyright 2017 Microsoft. All rights reserved. +// MIT License + +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/Azure/azure-container-networking/cns/common" + "github.com/Azure/azure-container-networking/cns/restserver" + acn "github.com/Azure/azure-container-networking/common" + "github.com/Azure/azure-container-networking/log" + "github.com/Azure/azure-container-networking/platform" + "github.com/Azure/azure-container-networking/store" +) + +const ( + // Service name. + name = "azure-cns" +) + +// Version is populated by make during build. +var version string + +// Command line arguments for CNM plugin. +var args = acn.ArgumentList{ + { + Name: acn.OptAPIServerURL, + Shorthand: acn.OptAPIServerURLAlias, + Description: "Set the API server URL", + Type: "string", + DefaultValue: "", + }, + { + Name: acn.OptLogLevel, + Shorthand: acn.OptLogLevelAlias, + Description: "Set the logging level", + Type: "int", + DefaultValue: acn.OptLogLevelInfo, + ValueMap: map[string]interface{}{ + acn.OptLogLevelInfo: log.LevelInfo, + acn.OptLogLevelDebug: log.LevelDebug, + }, + }, + { + Name: acn.OptLogTarget, + Shorthand: acn.OptLogTargetAlias, + Description: "Set the logging target", + Type: "int", + DefaultValue: acn.OptLogTargetFile, + ValueMap: map[string]interface{}{ + acn.OptLogTargetSyslog: log.TargetSyslog, + acn.OptLogTargetStderr: log.TargetStderr, + acn.OptLogTargetFile: log.TargetLogfile, + }, + }, + { + Name: acn.OptVersion, + Shorthand: acn.OptVersionAlias, + Description: "Print version information", + Type: "bool", + DefaultValue: false, + }, +} + +// Prints description and version information. +func printVersion() { + fmt.Printf("Azure Container Network Service\n") + fmt.Printf("Version %v\n", version) +} + +// Main is the entry point for CNS. +func main() { + // Initialize and parse command line arguments. + acn.ParseArgs(&args, printVersion) + + url := acn.GetArg(acn.OptAPIServerURL).(string) + logLevel := acn.GetArg(acn.OptLogLevel).(int) + logTarget := acn.GetArg(acn.OptLogTarget).(int) + vers := acn.GetArg(acn.OptVersion).(bool) + + if vers { + printVersion() + os.Exit(0) + } + + // Initialize CNS. + var config common.ServiceConfig + config.Version = version + config.Name = name + + // Create a channel to receive unhandled errors from CNS. + config.ErrChan = make(chan error, 1) + + // Create the key value store. + var err error + config.Store, err = store.NewJsonFileStore(platform.RuntimePath + name + ".json") + if err != nil { + fmt.Printf("Failed to create store: %v\n", err) + return + } + + // Create CNS object. + httpRestService, err := restserver.NewHTTPRestService(&config) + if err != nil { + fmt.Printf("Failed to create CNS object, err:%v.\n", err) + return + } + + // Create logging provider. + log.SetName(name) + log.SetLevel(logLevel) + err = log.SetTarget(logTarget) + if err != nil { + fmt.Printf("Failed to configure logging: %v\n", err) + return + } + + // Log platform information. + log.Printf("Running on %v", platform.GetOSInfo()) + + // Set CNS options. + httpRestService.SetOption(acn.OptAPIServerURL, url) + + // Start CNS. + if httpRestService != nil { + err = httpRestService.Start(&config) + if err != nil { + fmt.Printf("Failed to start CNS, err:%v.\n", err) + return + } + } + + // Relay these incoming signals to OS signal channel. + osSignalChannel := make(chan os.Signal, 1) + signal.Notify(osSignalChannel, os.Interrupt, os.Kill, syscall.SIGTERM) + + // Wait until receiving a signal. + select { + case sig := <-osSignalChannel: + log.Printf("CNS Received OS signal <" + sig.String() + ">, shutting down.") + case err := <-config.ErrChan: + log.Printf("CNS Received unhandled error %v, shutting down.", err) + } + + // Cleanup. + if httpRestService != nil { + httpRestService.Stop() + } +}