// Copyright 2022 NetApp, Inc. All Rights Reserved. package azgo import ( "bytes" "crypto/tls" "crypto/x509" "encoding/base64" "encoding/xml" "errors" "fmt" "io/ioutil" "net/http" "reflect" "strings" "time" xrv "github.com/mattermost/xml-roundtrip-validator" log "github.com/sirupsen/logrus" tridentconfig "github.com/netapp/trident/config" utils "github.com/netapp/trident/utils" ) type ZAPIRequest interface { ToXML() (string, error) } type ZAPIResponseIterable interface { NextTag() string } type ZapiRunner struct { ManagementLIF string SVM string Username string Password string ClientPrivateKey string ClientCertificate string TrustedCACertificate string Secure bool OntapiVersion string DebugTraceFlags map[string]bool // Example: {"api":false, "method":true} } // GetZAPIName returns the name of the ZAPI request; it must parse the XML because ZAPIRequest is an interface // See also: https://play.golang.org/p/IqHhgVB3Q7x func GetZAPIName(zr ZAPIRequest) (string, error) { zapiXML, err := zr.ToXML() if err != nil { return "", err } decoder := xml.NewDecoder(strings.NewReader(zapiXML)) for { token, _ := decoder.Token() if token == nil { break } switch startElement := token.(type) { case xml.StartElement: return startElement.Name.Local, nil } } return "", fmt.Errorf("could not find start tag for ZAPI: %v", zapiXML) } // SendZapi sends the provided ZAPIRequest to the Ontap system func (o *ZapiRunner) SendZapi(r ZAPIRequest) (*http.Response, error) { startTime := time.Now() if o.DebugTraceFlags["method"] { fields := log.Fields{"Method": "SendZapi", "Type": "ZapiRunner"} log.WithFields(fields).Debug(">>>> SendZapi") defer log.WithFields(fields).Debug("<<<< SendZapi") } zapiCommand, err := r.ToXML() if err != nil { return nil, err } zapiName, zapiNameErr := GetZAPIName(r) if zapiNameErr == nil { zapiOpsTotal.WithLabelValues(o.SVM, zapiName).Inc() defer func() { endTime := float64(time.Since(startTime).Milliseconds()) zapiOpsDurationInMsBySVMSummary.WithLabelValues(o.SVM, zapiName).Observe(endTime) }() } s := "" redactedRequest := "" if o.SVM == "" { s = fmt.Sprintf(` %s `, zapiCommand) } else { s = fmt.Sprintf(` %s `, "vfiler=\""+o.SVM+"\"", zapiCommand) } if o.DebugTraceFlags["api"] { secretFields := []string{ "outbound-passphrase", "outbound-user-name", "passphrase", "user-name" } secrets := make(map[string]string) for _, f := range secretFields { fmtString := "<%s>%s" secrets[fmt.Sprintf(fmtString, f, ".*", f)] = fmt.Sprintf(fmtString, f, utils.REDACTED, f) } redactedRequest = utils.RedactSecretsFromString(s, secrets, true) log.Debugf("sending to '%s' xml: \n%s", o.ManagementLIF, redactedRequest) } url := "http://" + o.ManagementLIF + "/servlets/netapp.servlets.admin.XMLrequest_filer" if o.Secure { url = "https://" + o.ManagementLIF + "/servlets/netapp.servlets.admin.XMLrequest_filer" } if o.DebugTraceFlags["api"] { log.Debugf("URL:> %s", url) } b := []byte(s) req, err := http.NewRequest("POST", url, bytes.NewBuffer(b)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/xml") // Check to use cert/key and load the cert pair var cert tls.Certificate caCertPool := x509.NewCertPool() skipVerify := true if o.ClientCertificate != "" && o.ClientPrivateKey != "" { certDecode, err := base64.StdEncoding.DecodeString(o.ClientCertificate) if err != nil { return nil, errors.New("failed to decode client certificate from base64") } keyDecode, err := base64.StdEncoding.DecodeString(o.ClientPrivateKey) if err != nil { return nil, errors.New("failed to decode private key from base64") } cert, err = tls.X509KeyPair(certDecode, keyDecode) if err != nil { log.Debugf("error: %v", err) return nil, errors.New("cannot load certificate and key") } } else { req.SetBasicAuth(o.Username, o.Password) } // Check to use trustedCACertificate to use InsecureSkipVerify or not if o.TrustedCACertificate != "" { trustedCACert, err := base64.StdEncoding.DecodeString(o.TrustedCACertificate) if err != nil { return nil, errors.New("failed to decode trusted CA certificate from base64") } skipVerify = false caCertPool.AppendCertsFromPEM(trustedCACert) } tr := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: skipVerify, MinVersion: tridentconfig.MinClientTLSVersion, Certificates: []tls.Certificate{cert}, RootCAs: caCertPool, }, } client := &http.Client{ Transport: tr, Timeout: time.Duration(tridentconfig.StorageAPITimeoutSeconds * time.Second), } response, err := client.Do(req) if err != nil { return nil, err } else if response.StatusCode == 401 { return nil, errors.New("response code 401 (Unauthorized): incorrect or missing credentials") } if o.DebugTraceFlags["api"] { log.Debugf("response Status: %s", response.Status) log.Debugf("response Headers: %s", response.Header) } return ValidateZAPIResponse(response) } // ExecuteUsing converts this object to a ZAPI XML representation and uses the supplied ZapiRunner to send to a filer func (o *ZapiRunner) ExecuteUsing(z ZAPIRequest, requestType string, v interface{}) (interface{}, error) { return o.ExecuteWithoutIteration(z, requestType, v) } // ExecuteWithoutIteration does not attempt to perform any nextTag style iteration func (o *ZapiRunner) ExecuteWithoutIteration(z ZAPIRequest, requestType string, v interface{}) (interface{}, error) { if o.DebugTraceFlags["method"] { fields := log.Fields{"Method": "ExecuteUsing", "Type": requestType} log.WithFields(fields).Debug(">>>> ExecuteUsing") defer log.WithFields(fields).Debug("<<<< ExecuteUsing") } resp, err := o.SendZapi(z) if err != nil { log.Errorf("API invocation failed. %v", err.Error()) return nil, err } defer resp.Body.Close() body, readErr := ioutil.ReadAll(resp.Body) if readErr != nil { log.Errorf("Error reading response body. %v", readErr.Error()) return nil, readErr } if o.DebugTraceFlags["api"] { log.Debugf("response Body:\n%s", string(body)) } // unmarshalErr := xml.Unmarshal(body, &v) unmarshalErr := xml.Unmarshal(body, v) if unmarshalErr != nil { log.WithField("body", string(body)).Warnf("Error unmarshaling response body. %v", unmarshalErr.Error()) } if o.DebugTraceFlags["api"] { log.Debugf("%s result:\n%v", requestType, v) } return v, nil } // ToString implements a String() function via reflection func ToString(val reflect.Value) string { if reflect.TypeOf(val).Kind() == reflect.Ptr { val = reflect.Indirect(val) } var buffer bytes.Buffer if reflect.ValueOf(val).Kind() == reflect.Struct { for i := 0; i < val.Type().NumField(); i++ { fieldName := val.Type().Field(i).Name fieldType := val.Type().Field(i) fieldTag := fieldType.Tag fieldValue := val.Field(i) switch val.Field(i).Kind() { case reflect.Ptr: fieldValue = reflect.Indirect(val.Field(i)) default: fieldValue = val.Field(i) } if fieldTag != "" { xmlTag := fieldTag.Get("xml") if xmlTag != "" { fieldName = xmlTag } } if fieldValue.IsValid() { buffer.WriteString(fmt.Sprintf("%s: %v\n", fieldName, fieldValue)) } else { buffer.WriteString(fmt.Sprintf("%s: %v\n", fieldName, "nil")) } } } return buffer.String() } func ValidateZAPIResponse(response *http.Response) (*http.Response, error) { resp, err := ioutil.ReadAll(response.Body) if err != nil { return nil, err } respString := string(resp) // Remove newlines sanitizedString := strings.ReplaceAll(respString, "\n", "") if errs := xrv.ValidateAll(strings.NewReader(sanitizedString)); len(errs) > 0 { for verr := range errs { log.Errorf("validation of ZAPI XML caused an error: %v", verr) } return nil, errors.New("zapiCommand XML response validation failed") } // Create a manual http response using the already read body, while using all the other fields as-is; this should // prevent us from having to change the return type to []byte, which would likely require changes to a large number // of azgo files. Creating a response manually with only the Body would not be seen as valid by consumers of the // response return &http.Response{ Status: response.Status, StatusCode: response.StatusCode, Proto: response.Proto, ProtoMajor: response.ProtoMajor, ProtoMinor: response.ProtoMinor, Body: ioutil.NopCloser(bytes.NewBufferString(respString)), ContentLength: int64(len(respString)), Request: response.Request, Header: response.Header, }, nil }