github.com/LINBIT/golinstor@v0.52.0/client/client.go (about) 1 // A REST client to interact with LINSTOR's REST API 2 // Copyright (C) LINBIT HA-Solutions GmbH 3 // All Rights Reserved. 4 // Author: Roland Kammerer <roland.kammerer@linbit.com> 5 // 6 // Licensed under the Apache License, Version 2.0 (the "License"); you may 7 // not use this file except in compliance with the License. You may obtain 8 // a copy of the License at 9 // 10 // http://www.apache.org/licenses/LICENSE-2.0 11 // 12 // Unless required by applicable law or agreed to in writing, software 13 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 // License for the specific language governing permissions and limitations 16 // under the License. 17 18 package client 19 20 import ( 21 "bytes" 22 "context" 23 "crypto/tls" 24 "crypto/x509" 25 "encoding/json" 26 "errors" 27 "fmt" 28 "io" 29 "log" 30 "net" 31 "net/http" 32 "net/url" 33 "os" 34 "strings" 35 "sync" 36 "time" 37 38 "github.com/donovanhide/eventsource" 39 "golang.org/x/time/rate" 40 "moul.io/http2curl/v2" 41 ) 42 43 // Client is a struct representing a LINSTOR REST client. 44 type Client struct { 45 httpClient *http.Client 46 baseURL *url.URL 47 basicAuth *BasicAuthCfg 48 bearerToken string 49 userAgent string 50 controllers []*url.URL 51 lim *rate.Limiter 52 log interface{} // must be either Logger or LeveledLogger 53 54 Nodes NodeProvider 55 ResourceDefinitions ResourceDefinitionProvider 56 Resources ResourceProvider 57 ResourceGroups ResourceGroupProvider 58 StoragePoolDefinitions StoragePoolDefinitionProvider 59 Encryption EncryptionProvider 60 Controller ControllerProvider 61 Events EventProvider 62 Vendor VendorProvider 63 Remote RemoteProvider 64 Backup BackupProvider 65 KeyValueStore KeyValueStoreProvider 66 Connections ConnectionProvider 67 } 68 69 // Logger represents a standard logger interface 70 type Logger interface { 71 Printf(string, ...interface{}) 72 } 73 74 // LeveledLogger interface implements the basic methods that a logger library needs 75 type LeveledLogger interface { 76 Errorf(string, ...interface{}) 77 Infof(string, ...interface{}) 78 Debugf(string, ...interface{}) 79 Warnf(string, ...interface{}) 80 } 81 82 type BasicAuthCfg struct { 83 Username, Password string 84 } 85 86 // const errors as in https://dave.cheney.net/2016/04/07/constant-errors 87 type clientError string 88 89 func (e clientError) Error() string { return string(e) } 90 91 const ( 92 // NotFoundError is the error type returned in case of a 404 error. This is required to test for this kind of error. 93 NotFoundError = clientError("404 Not Found") 94 // Name of the environment variable that stores the certificate used for TLS client authentication 95 UserCertEnv = "LS_USER_CERTIFICATE" 96 // Name of the environment variable that stores the key used for TLS client authentication 97 UserKeyEnv = "LS_USER_KEY" 98 // Name of the environment variable that stores the certificate authority for the LINSTOR HTTPS API 99 RootCAEnv = "LS_ROOT_CA" 100 // Name of the environment variable that holds the URL(s) of LINSTOR controllers 101 ControllerUrlEnv = "LS_CONTROLLERS" 102 // Name of the environment variable that holds the username for authentication 103 UsernameEnv = "LS_USERNAME" 104 // Name of the environment variable that holds the password for authentication 105 PasswordEnv = "LS_PASSWORD" 106 // Name of the environment variable that points to the file containing the token for authentication 107 BearerTokenFileEnv = "LS_BEARER_TOKEN_FILE" 108 ) 109 110 // For example: 111 // u, _ := url.Parse("http://somehost:3370") 112 // c, _ := linstor.NewClient(linstor.BaseURL(u)) 113 114 // Option configures a LINSTOR Client 115 type Option func(*Client) error 116 117 // BaseURL is a client's option to set the baseURL of the REST client. 118 func BaseURL(URL *url.URL) Option { 119 return func(c *Client) error { 120 c.baseURL = URL 121 return nil 122 } 123 } 124 125 // BasicAuth is a client's option to set username and password for the REST client. 126 func BasicAuth(basicauth *BasicAuthCfg) Option { 127 return func(c *Client) error { 128 c.basicAuth = basicauth 129 return nil 130 } 131 } 132 133 // HTTPClient is a client's option to set a specific http.Client. 134 func HTTPClient(httpClient *http.Client) Option { 135 return func(c *Client) error { 136 c.httpClient = httpClient 137 return nil 138 } 139 } 140 141 // Log is a client's option to set a Logger 142 func Log(logger interface{}) Option { 143 return func(c *Client) error { 144 switch logger.(type) { 145 case Logger, LeveledLogger, nil: 146 c.log = logger 147 default: 148 return errors.New("Invalid logger type, expected Logger or LeveledLogger") 149 } 150 return nil 151 } 152 } 153 154 // Limiter to use when making queries. 155 // Mutually exclusive with Limit, last applied option wins. 156 func Limiter(limiter *rate.Limiter) Option { 157 return func(c *Client) error { 158 if limiter.Burst() == 0 && limiter.Limit() != rate.Inf { 159 return fmt.Errorf("invalid rate limit, burst must not be zero for non-unlimited rates") 160 } 161 c.lim = limiter 162 return nil 163 } 164 } 165 166 // Limit is the client's option to set number of requests per second and 167 // max number of bursts. 168 // Mutually exclusive with Limiter, last applied option wins. 169 // Deprecated: Use Limiter instead. 170 func Limit(r rate.Limit, b int) Option { 171 return Limiter(rate.NewLimiter(r, b)) 172 } 173 174 func Controllers(controllers []string) Option { 175 return func(c *Client) error { 176 var err error 177 c.controllers, err = parseURLs(controllers) 178 return err 179 } 180 } 181 182 // BearerToken configures authentication via the given token send in the Authorization Header. 183 // If set, this will override any authentication happening via Basic Authentication. 184 func BearerToken(token string) Option { 185 return func(c *Client) error { 186 c.bearerToken = token 187 return nil 188 } 189 } 190 191 // UserAgent sets the User-Agent header for every request to the given string. 192 func UserAgent(ua string) Option { 193 return func(c *Client) error { 194 c.userAgent = ua 195 return nil 196 } 197 } 198 199 // buildHttpClient constructs an HTTP client which will be used to connect to 200 // the LINSTOR controller. It recongnizes some environment variables which can 201 // be used to configure the HTTP client at runtime. If an invalid key or 202 // certificate is passed, an error is returned. 203 // If none or not all of the environment variables are passed, the default 204 // client is used as a fallback. 205 func buildHttpClient() (*http.Client, error) { 206 certPEM, cert := os.LookupEnv(UserCertEnv) 207 keyPEM, key := os.LookupEnv(UserKeyEnv) 208 caPEM, ca := os.LookupEnv(RootCAEnv) 209 210 if key != cert { 211 return nil, fmt.Errorf("'%s', '%s': specify both or none", UserKeyEnv, UserCertEnv) 212 } 213 214 if !cert && !key && !ca { 215 // Non of the special variables was set -> if TLS is used, default configuration can be used 216 return http.DefaultClient, nil 217 } 218 219 tlsConfig := &tls.Config{} 220 221 if ca { 222 caPool := x509.NewCertPool() 223 ok := caPool.AppendCertsFromPEM([]byte(caPEM)) 224 if !ok { 225 return nil, fmt.Errorf("failed to get a valid certificate from '%s'", RootCAEnv) 226 } 227 tlsConfig.RootCAs = caPool 228 } 229 230 if key && cert { 231 keyPair, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM)) 232 if err != nil { 233 return nil, fmt.Errorf("failed to load keys: %w", err) 234 } 235 tlsConfig.Certificates = append(tlsConfig.Certificates, keyPair) 236 } 237 238 return &http.Client{ 239 Transport: &http.Transport{ 240 TLSClientConfig: tlsConfig, 241 }, 242 }, nil 243 } 244 245 // Return the default scheme to access linstor 246 // If one of the HTTPS environment variables is set, will return "https". 247 // If not, will return "http" 248 func defaultScheme() string { 249 _, ca := os.LookupEnv(RootCAEnv) 250 _, cert := os.LookupEnv(UserCertEnv) 251 _, key := os.LookupEnv(UserKeyEnv) 252 if ca || cert || key { 253 return "https" 254 } 255 return "http" 256 } 257 258 const defaultHost = "localhost" 259 260 // Return the default port to access linstor. 261 // Defaults are: 262 // "https": 3371 263 // "http": 3370 264 func defaultPort(scheme string) string { 265 if scheme == "https" { 266 return "3371" 267 } 268 return "3370" 269 } 270 271 // tryConnect takes a slice of urls and tries to Dial each one of the hosts. 272 // If a working URL is found, it is returned. 273 // If the slice contains no working URL, a list of all connection errors is returned. 274 func tryConnect(urls []*url.URL) (*url.URL, []error) { 275 var wg sync.WaitGroup 276 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 277 defer cancel() 278 errChan := make(chan error) 279 indexChan := make(chan int) 280 doneChan := make(chan bool) 281 wg.Add(len(urls)) 282 for i := range urls { 283 i := i 284 go func() { 285 defer wg.Done() 286 conn, err := (&net.Dialer{}).DialContext(ctx, "tcp", urls[i].Host) 287 if err != nil { 288 errChan <- err 289 return 290 } 291 cancel() 292 conn.Close() 293 indexChan <- i 294 }() 295 } 296 297 go func() { 298 wg.Wait() 299 doneChan <- true 300 }() 301 302 var errs []error 303 for { 304 select { 305 case result := <-indexChan: 306 return urls[result], nil 307 case err := <-errChan: 308 errs = append(errs, err) 309 case <-doneChan: 310 return nil, errs 311 } 312 } 313 } 314 315 func parseBaseURL(urlString string) (*url.URL, error) { 316 // Check scheme 317 urlSplit := strings.Split(urlString, "://") 318 319 if len(urlSplit) == 1 { 320 if urlSplit[0] == "" { 321 urlSplit[0] = defaultHost 322 } 323 urlSplit = []string{defaultScheme(), urlSplit[0]} 324 } 325 326 if len(urlSplit) != 2 { 327 return nil, fmt.Errorf("URL with multiple scheme separators. parts: %v", urlSplit) 328 } 329 scheme, endpoint := urlSplit[0], urlSplit[1] 330 switch scheme { 331 case "linstor": 332 scheme = defaultScheme() 333 case "linstor+ssl": 334 scheme = "https" 335 } 336 337 // Check port 338 endpointSplit := strings.Split(endpoint, ":") 339 if len(endpointSplit) == 1 { 340 endpointSplit = []string{endpointSplit[0], defaultPort(scheme)} 341 } 342 if len(endpointSplit) != 2 { 343 return nil, fmt.Errorf("URL with multiple port separators. parts: %v", endpointSplit) 344 } 345 host, port := endpointSplit[0], endpointSplit[1] 346 347 return url.Parse(fmt.Sprintf("%s://%s:%s", scheme, host, port)) 348 } 349 350 func parseURLs(urls []string) ([]*url.URL, error) { 351 var result []*url.URL 352 for _, controller := range urls { 353 url, err := parseBaseURL(controller) 354 if err != nil { 355 return nil, err 356 } 357 result = append(result, url) 358 } 359 360 return result, nil 361 } 362 363 // NewClient takes an arbitrary number of options and returns a Client or an error. 364 // It recognizes several environment variables which can be used to configure 365 // the client at runtime: 366 // 367 // - LS_CONTROLLERS: a comma-separated list of LINSTOR controllers to connect to. 368 // 369 // - LS_USERNAME, LS_PASSWORD: can be used to authenticate against the LINSTOR 370 // controller using HTTP basic authentication. 371 // 372 // - LS_USER_CERTIFICATE, LS_USER_KEY, LS_ROOT_CA: can be used to enable TLS on 373 // the HTTP client, enabling encrypted communication with the LINSTOR controller. 374 // 375 // - LS_BEARER_TOKEN_FILE: can be set to a file containing the bearer token used 376 // for authentication. 377 // 378 // Options passed to NewClient take precedence over options passed in via 379 // environment variables. 380 func NewClient(options ...Option) (*Client, error) { 381 httpClient, err := buildHttpClient() 382 if err != nil { 383 return nil, fmt.Errorf("failed to build http client: %w", err) 384 } 385 386 c := &Client{ 387 httpClient: httpClient, 388 basicAuth: &BasicAuthCfg{ 389 Username: os.Getenv(UsernameEnv), 390 Password: os.Getenv(PasswordEnv), 391 }, 392 lim: rate.NewLimiter(rate.Inf, 0), 393 log: log.New(os.Stderr, "", 0), 394 } 395 396 c.Nodes = &NodeService{client: c} 397 c.ResourceDefinitions = &ResourceDefinitionService{client: c} 398 c.Resources = &ResourceService{client: c} 399 c.Encryption = &EncryptionService{client: c} 400 c.ResourceGroups = &ResourceGroupService{client: c} 401 c.StoragePoolDefinitions = &StoragePoolDefinitionService{client: c} 402 c.Controller = &ControllerService{client: c} 403 c.Events = &EventService{client: c} 404 c.Vendor = &VendorService{client: c} 405 c.Remote = &RemoteService{client: c} 406 c.Backup = &BackupService{client: c} 407 c.KeyValueStore = &KeyValueStoreService{client: c} 408 c.Connections = &ConnectionService{client: c} 409 410 if path, ok := os.LookupEnv(BearerTokenFileEnv); ok { 411 token, err := os.ReadFile(path) 412 if err != nil { 413 return nil, fmt.Errorf("failed to read token from file: %w", err) 414 } 415 416 c.bearerToken = string(token) 417 } 418 419 for _, opt := range options { 420 if err := opt(c); err != nil { 421 return nil, err 422 } 423 } 424 425 if c.baseURL == nil { 426 if len(c.controllers) == 0 { 427 // if not already set by option, get from environment... 428 controllersStr := os.Getenv(ControllerUrlEnv) 429 if controllersStr == "" { 430 // ... or fall back to defaults 431 controllersStr = fmt.Sprintf("%v://%v:%v", defaultScheme(), defaultHost, defaultPort(defaultScheme())) 432 } 433 434 c.controllers, err = parseURLs(strings.Split(controllersStr, ",")) 435 if err != nil { 436 return nil, fmt.Errorf("failed to parse controller URLs: %w", err) 437 } 438 } 439 440 // if we have exactly one controller, use that directly, otherwise the 441 // controller will be figured out in findRespondingController(). 442 if len(c.controllers) == 1 { 443 c.baseURL = c.controllers[0] 444 } 445 } 446 447 return c, nil 448 } 449 450 func (c *Client) BaseURL() *url.URL { 451 return c.baseURL 452 } 453 454 func (c *Client) newRequest(method, path string, body interface{}) (*http.Request, error) { 455 rel, err := url.Parse(path) 456 if err != nil { 457 return nil, err 458 } 459 460 if c.baseURL == nil { 461 if err := c.findRespondingController(); err != nil { 462 return nil, fmt.Errorf("failed to connect: %w", err) 463 } 464 if c.baseURL == nil { 465 // should not happen since findRespondingController() 466 // always either sets baseURL or errors out, but just in case... 467 return nil, fmt.Errorf("failed to determine base URL") 468 } 469 } 470 u := c.baseURL.ResolveReference(rel) 471 472 var buf io.ReadWriter 473 if body != nil { 474 buf = new(bytes.Buffer) 475 err := json.NewEncoder(buf).Encode(body) 476 if err != nil { 477 return nil, err 478 } 479 switch l := c.log.(type) { 480 case LeveledLogger: 481 l.Debugf("%s", buf) 482 case Logger: 483 l.Printf("[DEBUG] %s", body) 484 } 485 } 486 487 req, err := http.NewRequest(method, u.String(), buf) 488 if err != nil { 489 return nil, err 490 } 491 492 if body != nil { 493 req.Header.Set("Content-Type", "application/json") 494 } 495 496 if c.userAgent != "" { 497 req.Header.Set("User-Agent", c.userAgent) 498 } 499 500 req.Header.Set("Accept", "application/json") 501 502 username := c.basicAuth.Username 503 if username != "" { 504 req.SetBasicAuth(username, c.basicAuth.Password) 505 } 506 507 if c.bearerToken != "" { 508 req.Header.Set("Authorization", "Bearer "+c.bearerToken) 509 } 510 511 return req, nil 512 } 513 514 func (c *Client) curlify(req *http.Request) (string, error) { 515 cc, err := http2curl.GetCurlCommand(req) 516 if err != nil { 517 return "", err 518 } 519 return cc.String(), nil 520 } 521 522 // findRespondingController scans the list of controllers for a working LINSTOR 523 // controller. It sets the baseURL of the client to the first working controller 524 // that is found. If there is only exactly one controller in the controller 525 // list, it is used directly. 526 func (c *Client) findRespondingController() error { 527 switch num := len(c.controllers); { 528 case num > 1: 529 url, errors := tryConnect(c.controllers) 530 if errors != nil { 531 logError := func(msg string) { 532 switch l := c.log.(type) { 533 case LeveledLogger: 534 l.Errorf(msg) 535 case Logger: 536 l.Printf("[ERROR] %s", msg) 537 } 538 } 539 logError("Unable to connect to any of the given controller hosts:") 540 for _, e := range errors { 541 logError(fmt.Sprintf(" - %v", e)) 542 } 543 return fmt.Errorf("could not connect to any controller") 544 } 545 c.baseURL = url 546 case num == 1: 547 c.baseURL = c.controllers[0] 548 default: 549 return fmt.Errorf("no controller to connect to") 550 } 551 552 return nil 553 } 554 555 func (c *Client) logCurlify(req *http.Request) { 556 var msg string 557 if curl, err := c.curlify(req); err != nil { 558 msg = err.Error() 559 } else { 560 msg = curl 561 } 562 563 switch l := c.log.(type) { 564 case LeveledLogger: 565 l.Debugf("%s", msg) 566 case Logger: 567 l.Printf("[DEBUG] %s", msg) 568 } 569 } 570 571 func (c *Client) retry(origErr error, req *http.Request) (*http.Response, error) { 572 // only retry on network errors and if we even have another controller to choose from 573 if _, ok := origErr.(net.Error); !ok || len(c.controllers) <= 1 { 574 return nil, origErr 575 } 576 577 prevBaseURL := c.baseURL 578 e := c.findRespondingController() 579 // if findRespondingController failed, or we just got the same base URL, don't bother retrying 580 if e != nil && c.baseURL == prevBaseURL { 581 return nil, origErr 582 } 583 584 req.URL.Host = c.baseURL.Host 585 return c.httpClient.Do(req) 586 } 587 588 func (c *Client) do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) { 589 if err := c.lim.Wait(ctx); err != nil { 590 return nil, err 591 } 592 req = req.WithContext(ctx) 593 594 c.logCurlify(req) 595 596 resp, err := c.httpClient.Do(req) 597 if err != nil { 598 select { 599 case <-ctx.Done(): 600 return nil, ctx.Err() 601 default: 602 } 603 604 // if this was a connectivity issue, attempt a retry 605 resp, err = c.retry(err, req) 606 if err != nil { 607 return nil, err 608 } 609 } 610 defer resp.Body.Close() 611 612 if resp.StatusCode < 200 || resp.StatusCode >= 400 { 613 msg := fmt.Sprintf("Status code not within 200 to 400, but %d (%s)\n", 614 resp.StatusCode, http.StatusText(resp.StatusCode)) 615 switch l := c.log.(type) { 616 case LeveledLogger: 617 l.Debugf("%s", msg) 618 case Logger: 619 l.Printf("[DEBUG] %s", msg) 620 } 621 if resp.StatusCode == 404 { 622 return nil, NotFoundError 623 } 624 625 var rets ApiCallError 626 if err = json.NewDecoder(resp.Body).Decode(&rets); err != nil { 627 return nil, err 628 } 629 return nil, rets 630 } 631 632 if v != nil { 633 err = json.NewDecoder(resp.Body).Decode(v) 634 } 635 return resp, err 636 } 637 638 // Higer Leve Abstractions 639 640 func (c *Client) doGET(ctx context.Context, url string, ret interface{}, opts ...*ListOpts) (*http.Response, error) { 641 642 u, err := addOptions(url, genOptions(opts...)) 643 if err != nil { 644 return nil, err 645 } 646 647 req, err := c.newRequest("GET", u, nil) 648 if err != nil { 649 return nil, err 650 } 651 return c.do(ctx, req, ret) 652 } 653 654 func (c *Client) doEvent(ctx context.Context, url, lastEventId string) (*eventsource.Stream, error) { 655 req, err := c.newRequest("GET", url, nil) 656 if err != nil { 657 return nil, err 658 } 659 req.Header.Set("Accept", "text/event-stream") 660 req = req.WithContext(ctx) 661 662 stream, err := eventsource.SubscribeWith(lastEventId, c.httpClient, req) 663 if err != nil { 664 return nil, err 665 } 666 667 return stream, nil 668 } 669 670 func (c *Client) doPOST(ctx context.Context, url string, body interface{}) (*http.Response, error) { 671 req, err := c.newRequest("POST", url, body) 672 if err != nil { 673 return nil, err 674 } 675 676 return c.do(ctx, req, nil) 677 } 678 679 func (c *Client) doPUT(ctx context.Context, url string, body interface{}) (*http.Response, error) { 680 req, err := c.newRequest("PUT", url, body) 681 if err != nil { 682 return nil, err 683 } 684 685 return c.do(ctx, req, nil) 686 } 687 688 func (c *Client) doPATCH(ctx context.Context, url string, body interface{}) (*http.Response, error) { 689 req, err := c.newRequest("PATCH", url, body) 690 if err != nil { 691 return nil, err 692 } 693 694 return c.do(ctx, req, nil) 695 } 696 697 func (c *Client) doDELETE(ctx context.Context, url string, body interface{}) (*http.Response, error) { 698 req, err := c.newRequest("DELETE", url, body) 699 if err != nil { 700 return nil, err 701 } 702 703 return c.do(ctx, req, nil) 704 } 705 706 func (c *Client) doOPTIONS(ctx context.Context, url string, ret interface{}, body interface{}) (*http.Response, error) { 707 req, err := c.newRequest("OPTIONS", url, body) 708 if err != nil { 709 return nil, err 710 } 711 712 return c.do(ctx, req, ret) 713 } 714 715 // ApiCallRc represents the struct returned by LINSTOR, when accessing its REST API. 716 type ApiCallRc struct { 717 // A masked error number 718 RetCode int64 `json:"ret_code"` 719 Message string `json:"message"` 720 // Cause of the error 721 Cause string `json:"cause,omitempty"` 722 // Details to the error message 723 Details string `json:"details,omitempty"` 724 // Possible correction options 725 Correction string `json:"correction,omitempty"` 726 // List of error report ids related to this api call return code. 727 ErrorReportIds []string `json:"error_report_ids,omitempty"` 728 // Map of objection that have been involved by the operation. 729 ObjRefs map[string]string `json:"obj_refs,omitempty"` 730 } 731 732 func (rc *ApiCallRc) String() string { 733 s := fmt.Sprintf("Message: '%s'", rc.Message) 734 if rc.Cause != "" { 735 s += fmt.Sprintf("; Cause: '%s'", rc.Cause) 736 } 737 if rc.Details != "" { 738 s += fmt.Sprintf("; Details: '%s'", rc.Details) 739 } 740 if rc.Correction != "" { 741 s += fmt.Sprintf("; Correction: '%s'", rc.Correction) 742 } 743 if len(rc.ErrorReportIds) > 0 { 744 s += fmt.Sprintf("; Reports: '[%s]'", strings.Join(rc.ErrorReportIds, ",")) 745 } 746 747 return s 748 } 749 750 // DeleteProps is a slice of properties to delete. 751 type DeleteProps []string 752 753 // OverrideProps is a map of properties to modify (key/value pairs) 754 type OverrideProps map[string]string 755 756 // Namespaces to delete 757 type DeleteNamespaces []string 758 759 // GenericPropsModify is a struct combining DeleteProps and OverrideProps 760 type GenericPropsModify struct { 761 DeleteProps DeleteProps `json:"delete_props,omitempty"` 762 OverrideProps OverrideProps `json:"override_props,omitempty"` 763 DeleteNamespaces DeleteNamespaces `json:"delete_namespaces,omitempty"` 764 }