github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/communicator/winrm/communicator.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package winrm
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"log"
    10  	"math/rand"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/masterzen/winrm"
    16  	"github.com/packer-community/winrmcp/winrmcp"
    17  	"github.com/terramate-io/tf/communicator/remote"
    18  	"github.com/terramate-io/tf/provisioners"
    19  	"github.com/zclconf/go-cty/cty"
    20  )
    21  
    22  // Communicator represents the WinRM communicator
    23  type Communicator struct {
    24  	connInfo *connectionInfo
    25  	client   *winrm.Client
    26  	endpoint *winrm.Endpoint
    27  	rand     *rand.Rand
    28  }
    29  
    30  // New creates a new communicator implementation over WinRM.
    31  func New(v cty.Value) (*Communicator, error) {
    32  	connInfo, err := parseConnectionInfo(v)
    33  	if err != nil {
    34  		return nil, err
    35  	}
    36  
    37  	endpoint := &winrm.Endpoint{
    38  		Host:     connInfo.Host,
    39  		Port:     int(connInfo.Port),
    40  		HTTPS:    connInfo.HTTPS,
    41  		Insecure: connInfo.Insecure,
    42  		Timeout:  connInfo.TimeoutVal,
    43  	}
    44  	if len(connInfo.CACert) > 0 {
    45  		endpoint.CACert = []byte(connInfo.CACert)
    46  	}
    47  
    48  	comm := &Communicator{
    49  		connInfo: connInfo,
    50  		endpoint: endpoint,
    51  		// Seed our own rand source so that script paths are not deterministic
    52  		rand: rand.New(rand.NewSource(time.Now().UnixNano())),
    53  	}
    54  
    55  	return comm, nil
    56  }
    57  
    58  // Connect implementation of communicator.Communicator interface
    59  func (c *Communicator) Connect(o provisioners.UIOutput) error {
    60  	// Set the client to nil since we'll (re)create it
    61  	c.client = nil
    62  
    63  	params := winrm.DefaultParameters
    64  	params.Timeout = formatDuration(c.Timeout())
    65  	if c.connInfo.NTLM {
    66  		params.TransportDecorator = func() winrm.Transporter { return &winrm.ClientNTLM{} }
    67  	}
    68  
    69  	client, err := winrm.NewClientWithParameters(
    70  		c.endpoint, c.connInfo.User, c.connInfo.Password, params)
    71  	if err != nil {
    72  		return err
    73  	}
    74  
    75  	if o != nil {
    76  		o.Output(fmt.Sprintf(
    77  			"Connecting to remote host via WinRM...\n"+
    78  				"  Host: %s\n"+
    79  				"  Port: %d\n"+
    80  				"  User: %s\n"+
    81  				"  Password: %t\n"+
    82  				"  HTTPS: %t\n"+
    83  				"  Insecure: %t\n"+
    84  				"  NTLM: %t\n"+
    85  				"  CACert: %t",
    86  			c.connInfo.Host,
    87  			c.connInfo.Port,
    88  			c.connInfo.User,
    89  			c.connInfo.Password != "",
    90  			c.connInfo.HTTPS,
    91  			c.connInfo.Insecure,
    92  			c.connInfo.NTLM,
    93  			c.connInfo.CACert != "",
    94  		))
    95  	}
    96  
    97  	log.Printf("[DEBUG] connecting to remote shell using WinRM")
    98  	shell, err := client.CreateShell()
    99  	if err != nil {
   100  		log.Printf("[ERROR] error creating shell: %s", err)
   101  		return err
   102  	}
   103  
   104  	err = shell.Close()
   105  	if err != nil {
   106  		log.Printf("[ERROR] error closing shell: %s", err)
   107  		return err
   108  	}
   109  
   110  	if o != nil {
   111  		o.Output("Connected!")
   112  	}
   113  
   114  	c.client = client
   115  
   116  	return nil
   117  }
   118  
   119  // Disconnect implementation of communicator.Communicator interface
   120  func (c *Communicator) Disconnect() error {
   121  	c.client = nil
   122  	return nil
   123  }
   124  
   125  // Timeout implementation of communicator.Communicator interface
   126  func (c *Communicator) Timeout() time.Duration {
   127  	return c.connInfo.TimeoutVal
   128  }
   129  
   130  // ScriptPath implementation of communicator.Communicator interface
   131  func (c *Communicator) ScriptPath() string {
   132  	return strings.Replace(
   133  		c.connInfo.ScriptPath, "%RAND%",
   134  		strconv.FormatInt(int64(c.rand.Int31()), 10), -1)
   135  }
   136  
   137  // Start implementation of communicator.Communicator interface
   138  func (c *Communicator) Start(rc *remote.Cmd) error {
   139  	rc.Init()
   140  	log.Printf("[DEBUG] starting remote command: %s", rc.Command)
   141  
   142  	// TODO: make sure communicators always connect first, so we can get output
   143  	// from the connection.
   144  	if c.client == nil {
   145  		log.Println("[WARN] winrm client not connected, attempting to connect")
   146  		if err := c.Connect(nil); err != nil {
   147  			return err
   148  		}
   149  	}
   150  
   151  	status, err := c.client.Run(rc.Command, rc.Stdout, rc.Stderr)
   152  	rc.SetExitStatus(status, err)
   153  
   154  	return nil
   155  }
   156  
   157  // Upload implementation of communicator.Communicator interface
   158  func (c *Communicator) Upload(path string, input io.Reader) error {
   159  	wcp, err := c.newCopyClient()
   160  	if err != nil {
   161  		return err
   162  	}
   163  	log.Printf("[DEBUG] Uploading file to '%s'", path)
   164  	return wcp.Write(path, input)
   165  }
   166  
   167  // UploadScript implementation of communicator.Communicator interface
   168  func (c *Communicator) UploadScript(path string, input io.Reader) error {
   169  	return c.Upload(path, input)
   170  }
   171  
   172  // UploadDir implementation of communicator.Communicator interface
   173  func (c *Communicator) UploadDir(dst string, src string) error {
   174  	log.Printf("[DEBUG] Uploading dir '%s' to '%s'", src, dst)
   175  	wcp, err := c.newCopyClient()
   176  	if err != nil {
   177  		return err
   178  	}
   179  	return wcp.Copy(src, dst)
   180  }
   181  
   182  func (c *Communicator) newCopyClient() (*winrmcp.Winrmcp, error) {
   183  	addr := fmt.Sprintf("%s:%d", c.endpoint.Host, c.endpoint.Port)
   184  
   185  	config := winrmcp.Config{
   186  		Auth: winrmcp.Auth{
   187  			User:     c.connInfo.User,
   188  			Password: c.connInfo.Password,
   189  		},
   190  		Https:                 c.connInfo.HTTPS,
   191  		Insecure:              c.connInfo.Insecure,
   192  		OperationTimeout:      c.Timeout(),
   193  		MaxOperationsPerShell: 15, // lowest common denominator
   194  	}
   195  
   196  	if c.connInfo.NTLM {
   197  		config.TransportDecorator = func() winrm.Transporter { return &winrm.ClientNTLM{} }
   198  	}
   199  
   200  	if c.connInfo.CACert != "" {
   201  		config.CACertBytes = []byte(c.connInfo.CACert)
   202  	}
   203  
   204  	return winrmcp.New(addr, &config)
   205  }