Reviewed-by: stardust <denglf@pcl.ac.cn>master
@@ -1046,16 +1046,15 @@ DEFAULT_QUEUE = DecompressTasksQueue | |||||
RESULT_BACKEND = redis://localhost:6379 | RESULT_BACKEND = redis://localhost:6379 | ||||
[cloudbrain] | [cloudbrain] | ||||
HOST = http://192.168.204.24 | |||||
USERNAME = | USERNAME = | ||||
PASSWORD = | PASSWORD = | ||||
USER_CENTER_HOST = http://192.168.202.73:31441 | |||||
CLIENT_ID = 3Z377wcplxeE2qpycpjv | |||||
CLIENT_SECRET = J5ykfVl2kcxW0H9cawSL | |||||
REST_SERVER_HOST = http://192.168.202.73 | REST_SERVER_HOST = http://192.168.202.73 | ||||
JOB_PATH = /datasets/minio/data/opendata/jobs/ | |||||
DEBUG_SERVER_HOST = http://192.168.202.73/ | |||||
; cloudbrain visit opendata | ; cloudbrain visit opendata | ||||
USER = cW4cMtH24eoWPE7X | USER = cW4cMtH24eoWPE7X | ||||
PWD = 4BPmgvK2hb2Eywwyp4YZRY4B7yQf4DAC | |||||
PWD = 4BPmgvK2hb2Eywwyp4YZRY4B7yQf4DA.C | |||||
JOB_TYPE = debug_openi | |||||
[decompress] | [decompress] | ||||
HOST = http://192.168.207.34:39987 | HOST = http://192.168.207.34:39987 | ||||
@@ -44,6 +44,11 @@ type Attachment struct { | |||||
CreatedUnix timeutil.TimeStamp `xorm:"created"` | CreatedUnix timeutil.TimeStamp `xorm:"created"` | ||||
} | } | ||||
type AttachmentUsername struct { | |||||
Attachment `xorm:"extends"` | |||||
Name string | |||||
} | |||||
func (a *Attachment) AfterUpdate() { | func (a *Attachment) AfterUpdate() { | ||||
if a.DatasetID > 0 { | if a.DatasetID > 0 { | ||||
datasetIsPublicCount, err := x.Where("dataset_id = ? AND is_private = ?", a.DatasetID, false).Count(new(Attachment)) | 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) | 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) | 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) | user, err := getUserByName(x, username) | ||||
if err != nil { | if err != nil { | ||||
log.Error("getUserByName(%s) failed:%v", username, err) | log.Error("getUserByName(%s) failed:%v", username, err) | ||||
@@ -366,8 +375,28 @@ func GetPrivateAttachments(username string) ([]*Attachment, error) { | |||||
return getPrivateAttachments(x, user.ID) | 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" | "errors" | ||||
"fmt" | "fmt" | ||||
"time" | "time" | ||||
"xorm.io/xorm" | |||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
"code.gitea.io/gitea/modules/timeutil" | "code.gitea.io/gitea/modules/timeutil" | ||||
@@ -18,17 +19,22 @@ const ( | |||||
JobStopped CloudbrainStatus = "STOPPED" | JobStopped CloudbrainStatus = "STOPPED" | ||||
JobSucceeded CloudbrainStatus = "SUCCEEDED" | JobSucceeded CloudbrainStatus = "SUCCEEDED" | ||||
JobFailed CloudbrainStatus = "FAILED" | JobFailed CloudbrainStatus = "FAILED" | ||||
JobRunning CloudbrainStatus = "RUNNING" | |||||
) | ) | ||||
type Cloudbrain struct { | type Cloudbrain struct { | ||||
ID int64 `xorm:"pk autoincr"` | ID int64 `xorm:"pk autoincr"` | ||||
JobID string `xorm:"INDEX NOT NULL"` | JobID string `xorm:"INDEX NOT NULL"` | ||||
JobName string | |||||
JobName string `xorm:"INDEX"` | |||||
Status string `xorm:"INDEX"` | Status string `xorm:"INDEX"` | ||||
UserID int64 `xorm:"INDEX"` | UserID int64 `xorm:"INDEX"` | ||||
RepoID int64 `xorm:"INDEX"` | RepoID int64 `xorm:"INDEX"` | ||||
SubTaskName string `xorm:"INDEX"` | |||||
ContainerID string | |||||
ContainerIp string | |||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | ||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | ||||
CanDebug bool `xorm:"-"` | |||||
User *User `xorm:"-"` | User *User `xorm:"-"` | ||||
Repo *Repository `xorm:"-"` | Repo *Repository `xorm:"-"` | ||||
@@ -52,6 +58,17 @@ type TaskRole struct { | |||||
Command string `json:"command"` | Command string `json:"command"` | ||||
NeedIBDevice bool `json:"needIBDevice"` | NeedIBDevice bool `json:"needIBDevice"` | ||||
IsMainRole bool `json:"isMainRole"` | 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 { | type CreateJobParams struct { | ||||
@@ -60,12 +77,13 @@ type CreateJobParams struct { | |||||
GpuType string `json:"gpuType"` | GpuType string `json:"gpuType"` | ||||
Image string `json:"image"` | Image string `json:"image"` | ||||
TaskRoles []TaskRole `json:"taskRoles"` | TaskRoles []TaskRole `json:"taskRoles"` | ||||
Volumes []Volume `json:"volumes"` | |||||
} | } | ||||
type CreateJobResult struct { | 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 { | type GetJobResult struct { | ||||
@@ -74,6 +92,12 @@ type GetJobResult struct { | |||||
Payload map[string]interface{} `json:"payload"` | 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 { | type CloudbrainsOptions struct { | ||||
ListOptions | ListOptions | ||||
RepoID int64 // include all repos if empty | RepoID int64 // include all repos if empty | ||||
@@ -101,6 +125,8 @@ type TaskPod struct { | |||||
ExitCode int `json:"exitCode"` | ExitCode int `json:"exitCode"` | ||||
ExitDiagnostics string `json:"exitDiagnostics"` | ExitDiagnostics string `json:"exitDiagnostics"` | ||||
RetriedCount int `json:"retriedCount"` | RetriedCount int `json:"retriedCount"` | ||||
StartTime string | |||||
FinishedTime string | |||||
} `json:"taskStatuses"` | } `json:"taskStatuses"` | ||||
} | } | ||||
@@ -108,6 +134,8 @@ func ConvertToTaskPod(input map[string]interface{}) (TaskPod, error) { | |||||
data, _ := json.Marshal(input) | data, _ := json.Marshal(input) | ||||
var taskPod TaskPod | var taskPod TaskPod | ||||
err := json.Unmarshal(data, &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 | return taskPod, err | ||||
} | } | ||||
@@ -132,6 +160,8 @@ type JobResultPayload struct { | |||||
AppExitDiagnostics string `json:"appExitDiagnostics"` | AppExitDiagnostics string `json:"appExitDiagnostics"` | ||||
AppExitType interface{} `json:"appExitType"` | AppExitType interface{} `json:"appExitType"` | ||||
VirtualCluster string `json:"virtualCluster"` | VirtualCluster string `json:"virtualCluster"` | ||||
StartTime string | |||||
EndTime string | |||||
} `json:"jobStatus"` | } `json:"jobStatus"` | ||||
TaskRoles map[string]interface{} `json:"taskRoles"` | TaskRoles map[string]interface{} `json:"taskRoles"` | ||||
Resource struct { | Resource struct { | ||||
@@ -170,9 +200,51 @@ func ConvertToJobResultPayload(input map[string]interface{}) (JobResultPayload, | |||||
data, _ := json.Marshal(input) | data, _ := json.Marshal(input) | ||||
var jobResultPayload JobResultPayload | var jobResultPayload JobResultPayload | ||||
err := json.Unmarshal(data, &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 | 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) { | func Cloudbrains(opts *CloudbrainsOptions) ([]*Cloudbrain, int64, error) { | ||||
sess := x.NewSession() | sess := x.NewSession() | ||||
defer sess.Close() | 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) | _, err = x.Cols("status").Where("cloudbrain.job_id=?", jobID).Update(cb) | ||||
return | 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 | // CreateDatasetForm form for dataset page | ||||
type CreateCloudBrainForm struct { | type CreateCloudBrainForm struct { | ||||
JobName string `form:"job_name" binding:"Required"` | 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 { | func (f *CreateCloudBrainForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | ||||
@@ -1,22 +1,39 @@ | |||||
package cloudbrain | package cloudbrain | ||||
import ( | import ( | ||||
"code.gitea.io/gitea/modules/setting" | |||||
"errors" | "errors" | ||||
"code.gitea.io/gitea/models" | |||||
"code.gitea.io/gitea/modules/context" | "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{ | jobResult, err := CreateJob(jobName, models.CreateJobParams{ | ||||
JobName: jobName, | JobName: jobName, | ||||
RetryCount: 1, | RetryCount: 1, | ||||
GpuType: "dgx", | |||||
GpuType: setting.JobType, | |||||
Image: image, | Image: image, | ||||
TaskRoles: []models.TaskRole{ | TaskRoles: []models.TaskRole{ | ||||
{ | { | ||||
Name: "task1", | |||||
Name: SubTaskName, | |||||
TaskNumber: 1, | TaskNumber: 1, | ||||
MinSucceededTaskCount: 1, | MinSucceededTaskCount: 1, | ||||
MinFailedTaskCount: 1, | MinFailedTaskCount: 1, | ||||
@@ -27,13 +44,39 @@ func GenerateTask(ctx *context.Context, jobName, image, command string) error { | |||||
Command: command, | Command: command, | ||||
NeedIBDevice: false, | NeedIBDevice: false, | ||||
IsMainRole: 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 { | if err != nil { | ||||
log.Error("CreateJob failed:", err.Error()) | |||||
return err | return err | ||||
} | } | ||||
if jobResult.Code != "S000" { | |||||
if jobResult.Code != Success { | |||||
log.Error("CreateJob(%s) failed:%s", jobName, jobResult.Msg) | |||||
return errors.New(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, | RepoID: ctx.Repo.Repository.ID, | ||||
JobID: jobID, | JobID: jobID, | ||||
JobName: jobName, | JobName: jobName, | ||||
SubTaskName: SubTaskName, | |||||
}) | }) | ||||
if err != nil { | if err != nil { | ||||
@@ -5,7 +5,7 @@ import ( | |||||
"code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
resty "github.com/go-resty/resty/v2" | |||||
"github.com/go-resty/resty/v2" | |||||
) | ) | ||||
var ( | var ( | ||||
@@ -47,7 +47,7 @@ func loginCloudbrain() error { | |||||
return fmt.Errorf("resty loginCloudbrain: %s", err) | return fmt.Errorf("resty loginCloudbrain: %s", err) | ||||
} | } | ||||
if loginResult.Code != "S000" { | |||||
if loginResult.Code != Success { | |||||
return fmt.Errorf("%s: %s", loginResult.Msg, res.String()) | return fmt.Errorf("%s: %s", loginResult.Msg, res.String()) | ||||
} | } | ||||
@@ -68,7 +68,7 @@ sendjob: | |||||
SetAuthToken(TOKEN). | SetAuthToken(TOKEN). | ||||
SetBody(createJobParams). | SetBody(createJobParams). | ||||
SetResult(&jobResult). | SetResult(&jobResult). | ||||
Put(HOST + "/rest-server/api/v1/jobs/" + jobName) | |||||
Post(HOST + "/rest-server/api/v1/jobs/") | |||||
if err != nil { | if err != nil { | ||||
return nil, fmt.Errorf("resty create job: %s", err) | return nil, fmt.Errorf("resty create job: %s", err) | ||||
@@ -80,7 +80,7 @@ sendjob: | |||||
goto sendjob | goto sendjob | ||||
} | } | ||||
if jobResult.Code != "S000" { | |||||
if jobResult.Code != Success { | |||||
return &jobResult, fmt.Errorf("jobResult err: %s", res.String()) | return &jobResult, fmt.Errorf("jobResult err: %s", res.String()) | ||||
} | } | ||||
@@ -103,7 +103,7 @@ sendjob: | |||||
Get(HOST + "/rest-server/api/v1/jobs/" + jobID) | Get(HOST + "/rest-server/api/v1/jobs/" + jobID) | ||||
if err != nil { | 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 { | if getJobResult.Code == "S401" && retry < 1 { | ||||
@@ -112,9 +112,103 @@ sendjob: | |||||
goto sendjob | goto sendjob | ||||
} | } | ||||
if getJobResult.Code != "S000" { | |||||
if getJobResult.Code != Success { | |||||
return &getJobResult, fmt.Errorf("jobResult GetJob err: %s", res.String()) | return &getJobResult, fmt.Errorf("jobResult GetJob err: %s", res.String()) | ||||
} | } | ||||
return &getJobResult, nil | 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") | cloudbrainSec := Cfg.Section("cloudbrain") | ||||
Cloudbrain.Username = cloudbrainSec.Key("USERNAME").MustString("") | Cloudbrain.Username = cloudbrainSec.Key("USERNAME").MustString("") | ||||
Cloudbrain.Password = cloudbrainSec.Key("PASSWORD").MustString("") | Cloudbrain.Password = cloudbrainSec.Key("PASSWORD").MustString("") | ||||
Cloudbrain.Host = cloudbrainSec.Key("HOST").MustString("") | |||||
Cloudbrain.Host = cloudbrainSec.Key("REST_SERVER_HOST").MustString("") | |||||
return Cloudbrain | return Cloudbrain | ||||
} | } |
@@ -434,10 +434,10 @@ var ( | |||||
//cloudbrain config | //cloudbrain config | ||||
CBAuthUser string | CBAuthUser string | ||||
CBAuthPassword string | CBAuthPassword string | ||||
ClientID string | |||||
ClientSecret string | |||||
UserCeterHost string | |||||
RestServerHost string | RestServerHost string | ||||
JobPath string | |||||
JobType string | |||||
DebugServerHost string | |||||
) | ) | ||||
// DateLang transforms standard language locale name to corresponding value in datetime plugin. | // DateLang transforms standard language locale name to corresponding value in datetime plugin. | ||||
@@ -1109,10 +1109,10 @@ func NewContext() { | |||||
sec = Cfg.Section("cloudbrain") | sec = Cfg.Section("cloudbrain") | ||||
CBAuthUser = sec.Key("USER").MustString("cW4cMtH24eoWPE7X") | CBAuthUser = sec.Key("USER").MustString("cW4cMtH24eoWPE7X") | ||||
CBAuthPassword = sec.Key("PWD").MustString("4BPmgvK2hb2Eywwyp4YZRY4B7yQf4DAC") | 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") | 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 { | func loadInternalToken(sec *ini.Section) string { | ||||
@@ -752,6 +752,7 @@ cloudbrain=云脑 | |||||
cloudbrain.new=新建任务 | cloudbrain.new=新建任务 | ||||
cloudbrain.desc=云脑功能 | cloudbrain.desc=云脑功能 | ||||
cloudbrain.cancel=取消 | cloudbrain.cancel=取消 | ||||
cloudbrain.commit_image=提交 | |||||
template.items=模板选项 | template.items=模板选项 | ||||
template.git_content=Git数据(默认分支) | template.git_content=Git数据(默认分支) | ||||
@@ -6,6 +6,7 @@ | |||||
package repo | package repo | ||||
import ( | import ( | ||||
"code.gitea.io/gitea/modules/log" | |||||
"net/http" | "net/http" | ||||
"time" | "time" | ||||
@@ -48,7 +49,7 @@ func GetCloudbrainTask(ctx *context.APIContext) { | |||||
jobID := ctx.Params(":jobid") | jobID := ctx.Params(":jobid") | ||||
repoID := ctx.Repo.Repository.ID | repoID := ctx.Repo.Repository.ID | ||||
_, err = models.GetRepoCloudBrainByJobID(repoID, jobID) | |||||
job, err := models.GetRepoCloudBrainByJobID(repoID, jobID) | |||||
if err != nil { | if err != nil { | ||||
ctx.NotFound(err) | ctx.NotFound(err) | ||||
return | return | ||||
@@ -64,8 +65,18 @@ func GetCloudbrainTask(ctx *context.APIContext) { | |||||
return | 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) { | 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{}{ | ctx.JSON(http.StatusOK, map[string]interface{}{ | ||||
@@ -635,7 +635,7 @@ func QueryAllPublicDataset(ctx *context.Context){ | |||||
return | return | ||||
} | } | ||||
queryDatasets(ctx, "admin", attachs) | |||||
queryDatasets(ctx, attachs) | |||||
} | } | ||||
func QueryPrivateDataset(ctx *context.Context){ | func QueryPrivateDataset(ctx *context.Context){ | ||||
@@ -650,26 +650,40 @@ func QueryPrivateDataset(ctx *context.Context){ | |||||
return | 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 | 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 { | for _, attch := range attachs { | ||||
has,err := storage.Attachments.HasObject(models.AttachmentRelativePath(attch.UUID)) | has,err := storage.Attachments.HasObject(models.AttachmentRelativePath(attch.UUID)) | ||||
if err != nil || !has { | if err != nil || !has { | ||||
continue | 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.RealPath + | ||||
setting.Attachment.Minio.Bucket + "/" + | setting.Attachment.Minio.Bucket + "/" + | ||||
setting.Attachment.Minio.BasePath + | setting.Attachment.Minio.BasePath + | ||||
models.AttachmentRelativePath(attch.UUID) + | models.AttachmentRelativePath(attch.UUID) + | ||||
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) | data,err := json.Marshal(datasets) | ||||
@@ -1,7 +1,11 @@ | |||||
package repo | package repo | ||||
import ( | import ( | ||||
"errors" | |||||
"os" | |||||
"os/exec" | |||||
"strconv" | "strconv" | ||||
"strings" | |||||
"time" | "time" | ||||
"code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
@@ -9,6 +13,7 @@ import ( | |||||
"code.gitea.io/gitea/modules/base" | "code.gitea.io/gitea/modules/base" | ||||
"code.gitea.io/gitea/modules/cloudbrain" | "code.gitea.io/gitea/modules/cloudbrain" | ||||
"code.gitea.io/gitea/modules/context" | "code.gitea.io/gitea/modules/context" | ||||
"code.gitea.io/gitea/modules/log" | |||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
) | ) | ||||
@@ -46,6 +51,15 @@ func CloudBrainIndex(ctx *context.Context) { | |||||
return | 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 := context.NewPagination(int(count), setting.UI.IssuePagingNum, page, 5) | ||||
pager.SetDefaultParams(ctx) | pager.SetDefaultParams(ctx) | ||||
ctx.Data["Page"] = pager | ctx.Data["Page"] = pager | ||||
@@ -66,10 +80,36 @@ func CloudBrainNew(ctx *context.Context) { | |||||
ctx.Data["PageIsCloudBrain"] = true | ctx.Data["PageIsCloudBrain"] = true | ||||
t := time.Now() | 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["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) | ctx.HTML(200, tplCloudBrainNew) | ||||
} | } | ||||
@@ -78,7 +118,23 @@ func CloudBrainCreate(ctx *context.Context, form auth.CreateCloudBrainForm) { | |||||
jobName := form.JobName | jobName := form.JobName | ||||
image := form.Image | image := form.Image | ||||
command := form.Command | 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 { | if err != nil { | ||||
ctx.RenderWithErr(err.Error(), tplCloudBrainNew, &form) | ctx.RenderWithErr(err.Error(), tplCloudBrainNew, &form) | ||||
return | return | ||||
@@ -94,7 +150,6 @@ func CloudBrainShow(ctx *context.Context) { | |||||
if err != nil { | if err != nil { | ||||
ctx.Data["error"] = err.Error() | ctx.Data["error"] = err.Error() | ||||
} | } | ||||
ctx.Data["task"] = task | |||||
result, err := cloudbrain.GetJob(jobID) | result, err := cloudbrain.GetJob(jobID) | ||||
if err != nil { | if err != nil { | ||||
@@ -105,10 +160,115 @@ func CloudBrainShow(ctx *context.Context) { | |||||
jobRes, _ := models.ConvertToJobResultPayload(result.Payload) | jobRes, _ := models.ConvertToJobResultPayload(result.Payload) | ||||
ctx.Data["result"] = jobRes | ctx.Data["result"] = jobRes | ||||
taskRoles := jobRes.TaskRoles | taskRoles := jobRes.TaskRoles | ||||
taskRes, _ := models.ConvertToTaskPod(taskRoles["task1"].(map[string]interface{})) | |||||
taskRes, _ := models.ConvertToTaskPod(taskRoles[cloudbrain.SubTaskName].(map[string]interface{})) | |||||
ctx.Data["taskRes"] = taskRes | 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.Data["jobID"] = jobID | ||||
ctx.HTML(200, tplCloudBrainShow) | 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.Group("/cloudbrain", func() { | ||||
m.Get("", reqRepoCloudBrainReader, repo.CloudBrainIndex) | 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.Get("/create", reqRepoCloudBrainWriter, repo.CloudBrainNew) | ||||
m.Post("/create", reqRepoCloudBrainWriter, bindIgnErr(auth.CreateCloudBrainForm{}), repo.CloudBrainCreate) | m.Post("/create", reqRepoCloudBrainWriter, bindIgnErr(auth.CreateCloudBrainForm{}), repo.CloudBrainCreate) | ||||
}, context.RepoRef()) | }, context.RepoRef()) | ||||
@@ -35,25 +35,74 @@ | |||||
{{range .Tasks}} | {{range .Tasks}} | ||||
<div class="ui grid item"> | <div class="ui grid item"> | ||||
<div class="row"> | <div class="row"> | ||||
<div class="seven wide column"> | |||||
<div class="five wide column"> | |||||
<a class="title" href="{{$.Link}}/{{.JobID}}"> | <a class="title" href="{{$.Link}}/{{.JobID}}"> | ||||
<span class="fitted">{{svg "octicon-tasklist" 16}}</span> | <span class="fitted">{{svg "octicon-tasklist" 16}}</span> | ||||
<span class="fitted">{{.JobName}}</span> | <span class="fitted">{{.JobName}}</span> | ||||
</a> | </a> | ||||
</div> | </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}} | {{.Status}} | ||||
</div> | </div> | ||||
<div class="three wide column"> | <div class="three wide column"> | ||||
<span class="ui text center">{{svg "octicon-flame" 16}} {{TimeSinceUnix .CreatedUnix $.Lang}}</span> | <span class="ui text center">{{svg "octicon-flame" 16}} {{TimeSinceUnix .CreatedUnix $.Lang}}</span> | ||||
</div> | </div> | ||||
<div class="two wide column"> | |||||
<div class="one wide column"> | |||||
<span class="ui text center clipboard"> | <span class="ui text center clipboard"> | ||||
<a class="title" href="{{$.Link}}/{{.JobID}}"> | <a class="title" href="{{$.Link}}/{{.JobID}}"> | ||||
<span class="fitted">查看</span> | <span class="fitted">查看</span> | ||||
</a> | </a> | ||||
</span> | </span> | ||||
</div> | </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> | ||||
</div> | </div> | ||||
{{end}} | {{end}} | ||||
@@ -70,7 +119,7 @@ $( document ).ready(function() { | |||||
$( ".job-status" ).each(( index, job ) => { | $( ".job-status" ).each(( index, job ) => { | ||||
const jobID = job.dataset.jobid; | const jobID = job.dataset.jobid; | ||||
const repoPath = job.dataset.repopath; | const repoPath = job.dataset.repopath; | ||||
if (job.textContent.trim() != 'WAITING') { | |||||
if (job.textContent.trim() == 'STOPPED') { | |||||
return | 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> | </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> | <br> | ||||
<div class="inline required field"> | <div class="inline required field"> | ||||
<label>镜像</label> | <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> | ||||
<div class="inline required field"> | <div class="inline required field"> | ||||
<label>启动命令</label> | <label>启动命令</label> | ||||
<textarea name="command" rows="10">{{.command}}</textarea> | |||||
<textarea name="command" rows="10" readonly="readonly">{{.command}}</textarea> | |||||
</div> | </div> | ||||
<div class="inline field"> | <div class="inline field"> | ||||
<label></label> | <label></label> | ||||
<button class="ui green button"> | <button class="ui green button"> | ||||
{{.i18n.Tr "repo.cloudbrain.new"}} | {{.i18n.Tr "repo.cloudbrain.new"}} | ||||
</button> | </button> | ||||
<a class="ui button" href="/">Cancel</a> | |||||
<a class="ui button" href="/">{{.i18n.Tr "repo.cloudbrain.cancel"}}</a> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</form> | </form> | ||||
@@ -26,11 +26,11 @@ | |||||
</tr> | </tr> | ||||
<tr> | <tr> | ||||
<td> 开始时间 </td> | <td> 开始时间 </td> | ||||
<td>{{.StartAt}}</td> | |||||
<td>{{.StartTime}}</td> | |||||
</tr> | </tr> | ||||
<tr> | <tr> | ||||
<td> 结束时间 </td> | <td> 结束时间 </td> | ||||
<td>{{.FinishedAt}}</td> | |||||
<td>{{.FinishedTime}}</td> | |||||
</tr> | </tr> | ||||
<tr> | <tr> | ||||
<td> ExitCode </td> | <td> ExitCode </td> | ||||
@@ -78,11 +78,11 @@ | |||||
</tr> | </tr> | ||||
<tr> | <tr> | ||||
<td> 开始时间 </td> | <td> 开始时间 </td> | ||||
<td>{{.JobStatus.CreatedTime}}</td> | |||||
<td>{{.JobStatus.StartTime}}</td> | |||||
</tr> | </tr> | ||||
<tr> | <tr> | ||||
<td> 结束时间 </td> | <td> 结束时间 </td> | ||||
<td>{{.JobStatus.CompletedTime}}</td> | |||||
<td>{{.JobStatus.EndTime}}</td> | |||||
</tr> | </tr> | ||||
<tr> | <tr> | ||||
<td> ExitCode </td> | <td> ExitCode </td> | ||||