github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/spawn/spawn.go (about)

     1  package spawn
     2  
     3  import (
     4  	"encoding/base64"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/url"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/evergreen-ci/evergreen"
    12  	"github.com/evergreen-ci/evergreen/cloud"
    13  	"github.com/evergreen-ci/evergreen/cloud/providers"
    14  	"github.com/evergreen-ci/evergreen/cloud/providers/ec2"
    15  	"github.com/evergreen-ci/evergreen/command"
    16  	"github.com/evergreen-ci/evergreen/model/distro"
    17  	"github.com/evergreen-ci/evergreen/model/host"
    18  	"github.com/evergreen-ci/evergreen/model/user"
    19  	"github.com/pkg/errors"
    20  	"gopkg.in/yaml.v2"
    21  )
    22  
    23  const (
    24  	MaxPerUser        = 3
    25  	DefaultExpiration = time.Duration(24 * time.Hour)
    26  )
    27  
    28  var SpawnLimitErr = errors.New("User is already running the max allowed # of spawn hosts")
    29  
    30  // BadOptionsErr represents an in valid set of spawn options.
    31  type BadOptionsErr struct {
    32  	message string
    33  }
    34  
    35  func (bsoe BadOptionsErr) Error() string {
    36  	return "Invalid spawn options:" + bsoe.message
    37  }
    38  
    39  // Spawn handles Spawning hosts for users.
    40  type Spawn struct {
    41  	settings *evergreen.Settings
    42  }
    43  
    44  // Options holds the required parameters for spawning a host.
    45  type Options struct {
    46  	Distro    string
    47  	UserName  string
    48  	PublicKey string
    49  	UserData  string
    50  	TaskId    string
    51  }
    52  
    53  // New returns an initialized Spawn controller.
    54  func New(settings *evergreen.Settings) Spawn {
    55  	return Spawn{settings}
    56  }
    57  
    58  // Validate returns an instance of BadOptionsErr if the SpawnOptions object contains invalid
    59  // data, SpawnLimitErr if the user is already at the spawned host limit, or some other untyped
    60  // instance of Error if something fails during validation.
    61  func (sm Spawn) Validate(so Options) error {
    62  	d, err := distro.FindOne(distro.ById(so.Distro))
    63  	if err != nil {
    64  		return BadOptionsErr{fmt.Sprintf("Invalid dist %v", so.Distro)}
    65  	}
    66  
    67  	if !d.SpawnAllowed {
    68  		return BadOptionsErr{fmt.Sprintf("Spawning not allowed for dist %v", so.Distro)}
    69  	}
    70  
    71  	// if the user already has too many active spawned hosts, deny the request
    72  	activeSpawnedHosts, err := host.Find(host.ByUserWithRunningStatus(so.UserName))
    73  	if err != nil {
    74  		return errors.Wrap(err, "Error occurred finding user's current hosts")
    75  	}
    76  
    77  	if len(activeSpawnedHosts) >= MaxPerUser {
    78  		return SpawnLimitErr
    79  	}
    80  
    81  	// validate public key
    82  	rsa := "ssh-rsa"
    83  	dss := "ssh-dss"
    84  	isRSA := strings.HasPrefix(so.PublicKey, rsa)
    85  	isDSS := strings.HasPrefix(so.PublicKey, dss)
    86  	if !isRSA && !isDSS {
    87  		return BadOptionsErr{"key does not start with ssh-rsa or ssh-dss"}
    88  	}
    89  
    90  	sections := strings.Split(so.PublicKey, " ")
    91  	if len(sections) < 2 {
    92  		keyType := rsa
    93  		if sections[0] == dss {
    94  			keyType = dss
    95  		}
    96  		return BadOptionsErr{fmt.Sprintf("missing space after '%v'", keyType)}
    97  	}
    98  
    99  	// check for valid base64
   100  	if _, err = base64.StdEncoding.DecodeString(sections[1]); err != nil {
   101  		return BadOptionsErr{"key contains invalid base64 string"}
   102  	}
   103  
   104  	if d.UserData.File != "" {
   105  		if strings.TrimSpace(so.UserData) == "" {
   106  			return BadOptionsErr{}
   107  		}
   108  
   109  		var err error
   110  		switch d.UserData.Validate {
   111  		case distro.UserDataFormatFormURLEncoded:
   112  			_, err = url.ParseQuery(so.UserData)
   113  		case distro.UserDataFormatJSON:
   114  			var out map[string]interface{}
   115  			err = json.Unmarshal([]byte(so.UserData), &out)
   116  		case distro.UserDataFormatYAML:
   117  			var out map[string]interface{}
   118  			err = yaml.Unmarshal([]byte(so.UserData), &out)
   119  		}
   120  
   121  		if err != nil {
   122  			return BadOptionsErr{fmt.Sprintf("invalid %v: %v", d.UserData.Validate, err)}
   123  		}
   124  	}
   125  	return nil
   126  }
   127  
   128  // CreateHost spawns a host with the given options.
   129  func (sm Spawn) CreateHost(so Options, owner *user.DBUser) error {
   130  
   131  	// load in the appropriate distro
   132  	d, err := distro.FindOne(distro.ById(so.Distro))
   133  	if err != nil {
   134  		return errors.WithStack(err)
   135  	}
   136  	// add any extra user-specified data into the setup script
   137  	if d.UserData.File != "" {
   138  		userDataCmd := fmt.Sprintf("echo \"%v\" > %v\n",
   139  			strings.Replace(so.UserData, "\"", "\\\"", -1), d.UserData.File)
   140  		// prepend the setup script to add the userdata file
   141  		if strings.HasPrefix(d.Setup, "#!") {
   142  			firstLF := strings.Index(d.Setup, "\n")
   143  			d.Setup = d.Setup[0:firstLF+1] + userDataCmd + d.Setup[firstLF+1:]
   144  		} else {
   145  			d.Setup = userDataCmd + d.Setup
   146  		}
   147  	}
   148  
   149  	// modify the setup script to add the user's public key
   150  	d.Setup += fmt.Sprintf("\necho \"\n%v\" >> ~%v/.ssh/authorized_keys\n", so.PublicKey, d.User)
   151  
   152  	// replace expansions in the script
   153  	exp := command.NewExpansions(sm.settings.Expansions)
   154  	d.Setup, err = exp.ExpandString(d.Setup)
   155  	if err != nil {
   156  		return errors.Wrap(err, "expansions error")
   157  	}
   158  
   159  	// fake out replacing spot instances with on-demand equivalents
   160  	if d.Provider == ec2.SpotProviderName {
   161  		d.Provider = ec2.OnDemandProviderName
   162  	}
   163  
   164  	// get the appropriate cloud manager
   165  	cloudManager, err := providers.GetCloudManager(d.Provider, sm.settings)
   166  	if err != nil {
   167  		return errors.WithStack(err)
   168  	}
   169  
   170  	// spawn the host
   171  	provisionOptions := &host.ProvisionOptions{
   172  		LoadCLI: true,
   173  		TaskId:  so.TaskId,
   174  		OwnerId: owner.Id,
   175  	}
   176  	expiration := DefaultExpiration
   177  	hostOptions := cloud.HostOptions{
   178  		ProvisionOptions:   provisionOptions,
   179  		UserName:           so.UserName,
   180  		ExpirationDuration: &expiration,
   181  		UserData:           so.UserData,
   182  		UserHost:           true,
   183  	}
   184  
   185  	_, err = cloudManager.SpawnInstance(d, hostOptions)
   186  	return errors.WithStack(err)
   187  }