Browse Source

cd notebook

tags/v1.22.9.1^2
lewis 3 years ago
parent
commit
e92d948822
8 changed files with 266 additions and 317 deletions
  1. +2
    -0
      go.sum
  2. +26
    -39
      models/cloudbrain.go
  3. +2
    -2
      modules/grampus/grampus.go
  4. +9
    -9
      modules/modelarts/modelarts.go
  5. +9
    -22
      modules/modelarts_cd/modelarts.go
  6. +90
    -116
      modules/modelarts_cd/resty.go
  7. +69
    -19
      modules/setting/setting.go
  8. +59
    -110
      routers/repo/modelarts.go

+ 2
- 0
go.sum View File

@@ -713,12 +713,14 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w=
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=


+ 26
- 39
models/cloudbrain.go View File

@@ -24,15 +24,16 @@ type ModelArtsJobStatus string
const ( const (
TypeCloudBrainOne int = iota TypeCloudBrainOne int = iota
TypeCloudBrainTwo TypeCloudBrainTwo
TypeC2Net //智算网络
TypeC2Net //智算网络
TypeCDCenter //成都智算中心


TypeCloudBrainAll = -1 TypeCloudBrainAll = -1
) )


const ( const (
NPUResource = "NPU"
GPUResource = "CPU/GPU"
AllResource = "all"
NPUResource = "NPU"
GPUResource = "CPU/GPU"
AllResource = "all"


//notebook storage category //notebook storage category
EVSCategory = "EVS" EVSCategory = "EVS"
@@ -584,37 +585,17 @@ type ResourceSpec struct {
ShareMemMiB int `json:"shareMemMiB"` ShareMemMiB int `json:"shareMemMiB"`
} }


type FlavorInfos struct {
FlavorInfo []*FlavorInfo `json:"flavor_info"`
}

type FlavorInfo struct {
Id int `json:"id"`
Value string `json:"value"`
Desc string `json:"desc"`
}

type SpecialPools struct { type SpecialPools struct {
Pools []*SpecialPool `json:"pools"` Pools []*SpecialPool `json:"pools"`
} }
type SpecialPool struct { type SpecialPool struct {
Org string `json:"org"`
Type string `json:"type"`
IsExclusive bool `json:"isExclusive"`
Pool []*GpuInfo `json:"pool"`
JobType []string `json:"jobType"`
ResourceSpec []*ResourceSpec `json:"resourceSpecs"`
Flavor []*FlavorInfo `json:"flavor"`
}

type ImageInfosModelArts struct {
ImageInfo []*ImageInfoModelArts `json:"image_info"`
}

type ImageInfoModelArts struct {
Id string `json:"id"`
Value string `json:"value"`
Desc string `json:"desc"`
Org string `json:"org"`
Type string `json:"type"`
IsExclusive bool `json:"isExclusive"`
Pool []*GpuInfo `json:"pool"`
JobType []string `json:"jobType"`
ResourceSpec []*ResourceSpec `json:"resourceSpecs"`
Flavor []*setting.FlavorInfo `json:"flavor"`
} }


type PoolInfos struct { type PoolInfos struct {
@@ -720,6 +701,17 @@ type CreateNotebook2Params struct {
Volume VolumeReq `json:"volume"` Volume VolumeReq `json:"volume"`
} }


type CreateNotebookWithoutPoolParams struct {
JobName string `json:"name"`
Description string `json:"description"`
Duration int64 `json:"duration"` //ms
Feature string `json:"feature"`
Flavor string `json:"flavor"`
ImageID string `json:"image_id"`
WorkspaceID string `json:"workspace_id"`
Volume VolumeReq `json:"volume"`
}

type VolumeReq struct { type VolumeReq struct {
Capacity int `json:"capacity"` Capacity int `json:"capacity"`
Category string `json:"category"` Category string `json:"category"`
@@ -943,6 +935,7 @@ type NotebookGetJobTokenResult struct {
} }


type NotebookDelResult struct { type NotebookDelResult struct {
NotebookResult
InstanceID string `json:"instance_id"` InstanceID string `json:"instance_id"`
} }


@@ -1434,12 +1427,6 @@ func Cloudbrains(opts *CloudbrainsOptions) ([]*CloudbrainInfo, int64, error) {
) )
} }


if len(opts.ComputeResource) > 0 {
cond = cond.And(
builder.Eq{"cloudbrain.compute_resource": opts.ComputeResource},
)
}

if len(opts.JobTypes) > 0 { if len(opts.JobTypes) > 0 {
if opts.JobTypeNot { if opts.JobTypeNot {
cond = cond.And( cond = cond.And(
@@ -1904,9 +1891,9 @@ func GetWaitingCloudbrainCount(cloudbrainType int, computeResource string, jobTy
return sess.Count(new(Cloudbrain)) return sess.Count(new(Cloudbrain))
} }


func GetCloudbrainNotebookCountByUserID(userID int64) (int, error) {
func GetCloudbrainNotebookCountByUserID(userID int64, typeCloudbrain int) (int, error) {
count, err := x.In("status", ModelArtsCreateQueue, ModelArtsCreating, ModelArtsStarting, ModelArtsReadyToStart, ModelArtsResizing, ModelArtsStartQueuing, ModelArtsRunning, ModelArtsRestarting). count, err := x.In("status", ModelArtsCreateQueue, ModelArtsCreating, ModelArtsStarting, ModelArtsReadyToStart, ModelArtsResizing, ModelArtsStartQueuing, ModelArtsRunning, ModelArtsRestarting).
And("job_type = ? and user_id = ? and type = ?", JobTypeDebug, userID, TypeCloudBrainTwo).Count(new(Cloudbrain))
And("job_type = ? and user_id = ? and type = ?", JobTypeDebug, userID, typeCloudbrain).Count(new(Cloudbrain))
return int(count), err return int(count), err
} }




+ 2
- 2
modules/grampus/grampus.go View File

@@ -30,8 +30,8 @@ const (


var ( var (
poolInfos *models.PoolInfos poolInfos *models.PoolInfos
FlavorInfos *models.FlavorInfos
ImageInfos *models.ImageInfosModelArts
FlavorInfos *setting.StFlavorInfos
ImageInfos *setting.StImageInfosModelArts


SpecialPools *models.SpecialPools SpecialPools *models.SpecialPools
) )


+ 9
- 9
modules/modelarts/modelarts.go View File

@@ -1,6 +1,7 @@
package modelarts package modelarts


import ( import (
"code.gitea.io/gitea/modules/modelarts_cd"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -68,8 +69,6 @@ const (


var ( var (
poolInfos *models.PoolInfos poolInfos *models.PoolInfos
FlavorInfos *models.FlavorInfos
ImageInfos *models.ImageInfosModelArts
TrainFlavorInfos *Flavor TrainFlavorInfos *Flavor
SpecialPools *models.SpecialPools SpecialPools *models.SpecialPools
) )
@@ -747,11 +746,7 @@ func GetNotebookImageName(imageId string) (string, error) {
var validImage = false var validImage = false
var imageName = "" var imageName = ""


if ImageInfos == nil {
json.Unmarshal([]byte(setting.ImageInfos), &ImageInfos)
}

for _, imageInfo := range ImageInfos.ImageInfo {
for _, imageInfo := range setting.StImageInfos.ImageInfo {
if imageInfo.Id == imageId { if imageInfo.Id == imageId {
validImage = true validImage = true
imageName = imageInfo.Value imageName = imageInfo.Value
@@ -808,8 +803,13 @@ func HandleTrainJobInfo(task *models.Cloudbrain) error {
} }


func HandleNotebookInfo(task *models.Cloudbrain) error { func HandleNotebookInfo(task *models.Cloudbrain) error {

result, err := GetNotebook2(task.JobID)
var result *models.GetNotebook2Result
var err error
if task.Type == models.TypeCloudBrainTwo {
result, err = GetNotebook2(task.JobID)
} else if task.Type == models.TypeCDCenter {
result, err = modelarts_cd.GetNotebook(task.JobID)
}
if err != nil { if err != nil {
log.Error("GetNotebook2(%s) failed:%v", task.DisplayJobName, err) log.Error("GetNotebook2(%s) failed:%v", task.DisplayJobName, err)
return err return err


+ 9
- 22
modules/modelarts_cd/modelarts.go View File

@@ -1,7 +1,6 @@
package modelarts_cd package modelarts_cd


import ( import (
"encoding/json"
"errors" "errors"
"strconv" "strconv"
"strings" "strings"
@@ -51,13 +50,7 @@ const (
TotalVersionCount = 1 TotalVersionCount = 1
) )


var (
poolInfos *models.PoolInfos
FlavorInfos *models.FlavorInfos
ImageInfos *models.ImageInfosModelArts
TrainFlavorInfos *Flavor
SpecialPools *models.SpecialPools
)
var ()


type VersionInfo struct { type VersionInfo struct {
Version []struct { Version []struct {
@@ -96,23 +89,18 @@ type Parameters struct {
} }


func GenerateNotebook(ctx *context.Context, displayJobName, jobName, uuid, description, flavor, imageId string) error { func GenerateNotebook(ctx *context.Context, displayJobName, jobName, uuid, description, flavor, imageId string) error {
if poolInfos == nil {
json.Unmarshal([]byte(setting.PoolInfos), &poolInfos)
}

imageName, err := GetNotebookImageName(imageId) imageName, err := GetNotebookImageName(imageId)
if err != nil { if err != nil {
log.Error("GetNotebookImageName failed: %v", err.Error()) log.Error("GetNotebookImageName failed: %v", err.Error())
return err return err
} }
createTime := timeutil.TimeStampNow() createTime := timeutil.TimeStampNow()
jobResult, err := createNotebook(models.CreateNotebook2Params{
jobResult, err := createNotebook(models.CreateNotebookWithoutPoolParams{
JobName: jobName, JobName: jobName,
Description: description, Description: description,
Flavor: flavor, Flavor: flavor,
Duration: autoStopDurationMs, Duration: autoStopDurationMs,
ImageID: imageId, ImageID: imageId,
PoolID: poolInfos.PoolInfo[0].PoolId,
Feature: models.NotebookFeature, Feature: models.NotebookFeature,
Volume: models.VolumeReq{ Volume: models.VolumeReq{
Capacity: setting.Capacity, Capacity: setting.Capacity,
@@ -122,14 +110,14 @@ func GenerateNotebook(ctx *context.Context, displayJobName, jobName, uuid, descr
WorkspaceID: "0", WorkspaceID: "0",
}) })
if err != nil { if err != nil {
log.Error("createNotebook2 failed: %v", err.Error())
log.Error("createNotebook failed: %v", err.Error())
if strings.HasPrefix(err.Error(), UnknownErrorPrefix) { if strings.HasPrefix(err.Error(), UnknownErrorPrefix) {
log.Info("(%s)unknown error, set temp status", displayJobName) log.Info("(%s)unknown error, set temp status", displayJobName)
errTemp := models.InsertCloudbrainTemp(&models.CloudbrainTemp{ errTemp := models.InsertCloudbrainTemp(&models.CloudbrainTemp{
JobID: models.TempJobId, JobID: models.TempJobId,
VersionID: models.TempVersionId, VersionID: models.TempVersionId,
Status: models.TempJobStatus, Status: models.TempJobStatus,
Type: models.TypeCloudBrainTwo,
Type: models.TypeCDCenter,
JobName: jobName, JobName: jobName,
JobType: string(models.JobTypeDebug), JobType: string(models.JobTypeDebug),
}) })
@@ -149,7 +137,7 @@ func GenerateNotebook(ctx *context.Context, displayJobName, jobName, uuid, descr
FlavorCode: flavor, FlavorCode: flavor,
DisplayJobName: displayJobName, DisplayJobName: displayJobName,
JobType: string(models.JobTypeDebug), JobType: string(models.JobTypeDebug),
Type: models.TypeCloudBrainTwo,
Type: models.TypeCDCenter,
Uuid: uuid, Uuid: uuid,
ComputeResource: models.NPUResource, ComputeResource: models.NPUResource,
Image: imageName, Image: imageName,
@@ -172,11 +160,7 @@ func GetNotebookImageName(imageId string) (string, error) {
var validImage = false var validImage = false
var imageName = "" var imageName = ""


if ImageInfos == nil {
json.Unmarshal([]byte(setting.ImageInfos), &ImageInfos)
}

for _, imageInfo := range ImageInfos.ImageInfo {
for _, imageInfo := range setting.StImageInfos.ImageInfo {
if imageInfo.Id == imageId { if imageInfo.Id == imageId {
validImage = true validImage = true
imageName = imageInfo.Value imageName = imageInfo.Value
@@ -191,6 +175,7 @@ func GetNotebookImageName(imageId string) (string, error) {
return imageName, nil return imageName, nil
} }


/*
func HandleNotebookInfo(task *models.Cloudbrain) error { func HandleNotebookInfo(task *models.Cloudbrain) error {


result, err := GetNotebook(task.JobID) result, err := GetNotebook(task.JobID)
@@ -225,3 +210,5 @@ func HandleNotebookInfo(task *models.Cloudbrain) error {


return nil return nil
} }

*/

+ 90
- 116
modules/modelarts_cd/resty.go View File

@@ -23,9 +23,6 @@ var (
) )


const ( const (
methodPassword = "password"

urlGetToken = "/v3/auth/tokens"
errorCodeExceedLimit = "ModelArts.0118" errorCodeExceedLimit = "ModelArts.0118"


//notebook 2.0 //notebook 2.0
@@ -50,196 +47,173 @@ func getHttpClient() *http.Client {
} }


func GetNotebook(jobID string) (*models.GetNotebook2Result, error) { func GetNotebook(jobID string) (*models.GetNotebook2Result, error) {
client := getHttpClient()
var result models.GetNotebook2Result var result models.GetNotebook2Result


retry := 0
client := getHttpClient()
s := core.Signer{
Key: setting.ModelartsCD.AccessKey,
Secret: setting.ModelartsCD.SecretKey,
}
r, _ := http.NewRequest(http.MethodGet,
setting.ModelartsCD.EndPoint+"/v1/"+setting.ModelartsCD.ProjectID+urlNotebook2+"/"+jobID,
nil)


sendjob:
res, err := client.R().
SetHeader("Content-Type", "application/json").
SetAuthToken(TOKEN).
SetResult(&result).
Get(HOST + "/v1/" + setting.ProjectID + urlNotebook2 + "/" + jobID)
r.Header.Add("content-type", "application/json")
s.Sign(r)


resp, err := client.Do(r)
if err != nil { if err != nil {
return nil, fmt.Errorf("resty GetJob: %v", err)
log.Error("client.Do failed: %s", err.Error())
return &result, fmt.Errorf("client.Do failed: %s", err.Error())
} }


if res.StatusCode() == http.StatusUnauthorized && retry < 1 {
retry++
_ = getToken()
goto sendjob
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Error("ioutil.ReadAll failed: %s", err.Error())
return &result, fmt.Errorf("ioutil.ReadAll failed: %s", err.Error())
} }


var response models.NotebookResult
err = json.Unmarshal(res.Body(), &response)
err = json.Unmarshal(body, &result)
if err != nil { if err != nil {
log.Error("json.Unmarshal failed: %s", err.Error()) log.Error("json.Unmarshal failed: %s", err.Error())
return &result, fmt.Errorf("son.Unmarshal failed: %s", err.Error()) return &result, fmt.Errorf("son.Unmarshal failed: %s", err.Error())
} }


if len(response.ErrorCode) != 0 {
log.Error("GetJob failed(%s): %s", response.ErrorCode, response.ErrorMsg)
if response.ErrorCode == modelartsIllegalToken && retry < 1 {
retry++
_ = getToken()
goto sendjob
}
return &result, fmt.Errorf("GetJob failed(%s): %s", response.ErrorCode, response.ErrorMsg)
if len(result.ErrorCode) != 0 {
log.Error("GetNotebook failed(%s): %s", result.ErrorCode, result.ErrorMsg)
return &result, fmt.Errorf("GetNotebook failed(%s): %s", result.ErrorCode, result.ErrorMsg)
} }


return &result, nil return &result, nil
} }


func ManageNotebook(jobID string, param models.NotebookAction) (*models.NotebookActionResult, error) { func ManageNotebook(jobID string, param models.NotebookAction) (*models.NotebookActionResult, error) {
client := getHttpClient()
var result models.NotebookActionResult var result models.NotebookActionResult


retry := 0
client := getHttpClient()
s := core.Signer{
Key: setting.ModelartsCD.AccessKey,
Secret: setting.ModelartsCD.SecretKey,
}
r, _ := http.NewRequest(http.MethodPost,
setting.ModelartsCD.EndPoint+"/v1/"+setting.ModelartsCD.ProjectID+urlNotebook2+"/"+jobID+"/"+param.Action+"?duration="+strconv.Itoa(autoStopDurationMs),
nil)


sendjob:
res, err := client.R().
SetHeader("Content-Type", "application/json").
SetAuthToken(TOKEN).
SetResult(&result).
Post(HOST + "/v1/" + setting.ProjectID + urlNotebook2 + "/" + jobID + "/" + param.Action + "?duration=" + strconv.Itoa(autoStopDurationMs))
r.Header.Add("content-type", "application/json")
s.Sign(r)


resp, err := client.Do(r)
if err != nil { if err != nil {
return &result, fmt.Errorf("resty ManageNotebook2: %v", err)
log.Error("client.Do failed: %s", err.Error())
return &result, fmt.Errorf("client.Do failed: %s", err.Error())
} }


if res.StatusCode() == http.StatusUnauthorized && retry < 1 {
retry++
_ = getToken()
goto sendjob
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Error("ioutil.ReadAll failed: %s", err.Error())
return &result, fmt.Errorf("ioutil.ReadAll failed: %s", err.Error())
} }


var response models.NotebookResult
err = json.Unmarshal(res.Body(), &response)
err = json.Unmarshal(body, &result)
if err != nil { if err != nil {
log.Error("json.Unmarshal failed: %s", err.Error()) log.Error("json.Unmarshal failed: %s", err.Error())
return &result, fmt.Errorf("son.Unmarshal failed: %s", err.Error()) return &result, fmt.Errorf("son.Unmarshal failed: %s", err.Error())
} }


if res.StatusCode() == http.StatusBadGateway {
return &result, fmt.Errorf(UnknownErrorPrefix+"createNotebook2 failed(%s): %s", response.ErrorCode, response.ErrorMsg)
}

if len(response.ErrorCode) != 0 {
log.Error("ManageNotebook2 failed(%s): %s", response.ErrorCode, response.ErrorMsg)
if response.ErrorCode == modelartsIllegalToken && retry < 1 {
retry++
_ = getToken()
goto sendjob
}
return &result, fmt.Errorf("ManageNotebook2 failed(%s): %s", response.ErrorCode, response.ErrorMsg)
if len(result.ErrorCode) != 0 {
log.Error("ManageNotebook2 failed(%s): %s", result.ErrorCode, result.ErrorMsg)
return &result, fmt.Errorf("ManageNotebook failed(%s): %s", result.ErrorCode, result.ErrorMsg)
} }


return &result, nil return &result, nil
} }


func DelNotebook(jobID string) (*models.NotebookDelResult, error) { func DelNotebook(jobID string) (*models.NotebookDelResult, error) {
client := getHttpClient()
var result models.NotebookDelResult var result models.NotebookDelResult


retry := 0
client := getHttpClient()
s := core.Signer{
Key: setting.ModelartsCD.AccessKey,
Secret: setting.ModelartsCD.SecretKey,
}


sendjob:
res, err := client.R().
SetHeader("Content-Type", "application/json").
SetAuthToken(TOKEN).
SetResult(&result).
Delete(HOST + "/v1/" + setting.ProjectID + urlNotebook2 + "/" + jobID)
r, _ := http.NewRequest(http.MethodDelete,
setting.ModelartsCD.EndPoint+"/v1/"+setting.ModelartsCD.ProjectID+urlNotebook2+"/"+jobID,
nil)
r.Header.Add("content-type", "application/json")
s.Sign(r)


resp, err := client.Do(r)
if err != nil { if err != nil {
return &result, fmt.Errorf("resty DelJob: %v", err)
log.Error("client.Do failed: %s", err.Error())
return &result, fmt.Errorf("client.Do failed: %s", err.Error())
} }


if res.StatusCode() == http.StatusUnauthorized && retry < 1 {
retry++
_ = getToken()
goto sendjob
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Error("ioutil.ReadAll failed: %s", err.Error())
return &result, fmt.Errorf("ioutil.ReadAll failed: %s", err.Error())
} }


var response models.NotebookResult
err = json.Unmarshal(res.Body(), &response)
err = json.Unmarshal(body, &result)
if err != nil { if err != nil {
log.Error("json.Unmarshal failed: %s", err.Error()) log.Error("json.Unmarshal failed: %s", err.Error())
return &result, fmt.Errorf("son.Unmarshal failed: %s", err.Error()) return &result, fmt.Errorf("son.Unmarshal failed: %s", err.Error())
} }


if len(response.ErrorCode) != 0 {
log.Error("DelNotebook2 failed(%s): %s", response.ErrorCode, response.ErrorMsg)
if response.ErrorCode == modelartsIllegalToken && retry < 1 {
retry++
_ = getToken()
goto sendjob
}
return &result, fmt.Errorf("DelNotebook2 failed(%s): %s", response.ErrorCode, response.ErrorMsg)
if len(result.ErrorCode) != 0 {
log.Error("DelNotebook2 failed(%s): %s", result.ErrorCode, result.ErrorMsg)
return &result, fmt.Errorf("DelNotebook2 failed(%s): %s", result.ErrorCode, result.ErrorMsg)
} }


return &result, nil return &result, nil
} }


func createNotebook(createJobParams models.CreateNotebook2Params) (*models.CreateNotebookResult, error) {
client := getHttpClient()
func createNotebook(createJobParams models.CreateNotebookWithoutPoolParams) (*models.CreateNotebookResult, error) {
var result models.CreateNotebookResult var result models.CreateNotebookResult

retry := 0

client := getHttpClient()
s := core.Signer{ s := core.Signer{
Key: "",
Secret: "",
Key: setting.ModelartsCD.AccessKey,
Secret: setting.ModelartsCD.SecretKey,
} }


r, _ := http.NewRequest(http.MethodPost, "", ioutil.NopCloser(bytes.NewBuffer([]byte(""))))
req, _ := json.Marshal(createJobParams)
r, _ := http.NewRequest(http.MethodPost,
setting.ModelartsCD.EndPoint+"/v1/"+setting.ModelartsCD.ProjectID+urlNotebook2,
ioutil.NopCloser(bytes.NewBuffer(req)))


r.Header.Add("content-type", "application/json")
s.Sign(r) s.Sign(r)


resp, err := http.DefaultClient.Do(r)
body, err := ioutil.ReadAll(resp.Body)

sendjob:
res, err := client.
SetHeader("Content-Type", "application/json").
SetAuthToken(TOKEN).
SetBody(createJobParams).
SetResult(&result).
Post(HOST + "/v1/" + setting.ProjectID + urlNotebook2)

resp, err := client.Do(r)
if err != nil { if err != nil {
return nil, fmt.Errorf("resty create notebook2: %s", err)
log.Error("client.Do failed: %s", err.Error())
return &result, fmt.Errorf("client.Do failed: %s", err.Error())
} }


if res.StatusCode() == http.StatusUnauthorized && retry < 1 {
retry++
_ = getToken()
goto sendjob
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Error("ioutil.ReadAll failed: %s", err.Error())
return &result, fmt.Errorf("ioutil.ReadAll failed: %s", err.Error())
} }


var response models.NotebookResult
err = json.Unmarshal(res.Body(), &response)
err = json.Unmarshal(body, &result)
if err != nil { if err != nil {
log.Error("json.Unmarshal failed: %s", err.Error()) log.Error("json.Unmarshal failed: %s", err.Error())
return &result, fmt.Errorf("json.Unmarshal failed: %s", err.Error()) return &result, fmt.Errorf("json.Unmarshal failed: %s", err.Error())
} }


if res.StatusCode() == http.StatusBadGateway {
return &result, fmt.Errorf(UnknownErrorPrefix+"createNotebook2 failed(%s): %s", response.ErrorCode, response.ErrorMsg)
}

if len(response.ErrorCode) != 0 {
log.Error("createNotebook2 failed(%s): %s", response.ErrorCode, response.ErrorMsg)
if response.ErrorCode == errorCodeExceedLimit {
response.ErrorMsg = "所选规格使用数量已超过最大配额限制。"
}
if response.ErrorCode == modelartsIllegalToken && retry < 1 {
retry++
_ = getToken()
goto sendjob
if len(result.ErrorCode) != 0 {
log.Error("createNotebook failed(%s): %s", result.ErrorCode, result.ErrorMsg)
if result.ErrorCode == errorCodeExceedLimit {
result.ErrorMsg = "所选规格使用数量已超过最大配额限制。"
} }
return &result, fmt.Errorf("createNotebook2 failed(%s): %s", response.ErrorCode, response.ErrorMsg)
return &result, fmt.Errorf("createNotebook failed(%s): %s", result.ErrorCode, result.ErrorMsg)
} }


return &result, nil return &result, nil


+ 69
- 19
modules/setting/setting.go View File

@@ -75,6 +75,26 @@ type C2NetSqInfos struct {
C2NetSqInfo []*C2NetSequenceInfo `json:"sequence"` C2NetSqInfo []*C2NetSequenceInfo `json:"sequence"`
} }


type StFlavorInfos struct {
FlavorInfo []*FlavorInfo `json:"flavor_info"`
}

type FlavorInfo struct {
Id int `json:"id"`
Value string `json:"value"`
Desc string `json:"desc"`
}

type StImageInfosModelArts struct {
ImageInfo []*ImageInfoModelArts `json:"image_info"`
}

type ImageInfoModelArts struct {
Id string `json:"id"`
Value string `json:"value"`
Desc string `json:"desc"`
}

var ( var (
// AppVer settings // AppVer settings
AppVer string AppVer string
@@ -535,33 +555,29 @@ var (
AllowedOrg string AllowedOrg string
ProfileID string ProfileID string
PoolInfos string PoolInfos string
Flavor string
FlavorInfos string
DebugHost string DebugHost string
ImageInfos string ImageInfos string
Capacity int Capacity int
MaxTempQueryTimes int MaxTempQueryTimes int
StFlavorInfo *StFlavorInfos
StImageInfos *StImageInfosModelArts
//train-job //train-job
ResourcePools string ResourcePools string
Engines string Engines string
EngineVersions string EngineVersions string
FlavorInfos string
TrainJobFLAVORINFOS string TrainJobFLAVORINFOS string
ModelArtsSpecialPools string ModelArtsSpecialPools string


// modelarts-cd config // modelarts-cd config
ModelartsCD = struct { ModelartsCD = struct {
ModelArtsHost string
IamHost string
ProjectID string
ProjectName string
ModelArtsUsername string
ModelArtsPassword string
ModelArtsDomain string
AllowedOrg string
ProfileID string
PoolInfos string
Flavor string
DebugHost string
Enabled bool
EndPoint string
ProjectID string
AccessKey string
SecretKey string
ImageInfos string
FlavorInfos string
}{} }{}


//grampus config //grampus config
@@ -1438,9 +1454,8 @@ func NewContext() {
AllowedOrg = sec.Key("ORGANIZATION").MustString("") AllowedOrg = sec.Key("ORGANIZATION").MustString("")
ProfileID = sec.Key("PROFILE_ID").MustString("") ProfileID = sec.Key("PROFILE_ID").MustString("")
PoolInfos = sec.Key("POOL_INFOS").MustString("") PoolInfos = sec.Key("POOL_INFOS").MustString("")
Flavor = sec.Key("FLAVOR").MustString("")
ImageInfos = sec.Key("IMAGE_INFOS").MustString("") ImageInfos = sec.Key("IMAGE_INFOS").MustString("")
Capacity = sec.Key("IMAGE_INFOS").MustInt(100)
Capacity = sec.Key("CAPACITY").MustInt(100)
MaxTempQueryTimes = sec.Key("MAX_TEMP_QUERY_TIMES").MustInt(30) MaxTempQueryTimes = sec.Key("MAX_TEMP_QUERY_TIMES").MustInt(30)
ResourcePools = sec.Key("Resource_Pools").MustString("") ResourcePools = sec.Key("Resource_Pools").MustString("")
Engines = sec.Key("Engines").MustString("") Engines = sec.Key("Engines").MustString("")
@@ -1488,8 +1503,8 @@ func NewContext() {
Course.OrgName = sec.Key("org_name").MustString("") Course.OrgName = sec.Key("org_name").MustString("")
Course.TeamName = sec.Key("team_name").MustString("") Course.TeamName = sec.Key("team_name").MustString("")


GetGrampusConfig()
getGrampusConfig()
getModelartsCDConfig()
getModelConvertConfig() getModelConvertConfig()
} }


@@ -1512,7 +1527,22 @@ func getModelConvertConfig() {
ModelConvert.NPU_TENSORFLOW_IMAGE_ID = sec.Key("NPU_TENSORFLOW_IMAGE_ID").MustInt(35) ModelConvert.NPU_TENSORFLOW_IMAGE_ID = sec.Key("NPU_TENSORFLOW_IMAGE_ID").MustInt(35)
} }


func GetGrampusConfig() {
func getModelartsCDConfig() {
sec := Cfg.Section("modelarts-cd")

ModelartsCD.Enabled = sec.Key("ENABLED").MustBool(false)
ModelartsCD.EndPoint = sec.Key("ENDPOINT").MustString("https://modelarts.cn-southwest-228.cdzs.cn")
ModelartsCD.ProjectID = sec.Key("PROJECT_ID").MustString("")
ModelartsCD.AccessKey = sec.Key("ACCESS_KEY").MustString("")
ModelartsCD.SecretKey = sec.Key("SECRET_KEY").MustString("")
ModelartsCD.ImageInfos = sec.Key("IMAGE_INFOS").MustString("")
ModelartsCD.FlavorInfos = sec.Key("FLAVOR_INFOS").MustString("")

getNotebookImageInfos()
getNotebookFlavorInfos()
}

func getGrampusConfig() {
sec := Cfg.Section("grampus") sec := Cfg.Section("grampus")


Grampus.Env = sec.Key("ENV").MustString("TEST") Grampus.Env = sec.Key("ENV").MustString("TEST")
@@ -1646,6 +1676,26 @@ func ensureLFSDirectory() {
} }
} }


func getNotebookImageInfos() {
if StImageInfos == nil {
if ModelartsCD.Enabled {
json.Unmarshal([]byte(ModelartsCD.ImageInfos), &StImageInfos)
} else {
json.Unmarshal([]byte(ImageInfos), &StImageInfos)
}
}
}

func getNotebookFlavorInfos() {
if StFlavorInfo == nil {
if ModelartsCD.Enabled {
json.Unmarshal([]byte(ModelartsCD.FlavorInfos), &StFlavorInfo)
} else {
json.Unmarshal([]byte(FlavorInfos), &StFlavorInfo)
}
}
}

// NewServices initializes the services // NewServices initializes the services
func NewServices() { func NewServices() {
InitDBConfig() InitDBConfig()


+ 59
- 110
routers/repo/modelarts.go View File

@@ -2,6 +2,7 @@ package repo


import ( import (
"archive/zip" "archive/zip"
"code.gitea.io/gitea/modules/modelarts_cd"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -60,18 +61,11 @@ func DebugJobIndex(ctx *context.Context) {
if page <= 0 { if page <= 0 {
page = 1 page = 1
} }
typeCloudBrain := models.TypeCloudBrainAll
jobTypeNot := false jobTypeNot := false
if listType == models.GPUResource {
typeCloudBrain = models.TypeCloudBrainOne
} else if listType == models.NPUResource {
typeCloudBrain = models.TypeCloudBrainTwo
} else if listType == models.AllResource {
typeCloudBrain = models.TypeCloudBrainAll
} else {
log.Error("listType(%s) error", listType)
ctx.ServerError("listType error", errors.New("listType error"))
return
var computeResource string
if listType != models.AllResource {
computeResource = listType
} }


var jobTypes []string var jobTypes []string
@@ -81,10 +75,11 @@ func DebugJobIndex(ctx *context.Context) {
Page: page, Page: page,
PageSize: setting.UI.IssuePagingNum, PageSize: setting.UI.IssuePagingNum,
}, },
RepoID: repo.ID,
Type: typeCloudBrain,
JobTypeNot: jobTypeNot,
JobTypes: jobTypes,
RepoID: repo.ID,
ComputeResource: computeResource,
Type: models.TypeCloudBrainAll,
JobTypeNot: jobTypeNot,
JobTypes: jobTypes,
}) })
if err != nil { if err != nil {
ctx.ServerError("Get debugjob faild:", err) ctx.ServerError("Get debugjob faild:", err)
@@ -134,16 +129,8 @@ func notebookNewDataPrepare(ctx *context.Context) error {
return err return err
} }
ctx.Data["attachments"] = attachs ctx.Data["attachments"] = attachs

if modelarts.ImageInfos == nil {
json.Unmarshal([]byte(setting.ImageInfos), &modelarts.ImageInfos)
}
ctx.Data["images"] = modelarts.ImageInfos.ImageInfo

if modelarts.FlavorInfos == nil {
json.Unmarshal([]byte(setting.FlavorInfos), &modelarts.FlavorInfos)
}
ctx.Data["flavors"] = modelarts.FlavorInfos.FlavorInfo
ctx.Data["images"] = setting.StImageInfos.ImageInfo
ctx.Data["flavors"] = setting.StFlavorInfo.FlavorInfo
setSpecBySpecialPoolConfig(ctx, string(models.JobTypeDebug)) setSpecBySpecialPoolConfig(ctx, string(models.JobTypeDebug))


ctx.Data["datasetType"] = models.TypeCloudBrainTwo ctx.Data["datasetType"] = models.TypeCloudBrainTwo
@@ -154,50 +141,6 @@ func notebookNewDataPrepare(ctx *context.Context) error {
return nil return nil
} }


func NotebookCreate(ctx *context.Context, form auth.CreateModelArtsNotebookForm) {
ctx.Data["PageIsNotebook"] = true
jobName := form.JobName
uuid := form.Attachment
description := form.Description
flavor := form.Flavor

count, err := models.GetCloudbrainNotebookCountByUserID(ctx.User.ID)
if err != nil {
log.Error("GetCloudbrainNotebookCountByUserID failed:%v", err, ctx.Data["MsgID"])
cloudBrainNewDataPrepare(ctx)
ctx.RenderWithErr("system error", tplModelArtsNotebookNew, &form)
return
} else {
if count >= 1 {
log.Error("the user already has running or waiting task", ctx.Data["MsgID"])
cloudBrainNewDataPrepare(ctx)
ctx.RenderWithErr("you have already a running or waiting task, can not create more", tplModelArtsNotebookNew, &form)
return
}
}
_, err = models.GetCloudbrainByName(jobName)
if err == nil {
log.Error("the job name did already exist", ctx.Data["MsgID"])
cloudBrainNewDataPrepare(ctx)
ctx.RenderWithErr("the job name did already exist", tplModelArtsNotebookNew, &form)
return
} else {
if !models.IsErrJobNotExist(err) {
log.Error("system error, %v", err, ctx.Data["MsgID"])
cloudBrainNewDataPrepare(ctx)
ctx.RenderWithErr("system error", tplModelArtsNotebookNew, &form)
return
}
}

err = modelarts.GenerateTask(ctx, jobName, uuid, description, flavor)
if err != nil {
ctx.RenderWithErr(err.Error(), tplModelArtsNotebookNew, &form)
return
}
ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/debugjob?debugListType=all")
}

func Notebook2Create(ctx *context.Context, form auth.CreateModelArtsNotebookForm) { func Notebook2Create(ctx *context.Context, form auth.CreateModelArtsNotebookForm) {
ctx.Data["PageIsNotebook"] = true ctx.Data["PageIsNotebook"] = true
displayJobName := form.DisplayJobName displayJobName := form.DisplayJobName
@@ -208,7 +151,12 @@ func Notebook2Create(ctx *context.Context, form auth.CreateModelArtsNotebookForm
imageId := form.ImageId imageId := form.ImageId
repo := ctx.Repo.Repository repo := ctx.Repo.Repository


count, err := models.GetCloudbrainNotebookCountByUserID(ctx.User.ID)
typeCloudbrain := models.TypeCloudBrainTwo
if setting.ModelartsCD.Enabled {
typeCloudbrain = models.TypeCDCenter
}

count, err := models.GetCloudbrainNotebookCountByUserID(ctx.User.ID, typeCloudbrain)
if err != nil { if err != nil {
log.Error("GetCloudbrainNotebookCountByUserID failed:%v", err, ctx.Data["MsgID"]) log.Error("GetCloudbrainNotebookCountByUserID failed:%v", err, ctx.Data["MsgID"])
notebookNewDataPrepare(ctx) notebookNewDataPrepare(ctx)
@@ -247,7 +195,12 @@ func Notebook2Create(ctx *context.Context, form auth.CreateModelArtsNotebookForm
return return
} }


err = modelarts.GenerateNotebook2(ctx, displayJobName, jobName, uuid, description, flavor, imageId)
if setting.ModelartsCD.Enabled {
err = modelarts_cd.GenerateNotebook(ctx, displayJobName, jobName, uuid, description, flavor, imageId)
} else {
err = modelarts.GenerateNotebook2(ctx, displayJobName, jobName, uuid, description, flavor, imageId)
}

if err != nil { if err != nil {
log.Error("GenerateNotebook2 failed, %v", err, ctx.Data["MsgID"]) log.Error("GenerateNotebook2 failed, %v", err, ctx.Data["MsgID"])
notebookNewDataPrepare(ctx) notebookNewDataPrepare(ctx)
@@ -292,14 +245,11 @@ func NotebookShow(ctx *context.Context) {
if err == nil { if err == nil {
task.User = user task.User = user
} }
if modelarts.FlavorInfos == nil {
json.Unmarshal([]byte(setting.FlavorInfos), &modelarts.FlavorInfos)
}


findSpec := false findSpec := false
if modelarts.FlavorInfos != nil {
ctx.Data["resource_spec"] = modelarts.FlavorInfos.FlavorInfo[0].Desc
for _, f := range modelarts.FlavorInfos.FlavorInfo {
if setting.StFlavorInfo != nil {
ctx.Data["resource_spec"] = setting.StFlavorInfo.FlavorInfo[0].Desc
for _, f := range setting.StFlavorInfo.FlavorInfo {
if fmt.Sprint(f.Value) == task.FlavorCode { if fmt.Sprint(f.Value) == task.FlavorCode {
ctx.Data["resource_spec"] = f.Desc ctx.Data["resource_spec"] = f.Desc
findSpec = true findSpec = true
@@ -378,36 +328,16 @@ func setShowSpecBySpecialPoolConfig(ctx *context.Context, findSpec bool, task *m
} }
} }


func NotebookDebug(ctx *context.Context) {
var jobID = ctx.Params(":jobid")

result, err := modelarts.GetJob(jobID)
if err != nil {
ctx.RenderWithErr(err.Error(), tplModelArtsNotebookIndex, nil)
return
}

res, err := modelarts.GetJobToken(jobID)
if err != nil {
ctx.RenderWithErr(err.Error(), tplModelArtsNotebookIndex, nil)
return
}

urls := strings.Split(result.Spec.Annotations.Url, "/")
urlPrefix := result.Spec.Annotations.TargetDomain
for i, url := range urls {
if i > 2 {
urlPrefix += "/" + url
}
}

debugUrl := urlPrefix + "?token=" + res.Token
ctx.Redirect(debugUrl)
}

func NotebookDebug2(ctx *context.Context) { func NotebookDebug2(ctx *context.Context) {
var err error
var result *models.GetNotebook2Result
task := ctx.Cloudbrain task := ctx.Cloudbrain
result, err := modelarts.GetNotebook2(task.JobID)
if task.Type == models.TypeCloudBrainTwo {
result, err = modelarts.GetNotebook2(task.JobID)
} else if task.Type == models.TypeCDCenter {
result, err = modelarts_cd.GetNotebook(task.JobID)
}

if err != nil { if err != nil {
ctx.RenderWithErr(err.Error(), tplModelArtsNotebookIndex, nil) ctx.RenderWithErr(err.Error(), tplModelArtsNotebookIndex, nil)
return return
@@ -435,7 +365,7 @@ func NotebookRestart(ctx *context.Context) {
break break
} }


count, err := models.GetCloudbrainNotebookCountByUserID(ctx.User.ID)
count, err := models.GetCloudbrainNotebookCountByUserID(ctx.User.ID, task.Type)
if err != nil { if err != nil {
log.Error("GetCloudbrainNotebookCountByUserID failed:%v", err, ctx.Data["MsgID"]) log.Error("GetCloudbrainNotebookCountByUserID failed:%v", err, ctx.Data["MsgID"])
errorMsg = "system error" errorMsg = "system error"
@@ -453,7 +383,13 @@ func NotebookRestart(ctx *context.Context) {
Action: models.ActionStart, Action: models.ActionStart,
} }


res, err := modelarts.ManageNotebook2(task.JobID, param)
var res *models.NotebookActionResult
if task.Type == models.TypeCloudBrainTwo {
res, err = modelarts.ManageNotebook2(task.JobID, param)
} else if task.Type == models.TypeCDCenter {
res, err = modelarts_cd.ManageNotebook(task.JobID, param)
}

if err != nil { if err != nil {
log.Error("ManageNotebook2(%s) failed:%v", task.DisplayJobName, err.Error(), ctx.Data["MsgID"]) log.Error("ManageNotebook2(%s) failed:%v", task.DisplayJobName, err.Error(), ctx.Data["MsgID"])
/* 暂不处理再次调试502的场景,详情见方案 /* 暂不处理再次调试502的场景,详情见方案
@@ -537,7 +473,14 @@ func NotebookStop(ctx *context.Context) {
Action: models.ActionStop, Action: models.ActionStop,
} }


res, err := modelarts.ManageNotebook2(task.JobID, param)
var err error
var res *models.NotebookActionResult
if task.Type == models.TypeCloudBrainTwo {
res, err = modelarts.ManageNotebook2(task.JobID, param)
} else if task.Type == models.TypeCDCenter {
res, err = modelarts_cd.ManageNotebook(task.JobID, param)
}

if err != nil { if err != nil {
log.Error("ManageNotebook2(%s) failed:%v", task.JobName, err.Error(), ctx.Data["MsgID"]) log.Error("ManageNotebook2(%s) failed:%v", task.JobName, err.Error(), ctx.Data["MsgID"])
resultCode = "-1" resultCode = "-1"
@@ -587,7 +530,13 @@ func NotebookDel(ctx *context.Context) {
return return
} }


_, err := modelarts.DelNotebook2(task.JobID)
var err error
if task.Type == models.TypeCloudBrainTwo {
_, err = modelarts.DelNotebook2(task.JobID)
} else if task.Type == models.TypeCDCenter {
_, err = modelarts_cd.DelNotebook(task.JobID)
}

if err != nil { if err != nil {
log.Error("DelNotebook2(%s) failed:%v", task.JobName, err.Error()) log.Error("DelNotebook2(%s) failed:%v", task.JobName, err.Error())
if strings.Contains(err.Error(), modelarts.NotebookNotFound) || strings.Contains(err.Error(), modelarts.NotebookNoPermission) || strings.Contains(err.Error(), modelarts.NotebookInvalid) { if strings.Contains(err.Error(), modelarts.NotebookNotFound) || strings.Contains(err.Error(), modelarts.NotebookNoPermission) || strings.Contains(err.Error(), modelarts.NotebookInvalid) {
@@ -2187,7 +2136,7 @@ func checkModelArtsSpecialPool(ctx *context.Context, flavorCode string, jobType
if !isMatchPool { if !isMatchPool {
isMatchSpec := false isMatchSpec := false
if jobType == string(models.JobTypeDebug) { if jobType == string(models.JobTypeDebug) {
for _, flavor := range modelarts.FlavorInfos.FlavorInfo {
for _, flavor := range setting.StFlavorInfo.FlavorInfo {
if flavor.Value == flavorCode { if flavor.Value == flavorCode {
isMatchSpec = true isMatchSpec = true
break break


Loading…
Cancel
Save