Reviewed-by: stardust <denglf@pcl.ac.cn>tags/v0.1.8
| @@ -44,6 +44,11 @@ type Attachment struct { | |||
| CreatedUnix timeutil.TimeStamp `xorm:"created"` | |||
| } | |||
| type AttachmentUsername struct { | |||
| Attachment `xorm:"extends"` | |||
| Name string | |||
| } | |||
| func (a *Attachment) AfterUpdate() { | |||
| if a.DatasetID > 0 { | |||
| datasetIsPublicCount, err := x.Where("dataset_id = ? AND is_private = ?", a.DatasetID, false).Count(new(Attachment)) | |||
| @@ -348,16 +353,20 @@ func getUnDecompressAttachments(e Engine) ([]*Attachment, error) { | |||
| return attachments, e.Where("decompress_state = ? and dataset_id != 0 and name like '%.zip'", DecompressStateInit).Find(&attachments) | |||
| } | |||
| func GetAllPublicAttachments() ([]*Attachment, error) { | |||
| func GetAllPublicAttachments() ([]*AttachmentUsername, error) { | |||
| return getAllPublicAttachments(x) | |||
| } | |||
| func getAllPublicAttachments(e Engine) ([]*Attachment, error) { | |||
| attachments := make([]*Attachment, 0, 10) | |||
| return attachments, e.Where("is_private = false and decompress_state = ?", DecompressStateDone).Find(&attachments) | |||
| func getAllPublicAttachments(e Engine) ([]*AttachmentUsername, error) { | |||
| attachments := make([]*AttachmentUsername, 0, 10) | |||
| if err := e.Table("attachment").Join("LEFT", "`user`", "attachment.uploader_id " + | |||
| "= `user`.id").Where("decompress_state= ? and is_private= ?", DecompressStateDone, false).Find(&attachments); err != nil { | |||
| return nil, err | |||
| } | |||
| return attachments, nil | |||
| } | |||
| func GetPrivateAttachments(username string) ([]*Attachment, error) { | |||
| func GetPrivateAttachments(username string) ([]*AttachmentUsername, error) { | |||
| user, err := getUserByName(x, username) | |||
| if err != nil { | |||
| log.Error("getUserByName(%s) failed:%v", username, err) | |||
| @@ -366,8 +375,28 @@ func GetPrivateAttachments(username string) ([]*Attachment, error) { | |||
| return getPrivateAttachments(x, user.ID) | |||
| } | |||
| func getPrivateAttachments(e Engine, userID int64) ([]*Attachment, error) { | |||
| attachments := make([]*Attachment, 0, 10) | |||
| return attachments, e.Where("uploader_id = ? and decompress_state = ?", userID, DecompressStateDone).Find(&attachments) | |||
| func getPrivateAttachments(e Engine, userID int64) ([]*AttachmentUsername, error) { | |||
| attachments := make([]*AttachmentUsername, 0, 10) | |||
| if err := e.Table("attachment").Join("LEFT", "`user`", "attachment.uploader_id " + | |||
| "= `user`.id").Where("decompress_state= ? and uploader_id= ?", DecompressStateDone, userID).Find(&attachments); err != nil { | |||
| return nil, err | |||
| } | |||
| return attachments, nil | |||
| } | |||
| func GetAllUserAttachments(userID int64) ([]*AttachmentUsername, error) { | |||
| attachsPub, err := getAllPublicAttachments(x) | |||
| if err != nil { | |||
| log.Error("getAllPublicAttachments failed:%v", err) | |||
| return nil, err | |||
| } | |||
| attachsPri, err := getPrivateAttachments(x, userID) | |||
| if err != nil { | |||
| log.Error("getPrivateAttachments failed:%v", err) | |||
| return nil, err | |||
| } | |||
| return append(attachsPub, attachsPri...), nil | |||
| } | |||
| @@ -5,6 +5,7 @@ import ( | |||
| "errors" | |||
| "fmt" | |||
| "time" | |||
| "xorm.io/xorm" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| "code.gitea.io/gitea/modules/timeutil" | |||
| @@ -18,17 +19,22 @@ const ( | |||
| JobStopped CloudbrainStatus = "STOPPED" | |||
| JobSucceeded CloudbrainStatus = "SUCCEEDED" | |||
| JobFailed CloudbrainStatus = "FAILED" | |||
| JobRunning CloudbrainStatus = "RUNNING" | |||
| ) | |||
| type Cloudbrain struct { | |||
| ID int64 `xorm:"pk autoincr"` | |||
| JobID string `xorm:"INDEX NOT NULL"` | |||
| JobName string | |||
| JobName string `xorm:"INDEX"` | |||
| Status string `xorm:"INDEX"` | |||
| UserID int64 `xorm:"INDEX"` | |||
| RepoID int64 `xorm:"INDEX"` | |||
| SubTaskName string `xorm:"INDEX"` | |||
| ContainerID string | |||
| ContainerIp string | |||
| CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | |||
| UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | |||
| CanDebug bool `xorm:"-"` | |||
| User *User `xorm:"-"` | |||
| Repo *Repository `xorm:"-"` | |||
| @@ -52,6 +58,17 @@ type TaskRole struct { | |||
| Command string `json:"command"` | |||
| NeedIBDevice bool `json:"needIBDevice"` | |||
| IsMainRole bool `json:"isMainRole"` | |||
| UseNNI bool `json:"useNNI"` | |||
| } | |||
| type StHostPath struct { | |||
| Path string `json:"path"` | |||
| MountPath string `json:"mountPath"` | |||
| ReadOnly bool `json:"readOnly"` | |||
| } | |||
| type Volume struct { | |||
| HostPath StHostPath `json:"hostPath"` | |||
| } | |||
| type CreateJobParams struct { | |||
| @@ -60,12 +77,13 @@ type CreateJobParams struct { | |||
| GpuType string `json:"gpuType"` | |||
| Image string `json:"image"` | |||
| TaskRoles []TaskRole `json:"taskRoles"` | |||
| Volumes []Volume `json:"volumes"` | |||
| } | |||
| type CreateJobResult struct { | |||
| Code string | |||
| Msg string | |||
| Payload map[string]interface{} | |||
| Code string `json:"code"` | |||
| Msg string `json:"msg"` | |||
| Payload map[string]interface{} `json:"payload"` | |||
| } | |||
| type GetJobResult struct { | |||
| @@ -74,6 +92,12 @@ type GetJobResult struct { | |||
| Payload map[string]interface{} `json:"payload"` | |||
| } | |||
| type GetImagesResult struct { | |||
| Code string `json:"code"` | |||
| Msg string `json:"msg"` | |||
| Payload map[string]*ImageInfo `json:"payload"` | |||
| } | |||
| type CloudbrainsOptions struct { | |||
| ListOptions | |||
| RepoID int64 // include all repos if empty | |||
| @@ -101,6 +125,8 @@ type TaskPod struct { | |||
| ExitCode int `json:"exitCode"` | |||
| ExitDiagnostics string `json:"exitDiagnostics"` | |||
| RetriedCount int `json:"retriedCount"` | |||
| StartTime string | |||
| FinishedTime string | |||
| } `json:"taskStatuses"` | |||
| } | |||
| @@ -108,6 +134,8 @@ func ConvertToTaskPod(input map[string]interface{}) (TaskPod, error) { | |||
| data, _ := json.Marshal(input) | |||
| var taskPod TaskPod | |||
| err := json.Unmarshal(data, &taskPod) | |||
| taskPod.TaskStatuses[0].StartTime = time.Unix(taskPod.TaskStatuses[0].StartAt.Unix() + 8*3600, 0).UTC().Format("2006-01-02 15:04:05") | |||
| taskPod.TaskStatuses[0].FinishedTime = time.Unix(taskPod.TaskStatuses[0].FinishedAt.Unix() + 8*3600, 0).UTC().Format("2006-01-02 15:04:05") | |||
| return taskPod, err | |||
| } | |||
| @@ -132,6 +160,8 @@ type JobResultPayload struct { | |||
| AppExitDiagnostics string `json:"appExitDiagnostics"` | |||
| AppExitType interface{} `json:"appExitType"` | |||
| VirtualCluster string `json:"virtualCluster"` | |||
| StartTime string | |||
| EndTime string | |||
| } `json:"jobStatus"` | |||
| TaskRoles map[string]interface{} `json:"taskRoles"` | |||
| Resource struct { | |||
| @@ -170,9 +200,51 @@ func ConvertToJobResultPayload(input map[string]interface{}) (JobResultPayload, | |||
| data, _ := json.Marshal(input) | |||
| var jobResultPayload JobResultPayload | |||
| err := json.Unmarshal(data, &jobResultPayload) | |||
| jobResultPayload.JobStatus.StartTime = time.Unix(jobResultPayload.JobStatus.CreatedTime/1000, 0).Format("2006-01-02 15:04:05") | |||
| jobResultPayload.JobStatus.EndTime = time.Unix(jobResultPayload.JobStatus.CompletedTime/1000, 0).Format("2006-01-02 15:04:05") | |||
| return jobResultPayload, err | |||
| } | |||
| type ImagesResultPayload struct { | |||
| Images []struct { | |||
| ID int `json:"id"` | |||
| Name string `json:"name"` | |||
| Place string `json:"place"` | |||
| Description string `json:"description"` | |||
| Provider string `json:"provider"` | |||
| Createtime string `json:"createtime"` | |||
| Remark string `json:"remark"` | |||
| } `json:"taskStatuses"` | |||
| } | |||
| type ImageInfo struct { | |||
| ID int `json:"id"` | |||
| Name string `json:"name"` | |||
| Place string `json:"place"` | |||
| Description string `json:"description"` | |||
| Provider string `json:"provider"` | |||
| Createtime string `json:"createtime"` | |||
| Remark string `json:"remark"` | |||
| PlaceView string | |||
| } | |||
| type CommitImageParams struct { | |||
| Ip string `json:"ip"` | |||
| TaskContainerId string `json:"taskContainerId"` | |||
| ImageTag string `json:"imageTag"` | |||
| ImageDescription string `json:"imageDescription"` | |||
| } | |||
| type CommitImageResult struct { | |||
| Code string `json:"code"` | |||
| Msg string `json:"msg"` | |||
| Payload map[string]interface{} `json:"payload"` | |||
| } | |||
| type StopJobResult struct { | |||
| Code string `json:"code"` | |||
| Msg string `json:"msg"` | |||
| } | |||
| func Cloudbrains(opts *CloudbrainsOptions) ([]*Cloudbrain, int64, error) { | |||
| sess := x.NewSession() | |||
| defer sess.Close() | |||
| @@ -269,3 +341,14 @@ func SetCloudbrainStatusByJobID(jobID string, status CloudbrainStatus) (err erro | |||
| _, err = x.Cols("status").Where("cloudbrain.job_id=?", jobID).Update(cb) | |||
| return | |||
| } | |||
| func UpdateJob(job *Cloudbrain) error { | |||
| return updateJob(x, job) | |||
| } | |||
| func updateJob(e Engine, job *Cloudbrain) error { | |||
| var sess *xorm.Session | |||
| sess = e.Where("job_id = ?", job.JobID) | |||
| _, err := sess.Cols("status", "container_id", "container_ip").Update(job) | |||
| return err | |||
| } | |||
| @@ -8,8 +8,14 @@ import ( | |||
| // CreateDatasetForm form for dataset page | |||
| type CreateCloudBrainForm struct { | |||
| JobName string `form:"job_name" binding:"Required"` | |||
| Image string `binding:"Required"` | |||
| Command string `binding:"Required"` | |||
| Image string `form:"image" binding:"Required"` | |||
| Command string `form:"command" binding:"Required"` | |||
| Attachment string `form:"attachment" binding:"Required"` | |||
| } | |||
| type CommitImageCloudBrainForm struct { | |||
| Description string `form:"description" binding:"Required"` | |||
| Tag string `form:"tag" binding:"Required"` | |||
| } | |||
| func (f *CreateCloudBrainForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | |||
| @@ -1,22 +1,39 @@ | |||
| package cloudbrain | |||
| import ( | |||
| "code.gitea.io/gitea/modules/setting" | |||
| "errors" | |||
| "code.gitea.io/gitea/models" | |||
| "code.gitea.io/gitea/modules/context" | |||
| "code.gitea.io/gitea/modules/log" | |||
| ) | |||
| "code.gitea.io/gitea/models" | |||
| const ( | |||
| Command = `pip3 install jupyterlab==2.2.5 -i https://pypi.tuna.tsinghua.edu.cn/simple;service ssh stop;jupyter lab --no-browser --ip=0.0.0.0 --allow-root --notebook-dir="/code" --port=80 --LabApp.token="" --LabApp.allow_origin="self https://cloudbrain.pcl.ac.cn"` | |||
| CodeMountPath = "/code" | |||
| DataSetMountPath = "/dataset" | |||
| ModelMountPath = "/model" | |||
| SubTaskName = "task1" | |||
| Success = "S000" | |||
| ) | |||
| func GenerateTask(ctx *context.Context, jobName, image, command string) error { | |||
| func GenerateTask(ctx *context.Context, jobName, image, command, uuid, codePath, modelPath string) error { | |||
| dataActualPath := setting.Attachment.Minio.RealPath + | |||
| setting.Attachment.Minio.Bucket + "/" + | |||
| setting.Attachment.Minio.BasePath + | |||
| models.AttachmentRelativePath(uuid) + | |||
| uuid | |||
| jobResult, err := CreateJob(jobName, models.CreateJobParams{ | |||
| JobName: jobName, | |||
| RetryCount: 1, | |||
| GpuType: "dgx", | |||
| GpuType: setting.JobType, | |||
| Image: image, | |||
| TaskRoles: []models.TaskRole{ | |||
| { | |||
| Name: "task1", | |||
| Name: SubTaskName, | |||
| TaskNumber: 1, | |||
| MinSucceededTaskCount: 1, | |||
| MinFailedTaskCount: 1, | |||
| @@ -27,13 +44,39 @@ func GenerateTask(ctx *context.Context, jobName, image, command string) error { | |||
| Command: command, | |||
| NeedIBDevice: false, | |||
| IsMainRole: false, | |||
| UseNNI: false, | |||
| }, | |||
| }, | |||
| Volumes: []models.Volume{ | |||
| { | |||
| HostPath: models.StHostPath{ | |||
| Path: codePath, | |||
| MountPath: CodeMountPath, | |||
| ReadOnly: false, | |||
| }, | |||
| }, | |||
| { | |||
| HostPath: models.StHostPath{ | |||
| Path: dataActualPath, | |||
| MountPath: DataSetMountPath, | |||
| ReadOnly: true, | |||
| }, | |||
| }, | |||
| { | |||
| HostPath: models.StHostPath{ | |||
| Path: modelPath, | |||
| MountPath: ModelMountPath, | |||
| ReadOnly: false, | |||
| }, | |||
| }, | |||
| }, | |||
| }) | |||
| if err != nil { | |||
| log.Error("CreateJob failed:", err.Error()) | |||
| return err | |||
| } | |||
| if jobResult.Code != "S000" { | |||
| if jobResult.Code != Success { | |||
| log.Error("CreateJob(%s) failed:%s", jobName, jobResult.Msg) | |||
| return errors.New(jobResult.Msg) | |||
| } | |||
| @@ -44,6 +87,7 @@ func GenerateTask(ctx *context.Context, jobName, image, command string) error { | |||
| RepoID: ctx.Repo.Repository.ID, | |||
| JobID: jobID, | |||
| JobName: jobName, | |||
| SubTaskName: SubTaskName, | |||
| }) | |||
| if err != nil { | |||
| @@ -5,7 +5,7 @@ import ( | |||
| "code.gitea.io/gitea/models" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| resty "github.com/go-resty/resty/v2" | |||
| "github.com/go-resty/resty/v2" | |||
| ) | |||
| var ( | |||
| @@ -47,7 +47,7 @@ func loginCloudbrain() error { | |||
| return fmt.Errorf("resty loginCloudbrain: %s", err) | |||
| } | |||
| if loginResult.Code != "S000" { | |||
| if loginResult.Code != Success { | |||
| return fmt.Errorf("%s: %s", loginResult.Msg, res.String()) | |||
| } | |||
| @@ -68,7 +68,7 @@ sendjob: | |||
| SetAuthToken(TOKEN). | |||
| SetBody(createJobParams). | |||
| SetResult(&jobResult). | |||
| Put(HOST + "/rest-server/api/v1/jobs/" + jobName) | |||
| Post(HOST + "/rest-server/api/v1/jobs/") | |||
| if err != nil { | |||
| return nil, fmt.Errorf("resty create job: %s", err) | |||
| @@ -80,7 +80,7 @@ sendjob: | |||
| goto sendjob | |||
| } | |||
| if jobResult.Code != "S000" { | |||
| if jobResult.Code != Success { | |||
| return &jobResult, fmt.Errorf("jobResult err: %s", res.String()) | |||
| } | |||
| @@ -103,7 +103,7 @@ sendjob: | |||
| Get(HOST + "/rest-server/api/v1/jobs/" + jobID) | |||
| if err != nil { | |||
| return nil, fmt.Errorf("resty GetJob: %s", err) | |||
| return nil, fmt.Errorf("resty GetJob: %v", err) | |||
| } | |||
| if getJobResult.Code == "S401" && retry < 1 { | |||
| @@ -112,9 +112,103 @@ sendjob: | |||
| goto sendjob | |||
| } | |||
| if getJobResult.Code != "S000" { | |||
| if getJobResult.Code != Success { | |||
| return &getJobResult, fmt.Errorf("jobResult GetJob err: %s", res.String()) | |||
| } | |||
| return &getJobResult, nil | |||
| } | |||
| func GetImages() (*models.GetImagesResult, error) { | |||
| checkSetting() | |||
| client := getRestyClient() | |||
| var getImagesResult models.GetImagesResult | |||
| retry := 0 | |||
| sendjob: | |||
| res, err := client.R(). | |||
| SetHeader("Content-Type", "application/json"). | |||
| SetAuthToken(TOKEN). | |||
| SetResult(&getImagesResult). | |||
| Get(HOST + "/rest-server/api/v1/image/list/") | |||
| if err != nil { | |||
| return nil, fmt.Errorf("resty GetImages: %v", err) | |||
| } | |||
| if getImagesResult.Code == "S401" && retry < 1 { | |||
| retry++ | |||
| _ = loginCloudbrain() | |||
| goto sendjob | |||
| } | |||
| if getImagesResult.Code != Success { | |||
| return &getImagesResult, fmt.Errorf("getImgesResult err: %s", res.String()) | |||
| } | |||
| return &getImagesResult, nil | |||
| } | |||
| func CommitImage(jobID string, params models.CommitImageParams) error { | |||
| checkSetting() | |||
| client := getRestyClient() | |||
| var result models.CommitImageResult | |||
| retry := 0 | |||
| sendjob: | |||
| res, err := client.R(). | |||
| SetHeader("Content-Type", "application/json"). | |||
| SetAuthToken(TOKEN). | |||
| SetBody(params). | |||
| SetResult(&result). | |||
| Post(HOST + "/rest-server/api/v1/jobs/" + jobID + "/commitImage") | |||
| if err != nil { | |||
| return fmt.Errorf("resty CommitImage: %v", err) | |||
| } | |||
| if result.Code == "S401" && retry < 1 { | |||
| retry++ | |||
| _ = loginCloudbrain() | |||
| goto sendjob | |||
| } | |||
| if result.Code != Success { | |||
| return fmt.Errorf("CommitImage err: %s", res.String()) | |||
| } | |||
| return nil | |||
| } | |||
| func StopJob(jobID string) error { | |||
| checkSetting() | |||
| client := getRestyClient() | |||
| var result models.StopJobResult | |||
| retry := 0 | |||
| sendjob: | |||
| res, err := client.R(). | |||
| SetHeader("Content-Type", "application/json"). | |||
| SetAuthToken(TOKEN). | |||
| SetResult(&result). | |||
| Delete(HOST + "/rest-server/api/v1/jobs/" + jobID) | |||
| if err != nil { | |||
| return fmt.Errorf("resty StopJob: %v", err) | |||
| } | |||
| if result.Code == "S401" && retry < 1 { | |||
| retry++ | |||
| _ = loginCloudbrain() | |||
| goto sendjob | |||
| } | |||
| if result.Code != Success { | |||
| return fmt.Errorf("StopJob err: %s", res.String()) | |||
| } | |||
| return nil | |||
| } | |||
| @@ -14,6 +14,6 @@ func GetCloudbrainConfig() CloudbrainLoginConfig { | |||
| cloudbrainSec := Cfg.Section("cloudbrain") | |||
| Cloudbrain.Username = cloudbrainSec.Key("USERNAME").MustString("") | |||
| Cloudbrain.Password = cloudbrainSec.Key("PASSWORD").MustString("") | |||
| Cloudbrain.Host = cloudbrainSec.Key("HOST").MustString("") | |||
| Cloudbrain.Host = cloudbrainSec.Key("REST_SERVER_HOST").MustString("") | |||
| return Cloudbrain | |||
| } | |||
| @@ -434,10 +434,10 @@ var ( | |||
| //cloudbrain config | |||
| CBAuthUser string | |||
| CBAuthPassword string | |||
| ClientID string | |||
| ClientSecret string | |||
| UserCeterHost string | |||
| RestServerHost string | |||
| JobPath string | |||
| JobType string | |||
| DebugServerHost string | |||
| ) | |||
| // DateLang transforms standard language locale name to corresponding value in datetime plugin. | |||
| @@ -1109,10 +1109,10 @@ func NewContext() { | |||
| sec = Cfg.Section("cloudbrain") | |||
| CBAuthUser = sec.Key("USER").MustString("cW4cMtH24eoWPE7X") | |||
| CBAuthPassword = sec.Key("PWD").MustString("4BPmgvK2hb2Eywwyp4YZRY4B7yQf4DAC") | |||
| ClientID = sec.Key("CLIENT_ID").MustString("3Z377wcplxeE2qpycpjv") | |||
| ClientSecret = sec.Key("CLIENT_SECRET").MustString("J5ykfVl2kcxW0H9cawSL") | |||
| UserCeterHost = sec.Key("USER_CENTER_HOST").MustString("http://192.168.202.73:31441") | |||
| RestServerHost = sec.Key("REST_SERVER_HOST").MustString("http://192.168.202.73") | |||
| JobPath = sec.Key("JOB_PATH").MustString("/datasets/minio/data/opendata/jobs/") | |||
| DebugServerHost = sec.Key("DEBUG_SERVER_HOST").MustString("http://192.168.202.73") | |||
| JobType = sec.Key("JOB_TYPE").MustString("debug_openi") | |||
| } | |||
| func loadInternalToken(sec *ini.Section) string { | |||
| @@ -752,6 +752,7 @@ cloudbrain=云脑 | |||
| cloudbrain.new=新建任务 | |||
| cloudbrain.desc=云脑功能 | |||
| cloudbrain.cancel=取消 | |||
| cloudbrain.commit_image=提交 | |||
| template.items=模板选项 | |||
| template.git_content=Git数据(默认分支) | |||
| @@ -6,6 +6,7 @@ | |||
| package repo | |||
| import ( | |||
| "code.gitea.io/gitea/modules/log" | |||
| "net/http" | |||
| "time" | |||
| @@ -48,7 +49,7 @@ func GetCloudbrainTask(ctx *context.APIContext) { | |||
| jobID := ctx.Params(":jobid") | |||
| repoID := ctx.Repo.Repository.ID | |||
| _, err = models.GetRepoCloudBrainByJobID(repoID, jobID) | |||
| job, err := models.GetRepoCloudBrainByJobID(repoID, jobID) | |||
| if err != nil { | |||
| ctx.NotFound(err) | |||
| return | |||
| @@ -64,8 +65,18 @@ func GetCloudbrainTask(ctx *context.APIContext) { | |||
| return | |||
| } | |||
| taskRoles := result.TaskRoles | |||
| taskRes, _ := models.ConvertToTaskPod(taskRoles[cloudbrain.SubTaskName].(map[string]interface{})) | |||
| job.ContainerIp = taskRes.TaskStatuses[0].ContainerIP | |||
| job.ContainerID = taskRes.TaskStatuses[0].ContainerID | |||
| job.Status = taskRes.TaskStatuses[0].State | |||
| if result.JobStatus.State != string(models.JobWaiting) { | |||
| go models.SetCloudbrainStatusByJobID(result.Config.JobID, models.CloudbrainStatus(result.JobStatus.State)) | |||
| err = models.UpdateJob(job) | |||
| if err != nil { | |||
| log.Error("UpdateJob failed:", err) | |||
| } | |||
| } | |||
| ctx.JSON(http.StatusOK, map[string]interface{}{ | |||
| @@ -635,7 +635,7 @@ func QueryAllPublicDataset(ctx *context.Context){ | |||
| return | |||
| } | |||
| queryDatasets(ctx, "admin", attachs) | |||
| queryDatasets(ctx, attachs) | |||
| } | |||
| func QueryPrivateDataset(ctx *context.Context){ | |||
| @@ -650,26 +650,40 @@ func QueryPrivateDataset(ctx *context.Context){ | |||
| return | |||
| } | |||
| queryDatasets(ctx, username, attachs) | |||
| for _, attach := range attachs { | |||
| attach.Name = username | |||
| } | |||
| queryDatasets(ctx, attachs) | |||
| } | |||
| func queryDatasets(ctx *context.Context, username string, attachs []*models.Attachment) { | |||
| func queryDatasets(ctx *context.Context, attachs []*models.AttachmentUsername) { | |||
| var datasets []CloudBrainDataset | |||
| if len(attachs) == 0 { | |||
| log.Info("dataset is null") | |||
| ctx.JSON(200, map[string]string{ | |||
| "result_code": "0", | |||
| "error_msg": "", | |||
| "data": "", | |||
| }) | |||
| return | |||
| } | |||
| for _, attch := range attachs { | |||
| has,err := storage.Attachments.HasObject(models.AttachmentRelativePath(attch.UUID)) | |||
| if err != nil || !has { | |||
| continue | |||
| } | |||
| datasets = append(datasets, CloudBrainDataset{attch.UUID, | |||
| attch.Name, | |||
| datasets = append(datasets, CloudBrainDataset{strconv.FormatInt(attch.ID, 10), | |||
| attch.Attachment.Name, | |||
| setting.Attachment.Minio.RealPath + | |||
| setting.Attachment.Minio.Bucket + "/" + | |||
| setting.Attachment.Minio.BasePath + | |||
| models.AttachmentRelativePath(attch.UUID) + | |||
| attch.UUID, | |||
| username, | |||
| attch.CreatedUnix.Format("2006-01-02 03:04:05")}) | |||
| attch.Name, | |||
| attch.CreatedUnix.Format("2006-01-02 03:04:05 PM")}) | |||
| } | |||
| data,err := json.Marshal(datasets) | |||
| @@ -1,7 +1,11 @@ | |||
| package repo | |||
| import ( | |||
| "errors" | |||
| "os" | |||
| "os/exec" | |||
| "strconv" | |||
| "strings" | |||
| "time" | |||
| "code.gitea.io/gitea/models" | |||
| @@ -9,6 +13,7 @@ import ( | |||
| "code.gitea.io/gitea/modules/base" | |||
| "code.gitea.io/gitea/modules/cloudbrain" | |||
| "code.gitea.io/gitea/modules/context" | |||
| "code.gitea.io/gitea/modules/log" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| ) | |||
| @@ -46,6 +51,15 @@ func CloudBrainIndex(ctx *context.Context) { | |||
| return | |||
| } | |||
| timestamp := time.Now().Unix() | |||
| for i, task := range ciTasks { | |||
| if task.Status == string(models.JobRunning) && (timestamp - int64(task.CreatedUnix) > 30){ | |||
| ciTasks[i].CanDebug = true | |||
| } else { | |||
| ciTasks[i].CanDebug = false | |||
| } | |||
| } | |||
| pager := context.NewPagination(int(count), setting.UI.IssuePagingNum, page, 5) | |||
| pager.SetDefaultParams(ctx) | |||
| ctx.Data["Page"] = pager | |||
| @@ -66,10 +80,36 @@ func CloudBrainNew(ctx *context.Context) { | |||
| ctx.Data["PageIsCloudBrain"] = true | |||
| t := time.Now() | |||
| var jobName = cutString(ctx.User.Name, 5) + t.Format("2006010215") + strconv.Itoa(int(t.Unix()))[:5] | |||
| var jobName = cutString(ctx.User.Name, 5) + t.Format("2006010215") + strconv.Itoa(int(t.Unix()))[5:] | |||
| ctx.Data["job_name"] = jobName | |||
| ctx.Data["image"] = "192.168.202.74:5000/user-images/deepo:v2.0" | |||
| ctx.Data["command"] = `pip3 install jupyterlab==1.1.4;service ssh stop;jupyter lab --no-browser --ip=0.0.0.0 --allow-root --notebook-dir=\"/userhome\" --port=80 --NotebookApp.token=\"\" --LabApp.allow_origin=\"self https://cloudbrain.pcl.ac.cn\"` | |||
| result, err := cloudbrain.GetImages() | |||
| if err != nil { | |||
| ctx.Data["error"] = err.Error() | |||
| } | |||
| for i,payload := range result.Payload { | |||
| if strings.HasPrefix(result.Payload[i].Place,"192.168") { | |||
| result.Payload[i].PlaceView = payload.Place[strings.Index(payload.Place, "/"): len(payload.Place)-1] | |||
| } else { | |||
| result.Payload[i].PlaceView = payload.Place | |||
| } | |||
| } | |||
| ctx.Data["images"] = result.Payload | |||
| attachs, err := models.GetAllUserAttachments(ctx.User.ID) | |||
| if err != nil { | |||
| ctx.ServerError("GetAllUserAttachments failed:", err) | |||
| return | |||
| } | |||
| ctx.Data["attachments"] = attachs | |||
| ctx.Data["command"] = cloudbrain.Command | |||
| ctx.Data["code_path"] = cloudbrain.CodeMountPath | |||
| ctx.Data["dataset_path"] = cloudbrain.DataSetMountPath | |||
| ctx.Data["model_path"] = cloudbrain.ModelMountPath | |||
| ctx.HTML(200, tplCloudBrainNew) | |||
| } | |||
| @@ -78,7 +118,23 @@ func CloudBrainCreate(ctx *context.Context, form auth.CreateCloudBrainForm) { | |||
| jobName := form.JobName | |||
| image := form.Image | |||
| command := form.Command | |||
| err := cloudbrain.GenerateTask(ctx, jobName, image, command) | |||
| uuid := form.Attachment | |||
| codePath := setting.JobPath + jobName + cloudbrain.CodeMountPath | |||
| repo := ctx.Repo.Repository | |||
| err := downloadCode(repo, codePath) | |||
| if err != nil { | |||
| ctx.RenderWithErr(err.Error(), tplCloudBrainNew, &form) | |||
| return | |||
| } | |||
| modelPath := setting.JobPath + jobName + "/model" | |||
| err = os.MkdirAll(modelPath, os.ModePerm) | |||
| if err != nil { | |||
| ctx.RenderWithErr(err.Error(), tplCloudBrainNew, &form) | |||
| return | |||
| } | |||
| err = cloudbrain.GenerateTask(ctx, jobName, image, command, uuid, codePath, modelPath) | |||
| if err != nil { | |||
| ctx.RenderWithErr(err.Error(), tplCloudBrainNew, &form) | |||
| return | |||
| @@ -94,7 +150,6 @@ func CloudBrainShow(ctx *context.Context) { | |||
| if err != nil { | |||
| ctx.Data["error"] = err.Error() | |||
| } | |||
| ctx.Data["task"] = task | |||
| result, err := cloudbrain.GetJob(jobID) | |||
| if err != nil { | |||
| @@ -105,10 +160,115 @@ func CloudBrainShow(ctx *context.Context) { | |||
| jobRes, _ := models.ConvertToJobResultPayload(result.Payload) | |||
| ctx.Data["result"] = jobRes | |||
| taskRoles := jobRes.TaskRoles | |||
| taskRes, _ := models.ConvertToTaskPod(taskRoles["task1"].(map[string]interface{})) | |||
| taskRes, _ := models.ConvertToTaskPod(taskRoles[cloudbrain.SubTaskName].(map[string]interface{})) | |||
| ctx.Data["taskRes"] = taskRes | |||
| task.Status = taskRes.TaskStatuses[0].State | |||
| task.ContainerID = taskRes.TaskStatuses[0].ContainerID | |||
| task.ContainerIp = taskRes.TaskStatuses[0].ContainerIP | |||
| err = models.UpdateJob(task) | |||
| if err != nil { | |||
| ctx.Data["error"] = err.Error() | |||
| } | |||
| } | |||
| ctx.Data["task"] = task | |||
| ctx.Data["jobID"] = jobID | |||
| ctx.HTML(200, tplCloudBrainShow) | |||
| } | |||
| func CloudBrainDebug(ctx *context.Context) { | |||
| var jobID = ctx.Params(":jobid") | |||
| task, err := models.GetCloudbrainByJobID(jobID) | |||
| if err != nil { | |||
| ctx.ServerError("GetCloudbrainByJobID failed", err) | |||
| return | |||
| } | |||
| debugUrl := setting.DebugServerHost + "jpylab_" + task.JobID + "_" + task.SubTaskName | |||
| ctx.Redirect(debugUrl) | |||
| } | |||
| func CloudBrainCommitImage(ctx *context.Context, form auth.CommitImageCloudBrainForm) { | |||
| var jobID = ctx.Params(":jobid") | |||
| task, err := models.GetCloudbrainByJobID(jobID) | |||
| if err != nil { | |||
| ctx.ServerError("GetCloudbrainByJobID failed", err) | |||
| return | |||
| } | |||
| err = cloudbrain.CommitImage(jobID, models.CommitImageParams{ | |||
| Ip: task.ContainerIp, | |||
| TaskContainerId: task.ContainerID, | |||
| ImageDescription: form.Description, | |||
| ImageTag: form.Tag, | |||
| }) | |||
| if err != nil { | |||
| log.Error("CommitImage(%s) failed:", task.JobName, err.Error()) | |||
| ctx.ServerError("CommitImage failed", err) | |||
| return | |||
| } | |||
| ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/cloudbrain") | |||
| } | |||
| func CloudBrainStop(ctx *context.Context) { | |||
| var jobID = ctx.Params(":jobid") | |||
| log.Info(jobID) | |||
| task, err := models.GetCloudbrainByJobID(jobID) | |||
| if err != nil { | |||
| ctx.ServerError("GetCloudbrainByJobID failed", err) | |||
| return | |||
| } | |||
| if task.Status != string(models.JobRunning) { | |||
| log.Error("the job(%s) is not running", task.JobName) | |||
| ctx.ServerError("the job is not running", errors.New("the job is not running")) | |||
| return | |||
| } | |||
| err = cloudbrain.StopJob(jobID) | |||
| if err != nil { | |||
| log.Error("StopJob(%s) failed:%v", task.JobName, err.Error()) | |||
| ctx.ServerError("StopJob failed", err) | |||
| return | |||
| } | |||
| task.Status = string(models.JobStopped) | |||
| err = models.UpdateJob(task) | |||
| if err != nil { | |||
| ctx.ServerError("UpdateJob failed", err) | |||
| return | |||
| } | |||
| ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/cloudbrain") | |||
| } | |||
| func downloadCode(repo *models.Repository, codePath string) error { | |||
| /* | |||
| if err := git.Clone(repo.RepoPath(), codePath, git.CloneRepoOptions{ | |||
| Bare: true, | |||
| Shared: true, | |||
| }); err != nil { | |||
| log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err) | |||
| return "", fmt.Errorf("Failed to clone repository: %s (%v)", repo.FullName(), err) | |||
| } | |||
| */ | |||
| err := os.MkdirAll(codePath, os.ModePerm) | |||
| if err != nil { | |||
| log.Error("MkdirAll failed:%v", err) | |||
| return err | |||
| } | |||
| command := "git clone " + repo.CloneLink().HTTPS + " " + codePath | |||
| cmd := exec.Command("/bin/bash", "-c", command) | |||
| output, err := cmd.Output() | |||
| log.Info(string(output)) | |||
| if err != nil { | |||
| log.Error("exec.Command(%s) failed:%v", command, err) | |||
| return err | |||
| } | |||
| return nil | |||
| } | |||
| @@ -894,7 +894,12 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| m.Group("/cloudbrain", func() { | |||
| m.Get("", reqRepoCloudBrainReader, repo.CloudBrainIndex) | |||
| m.Get("/:jobid", reqRepoCloudBrainReader, repo.CloudBrainShow) | |||
| m.Group("/:jobid", func() { | |||
| m.Get("", reqRepoCloudBrainReader, repo.CloudBrainShow) | |||
| m.Get("/debug", reqRepoCloudBrainReader, repo.CloudBrainDebug) | |||
| m.Post("/commit_image", reqRepoCloudBrainWriter, bindIgnErr(auth.CommitImageCloudBrainForm{}), repo.CloudBrainCommitImage) | |||
| m.Post("/stop", reqRepoCloudBrainWriter, repo.CloudBrainStop) | |||
| }) | |||
| m.Get("/create", reqRepoCloudBrainWriter, repo.CloudBrainNew) | |||
| m.Post("/create", reqRepoCloudBrainWriter, bindIgnErr(auth.CreateCloudBrainForm{}), repo.CloudBrainCreate) | |||
| }, context.RepoRef()) | |||
| @@ -35,25 +35,74 @@ | |||
| {{range .Tasks}} | |||
| <div class="ui grid item"> | |||
| <div class="row"> | |||
| <div class="seven wide column"> | |||
| <div class="five wide column"> | |||
| <a class="title" href="{{$.Link}}/{{.JobID}}"> | |||
| <span class="fitted">{{svg "octicon-tasklist" 16}}</span> | |||
| <span class="fitted">{{.JobName}}</span> | |||
| </a> | |||
| </div> | |||
| <div class="four wide column job-status" id="{{.JobID}}" data-repopath="{{$.RepoRelPath}}" data-jobid="{{.JobID}}"> | |||
| <div class="three wide column job-status" id="{{.JobID}}" data-repopath="{{$.RepoRelPath}}" data-jobid="{{.JobID}}"> | |||
| {{.Status}} | |||
| </div> | |||
| <div class="three wide column"> | |||
| <span class="ui text center">{{svg "octicon-flame" 16}} {{TimeSinceUnix .CreatedUnix $.Lang}}</span> | |||
| </div> | |||
| <div class="two wide column"> | |||
| <div class="one wide column"> | |||
| <span class="ui text center clipboard"> | |||
| <a class="title" href="{{$.Link}}/{{.JobID}}"> | |||
| <span class="fitted">查看</span> | |||
| </a> | |||
| </span> | |||
| </div> | |||
| <div class="one wide column"> | |||
| <span class="ui text center clipboard"> | |||
| <a class="title" href="{{if not .CanDebug}}javascript:void(0){{else}}{{$.Link}}/{{.JobID}}/debug{{end}}" style="{{if not .CanDebug}}color:#CCCCCC{{end}}"> | |||
| <span class="fitted">调试</span> | |||
| </a> | |||
| </span> | |||
| </div> | |||
| <div class="one wide column"> | |||
| <span class="ui text center clipboard"> | |||
| <form id="stopForm-{{.JobID}}" action="{{if ne .Status "RUNNING"}}javascript:void(0){{else}}{{$.Link}}/{{.JobID}}/stop{{end}}" method="post"> | |||
| {{$.CsrfTokenHtml}} | |||
| <a class="fitted" onclick="document.getElementById('stopForm-{{.JobID}}').submit();" style="{{if ne .Status "RUNNING"}}color:#CCCCCC{{end}}; font-size:16px; font-weight:bold">停止</a> | |||
| </form> | |||
| </span> | |||
| </div> | |||
| <!-- 打开弹窗按钮 --> | |||
| <a class="imageBtn" style="{{if not .CanDebug}}color:#CCCCCC;cursor:pointer;pointer-events:none;{{end}}; font-size:16px; font-weight:bold" value="{{.CanDebug}}">提交镜像</a> | |||
| <!-- 弹窗 --> | |||
| <div id="imageModal" class="modal"> | |||
| <!-- 弹窗内容 --> | |||
| <div class="modal-content"> | |||
| <span class="close">×</span> | |||
| <form id="commitImageForm" action="{{$.Link}}/{{.JobID}}/commit_image" method="post"> | |||
| {{$.CsrfTokenHtml}} | |||
| <p>提交任务镜像</p> | |||
| <div class="ui divider"></div> | |||
| <div class="inline required field dis"> | |||
| <label>镜像标签:</label> | |||
| <input name="tag" id="image_tag" tabindex="3" autofocus required maxlength="255" style="width:75%"> | |||
| </div> | |||
| <div class="inline required field" style="position:relative;height:180px;"> | |||
| <div style="height:20px;width:75px;"> | |||
| <label>镜像描述:</label> | |||
| </div> | |||
| <div style="position:absolute;left:75px;top:0;width:75%"> | |||
| <textarea name="description" rows="10" style="width:100%"></textarea> | |||
| </div> | |||
| </div> | |||
| <div class="ui divider"></div> | |||
| <div class="inline field"> | |||
| <label></label> | |||
| <button class="ui green button"> | |||
| {{$.i18n.Tr "repo.cloudbrain.commit_image"}} | |||
| </button> | |||
| </div> | |||
| </form> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| {{end}} | |||
| @@ -70,7 +119,7 @@ $( document ).ready(function() { | |||
| $( ".job-status" ).each(( index, job ) => { | |||
| const jobID = job.dataset.jobid; | |||
| const repoPath = job.dataset.repopath; | |||
| if (job.textContent.trim() != 'WAITING') { | |||
| if (job.textContent.trim() == 'STOPPED') { | |||
| return | |||
| } | |||
| @@ -84,4 +133,83 @@ $( document ).ready(function() { | |||
| }); | |||
| }); | |||
| }); | |||
| // 获取弹窗 | |||
| var modal = document.getElementById('imageModal'); | |||
| // 打开弹窗的按钮对象 | |||
| var btns = document.getElementsByClassName("imageBtn"); | |||
| // 获取 <span> 元素,用于关闭弹窗 | |||
| var spans = document.getElementsByClassName('close'); | |||
| // 点击按钮打开弹窗 | |||
| for(i=0;i<btns.length;i++){ | |||
| btns[i].onclick = function() { | |||
| modal.style.display = "block"; | |||
| } | |||
| } | |||
| // 点击 <span> (x), 关闭弹窗 | |||
| for(i=0;i<spans.length;i++){ | |||
| spans[i].onclick = function() { | |||
| modal.style.display = "none"; | |||
| } | |||
| } | |||
| // 在用户点击其他地方时,关闭弹窗 | |||
| window.onclick = function(event) { | |||
| if (event.target == modal) { | |||
| modal.style.display = "none"; | |||
| } | |||
| } | |||
| </script> | |||
| <style> | |||
| /* 弹窗 (background) */ | |||
| .modal { | |||
| display: none; /* 默认隐藏 */ | |||
| position: fixed; /* 固定定位 */ | |||
| z-index: 1; /* 设置在顶层 */ | |||
| left: 0; | |||
| top: 0; | |||
| width: 100%; | |||
| height: 100%; | |||
| overflow: auto; | |||
| background-color: rgb(0,0,0); | |||
| background-color: rgba(0,0,0,0.4); | |||
| } | |||
| /* 弹窗内容 */ | |||
| .modal-content { | |||
| background-color: #fefefe; | |||
| margin: 15% auto; | |||
| padding: 20px; | |||
| border: 1px solid #888; | |||
| width: 25%; | |||
| } | |||
| /* 关闭按钮 */ | |||
| .close { | |||
| color: #aaa; | |||
| float: right; | |||
| font-size: 28px; | |||
| font-weight: bold; | |||
| } | |||
| .close:hover, | |||
| .close:focus { | |||
| color: black; | |||
| text-decoration: none; | |||
| cursor: pointer; | |||
| } | |||
| .dis{ | |||
| margin-bottom:20px; | |||
| } | |||
| </style> | |||
| @@ -18,18 +18,42 @@ | |||
| <br> | |||
| <div class="inline required field"> | |||
| <label>镜像</label> | |||
| <input name="image" id="cloudbrain_image" placeholder="输入镜像" value="{{.image}}" tabindex="3" autofocus required maxlength="255"> | |||
| <select id="cloudbrain_image" placeholder="选择镜像" style='width:385px' name="image"> | |||
| {{range .images}} | |||
| <option name="image" value="{{.Place}}">{{.PlaceView}}</option> | |||
| {{end}} | |||
| </select> | |||
| </div> | |||
| <div class="inline required field"> | |||
| <label>数据集(只有zip格式的数据集才能发起云脑任务)</label> | |||
| <select id="cloudbrain_dataset" placeholder="选择数据集" style='width:385px' name="attachment"> | |||
| {{range .attachments}} | |||
| <option name="attachment" value="{{.UUID}}">{{.Attachment.Name}}</option> | |||
| {{end}} | |||
| </select> | |||
| </div> | |||
| <div class="inline required field"> | |||
| <label>数据集存放路径</label> | |||
| <input name="dataset_path" id="cloudbrain_dataset_path" value="{{.dataset_path}}" tabindex="3" autofocus required maxlength="255" readonly="readonly"> | |||
| </div> | |||
| <div class="inline required field"> | |||
| <label>模型存放路径</label> | |||
| <input name="model_path" id="cloudbrain_model_path" value="{{.model_path}}" tabindex="3" autofocus required maxlength="255" readonly="readonly"> | |||
| </div> | |||
| <div class="inline required field"> | |||
| <label>代码存放路径</label> | |||
| <input name="code_path" id="cloudbrain_code_path" value="{{.code_path}}" tabindex="3" autofocus required maxlength="255" readonly="readonly"> | |||
| </div> | |||
| <div class="inline required field"> | |||
| <label>启动命令</label> | |||
| <textarea name="command" rows="10">{{.command}}</textarea> | |||
| <textarea name="command" rows="10" readonly="readonly">{{.command}}</textarea> | |||
| </div> | |||
| <div class="inline field"> | |||
| <label></label> | |||
| <button class="ui green button"> | |||
| {{.i18n.Tr "repo.cloudbrain.new"}} | |||
| </button> | |||
| <a class="ui button" href="/">Cancel</a> | |||
| <a class="ui button" href="/">{{.i18n.Tr "repo.cloudbrain.cancel"}}</a> | |||
| </div> | |||
| </div> | |||
| </form> | |||
| @@ -26,11 +26,11 @@ | |||
| </tr> | |||
| <tr> | |||
| <td> 开始时间 </td> | |||
| <td>{{.StartAt}}</td> | |||
| <td>{{.StartTime}}</td> | |||
| </tr> | |||
| <tr> | |||
| <td> 结束时间 </td> | |||
| <td>{{.FinishedAt}}</td> | |||
| <td>{{.FinishedTime}}</td> | |||
| </tr> | |||
| <tr> | |||
| <td> ExitCode </td> | |||
| @@ -78,11 +78,11 @@ | |||
| </tr> | |||
| <tr> | |||
| <td> 开始时间 </td> | |||
| <td>{{.JobStatus.CreatedTime}}</td> | |||
| <td>{{.JobStatus.StartTime}}</td> | |||
| </tr> | |||
| <tr> | |||
| <td> 结束时间 </td> | |||
| <td>{{.JobStatus.CompletedTime}}</td> | |||
| <td>{{.JobStatus.EndTime}}</td> | |||
| </tr> | |||
| <tr> | |||
| <td> ExitCode </td> | |||