diff --git a/agent-go/a_executor/BaseFunction.go b/agent-go/a_executor/BaseFunction.go index 898f672..6fc4169 100644 --- a/agent-go/a_executor/BaseFunction.go +++ b/agent-go/a_executor/BaseFunction.go @@ -214,6 +214,23 @@ func (op *AgentOsOperator) shutdownFirewall() [][]string { return shutdownFunc } +func (op *AgentOsOperator) shutdownFirewallBastion() (bool, []string) { + + shutdownFunc := [][]string{ + {"systemctl", "stop", "firewalld"}, + {"systemctl", "disable", "firewalld"}, + {"systemctl", "stop", "ufw"}, + {"systemctl", "disable", "ufw"}, + {"iptables", "-F"}, + } + // 忽略错误 + _, resultLog := AllCompleteExecutor(shutdownFunc) + + // centos + + return true, resultLog +} + func (op *AgentOsOperator) shutdownFirewallExec() (bool, []string) { shutdownFunc := [][]string{ @@ -838,7 +855,7 @@ func (op *AgentOsOperator) installDockerOfflineExec(args []string) (bool, []stri dockerOfflineFileName = "docker-arm64-20.10.15.tgz" } - ok, resultLog := BasicDownloadFileByCurl(op.OssOfflinePrefix+dockerOfflineFileName, "/root/wdd/"+dockerOfflineFileName) + ok, resultLog := BasicDownloadFile(op.OssOfflinePrefix+dockerOfflineFileName, "", "", "", "/root/wdd/"+dockerOfflineFileName) if !ok { return false, resultLog } @@ -932,10 +949,48 @@ func (op *AgentOsOperator) installDockerOfflineExec(args []string) (bool, []stri "[installDockerOfflineExec] - docker offline installation success!", } } -func (op *AgentOsOperator) InstallDockerFromLocalExec(args []string) (bool, []string) { +func (op *AgentOsOperator) InstallDockerBastion() (bool, []string) { + + // check offline file exits + log.InfoF("[InstallDockerBastion] - install docker 20.10.15 by local file method !") + BasicCreateFolder("/root/wdd") + + // install docker + var dockerOfflineFileName string + if strings.HasPrefix(op.AgentArch, "amd") { + dockerOfflineFileName = "docker-amd64-20.10.15.tgz" + } else if strings.HasPrefix(op.AgentArch, "arm64") { + dockerOfflineFileName = "docker-arm64-20.10.15.tgz" + } + + BasicRemoveFolderComplete("/root/wdd/docker") + + dockerLocalFile := "/root/wdd/" + dockerOfflineFileName + if !BasicFileExistAndNotNull(dockerLocalFile) { + sprintf := fmt.Sprintf("docker offline file not exists ! => %s", dockerLocalFile) + log.Error(sprintf) + return false, []string{sprintf} + } + + PureResultSingleExecute([]string{ + "tar", + "-vxf", + dockerLocalFile, + "-C", + "/root/wdd", + }) + + HardCodeCommandExecutor("chmod 777 -R /root/wdd/docker/*") + + resultOk, l := HardCodeCommandExecutor("mv /root/wdd/docker/* /usr/bin") + if !resultOk { + return false, append(l, "[InstallDockerBastion] - cp docker executable file error!") + } + + // daemon docker return true, []string{ - "[installDockerFromLocalExec] - docker offline installation from local success!", + "[InstallDockerBastion] - docker offline installation from local success!", } } @@ -1025,7 +1080,7 @@ func (op *AgentOsOperator) installDockerComposeExec() (bool, []string) { return true, []string{"docker-compose安装成功!"} } -func (op *AgentOsOperator) InstallDockerComposeFromLocalExec() (bool, []string) { +func (op *AgentOsOperator) InstallDockerComposeBastion() (bool, []string) { return true, []string{ "[installDockerComposeFromLocalExec] - docker-compose offline installation from local success!", } @@ -2126,6 +2181,11 @@ func (op *AgentOsOperator) chronyToMasterExec(args []string) (bool, []string) { } } +func (op *AgentOsOperator) InstallMinioBastion() (bool, []string) { + + return true, nil +} + func (op *AgentOsOperator) chronyToMasterByDocker(args []string) (bool, []string) { return true, nil diff --git a/agent-go/a_executor/CommandExecutor.go b/agent-go/a_executor/CommandExecutor.go index c03a853..58d7a66 100644 --- a/agent-go/a_executor/CommandExecutor.go +++ b/agent-go/a_executor/CommandExecutor.go @@ -27,6 +27,7 @@ type ExecutionMessage struct { var log = logger.Log +// AgentOsOperatorCache global agent operator cache var AgentOsOperatorCache = &AgentOsOperator{} func Activate() { diff --git a/agent-go/a_init/BastionInitializaion.go b/agent-go/a_init/BastionInitializaion.go index 2170ff5..d061f4c 100644 --- a/agent-go/a_init/BastionInitializaion.go +++ b/agent-go/a_init/BastionInitializaion.go @@ -6,7 +6,9 @@ import ( "os" "strings" "wdd.io/agent-go/a_agent" + "wdd.io/agent-go/a_executor" "wdd.io/agent-go/a_init/bastion_init" + "wdd.io/agent-go/a_status" ) /* @@ -25,11 +27,22 @@ import ( 3.5 安装kubernetes */ +var AllFunctionCache = &bastion_init.Trie{} + +const ( + InstallDocker = "docker" + InstallDockerCompose = "dockercompose" + InstallMinio = "minio" + InstallRabbitmq = "rabbitmq" + Exit = "exit" + Help = "help" +) + // BastionModeInit 堡垒机模式 完全离线模式 func BastionModeInit() { // Build For Operator - _ = &a_agent.AgentServerInfo{ + bastionAgentServerInfo := &a_agent.AgentServerInfo{ ServerName: "BastionSingle", ServerIPPbV4: "127.0.0.1", ServerIPInV4: "127.0.0.1", @@ -59,53 +72,78 @@ func BastionModeInit() { TopicName: "BastionNode", } + // build for bastion mode operator // re-get agentInfo from status module - //agentInfo := a_status.ReportAgentInfo() - //refreshAgentInfoByStatusInfo(agentInfo, agentServerInfo) - //buildAgentOsOperator(agentInfo, agentServerInfo) + agentInfo := a_status.ReportAgentInfo() + refreshAgentInfoByStatusInfo(agentInfo, bastionAgentServerInfo) - // install docker - //agentOsOperator := a_executor.AgentOsOperatorCache - // boot up minio & rabbitmq - //agentOsOperator.InstallDockerFromLocalExec(nil) - //agentOsOperator.InstallDockerComposeFromLocalExec() + // build operator cache + buildAgentOsOperator(agentInfo, bastionAgentServerInfo) - // build for socks server + // 缓存此内容 + a_agent.AgentServerInfoCache = bastionAgentServerInfo + agentOperator := a_executor.AgentOsOperatorCache - words := []string{"apple", "apricot", "apprentice", "application"} - t := bastion_init.NewTrie() - t.InsertAll(words) + // build for all functions + buildBastionModeFunction() - prefix := "ap" - closest, err := bastion_init.FindClosestWord(t, prefix) - if err != nil { - fmt.Println(err) - } else { - fmt.Printf("The closest word to '%s' is '%s'\n", prefix, closest) - } + // build for function arguments list + //var funcArgsList []string reader := bufio.NewReader(os.Stdin) + bastion_init.PrintBastionHelp() for { - bastion_init.PrintBastionHelp() + fmt.Println() fmt.Print("enter ==> ") + text, _ := reader.ReadString('\n') text = strings.TrimSpace(text) + inputCommand := uniformInputCommand(text) + fmt.Println("inputCommand: ", inputCommand) - if text == "quit" { - break - } else if text == "help" { + // execute the function + switch inputCommand { + case InstallDocker: + agentOperator.InstallDockerBastion() + case InstallDockerCompose: + agentOperator.InstallDockerComposeBastion() + case InstallMinio: + agentOperator.InstallMinioBastion() + case Exit: + os.Exit(0) + case Help: bastion_init.PrintBastionHelp() - } else { - // Execute the command - fmt.Println("Executing command:", text) + default: + fmt.Println("inputCommand is not exist ! Please input again") } } } -// uniformInputCommand 归一化输入的命令 -func uniformInputCommand(inputString string) { +func buildBastionModeFunction() { + // build the tree search node + log.Info("build the tree search node") + tcc := bastion_init.NewTrie() + tcc.Insert(InstallDocker) + tcc.Insert(InstallDockerCompose) + tcc.Insert(InstallMinio) + tcc.Insert(InstallRabbitmq) + tcc.Insert(Help) + tcc.Insert(Exit) + + AllFunctionCache = tcc +} + +// uniformInputCommand 归一化输入的命令 +func uniformInputCommand(inputString string) string { + + findClosestWord, err := bastion_init.FindClosestWord(AllFunctionCache, inputString) + if err != nil { + log.ErrorF("inputString error: %s", err.Error()) + } + + return findClosestWord } diff --git a/agent-go/a_init/bastion_init/BastionTest.go b/agent-go/a_init/bastion_init/BastionTest.go index 7318d94..09931d7 100644 --- a/agent-go/a_init/bastion_init/BastionTest.go +++ b/agent-go/a_init/bastion_init/BastionTest.go @@ -1,140 +1 @@ package bastion_init - -import ( - "fmt" - "sort" -) - -type TrieNode struct { - children [26]*TrieNode - isEnd bool - word string -} - -type Trie struct { - root *TrieNode -} - -func NewTrie() *Trie { - return &Trie{root: &TrieNode{}} -} - -func (tn *TrieNode) Insert(word string) { - node := tn - for _, c := range word { - c -= 'a' // Convert char to int index - if node.children[c] == nil { - node.children[c] = &TrieNode{} - } - node = node.children[c] - } - node.isEnd = true - node.word = word -} - -func (t *Trie) InsertAll(words []string) { - for _, word := range words { - t.root.Insert(word) - } -} - -func (tn *TrieNode) Find(prefix string) []string { - node := tn - result := make([]string, 0) - for _, c := range prefix { - c -= 'a' // Convert char to int index - if node.children[c] != nil { - node = node.children[c] - } else { - return result // No more matching nodes - } - } - trieWalk(node, &result) - return result -} - -func trieWalk(node *TrieNode, result *[]string) { - if node.isEnd { - *result = append(*result, node.word) - } - for _, child := range node.children { - if child != nil { - trieWalk(child, result) - } - } -} - -type Word struct { - Word string - Rank int -} - -func SortWords(words []string) []Word { - sortedWords := make([]Word, len(words)) - for i, word := range words { - sortedWords[i] = Word{Word: word, Rank: i} - } - - sort.Slice(sortedWords, func(i, j int) bool { - return sortedWords[i].Word < sortedWords[j].Word - }) - - return sortedWords -} - -func FindClosestWord(trie *Trie, prefix string) (string, error) { - words := trie.root.Find(prefix) - if len(words) == 0 { - return "", fmt.Errorf("no words found for prefix: %s", prefix) - } - - sortedWords := SortWords(words) - minDistance := len(prefix) // Initialize with the maximum possible distance - closestWord := sortedWords[0].Word - - for _, word := range sortedWords { - distance := levDist(prefix, word.Word) - if distance < minDistance { - minDistance = distance - closestWord = word.Word - } - } - - return closestWord, nil -} - -func levDist(s1, s2 string) int { - m, n := len(s1), len(s2) - if m == 0 { - return n - } - if n == 0 { - return m - } - - dp := make([][]int, m+1) - for i := range dp { - dp[i] = make([]int, n+1) - } - - for i := 1; i <= m; i++ { - for j := 1; j <= n; j++ { - cost := 0 - if s1[i-1] != s2[j-1] { - cost = 1 - } - dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + cost - } - } - return dp[m][n] -} - -func min(a, b, c int) int { - if a < b && a < c { - return a - } else if b < a && b < c { - return b - } else { - return c - } -} diff --git a/agent-go/a_init/bastion_init/TrieSearch.go b/agent-go/a_init/bastion_init/TrieSearch.go new file mode 100644 index 0000000..9151e76 --- /dev/null +++ b/agent-go/a_init/bastion_init/TrieSearch.go @@ -0,0 +1,143 @@ +package bastion_init + +import ( + "fmt" + "sort" +) + +type TrieNode struct { + children [26]*TrieNode + isEnd bool + word string +} + +type Trie struct { + root *TrieNode +} + +func NewTrie() *Trie { + return &Trie{root: &TrieNode{}} +} + +func (tn *TrieNode) Insert(word string) { + node := tn + for _, c := range word { + c -= 'a' // Convert char to int index + if node.children[c] == nil { + node.children[c] = &TrieNode{} + } + node = node.children[c] + } + node.isEnd = true + node.word = word +} + +func (t *Trie) Insert(word string) { + t.root.Insert(word) +} +func (t *Trie) InsertAll(words []string) { + for _, word := range words { + t.root.Insert(word) + } +} + +func (tn *TrieNode) Find(prefix string) []string { + node := tn + result := make([]string, 0) + for _, c := range prefix { + c -= 'a' // Convert char to int index + if node.children[c] != nil { + node = node.children[c] + } else { + return result // No more matching nodes + } + } + trieWalk(node, &result) + return result +} + +func trieWalk(node *TrieNode, result *[]string) { + if node.isEnd { + *result = append(*result, node.word) + } + for _, child := range node.children { + if child != nil { + trieWalk(child, result) + } + } +} + +type Word struct { + Word string + Rank int +} + +func SortWords(words []string) []Word { + sortedWords := make([]Word, len(words)) + for i, word := range words { + sortedWords[i] = Word{Word: word, Rank: i} + } + + sort.Slice(sortedWords, func(i, j int) bool { + return sortedWords[i].Word < sortedWords[j].Word + }) + + return sortedWords +} + +func FindClosestWord(trie *Trie, prefix string) (string, error) { + words := trie.root.Find(prefix) + if len(words) == 0 { + return "", fmt.Errorf("no words found for prefix: %s", prefix) + } + + sortedWords := SortWords(words) + minDistance := len(prefix) // Initialize with the maximum possible distance + closestWord := sortedWords[0].Word + + for _, word := range sortedWords { + distance := levDist(prefix, word.Word) + if distance < minDistance { + minDistance = distance + closestWord = word.Word + } + } + + return closestWord, nil +} + +func levDist(s1, s2 string) int { + m, n := len(s1), len(s2) + if m == 0 { + return n + } + if n == 0 { + return m + } + + dp := make([][]int, m+1) + for i := range dp { + dp[i] = make([]int, n+1) + } + + for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + cost := 0 + if s1[i-1] != s2[j-1] { + cost = 1 + } + dp[i][j] = minThree(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + cost + } + } + return dp[m][n] +} + +func minThree(a, b, c int) int { + if a < b && a < c { + return a + } else if b < a && b < c { + return b + } else { + return c + } +} diff --git a/agent-go/a_init/bastion_init/TrieSearch_test.go b/agent-go/a_init/bastion_init/TrieSearch_test.go new file mode 100644 index 0000000..4b6aa08 --- /dev/null +++ b/agent-go/a_init/bastion_init/TrieSearch_test.go @@ -0,0 +1,21 @@ +package bastion_init + +import ( + "fmt" + "testing" +) + +func TestNewTrie(t *testing.T) { + + words := []string{"apple", "apricot", "apprentice", "application"} + tcc := NewTrie() + tcc.InsertAll(words) + + prefix := "ap" + closest, err := FindClosestWord(tcc, prefix) + if err != nil { + fmt.Println(err) + } else { + fmt.Printf("The closest word to '%s' is '%s'\n", prefix, closest) + } +} diff --git a/agent-go/a_init/bastion_init/config.go b/agent-go/a_init/bastion_init/config.go index d863dbf..fad48cf 100644 --- a/agent-go/a_init/bastion_init/config.go +++ b/agent-go/a_init/bastion_init/config.go @@ -1,27 +1,5 @@ package bastion_init -import ( - "fmt" - "io" - "os" -) - -// getStandardOutPutLength 获取到标准输出终端的长度 -func getStandardOutPutLength() (int, error) { - buffer := make([]byte, 0) - _, err := io.ReadFull(os.Stdout, buffer) - if err != nil { - return 0, err - } - return len(buffer), nil -} - func PrintBastionHelp() { - length, err := getStandardOutPutLength() - if err == nil { - fmt.Printf("标准输出终端的长度: %d 字节\n", length) - } else { - fmt.Println("获取标准输出终端长度时发生错误:", err) - } } diff --git a/agent-operator/CmiiK8sOperator_test.go b/agent-operator/CmiiK8sOperator_test.go index fb26784..b826517 100644 --- a/agent-operator/CmiiK8sOperator_test.go +++ b/agent-operator/CmiiK8sOperator_test.go @@ -251,7 +251,7 @@ func TestUpdateCmiiDeploymentImageTag(t *testing.T) { // 计算20:00的时间 now := time.Now() - targetTime := time.Date(now.Year(), now.Month(), now.Day(), 16, 12, 0, 0, now.Location()) + targetTime := time.Date(now.Year(), now.Month(), now.Day(), 17, 57, 00, 0, now.Location()) duration := time.Duration(0) @@ -268,12 +268,12 @@ func TestUpdateCmiiDeploymentImageTag(t *testing.T) { // 等待到20:00 time.Sleep(duration) - cmiiEnv := devFlight + cmiiEnv := demo //appName := "cmii-uav-platform" //newTag := "5.4.0-032601" appNameTagMap := map[string]string{ - "cmii-uav-industrial-portfolio": "5.4.0-041001", + "cmii-uav-platform-media": "5.4.0", } for appName, newTag := range appNameTagMap { diff --git a/agent-operator/CmiiOperator.go b/agent-operator/CmiiOperator.go index 6a3661f..a26be72 100644 --- a/agent-operator/CmiiOperator.go +++ b/agent-operator/CmiiOperator.go @@ -26,7 +26,7 @@ type ImageSyncEntity struct { CompressImageToGzip bool // 压缩镜像 UploadToDemoMinio bool // 上传镜像 ShouldDirectPushToHarbor bool // 直接推送到对方的主机 || 离线部署机 使用此部门 - DirectHarborHost string // 目标Harbor仓库的Host,全名称带端口 + DirectHarborHost string // 目标Harbor仓库的Host,全名称带端口 不带http前缀 } type ImageSyncResult struct { diff --git a/agent-operator/CmiiOperator_test.go b/agent-operator/CmiiOperator_test.go index 081efcd..8dda218 100644 --- a/agent-operator/CmiiOperator_test.go +++ b/agent-operator/CmiiOperator_test.go @@ -52,14 +52,81 @@ func TestPullFromEntityAndSyncConditionally(t *testing.T) { // 创建一个模拟的sync对象,用于测试函数的行为。这里需要根据你的实际需求来设置mock数据和预期结果。 sync := ImageSyncEntity{ CmiiNameTagList: []string{ + "cmii-uav-tower:5.4.0-0319", + "cmii-uav-platform-logistics:5.4.0", + "cmii-uav-platform-qinghaitourism:4.1.0-21377-0508", + "cmii-uav-platform-securityh5:5.4.0", + "cmii-uav-platform:5.4.0-25263-041102", + "cmii-uav-platform-ai-brain:5.4.0", + "cmii-uav-emergency:5.3.0", + "cmii-uav-kpi-monitor:5.4.0", + "cmii-uav-platform-splice:5.4.0-040301", + "cmii-uav-platform-jiangsuwenlv:4.1.3-jiangsu-0427", + "cmii-live-operator:5.2.0", "cmii-uav-gateway:5.4.0", + "cmii-uav-platform-security:4.1.6", + "cmii-uav-integration:5.4.0-25916", + "cmii-uav-notice:5.4.0", + "cmii-uav-platform-open:5.4.0", + "cmii-srs-oss-adaptor:2023-SA", + "cmii-admin-gateway:5.4.0", + "cmii-uav-process:5.4.0-0410", + "cmii-suav-supervision:5.4.0-032501", + "cmii-uav-platform-cms-portal:5.4.0", + "cmii-uav-platform-multiterminal:5.4.0", + "cmii-admin-data:5.4.0-0403", + "cmii-uav-cloud-live:5.4.0", + "cmii-uav-grid-datasource:5.2.0-24810", + "cmii-uav-platform-qingdao:4.1.6-24238-qingdao", + "cmii-admin-user:5.4.0", + "cmii-uav-industrial-portfolio:5.4.0-28027-041102", + "cmii-uav-alarm:5.4.0-0409", + "cmii-uav-clusters:5.2.0", + "cmii-uav-platform-oms:5.4.0", + "cmii-uav-platform-hljtt:5.3.0-hjltt", + "cmii-uav-platform-mws:5.4.0", + "cmii-uav-autowaypoint:4.1.6-cm", + "cmii-uav-grid-manage:5.1.0", + "cmii-uav-platform-share:5.4.0", + "cmii-uav-cms:5.3.0", + "cmii-uav-oauth:5.4.0-032901", + "cmii-open-gateway:5.4.0", + "cmii-uav-data-post-process:5.4.0", + "cmii-uav-multilink:5.4.0-032701", + "cmii-uav-platform-media:5.4.0", + "cmii-uav-platform-visualization:5.2.0", + "cmii-uav-platform-emergency-rescue:5.2.0", + "cmii-app-release:4.2.0-validation", + "cmii-uav-device:5.4.0-28028-0409", + "cmii-uav-gis-server:5.4.0", + "cmii-uav-brain:5.4.0", + "cmii-uav-depotautoreturn:5.4.0", + "cmii-uav-threedsimulation:5.1.0", + "cmii-uav-grid-engine:5.1.0", + "cmii-uav-developer:5.4.0-040701", + "cmii-uav-waypoint:5.4.0-032901", + "cmii-uav-platform-base:5.4.0", + "cmii-uav-platform-threedsimulation:5.2.0-21392", + "cmii-uav-platform-detection:5.4.0", + "cmii-uav-logger:5.4.0-0319", + "cmii-uav-platform-seniclive:5.2.0", + "cmii-suav-platform-supervisionh5:5.4.0", + "cmii-uav-user:5.4.0", + "cmii-uav-surveillance:5.4.0-28028-0409", + "cmii-uav-mission:5.4.0-28028-041006", + "cmii-uav-mqtthandler:5.4.0-25916-041001", + "srs:v5.0.195", + "cmii-uav-material-warehouse:5.4.0-0407", + "cmii-uav-platform-armypeople:5.4.0-041201", + "cmii-suav-platform-supervision:5.4.0", + "cmii-uav-airspace:5.4.0-0402", }, FullNameImageList: nil, ProjectVersion: "", - DirectHarborHost: "", + DirectHarborHost: "harbor.wdd.io", CompressImageToGzip: false, - UploadToDemoMinio: true, - ShouldDirectPushToHarbor: false, + UploadToDemoMinio: false, + ShouldDirectPushToHarbor: true, } // 调用函数并获取结果。这里需要根据你的实际需求来验证返回的结果是否符合预期。 diff --git a/agent-operator/K8sOperator_test.go b/agent-operator/K8sOperator_test.go index dee84e3..1867ba2 100644 --- a/agent-operator/K8sOperator_test.go +++ b/agent-operator/K8sOperator_test.go @@ -91,8 +91,8 @@ func TestCmiiK8sOperator_DeploymentUpdateTag(t *testing.T) { func TestCmiiK8sOperator_DeploymentRestart(t *testing.T) { - cmiiEnv := "demo" - appName := "cmii-uav-gis-server" + cmiiEnv := integration + appName := "cmii-uav-data-post-process" CmiiOperator.DeploymentRestart(cmiiEnv, appName) diff --git a/cmii_operator/log/cmii-update-log.txt b/cmii_operator/log/cmii-update-log.txt index 4361bd1..fd4c69c 100644 --- a/cmii_operator/log/cmii-update-log.txt +++ b/cmii_operator/log/cmii-update-log.txt @@ -31,3 +31,14 @@ 2024-04-08-15-02-00 uavcloud-devflightcmii-uav-industrial-portfolio 5.4.0-040801 5.4.0-040802 2024-04-08-17-30-00 uavcloud-devflightcmii-uav-industrial-portfolio 5.4.0-040802 5.4.0-040803 2024-04-10-16-12-00 uavcloud-devflightcmii-uav-industrial-portfolio 5.5.0-snapshot 5.4.0-041001 +2024-04-12-17-30-30 uavcloud-devflightcmii-uav-threedsimulation 5.2.0-snapshot 5.4.0-041201 +2024-04-14-11-34-00 uavcloud-demo cmii-uav-surveillance 5.4.0-28028-0409 5.4.0-28028-041401 +2024-04-15-10-37-51 uavcloud-demo cmii-uav-platform-media 5.4.0 5.4.0-041401 +2024-04-15-17-35-00 uavcloud-demo cmii-uav-airspace 5.4.0-0402 5.4.0-041501 +2024-04-15-17-41-00 uavcloud-demo cmii-uav-platform 5.4.0-25263-041102 5.4.0-041501 +2024-04-16-10-24-00 uavcloud-demo cmii-uav-multilink 5.4.0-032701 5.4.0-041601 +2024-04-16-16-24-00 uavcloud-demo cmii-uav-airspace 5.4.0-041501 5.4.0-041601 +2024-04-16-16-25-20 uavcloud-demo cmii-admin-data 5.4.0-0403 5.4.0-041601 +2024-04-16-16-25-35 uavcloud-demo cmii-uav-platform-oms 5.4.0 5.4.0-041601 +2024-04-16-17-38-00 uavcloud-demo cmii-uav-platform-armypeople 5.4.0-041201 5.4.0-28028-041601 +2024-04-16-17-57-00 uavcloud-demo cmii-uav-platform-media 5.4.0-041401 5.4.0