blob: 06167030d3ee153d1937a29710a743594ebd3290 [file]
// 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 main
import (
"context"
"fmt"
"log"
"math/rand"
"net"
"net/url"
"strconv"
"strings"
"time"
"go.chromium.org/chromiumos/config/go/api/test/tls"
"go.chromium.org/chromiumos/config/go/api/test/tls/dependencies/longrunning"
"go.chromium.org/luci/common/errors"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
"golang.org/x/crypto/ssh"
"infra/cros/cmd/fleet-tlw/internal/cache"
"infra/libs/lro"
"infra/libs/sshpool"
ufsapi "infra/unifiedfleet/api/v1/rpc"
ufsUtil "infra/unifiedfleet/app/util"
)
type tlwServer struct {
tls.UnimplementedWiringServer
lroMgr *lro.Manager
tMgr *tunnelManager
dutPool *sshpool.Pool
proxyPool *sshpool.Pool
cFrontend *cache.Frontend
ufsClient ufsapi.FleetClient
}
func newTLWServer(ufsService string, proxySSHSigner ssh.Signer, serviceAcctJSON string) (*tlwServer, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ufsClient, err := ufsapi.NewClient(ctx, ufsapi.ServiceName(ufsService), ufsapi.ServiceAccountJSONPath(serviceAcctJSON), ufsapi.UserAgent("fleet-tlw/6.0.0"))
if err != nil {
return nil, errors.Reason("newTLWServer: %s", err).Err()
}
ce, err := cache.NewUFSEnv(ufsClient)
if err != nil {
return nil, errors.Reason("newTLWServer: %s", err).Err()
}
s := &tlwServer{
lroMgr: lro.New(),
dutPool: sshpool.New(getSSHClientConfig()),
proxyPool: sshpool.New(getSSHClientConfigForProxy(proxySSHSigner)),
tMgr: newTunnelManager(),
cFrontend: cache.NewFrontend(ce),
ufsClient: ufsClient,
}
return s, nil
}
func (s *tlwServer) registerWith(g *grpc.Server) {
tls.RegisterWiringServer(g, s)
longrunning.RegisterOperationsServer(g, s.lroMgr)
}
// Close closes all open server resources.
func (s *tlwServer) Close() {
s.tMgr.Close()
s.dutPool.Close()
s.proxyPool.Close()
s.lroMgr.Close()
}
func (s *tlwServer) OpenDutPort(ctx context.Context, req *tls.OpenDutPortRequest) (*tls.OpenDutPortResponse, error) {
addr, err := lookupHost(req.GetName())
if err != nil {
return nil, status.Errorf(codes.NotFound, err.Error())
}
return &tls.OpenDutPortResponse{
Address: addr,
Port: req.GetPort(),
}, nil
}
func (s *tlwServer) ExposePortToDut(ctx context.Context, req *tls.ExposePortToDutRequest) (*tls.ExposePortToDutResponse, error) {
localServicePort := req.GetLocalPort()
dutName := req.GetDutName()
if dutName == "" {
return nil, status.Errorf(codes.InvalidArgument, "DutName cannot be empty")
}
addr, err := lookupHost(dutName)
if err != nil {
return nil, status.Errorf(codes.FailedPrecondition, err.Error())
}
callerIP, err := getCallerIP(ctx)
if err != nil {
return nil, status.Errorf(codes.Aborted, err.Error())
}
localService := net.JoinHostPort(callerIP, strconv.Itoa(int(localServicePort)))
if req.GetRequireRemoteProxy() {
exposedAddr, exposedPort, err := s.exposePortUsingProxy(addr, localService)
if err != nil {
return nil, status.Errorf(codes.Aborted, "Error setting up SSH tunnel to proxy: %s", err)
}
return &tls.ExposePortToDutResponse{
ExposedAddress: exposedAddr,
ExposedPort: exposedPort,
}, nil
}
exposedAddr, exposedPort, err := s.exposePortDirectly(addr, localService)
if err != nil {
return nil, status.Errorf(codes.Aborted, "Error setting up SSH tunnel: %s", err)
}
return &tls.ExposePortToDutResponse{
ExposedAddress: exposedAddr,
ExposedPort: exposedPort,
}, nil
}
func (s *tlwServer) exposePortUsingProxy(dutAddr, localService string) (string, int32, error) {
// Use cache.Frontend here since we are depending on the Virtual IPs of
// the caching backends.
// TODO(crbug/1145811) Refactor the code to create a new package
// 'lab subnet' which both CacheForDut and ExposePortToDut can use.
// The new package 'lab subnet' can accept an IP/subnet mask and return
// the servers in that subnet. Then CacheForDut and ExposePortToDut can
// define their own logic to select one from them.
// Pass a random string so we can get an random proxy server.
cachingURL, err := s.cFrontend.AssignBackend(dutAddr, strconv.Itoa(rand.Int()))
if err != nil {
return "", 0, err
}
// cFrontend returns a URL in the format http://<ip>:<port>. Extract the
// <ip> from this URL.
u, err := url.Parse(cachingURL)
if err != nil {
return "", 0, err
}
proxyServerIP, _, err := net.SplitHostPort(u.Host)
if err != nil {
return "", 0, err
}
remoteDeviceClient, err := s.proxyPool.Get(net.JoinHostPort(proxyServerIP, "2222"))
if err != nil {
return "", 0, err
}
t, err := s.tMgr.NewTunnel(localService, "127.0.0.1:0", remoteDeviceClient)
if err != nil {
return "", 0, err
}
return proxyServerIP, int32(t.RemoteAddr().(*net.TCPAddr).Port), nil
}
func (s *tlwServer) exposePortDirectly(dutAddr, localService string) (string, int32, error) {
remoteDeviceClient, err := s.dutPool.Get(net.JoinHostPort(dutAddr, "22"))
if err != nil {
return "", 0, err
}
t, err := s.tMgr.NewTunnel(localService, "127.0.0.1:0", remoteDeviceClient)
if err != nil {
return "", 0, err
}
listenAddr := t.RemoteAddr().(*net.TCPAddr)
return listenAddr.IP.String(), int32(listenAddr.Port), nil
}
func (s *tlwServer) CacheForDut(ctx context.Context, req *tls.CacheForDutRequest) (*longrunning.Operation, error) {
rawURL := req.GetUrl()
if rawURL == "" {
return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("CacheForDut: unsupported url %s in request", rawURL))
}
parsedURL, err := url.Parse(rawURL)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("CacheForDut: unsupported url %s in request", rawURL))
}
dutName := req.GetDutName()
if dutName == "" {
return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("CacheForDut: unsupported DutName %s in request", dutName))
}
addr, err := lookupHost(dutName)
if err != nil {
return nil, status.Errorf(codes.NotFound, fmt.Sprintf("CacheForDut: lookup IP of %q: %s", dutName, err.Error()))
}
log.Printf("CacheForDut: the IP of %q is %q", dutName, addr)
op := s.lroMgr.NewOperation()
go s.cache(context.TODO(), parsedURL, addr, op.Name)
return op, status.Error(codes.OK, "Started: CacheForDut Operation.")
}
// cache implements the logic for the CacheForDut method and runs as a goroutine.
func (s *tlwServer) cache(ctx context.Context, parsedURL *url.URL, addr, opName string) {
log.Printf("CacheForDut: Started Operation = %v", opName)
path := fmt.Sprintf("%s%s", parsedURL.Host, parsedURL.Path)
// TODO (guocb): return a url.URL instead of string.
cs, err := s.cFrontend.AssignBackend(addr, path)
if err != nil {
log.Printf("CacheForDut: %s", err)
if err := s.lroMgr.SetError(opName, status.New(codes.FailedPrecondition, err.Error())); err != nil {
log.Printf("CacheForDut: failed to set error on %s: %s", opName, err)
}
return
}
u := fmt.Sprintf("%s/download/%s", strings.TrimSuffix(cs, "/"), path)
log.Printf("CacheForDut: result URL: %s", u)
if err := s.lroMgr.SetResult(opName, &tls.CacheForDutResponse{Url: u}); err != nil {
log.Printf("CacheForDut: failed to set result on %s: %s", opName, err)
}
log.Printf("CacheForDut: Operation Completed = %v", opName)
}
// lookupHost is a helper function that looks up the IP address of the provided
// host by using the local resolver.
func lookupHost(hostname string) (string, error) {
addrs, err := net.LookupHost(hostname)
if err != nil {
return "", err
}
if len(addrs) == 0 {
return "", fmt.Errorf("No IP addresses found for %s", hostname)
}
return addrs[0], nil
}
// getCallerIP gets the peer IP address from the provide context.
func getCallerIP(ctx context.Context) (string, error) {
p, ok := peer.FromContext(ctx)
if !ok {
return "", fmt.Errorf("Error determining IP address")
}
callerIP, _, err := net.SplitHostPort(p.Addr.String())
if err != nil {
return "", fmt.Errorf("Error determining IP address: %s", err)
}
return callerIP, nil
}
func getSSHClientConfig() *ssh.ClientConfig {
return &ssh.ClientConfig{
User: "root",
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
Auth: []ssh.AuthMethod{ssh.PublicKeys(sshSigner)},
}
}
func getSSHClientConfigForProxy(sshSigner ssh.Signer) *ssh.ClientConfig {
return &ssh.ClientConfig{
User: "chromeos-test",
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
Auth: []ssh.AuthMethod{ssh.PublicKeys(sshSigner)},
}
}
func (s *tlwServer) GetDut(ctx context.Context, req *tls.GetDutRequest) (*tls.Dut, error) {
name := req.GetName()
if name == "" {
return nil, status.Errorf(codes.InvalidArgument, "GetDut: empty name in request")
}
ctx = setupUFSContext(ctx)
licenses, err := getUFSDeviceLicenses(ctx, s.ufsClient, name)
if err != nil {
return nil, status.Errorf(codes.FailedPrecondition, "GetDut: %s", err.Error())
}
return &tls.Dut{
Name: name,
Licenses: licenses,
}, nil
}
func setupUFSContext(ctx context.Context) context.Context {
md := metadata.Pairs(ufsUtil.Namespace, ufsUtil.OSNamespace)
return metadata.NewOutgoingContext(ctx, md)
}