blob: b7f7d6f123183e72b0e3cc0c1468b5f8d5877fcd [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package hwid
import (
"context"
"encoding/json"
"fmt"
"infra/cros/lab_inventory/utils"
"io/ioutil"
"net/http"
"net/url"
"time"
authclient "go.chromium.org/luci/auth"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/server/auth"
invlibs "infra/cros/lab_inventory/protos"
)
var (
hwidServerURL = "https://chromeos-hwid.appspot.com/api/chromeoshwid/v1/%s/%s/?key=%s"
hwidServerResponseErrorKey = "error"
cacheMaxAge = 10 * time.Minute
// New HWID oneplatform API endpoint
hwidEndpoint = "chromeoshwid-pa.googleapis.com"
hwidEndpointScope = "https://www.googleapis.com/auth/chromeoshwid"
)
// Data we interested from HWID server.
type Data struct {
// The Sku string returned by hwid server. It's not the SKU (aka variant).
Sku string
// The variant string returned by hwid server. It's not the variant (aka
// SKU).
Variant string
}
type hwidEntity struct {
_kind string `gae:"$kind,HwidData"`
ID string `gae:"$id"`
Data Data `gae:",noindex"`
Updated time.Time
}
func callHwidServer(rpc string, hwid string, secret string) ([]byte, error) {
url := fmt.Sprintf(hwidServerURL, rpc, url.PathEscape(hwid), secret)
rsp, err := http.Get(url)
if err != nil {
return nil, err
}
defer rsp.Body.Close()
body, err := ioutil.ReadAll(rsp.Body)
if err != nil {
return nil, err
}
if rsp.StatusCode != http.StatusOK {
return nil, errors.Reason("HWID server responsonse was not OK: %s", body).Err()
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
// HWID server response errors as a json stream with 200 code.
if v, ok := result[hwidServerResponseErrorKey]; ok {
return nil, errors.Reason(v.(string)).Err()
}
return body, nil
}
// GetHwidData gets the hwid data from hwid server.
func GetHwidData(ctx context.Context, hwid, secret string) (*Data, error) {
now := time.Now().UTC()
e := hwidEntity{ID: hwid}
errFromDatastore := datastore.Get(ctx, &e)
if errFromDatastore == nil {
if now.Sub(e.Updated) < cacheMaxAge {
logging.Debugf(ctx, "HWID HIT: %#v", hwid)
return &e.Data, nil
}
}
logging.Debugf(ctx, "HWID MISS or STALE: %#v", hwid)
d, err := getDataFromHwidServer(ctx, hwid, secret)
if err != nil {
if errFromDatastore == nil {
logging.Warningf(ctx, "Use stale data as HWID server failed: %s", err.Error())
return &e.Data, nil
}
return nil, err
}
e.Data = *d
e.Updated = now
if err := datastore.Put(ctx, &e); err != nil {
logging.Warningf(ctx, "failed to cache hwid: %#v: %s", hwid, err.Error())
}
return d, nil
}
func getDataFromHwidServer(ctx context.Context, hwid string, secret string) (*Data, error) {
data := Data{}
rspBody, err := callHwidServer("dutlabel", hwid, secret)
if err != nil {
return nil, err
}
var dutlabels map[string][]interface{}
if err := json.Unmarshal(rspBody, &dutlabels); err != nil {
return nil, err
}
for key, value := range dutlabels {
if key != "labels" {
continue
}
for _, labelData := range value {
label := labelData.(map[string]interface{})
switch label["name"].(string) {
case "sku":
data.Sku = label["value"].(string)
case "variant":
data.Variant = label["value"].(string)
}
}
}
return &data, nil
}
type HWIDClient struct {
hc *http.Client
}
func Init(ctx context.Context) (*HWIDClient, error) {
tr, err := auth.GetRPCTransport(ctx, auth.AsSelf, auth.WithScopes(authclient.OAuthScopeEmail, hwidEndpointScope))
if err != nil {
return nil, err
}
return &HWIDClient{
hc: &http.Client{Transport: tr},
}, nil
}
func (c *HWIDClient) QueryHWID(ctx context.Context, hwid string) (*Data, error) {
u := &url.URL{
Scheme: "https",
Host: hwidEndpoint,
Path: fmt.Sprintf("v2/dutlabel/%s", hwid),
}
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, err
}
dutLabel := &invlibs.GetDutLabelResponse{}
if err := utils.ExecuteRequest(ctx, c.hc, req, dutLabel); err != nil {
return nil, err
}
return parseGetDutLabelResponse(dutLabel), nil
}
func parseGetDutLabelResponse(resp *invlibs.GetDutLabelResponse) *Data {
data := Data{}
for _, l := range resp.GetDutLabel().GetLabels() {
switch l.GetName() {
case "sku":
data.Sku = l.GetValue()
case "variant":
data.Variant = l.GetValue()
}
}
return &data
}