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 }