// Copyright (C) 2020 Marius Schellenberger package pve import ( "bytes" "context" "crypto/tls" "errors" "fmt" "io" "io/ioutil" "net" "net/http" "net/http/httputil" "os" "strconv" "strings" "sync" "time" ) /* type BackoffFunc func(retries int) time.Duration func ConstantBackoff(d time.Duration) BackoffFunc { return func(_ int) time.Duration { return d } } func ExponentialBackoff(b float64, d time.Duration) BackoffFunc { return func(retries int) time.Duration { return time.Duration(math.Pow(b, float64(retries))) * d } } */ type Client struct { sync.Mutex endpoints []string username string password string session session //pollInterval time.Duration //backoffFunc BackoffFunc httpClient *http.Client debugWriter io.Writer insecure bool Server ServerClient Task TaskClient Pool PoolClient Node NodeClient Snippet SnippetClient } type session struct { Username string `json:"username"` Ticket string `json:"ticket"` CSRFPreventionToken string `json:"CSRFPreventionToken"` Time time.Time `json:"-"` } type ClientOption func(*Client) func WithEndpoint(endpoint string) ClientOption { return func(client *Client) { client.endpoints = append(client.endpoints, strings.TrimRight(endpoint, "/")) } } func WithEndpoints(endpoints []string) ClientOption { return func(client *Client) { for _, e := range endpoints { client.endpoints = append(client.endpoints, strings.TrimRight(e, "/")) } } } /* func WithPollInterval(pollInterval time.Duration) ClientOption { return func(client *Client) { client.pollInterval = pollInterval } } func WithBackoffFunc(f BackoffFunc) ClientOption { return func(client *Client) { client.backoffFunc = f } } */ func WithDebugWriter(debugWriter io.Writer) ClientOption { return func(client *Client) { client.debugWriter = debugWriter } } func WithHTTPClient(httpClient *http.Client) ClientOption { return func(client *Client) { client.httpClient = httpClient } } func WithInsecureClient(insecure bool) ClientOption { return func(client *Client) { client.insecure = insecure } } func WithCredentials(username, password string) ClientOption { return func(client *Client) { client.username = username client.password = password } } var ( ErrMissingEndpointsEnv = errors.New("missing environment variable PVE_ENDPOINTS") ErrMissingUserEnv = errors.New("missing environment variable PVE_USER") ErrMissingPasswordEnv = errors.New("missing environment variable PVE_PASSWORD") ErrMissingEndpoints = errors.New("missing Endpoints in config") ErrMissingUser = errors.New("missing User in config") ErrMissingPassword = errors.New("missing Password in config") ) type Config struct { Endpoints []string `json:"endpoints"` User string `json:"user"` Password string `json:"password"` Insecure bool `json:"insecure"` } func ClientOptionsFromConfig(cfg Config) (options []ClientOption, err error) { if len(cfg.Endpoints) == 0 { err = ErrMissingEndpoints return } if cfg.User == "" { err = ErrMissingUser return } if cfg.Password == "" { err = ErrMissingPassword return } options = []ClientOption{ WithEndpoints(cfg.Endpoints), WithCredentials(cfg.User, cfg.Password), WithInsecureClient(cfg.Insecure), } return } func ClientOptionsFromEnv() (options []ClientOption, err error) { endpoints := os.Getenv("PVE_ENDPOINTS") if endpoints == "" { err = ErrMissingEndpointsEnv return } user := os.Getenv("PVE_USER") if user == "" { err = ErrMissingUserEnv return } pass := os.Getenv("PVE_PASSWORD") if pass == "" { err = ErrMissingPasswordEnv return } insecure, _ := strconv.ParseBool(os.Getenv("PVE_INSECURE")) options = []ClientOption{ WithEndpoints(strings.Split(endpoints, ",")), WithCredentials(user, pass), WithInsecureClient(insecure), } return } func NewClient(options ...ClientOption) *Client { client := &Client{} /* endpoint: Endpoint, backoffFunc: ExponentialBackoff(2, 500*time.Millisecond), pollInterval: 500 * time.Millisecond, }*/ for _, option := range options { option(client) } if client.httpClient == nil { client.httpClient = &http.Client{Transport: &http.Transport{ DialContext: (&net.Dialer{ Timeout: 30 * time.Second, }).DialContext, TLSClientConfig: &tls.Config{ InsecureSkipVerify: client.insecure, }, }} } client.Server = ServerClient{client: client} client.Task = TaskClient{client: client} client.Pool = PoolClient{client: client} client.Node = NodeClient{client: client} client.Snippet = SnippetClient{client: client} return client } func (c *Client) getEndpoint() string { if len(c.endpoints) == 0 { return "" } return c.endpoints[0] } func (c *Client) Auth() (err error) { if !time.Now().After(c.session.Time) { return } c.Lock() defer c.Unlock() if !time.Now().After(c.session.Time) { return } u := c.getEndpoint() + "/access/ticket" body := httpbody{"username": c.username, "password": c.password} r, err := http.NewRequest("POST", u, body.Reader()) if err != nil { return } resp, err := c.httpClient.Do(r) if err != nil { return } if resp.StatusCode != http.StatusOK { return fmt.Errorf("api authentication failed: %w", errors.New(resp.Status)) } err = unwrapData(resp, &c.session) if err != nil { return } c.session.Time = time.Now().Add(time.Hour) return } func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { err := c.Auth() if err != nil { return nil, err } url := c.getEndpoint() + path req, err := http.NewRequest(method, url, body) if err != nil { return nil, err } req.AddCookie(&http.Cookie{Name: "PVEAuthCookie", Value: c.session.Ticket}) if method != "GET" { req.Header.Add("CSRFPreventionToken", c.session.CSRFPreventionToken) } req = req.WithContext(ctx) return req, nil } var HTTPErr = errors.New("http error") func (c *Client) Do(r *http.Request, v interface{}) (resp *http.Response, err error) { if c.debugWriter != nil { dumpReq, err := httputil.DumpRequestOut(r, true) if err != nil { return &http.Response{}, err } fmt.Fprintf(c.debugWriter, "--- Request:\n%s\n\n", dumpReq) } resp, err = c.httpClient.Do(r) if err != nil { return } body, err := ioutil.ReadAll(resp.Body) if err != nil { resp.Body.Close() return } resp.Body.Close() resp.Body = ioutil.NopCloser(bytes.NewReader(body)) if c.debugWriter != nil { dumpResp, err := httputil.DumpResponse(resp, true) if err != nil { return resp, err } fmt.Fprintf(c.debugWriter, "--- Response:\n%s\n\n", dumpResp) } err = unwrapData(resp, v) if resp.StatusCode >= 400 && resp.StatusCode <= 599 { if err != nil { err = fmt.Errorf("%w: %s: %s", HTTPErr, errors.New(resp.Status), err.Error()) return } err = fmt.Errorf("%w: %s", HTTPErr, errors.New(resp.Status)) } return }