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

     1  package service
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"strconv"
     7  	"time"
     8  
     9  	"github.com/evergreen-ci/evergreen"
    10  	"github.com/evergreen-ci/evergreen/cloud/providers"
    11  	"github.com/evergreen-ci/evergreen/command"
    12  	"github.com/evergreen-ci/evergreen/model"
    13  	"github.com/evergreen-ci/evergreen/model/distro"
    14  	"github.com/evergreen-ci/evergreen/model/host"
    15  	"github.com/evergreen-ci/evergreen/model/task"
    16  	"github.com/evergreen-ci/evergreen/model/user"
    17  	"github.com/evergreen-ci/evergreen/notify"
    18  	"github.com/evergreen-ci/evergreen/spawn"
    19  	"github.com/evergreen-ci/evergreen/util"
    20  	"github.com/mongodb/grip"
    21  	"github.com/pkg/errors"
    22  )
    23  
    24  const (
    25  	HostPasswordUpdate         = "updateRDPPassword"
    26  	HostExpirationExtension    = "extendHostExpiration"
    27  	HostTerminate              = "terminate"
    28  	MaxExpirationDurationHours = 24 * 7 // 7 days
    29  )
    30  
    31  func (uis *UIServer) spawnPage(w http.ResponseWriter, r *http.Request) {
    32  	flashes := PopFlashes(uis.CookieStore, r, w)
    33  	projCtx := MustHaveProjectContext(r)
    34  
    35  	var spawnDistro *distro.Distro
    36  	var spawnTask *task.Task
    37  	var err error
    38  	if len(r.FormValue("distro_id")) > 0 {
    39  		spawnDistro, err = distro.FindOne(distro.ById(r.FormValue("distro_id")))
    40  		if err != nil {
    41  			uis.LoggedError(w, r, http.StatusInternalServerError,
    42  				errors.Wrapf(err, "Error finding distro %v", r.FormValue("distro_id")))
    43  			return
    44  		}
    45  	}
    46  	if len(r.FormValue("task_id")) > 0 {
    47  		spawnTask, err = task.FindOne(task.ById(r.FormValue("task_id")))
    48  		if err != nil {
    49  			uis.LoggedError(w, r, http.StatusInternalServerError,
    50  				errors.Wrapf(err, "Error finding task %v", r.FormValue("task_id")))
    51  			return
    52  		}
    53  	}
    54  
    55  	uis.WriteHTML(w, http.StatusOK, struct {
    56  		ProjectData     projectContext
    57  		User            *user.DBUser
    58  		Flashes         []interface{}
    59  		Distro          *distro.Distro
    60  		Task            *task.Task
    61  		MaxHostsPerUser int
    62  	}{projCtx, GetUser(r), flashes, spawnDistro, spawnTask, spawn.MaxPerUser}, "base", "spawned_hosts.html", "base_angular.html", "menu.html")
    63  }
    64  
    65  func (uis *UIServer) getSpawnedHosts(w http.ResponseWriter, r *http.Request) {
    66  	user := MustHaveUser(r)
    67  
    68  	hosts, err := host.Find(host.ByUserWithRunningStatus(user.Username()))
    69  	if err != nil {
    70  		uis.LoggedError(w, r, http.StatusInternalServerError,
    71  			errors.Wrapf(err, "Error finding running hosts for user %v", user.Username()))
    72  		return
    73  	}
    74  
    75  	uis.WriteJSON(w, http.StatusOK, hosts)
    76  }
    77  
    78  func (uis *UIServer) getUserPublicKeys(w http.ResponseWriter, r *http.Request) {
    79  	user := MustHaveUser(r)
    80  	uis.WriteJSON(w, http.StatusOK, user.PublicKeys())
    81  }
    82  
    83  func (uis *UIServer) listSpawnableDistros(w http.ResponseWriter, r *http.Request) {
    84  	// load in the distros
    85  	distros, err := distro.Find(distro.All)
    86  	if err != nil {
    87  		uis.LoggedError(w, r, http.StatusInternalServerError, errors.Wrap(err, "Error loading distros"))
    88  		return
    89  	}
    90  
    91  	distroList := []map[string]interface{}{}
    92  
    93  	for _, d := range distros {
    94  		if d.SpawnAllowed {
    95  			distroList = append(distroList, map[string]interface{}{
    96  				"name":             d.Id,
    97  				"userDataFile":     d.UserData.File,
    98  				"userDataValidate": d.UserData.Validate})
    99  		}
   100  	}
   101  	uis.WriteJSON(w, http.StatusOK, distroList)
   102  }
   103  
   104  func (uis *UIServer) requestNewHost(w http.ResponseWriter, r *http.Request) {
   105  	authedUser := MustHaveUser(r)
   106  
   107  	putParams := struct {
   108  		Task      string `json:"task_id"`
   109  		Distro    string `json:"distro"`
   110  		KeyName   string `json:"key_name"`
   111  		PublicKey string `json:"public_key"`
   112  		SaveKey   bool   `json:"save_key"`
   113  		UserData  string `json:"userdata"`
   114  	}{}
   115  
   116  	if err := util.ReadJSONInto(util.NewRequestReader(r), &putParams); err != nil {
   117  		http.Error(w, fmt.Sprintf("Bad json in request: %v", err), http.StatusBadRequest)
   118  		return
   119  	}
   120  
   121  	opts := spawn.Options{
   122  		TaskId:    putParams.Task,
   123  		Distro:    putParams.Distro,
   124  		UserName:  authedUser.Username(),
   125  		PublicKey: putParams.PublicKey,
   126  		UserData:  putParams.UserData,
   127  	}
   128  
   129  	spawner := spawn.New(&uis.Settings)
   130  
   131  	if err := spawner.Validate(opts); err != nil {
   132  		errCode := http.StatusBadRequest
   133  		if _, ok := err.(spawn.BadOptionsErr); !ok {
   134  			errCode = http.StatusInternalServerError
   135  		}
   136  		uis.LoggedError(w, r, errCode, err)
   137  		return
   138  	}
   139  
   140  	// save the supplied public key if needed
   141  	if putParams.SaveKey {
   142  		dbuser, err := user.FindOne(user.ById(authedUser.Username()))
   143  		if err != nil {
   144  			uis.LoggedError(w, r, http.StatusInternalServerError, errors.Wrap(err, "Error fetching user"))
   145  			return
   146  		}
   147  		err = model.AddUserPublicKey(dbuser.Id, putParams.KeyName, putParams.PublicKey)
   148  		if err != nil {
   149  			uis.LoggedError(w, r, http.StatusInternalServerError, errors.Wrap(err, "Error saving public key"))
   150  			return
   151  		}
   152  		PushFlash(uis.CookieStore, r, w, NewSuccessFlash("Public key successfully saved."))
   153  	}
   154  
   155  	err := spawner.CreateHost(opts, authedUser)
   156  	if err != nil {
   157  		grip.Errorln("error spawning host:", err)
   158  		mailErr := notify.TrySendNotificationToUser(authedUser.Username(), fmt.Sprintf("Spawning failed"),
   159  			err.Error(), notify.ConstructMailer(uis.Settings.Notify))
   160  		if mailErr != nil {
   161  			grip.Errorln("Failed to send notification:", mailErr)
   162  		}
   163  	}
   164  
   165  	PushFlash(uis.CookieStore, r, w, NewSuccessFlash("Host spawned"))
   166  	uis.WriteJSON(w, http.StatusOK, "Host successfully spawned")
   167  	return
   168  
   169  }
   170  
   171  func (uis *UIServer) modifySpawnHost(w http.ResponseWriter, r *http.Request) {
   172  	_ = MustHaveUser(r)
   173  	updateParams := struct {
   174  		Action   string `json:"action"`
   175  		HostId   string `json:"host_id"`
   176  		RDPPwd   string `json:"rdp_pwd"`
   177  		AddHours string `json:"add_hours"`
   178  	}{}
   179  
   180  	if err := util.ReadJSONInto(util.NewRequestReader(r), &updateParams); err != nil {
   181  		http.Error(w, err.Error(), http.StatusBadRequest)
   182  	}
   183  
   184  	hostId := updateParams.HostId
   185  	host, err := host.FindOne(host.ById(hostId))
   186  	if err != nil {
   187  		uis.LoggedError(w, r, http.StatusInternalServerError, errors.Wrapf(err, "error finding host with id %v", hostId))
   188  		return
   189  	}
   190  	if host == nil {
   191  		uis.LoggedError(w, r, http.StatusInternalServerError, errors.Errorf("No host with id %v found", hostId))
   192  		return
   193  	}
   194  	// determine what action needs to be taken
   195  	switch updateParams.Action {
   196  	case HostTerminate:
   197  		if host.Status == evergreen.HostTerminated {
   198  			uis.WriteJSON(w, http.StatusBadRequest, fmt.Sprintf("Host %v is already terminated", host.Id))
   199  			return
   200  		}
   201  		cloudHost, err := providers.GetCloudHost(host, &uis.Settings)
   202  		if err != nil {
   203  			uis.LoggedError(w, r, http.StatusInternalServerError, err)
   204  			return
   205  		}
   206  		if err = cloudHost.TerminateInstance(); err != nil {
   207  			uis.LoggedError(w, r, http.StatusInternalServerError, err)
   208  			return
   209  		}
   210  		uis.WriteJSON(w, http.StatusOK, "host terminated")
   211  		return
   212  	case HostPasswordUpdate:
   213  		pwdUpdateCmd, err := constructPwdUpdateCommand(&uis.Settings, host, updateParams.RDPPwd)
   214  		if err != nil {
   215  			uis.LoggedError(w, r, http.StatusInternalServerError, errors.Wrap(err, "Error constructing host RDP password"))
   216  			return
   217  		}
   218  		// update RDP and sshd password
   219  		if err = pwdUpdateCmd.Run(); err != nil {
   220  			uis.LoggedError(w, r, http.StatusInternalServerError, errors.Wrap(err, "Error updating host RDP password"))
   221  			return
   222  		}
   223  		PushFlash(uis.CookieStore, r, w, NewSuccessFlash("Host RDP password successfully updated."))
   224  		uis.WriteJSON(w, http.StatusOK, "Successfully updated host password")
   225  	case HostExpirationExtension:
   226  		addtHours, err := strconv.Atoi(updateParams.AddHours)
   227  		if err != nil {
   228  			http.Error(w, "bad hours param", http.StatusBadRequest)
   229  			return
   230  		}
   231  		// ensure this request is valid
   232  		addtHourDuration := time.Duration(addtHours) * time.Hour
   233  		futureExpiration := host.ExpirationTime.Add(addtHourDuration)
   234  		expirationExtensionDuration := futureExpiration.Sub(time.Now()).Hours()
   235  		if expirationExtensionDuration > MaxExpirationDurationHours {
   236  			http.Error(w, fmt.Sprintf("Can not extend %v expiration by %v hours. "+
   237  				"Maximum extension is limited to %v hours", hostId,
   238  				int(expirationExtensionDuration), MaxExpirationDurationHours), http.StatusBadRequest)
   239  			return
   240  
   241  		}
   242  		if err = host.SetExpirationTime(futureExpiration); err != nil {
   243  			uis.LoggedError(w, r, http.StatusInternalServerError, errors.Wrap(err, "Error extending host expiration time"))
   244  			return
   245  		}
   246  		PushFlash(uis.CookieStore, r, w, NewSuccessFlash(fmt.Sprintf("Host expiration "+
   247  			"extension successful; %v will expire on %v", hostId,
   248  			futureExpiration.Format(time.RFC850))))
   249  		uis.WriteJSON(w, http.StatusOK, "Successfully extended host expiration time")
   250  		return
   251  	default:
   252  		http.Error(w, fmt.Sprintf("Unrecognized action: %v", updateParams.Action), http.StatusBadRequest)
   253  		return
   254  	}
   255  }
   256  
   257  // constructPwdUpdateCommand returns a RemoteCommand struct used to
   258  // set the RDP password on a remote windows machine.
   259  func constructPwdUpdateCommand(settings *evergreen.Settings, hostObj *host.Host,
   260  	password string) (*command.RemoteCommand, error) {
   261  
   262  	cloudHost, err := providers.GetCloudHost(hostObj, settings)
   263  	if err != nil {
   264  		return nil, err
   265  	}
   266  
   267  	hostInfo, err := util.ParseSSHInfo(hostObj.Host)
   268  	if err != nil {
   269  		return nil, err
   270  	}
   271  
   272  	sshOptions, err := cloudHost.GetSSHOptions()
   273  	if err != nil {
   274  		return nil, err
   275  	}
   276  
   277  	outputLineHandler := evergreen.NewInfoLoggingWriter(&evergreen.Logger)
   278  	errorLineHandler := evergreen.NewErrorLoggingWriter(&evergreen.Logger)
   279  
   280  	updatePwdCmd := fmt.Sprintf("net user %v %v && sc config "+
   281  		"sshd obj= '.\\%v' password= \"%v\"", hostObj.User, password,
   282  		hostObj.User, password)
   283  
   284  	// construct the required termination command
   285  	remoteCommand := &command.RemoteCommand{
   286  		CmdString:       updatePwdCmd,
   287  		Stdout:          outputLineHandler,
   288  		Stderr:          errorLineHandler,
   289  		LoggingDisabled: true,
   290  		RemoteHostName:  hostInfo.Hostname,
   291  		User:            hostObj.User,
   292  		Options:         append([]string{"-p", hostInfo.Port}, sshOptions...),
   293  		Background:      false,
   294  	}
   295  	return remoteCommand, nil
   296  }