feat: wire mysql delivery to awx callbacks
This commit is contained in:
@@ -13,6 +13,8 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -72,6 +74,12 @@ type DeliveryTarget struct {
|
||||
Metadata string `json:"metadata"`
|
||||
}
|
||||
|
||||
type DeliveryMountPath struct {
|
||||
Path string `json:"path"`
|
||||
AvailableGi int64 `json:"available_gi"`
|
||||
FSType string `json:"fstype,omitempty"`
|
||||
}
|
||||
|
||||
type DeliveryStageEventInput struct {
|
||||
Stage string `json:"stage" binding:"required"`
|
||||
Status string `json:"status" binding:"required"`
|
||||
@@ -125,6 +133,81 @@ func parseTargetMetadata(raw string) targetMetadata {
|
||||
return meta
|
||||
}
|
||||
|
||||
func mountPathsFromFacts(facts map[string]any) []DeliveryMountPath {
|
||||
rawMounts, ok := facts["ansible_mounts"].([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
items := make([]DeliveryMountPath, 0, len(rawMounts))
|
||||
seen := map[string]struct{}{}
|
||||
for _, raw := range rawMounts {
|
||||
mount, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
path := strings.TrimSpace(stringValue(mount["mount"]))
|
||||
if path == "" || !strings.HasPrefix(path, "/") {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[path]; exists {
|
||||
continue
|
||||
}
|
||||
seen[path] = struct{}{}
|
||||
items = append(items, DeliveryMountPath{
|
||||
Path: path,
|
||||
AvailableGi: bytesToGi(int64Value(mount["size_available"])),
|
||||
FSType: strings.TrimSpace(stringValue(mount["fstype"])),
|
||||
})
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
if items[i].Path == "/" {
|
||||
return false
|
||||
}
|
||||
if items[j].Path == "/" {
|
||||
return true
|
||||
}
|
||||
return items[i].Path < items[j].Path
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
func stringValue(value any) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return v
|
||||
case fmt.Stringer:
|
||||
return v.String()
|
||||
default:
|
||||
return fmt.Sprintf("%v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func int64Value(value any) int64 {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
return int64(v)
|
||||
case int64:
|
||||
return v
|
||||
case float64:
|
||||
return int64(v)
|
||||
case json.Number:
|
||||
n, _ := v.Int64()
|
||||
return n
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func bytesToGi(bytes int64) int64 {
|
||||
if bytes <= 0 {
|
||||
return 0
|
||||
}
|
||||
return bytes / 1073741824
|
||||
}
|
||||
|
||||
// firstFreeHost 返回候选池中非失败任务数未达单机实例上限的第一个节点。
|
||||
func firstFreeHost(hosts []targetHost, occupied []string, limit int) *targetHost {
|
||||
if limit < 1 {
|
||||
@@ -222,6 +305,64 @@ func (s *DeliveryService) getTarget(ctx context.Context, templateID uint64) (Del
|
||||
return s.awxDeliveryTarget(ctx, *template)
|
||||
}
|
||||
|
||||
func (s *DeliveryService) ListHostMountPaths(ctx context.Context, targetID uint64, hostName string) ([]DeliveryMountPath, error) {
|
||||
if targetID == 0 {
|
||||
return nil, fmt.Errorf("target_id is required")
|
||||
}
|
||||
if hostName == "" || len(hostName) > 253 || !hostNamePattern.MatchString(hostName) {
|
||||
return nil, fmt.Errorf("host must be a valid inventory host name")
|
||||
}
|
||||
template, err := s.awx.GetJobTemplate(ctx, targetID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("deployment target is unavailable: %w", err)
|
||||
}
|
||||
hosts, err := s.awx.ListInventoryHosts(ctx, template.Inventory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var matched *AWXInventoryHost
|
||||
for i := range hosts {
|
||||
if hosts[i].Enabled && hosts[i].Name == hostName {
|
||||
matched = &hosts[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if matched == nil {
|
||||
return nil, fmt.Errorf("host %q is not in the deployment target inventory", hostName)
|
||||
}
|
||||
if s.cfg.AWXFactsTemplateID != 0 {
|
||||
if err := s.refreshHostFacts(ctx, hostName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
facts, err := s.awx.GetHostFacts(ctx, matched.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := mountPathsFromFacts(facts)
|
||||
if items == nil {
|
||||
items = []DeliveryMountPath{}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *DeliveryService) refreshHostFacts(ctx context.Context, hostName string) error {
|
||||
job, err := s.awx.Launch(ctx, s.cfg.AWXFactsTemplateID, AWXLaunchRequest{
|
||||
Limit: hostName,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("launch AWX facts job: %w", err)
|
||||
}
|
||||
done, err := s.awx.WaitJob(ctx, strconv.FormatUint(job.ID, 10), time.Duration(s.cfg.AWXFactsTimeoutSeconds)*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if done.Status != "successful" || done.Failed {
|
||||
return fmt.Errorf("AWX facts job %d finished with status %s", done.ID, done.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DeliveryService) awxDeliveryTarget(ctx context.Context, template AWXJobTemplate) (DeliveryTarget, error) {
|
||||
if !template.AskVariablesOnLaunch || !template.AskLimitOnLaunch {
|
||||
return DeliveryTarget{}, fmt.Errorf("AWX job template %d must enable Prompt on launch for Variables and Limit", template.ID)
|
||||
@@ -424,22 +565,6 @@ func validateDeliveryInput(input MySQLDeliveryInput, dataDisks []string) error {
|
||||
if input.MySQLPort != 0 && (input.MySQLPort < mysqlPortPoolStart || input.MySQLPort > mysqlPortPoolEnd) {
|
||||
return fmt.Errorf("mysql_port must be empty for auto assignment or between %d and %d", mysqlPortPoolStart, mysqlPortPoolEnd)
|
||||
}
|
||||
if input.DataDisk != "" {
|
||||
allowed := dataDisks
|
||||
if len(allowed) == 0 {
|
||||
allowed = []string{"/data"}
|
||||
}
|
||||
found := false
|
||||
for _, disk := range allowed {
|
||||
if input.DataDisk == disk {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("data_disk %q is not in the allowed mount point list %v", input.DataDisk, allowed)
|
||||
}
|
||||
}
|
||||
if input.TargetHost != "" && (len(input.TargetHost) > 253 || !hostNamePattern.MatchString(input.TargetHost)) {
|
||||
return fmt.Errorf("target_host must be a valid inventory host name")
|
||||
}
|
||||
@@ -889,7 +1014,7 @@ func (s *DeliveryService) CreateExecution(ctx context.Context, taskID, payloadHa
|
||||
"target_hosts": task.TargetHost, "topology": topology,
|
||||
"instance_name": payload.InstanceName, "mysql_port": task.MySQLPort,
|
||||
"data_disk": payload.DataDisk, "cpu_cores": payload.CPUCores,
|
||||
"memory_gb": payload.MemoryGB, "storage_gb": payload.StorageGB,
|
||||
"memory_mb": payload.MemoryMi, "storage_gb": payload.StorageGi,
|
||||
"cpu_milli": payload.CPUMilli, "memory_mi": payload.MemoryMi, "storage_gi": payload.StorageGi,
|
||||
"mysql_version": payload.MySQLVersion, "param_template": payload.ParamTemplate,
|
||||
"target_host": payload.TargetHost,
|
||||
|
||||
Reference in New Issue
Block a user