cns/restserver/api.go (1,060 lines of code) (raw):

// Copyright 2017 Microsoft. All rights reserved. // MIT License package restserver import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "regexp" "runtime" "strings" "github.com/Azure/azure-container-networking/cns" "github.com/Azure/azure-container-networking/cns/hnsclient" "github.com/Azure/azure-container-networking/cns/logger" "github.com/Azure/azure-container-networking/cns/types" "github.com/Azure/azure-container-networking/cns/wireserver" "github.com/Azure/azure-container-networking/common" "github.com/Azure/azure-container-networking/nmagent" "github.com/pkg/errors" ) var ( ncRegex = regexp.MustCompile(`NetworkManagement/interfaces/(.{0,36})/networkContainers/(.{0,36})/authenticationToken/(.{0,36})/api-version/1(/method/DELETE)?`) ErrInvalidNcURLFormat = errors.New("Invalid network container url format") ) // ncURLExpectedMatches defines the size of matches expected from exercising the ncRegex // 1) the original url (nuance related to golangs regex package) // 2) the associated interface parameter // 3) the ncid parameter // 4) the authentication token parameter // 5) the optional delete path const ( ncURLExpectedMatches = 5 ) // This file contains implementation of all HTTP APIs which are exposed to external clients. // TODO: break it even further per module (network, nc, etc) like it is done for ipam // Handles requests to set the environment type. func (service *HTTPRestService) setEnvironment(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure CNS] setEnvironment") var req cns.SetEnvironmentRequest err := common.Decode(w, r, &req) logger.Request(service.Name, &req, err) if err != nil { return } switch r.Method { case http.MethodPost: logger.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 = common.Encode(w, &resp) logger.Response(service.Name, resp, resp.ReturnCode, err) } // Handles CreateNetwork requests. func (service *HTTPRestService) createNetwork(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure CNS] createNetwork") var err error var returnCode types.ResponseCode returnMessage := "" if service.state.Initialized { var req cns.CreateNetworkRequest err = common.Decode(w, r, &req) logger.Request(service.Name, &req, err) if err != nil { //nolint:goconst // ignore const string returnMessage = "[Azure CNS] Error. Unable to decode input request." returnCode = types.InvalidParameter } else { switch r.Method { case http.MethodPost: 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": logger.Printf("[Azure CNS] Creating 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. logger.Printf("[Azure CNS] Unable to get routing table from node, %+v.", err.Error()) } var nicInfo *wireserver.InterfaceInfo nicInfo, err = service.getPrimaryHostInterface(context.TODO()) if err != nil { returnMessage = fmt.Sprintf("[Azure CNS] Error. GetPrimaryInterfaceInfoFromHost failed %v.", err.Error()) returnCode = types.UnexpectedError break } err = dc.CreateNetwork(req.NetworkName, nicInfo, req.Options) if err != nil { returnMessage = fmt.Sprintf("[Azure CNS] Error. CreateNetwork failed %v.", err.Error()) returnCode = types.UnexpectedError } err = rt.RestoreRoutingTable() if err != nil { logger.Printf("[Azure CNS] Unable to restore routing table on node, %+v.", err.Error()) } networkInfo := &networkInfo{ NetworkName: req.NetworkName, NicInfo: nicInfo, Options: req.Options, } service.state.Networks[req.NetworkName] = networkInfo case "StandAlone": returnMessage = fmt.Sprintf("[Azure CNS] Error. Underlay network is not supported in StandAlone environment. %v.", err.Error()) returnCode = types.UnsupportedEnvironment } case "Overlay": returnMessage = fmt.Sprintf("[Azure CNS] Error. Overlay support not yet available. %v.", err.Error()) returnCode = types.UnsupportedEnvironment } } else { returnMessage = fmt.Sprintf("[Azure CNS] Received a request to create an already existing network %v", req.NetworkName) logger.Printf(returnMessage) } default: returnMessage = "[Azure CNS] Error. CreateNetwork did not receive a POST." returnCode = types.InvalidParameter } } } else { returnMessage = "[Azure CNS] Error. CNS is not yet initialized with environment." returnCode = types.UnsupportedEnvironment } resp := &cns.Response{ ReturnCode: returnCode, Message: returnMessage, } err = common.Encode(w, &resp) if returnCode == 0 { service.saveState() } logger.Response(service.Name, resp, resp.ReturnCode, err) } // Handles DeleteNetwork requests. func (service *HTTPRestService) deleteNetwork(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure CNS] deleteNetwork") var req cns.DeleteNetworkRequest var returnCode types.ResponseCode returnMessage := "" err := common.Decode(w, r, &req) logger.Request(service.Name, &req, err) if err != nil { return } switch r.Method { case http.MethodPost: dc := service.dockerClient err := dc.NetworkExists(req.NetworkName) // Network does exist if err == nil { logger.Printf("[Azure CNS] Deleting 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 = types.UnexpectedError } } else { if err == fmt.Errorf("Network not found") { logger.Printf("[Azure CNS] Received a request to delete network that does not exist: %v.", req.NetworkName) } else { returnCode = types.UnexpectedError returnMessage = err.Error() } } default: returnMessage = "[Azure CNS] Error. DeleteNetwork did not receive a POST." returnCode = types.InvalidParameter } resp := &cns.Response{ ReturnCode: returnCode, Message: returnMessage, } err = common.Encode(w, &resp) if returnCode == 0 { service.removeNetworkInfo(req.NetworkName) service.saveState() } logger.Response(service.Name, resp, resp.ReturnCode, err) } // Handles CreateHnsNetwork requests. func (service *HTTPRestService) createHnsNetwork(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure CNS] createHnsNetwork") var err error var returnCode types.ResponseCode returnMessage := "" var req cns.CreateHnsNetworkRequest err = common.Decode(w, r, &req) logger.Request(service.Name, &req, err) if err != nil { //nolint:goconst returnMessage = "[Azure CNS] Error. Unable to decode input request." returnCode = types.InvalidParameter } else { switch r.Method { case http.MethodPost: if err := hnsclient.CreateHnsNetwork(req); err == nil { // Save the newly created HnsNetwork name. CNS deleteHnsNetwork API // will only allow deleting these networks. networkInfo := &networkInfo{ NetworkName: req.NetworkName, } service.setNetworkInfo(req.NetworkName, networkInfo) returnMessage = fmt.Sprintf("[Azure CNS] Successfully created HNS network: %s", req.NetworkName) } else { returnMessage = fmt.Sprintf("[Azure CNS] CreateHnsNetwork failed with error %v", err.Error()) returnCode = types.UnexpectedError } default: returnMessage = "[Azure CNS] Error. CreateHnsNetwork did not receive a POST." returnCode = types.InvalidParameter } } resp := &cns.Response{ ReturnCode: returnCode, Message: returnMessage, } err = common.Encode(w, &resp) if returnCode == 0 { service.saveState() } logger.Response(service.Name, resp, resp.ReturnCode, err) } // Handles deleteHnsNetwork requests. func (service *HTTPRestService) deleteHnsNetwork(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure CNS] deleteHnsNetwork") var err error var req cns.DeleteHnsNetworkRequest var returnCode types.ResponseCode returnMessage := "" err = common.Decode(w, r, &req) logger.Request(service.Name, &req, err) if err != nil { //nolint:goconst returnMessage = "[Azure CNS] Error. Unable to decode input request." returnCode = types.InvalidParameter } else { switch r.Method { case http.MethodPost: networkInfo, found := service.getNetworkInfo(req.NetworkName) if found && networkInfo.NetworkName == req.NetworkName { if err = hnsclient.DeleteHnsNetwork(req.NetworkName); err == nil { returnMessage = fmt.Sprintf("[Azure CNS] Successfully deleted HNS network: %s", req.NetworkName) } else { returnMessage = fmt.Sprintf("[Azure CNS] DeleteHnsNetwork failed with error %v", err.Error()) returnCode = types.UnexpectedError } } else { returnMessage = fmt.Sprintf("[Azure CNS] Network %s not found", req.NetworkName) returnCode = types.InvalidParameter } default: returnMessage = "[Azure CNS] Error. DeleteHnsNetwork did not receive a POST." returnCode = types.InvalidParameter } } resp := &cns.Response{ ReturnCode: returnCode, Message: returnMessage, } err = common.Encode(w, &resp) if returnCode == 0 { service.removeNetworkInfo(req.NetworkName) service.saveState() } logger.Response(service.Name, resp, resp.ReturnCode, 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) { logger.Printf("[Azure CNS] getHostLocalIP") logger.Request(service.Name, "getHostLocalIP", nil) var found bool var errmsg string hostLocalIP := "0.0.0.0" if service.state.Initialized { switch r.Method { case http.MethodGet: switch service.state.NetworkType { case "Underlay": if service.wscli != nil { piface, err := service.getPrimaryHostInterface(context.TODO()) if err == nil { hostLocalIP = piface.PrimaryIP found = true } else { logger.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." } } var returnCode types.ResponseCode if !found { returnCode = types.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 := common.Encode(w, &hostLocalIPResponse) logger.Response(service.Name, hostLocalIPResponse, resp.ReturnCode, 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) { logger.Printf("[Azure CNS] getAvailableIPAddresses") logger.Request(service.Name, "getAvailableIPAddresses", nil) resp := cns.Response{ReturnCode: 0} ipResp := &cns.GetIPAddressesResponse{Response: resp} err := common.Encode(w, &ipResp) logger.Response(service.Name, ipResp, resp.ReturnCode, err) } // Handles retrieval of reserved ip addresses from ipam driver. func (service *HTTPRestService) getReservedIPAddresses(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure CNS] getReservedIPAddresses") logger.Request(service.Name, "getReservedIPAddresses", nil) resp := cns.Response{ReturnCode: 0} ipResp := &cns.GetIPAddressesResponse{Response: resp} err := common.Encode(w, &ipResp) logger.Response(service.Name, ipResp, resp.ReturnCode, err) } // getAllIPAddresses retrieves all ip addresses from ipam driver. func (service *HTTPRestService) getAllIPAddresses(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure CNS] getAllIPAddresses") logger.Request(service.Name, "getAllIPAddresses", nil) resp := cns.Response{ReturnCode: 0} ipResp := &cns.GetIPAddressesResponse{Response: resp} err := common.Encode(w, &ipResp) logger.Response(service.Name, ipResp, resp.ReturnCode, err) } // Handles health report requests. func (service *HTTPRestService) getHealthReport(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure CNS] getHealthReport") logger.Request(service.Name, "getHealthReport", nil) resp := &cns.Response{ReturnCode: 0} err := common.Encode(w, &resp) logger.Response(service.Name, resp, resp.ReturnCode, err) } func (service *HTTPRestService) setOrchestratorType(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure CNS] setOrchestratorType") var ( req cns.SetOrchestratorTypeRequest returnMessage string returnCode types.ResponseCode nodeID string ) err := common.Decode(w, r, &req) if err != nil { return } service.Lock() service.dncPartitionKey = req.DncPartitionKey nodeID = service.state.NodeID if nodeID == "" || nodeID == req.NodeID || !service.areNCsPresent() { switch req.OrchestratorType { case cns.ServiceFabric, cns.Kubernetes, cns.KubernetesCRD, cns.WebApps, cns.Batch, cns.DBforPostgreSQL, cns.AzureFirstParty: service.state.OrchestratorType = req.OrchestratorType service.state.NodeID = req.NodeID logger.SetContextDetails(req.OrchestratorType, req.NodeID) service.saveState() default: returnMessage = fmt.Sprintf("Invalid Orchestrator type %v", req.OrchestratorType) returnCode = types.UnsupportedOrchestratorType } } else { returnMessage = fmt.Sprintf("Invalid request since this node has already been registered as %s", nodeID) returnCode = types.InvalidRequest } service.Unlock() resp := cns.Response{ ReturnCode: returnCode, Message: returnMessage, } err = common.Encode(w, &resp) logger.Response(service.Name, resp, resp.ReturnCode, err) } // getHomeAz retrieves home AZ of host func (service *HTTPRestService) getHomeAz(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure CNS] getHomeAz") logger.Request(service.Name, "getHomeAz", nil) ctx := r.Context() switch r.Method { case http.MethodGet: getHomeAzResponse := service.homeAzMonitor.GetHomeAz(ctx) service.setResponse(w, getHomeAzResponse.Response.ReturnCode, getHomeAzResponse) default: returnMessage := "[Azure CNS] Error. getHomeAz did not receive a GET." returnCode := types.UnsupportedVerb service.setResponse(w, returnCode, cns.GetHomeAzResponse{ Response: cns.Response{ReturnCode: returnCode, Message: returnMessage}, }) } } func (service *HTTPRestService) createOrUpdateNetworkContainer(w http.ResponseWriter, r *http.Request) { var req cns.CreateNetworkContainerRequest if err := common.Decode(w, r, &req); err != nil { logger.Errorf("[Azure CNS] could not decode request: %v", err) w.WriteHeader(http.StatusBadRequest) return } if err := req.Validate(); err != nil { logger.Errorf("[Azure CNS] invalid request %+v: %s", req, err) w.WriteHeader(http.StatusBadRequest) return } logger.Request(service.Name, req.String(), nil) var returnCode types.ResponseCode var returnMessage string var err error switch r.Method { case http.MethodPost: if req.NetworkContainerType == cns.WebApps { // try to get the saved nc state if it exists existing, ok := service.getNetworkContainerDetails(req.NetworkContainerid) // create/update nc only if it doesn't exist or it exists and the requested version is different from the saved version if !ok || (ok && existing.VMVersion != req.Version) { nc := service.networkContainer if err = nc.Create(req); err != nil { returnMessage = fmt.Sprintf("[Azure CNS] Error. CreateOrUpdateNetworkContainer failed %v", err.Error()) returnCode = types.UnexpectedError break } } } else if req.NetworkContainerType == cns.AzureContainerInstance { // try to get the saved nc state if it exists existing, ok := service.getNetworkContainerDetails(req.NetworkContainerid) // create/update nc only if it doesn't exist or it exists and the requested version is different from the saved version if ok && existing.VMVersion != req.Version { nc := service.networkContainer netPluginConfig := service.getNetPluginDetails() if err = nc.Update(req, netPluginConfig); err != nil { returnMessage = fmt.Sprintf("[Azure CNS] Error. CreateOrUpdateNetworkContainer failed %v", err.Error()) returnCode = types.UnexpectedError break } } } returnCode, returnMessage = service.saveNetworkContainerGoalState(req) default: returnMessage = "[Azure CNS] Error. CreateOrUpdateNetworkContainer did not receive a POST." returnCode = types.InvalidParameter } resp := cns.Response{ ReturnCode: returnCode, Message: returnMessage, } reserveResp := &cns.CreateNetworkContainerResponse{Response: resp} err = common.Encode(w, &reserveResp) // If the NC was created successfully, log NC snapshot. if returnCode == types.Success { logNCSnapshot(req) } logger.Response(service.Name, reserveResp, resp.ReturnCode, err) } func (service *HTTPRestService) getNetworkContainerByID(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure CNS] getNetworkContainerByID") var req cns.GetNetworkContainerRequest var returnCode types.ResponseCode returnMessage := "" err := common.Decode(w, r, &req) logger.Request(service.Name, &req, err) if err != nil { return } resp := cns.Response{ ReturnCode: returnCode, Message: returnMessage, } reserveResp := &cns.GetNetworkContainerResponse{Response: resp} err = common.Encode(w, &reserveResp) logger.Response(service.Name, reserveResp, resp.ReturnCode, err) } // the function is to get all network containers based on given OrchestratorContext func (service *HTTPRestService) GetAllNetworkContainers(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure CNS] GetAllNetworkContainers") var req cns.GetNetworkContainerRequest err := common.Decode(w, r, &req) logger.Request(service.Name, &req, err) if err != nil { logger.Errorf("[Azure CNS] failed to decode cns request with req %+v due to %+v", req, err) return } getAllNetworkContainerResponses := service.getAllNetworkContainerResponses(req) // nolint var resp cns.GetAllNetworkContainersResponse failedNetworkContainerResponses := make([]cns.GetNetworkContainerResponse, 0) for i := 0; i < len(getAllNetworkContainerResponses); i++ { if getAllNetworkContainerResponses[i].Response.ReturnCode != types.Success { failedNetworkContainerResponses = append(failedNetworkContainerResponses, getAllNetworkContainerResponses[i]) } } resp.NetworkContainers = getAllNetworkContainerResponses if len(failedNetworkContainerResponses) > 0 { failedToGetNCErrMsg := make([]string, 0) for _, failedNetworkContainerResponse := range failedNetworkContainerResponses { // nolint failedToGetNCErrMsg = append(failedToGetNCErrMsg, fmt.Sprintf("Failed to get NC %s due to %s", failedNetworkContainerResponse.NetworkContainerID, failedNetworkContainerResponse.Response.Message)) } resp.Response.ReturnCode = types.UnexpectedError resp.Response.Message = strings.Join(failedToGetNCErrMsg, "\n") } else { resp.Response.ReturnCode = types.Success resp.Response.Message = "Successfully retrieved NCs" } err = common.Encode(w, &resp) logger.Response(service.Name, resp, resp.Response.ReturnCode, err) } func (service *HTTPRestService) GetNetworkContainerByOrchestratorContext(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure CNS] GetNetworkContainerByOrchestratorContext") var req cns.GetNetworkContainerRequest err := common.Decode(w, r, &req) logger.Request(service.Name, &req, err) if err != nil { return } getNetworkContainerResponses := service.getAllNetworkContainerResponses(req) // nolint err = common.Encode(w, &getNetworkContainerResponses[0]) logger.Response(service.Name, getNetworkContainerResponses[0], getNetworkContainerResponses[0].Response.ReturnCode, err) } // getOrRefreshNetworkContainers is to check whether refresh association is needed. The state file in CNS will get updated if it is lost. // If received "GET": Return all NCs in CNS's state file to DNC in order to check if NC refresh is needed // If received "POST": Store all the NCs (from the request body that client sent) into CNS's state file func (service *HTTPRestService) getOrRefreshNetworkContainers(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: logger.Printf("[Azure CNS] getOrRefreshNetworkContainers received GET") service.handleGetNetworkContainers(w) return case http.MethodPost: logger.Printf("[Azure CNS] getOrRefreshNetworkContainers received POST") service.handlePostNetworkContainers(w, r) return default: w.WriteHeader(http.StatusMethodNotAllowed) err := errors.New("[Azure CNS] getOrRefreshNetworkContainers did not receive a GET or POST") logger.Response(service.Name, nil, types.InvalidParameter, err) return } } func (service *HTTPRestService) deleteNetworkContainer(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure CNS] deleteNetworkContainer") var req cns.DeleteNetworkContainerRequest var returnCode types.ResponseCode returnMessage := "" err := common.Decode(w, r, &req) logger.Request(service.Name, &req, err) if err != nil { return } ncid := req.NetworkContainerid if ncid == "" { returnCode = types.NetworkContainerNotSpecified returnMessage = "[Azure CNS] Error. NetworkContainerid is empty" } switch r.Method { case http.MethodPost: var containerStatus containerstatus var ok bool containerStatus, ok = service.getNetworkContainerDetails(ncid) if !ok { logger.Printf("Not able to retrieve network container details for this container id %v", ncid) break } if containerStatus.CreateNetworkContainerRequest.NetworkContainerType == cns.WebApps { nc := service.networkContainer if deleteErr := nc.Delete(ncid); deleteErr != nil { // nolint:gocritic returnMessage = fmt.Sprintf("[Azure CNS] Error. DeleteNetworkContainer failed %v", deleteErr.Error()) returnCode = types.UnexpectedError break } } service.Lock() defer service.Unlock() if service.state.ContainerStatus != nil { delete(service.state.ContainerStatus, ncid) } if service.state.ContainerIDByOrchestratorContext != nil { for orchestratorContext, networkContainerIDs := range service.state.ContainerIDByOrchestratorContext { //nolint:gocritic // copy is ok if networkContainerIDs.Contains(ncid) { networkContainerIDs.Delete(ncid) if *networkContainerIDs == "" { delete(service.state.ContainerIDByOrchestratorContext, orchestratorContext) break } } } } service.saveState() default: returnMessage = "[Azure CNS] Error. DeleteNetworkContainer did not receive a POST." returnCode = types.InvalidParameter } resp := cns.Response{ ReturnCode: returnCode, Message: returnMessage, } reserveResp := &cns.DeleteNetworkContainerResponse{Response: resp} err = common.Encode(w, &reserveResp) logger.Response(service.Name, reserveResp, resp.ReturnCode, err) } func (service *HTTPRestService) getInterfaceForContainer(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure CNS] getInterfaceForContainer") var req cns.GetInterfaceForContainerRequest var returnCode types.ResponseCode returnMessage := "" err := common.Decode(w, r, &req) logger.Request(service.Name, &req, err) if err != nil { return } containerInfo := service.state.ContainerStatus containerDetails, ok := containerInfo[req.NetworkContainerID] var interfaceName string var ipaddress string var cnetSpace []cns.IPSubnet var dnsServers []string var version string if ok { savedReq := containerDetails.CreateNetworkContainerRequest interfaceName = savedReq.NetworkContainerid cnetSpace = savedReq.CnetAddressSpace ipaddress = savedReq.IPConfiguration.IPSubnet.IPAddress // it has to exist dnsServers = savedReq.IPConfiguration.DNSServers version = savedReq.Version } else { returnMessage = "[Azure CNS] Never received call to create this container." returnCode = types.UnknownContainerID interfaceName = "" ipaddress = "" version = "" } resp := cns.Response{ ReturnCode: returnCode, Message: returnMessage, } getInterfaceForContainerResponse := cns.GetInterfaceForContainerResponse{ Response: resp, NetworkInterface: cns.NetworkInterface{Name: interfaceName, IPAddress: ipaddress}, CnetAddressSpace: cnetSpace, DNSServers: dnsServers, NetworkContainerVersion: version, } err = common.Encode(w, &getInterfaceForContainerResponse) logger.Response(service.Name, getInterfaceForContainerResponse, resp.ReturnCode, err) } func (service *HTTPRestService) attachNetworkContainerToNetwork(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure CNS] attachNetworkContainerToNetwork") var req cns.ConfigureContainerNetworkingRequest err := common.Decode(w, r, &req) logger.Request(service.Name, &req, err) if err != nil { return } resp := service.attachOrDetachHelper(req, attach, r.Method) attachResp := &cns.AttachContainerToNetworkResponse{Response: resp} err = common.Encode(w, &attachResp) logger.Response(service.Name, attachResp, resp.ReturnCode, err) } func (service *HTTPRestService) detachNetworkContainerFromNetwork(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure CNS] detachNetworkContainerFromNetwork") var req cns.ConfigureContainerNetworkingRequest err := common.Decode(w, r, &req) logger.Request(service.Name, &req, err) if err != nil { return } resp := service.attachOrDetachHelper(req, detach, r.Method) detachResp := &cns.DetachContainerFromNetworkResponse{Response: resp} err = common.Encode(w, &detachResp) logger.Response(service.Name, detachResp, resp.ReturnCode, err) } // Retrieves the number of logic processors on a node. It will be primarily // used to enforce per VM delegated NIC limit by DNC. func (service *HTTPRestService) getNumberOfCPUCores(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure-CNS] getNumberOfCPUCores") logger.Request(service.Name, "getNumberOfCPUCores", nil) var ( num int returnCode types.ResponseCode errMsg string ) switch r.Method { case http.MethodGet: num = runtime.NumCPU() default: errMsg = "[Azure-CNS] getNumberOfCPUCores API expects a GET." returnCode = types.UnsupportedVerb } resp := cns.Response{ReturnCode: returnCode, Message: errMsg} numOfCPUCoresResp := cns.NumOfCPUCoresResponse{ Response: resp, NumOfCPUCores: num, } err := common.Encode(w, &numOfCPUCoresResp) logger.Response(service.Name, numOfCPUCoresResp, resp.ReturnCode, err) } func extractNCParamsFromURL(networkContainerURL string) (cns.NetworkContainerParameters, error) { ncURL, err := url.Parse(networkContainerURL) if err != nil { return cns.NetworkContainerParameters{}, fmt.Errorf("failed to parse network container url, %w", err) } queryParams := ncURL.Query() // current format of create network url has a path after a query parameter "type" // doing this parsing due to this structure typeQueryParamVal := queryParams.Get("type") if typeQueryParamVal == "" { return cns.NetworkContainerParameters{}, fmt.Errorf("no type query param, %w", ErrInvalidNcURLFormat) } // .{0,128} gets from zero to 128 characters of any kind // ()? is optional matches := ncRegex.FindStringSubmatch(typeQueryParamVal) if len(matches) != ncURLExpectedMatches { return cns.NetworkContainerParameters{}, fmt.Errorf("unexpected number of matches in url, %w", ErrInvalidNcURLFormat) } return cns.NetworkContainerParameters{ AssociatedInterfaceID: matches[1], NCID: matches[2], AuthToken: matches[3], }, nil } func respondJSON(w http.ResponseWriter, statusCode int, body any) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(statusCode) if err := json.NewEncoder(w).Encode(body); err != nil { logger.Printf("could not write json response: %v", err) } } // Publish Network Container by calling nmagent func (service *HTTPRestService) publishNetworkContainer(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "PublishNetworkContainer expects a POST", http.StatusBadRequest) return } var req cns.PublishNetworkContainerRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, fmt.Sprintf("could not decode request body: %v", err), http.StatusBadRequest) return } logger.Request(service.Name, req, nil) ncParams, err := extractNCParamsFromURL(req.CreateNetworkContainerURL) if err != nil { resp := cns.PublishNetworkContainerResponse{ Response: cns.Response{ ReturnCode: http.StatusBadRequest, Message: fmt.Sprintf("unexpected create nc url format. url %s: %v ", req.CreateNetworkContainerURL, err), }, } respondJSON(w, http.StatusBadRequest, resp) logger.Response(service.Name, resp, resp.Response.ReturnCode, err) return } ctx := r.Context() joinResp, err := service.wsproxy.JoinNetwork(ctx, req.NetworkID) //nolint:govet // ok to shadow if err != nil { resp := cns.PublishNetworkContainerResponse{ Response: cns.Response{ ReturnCode: types.NetworkJoinFailed, Message: fmt.Sprintf("failed to join network %s: %v", req.NetworkID, err), }, PublishErrorStr: err.Error(), } respondJSON(w, http.StatusOK, resp) // legacy behavior logger.Response(service.Name, resp, resp.Response.ReturnCode, err) return } joinBytes, _ := io.ReadAll(joinResp.Body) _ = joinResp.Body.Close() if joinResp.StatusCode != http.StatusOK { resp := cns.PublishNetworkContainerResponse{ Response: cns.Response{ ReturnCode: types.NetworkJoinFailed, Message: fmt.Sprintf("failed to join network %s. did not get 200 from wireserver", req.NetworkID), }, PublishStatusCode: joinResp.StatusCode, PublishResponseBody: joinBytes, } respondJSON(w, http.StatusOK, resp) // legacy behavior logger.Response(service.Name, resp, resp.Response.ReturnCode, nil) return } service.setNetworkStateJoined(req.NetworkID) logger.Printf("[Azure-CNS] joined vnet %s during nc %s publish. wireserver response: %v", req.NetworkID, req.NetworkContainerID, string(joinBytes)) publishResp, err := service.wsproxy.PublishNC(ctx, ncParams, req.CreateNetworkContainerRequestBody) if err != nil { resp := cns.PublishNetworkContainerResponse{ Response: cns.Response{ ReturnCode: types.NetworkContainerPublishFailed, Message: fmt.Sprintf("failed to publish nc %s: %v", req.NetworkContainerID, err), }, PublishErrorStr: err.Error(), } respondJSON(w, http.StatusOK, resp) // legacy behavior logger.Response(service.Name, resp, resp.Response.ReturnCode, err) return } publishBytes, _ := io.ReadAll(publishResp.Body) _ = publishResp.Body.Close() resp := cns.PublishNetworkContainerResponse{ PublishStatusCode: publishResp.StatusCode, PublishResponseBody: publishBytes, } if publishResp.StatusCode != http.StatusOK { resp.Response = cns.Response{ ReturnCode: types.NetworkContainerPublishFailed, Message: fmt.Sprintf("failed to publish nc %s. did not get 200 from wireserver", req.NetworkContainerID), } } respondJSON(w, http.StatusOK, resp) logger.Response(service.Name, resp, resp.Response.ReturnCode, nil) } func (service *HTTPRestService) unpublishNetworkContainer(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "UnpublishNetworkContainer expects a POST", http.StatusBadRequest) return } var req cns.UnpublishNetworkContainerRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, fmt.Sprintf("could not decode request body: %v", err), http.StatusBadRequest) return } logger.Request(service.Name, req, nil) ncParams, err := extractNCParamsFromURL(req.DeleteNetworkContainerURL) if err != nil { resp := cns.UnpublishNetworkContainerResponse{ Response: cns.Response{ ReturnCode: http.StatusBadRequest, Message: fmt.Sprintf("unexpected delete nc url format. url %s: %v ", req.DeleteNetworkContainerURL, err), }, } respondJSON(w, http.StatusBadRequest, resp) logger.Response(service.Name, resp, resp.Response.ReturnCode, err) return } ctx := r.Context() var unpublishBody nmagent.DeleteContainerRequest var azrNC bool err = json.Unmarshal(req.DeleteNetworkContainerRequestBody, &unpublishBody) if err != nil { // If the body contains only `""\n`, it is non-AZR NC // In this case, we should not return an error // However, if the body is not `""\n`, it is invalid and therefore, we must return an error // []byte{34, 34, 10} here represents []byte(`""`+"\n") if !bytes.Equal(req.DeleteNetworkContainerRequestBody, []byte{34, 34, 10}) { http.Error(w, fmt.Sprintf("could not unmarshal delete network container body: %v", err), http.StatusBadRequest) return } } else { // If unmarshalling was successful, it is an AZR NC azrNC = true } /* For AZR scenarios, if NMAgent is restarted, it loses state and does not know what VNETs to subscribe to. As it no longer has VNET state, delete nc calls would fail. We need to add join VNET call for all AZR nc unpublish calls just like publish nc calls. */ if azrNC || !service.isNetworkJoined(req.NetworkID) { joinResp, err := service.wsproxy.JoinNetwork(ctx, req.NetworkID) //nolint:govet // ok to shadow if err != nil { resp := cns.UnpublishNetworkContainerResponse{ Response: cns.Response{ ReturnCode: types.NetworkJoinFailed, Message: fmt.Sprintf("failed to join network %s: %v", req.NetworkID, err), }, UnpublishErrorStr: err.Error(), } respondJSON(w, http.StatusOK, resp) // legacy behavior logger.Response(service.Name, resp, resp.Response.ReturnCode, err) return } joinBytes, _ := io.ReadAll(joinResp.Body) _ = joinResp.Body.Close() if joinResp.StatusCode != http.StatusOK { resp := cns.UnpublishNetworkContainerResponse{ Response: cns.Response{ ReturnCode: types.NetworkJoinFailed, Message: fmt.Sprintf("failed to join network %s. did not get 200 from wireserver", req.NetworkID), }, UnpublishStatusCode: joinResp.StatusCode, UnpublishResponseBody: joinBytes, } respondJSON(w, http.StatusOK, resp) // legacy behavior logger.Response(service.Name, resp, resp.Response.ReturnCode, nil) return } service.setNetworkStateJoined(req.NetworkID) logger.Printf("[Azure-CNS] joined vnet %s during nc %s unpublish. AZREnabled: %t, wireserver response: %v", req.NetworkID, req.NetworkContainerID, unpublishBody.AZREnabled, string(joinBytes)) } publishResp, err := service.wsproxy.UnpublishNC(ctx, ncParams, req.DeleteNetworkContainerRequestBody) if err != nil { resp := cns.UnpublishNetworkContainerResponse{ Response: cns.Response{ ReturnCode: types.NetworkContainerUnpublishFailed, Message: fmt.Sprintf("failed to publish nc %s: %v", req.NetworkContainerID, err), }, UnpublishErrorStr: err.Error(), } respondJSON(w, http.StatusOK, resp) // legacy behavior logger.Response(service.Name, resp, resp.Response.ReturnCode, err) return } publishBytes, _ := io.ReadAll(publishResp.Body) _ = publishResp.Body.Close() resp := cns.UnpublishNetworkContainerResponse{ UnpublishStatusCode: publishResp.StatusCode, UnpublishResponseBody: publishBytes, } if publishResp.StatusCode != http.StatusOK { resp.Response = cns.Response{ ReturnCode: types.NetworkContainerUnpublishFailed, Message: fmt.Sprintf("failed to unpublish nc %s. did not get 200 from wireserver", req.NetworkContainerID), } } respondJSON(w, http.StatusOK, resp) logger.Response(service.Name, resp, resp.Response.ReturnCode, nil) } func (service *HTTPRestService) CreateHostNCApipaEndpoint(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure-CNS] CreateHostNCApipaEndpoint") var ( err error req cns.CreateHostNCApipaEndpointRequest returnCode types.ResponseCode returnMessage string endpointID string ) err = common.Decode(w, r, &req) logger.Request(service.Name, &req, err) if err != nil { return } switch r.Method { case http.MethodPost: networkContainerDetails, found := service.getNetworkContainerDetails(req.NetworkContainerID) if found { if !networkContainerDetails.CreateNetworkContainerRequest.AllowNCToHostCommunication && !networkContainerDetails.CreateNetworkContainerRequest.AllowHostToNCCommunication { returnMessage = fmt.Sprintf("HostNCApipaEndpoint creation is not supported unless " + "AllowNCToHostCommunication or AllowHostToNCCommunication is set to true") returnCode = types.InvalidRequest } else { if endpointID, err = hnsclient.CreateHostNCApipaEndpoint( req.NetworkContainerID, networkContainerDetails.CreateNetworkContainerRequest.LocalIPConfiguration, networkContainerDetails.CreateNetworkContainerRequest.AllowNCToHostCommunication, networkContainerDetails.CreateNetworkContainerRequest.AllowHostToNCCommunication, networkContainerDetails.CreateNetworkContainerRequest.EndpointPolicies); err != nil { returnMessage = fmt.Sprintf("CreateHostNCApipaEndpoint failed with error: %v", err) returnCode = types.UnexpectedError } } } else { returnMessage = fmt.Sprintf("CreateHostNCApipaEndpoint failed with error: Unable to find goal state for"+ " the given Network Container: %s", req.NetworkContainerID) returnCode = types.UnknownContainerID } default: returnMessage = "createHostNCApipaEndpoint API expects a POST" returnCode = types.UnsupportedVerb } response := cns.CreateHostNCApipaEndpointResponse{ Response: cns.Response{ ReturnCode: returnCode, Message: returnMessage, }, EndpointID: endpointID, } err = common.Encode(w, &response) logger.Response(service.Name, response, response.Response.ReturnCode, err) } func (service *HTTPRestService) DeleteHostNCApipaEndpoint(w http.ResponseWriter, r *http.Request) { logger.Printf("[Azure-CNS] DeleteHostNCApipaEndpoint") var ( err error req cns.DeleteHostNCApipaEndpointRequest returnCode types.ResponseCode returnMessage string ) err = common.Decode(w, r, &req) logger.Request(service.Name, &req, err) if err != nil { return } switch r.Method { case http.MethodPost: if err = hnsclient.DeleteHostNCApipaEndpoint(req.NetworkContainerID); err != nil { returnMessage = fmt.Sprintf("Failed to delete endpoint for Network Container: %s "+ "due to error: %v", req.NetworkContainerID, err) returnCode = types.UnexpectedError } default: returnMessage = "deleteHostNCApipaEndpoint API expects a DELETE" returnCode = types.UnsupportedVerb } response := cns.DeleteHostNCApipaEndpointResponse{ Response: cns.Response{ ReturnCode: returnCode, Message: returnMessage, }, } err = common.Encode(w, &response) logger.Response(service.Name, response, response.Response.ReturnCode, err) } // This function is used to query NMagents's supported APIs list func (service *HTTPRestService) nmAgentSupportedApisHandler(w http.ResponseWriter, r *http.Request) { logger.Request(service.Name, "nmAgentSupportedApisHandler", nil) var ( err, retErr error req cns.NmAgentSupportedApisRequest returnCode types.ResponseCode returnMessage string supportedApis []string ) ctx := r.Context() err = common.Decode(w, r, &req) logger.Request(service.Name, &req, err) if err != nil { return } switch r.Method { case http.MethodPost: apis, err := service.nma.SupportedAPIs(ctx) if err != nil { returnCode = types.NmAgentSupportedApisError returnMessage = fmt.Sprintf("[Azure-CNS] %s", retErr.Error()) } supportedApis = apis default: returnMessage = "[Azure-CNS] NmAgentSupported API list expects a POST method." } resp := cns.Response{ReturnCode: returnCode, Message: returnMessage} nmAgentSupportedApisResponse := &cns.NmAgentSupportedApisResponse{ Response: resp, SupportedApis: supportedApis, } serviceErr := common.Encode(w, &nmAgentSupportedApisResponse) logger.Response(service.Name, nmAgentSupportedApisResponse, resp.ReturnCode, serviceErr) } // getVMUniqueID retrieves VMUniqueID from the IMDS func (service *HTTPRestService) getVMUniqueID(w http.ResponseWriter, r *http.Request) { logger.Request(service.Name, "getVMUniqueID", nil) ctx := r.Context() switch r.Method { case http.MethodGet: vmUniqueID, err := service.imdsClient.GetVMUniqueID(ctx) if err != nil { resp := cns.GetVMUniqueIDResponse{ Response: cns.Response{ ReturnCode: types.UnexpectedError, Message: errors.Wrap(err, "failed to get vmuniqueid").Error(), }, } respondJSON(w, http.StatusInternalServerError, resp) logger.Response(service.Name, resp, resp.Response.ReturnCode, err) return } resp := cns.GetVMUniqueIDResponse{ Response: cns.Response{ ReturnCode: types.Success, }, VMUniqueID: vmUniqueID, } respondJSON(w, http.StatusOK, resp) logger.Response(service.Name, resp, resp.Response.ReturnCode, err) default: returnMessage := fmt.Sprintf("[Azure CNS] Error. getVMUniqueID did not receive a GET."+ " Received: %s", r.Method) returnCode := types.UnsupportedVerb service.setResponse(w, returnCode, cns.GetHomeAzResponse{ Response: cns.Response{ReturnCode: returnCode, Message: returnMessage}, }) } } // This function is used to query all NCs on a node from NMAgent func (service *HTTPRestService) nmAgentNCListHandler(w http.ResponseWriter, r *http.Request) { logger.Request(service.Name, "nmAgentNCListHandler", nil) var ( returnCode types.ResponseCode networkContainerList []string ) returnMessage := "Successfully fetched NC list from NMAgent" ctx := r.Context() switch r.Method { case http.MethodGet: ncVersionList, ncVersionerr := service.nma.GetNCVersionList(ctx) if ncVersionerr != nil { returnCode = types.NmAgentNCVersionListError returnMessage = "[Azure-CNS] " + ncVersionerr.Error() break } for _, container := range ncVersionList.Containers { networkContainerList = append(networkContainerList, container.NetworkContainerID) } default: returnMessage = "[Azure-CNS] NmAgentNCList API expects a GET method." } resp := cns.Response{ReturnCode: returnCode, Message: returnMessage} NCListResponse := &cns.NCListResponse{ Response: resp, NCList: networkContainerList, } serviceErr := common.Encode(w, &NCListResponse) logger.Response(service.Name, NCListResponse, resp.ReturnCode, serviceErr) }