azure-container-networking/common/utils.go

309 строки
7.9 KiB
Go

// Copyright 2017 Microsoft. All rights reserved.
// MIT License
package common
import (
"context"
"encoding/binary"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/Azure/azure-container-networking/log"
)
const (
metadataURL = "http://169.254.169.254/metadata/instance?api-version=2017-08-01&format=json"
azCloudUrl = "http://169.254.169.254/metadata/instance/compute/azEnvironment?api-version=2018-10-01&format=text"
httpConnectionTimeout = 7
headerTimeout = 7
RegisterNodeURLFmt = "%s/networks/%s/node/%s%s"
SyncNodeNetworkContainersURLFmt = "%s/networks/%s/node/%s/networkcontainers%s"
FiveSeconds = 5 * time.Second
JsonContent = "application/json; charset=UTF-8"
ContentType = "Content-Type"
)
// XmlDocument - 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"`
}
}
}
}
// Metadata retrieved from wireserver
type Metadata struct {
Location string `json:"location"`
VMName string `json:"name"`
Offer string `json:"offer"`
OsType string `json:"osType"`
PlacementGroupID string `json:"placementGroupId"`
PlatformFaultDomain string `json:"platformFaultDomain"`
PlatformUpdateDomain string `json:"platformUpdateDomain"`
Publisher string `json:"publisher"`
ResourceGroupName string `json:"resourceGroupName"`
Sku string `json:"sku"`
SubscriptionID string `json:"subscriptionId"`
Tags string `json:"tags"`
OSVersion string `json:"version"`
VMID string `json:"vmId"`
VMSize string `json:"vmSize"`
KernelVersion string
}
// This is how metadata server returns in response for querying metadata
type metadataWrapper struct {
Metadata Metadata `json:"compute"`
}
// Creating http client object to be reused instead of creating one every time.
// This helps make use of the cached tcp connections.
// Clients are safe for concurrent use by multiple goroutines.
var httpClient *http.Client
// InitHttpClient initializes the httpClient object
func InitHttpClient(
connectionTimeoutSec int,
responseHeaderTimeoutSec int) *http.Client {
log.Printf("[Utils] Initializing HTTP client with connection timeout: %d, response header timeout: %d",
connectionTimeoutSec, responseHeaderTimeoutSec)
httpClient = &http.Client{
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: time.Duration(connectionTimeoutSec) * time.Second,
}).DialContext,
ResponseHeaderTimeout: time.Duration(responseHeaderTimeoutSec) * time.Second,
},
}
return httpClient
}
// GetHttpClient returns the singleton httpClient object
func GetHttpClient() *http.Client {
return httpClient
}
// LogNetworkInterfaces logs the host's network interfaces in the default namespace.
func LogNetworkInterfaces() {
interfaces, err := net.Interfaces()
if err != nil {
log.Printf("Failed to query network interfaces, err:%v", err)
return
}
for _, iface := range interfaces {
addrs, _ := iface.Addrs()
log.Printf("[net] Network interface: %+v with IP: %+v", iface, addrs)
}
}
func IpToInt(ip net.IP) uint32 {
if len(ip) == 16 {
return binary.BigEndian.Uint32(ip[12:16])
}
return binary.BigEndian.Uint32(ip)
}
func GetInterfaceSubnetWithSpecificIP(ipAddr string) *net.IPNet {
addrs, err := net.InterfaceAddrs()
if err != nil {
log.Printf("InterfaceAddrs failed with %+v", err)
return nil
}
for _, a := range addrs {
if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
if ipnet.IP.String() == ipAddr {
return ipnet
}
}
}
}
return nil
}
func StartProcess(path string, args []string) error {
attr := os.ProcAttr{
Env: os.Environ(),
Files: []*os.File{
os.Stdin,
nil,
nil,
},
}
processArgs := append([]string{path}, args...)
process, err := os.StartProcess(path, processArgs, &attr)
if err == nil {
// Release detaches the process
return process.Release()
}
return err
}
// GetHostMetadata - retrieve VM metadata from wireserver
func GetHostMetadata(fileName string) (Metadata, error) {
content, err := os.ReadFile(fileName)
if err == nil {
var metadata Metadata
if err = json.Unmarshal(content, &metadata); err == nil {
return metadata, nil
}
}
log.Printf("[Telemetry] Request metadata from wireserver")
req, err := http.NewRequest("GET", metadataURL, nil)
if err != nil {
return Metadata{}, err
}
req.Header.Set("Metadata", "True")
client := &http.Client{
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: time.Duration(httpConnectionTimeout) * time.Second,
}).DialContext,
ResponseHeaderTimeout: time.Duration(headerTimeout) * time.Second,
},
}
resp, err := client.Do(req)
if err != nil {
return Metadata{}, err
}
defer resp.Body.Close()
metareport := metadataWrapper{}
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("[Telemetry] Request failed with HTTP error %d", resp.StatusCode)
} else if resp.Body != nil {
err = json.NewDecoder(resp.Body).Decode(&metareport)
if err != nil {
err = fmt.Errorf("[Telemetry] Unable to decode response body due to error: %s", err.Error())
}
} else {
err = fmt.Errorf("[Telemetry] Response body is empty")
}
return metareport.Metadata, err
}
// SaveHostMetadata - save metadata got from wireserver to json file
func SaveHostMetadata(metadata Metadata, fileName string) error {
dataBytes, err := json.Marshal(metadata)
if err != nil {
return fmt.Errorf("[Telemetry] marshal data failed with err %+v", err)
}
if err = os.WriteFile(fileName, dataBytes, 0o644); err != nil {
log.Printf("[Telemetry] Writing metadata to file failed: %v", err)
}
return err
}
func GetAzureCloud(url string) (string, error) {
if url == "" {
url = azCloudUrl
}
log.Printf("GetAzureCloud querying url: %s", url)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", err
}
req.Header.Set("Metadata", "True")
client := InitHttpClient(httpConnectionTimeout, headerTimeout)
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("Bad http status:%v", resp.Status)
}
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return strings.TrimSpace(string(bodyBytes)), nil
}
func GetExecutableDirectory() (string, error) {
var (
dir string
ex string
err error
)
ex, err = os.Executable()
if err == nil {
dir = filepath.Dir(ex)
} else {
var exReal string
// If a symlink was used to start the process, depending on the operating system,
// the result might be the symlink or the path it pointed to.
// filepath.EvalSymlinks returns stable results
exReal, err = filepath.EvalSymlinks(ex)
if err == nil {
dir = filepath.Dir(exReal)
}
}
return dir, err
}
func PostCtx(ctx context.Context, cl *http.Client, url, contentType string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
return nil, fmt.Errorf("could not create POST request: %w", err)
}
req.Header.Set("Content-Type", contentType)
var resp *http.Response
resp, err = cl.Do(req)
if err != nil {
// returning response as well
// cause some methods seem to depend on that for error handling
return resp, fmt.Errorf("POST request received response %w", err)
}
return resp, nil
}