|
|
@@ -0,0 +1,781 @@
|
|
|
+package controller
|
|
|
+
|
|
|
+import (
|
|
|
+ "fmt"
|
|
|
+ "strconv"
|
|
|
+ "strings"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "github.com/QuantumNous/new-api/common"
|
|
|
+ "github.com/QuantumNous/new-api/pkg/ionet"
|
|
|
+ "github.com/gin-gonic/gin"
|
|
|
+)
|
|
|
+
|
|
|
+func getIoAPIKey(c *gin.Context) (string, bool) {
|
|
|
+ common.OptionMapRWMutex.RLock()
|
|
|
+ enabled := common.OptionMap["model_deployment.ionet.enabled"] == "true"
|
|
|
+ apiKey := common.OptionMap["model_deployment.ionet.api_key"]
|
|
|
+ common.OptionMapRWMutex.RUnlock()
|
|
|
+ if !enabled || strings.TrimSpace(apiKey) == "" {
|
|
|
+ common.ApiErrorMsg(c, "io.net model deployment is not enabled or api key missing")
|
|
|
+ return "", false
|
|
|
+ }
|
|
|
+ return apiKey, true
|
|
|
+}
|
|
|
+
|
|
|
+func getIoClient(c *gin.Context) (*ionet.Client, bool) {
|
|
|
+ apiKey, ok := getIoAPIKey(c)
|
|
|
+ if !ok {
|
|
|
+ return nil, false
|
|
|
+ }
|
|
|
+ return ionet.NewClient(apiKey), true
|
|
|
+}
|
|
|
+
|
|
|
+func getIoEnterpriseClient(c *gin.Context) (*ionet.Client, bool) {
|
|
|
+ apiKey, ok := getIoAPIKey(c)
|
|
|
+ if !ok {
|
|
|
+ return nil, false
|
|
|
+ }
|
|
|
+ return ionet.NewEnterpriseClient(apiKey), true
|
|
|
+}
|
|
|
+
|
|
|
+func TestIoNetConnection(c *gin.Context) {
|
|
|
+ var req struct {
|
|
|
+ APIKey string `json:"api_key"`
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
+ common.ApiErrorMsg(c, "invalid request payload")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ apiKey := strings.TrimSpace(req.APIKey)
|
|
|
+ if apiKey == "" {
|
|
|
+ common.ApiErrorMsg(c, "api_key is required")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ client := ionet.NewEnterpriseClient(apiKey)
|
|
|
+ result, err := client.GetMaxGPUsPerContainer()
|
|
|
+ if err != nil {
|
|
|
+ if apiErr, ok := err.(*ionet.APIError); ok {
|
|
|
+ message := strings.TrimSpace(apiErr.Message)
|
|
|
+ if message == "" {
|
|
|
+ message = "failed to validate api key"
|
|
|
+ }
|
|
|
+ common.ApiErrorMsg(c, message)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ totalHardware := 0
|
|
|
+ totalAvailable := 0
|
|
|
+ if result != nil {
|
|
|
+ totalHardware = len(result.Hardware)
|
|
|
+ totalAvailable = result.Total
|
|
|
+ if totalAvailable == 0 {
|
|
|
+ for _, hw := range result.Hardware {
|
|
|
+ totalAvailable += hw.Available
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ common.ApiSuccess(c, gin.H{
|
|
|
+ "hardware_count": totalHardware,
|
|
|
+ "total_available": totalAvailable,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func requireDeploymentID(c *gin.Context) (string, bool) {
|
|
|
+ deploymentID := strings.TrimSpace(c.Param("id"))
|
|
|
+ if deploymentID == "" {
|
|
|
+ common.ApiErrorMsg(c, "deployment ID is required")
|
|
|
+ return "", false
|
|
|
+ }
|
|
|
+ return deploymentID, true
|
|
|
+}
|
|
|
+
|
|
|
+func requireContainerID(c *gin.Context) (string, bool) {
|
|
|
+ containerID := strings.TrimSpace(c.Param("container_id"))
|
|
|
+ if containerID == "" {
|
|
|
+ common.ApiErrorMsg(c, "container ID is required")
|
|
|
+ return "", false
|
|
|
+ }
|
|
|
+ return containerID, true
|
|
|
+}
|
|
|
+
|
|
|
+func mapIoNetDeployment(d ionet.Deployment) map[string]interface{} {
|
|
|
+ var created int64
|
|
|
+ if d.CreatedAt.IsZero() {
|
|
|
+ created = time.Now().Unix()
|
|
|
+ } else {
|
|
|
+ created = d.CreatedAt.Unix()
|
|
|
+ }
|
|
|
+
|
|
|
+ timeRemainingHours := d.ComputeMinutesRemaining / 60
|
|
|
+ timeRemainingMins := d.ComputeMinutesRemaining % 60
|
|
|
+ var timeRemaining string
|
|
|
+ if timeRemainingHours > 0 {
|
|
|
+ timeRemaining = fmt.Sprintf("%d hour %d minutes", timeRemainingHours, timeRemainingMins)
|
|
|
+ } else if timeRemainingMins > 0 {
|
|
|
+ timeRemaining = fmt.Sprintf("%d minutes", timeRemainingMins)
|
|
|
+ } else {
|
|
|
+ timeRemaining = "completed"
|
|
|
+ }
|
|
|
+
|
|
|
+ hardwareInfo := fmt.Sprintf("%s %s x%d", d.BrandName, d.HardwareName, d.HardwareQuantity)
|
|
|
+
|
|
|
+ return map[string]interface{}{
|
|
|
+ "id": d.ID,
|
|
|
+ "deployment_name": d.Name,
|
|
|
+ "container_name": d.Name,
|
|
|
+ "status": strings.ToLower(d.Status),
|
|
|
+ "type": "Container",
|
|
|
+ "time_remaining": timeRemaining,
|
|
|
+ "time_remaining_minutes": d.ComputeMinutesRemaining,
|
|
|
+ "hardware_info": hardwareInfo,
|
|
|
+ "hardware_name": d.HardwareName,
|
|
|
+ "brand_name": d.BrandName,
|
|
|
+ "hardware_quantity": d.HardwareQuantity,
|
|
|
+ "completed_percent": d.CompletedPercent,
|
|
|
+ "compute_minutes_served": d.ComputeMinutesServed,
|
|
|
+ "compute_minutes_remaining": d.ComputeMinutesRemaining,
|
|
|
+ "created_at": created,
|
|
|
+ "updated_at": created,
|
|
|
+ "model_name": "",
|
|
|
+ "model_version": "",
|
|
|
+ "instance_count": d.HardwareQuantity,
|
|
|
+ "resource_config": map[string]interface{}{
|
|
|
+ "cpu": "",
|
|
|
+ "memory": "",
|
|
|
+ "gpu": strconv.Itoa(d.HardwareQuantity),
|
|
|
+ },
|
|
|
+ "description": "",
|
|
|
+ "provider": "io.net",
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func computeStatusCounts(total int, deployments []ionet.Deployment) map[string]int64 {
|
|
|
+ counts := map[string]int64{
|
|
|
+ "all": int64(total),
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, status := range []string{"running", "completed", "failed", "deployment requested", "termination requested", "destroyed"} {
|
|
|
+ counts[status] = 0
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, d := range deployments {
|
|
|
+ status := strings.ToLower(strings.TrimSpace(d.Status))
|
|
|
+ counts[status] = counts[status] + 1
|
|
|
+ }
|
|
|
+
|
|
|
+ return counts
|
|
|
+}
|
|
|
+
|
|
|
+func GetAllDeployments(c *gin.Context) {
|
|
|
+ pageInfo := common.GetPageQuery(c)
|
|
|
+ client, ok := getIoEnterpriseClient(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ status := c.Query("status")
|
|
|
+ opts := &ionet.ListDeploymentsOptions{
|
|
|
+ Status: strings.ToLower(strings.TrimSpace(status)),
|
|
|
+ Page: pageInfo.GetPage(),
|
|
|
+ PageSize: pageInfo.GetPageSize(),
|
|
|
+ SortBy: "created_at",
|
|
|
+ SortOrder: "desc",
|
|
|
+ }
|
|
|
+
|
|
|
+ dl, err := client.ListDeployments(opts)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ items := make([]map[string]interface{}, 0, len(dl.Deployments))
|
|
|
+ for _, d := range dl.Deployments {
|
|
|
+ items = append(items, mapIoNetDeployment(d))
|
|
|
+ }
|
|
|
+
|
|
|
+ data := gin.H{
|
|
|
+ "page": pageInfo.GetPage(),
|
|
|
+ "page_size": pageInfo.GetPageSize(),
|
|
|
+ "total": dl.Total,
|
|
|
+ "items": items,
|
|
|
+ "status_counts": computeStatusCounts(dl.Total, dl.Deployments),
|
|
|
+ }
|
|
|
+ common.ApiSuccess(c, data)
|
|
|
+}
|
|
|
+
|
|
|
+func SearchDeployments(c *gin.Context) {
|
|
|
+ pageInfo := common.GetPageQuery(c)
|
|
|
+ client, ok := getIoEnterpriseClient(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ status := strings.ToLower(strings.TrimSpace(c.Query("status")))
|
|
|
+ keyword := strings.TrimSpace(c.Query("keyword"))
|
|
|
+
|
|
|
+ dl, err := client.ListDeployments(&ionet.ListDeploymentsOptions{
|
|
|
+ Status: status,
|
|
|
+ Page: pageInfo.GetPage(),
|
|
|
+ PageSize: pageInfo.GetPageSize(),
|
|
|
+ SortBy: "created_at",
|
|
|
+ SortOrder: "desc",
|
|
|
+ })
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ filtered := make([]ionet.Deployment, 0, len(dl.Deployments))
|
|
|
+ if keyword == "" {
|
|
|
+ filtered = dl.Deployments
|
|
|
+ } else {
|
|
|
+ kw := strings.ToLower(keyword)
|
|
|
+ for _, d := range dl.Deployments {
|
|
|
+ if strings.Contains(strings.ToLower(d.Name), kw) {
|
|
|
+ filtered = append(filtered, d)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ items := make([]map[string]interface{}, 0, len(filtered))
|
|
|
+ for _, d := range filtered {
|
|
|
+ items = append(items, mapIoNetDeployment(d))
|
|
|
+ }
|
|
|
+
|
|
|
+ total := dl.Total
|
|
|
+ if keyword != "" {
|
|
|
+ total = len(filtered)
|
|
|
+ }
|
|
|
+
|
|
|
+ data := gin.H{
|
|
|
+ "page": pageInfo.GetPage(),
|
|
|
+ "page_size": pageInfo.GetPageSize(),
|
|
|
+ "total": total,
|
|
|
+ "items": items,
|
|
|
+ }
|
|
|
+ common.ApiSuccess(c, data)
|
|
|
+}
|
|
|
+
|
|
|
+func GetDeployment(c *gin.Context) {
|
|
|
+ client, ok := getIoEnterpriseClient(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ deploymentID, ok := requireDeploymentID(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ details, err := client.GetDeployment(deploymentID)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ data := map[string]interface{}{
|
|
|
+ "id": details.ID,
|
|
|
+ "deployment_name": details.ID,
|
|
|
+ "model_name": "",
|
|
|
+ "model_version": "",
|
|
|
+ "status": strings.ToLower(details.Status),
|
|
|
+ "instance_count": details.TotalContainers,
|
|
|
+ "hardware_id": details.HardwareID,
|
|
|
+ "resource_config": map[string]interface{}{
|
|
|
+ "cpu": "",
|
|
|
+ "memory": "",
|
|
|
+ "gpu": strconv.Itoa(details.TotalGPUs),
|
|
|
+ },
|
|
|
+ "created_at": details.CreatedAt.Unix(),
|
|
|
+ "updated_at": details.CreatedAt.Unix(),
|
|
|
+ "description": "",
|
|
|
+ "amount_paid": details.AmountPaid,
|
|
|
+ "completed_percent": details.CompletedPercent,
|
|
|
+ "gpus_per_container": details.GPUsPerContainer,
|
|
|
+ "total_gpus": details.TotalGPUs,
|
|
|
+ "total_containers": details.TotalContainers,
|
|
|
+ "hardware_name": details.HardwareName,
|
|
|
+ "brand_name": details.BrandName,
|
|
|
+ "compute_minutes_served": details.ComputeMinutesServed,
|
|
|
+ "compute_minutes_remaining": details.ComputeMinutesRemaining,
|
|
|
+ "locations": details.Locations,
|
|
|
+ "container_config": details.ContainerConfig,
|
|
|
+ }
|
|
|
+
|
|
|
+ common.ApiSuccess(c, data)
|
|
|
+}
|
|
|
+
|
|
|
+func UpdateDeploymentName(c *gin.Context) {
|
|
|
+ client, ok := getIoEnterpriseClient(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ deploymentID, ok := requireDeploymentID(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ var req struct {
|
|
|
+ Name string `json:"name" binding:"required"`
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ updateReq := &ionet.UpdateClusterNameRequest{
|
|
|
+ Name: strings.TrimSpace(req.Name),
|
|
|
+ }
|
|
|
+
|
|
|
+ if updateReq.Name == "" {
|
|
|
+ common.ApiErrorMsg(c, "deployment name cannot be empty")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ available, err := client.CheckClusterNameAvailability(updateReq.Name)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, fmt.Errorf("failed to check name availability: %w", err))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if !available {
|
|
|
+ common.ApiErrorMsg(c, "deployment name is not available, please choose a different name")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ resp, err := client.UpdateClusterName(deploymentID, updateReq)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ data := gin.H{
|
|
|
+ "status": resp.Status,
|
|
|
+ "message": resp.Message,
|
|
|
+ "id": deploymentID,
|
|
|
+ "name": updateReq.Name,
|
|
|
+ }
|
|
|
+ common.ApiSuccess(c, data)
|
|
|
+}
|
|
|
+
|
|
|
+func UpdateDeployment(c *gin.Context) {
|
|
|
+ client, ok := getIoEnterpriseClient(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ deploymentID, ok := requireDeploymentID(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ var req ionet.UpdateDeploymentRequest
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ resp, err := client.UpdateDeployment(deploymentID, &req)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ data := gin.H{
|
|
|
+ "status": resp.Status,
|
|
|
+ "deployment_id": resp.DeploymentID,
|
|
|
+ }
|
|
|
+ common.ApiSuccess(c, data)
|
|
|
+}
|
|
|
+
|
|
|
+func ExtendDeployment(c *gin.Context) {
|
|
|
+ client, ok := getIoEnterpriseClient(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ deploymentID, ok := requireDeploymentID(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ var req ionet.ExtendDurationRequest
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ details, err := client.ExtendDeployment(deploymentID, &req)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ data := mapIoNetDeployment(ionet.Deployment{
|
|
|
+ ID: details.ID,
|
|
|
+ Status: details.Status,
|
|
|
+ Name: deploymentID,
|
|
|
+ CompletedPercent: float64(details.CompletedPercent),
|
|
|
+ HardwareQuantity: details.TotalGPUs,
|
|
|
+ BrandName: details.BrandName,
|
|
|
+ HardwareName: details.HardwareName,
|
|
|
+ ComputeMinutesServed: details.ComputeMinutesServed,
|
|
|
+ ComputeMinutesRemaining: details.ComputeMinutesRemaining,
|
|
|
+ CreatedAt: details.CreatedAt,
|
|
|
+ })
|
|
|
+
|
|
|
+ common.ApiSuccess(c, data)
|
|
|
+}
|
|
|
+
|
|
|
+func DeleteDeployment(c *gin.Context) {
|
|
|
+ client, ok := getIoEnterpriseClient(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ deploymentID, ok := requireDeploymentID(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ resp, err := client.DeleteDeployment(deploymentID)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ data := gin.H{
|
|
|
+ "status": resp.Status,
|
|
|
+ "deployment_id": resp.DeploymentID,
|
|
|
+ "message": "Deployment termination requested successfully",
|
|
|
+ }
|
|
|
+ common.ApiSuccess(c, data)
|
|
|
+}
|
|
|
+
|
|
|
+func CreateDeployment(c *gin.Context) {
|
|
|
+ client, ok := getIoEnterpriseClient(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ var req ionet.DeploymentRequest
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ resp, err := client.DeployContainer(&req)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ data := gin.H{
|
|
|
+ "deployment_id": resp.DeploymentID,
|
|
|
+ "status": resp.Status,
|
|
|
+ "message": "Deployment created successfully",
|
|
|
+ }
|
|
|
+ common.ApiSuccess(c, data)
|
|
|
+}
|
|
|
+
|
|
|
+func GetHardwareTypes(c *gin.Context) {
|
|
|
+ client, ok := getIoEnterpriseClient(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ hardwareTypes, totalAvailable, err := client.ListHardwareTypes()
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ data := gin.H{
|
|
|
+ "hardware_types": hardwareTypes,
|
|
|
+ "total": len(hardwareTypes),
|
|
|
+ "total_available": totalAvailable,
|
|
|
+ }
|
|
|
+ common.ApiSuccess(c, data)
|
|
|
+}
|
|
|
+
|
|
|
+func GetLocations(c *gin.Context) {
|
|
|
+ client, ok := getIoClient(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ locationsResp, err := client.ListLocations()
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ total := locationsResp.Total
|
|
|
+ if total == 0 {
|
|
|
+ total = len(locationsResp.Locations)
|
|
|
+ }
|
|
|
+
|
|
|
+ data := gin.H{
|
|
|
+ "locations": locationsResp.Locations,
|
|
|
+ "total": total,
|
|
|
+ }
|
|
|
+ common.ApiSuccess(c, data)
|
|
|
+}
|
|
|
+
|
|
|
+func GetAvailableReplicas(c *gin.Context) {
|
|
|
+ client, ok := getIoEnterpriseClient(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ hardwareIDStr := c.Query("hardware_id")
|
|
|
+ gpuCountStr := c.Query("gpu_count")
|
|
|
+
|
|
|
+ if hardwareIDStr == "" {
|
|
|
+ common.ApiErrorMsg(c, "hardware_id parameter is required")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ hardwareID, err := strconv.Atoi(hardwareIDStr)
|
|
|
+ if err != nil || hardwareID <= 0 {
|
|
|
+ common.ApiErrorMsg(c, "invalid hardware_id parameter")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ gpuCount := 1
|
|
|
+ if gpuCountStr != "" {
|
|
|
+ if parsed, err := strconv.Atoi(gpuCountStr); err == nil && parsed > 0 {
|
|
|
+ gpuCount = parsed
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ replicas, err := client.GetAvailableReplicas(hardwareID, gpuCount)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ common.ApiSuccess(c, replicas)
|
|
|
+}
|
|
|
+
|
|
|
+func GetPriceEstimation(c *gin.Context) {
|
|
|
+ client, ok := getIoEnterpriseClient(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ var req ionet.PriceEstimationRequest
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ priceResp, err := client.GetPriceEstimation(&req)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ common.ApiSuccess(c, priceResp)
|
|
|
+}
|
|
|
+
|
|
|
+func CheckClusterNameAvailability(c *gin.Context) {
|
|
|
+ client, ok := getIoEnterpriseClient(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ clusterName := strings.TrimSpace(c.Query("name"))
|
|
|
+ if clusterName == "" {
|
|
|
+ common.ApiErrorMsg(c, "name parameter is required")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ available, err := client.CheckClusterNameAvailability(clusterName)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ data := gin.H{
|
|
|
+ "available": available,
|
|
|
+ "name": clusterName,
|
|
|
+ }
|
|
|
+ common.ApiSuccess(c, data)
|
|
|
+}
|
|
|
+
|
|
|
+func GetDeploymentLogs(c *gin.Context) {
|
|
|
+ client, ok := getIoClient(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ deploymentID, ok := requireDeploymentID(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ containerID := c.Query("container_id")
|
|
|
+ if containerID == "" {
|
|
|
+ common.ApiErrorMsg(c, "container_id parameter is required")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ level := c.Query("level")
|
|
|
+ stream := c.Query("stream")
|
|
|
+ cursor := c.Query("cursor")
|
|
|
+ limitStr := c.Query("limit")
|
|
|
+ follow := c.Query("follow") == "true"
|
|
|
+
|
|
|
+ var limit int = 100
|
|
|
+ if limitStr != "" {
|
|
|
+ if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
|
|
|
+ limit = parsedLimit
|
|
|
+ if limit > 1000 {
|
|
|
+ limit = 1000
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ opts := &ionet.GetLogsOptions{
|
|
|
+ Level: level,
|
|
|
+ Stream: stream,
|
|
|
+ Limit: limit,
|
|
|
+ Cursor: cursor,
|
|
|
+ Follow: follow,
|
|
|
+ }
|
|
|
+
|
|
|
+ if startTime := c.Query("start_time"); startTime != "" {
|
|
|
+ if t, err := time.Parse(time.RFC3339, startTime); err == nil {
|
|
|
+ opts.StartTime = &t
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if endTime := c.Query("end_time"); endTime != "" {
|
|
|
+ if t, err := time.Parse(time.RFC3339, endTime); err == nil {
|
|
|
+ opts.EndTime = &t
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ rawLogs, err := client.GetContainerLogsRaw(deploymentID, containerID, opts)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ common.ApiSuccess(c, rawLogs)
|
|
|
+}
|
|
|
+
|
|
|
+func ListDeploymentContainers(c *gin.Context) {
|
|
|
+ client, ok := getIoEnterpriseClient(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ deploymentID, ok := requireDeploymentID(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ containers, err := client.ListContainers(deploymentID)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ items := make([]map[string]interface{}, 0)
|
|
|
+ if containers != nil {
|
|
|
+ items = make([]map[string]interface{}, 0, len(containers.Workers))
|
|
|
+ for _, ctr := range containers.Workers {
|
|
|
+ events := make([]map[string]interface{}, 0, len(ctr.ContainerEvents))
|
|
|
+ for _, event := range ctr.ContainerEvents {
|
|
|
+ events = append(events, map[string]interface{}{
|
|
|
+ "time": event.Time.Unix(),
|
|
|
+ "message": event.Message,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ items = append(items, map[string]interface{}{
|
|
|
+ "container_id": ctr.ContainerID,
|
|
|
+ "device_id": ctr.DeviceID,
|
|
|
+ "status": strings.ToLower(strings.TrimSpace(ctr.Status)),
|
|
|
+ "hardware": ctr.Hardware,
|
|
|
+ "brand_name": ctr.BrandName,
|
|
|
+ "created_at": ctr.CreatedAt.Unix(),
|
|
|
+ "uptime_percent": ctr.UptimePercent,
|
|
|
+ "gpus_per_container": ctr.GPUsPerContainer,
|
|
|
+ "public_url": ctr.PublicURL,
|
|
|
+ "events": events,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ response := gin.H{
|
|
|
+ "total": 0,
|
|
|
+ "containers": items,
|
|
|
+ }
|
|
|
+ if containers != nil {
|
|
|
+ response["total"] = containers.Total
|
|
|
+ }
|
|
|
+
|
|
|
+ common.ApiSuccess(c, response)
|
|
|
+}
|
|
|
+
|
|
|
+func GetContainerDetails(c *gin.Context) {
|
|
|
+ client, ok := getIoEnterpriseClient(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ deploymentID, ok := requireDeploymentID(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ containerID, ok := requireContainerID(c)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ details, err := client.GetContainerDetails(deploymentID, containerID)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if details == nil {
|
|
|
+ common.ApiErrorMsg(c, "container details not found")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ events := make([]map[string]interface{}, 0, len(details.ContainerEvents))
|
|
|
+ for _, event := range details.ContainerEvents {
|
|
|
+ events = append(events, map[string]interface{}{
|
|
|
+ "time": event.Time.Unix(),
|
|
|
+ "message": event.Message,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ data := gin.H{
|
|
|
+ "deployment_id": deploymentID,
|
|
|
+ "container_id": details.ContainerID,
|
|
|
+ "device_id": details.DeviceID,
|
|
|
+ "status": strings.ToLower(strings.TrimSpace(details.Status)),
|
|
|
+ "hardware": details.Hardware,
|
|
|
+ "brand_name": details.BrandName,
|
|
|
+ "created_at": details.CreatedAt.Unix(),
|
|
|
+ "uptime_percent": details.UptimePercent,
|
|
|
+ "gpus_per_container": details.GPUsPerContainer,
|
|
|
+ "public_url": details.PublicURL,
|
|
|
+ "events": events,
|
|
|
+ }
|
|
|
+
|
|
|
+ common.ApiSuccess(c, data)
|
|
|
+}
|