Merge Go frontend (apiserver + xcecli) into meson build

This commit is contained in:
Rongsong Shen 2026-04-14 11:20:17 +08:00
parent 43dc12a36c
commit c9dc8c4676
205 changed files with 30793 additions and 328 deletions

View file

@ -0,0 +1,82 @@
package cert
import (
"crypto/tls"
"encoding/pem"
"log"
"os"
"os/user"
"path"
logger "xcecli/config"
"go.uber.org/zap"
)
func GeneratorCert(serverAddress, configPath string) error {
serverAddress, err := extractHostAndPort(serverAddress)
logger.Debug("Server address", zap.String("serverAddress", serverAddress))
if err != nil {
return err
}
// The HTTPS server's address (replace with the server you want to check)
//serverAddress := "172.16.10.10:8443"
// Create a tls.Config with InsecureSkipVerify set to true
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
}
// Dial the server using TLS with the custom tls.Config
conn, err := tls.Dial("tcp", serverAddress, tlsConfig)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// Retrieve the certificate chain
certs := conn.ConnectionState().PeerCertificates
// Print the certificate chain
// for i, cert := range certs {
// fmt.Printf("Certificate %d:\n", i)
// fmt.Printf(" Subject: %s\n", cert.Subject)
// fmt.Printf(" Issuer: %s\n", cert.Issuer)
// fmt.Printf(" Not Before: %s\n", cert.NotBefore)
// fmt.Printf(" Not After: %s\n", cert.NotAfter)
// }
// The root CA is usually the last certificate in the chain
//rootCert := certs[len(certs)-1]
// fmt.Println("\nRoot CA Certificate:")
// fmt.Printf(" Subject: %s\n", rootCert.Subject)
// fmt.Printf(" Issuer: %s\n", rootCert.Issuer)
// fmt.Printf(" Not Before: %s\n", rootCert.NotBefore)
// fmt.Printf(" Not After: %s\n", rootCert.NotAfter)
// Save the server certificate to a file
certOut, err := os.Create(configPath)
if err != nil {
return err
}
defer certOut.Close()
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certs[0].Raw})
//fmt.Println("Server certificate saved to server.crt")
return nil
}
func GetCertPath() (string, error) {
usr, err := user.Current()
if err != nil {
return "", err
}
configDir := path.Join(usr.HomeDir, ".xce")
configPath := path.Join(configDir, "server.crt")
// create the config directory if it doesn't exist
if err := os.MkdirAll(configDir, 0700); err != nil {
return "", err
}
return configPath, nil
}

View file

@ -0,0 +1,37 @@
package cert
import (
"fmt"
"net/url"
"xcecli/common/request"
)
func extractHostAndPort(rawURL string) (string, error) {
// parse the URL
parsedURL, err := url.Parse(rawURL)
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return "", fmt.Errorf("failed to parse URL: [%s]", errMsg)
}
// get the host and port
host := parsedURL.Hostname()
port := parsedURL.Port()
// if the port is empty, use the default port (HTTP is 80, HTTPS is 443)
if port == "" {
if parsedURL.Scheme == "https" {
port = "443"
} else if parsedURL.Scheme == "http" {
port = "80"
}
}
// concatenate the host and port
result := host
if port != "" {
result = fmt.Sprintf("%s:%s", host, port)
}
return result, nil
}

View file

@ -0,0 +1,50 @@
package cmd
import (
"fmt"
"os"
"text/tabwriter"
"xcecli/common/request"
"xcecli/config"
"github.com/spf13/cobra"
)
// NewListCmd create the list command
func NewListCmd(configManager *config.ConfigManager) *cobra.Command {
return &cobra.Command{
Use: "list",
GroupID: "api",
Short: "List the API Servers",
Long: "List all the configured API Servers and the default API Server",
RunE: func(cmd *cobra.Command, args []string) error {
return runList(configManager)
},
}
}
func runList(cm *config.ConfigManager) error {
cfg, err := cm.GetConfig()
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to get the config: [%s]", errMsg)
}
// use tabwriter to format the output
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
fmt.Fprintln(w, "Default\tVersion\tURL")
for _, server := range cfg.APIServerInfo {
isDefault := " "
if server.Token == "" {
continue
}
fmt.Fprintf(w, "%s\t%s\n",
isDefault,
server.URL)
}
return w.Flush()
}

View file

@ -0,0 +1,189 @@
package cmd
import (
"fmt"
"net/url"
"os"
"strings"
"syscall"
"xcecli/cmd/auth/cert"
"xcecli/common/request"
"xcecli/config"
logger "xcecli/config"
"xcecli/models"
"go.uber.org/zap"
"golang.org/x/term"
"github.com/spf13/cobra"
)
// updateServerConfig
func updateServerConfig(cfg *config.Config, url, certPath string, loginResp *models.LogonResponse) error {
logger.Debug("updateServerConfig", zap.String("url", url), zap.String("certPath", certPath))
logger.Info("updateServerConfig", zap.String("url", url), zap.String("certPath", certPath))
// check if the server with the same URL already exists
Found := false
for i, server := range cfg.APIServerInfo {
if server.URL == url {
// update the existing server info
cfg.APIServerInfo[i].Token = loginResp.Data.Token
cfg.APIServerInfo[i].Path = loginResp.Data.Path //save the path
cfg.APIServerInfo[i].CreateTime = loginResp.Data.CreateTime
cfg.APIServerInfo[i].CACert = certPath
Found = true
break
}
}
// if it's a new server, add it to the list
if !Found {
newServer := config.APIServerInfo{
URL: url,
Token: loginResp.Data.Token,
Path: loginResp.Data.Path,
CreateTime: loginResp.Data.CreateTime,
CACert: certPath,
}
// add it to the end of the array
cfg.APIServerInfo = append(cfg.APIServerInfo, newServer)
logger.Debug("newServer", zap.Any("newServer", newServer))
logger.Info("newServer", zap.Any("newServer", newServer))
}
return nil
}
func NewLogonCmd(configManager *config.ConfigManager) *cobra.Command {
var opts struct {
username string
password string
url string
}
logger.Debug("NewLogonCmd", zap.Any("opts", opts))
logger.Info("NewLogonCmd", zap.Any("opts", opts))
cmd := &cobra.Command{
Use: "login",
GroupID: "api",
Short: "Login to API Server",
Long: `Login to the specified API Server, and save the login info to the config file.
Example:
xce login -u USERNAME -p PASSWORD --url http://IP:8080
xce login -u USERNAME -p PASSWORD --url https://IP:8443`,
Run: func(cmd *cobra.Command, args []string) {
// if the password is not provided, prompt the user to enter it
if opts.password == "" {
fmt.Print("enter password: ")
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println() // new line
if err != nil {
errMsg := request.ExtractErrorMessage(err)
fmt.Printf("error reading password: [%s]", errMsg)
os.Exit(1)
}
opts.password = string(bytePassword)
}
err := runLogon(opts, configManager)
if err != nil {
fmt.Println(strings.ToLower(err.Error()))
}
},
}
// set the command line parameters
flags := cmd.Flags()
flags.StringVarP(&opts.username, "username", "u", "", "username")
flags.StringVarP(&opts.password, "password", "p", "", "password (optional, will prompt if not provided)")
flags.StringVar(&opts.url, "url", "", "API Server URL")
// required parameters
cmd.MarkFlagRequired("username")
//cmd.MarkFlagRequired("password")
cmd.MarkFlagRequired("url")
logger.Debug("cmd", zap.Any("cmd", cmd))
logger.Info("cmd", zap.Any("cmd", cmd))
return cmd
}
func runLogon(opts struct{ username, password, url string }, cm *config.ConfigManager) error {
if cm == nil {
return fmt.Errorf("fail to initialize the config manager")
}
logger.Debug("runLogon", zap.Any("opts", opts))
logger.Info("runLogon", zap.Any("opts", opts))
//get the config
cfg, err := cm.GetConfig()
if err != nil {
cfg = &config.Config{}
}
logger.Debug("cfg", zap.Any("cfg", cfg))
logger.Info("cfg", zap.Any("cfg", cfg))
// create the API client
apiClient := request.NewAPIClient(opts.url)
certPath := ""
// Only setup certs for HTTPS connections
if u, err := url.Parse(opts.url); err == nil && u.Scheme == "https" {
certPath, err = cert.GetCertPath()
apiClient.SetRootCAs(certPath)
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to get cert path: [%s]", errMsg)
}
if err := cert.GeneratorCert(opts.url, certPath); err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to generate cert: [%s]", errMsg)
}
}
logger.Debug("apiClient", zap.Any("apiClient", apiClient))
logger.Info("apiClient", zap.Any("apiClient", apiClient))
// perform the login
loginResp, err := apiClient.Logon(opts.username, opts.password)
if err != nil {
// If login fails, remove the server entry with the same URL if it exists
for i, server := range cfg.APIServerInfo {
if server.URL == opts.url {
// Remove this server from the slice
cfg.APIServerInfo = append(cfg.APIServerInfo[:i], cfg.APIServerInfo[i+1:]...)
// Save the updated config
if saveErr := cm.SaveConfig(cfg); saveErr != nil {
logger.Error("Failed to save the config after removing server entry", zap.Error(saveErr))
}
break
}
}
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to login: [%s]", errMsg)
}
logger.Debug("loginResp", zap.Any("loginResp", loginResp))
logger.Info("loginResp", zap.Any("loginResp", loginResp))
// update the server config
if err := updateServerConfig(cfg, opts.url, certPath, loginResp); err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to update the server config: [%s]", errMsg)
}
logger.Debug("cfg", zap.Any("cfg", cfg))
logger.Info("cfg", zap.Any("cfg", cfg))
// save the config
if err := cm.SaveConfig(cfg); err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to save the config: [%s]", errMsg)
}
logger.Debug("cfg", zap.Any("cfg", cfg))
logger.Info("cfg", zap.Any("cfg", cfg))
fmt.Println("login successfully")
return nil
}

View file

@ -0,0 +1,74 @@
package cmd
import (
"fmt"
"xcecli/common/request"
"xcecli/config"
"github.com/spf13/cobra"
)
// NewLogoutCmd create logout command
func NewLogoutCmd(configManager *config.ConfigManager) *cobra.Command {
var url string
cmd := &cobra.Command{
Use: "logout",
GroupID: "api",
Short: "Logout from API Server",
Long: `Logout from specified API Server, clear local saved token.
Example:
xce logout --url http://tt1.test.com:8080
xce logout --url https://tt1.test.com:8443`,
RunE: func(cmd *cobra.Command, args []string) error {
return runLogout(url, configManager)
},
}
// add url arg
cmd.Flags().StringVar(&url, "url", "", "specify API Server url (defaults to active server)")
return cmd
}
func runLogout(url string, cm *config.ConfigManager) error {
cfg, err := cm.GetConfig()
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("get config failed: [%s]", errMsg)
}
// default to active server when --url not provided
if url == "" {
if len(cfg.APIServerInfo) == 0 {
return fmt.Errorf("not logged in")
}
url = cfg.APIServerInfo[0].URL
}
apiServerInfo := make([]config.APIServerInfo, 0)
// find server with specified url
found := false
for _, server := range cfg.APIServerInfo {
switch server.URL {
case "":
continue
case url:
found = true
default:
apiServerInfo = append(apiServerInfo, server)
}
}
//fmt.Printf("apiServerInfo: %+v\n", apiServerInfo)
if !found {
return fmt.Errorf("server not found: %s", url)
}
cfg.APIServerInfo = apiServerInfo
// save config
if err := cm.SaveConfig(cfg); err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("save config failed: [%s]", errMsg)
}
fmt.Printf("clear login info for server %s\n", url)
return nil
}

View file

@ -0,0 +1,53 @@
package cmd
import (
"testing"
"xcecli/config"
)
func TestRunLogout_WithURL(t *testing.T) {
cm, cfg := newTestConfig(t, "http://server1:8080", "http://server2:8080")
if err := runLogout("http://server1:8080", cm); err != nil {
t.Fatal(err)
}
cfg, _ = cm.GetConfig()
if len(cfg.APIServerInfo) != 1 || cfg.APIServerInfo[0].URL != "http://server2:8080" {
t.Errorf("expected server2 to remain, got %+v", cfg.APIServerInfo)
}
}
func TestRunLogout_DefaultsToActiveServer(t *testing.T) {
// empty url should use the first (active) server
cm, _ := newTestConfig(t, "http://server1:8080")
if err := runLogout("", cm); err != nil {
t.Fatal(err)
}
cfg, _ := cm.GetConfig()
if len(cfg.APIServerInfo) != 0 {
t.Errorf("expected empty config, got %+v", cfg.APIServerInfo)
}
}
func TestRunLogout_NotFound(t *testing.T) {
cm, _ := newTestConfig(t, "http://server1:8080")
err := runLogout("http://notexist:8080", cm)
if err == nil {
t.Fatal("expected error for unknown server")
}
}
// newTestConfig creates a temp ConfigManager with the given server URLs.
func newTestConfig(t *testing.T, urls ...string) (*config.ConfigManager, *config.Config) {
t.Helper()
dir := t.TempDir()
cm := config.NewConfigManagerWithPath(dir + "/config.json")
servers := make([]config.APIServerInfo, len(urls))
for i, u := range urls {
servers[i] = config.APIServerInfo{URL: u, Token: "tok"}
}
cfg := &config.Config{APIServerInfo: servers}
if err := cm.SaveConfig(cfg); err != nil {
t.Fatal(err)
}
return cm, cfg
}

View file

@ -0,0 +1,80 @@
package clusters
import (
"fmt"
"strings"
"xcecli/common/output"
"xcecli/common/request"
"xcecli/config"
"xcecli/models"
"github.com/spf13/cobra"
)
var (
cluster string
fields string
Verbose bool
)
// NewClustersCmd create cluster query command
func NewClustersCmd(configManager *config.ConfigManager) *cobra.Command {
cmd := &cobra.Command{
Use: "id",
GroupID: "query",
Short: "Query cluster info",
Long: `Query cluster info, support filter by cluster name .
example:
xce clusters # query all clusters`,
RunE: func(cmd *cobra.Command, args []string) error {
err := runClusters(configManager)
if err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
//cmd.Flags().BoolVarP(&Verbose, "verbose", "l", false, "show cluster detail")
return cmd
}
func runClusters(cm *config.ConfigManager) error {
// get the config
serverInfo, _ := cm.GetServerInfo()
// create API client
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
// validate the parameters
cluster = strings.ToLower(cluster)
// create the query parameters
params := make(map[string]string)
// handle field selection
if fields != "" || cluster != "" {
params["fields"] = fields
}
// create the API client and query the host info
var resp models.ClusterResponse
err := apiClient.GetRequestFromAPIServer(serverInfo.Token, "", serverInfo.CACert, "/xce/v1/clusters", &resp, params)
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to get clusters: [%s]", errMsg)
}
if resp.Data.Count == 0 {
fmt.Println("no cluster found")
return nil
}
// print result
output := output.NewOutput(fields, false, false)
output.SetVerbose(Verbose)
output.ClusterPrintData(&resp.Data.Rows[0])
return nil
}

View file

@ -0,0 +1,179 @@
package hosts
import (
"encoding/json"
"fmt"
"strings"
"xcecli/common/request"
"xcecli/config"
"xcecli/models"
"github.com/spf13/cobra"
)
const (
HOST_OPEN = 1
HOST_CLOSE = 2
)
// HostController host control controller
type HostController struct {
configManager *config.ConfigManager
action string
hostnames []string
comment string
allHosts bool
}
// NewHostController create host control controller
func NewHostController(configManager *config.ConfigManager, action string) *HostController {
return &HostController{
configManager: configManager,
action: action,
hostnames: []string{},
}
}
// Run execute host control
func (c *HostController) RunHostsControl() error {
// get server info and verify authentication
serverInfo, _ := c.configManager.GetServerInfo()
// create API client
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
// handle all hosts case
if c.allHosts {
c.hostnames = []string{"all"}
}
// if no hostnames specified, return error
if len(c.hostnames) == 0 {
return fmt.Errorf("must specify hostnames or use --all option")
}
// create request
req := models.HostControlRequest{
ControlAction: c.action,
Hostname: c.hostnames,
Comment: c.comment,
}
// encode request to JSON bytes
reqData, err := json.Marshal(req)
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to encode request data: [%s]", errMsg)
}
var resp models.HostControlResponse
err = apiClient.PostRequestToAPIServer(
serverInfo.Token,
serverInfo.CACert,
"/xce/v1/hosts/control",
reqData,
&resp,
)
if err != nil {
return err
}
if len(c.hostnames) == 1 && c.hostnames[0] == "all" {
for _, data := range resp.Data.Accepted {
fmt.Printf("host <%s> is being <%s>\n", data.HostName, c.action)
}
for _, host := range resp.Data.Failed {
fmt.Printf("host <%s> cannot be <%s> for reason <%s>\n", host.HostName, c.action, host.Msg)
}
} else {
for _, data := range resp.Data.Accepted {
fmt.Printf("host <%s> is being <%s>\n", data.HostName, c.action)
}
for _, host := range resp.Data.Failed {
fmt.Printf("host <%s> cannot be <%s> for reason <%s>\n", host.HostName, c.action, host.Msg)
}
}
return nil
}
// Parse parse command line arguments
func (c *HostController) ParseCmdArgs(cmd *cobra.Command, args []string) error {
// handle hostnames
if len(args) > 0 {
c.hostnames = append(c.hostnames, args...)
}
return nil
}
// NewOpenHostsCmd create open hosts command
func NewOpenHostsCmd(configManager *config.ConfigManager) *cobra.Command {
controller := NewHostController(configManager, "open")
cmd := &cobra.Command{
Use: "open [hostname1 hostname2...]",
Short: "Open hosts",
Long: `Open hosts by hostname.
Open hosts changes hosts from DOWN, UNREACH, or UNAVAIL status to OK status.
Examples:
xce hosts open host1,host2,... # open specific hosts
xce hosts open --all # open all hosts`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := controller.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := controller.RunHostsControl(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// add control options
flags := cmd.Flags()
flags.BoolVarP(&controller.allHosts, "all", "a", false, "apply to all hosts")
flags.StringVarP(&controller.comment, "comment", "c", "", "add comment for this operation")
return cmd
}
// NewCloseHostsCmd create close hosts command
func NewCloseHostsCmd(configManager *config.ConfigManager) *cobra.Command {
controller := NewHostController(configManager, "close")
cmd := &cobra.Command{
Use: "close [hostname1 hostname2...]",
Short: "Close hosts",
Long: `Close hosts by hostname.
Close hosts changes hosts from OK, BUSY, or IDLE status to UNAVAIL status.
Examples:
xce hosts close host1 host2 # close specific hosts
xce hosts close --all # close all hosts`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := controller.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := controller.RunHostsControl(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// add control options
flags := cmd.Flags()
flags.BoolVarP(&controller.allHosts, "all", "a", false, "apply to all hosts")
flags.StringVarP(&controller.comment, "comment", "c", "", "add comment for this operation")
return cmd
}

View file

@ -0,0 +1,32 @@
package hosts
import (
"xcecli/config"
"github.com/spf13/cobra"
)
// NewHostsCmd creates a command for querying host information
func NewHostsCmd(configManager *config.ConfigManager) *cobra.Command {
cmd := &cobra.Command{
Use: "hosts",
GroupID: "query",
Short: "Manage hosts",
Long: `Manage hosts in the system.
Use 'xce hosts list' to query host information.
Use 'xce hosts open' to open hosts.
Use 'xce hosts close' to close hosts.`,
Run: func(cmd *cobra.Command, args []string) {
// Display help when no subcommand is specified
cmd.Help()
},
}
// Add list subcommand
cmd.AddCommand(NewListHostsCmd(configManager))
cmd.AddCommand(NewOpenHostsCmd(configManager))
cmd.AddCommand(NewCloseHostsCmd(configManager))
cmd.AddCommand(NewSelectHostsCmd(configManager))
return cmd
}

View file

@ -0,0 +1,199 @@
package hosts
import (
"fmt"
"strings"
"xcecli/common/output"
"xcecli/common/request"
"xcecli/config"
logger "xcecli/config"
host_filter "xcecli/filters"
"xcecli/models"
"go.uber.org/zap"
"github.com/spf13/cobra"
)
// HostsQuerier struct for querying hosts
type HostsQuerier struct {
configManager *config.ConfigManager
filter *host_filter.HostsFilter
fields string
jsonOutput bool
noHeader bool
verbose bool
wideFormat bool // wide format
}
// NewHostsQuerier creates a new hosts querier
func NewHostsQuerier(configManager *config.ConfigManager) *HostsQuerier {
return &HostsQuerier{
configManager: configManager,
filter: &host_filter.HostsFilter{},
}
}
// Parse parses command line arguments, simplified version that doesn't need to process flags already bound to filter
func (hq *HostsQuerier) ParseCmdArgs(cmd *cobra.Command, args []string) error {
// No additional processing needed, all parameters are bound in flags
if len(args) > 0 {
hq.filter.HostName = strings.Join(args, ",")
}
// Define mutual exclusions for verbose flag
if hq.verbose {
conflicts := make([]string, 0)
if hq.wideFormat {
conflicts = append(conflicts, "--wided (-w)")
}
if hq.fields != "" {
conflicts = append(conflicts, "--output (-o)")
}
if len(conflicts) > 0 {
return fmt.Errorf("flag --verbose (-l) cannot be used with: %s\n\n%s",
strings.Join(conflicts, ", "), cmd.UsageString())
}
}
return nil
}
// Run executes the hosts query command
func (hq *HostsQuerier) RunHostsList() error {
// Get configuration
// Get server information
serverInfo, _ := hq.configManager.GetServerInfo()
// create API client
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
// Create query parameters
params := make(map[string]string)
// Get filter string
filterStr, err := hq.filter.HandleHostFilter()
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to get filter: [%s]", errMsg)
} else if filterStr != "" {
params["filter"] = filterStr
}
// Field mapping
fieldsMap := models.HostFieldsMap
if hq.verbose && hq.fields == "" {
for header := range fieldsMap {
hq.fields += header + ","
}
hq.fields = strings.TrimSuffix(hq.fields, ",")
}
// Process field selection
if hq.fields != "" {
hq.fields = request.CleanFields(hq.fields)
mappedFields, err := request.CheckAndMapFields(hq.fields, fieldsMap)
if err != nil {
return err
}
params["fields"] = strings.Join(mappedFields, ",")
}
// create page printer
pagePrinter := output.NewPagePrinter(output.DataTypeHosts, output.WithFields(hq.fields),
output.WithFormatJson(hq.jsonOutput), output.WithNoHeader(hq.noHeader),
output.WithVerbose(hq.verbose), output.WithWideFormat(hq.wideFormat))
// Record if the first page has been processed
isFirstPage := true
var (
resp models.HostsResponse
loopCount int
pageToken string
)
// Loop to get all pages of data
for {
if loopCount > 0 && pageToken == "" {
break
}
err = apiClient.GetRequestFromAPIServer(serverInfo.Token, pageToken, serverInfo.CACert, "/xce/v1/hosts", &resp, params)
if err != nil {
return err
}
if len(resp.Data.Rows) == 0 {
if isFirstPage {
fmt.Println("no host found")
return nil
}
}
// Process the first page
if isFirstPage {
// Start printing process
pagePrinter.StartPrinting()
// Prepare for printing, calculate column width and table header, etc.
pagePrinter.PrepareForPrinting(&resp)
isFirstPage = false
}
// Print the current page
pagePrinter.PrintPage(&resp)
// Update page token and loop counter
pageToken = resp.Data.Page_token
loopCount++
}
// Finish printing
pagePrinter.Finish()
logger.Debug("hosts response completed", zap.Int("pages", loopCount))
return nil
}
func NewListHostsCmd(configManager *config.ConfigManager) *cobra.Command {
hostsQuerier := NewHostsQuerier(configManager)
cmd := &cobra.Command{
Use: "list",
Short: "Query the host info",
Long: `Query the host info, support basic info and detailed info.
Example:
xce hosts list # query the full info
xce hosts list -a # query the detailed info for all hosts
xce hosts list -o "host_name status max_cpus .." # query the specified fields
xce hosts list -o "host_name status max_cpus .." --json # query the specified fields and output in json format`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := hostsQuerier.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := hostsQuerier.RunHostsList(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// Add command line parameters
flags := cmd.Flags()
flags.BoolVarP(&hostsQuerier.verbose, "host_info", "a", false, "host information")
flags.StringVarP(&hostsQuerier.filter.HostType, "host_type", "t", "", "host type (X86_64 or ARM)")
flags.StringVarP(&hostsQuerier.fields, "output", "o", "", "output fields for hosts")
flags.BoolVarP(&hostsQuerier.wideFormat, "wided", "w", false, "output in wide format")
flags.BoolVarP(&hostsQuerier.verbose, "verbose", "l", false, "show host detail")
flags.BoolVarP(&hostsQuerier.jsonOutput, "json", "", false, "output in JSON format")
flags.BoolVarP(&hostsQuerier.noHeader, "noheader", "n", false, "output no header")
return cmd
}

View file

@ -0,0 +1,95 @@
package hosts
import (
"encoding/json"
"fmt"
"strings"
"xcecli/common/output"
"xcecli/common/request"
"xcecli/config"
"xcecli/models"
"github.com/spf13/cobra"
)
// HostSelector handles the hosts select command.
type HostSelector struct {
configManager *config.ConfigManager
resReq string
order string
limit int
}
func (s *HostSelector) validate() error {
if strings.TrimSpace(s.resReq) == "" {
return fmt.Errorf("-R/--resreq is required")
}
if len(s.resReq) > 4094 {
return fmt.Errorf("select string exceeds the limit of 4094 characters")
}
return nil
}
func (s *HostSelector) run() error {
serverInfo, err := s.configManager.GetServerInfo()
if err != nil {
return err
}
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
req := models.ResReqSelectRequest{
Select: s.resReq,
Order: s.order,
Limit: s.limit,
}
body, _ := json.Marshal(req)
var resp models.HostsResponse
if err := apiClient.PostRequestToAPIServer(serverInfo.Token, serverInfo.CACert,
"/xce/v1/hosts/select", body, &resp); err != nil {
return err
}
if resp.Data.Count == 0 {
fmt.Println("no host found")
return nil
}
pagePrinter := output.NewPagePrinter(output.DataTypeHosts)
pagePrinter.StartPrinting()
pagePrinter.PrepareForPrinting(&resp)
pagePrinter.PrintPage(&resp)
pagePrinter.Finish()
return nil
}
func NewSelectHostsCmd(configManager *config.ConfigManager) *cobra.Command {
s := &HostSelector{configManager: configManager}
cmd := &cobra.Command{
Use: "select",
Short: "Select hosts by resource requirement",
Long: `Select hosts matching a resource requirement expression.
Examples:
xce hosts select -R "type==linux"
xce hosts select -R "ncpus>=8" --order ncpus --limit 10`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := s.validate(); err != nil {
fmt.Println(err.Error())
return nil
}
if err := s.run(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
}
return nil
},
}
flags := cmd.Flags()
flags.StringVarP(&s.resReq, "resreq", "R", "", "resource requirement select expression (required)")
flags.StringVar(&s.order, "order", "", "order field")
flags.IntVar(&s.limit, "limit", 0, "max results")
return cmd
}

View file

@ -0,0 +1,28 @@
package hosts
import (
"testing"
)
func TestParseSelectArgs_RequiresResReq(t *testing.T) {
s := &HostSelector{}
err := s.validate()
if err == nil {
t.Error("expected error when -R not provided")
}
}
func TestParseSelectArgs_MaxLength(t *testing.T) {
s := &HostSelector{resReq: string(make([]byte, 4095))}
err := s.validate()
if err == nil {
t.Error("expected error when select string exceeds 4094 chars")
}
}
func TestParseSelectArgs_Valid(t *testing.T) {
s := &HostSelector{resReq: "type==linux"}
if err := s.validate(); err != nil {
t.Errorf("unexpected error: %v", err)
}
}

View file

@ -0,0 +1,287 @@
package jobs
import (
"encoding/json"
"fmt"
"strings"
"xcecli/common/request"
"xcecli/config"
"xcecli/models"
"github.com/spf13/cobra"
)
// JobController defines job control
type JobController struct {
configManager *config.ConfigManager
action string
filter models.JobControlFilter
jobIDs []string
}
// NewJobController create job controller
func NewJobController(configManager *config.ConfigManager, action string) *JobController {
return &JobController{
configManager: configManager,
action: action,
}
}
// Run execute job control
func (c *JobController) RunJobsControl() error {
// Get server information
serverInfo, _ := c.configManager.GetServerInfo()
// Create API client
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
if len(c.jobIDs) > 0 {
// if job IDs are specified, use job ID interface
return c.controlJobsByIDs(apiClient, serverInfo.Token, serverInfo.CACert)
} else {
// otherwise use filter conditions
return c.controlJobsByFilter(apiClient, serverInfo.Token, serverInfo.CACert)
}
}
// control jobs by job IDs
func (c *JobController) controlJobsByIDs(apiClient *request.APIClient, token string, certPath string) error {
jobIDsStr := strings.Join(c.jobIDs, ",")
url := fmt.Sprintf("/xce/v1/jobs/%s/control", jobIDsStr)
req := models.JobControlJobIDRequest{
Action: c.action,
}
// encode request data to JSON bytes
return c.printJobControlResponse(req, apiClient, token, certPath, url)
}
// control jobs by filter conditions
func (c *JobController) controlJobsByFilter(apiClient *request.APIClient, token string, certPath string) error {
url := "/xce/v1/jobs/control"
req := models.JobControlRequest{
Action: c.action,
Filter: c.filter,
}
// encode request data to JSON bytes
return c.printJobControlResponse(req, apiClient, token, certPath, url)
}
func (c *JobController) printJobControlResponse(req interface{}, apiClient *request.APIClient, token string, certPath string, url string) error {
reqData, err := json.Marshal(req)
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to encode request data: [%s]", errMsg)
}
var resp models.JobControlResponse
err = apiClient.PostRequestToAPIServer(
token,
certPath,
url,
reqData,
&resp,
)
if err != nil {
return err
}
for _, data := range resp.Data.Accepted {
if data.ArrayIdx != 0 {
fmt.Printf("job <%d[%d]> %s\n", data.JobID, data.ArrayIdx, strings.TrimPrefix(data.Msg, "job "))
} else {
fmt.Printf("job <%d> %s\n", data.JobID, strings.TrimPrefix(data.Msg, "job "))
}
}
for _, data := range resp.Data.Failed {
if data.ArrayIdx != 0 {
fmt.Printf("job <%d[%d]> %s\n", data.JobID, data.ArrayIdx, strings.TrimPrefix(data.Msg, "job "))
} else {
fmt.Printf("job <%d> %s\n", data.JobID, strings.TrimPrefix(data.Msg, "job "))
}
}
return nil
}
// Parse parse command line arguments
// Parse command line arguments
func (c *JobController) ParseCmdArgs(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
if len(args) == 1 && args[0] == "0" {
c.filter.AllJobs = true
} else {
// parse job ID parameters
c.jobIDs = strings.Split(args[0], ",")
return nil
}
}
// check if any filter conditions are set
if c.filter.JobName == "" && c.filter.QueueName == "" && c.filter.Users == "" && !c.filter.AllJobs {
// if no job IDs or filter conditions are specified, default to processing all jobs of the current user
return fmt.Errorf("no job IDs or filter conditions specified")
}
return nil
}
// NewKillJobsCmd create kill job command
func NewKillJobsCmd(configManager *config.ConfigManager) *cobra.Command {
controller := NewJobController(configManager, "kill")
cmd := &cobra.Command{
Use: "kill",
Short: "Kill jobs",
Long: `Kill jobs by job IDs or filters.
Examples:
xce jobs kill jobid1,jobid2,... # kill jobs by job IDs
xce jobs kill -J jobname # kill jobs by job name
xce jobs kill -q queue_name # kill jobs in a specific queue
xce jobs kill -u username # kill jobs by user`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := controller.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := controller.RunJobsControl(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// add control options
flags := cmd.Flags()
flags.StringVarP(&controller.filter.JobName, "job_name", "J", "", "filter by job name")
flags.StringVarP(&controller.filter.QueueName, "queue", "q", "", "filter by queue name")
flags.StringVarP(&controller.filter.Users, "user", "u", "", "filter by user")
flags.BoolVarP(&controller.filter.AllJobs, "all", "a", false, "select all jobs")
return cmd
}
// NewStopJobsCmd create stop job command
func NewStopJobsCmd(configManager *config.ConfigManager) *cobra.Command {
controller := NewJobController(configManager, "stop")
cmd := &cobra.Command{
Use: "stop",
Short: "Stop jobs",
Long: `Stop jobs by job IDs or filters.
Examples:
xce jobs stop jobid1,jobid2,... # stop jobs by job IDs
xce jobs stop -J jobname # stop jobs by job name
xce jobs stop -q queue_name # stop jobs in a specific queue
xce jobs stop -u username # stop jobs by user`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := controller.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := controller.RunJobsControl(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// add control options
flags := cmd.Flags()
flags.StringVarP(&controller.filter.JobName, "job_name", "J", "", "filter by job name")
flags.StringVarP(&controller.filter.QueueName, "queue", "q", "", "filter by queue name")
flags.StringVarP(&controller.filter.Users, "user", "u", "", "filter by user")
flags.BoolVarP(&controller.filter.AllJobs, "all", "a", false, "select all jobs")
return cmd
}
// NewResumeJobsCmd create resume job command
func NewResumeJobsCmd(configManager *config.ConfigManager) *cobra.Command {
controller := NewJobController(configManager, "resume")
cmd := &cobra.Command{
Use: "resume",
Short: "Resume jobs",
Long: `Resume jobs by job IDs or filters.
Examples:
xce jobs resume jobid1,jobid2,... # resume jobs by job IDs
xce jobs resume -J jobname # resume jobs by job name
xce jobs resume -u username # resume jobs by user`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := controller.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := controller.RunJobsControl(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// add control options
flags := cmd.Flags()
flags.StringVarP(&controller.filter.JobName, "job_name", "J", "", "filter by job name")
flags.StringVarP(&controller.filter.Users, "user", "u", "", "filter by user")
flags.BoolVarP(&controller.filter.AllJobs, "all", "a", false, "select all jobs")
return cmd
}
func NewCheckPointJobsCmd(configManager *config.ConfigManager) *cobra.Command {
controller := NewJobController(configManager, "checkpoint")
cmd := &cobra.Command{
Use: "checkpoint",
Short: "Checkpoint jobs",
Long: `Checkpoint jobs by job IDs or filters.
Examples:
xce jobs checkpoint jobid1,jobid2,... # checkpoint jobs by job IDs`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := controller.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := controller.RunJobsControl(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// add control options
return cmd
}
func NewRestartJobsCmd(configManager *config.ConfigManager) *cobra.Command {
restarter := NewJobRestart(configManager)
cmd := &cobra.Command{
Use: "restart",
Short: "Restart jobs",
Long: `Restart jobs by job ID.
Examples:
xce jobs restart jobid # restart job by jobid
xce jobs restart -m hostname # restart jobs by hostname`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := restarter.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := restarter.RunJobsRestart(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// add control options
flags := cmd.Flags()
flags.StringVarP(&restarter.askedHosts, "hostname", "m", "", "restart on the specific host name")
return cmd
}

View file

@ -0,0 +1,129 @@
package jobs
import (
"fmt"
"path/filepath"
"regexp"
"strings"
"xcecli/common/request"
"xcecli/models"
)
type fileTransfer struct {
tranfers []*transferInfo
uploaded []models.UploadedFile
}
func newFileTransfer(uploadFiles []string) *fileTransfer {
f := &fileTransfer{tranfers: make([]*transferInfo, 0), uploaded: make([]models.UploadedFile, 0)}
for _, uploadFile := range uploadFiles {
transfer, err := parseFileTransfer(uploadFile)
if err != nil {
errMsg := request.ExtractErrorMessage(err)
fmt.Printf("parse file transfer config failed: [%s]", errMsg)
continue
}
f.tranfers = append(f.tranfers, transfer)
}
return f
}
func (f *fileTransfer) uploadFiles(postFunc func([]string) (*models.UploadResponse, error)) error {
// upload files
files := make([]string, 0)
for _, transfer := range f.tranfers {
if transfer.isUpload {
files = append(files, transfer.sourcePath)
}
}
if len(files) > 0 {
resp, err := postFunc(files)
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("upload files failed: [%s]", errMsg)
}
if resp.Data.Status == "accepted" {
f.uploaded = resp.Data.UploadedFiles
} else {
return fmt.Errorf("upload files failed: %v", resp.Data.Failed)
}
}
return nil
}
// find the matching uploaded file
func (f *fileTransfer) findMatchingUploadedFile(transfer *transferInfo) *models.UploadedFile {
// get the source file name
srcFileName := filepath.Base(transfer.sourcePath)
// iterate the uploaded files
for _, uploadedFile := range f.uploaded {
// if the source file name matches
if uploadedFile.SrcFile == srcFileName {
return &uploadedFile
}
}
return nil
}
func (f *fileTransfer) buildTransferFileList() []string {
var transferList []string
for _, transfer := range f.tranfers {
if transfer.isUpload {
// find the matching uploaded file
uploadedFile := f.findMatchingUploadedFile(transfer)
if uploadedFile == nil {
fmt.Printf("warning: no matching uploaded file (source file: %s, dest file: %s)\n",
transfer.sourcePath, transfer.destPath)
continue
}
transferList = append(transferList, fmt.Sprintf("%s %s %s",
uploadedFile.WebURL,
transfer.operator,
transfer.destPath))
} else {
// for the files that do not need to be uploaded, add them to the transfer list
transferList = append(transferList, fmt.Sprintf("%s %s %s",
transfer.destPath,
transfer.operator,
transfer.sourcePath))
}
}
return transferList
}
// transferInfo upload file related information
type transferInfo struct {
sourcePath string // source file path
operator string // operator
destPath string // dest file path
isUpload bool // is upload
}
// parseFileTransfer parse the file transfer string.
// Accepts "src > dst" style specs or a plain path (treated as upload to basename).
func parseFileTransfer(transfer string) (*transferInfo, error) {
re := regexp.MustCompile(`^\s*([^<>]+?)\s*(>|><|<>|<<|<)\s*([^<>]+?)\s*$`)
matches := re.FindStringSubmatch(transfer)
if len(matches) == 4 {
operator := strings.TrimSpace(matches[2])
isUpload := operator == ">" || operator == "><" || operator == "<>"
return &transferInfo{
sourcePath: strings.TrimSpace(matches[1]),
operator: operator,
destPath: strings.TrimSpace(matches[3]),
isUpload: isUpload,
}, nil
}
// plain path — treat as upload to basename
plain := strings.TrimSpace(transfer)
if plain != "" {
return &transferInfo{
sourcePath: plain,
operator: ">",
destPath: filepath.Base(plain),
isUpload: true,
}, nil
}
return nil, fmt.Errorf("cannot parse the file transfer config: [%s]", transfer)
}

View file

@ -0,0 +1,43 @@
package jobs
import (
"path/filepath"
"testing"
)
func TestParseFileTransfer_ExplicitOperator(t *testing.T) {
ti, err := parseFileTransfer("/tmp/test.txt > remote.txt")
if err != nil {
t.Fatal(err)
}
if ti.sourcePath != "/tmp/test.txt" || ti.destPath != "remote.txt" || !ti.isUpload {
t.Errorf("unexpected: %+v", ti)
}
}
func TestParseFileTransfer_PlainPath(t *testing.T) {
// plain path with no operator: treat as upload to basename
ti, err := parseFileTransfer("/tmp/test.txt")
if err != nil {
t.Fatal(err)
}
if ti.sourcePath != "/tmp/test.txt" {
t.Errorf("sourcePath: got %q want /tmp/test.txt", ti.sourcePath)
}
if ti.destPath != filepath.Base("/tmp/test.txt") {
t.Errorf("destPath: got %q want %q", ti.destPath, filepath.Base("/tmp/test.txt"))
}
if !ti.isUpload {
t.Error("expected isUpload=true for plain path")
}
}
func TestParseFileTransfer_PlainFilename(t *testing.T) {
ti, err := parseFileTransfer("myfile.sh")
if err != nil {
t.Fatal(err)
}
if ti.sourcePath != "myfile.sh" || ti.destPath != "myfile.sh" || !ti.isUpload {
t.Errorf("unexpected: %+v", ti)
}
}

View file

@ -0,0 +1,36 @@
package jobs
import (
"xcecli/config"
"github.com/spf13/cobra"
)
// NewJobsCmd creates a jobs query command
func NewJobsCmd(configManager *config.ConfigManager) *cobra.Command {
// Create the parent jobs command
cmd := &cobra.Command{
Use: "jobs",
GroupID: "query",
Short: "Manage jobs",
Long: `Manage jobs in the system.
Use 'xce jobs list' to query jobs information.
Use 'xce jobs <action>' to submit/kill/stop/resume/checkpoint/restart jobs.`,
Run: func(cmd *cobra.Command, args []string) {
// Display help when no subcommand is specified
cmd.Help()
},
}
cmd.AddCommand(NewListJobsCmd(configManager))
// Add submit command to jobs command
cmd.AddCommand(NewSubmitJobsCmd(configManager))
// Add job control sub-commands to jobs command
cmd.AddCommand(NewKillJobsCmd(configManager))
cmd.AddCommand(NewStopJobsCmd(configManager))
cmd.AddCommand(NewResumeJobsCmd(configManager))
cmd.AddCommand(NewCheckPointJobsCmd(configManager))
cmd.AddCommand(NewRestartJobsCmd(configManager))
return cmd
}

View file

@ -0,0 +1,369 @@
package jobs
import (
"fmt"
"strings"
"xcecli/common/output"
"xcecli/common/request"
"xcecli/config"
job_filter "xcecli/filters"
"xcecli/models"
"github.com/spf13/cobra"
)
var (
resp models.JobsResponse
loopCount int
pageToken string
arrayJobsStatsResp models.ArrayJobsStatsResponse
)
// JobsQuerier struct for querying jobs
type JobsQuerier struct {
configManager *config.ConfigManager
filter *job_filter.JobsFilter
fields string
jsonOutput bool
noHeader bool
verbose bool
wideFormat bool // wide format
arrayFlag bool // show array job statistics
arrayElementsFlag bool // show array job elements (sub-tasks of array jobs)
}
// NewJobsQuerier creates a new jobs querier
func NewJobsQuerier(configManager *config.ConfigManager) *JobsQuerier {
return &JobsQuerier{
configManager: configManager,
filter: &job_filter.JobsFilter{},
}
}
// Parses command line arguments, simplified version that only processes arguments not directly bound to filter
func (jq *JobsQuerier) ParseCmdArgs(cmd *cobra.Command, args []string) error {
// Process JobIDs parameter, which needs special handling
// input multiple jobids, it will use , to connect
if len(args) > 0 {
jq.filter.JobIDs = strings.Join(args, ",")
}
// Define mutual exclusions for verbose flag
if jq.verbose {
conflicts := make([]string, 0)
if jq.wideFormat {
conflicts = append(conflicts, "--wided (-w)")
}
if jq.fields != "" {
conflicts = append(conflicts, "--output (-o)")
}
if len(conflicts) > 0 {
return fmt.Errorf("flag --verbose (-l) cannot be used with: %s\n\n%s",
strings.Join(conflicts, ", "), cmd.UsageString())
}
}
return nil
}
// Run executes the jobs query command
func (jq *JobsQuerier) RunJobsList() error {
// Get server information
serverInfo, _ := jq.configManager.GetServerInfo()
// create API client
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
// if array job statistics
if jq.arrayFlag {
if jq.verbose {
fmt.Println("not support -A and -l parameters together")
return nil
}
return jq.runArrayJobStats(serverInfo, apiClient)
}
// if array job elements (sub-tasks of array jobs)
if jq.arrayElementsFlag {
return jq.runArrayJobElements(serverInfo, apiClient)
}
// the following is the regular job query logic
// Build query parameters
params := make(map[string]string)
//fmt.Println(params)
// Get filter string
filterStr, err := jq.filter.HandleJobFilter("")
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to get parameter: [%s]", errMsg)
} else if filterStr != "" {
params["filter"] = filterStr
}
// Field mapping
fieldsMap := models.JobFieldsMap
if jq.verbose && jq.fields == "" {
for header := range fieldsMap {
jq.fields += header + ","
}
jq.fields = strings.TrimSuffix(jq.fields, ",")
}
// Process field selection
if jq.fields != "" {
jq.fields = request.CleanFields(jq.fields)
mappedFields, err := request.CheckAndMapFields(jq.fields, fieldsMap)
if err != nil {
return err
}
params["fields"] = strings.Join(mappedFields, ",")
}
// create page printer
pagePrinter := output.NewPagePrinter(output.DataTypeJobs, output.WithFields(jq.fields),
output.WithFormatJson(jq.jsonOutput), output.WithNoHeader(jq.noHeader),
output.WithVerbose(jq.verbose), output.WithWideFormat(jq.wideFormat))
// record if the first page has been processed
isFirstPage := true
// Loop to get all pages of data
loopCount = 0
pageToken = ""
for {
if loopCount > 0 && pageToken == "" {
break
}
// Only add page_token to params if it's not empty
//if pageToken != "" {
//params["page_token"] = pageToken
//}
err = apiClient.GetRequestFromAPIServer(serverInfo.Token, pageToken, serverInfo.CACert, "/xce/v1/jobs", &resp, params)
if err != nil {
return err
}
if len(resp.Data.Rows) == 0 {
if isFirstPage {
fmt.Println("no job found")
return nil
}
}
// process the first page
if isFirstPage {
// start printing process
pagePrinter.StartPrinting()
//
pagePrinter.PrepareForPrinting(&resp)
isFirstPage = false
}
// print current page
pagePrinter.PrintPage(&resp)
// update page token and loop count
pageToken = resp.Data.Page_token
loopCount++
}
// finish printing
pagePrinter.Finish()
return nil
}
// runArrayJobStats process array job statistics
func (jq *JobsQuerier) runArrayJobStats(serverInfo *config.APIServerInfo, apiClient *request.APIClient) error {
// set query parameters, request array job statistics
params := map[string]string{
"type": "array_stats",
}
params["type"] = "array_stats"
filterStr, err := jq.filter.HandleJobFilter("array_stats")
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to get parameter: [%s]", errMsg)
}
if filterStr != "" {
params["filter"] = filterStr
}
// create response object
// create array job page printer
pagePrinter := output.NewPagePrinter(output.DataTypeArrayJobs, output.WithFields(jq.fields),
output.WithFormatJson(jq.jsonOutput), output.WithNoHeader(jq.noHeader),
output.WithVerbose(jq.verbose), output.WithWideFormat(jq.wideFormat))
// record if the first page has been processed
isFirstPage := true
// Loop to get all pages of data
loopCount = 0
pageToken = ""
for {
if loopCount > 0 && pageToken == "" {
break
}
err = apiClient.GetRequestFromAPIServer(serverInfo.Token, pageToken, serverInfo.CACert, "/xce/v1/jobs", &arrayJobsStatsResp, params)
if err != nil {
return err
}
if len(arrayJobsStatsResp.Data.Rows) == 0 {
if isFirstPage {
fmt.Println("no job found")
return nil
}
}
// process the first page
if isFirstPage {
// start printing process
pagePrinter.StartPrinting()
//
pagePrinter.PrepareForPrinting(&arrayJobsStatsResp)
isFirstPage = false
}
// print current page
pagePrinter.PrintPage(&arrayJobsStatsResp)
// update page token and loop count
pageToken = arrayJobsStatsResp.Data.Page_token
loopCount++
}
// finish printing
pagePrinter.Finish()
return nil
}
// runArrayJobElements process array job elements
func (jq *JobsQuerier) runArrayJobElements(serverInfo *config.APIServerInfo, apiClient *request.APIClient) error {
// set query parameters, request array job elements
params := map[string]string{
"filter": "array_idx:ne:0", // query array_idx not equal to 0, that is, the sub-tasks of array jobs
}
// create response object
var jobsResp models.JobsResponse
// request array job elements data
err := apiClient.GetRequestFromAPIServer(serverInfo.Token, "", serverInfo.CACert, "/xce/v1/jobs?limit=1000", &jobsResp, params)
if err != nil {
return err
}
// create array job page printer
pagePrinter := output.NewPagePrinter(output.DataTypeArrayElements, output.WithFields(jq.fields),
output.WithFormatJson(jq.jsonOutput), output.WithNoHeader(jq.noHeader),
output.WithVerbose(jq.verbose), output.WithWideFormat(jq.wideFormat))
// start printing process
pagePrinter.StartPrinting()
// prepare and print data
pagePrinter.PrepareForPrinting(&jobsResp)
pagePrinter.PrintPage(&jobsResp)
// finish printing
pagePrinter.Finish()
return nil
}
func NewListJobsCmd(configManager *config.ConfigManager) *cobra.Command {
jobsQuerier := NewJobsQuerier(configManager)
// Create the list subcommand
listCmd := &cobra.Command{
Use: "list",
Short: "Query jobs info",
Long: `Query jobs detail info.
Example:
xce jobs list # list all jobs
xce jobs list jobid0,jobid1,...jobidn # list jobs by job id
xce jobs list -a # list run,done,pending,suspended jobs
xce jobs list -d # list done jobs
xce jobs list -d -r # list done and running jobs
xce jobs list -d -r -s # list done and running and suspended jobs
xce jobs list -d -r -s -p # list done and running and suspended and pending jobs
xce jobs list -d -u all # list done jobs by all users
xce jobs list -d -r -s -p -u username # list done and running and suspended and pending jobs by user
xce jobs list -d -r -s -p -u username -q queuename # list done and running and suspended and pending jobs by user and queue
xce jobs list -j jobname # list jobs by job name
xce jobs list -Jd jobdescription # list jobs by job description
xce jobs list --Lp licenseprojectname # list license project jobs
xce jobs list -o "jobid submit_user ..." # list jobs by fields
xce jobs list -o "jobid submit_user ..." --json # list jobs by fields and output in json format
xce jobs list -p # list pending jobs
xce jobs list -p -u username # list pending jobs by user
xce jobs list -p -u all # list pending jobs by all users
xce jobs list -P projectname # show project jobs
xce jobs list -q queuename # list jobs by queue
xce jobs list -q queuename -u username # list jobs by user in queue
xce jobs list -r # list running jobs
xce jobs list -r -u username # list running jobs by user
xce jobs list -s # list suspended jobs
xce jobs list -s -u username # list suspended jobs by user
xce jobs list -s -u all # list suspended jobs by all users
xce jobs list --sla serviceclassname # list jobs by service class
xce jobs list -u all # list jobs by all users
xce jobs list -u username # list jobs by user
xce jobs list -u username all # list jobs by all user
xce jobs list -u username -q queuename # list jobs by user and queue
xce jobs list -u all -q queuename # list jobs by all users and queue
xce jobs list -q -u all # list jobs by all users and all queues
xce jobs list -A # list array job statistics
xce jobs list -W # list array job elements
xce jobs list -m host1 # list jobs executing on specific host
xce jobs list -m host1,host2 # list jobs executing on multiple hosts`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := jobsQuerier.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := jobsQuerier.RunJobsList(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// Add command line parameters
flags := listCmd.Flags()
flags.BoolVarP(&jobsQuerier.filter.AllJobInfoFlag, "job", "a", false, "show all jobs status")
flags.StringVarP(&jobsQuerier.filter.User, "user", "u", "", "filter by user")
flags.StringVarP(&jobsQuerier.filter.Queue, "queue", "q", "", "filter by queue")
flags.BoolVarP(&jobsQuerier.filter.PendingFlag, "pending", "p", false, "show pending jobs")
flags.StringVarP(&jobsQuerier.filter.ProjectName, "project", "P", "", "show project jobs")
flags.BoolVarP(&jobsQuerier.filter.RunningFlag, "running", "r", false, "show running jobs")
flags.BoolVarP(&jobsQuerier.filter.DoneFlag, "done", "d", false, "show done jobs")
flags.BoolVarP(&jobsQuerier.filter.SSUSPFlag, "ssuspFlag", "s", false, "show suspended jobs")
flags.StringVarP(&jobsQuerier.filter.JobName, "job_name", "J", "", "filter by job name")
flags.StringVarP(&jobsQuerier.filter.LicenseProject, "Lp", "", "", "show license project jobs")
flags.StringVarP(&jobsQuerier.filter.JobDescription, "Jd", "", "", "show job description")
flags.StringVarP(&jobsQuerier.filter.ServiceClass, "sla", "", "", "show job by service class")
flags.BoolVarP(&jobsQuerier.arrayFlag, "array", "A", false, "show array job statistics")
//flags.BoolVarP(&jobsQuerier.arrayElementsFlag, "wide", "w", false, "show array job elements (sub-tasks of array jobs)")
flags.StringVarP(&jobsQuerier.filter.ExecHosts, "exechost", "m", "", "filter jobs by execution host, comma separated for multiple hosts")
flags.StringVarP(&jobsQuerier.fields, "output", "o", "", "output fields for jobs")
flags.BoolVarP(&jobsQuerier.wideFormat, "wided", "w", false, "output in wide format")
flags.BoolVarP(&jobsQuerier.verbose, "verbose", "l", false, "show job detail")
flags.BoolVarP(&jobsQuerier.jsonOutput, "json", "", false, "output in JSON format")
flags.BoolVarP(&jobsQuerier.noHeader, "noheader", "n", false, "output no header")
return listCmd
}

View file

@ -0,0 +1,86 @@
package jobs
import (
"encoding/json"
"fmt"
"xcecli/common/request"
"xcecli/config"
"xcecli/models"
"github.com/spf13/cobra"
)
func NewJobRestart(configManager *config.ConfigManager) *JobRestart {
return &JobRestart{
configManager: configManager,
jobID: "",
askedHosts: "",
}
}
type JobRestart struct {
configManager *config.ConfigManager
jobID string
askedHosts string
}
func (c *JobRestart) ParseCmdArgs(cmd *cobra.Command, args []string) error {
for _, arg := range args {
if arg != "" && c.jobID == "" {
c.jobID = arg
}
}
// check if any filter conditions are set
if c.jobID == "" {
// if no job IDs or filter conditions are specified, default to processing all jobs of the current user
return fmt.Errorf("no job ID is specified")
}
return nil
}
func (c *JobRestart) RunJobsRestart() error {
// Get server information
serverInfo, _ := c.configManager.GetServerInfo()
// Create API client
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
return c.restartJobsByID(apiClient, serverInfo.Token, serverInfo.CACert)
}
func (c *JobRestart) restartJobsByID(apiClient *request.APIClient, token string, certPath string) error {
url := fmt.Sprintf("/xce/v1/jobs/%s/restart", c.jobID)
req := &models.JobRestartRequest{
AskedHosts: c.askedHosts,
}
return c.printJobRestartResponse(req, apiClient, token, certPath, url)
}
func (c *JobRestart) printJobRestartResponse(req interface{}, apiClient *request.APIClient, token string, certPath string, url string) error {
reqData, err := json.Marshal(req)
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to encode request data: [%s]", errMsg)
}
var resp models.JobRestartResponse
err = apiClient.PostRequestToAPIServer(
token,
certPath,
url,
reqData,
&resp,
)
if err != nil {
return err
}
row := resp.Data
if row != nil {
fmt.Printf("%s\n", row.Message)
}
return nil
}

View file

@ -0,0 +1,734 @@
package jobs
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"regexp"
"runtime"
"strings"
"time"
"xcecli/common/request"
"xcecli/common/utils"
"xcecli/config"
logger "xcecli/config"
"xcecli/models"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"go.uber.org/zap"
)
// JobSubmitter structure for managing job submission from the CLI parameters and operations
type JobSubmitter struct {
configManager *config.ConfigManager
queue string
resReq string
beginTime string
termTime string
jobName string
jobDescription string
cpuLimit string
command string
runLimit string
coreLimit string
fileLimit string
memLimit string
swapLimit string
dataLimit string
processLimit string
stackLimit string
threadLimit string
useShellLimit bool
useSystemEnv bool
subEnvVars []string
cwd string
uploadFiles []string
processorRange string
infile string
outfile string
errfile string
outdir string
configFile string
interactive bool
useTTY bool
useXJob bool
projectName string
askedHosts string
launchType string
preExecCmd string
postExecCmd string
}
// NewJobSubmitter creates a new JobSubmitter instance
func NewJobSubmitter(configManager *config.ConfigManager) *JobSubmitter {
return &JobSubmitter{
configManager: configManager,
}
}
// Parse parses command line arguments into JobSubmitter struct
func (js *JobSubmitter) ParseCmdArgs(cmd *cobra.Command, args []string) error {
// check if the -R flag is used more than once
if cmd.Flags().Changed("resreq") {
count := 0
cmd.Flags().Visit(func(f *pflag.Flag) {
if f.Name == "resreq" {
count++
}
})
if count > 1 {
return fmt.Errorf("error: the -R/--resreq flag can only be used once")
}
}
if js.interactive {
if js.useTTY && js.useXJob {
return fmt.Errorf("error: cannot use both --tty and --xjob")
}
}
// check if there is standard input redirection
fileInfo, err := os.Stdin.Stat()
if err == nil && (fileInfo.Mode()&os.ModeCharDevice) == 0 {
if tmpFileName, err := js.handleStdin(); err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("error reading from stdin: [%s]", errMsg)
} else if tmpFileName != "" {
defer os.Remove(tmpFileName)
}
}
// if the config file is specified, parse the config file first
if js.configFile != "" {
return js.parseConfigFile()
}
// command line parameters will override the settings in the config file
if len(args) > 0 {
js.command = strings.Join(args, " ")
}
return nil
}
func (js *JobSubmitter) handleStdin() (string, error) {
content, err := io.ReadAll(os.Stdin)
if err != nil {
return "", err
}
if len(content) == 0 {
return "", nil
}
tmpFile, err := os.CreateTemp("", "xcecli_stdin_*.xce")
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return "", fmt.Errorf("failed to create temporary file: [%s]", errMsg)
}
defer tmpFile.Close()
if _, err := tmpFile.Write(content); err != nil {
errMsg := request.ExtractErrorMessage(err)
return "", fmt.Errorf("failed to write to temporary file: [%s]", errMsg)
}
js.configFile = tmpFile.Name()
return js.configFile, nil
}
// parseConfigFile parse the parameters in the config file
func (js *JobSubmitter) parseConfigFile() error {
// check if the file exists
fileInfo, err := os.Stat(js.configFile)
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("config file %s does not exist or cannot be accessed: [%s]", js.configFile, errMsg)
}
if fileInfo.IsDir() {
return fmt.Errorf("%s is a directory, not a valid config file", js.configFile)
}
// if the file is a temporary file, delete it when the function ends
if strings.Contains(js.configFile, "xcecli_stdin_") {
defer os.Remove(js.configFile)
}
// read the config file content
content, err := os.ReadFile(js.configFile)
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to read the config file: %s", errMsg)
}
lines := strings.Split(string(content), "\n")
fmt.Printf("config file %s contains %d lines of commands\n", js.configFile, len(lines))
// create the main command
rootCmd := NewSubmitJobsCmd(js.configManager)
// collect all parameters and commands
var allParams []string
var allCmdParts []string
// process each line
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue // skip empty lines
}
if strings.HasPrefix(line, "#xce job submit") {
// process the parameters in the comment line
paramLine := strings.TrimPrefix(line, "#xce job submit")
paramLine = strings.TrimSpace(paramLine)
if paramLine != "" {
parts := strings.Fields(paramLine)
// separate the parameters and command parts
for j := 0; j < len(parts); j++ {
if strings.HasPrefix(parts[j], "-") {
allParams = append(allParams, parts[j])
// if the parameter has a value and is not a -
if j+1 < len(parts) && !strings.HasPrefix(parts[j+1], "-") {
allParams = append(allParams, parts[j+1])
j++
}
} else {
// non-parameter part as command
cmdPart := strings.Join(parts[j:], " ")
if cmdPart != "" {
allCmdParts = append(allCmdParts, cmdPart)
}
break
}
}
}
} else {
// non-comment line as command part
allCmdParts = append(allCmdParts, line)
}
}
// merge all command parts
fullCommand := strings.Join(allCmdParts, " ")
// merge all parameters
fullArgs := append(allParams, strings.Fields(fullCommand)...)
fmt.Println("merged full command:")
fmt.Printf("xce jobs submit %s\n", strings.Join(fullArgs, " "))
// execute the merged command
rootCmd.SetArgs(fullArgs)
if err := rootCmd.Execute(); err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to execute the command: [%s]", errMsg)
}
os.Exit(0)
return nil
}
// BuildJobRequest
func (js *JobSubmitter) BuildJobRequest() (*models.JobSubmitRequest, error) {
// create the job request with only required fields
jobReq := &models.JobSubmitRequest{
Command: js.command,
}
if js.interactive {
if js.useTTY {
jobReq.Interactive = 1 // TTY mode
} else if js.useXJob {
jobReq.Interactive = 2 // XJob mode
} else {
jobReq.Interactive = 3 // only interactive, no special mode
}
} else {
jobReq.Interactive = 0 // default value
}
// Helper function to format limit values with space
formatLimit := func(value string) string {
if value == "" {
return ""
}
re := regexp.MustCompile(`^(\d+)(MB|GB|TB|PB|[MGTP])$`)
matches := re.FindStringSubmatch(strings.ToUpper(value))
if matches != nil {
return matches[1] + " " + matches[2]
}
return value
}
// Set resource limits
jobReq.SwapLimit = formatLimit(js.swapLimit)
jobReq.DataLimit = formatLimit(js.dataLimit)
jobReq.MemLimit = formatLimit(js.memLimit)
jobReq.FileLimit = formatLimit(js.fileLimit)
jobReq.CoreLimit = formatLimit(js.coreLimit)
jobReq.ProcessLimit = formatLimit(js.processLimit)
jobReq.StackLimit = formatLimit(js.stackLimit)
jobReq.ThreadLimit = formatLimit(js.threadLimit)
jobReq.CPULimit = formatLimit(js.cpuLimit)
jobReq.RunLimit = formatLimit(js.runLimit)
// Set other fields if explicitly set
jobReq.QueueName = js.queue
jobReq.Resreq = js.resReq
jobReq.JobName = js.jobName
jobReq.JobDescription = js.jobDescription
// Parse processor range if specified
if js.processorRange != "" {
parts := strings.Split(js.processorRange, ",")
if len(parts) == 2 {
// Try to parse min and max processors
minProc, err := utils.ParseInt(parts[0])
if err != nil {
return nil, errors.New("invalid min processor value")
}
maxProc, err := utils.ParseInt(parts[1])
if err != nil {
return nil, errors.New("invalid max processor value")
}
jobReq.NumProcessors = minProc
jobReq.MaxNumProcessors = maxProc
} else {
// Single value case
numProc, err := utils.ParseInt(js.processorRange)
if err != nil {
return nil, errors.New("invalid processor value")
}
jobReq.NumProcessors = numProc
}
}
// parse the begin time
if js.beginTime != "" {
// Parse the time in local timezone
parsedTime, err := utils.ParseTime(js.beginTime)
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return nil, fmt.Errorf("invalid begin time value and %s", errMsg)
}
jobReq.BeginTime = parsedTime
}
// If the date is in the past, add appropriate time
// parse the end time
if js.termTime != "" {
parsedTime, err := utils.ParseTime(js.termTime)
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return nil, fmt.Errorf("invalid end time value and %s", errMsg)
}
jobReq.TermTime = parsedTime
}
// if shell limit is enabled, get the system limits
if js.useShellLimit {
limits := utils.NewSystemLimits()
// set the limits in the job request
if val, err := limits.GetCPULimit(); err == nil && val > 0 {
jobReq.CPULimit = fmt.Sprintf("%d", val)
}
if val, err := limits.GetFileLimit(); err == nil && val > 0 {
jobReq.FileLimit = fmt.Sprintf("%d", val)
}
if val, err := limits.GetDataLimit(); err == nil && val > 0 {
jobReq.DataLimit = fmt.Sprintf("%d", val)
}
if val, err := limits.GetStackLimit(); err == nil && val > 0 {
jobReq.StackLimit = fmt.Sprintf("%d", val)
}
if val, err := limits.GetCoreLimit(); err == nil && val > 0 {
jobReq.CoreLimit = fmt.Sprintf("%d", val)
}
if val, err := limits.GetProcessLimit(); err == nil && val > 0 {
jobReq.ProcessLimit = fmt.Sprintf("%d", val)
}
if val, err := limits.GetThreadLimit(); err == nil && val > 0 {
jobReq.ThreadLimit = fmt.Sprintf("%d", val)
}
if val, err := limits.GetMemLimit(); err == nil && val > 0 {
jobReq.MemLimit = fmt.Sprintf("%d", val)
}
if val, err := limits.GetSwapLimit(); err == nil && val > 0 {
jobReq.SwapLimit = fmt.Sprintf("%d", val)
}
}
// if system environment variables are enabled, get and set them
if js.useSystemEnv {
systemEnv := utils.GetSystemEnv()
filteredEnv := []string{}
filteredEnv = append(filteredEnv, systemEnv...)
jobReq.Env = filteredEnv
}
// Add submitted environment variables
if len(js.subEnvVars) > 0 {
jobReq.Env = append(jobReq.Env, js.getEnvironmentVariables()...)
}
if js.infile != "" {
jobReq.InFile = js.infile
}
if js.outfile != "" {
jobReq.OutFile = js.outfile
}
if js.errfile != "" {
jobReq.ErrFile = js.errfile
}
if js.outdir != "" {
jobReq.OutDir = js.outdir
}
if js.projectName != "" {
jobReq.ProjectName = js.projectName
}
if js.cwd != "" {
jobReq.CWD = js.cwd
} else {
cwd, _ := os.Getwd()
jobReq.CWD = cwd
}
if js.askedHosts != "" {
jobReq.AskedHosts = js.askedHosts
}
if js.launchType != "" {
jobReq.LaunchType = js.launchType
}
if js.preExecCmd != "" {
jobReq.PreExecCmd = js.preExecCmd
}
if js.postExecCmd != "" {
jobReq.PostExecCmd = js.postExecCmd
}
return jobReq, nil
}
// Run executes the job submission operation
func (js *JobSubmitter) RunJobsSubmit() error {
// Get server info and validate authentication
serverInfo, _ := js.configManager.GetServerInfo()
if js.command == "" {
return fmt.Errorf("please provide the command to execute")
}
if js.infile != "" {
js.uploadFiles = append(js.uploadFiles, fmt.Sprintf("%s > %s", js.infile, js.infile))
}
if js.outfile != "" {
js.uploadFiles = append(js.uploadFiles, fmt.Sprintf("%s < %s", js.outfile, js.outfile))
}
if js.errfile != "" {
js.uploadFiles = append(js.uploadFiles, fmt.Sprintf("%s < %s", js.errfile, js.errfile))
}
fileExists := func(path string) error {
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("error: the file %s does not exist, please check the file name or path", path)
}
if info.IsDir() {
return fmt.Errorf("error: the file %s is a directory, please check the file name or path", path)
}
return nil
}
if js.infile != "" {
if err := fileExists(js.infile); err != nil {
return err
}
}
if js.outdir != "" {
info, err := os.Stat(js.outdir)
if os.IsNotExist(err) {
return fmt.Errorf("error: the output directory %s does not exist", js.outdir)
}
if !info.IsDir() {
return fmt.Errorf("error: the output directory %s is not a directory", js.outdir)
}
}
// create the API client and submit the job
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
var transferFile []string
if len(js.uploadFiles) > 0 {
fileTrans := newFileTransfer(js.uploadFiles)
err := fileTrans.uploadFiles(func(s []string) (*models.UploadResponse, error) {
return apiClient.PostFile("Bearer "+serverInfo.Token, s)
})
if err != nil {
return err
}
transferFile = fileTrans.buildTransferFileList()
}
// build the job request
jobReq, err := js.BuildJobRequest()
if err != nil {
return err
}
jobReq.TransferFiles = transferFile
jobResp, err := apiClient.SubmitJob("Bearer "+serverInfo.Token, jobReq)
logger.Debug("jobResp", zap.Any("jobResp", jobResp), zap.Error(err))
logger.Info("jobResp", zap.Any("jobResp", jobResp), zap.Error(err))
if err != nil {
return err
}
fmt.Printf("%s\n", jobResp.Data.Message)
logger.Debug("job submitted successfully", zap.String("message", jobResp.Data.Message))
logger.Info("job submitted successfully", zap.String("message", jobResp.Data.Message))
if js.interactive {
fmt.Println("<<waiting for dispatch ...>>")
url, err := js.waitForInteractiveURL(apiClient, serverInfo.Token, fmt.Sprintf("%d", jobResp.Data.JobID))
if err != nil {
return err
}
fmt.Printf("interactive session URL: %s\n", url)
// automatically open the browser
if err := openBrowser(url); err != nil {
errMsg := request.ExtractErrorMessage(err)
fmt.Printf("warning: failed to open browser: [%s]\n", errMsg)
fmt.Println("Please manually open the URL in your browser.")
}
}
return nil
}
func (js *JobSubmitter) checkJobStatus(apiClient *request.APIClient, token string, jobID string) (string, error) {
var resp models.JobsResponse
err := apiClient.GetRequestFromAPIServer(
"Bearer "+token,
"",
"",
fmt.Sprintf("/xce/v1/jobs/%s", jobID),
&resp,
map[string]string{ /*"fields": "interactive_job_url"*/ },
)
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return "", fmt.Errorf("failed to get job details: [%s]", errMsg)
}
for _, row := range resp.Data.Rows {
switch row.JobStatus {
case "PEND":
return row.JobStatus, nil
case "RUN":
var execHosts models.ExecHosts
if err := json.Unmarshal([]byte(row.ExecHosts.String()), &execHosts); err == nil && len(execHosts.HostSlots) > 0 {
fmt.Println("<<Starting on", execHosts.HostSlots[0].HostName, ">>")
}
return row.JobStatus, nil
default:
return "", fmt.Errorf("job %s is in state %s, please check the job status", jobID, row.JobStatus)
}
}
return "", fmt.Errorf("timeout waiting for job status")
}
func (js *JobSubmitter) checkExtendData(apiClient *request.APIClient, token string, jobID string) (string, error) {
var resp models.JobsResponse
err := apiClient.GetRequestFromAPIServer(
"Bearer "+token,
"",
"",
fmt.Sprintf("/xce/v1/jobs/%s", jobID),
&resp,
map[string]string{"fields": "interactive_job_url"},
)
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return "", fmt.Errorf("failed to get job details: [%s]", errMsg)
}
for _, row := range resp.Data.Rows {
//fmt.Println(row)
if row.InteractiveJobURL != "" {
return row.InteractiveJobURL, nil
}
}
return "", fmt.Errorf("no interactive job URL found")
}
func (js *JobSubmitter) waitForInteractiveURL(apiClient *request.APIClient, token string, jobID string) (string, error) {
timeout := time.After(24 * time.Hour)
timer := time.NewTimer(0)
defer timer.Stop()
for {
select {
case <-timeout:
return "", fmt.Errorf("timeout waiting for interactive URL")
case <-timer.C:
status, err := js.checkJobStatus(apiClient, token, jobID)
if err != nil {
return "", err
}
if status == "PEND" {
timer.Reset(5 * time.Second)
continue
}
if status == "RUN" {
//time.Sleep(45 * time.Second)
return js.checkExtendData(apiClient, token, jobID)
}
return "", fmt.Errorf("job %s is in state %s, please check the job status", jobID, status)
}
}
}
func openBrowser(url string) error {
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("cmd", "/c", "start", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
return err
}
// NewSubCmd creates a command for submitting jobs
func NewSubmitJobsCmd(configManager *config.ConfigManager) *cobra.Command {
jobSubmitter := NewJobSubmitter(configManager)
cmd := &cobra.Command{
Use: "submit",
Short: "Submit job to API Server",
Long: `Submit job to API Server.
Example:
xce jobs submit -q queue_name sleep 10 # submit job to queue queue_name with command "sleep 10"
xce jobs submit -R "select(!mg)" sleep 10 # submit job with resource requirement "select(!mg)"
xce jobs submit -c 10 sleep 10 # submit job with cpu limit 10
xce jobs submit -C 10 sleep 10 # submit job with core limit 10
xce jobs submit -b "2025-03-20 10:00" sleep 10 # submit job with begin time "2025-03-20 10:00"
xce jobs submit -t "2025-03-20 10:00" sleep 10 # submit job with termination time "2025-03-20 10:00"
xce jobs submit -J "test" sleep 10 # submit job with job name "test"
xce jobs submit --Jd "test" sleep 10 # submit job with job description "test"
xce jobs submit -F 1024 sleep 10 # submit job with file limit 1024
xce jobs submit -W 10 sleep 10 # sets the runtime limit of the job
xce jobs submit -M 1024 sleep 10 # sets the memory limit of the job
xce jobs submit -v 1024 sleep 10 # sets the swap limit of the job
xce jobs submit -D 1024 sleep 10 # sets the data limit of the job
xce jobs submit -p 10 sleep 10 # sets the process limit of the job
xce jobs submit -S 1024 sleep 10 # sets the stack limit of the job
xce jobs submit -T 1024 sleep 10 # sets the thread limit of the job
xce jobs submit --ul sleep 10 # use current shell limits
xce jobs submit --env sleep 10 # use system environment variables
xce jobs submit --cwd "/tmp/" sleep 10 # use current working directory for job execution
xce jobs submit -f "t1.txt > t2.txt" sleep 10 # submit job with file "t1.txt" and redirect the output to "t2.txt"
xce jobs submit -q "normal" sleep 10 # submit job to queue "normal" and "priority"
xce jobs submit -n 4,6 sleep 10 # submit job with processor range 4-6
xce jobs submit -P project_name sleep 10 # submit job with project name
xce jobs submit --configfile job.xce # submit job using parameters from a configuration file
xce jobs submit -m "host1 host2" sleep 10 # submit job into host1 host2
xce jobs submit -I --tty sleep 10 # submit interactive job with tty
xce jobs submit -I --xjob sleep 10 # submit interactive job with xjob`,
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {
if err := jobSubmitter.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return
}
if err := jobSubmitter.RunJobsSubmit(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return
}
},
}
// add the flags
flags := cmd.Flags()
// modify the resreq flag
resReqFlag := &pflag.Flag{
Name: "resreq",
Shorthand: "R",
Usage: "specify the resource requirement (can only be used once)",
Value: (*stringValue)(&jobSubmitter.resReq),
NoOptDefVal: "",
}
flags.AddFlag(resReqFlag)
//modify the flags
flags.StringVarP(&jobSubmitter.beginTime, "begin_time", "b", "", "specify the begin time")
flags.StringVarP(&jobSubmitter.coreLimit, "core_limit", "C", "", "specify the core number")
flags.StringVarP(&jobSubmitter.jobName, "job_name", "J", "", "specify the job name")
flags.StringVarP(&jobSubmitter.jobDescription, "Jd", "", "", "specify the job description")
flags.StringVarP(&jobSubmitter.termTime, "term_time", "t", "", "specify the termination time")
flags.StringVarP(&jobSubmitter.cpuLimit, "cpu_limit", "c", "", "specify the cpu limit")
flags.StringVarP(&jobSubmitter.runLimit, "run_limit", "W", "", "specify the run time")
flags.StringVarP(&jobSubmitter.fileLimit, "file_limit", "F", "", "specify the file limit")
flags.StringVarP(&jobSubmitter.memLimit, "mem_limit", "M", "", "specify the memory limit")
flags.StringVarP(&jobSubmitter.swapLimit, "swap_limit", "v", "", "specify the swap limit")
flags.StringVarP(&jobSubmitter.dataLimit, "data_limit", "D", "", "specify the data limit")
flags.StringVarP(&jobSubmitter.processLimit, "process_limit", "p", "", "specify the process limit")
flags.StringVarP(&jobSubmitter.stackLimit, "stack_limit", "S", "", "specify the stack limit")
flags.StringVarP(&jobSubmitter.threadLimit, "thread_limit", "T", "", "specify the thread limit")
flags.BoolVarP(&jobSubmitter.useShellLimit, "ul", "", false, "use current shell limits")
flags.BoolVarP(&jobSubmitter.useSystemEnv, "env", "", false, "use system environment variables")
flags.StringVarP(&jobSubmitter.cwd, "cwd", "", "", "current working directory")
flags.StringArrayVarP(&jobSubmitter.uploadFiles, "file", "f", []string{}, "specify the files to upload")
flags.StringVarP(&jobSubmitter.queue, "queue_name", "q", "", "specify the job queue")
flags.StringVarP(&jobSubmitter.processorRange, "processor_range", "n", "", "specify processor range in format min,max (e.g. 4,6)")
flags.StringVarP(&jobSubmitter.infile, "infile", "i", "", "specify the input file")
flags.StringVarP(&jobSubmitter.outfile, "outfile", "o", "", "specify the output file")
flags.StringVarP(&jobSubmitter.errfile, "errfile", "e", "", "specify the error file")
flags.StringVarP(&jobSubmitter.outdir, "outdir", "d", "", "specify the output directory")
flags.StringVarP(&jobSubmitter.projectName, "project_name", "P", "", "specify the project name")
flags.StringVar(&jobSubmitter.configFile, "configfile", "", "specify the configuration file for job submission")
flags.BoolVarP(&jobSubmitter.interactive, "interactive", "I", false, "submit interactive job")
flags.BoolVar(&jobSubmitter.useTTY, "tty", false, "use TTY for interactive job")
flags.BoolVar(&jobSubmitter.useXJob, "xjob", false, "use XJob for interactive job")
flags.StringVarP(&jobSubmitter.askedHosts, "m", "m", "", "specify the running hosts, with space for multiple values")
flags.StringVarP(&jobSubmitter.launchType, "launch_type", "", "", "specify the launch type")
flags.StringVarP(&jobSubmitter.preExecCmd, "pre_exec", "", "", "specify the pre-execution command")
flags.StringVarP(&jobSubmitter.postExecCmd, "post_exec", "", "", "specify the post-execution command")
return cmd
//
}
// modify the stringValue
type stringValue string
func (s *stringValue) String() string { return string(*s) }
func (s *stringValue) Set(val string) error {
if *s != "" {
return fmt.Errorf("the -R/--resreq flag can only be used once")
}
*s = stringValue(val)
return nil
}
func (s *stringValue) Type() string { return "string" }
func (js *JobSubmitter) getEnvironmentVariables() []string {
var envVars []string
for _, env := range js.subEnvVars {
// Skip environment variables with multiple equal signs
if strings.Count(env, "=") > 1 {
continue
}
envVars = append(envVars, env)
}
return envVars
}

66
src/frontend/xcecli/cmd/main.go Executable file
View file

@ -0,0 +1,66 @@
/*
The MIT License (MIT)
Copyright © 2025 xce
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package main
import (
"fmt"
"os"
"strings"
"xcecli/config"
logger "xcecli/config"
cmd "xcecli/router"
"go.uber.org/zap"
)
func main() {
// init logger
if err := logger.InitLogger(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
os.Exit(1)
}
logger.Info("********** Initialize Logger on client side done!")
// initialize the config manager
configManager, err := config.NewConfigManager()
if err != nil {
fmt.Println("failed to initialize the config manager", zap.Error(err))
os.Exit(1)
}
// ensure the config manager is initialized correctly
if configManager == nil {
fmt.Println("failed to initialize the config manager")
os.Exit(1)
}
// execute the command
if err := cmd.Execute(configManager); err != nil {
fmt.Println(strings.ToLower(err.Error()))
os.Exit(1)
}
}

View file

@ -0,0 +1,137 @@
package permissions
import (
"fmt"
"strings"
"xcecli/common/request"
"xcecli/config"
"xcecli/models"
"github.com/spf13/cobra"
)
var (
resp models.PermsResponse
)
const (
minIDWidth = 7 // Length of "PERM_ID"
minNameWidth = 9 // Length of "PERM_NAME"
padding = 2
maxNameWidth = 60
)
type PermsQuerier struct {
configManager *config.ConfigManager
noHeader bool
fields []string
}
func NewPermsQuerier(configManager *config.ConfigManager) *PermsQuerier {
return &PermsQuerier{
configManager: configManager,
}
}
func NewListPermsCmd(configManager *config.ConfigManager) *cobra.Command {
permsQuerier := NewPermsQuerier(configManager)
listCmd := &cobra.Command{
Use: "list",
Short: "Query permissions info",
Long: `Query permissions detail info.
Example:
xce permissions list # list all permissions`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := permsQuerier.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := permsQuerier.RunPermsList(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
flags := listCmd.Flags()
flags.BoolVarP(&permsQuerier.noHeader, "noheader", "n", false, "output no header")
return listCmd
}
func (pq *PermsQuerier) ParseCmdArgs(cmd *cobra.Command, args []string) error {
// No additional processing needed, all parameters are bound in flags
if len(args) > 0 {
return fmt.Errorf("too many arguments")
}
return nil
}
func (pq *PermsQuerier) RunPermsList() error {
// Get server information
serverInfo, err := pq.configManager.GetServerInfo()
if err != nil {
return fmt.Errorf("failed to get server info: %w", err)
}
// create API client
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
// Build query parameters
params := make(map[string]string)
pq.fields = []string{}
// Make API request
err = apiClient.GetRequestFromAPIServer(serverInfo.Token, "", serverInfo.CACert, "/xce/v1/permissions", &resp, params)
if err != nil {
return err
}
data := resp.Data
if len(data) == 0 {
fmt.Println("no permissions found")
return nil
}
// Calculate column widths for proper alignment
permIDWidth := minIDWidth + padding
permNameWidth := minNameWidth + padding
// Get the maximum ID width (last item)
if len(data) > 0 {
lastIDStr := fmt.Sprintf("%d", len(data))
if len(lastIDStr) > permIDWidth-padding {
permIDWidth = len(lastIDStr) + padding
}
}
// Get maximum permission name width
for _, permStr := range data {
if len(permStr) > permNameWidth-padding {
permNameWidth = len(permStr) + padding
}
}
// Apply max width limit
if permNameWidth > maxNameWidth {
permNameWidth = maxNameWidth
}
// Add headers with proper alignment
if !pq.noHeader {
fmt.Printf("%-*s %-*s\n", permIDWidth, "PERM_ID", permNameWidth, "PERM_NAME")
}
// Print permissions with auto-incrementing IDs and aligned columns
for i, permStr := range data {
// Truncate long permission names
displayPermStr := permStr
if len(permStr) > permNameWidth-padding {
displayPermStr = permStr[:permNameWidth-padding] + ".."
}
fmt.Printf("%-*d %-*s\n", permIDWidth, i+1, permNameWidth, displayPermStr)
}
return nil
}

View file

@ -0,0 +1,26 @@
package permissions
import (
"xcecli/config"
"github.com/spf13/cobra"
)
func NewPermsCmd(configManager *config.ConfigManager) *cobra.Command {
// Create the parent permissions command
cmd := &cobra.Command{
Use: "permissions",
GroupID: "query",
Short: "Manage permissions",
Long: `Manage permissions in the system.
Use 'xce permissions list' to query permissions information.`,
Run: func(cmd *cobra.Command, args []string) {
// Display help when no subcommand is specified
cmd.Help()
},
}
cmd.AddCommand(NewListPermsCmd(configManager))
return cmd
}

View file

@ -0,0 +1,239 @@
package queues
import (
"encoding/json"
"fmt"
"strings"
"xcecli/common/request"
"xcecli/config"
"xcecli/models"
"github.com/spf13/cobra"
)
// QueueController queue controller
type QueueController struct {
configManager *config.ConfigManager
action string
queueNames []string
comment string
allQueues bool
}
// NewQueueController create queue controller
func NewQueueController(configManager *config.ConfigManager, action string) *QueueController {
return &QueueController{
configManager: configManager,
action: action,
queueNames: []string{},
}
}
// Run execute queue control
func (c *QueueController) RunQueuesControl() error {
// get server info and verify authentication
serverInfo, _ := c.configManager.GetServerInfo()
// create API client
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
// handle all queues case
if c.allQueues {
c.queueNames = []string{"all"}
}
// if no queue names are specified, return an error
if len(c.queueNames) == 0 {
return fmt.Errorf("must specify queue names or use --all option")
}
// create request
req := models.QueueControlRequest{
ControlAction: c.action,
QueueName: c.queueNames,
Comment: c.comment,
}
// convert request to JSON bytes
reqData, err := json.Marshal(req)
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to encode request data: [%s]", errMsg)
}
// send request
var resp models.QueueControlResponse
err = apiClient.PostRequestToAPIServer(
serverInfo.Token,
serverInfo.CACert,
"/xce/v1/queues/control",
reqData,
&resp,
)
if err != nil {
return err
}
if len(c.queueNames) == 1 && c.queueNames[0] == "all" {
for _, data := range resp.Data.Accepted {
fmt.Printf("queue <%s> is being <%s>\n", data.QueueName, c.action)
}
for _, queue := range resp.Data.Failed {
fmt.Printf("queue <%s> cannot be <%s> for reason <%s>\n", queue.QueueName, c.action, queue.Msg)
}
} else {
for _, data := range resp.Data.Accepted {
fmt.Printf("queue <%s> is being <%s>\n", data.QueueName, c.action)
}
for _, queue := range resp.Data.Failed {
fmt.Printf("queue <%s> cannot be <%s> for reason <%s>\n", queue.QueueName, c.action, queue.Msg)
}
}
return nil
}
// Parse parse command line arguments
func (c *QueueController) ParseCmdArgs(cmd *cobra.Command, args []string) error {
// handle queue name parameters
if len(args) > 0 {
// Split comma-separated queue names
for _, arg := range args {
queues := strings.Split(arg, ",")
c.queueNames = append(c.queueNames, queues...)
}
}
return nil
}
// NewOpenQueuesCmd create open queues command
func NewOpenQueuesCmd(configManager *config.ConfigManager) *cobra.Command {
controller := NewQueueController(configManager, "open")
cmd := &cobra.Command{
Use: "open",
Short: "Open queues",
Long: `Open queues by queue name.
Opens queues to accept new job submissions.
Examples:
xce queues open normal priority # open specific queues
xce queues open --all # open all queues`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := controller.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := controller.RunQueuesControl(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// add control options
flags := cmd.Flags()
flags.BoolVarP(&controller.allQueues, "all", "a", false, "apply to all queues")
flags.StringVarP(&controller.comment, "comment", "c", "", "add comment for this operation")
return cmd
}
// NewCloseQueuesCmd create close queues command
func NewCloseQueuesCmd(configManager *config.ConfigManager) *cobra.Command {
controller := NewQueueController(configManager, "close")
cmd := &cobra.Command{
Use: "close",
Short: "Close queues",
Long: `Close queues by queue name.
Closes queues to prevent new job submissions.
Examples:
xce queues close normal priority # close specific queues
xce queues close --all # close all queues`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := controller.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := controller.RunQueuesControl(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// add control options
flags := cmd.Flags()
flags.BoolVarP(&controller.allQueues, "all", "a", false, "apply to all queues")
flags.StringVarP(&controller.comment, "comment", "c", "", "add comment for this operation")
return cmd
}
// NewActiveQueuesCmd create active queues command
func NewActiveQueuesCmd(configManager *config.ConfigManager) *cobra.Command {
controller := NewQueueController(configManager, "active")
cmd := &cobra.Command{
Use: "active",
Short: "Active queues",
Long: `Active queues by queue name.
Examples:
xce queues active # queues_name status close/d`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := controller.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := controller.RunQueuesControl(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// add control options
flags := cmd.Flags()
flags.BoolVarP(&controller.allQueues, "all", "a", false, "apply to all queues")
flags.StringVarP(&controller.comment, "comment", "c", "", "add comment for this operation")
return cmd
}
func NewDeactiveQueuesCmd(configManager *config.ConfigManager) *cobra.Command {
controller := NewQueueController(configManager, "deactive")
cmd := &cobra.Command{
Use: "deactive",
Short: "Deactive queues",
Long: `Deactive queues by queue name.
Examples:
xce queues deactive # queues_name status close/d`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := controller.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := controller.RunQueuesControl(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// add control options
flags := cmd.Flags()
flags.BoolVarP(&controller.allQueues, "all", "a", false, "apply to all queues")
flags.StringVarP(&controller.comment, "comment", "c", "", "add comment for this operation")
return cmd
}

View file

@ -0,0 +1,199 @@
package queues
import (
"fmt"
"strings"
"xcecli/common/output"
"xcecli/common/request"
"xcecli/config"
logger "xcecli/config"
queue_filter "xcecli/filters"
"xcecli/models"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
var (
user string
queue string
fields string
)
type QueuesQuerier struct {
configManager *config.ConfigManager
filter *queue_filter.QueuesFilter
fields string
jsonOutput bool
noHeader bool
verbose bool
wideFormat bool // wide format
}
func NewQueuesQuerier(configManager *config.ConfigManager) *QueuesQuerier {
return &QueuesQuerier{
configManager: configManager,
filter: &queue_filter.QueuesFilter{},
}
}
// Parse parses command line arguments, simplified version that doesn't need to process flags already bound to filter
func (qq *QueuesQuerier) ParseCmdArgs(cmd *cobra.Command, args []string) error {
// No additional processing needed, all parameters are bound in flags
if len(args) > 0 {
qq.filter.QueueName = strings.Join(args, ",")
}
// Define mutual exclusions for verbose flag
if qq.verbose {
conflicts := make([]string, 0)
if qq.wideFormat {
conflicts = append(conflicts, "--wided (-w)")
}
if qq.fields != "" {
conflicts = append(conflicts, "--output (-o)")
}
if len(conflicts) > 0 {
return fmt.Errorf("flag --verbose (-l) cannot be used with: %s\n\n%s",
strings.Join(conflicts, ", "), cmd.UsageString())
}
}
return nil
}
func NewListQueuesCmd(configManager *config.ConfigManager) *cobra.Command {
queuesQuerier := NewQueuesQuerier(configManager)
cmd := &cobra.Command{
Use: "list",
Short: "Query queue info",
Long: `Query queue info, support filter by queue.
example:
xce queues list # query all queues
xce queues list -l # query all queues with detail
xce queues list -w # list queues with wide format`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := queuesQuerier.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := queuesQuerier.RunQueuesList(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// Add command line parameters
flags := cmd.Flags()
flags.StringVarP(&user, "user", "u", "", "filter by user")
flags.StringVarP(&queuesQuerier.filter.QueueName, "queue", "q", "", "filter by queue name")
flags.StringVarP(&queuesQuerier.fields, "output", "o", "", "output fields for queues")
flags.BoolVarP(&queuesQuerier.wideFormat, "wided", "w", false, "output in wide format")
flags.BoolVarP(&queuesQuerier.verbose, "verbose", "l", false, "show queue detail")
flags.BoolVarP(&queuesQuerier.jsonOutput, "json", "", false, "output in JSON format")
flags.BoolVarP(&queuesQuerier.noHeader, "noheader", "n", false, "output no header")
return cmd
}
func (qq *QueuesQuerier) RunQueuesList() error {
// Get configuration
// Get server information
serverInfo, _ := qq.configManager.GetServerInfo()
// create API client
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
// Create query parameters
params := make(map[string]string)
// Get filter string
filterStr, err := qq.filter.HandleQueueFilter()
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to get filter: [%s]", errMsg)
} else if filterStr != "" {
params["filter"] = filterStr
}
// Field mapping
fieldsMap := models.QueueFieldsMap
if qq.verbose && qq.fields == "" {
for header := range fieldsMap {
qq.fields += header + ","
}
qq.fields = strings.TrimSuffix(qq.fields, ",")
}
// Process field selection
if qq.fields != "" {
qq.fields = request.CleanFields(qq.fields)
mappedFields, err := request.CheckAndMapFields(qq.fields, fieldsMap)
if err != nil {
return err
}
params["fields"] = strings.Join(mappedFields, ",")
}
// create page printer
pagePrinter := output.NewPagePrinter(output.DataTypeQueues, output.WithFields(qq.fields),
output.WithFormatJson(qq.jsonOutput), output.WithNoHeader(qq.noHeader),
output.WithVerbose(qq.verbose), output.WithWideFormat(qq.wideFormat))
// Record if the first page has been processed
isFirstPage := true
var (
resp models.QueuesResponse
loopCount int
pageToken string
)
// Loop to get all pages of data
for {
if loopCount > 0 && pageToken == "" {
break
}
err = apiClient.GetRequestFromAPIServer(serverInfo.Token, pageToken, serverInfo.CACert, "/xce/v1/queues", &resp, params)
if err != nil {
return err
}
if len(resp.Data.Rows) == 0 {
if isFirstPage {
fmt.Println("no queue found")
return nil
}
//
}
// Process the first page
if isFirstPage {
// Start printing process
pagePrinter.StartPrinting()
// Prepare for printing, calculate column width and table header, etc.
pagePrinter.PrepareForPrinting(&resp)
isFirstPage = false
}
// Print the current page
pagePrinter.PrintPage(&resp)
// Update page token and loop counter
pageToken = resp.Data.Page_token
loopCount++
}
// Finish printing
pagePrinter.Finish()
logger.Debug("queues response completed", zap.Int("pages", loopCount))
return nil
}

View file

@ -0,0 +1,34 @@
package queues
import (
"xcecli/config"
"github.com/spf13/cobra"
)
// NewQueuesCmd creates a command for managing queues
func NewQueuesCmd(configManager *config.ConfigManager) *cobra.Command {
cmd := &cobra.Command{
Use: "queues",
GroupID: "query",
Short: "Manage queues",
Long: `Manage queues in the system.
Use 'xce queues list' to query queues information.
Use 'xce queues <action>' to open/close/active/deactive queues.`,
Run: func(cmd *cobra.Command, args []string) {
// Display help when no subcommand is specified
cmd.Help()
},
}
// Add list subcommand
cmd.AddCommand(NewListQueuesCmd(configManager))
cmd.AddCommand(NewOpenQueuesCmd(configManager))
cmd.AddCommand(NewCloseQueuesCmd(configManager))
cmd.AddCommand(NewActiveQueuesCmd(configManager))
cmd.AddCommand(NewDeactiveQueuesCmd(configManager))
return cmd
}

View file

@ -0,0 +1,359 @@
package resources
import (
"fmt"
"strings"
"xcecli/common/output"
"xcecli/common/request"
"xcecli/config"
"xcecli/models"
logger "xcecli/config"
resource_filter "xcecli/filters"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
type ResourcesDistQuerier struct {
configManager *config.ConfigManager
filter *resource_filter.ResourceDistFilter
fields string
distType string
jsonOutput bool
noHeader bool
verbose bool
wideFormat bool // wide format
}
func NewResourcesDistQuerier(configManager *config.ConfigManager) *ResourcesDistQuerier {
return &ResourcesDistQuerier{
configManager: configManager,
filter: &resource_filter.ResourceDistFilter{},
}
}
func (rq *ResourcesDistQuerier) ParseCmdArgs(cmd *cobra.Command, args []string) error {
// No additional processing needed, all parameters are bound in flags
if len(args) > 0 {
rq.filter.ResName = strings.Join(args, ",")
}
// Define mutual exclusions for verbose flag
if rq.verbose {
conflicts := make([]string, 0)
if rq.wideFormat {
conflicts = append(conflicts, "--wided (-w)")
}
if len(conflicts) > 0 {
return fmt.Errorf("flag --verbose (-l) cannot be used with: %s\n\n%s",
strings.Join(conflicts, ", "), cmd.UsageString())
}
}
return nil
}
// ResourcesDefQuerier handles querying resource definition information
type ResourcesDefQuerier struct {
configManager *config.ConfigManager
filter *resource_filter.ResourceDefFilter
fields string
jsonOutput bool
noHeader bool
verbose bool
wideFormat bool // wide format
}
func NewResourcesDefQuerier(configManager *config.ConfigManager) *ResourcesDefQuerier {
return &ResourcesDefQuerier{
configManager: configManager,
filter: &resource_filter.ResourceDefFilter{},
}
}
func (rq *ResourcesDefQuerier) ParseCmdArgs(cmd *cobra.Command, args []string) error {
// No additional processing needed, all parameters are bound in flags
if len(args) > 0 {
rq.filter.ResName = strings.Join(args, ",")
}
// Define mutual exclusions for verbose flag
if rq.verbose {
conflicts := make([]string, 0)
if rq.wideFormat {
conflicts = append(conflicts, "--wided (-w)")
}
if len(conflicts) > 0 {
return fmt.Errorf("flag --verbose (-l) cannot be used with: %s\n\n%s",
strings.Join(conflicts, ", "), cmd.UsageString())
}
}
return nil
}
// NewListResourcesDefCmd creates a command for listing resource definition information
func NewListResourcesDefCmd(configManager *config.ConfigManager) *cobra.Command {
resourcesDefQuerier := NewResourcesDefQuerier(configManager)
cmd := &cobra.Command{
Use: "list",
Short: "Query the resources definition info",
Long: `Query the resources definition info.
Example:
xce resources definition list # query the definition info`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := resourcesDefQuerier.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := resourcesDefQuerier.RunResourcesDefList(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// Add command line parameters
flags := cmd.Flags()
flags.BoolVarP(&resourcesDefQuerier.verbose, "verbose", "l", false, "show resource detail")
flags.BoolVarP(&resourcesDefQuerier.wideFormat, "wided", "w", false, "output in wide format")
flags.BoolVarP(&resourcesDefQuerier.jsonOutput, "json", "", false, "output in JSON format")
flags.BoolVarP(&resourcesDefQuerier.noHeader, "noheader", "n", false, "output no header")
return cmd
}
func (rq *ResourcesDefQuerier) RunResourcesDefList() error {
// Get configuration
// Get server information
serverInfo, _ := rq.configManager.GetServerInfo()
// create API client
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
// Create query parameters
params := make(map[string]string)
// Get filter string
filterStr, err := rq.filter.HandleResourceDefFilter()
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to get filter: [%s]", errMsg)
} else if filterStr != "" {
params["filter"] = filterStr
}
// Field mapping
fieldsMap := models.ResourceDefFieldsMap
if rq.verbose && rq.fields == "" {
for header := range fieldsMap {
rq.fields += header + ","
}
rq.fields = strings.TrimSuffix(rq.fields, ",")
}
// Process field selection
if rq.fields != "" {
rq.fields = request.CleanFields(rq.fields)
mappedFields, err := request.CheckAndMapFields(rq.fields, fieldsMap)
if err != nil {
return err
}
params["fields"] = strings.Join(mappedFields, ",")
}
// create page printer
pagePrinter := output.NewPagePrinter(output.DataTypeResourcesDef, output.WithFields(rq.fields),
output.WithFormatJson(rq.jsonOutput), output.WithNoHeader(rq.noHeader),
output.WithVerbose(rq.verbose), output.WithWideFormat(rq.wideFormat))
// Record if the first page has been processed
isFirstPage := true
var (
resp models.ResourceDefResponse
loopCount int
pageToken string
)
// Loop to get all pages of data
for {
if loopCount > 0 && pageToken == "" {
break
}
err = apiClient.GetRequestFromAPIServer(serverInfo.Token, pageToken, serverInfo.CACert, "/xce/v1/resources/definition", &resp, params)
if err != nil {
return err
}
if len(resp.Data.Rows) == 0 {
if isFirstPage {
fmt.Println("no resource definition found")
return nil
}
}
// Process the first page
if isFirstPage {
// Start printing process
pagePrinter.StartPrinting()
// Prepare for printing, calculate column width and table header, etc.
pagePrinter.PrepareForPrinting(&resp)
isFirstPage = false
}
// Print the current page
pagePrinter.PrintPage(&resp)
// Update page token and loop counter
pageToken = resp.Data.Page_token
loopCount++
}
// Finish printing
pagePrinter.Finish()
logger.Debug("resource definition response completed", zap.Int("pages", loopCount))
return nil
}
// NewListResourcesDistCmd creates a command for listing resource distribution information
func NewListResourcesDistCmd(configManager *config.ConfigManager) *cobra.Command {
resourcesDistQuerier := NewResourcesDistQuerier(configManager)
cmd := &cobra.Command{
Use: "list",
Short: "Query the resources distribution info",
Long: `Query the resources info, support static and dynamic type.
Example:
xce resources distribution list # query the distribution info`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := resourcesDistQuerier.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := resourcesDistQuerier.RunResourcesDistList(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// Add command line parameters
flags := cmd.Flags()
flags.StringVarP(&resourcesDistQuerier.distType, "type", "t", "", "static or dynamic")
flags.BoolVarP(&resourcesDistQuerier.wideFormat, "wided", "w", false, "output in wide format")
flags.BoolVarP(&resourcesDistQuerier.jsonOutput, "json", "", false, "output in JSON format")
flags.BoolVarP(&resourcesDistQuerier.noHeader, "noheader", "n", false, "output no header")
return cmd
}
func (rq *ResourcesDistQuerier) RunResourcesDistList() error {
// Get configuration
// Get server information
serverInfo, _ := rq.configManager.GetServerInfo()
// create API client
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
// Create query parameters
params := make(map[string]string)
// Get filter string
filterStr, err := rq.filter.HandleResourceDistFilter()
if rq.distType != "" {
if rq.distType != "static" && rq.distType != "dynamic" {
return fmt.Errorf("invalid type, must be 'static' or 'dynamic'")
} else {
params["type"] = rq.distType
}
}
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to get filter: [%s]", errMsg)
} else if filterStr != "" {
params["filter"] = filterStr
}
// Field mapping
fieldsMap := models.ResourceDistFieldsMap
if rq.verbose && rq.fields == "" {
for header := range fieldsMap {
rq.fields += header + ","
}
rq.fields = strings.TrimSuffix(rq.fields, ",")
}
// Process field selection
if rq.fields != "" {
rq.fields = request.CleanFields(rq.fields)
mappedFields, err := request.CheckAndMapFields(rq.fields, fieldsMap)
if err != nil {
return err
}
params["fields"] = strings.Join(mappedFields, ",")
}
// create page printer
pagePrinter := output.NewPagePrinter(output.DataTypeResourcesDist, output.WithFields(rq.fields),
output.WithFormatJson(rq.jsonOutput), output.WithNoHeader(rq.noHeader),
output.WithVerbose(rq.verbose), output.WithWideFormat(rq.wideFormat))
// Record if the first page has been processed
isFirstPage := true
var (
resp models.ResourceDistResponse
loopCount int
pageToken string
)
// Loop to get all pages of data
for {
if loopCount > 0 && pageToken == "" {
break
}
err = apiClient.GetRequestFromAPIServer(serverInfo.Token, pageToken, serverInfo.CACert, "/xce/v1/resources/distribution", &resp, params)
if err != nil {
return err
}
if len(resp.Data.Rows) == 0 {
if isFirstPage {
fmt.Println("no resource found")
return nil
}
}
// Process the first page
if isFirstPage {
// Start printing process
pagePrinter.StartPrinting()
// Prepare for printing, calculate column width and table header, etc.
pagePrinter.PrepareForPrinting(&resp)
isFirstPage = false
}
// Print the current page
pagePrinter.PrintPage(&resp)
// Update page token and loop counter
pageToken = resp.Data.Page_token
loopCount++
}
// Finish printing
pagePrinter.Finish()
logger.Debug("resource dirstribution response completed", zap.Int("pages", loopCount))
return nil
}

View file

@ -0,0 +1,56 @@
package resources
import (
"xcecli/config"
"github.com/spf13/cobra"
)
// NewResourcesCmd creates a command for querying resource information
func NewResourcesCmd(configManager *config.ConfigManager) *cobra.Command {
cmd := &cobra.Command{
Use: "resources",
GroupID: "query",
Short: "Manage resources",
Long: `Manage resources in the system.
Use 'xce resources distribution list' to query resources distribution information.
Use 'xce resources definition list' to query resources definition information.`,
Run: func(cmd *cobra.Command, args []string) {
// Display help when no subcommand is specified
cmd.Help()
},
}
// Add distribution subcommand
distributionCmd := &cobra.Command{
Use: "distribution",
Short: "Manage resource distribution",
Long: `Manage resource distribution information.
Use 'xce resources distribution list' to query resources distribution information.`,
Run: func(cmd *cobra.Command, args []string) {
// Display help when no subcommand is specified
cmd.Help()
},
}
distributionCmd.AddCommand(NewListResourcesDistCmd(configManager))
// Add definition subcommand
definitionCmd := &cobra.Command{
Use: "definition",
Short: "Manage resource definition",
Long: `Manage resource definition information.
Use 'xce resources definition list' to query resources definition information.`,
Run: func(cmd *cobra.Command, args []string) {
// Display help when no subcommand is specified
cmd.Help()
},
}
definitionCmd.AddCommand(NewListResourcesDefCmd(configManager))
// Add subcommands to main command
cmd.AddCommand(distributionCmd)
cmd.AddCommand(definitionCmd)
return cmd
}

View file

@ -0,0 +1,308 @@
package roles
import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"
"xcecli/common/request"
"xcecli/config"
logger "xcecli/config"
"xcecli/models"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
func NewRoleSubmitter(configManager *config.ConfigManager) *RoleSubmitter {
return &RoleSubmitter{
configManager: configManager,
}
}
type RoleSubmitter struct {
configManager *config.ConfigManager
roleName string
rolePerms string
permissions []models.Permission
}
func NewAddRolesCmd(configManager *config.ConfigManager) *cobra.Command {
roleSubmitter := NewRoleSubmitter(configManager)
cmd := &cobra.Command{
Use: "add",
Short: "Add role to API Server",
Long: `Add role to API Server.
Example:
xce roles add -r role_name -p perm_name=type[values,...] # add role with permission"`,
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {
if err := roleSubmitter.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return
}
if err := roleSubmitter.RunRolesAdd(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return
}
},
}
// add the flags
flags := cmd.Flags()
//modify the flags
flags.StringVarP(&roleSubmitter.roleName, "role_name", "r", "", "specify the role name")
flags.StringVarP(&roleSubmitter.rolePerms, "permissions", "p", "", "specify the permissions")
return cmd
//
}
func NewAssignPermsCmd(configManager *config.ConfigManager) *cobra.Command {
roleSubmitter := NewRoleSubmitter(configManager)
cmd := &cobra.Command{
Use: "assignpermission",
Short: "Assign permissions to the existing role to API Server",
Long: `Assign permissions to the existing role to API Server.
Example:
xce roles assignpermission -r role_name -p perm_name=type[values,...] # assign permission to the role"`,
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {
if err := roleSubmitter.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return
}
if err := roleSubmitter.RunPermsAdd(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return
}
},
}
// add the flags
flags := cmd.Flags()
//modify the flags
flags.StringVarP(&roleSubmitter.roleName, "role_name", "r", "", "specify the role name")
flags.StringVarP(&roleSubmitter.rolePerms, "permissions", "p", "", "specify the permissions")
return cmd
//
}
func (rs *RoleSubmitter) ParseCmdArgs(cmd *cobra.Command, args []string) error {
if rs.roleName == "" {
return fmt.Errorf("please provide the role name")
}
if len(rs.roleName) > 40 {
return fmt.Errorf("error: role name cannot be longer than 40 characters")
}
if rs.rolePerms == "" {
return fmt.Errorf("please provide the role permissions")
} else {
permissions, err := parsePermissions(rs.rolePerms)
if err != nil {
return err
}
rs.permissions = permissions
}
return nil
}
func parsePermissions(pStr string) ([]models.Permission, error) {
var permissions []models.Permission
permItems := splitPermissions(pStr)
for _, item := range permItems {
item = strings.TrimSpace(item)
if item == "" {
continue
}
perm, err := parseSinglePermission(item)
if err != nil {
return nil, fmt.Errorf("failed to parse the item <%s> for reason <%v>", item, err)
}
permissions = append(permissions, perm)
}
return permissions, nil
}
func splitPermissions(pStr string) []string {
var result []string
var current strings.Builder
bracketDepth := 0
for _, char := range pStr {
switch char {
case ',':
if bracketDepth == 0 {
// Only split when not inside brackets
result = append(result, strings.TrimSpace(current.String()))
current.Reset()
} else {
current.WriteRune(char)
}
case '[':
bracketDepth++
current.WriteRune(char)
case ']':
bracketDepth--
current.WriteRune(char)
default:
current.WriteRune(char)
}
}
// Add the last item
if current.Len() > 0 {
result = append(result, strings.TrimSpace(current.String()))
}
return result
}
func parseSinglePermission(item string) (models.Permission, error) {
re := regexp.MustCompile(`^(.+?)=(.+?)(?:\[(.+?)\])?$`)
matches := re.FindStringSubmatch(item)
if len(matches) != 4 {
return models.Permission{}, fmt.Errorf("incorrect format for permission, 'permission_name=domain_type' or 'permission_name=domain_type[value1,value2]' required")
}
permName := strings.TrimSpace(matches[1])
domainType := strings.TrimSpace(matches[2])
valuesStr := strings.TrimSpace(matches[3])
// parse domain values
var domainValues []string
if valuesStr != "" {
values := strings.Split(valuesStr, ",")
for _, v := range values {
domainValues = append(domainValues, strings.TrimSpace(v))
}
}
if permName == "" {
return models.Permission{}, fmt.Errorf("permission_name cannot be empty")
}
if domainType == "" {
return models.Permission{}, fmt.Errorf("domain_type cannot be empty")
}
return models.Permission{
PermissionName: permName,
DomainType: domainType,
DomainValues: domainValues,
}, nil
}
func (rs *RoleSubmitter) RunRolesAdd() error {
// Get server info and validate authentication
serverInfo, _ := rs.configManager.GetServerInfo()
// create the API client and submit the role
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
// build the role request
roleReq, err := rs.BuildRoleRequest()
if err != nil {
return err
}
// encode request to JSON bytes
reqData, err := json.Marshal(roleReq)
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to encode request data: [%s]", errMsg)
}
var resp models.CreateRoleResponse
err = apiClient.PostRequestToAPIServer(
serverInfo.Token,
serverInfo.CACert,
"/xce/v1/roles/create",
reqData,
&resp,
)
if err != nil {
return err
}
if resp.Code != http.StatusCreated {
logger.Info("create role failed", zap.Any("resp", resp), zap.Error(err))
return fmt.Errorf("%s", resp.Data.Message)
}
logger.Debug("role created successfully", zap.String("message", resp.Data.Message))
logger.Info("role created successfully", zap.String("message", resp.Data.Message))
fmt.Printf("%s\n", resp.Data.Message)
return nil
}
func (rs *RoleSubmitter) RunPermsAdd() error {
// Get server info and validate authentication
serverInfo, _ := rs.configManager.GetServerInfo()
// create the API client and submit the role
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
// build the role request
roleReq, err := rs.BuildRoleRequest()
if err != nil {
return err
}
// encode request to JSON bytes
reqData, err := json.Marshal(roleReq)
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to encode request data: [%s]", errMsg)
}
url := fmt.Sprintf("/xce/v1/roles/%s/permissions", rs.roleName)
var resp models.CreateRoleResponse
err = apiClient.PostRequestToAPIServer(
serverInfo.Token,
serverInfo.CACert,
url,
reqData,
&resp,
)
if err != nil {
return err
}
if resp.Code != http.StatusCreated {
logger.Info("assign permission failed", zap.Any("resp", resp), zap.Error(err))
return fmt.Errorf("%s", resp.Data.Message)
}
logger.Debug("assign permission successfully", zap.String("message", resp.Data.Message))
logger.Info("assign permission successfully", zap.String("message", resp.Data.Message))
fmt.Printf("%s\n", resp.Data.Message)
return nil
}
func (rs *RoleSubmitter) BuildRoleRequest() (*models.CreateRoleRequest, error) {
// create the role request with only required fields
var perms []models.Permission
for _, perm := range rs.permissions {
rolePerm := models.Permission{
PermissionName: perm.PermissionName,
DomainType: perm.DomainType,
}
if len(perm.DomainValues) > 0 {
rolePerm.DomainValues = perm.DomainValues
}
perms = append(perms, rolePerm)
}
roleReq := &models.CreateRoleRequest{
RoleName: rs.roleName,
Permission: perms,
}
return roleReq, nil
}

View file

@ -0,0 +1,92 @@
package roles
import (
"fmt"
"net/http"
"strings"
"xcecli/common/request"
"xcecli/config"
logger "xcecli/config"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
func NewDeleteRoleQuerier(configManager *config.ConfigManager) *DeleteRole {
return &DeleteRole{
configManager: configManager,
}
}
type DeleteRole struct {
configManager *config.ConfigManager
roleName string
}
func NewDeleteRolesCmd(configManager *config.ConfigManager) *cobra.Command {
deleteRoleQuerier := NewDeleteRoleQuerier(configManager)
cmd := &cobra.Command{
Use: "delete",
Short: "Delete role",
Long: `Delete role, by specifying the role name.
example:
xce roles delete -r role_name # delete role`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := deleteRoleQuerier.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := deleteRoleQuerier.RunDeleteRoleInfo(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// Add command line parameters
flags := cmd.Flags()
flags.StringVarP(&deleteRoleQuerier.roleName, "role_name", "r", "", "filter by role name")
return cmd
}
func (dr *DeleteRole) ParseCmdArgs(cmd *cobra.Command, args []string) error {
if dr.roleName == "" {
return fmt.Errorf("please provide the role name")
}
return nil
}
func (dr *DeleteRole) RunDeleteRoleInfo() error {
// Get server info and validate authentication
serverInfo, _ := dr.configManager.GetServerInfo()
// create the API client and submit the user
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
url := fmt.Sprintf("/xce/v1/roles/%s", dr.roleName)
var resp request.APIResponse
err := apiClient.DeleteRequestFromAPIServer(
serverInfo.Token,
serverInfo.CACert,
url,
[]byte(nil),
&resp,
)
if err != nil {
return err
}
if resp.Code != http.StatusOK {
logger.Info("delete role failed", zap.Any("resp", resp), zap.Error(err))
return fmt.Errorf("%v", resp.Data)
}
logger.Debug("delete role successfully", zap.Any("data", resp.Data))
logger.Info("delete role successfully", zap.Any("data", resp.Data))
fmt.Printf("%v\n", resp.Data)
return nil
}

View file

@ -0,0 +1,216 @@
package roles
import (
"fmt"
"strings"
"xcecli/common/output"
"xcecli/common/request"
"xcecli/config"
logger "xcecli/config"
role_filter "xcecli/filters"
"xcecli/models"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
type RolesQuerier struct {
configManager *config.ConfigManager
filter *role_filter.RolesFilter
fields string
jsonOutput bool
noHeader bool
verbose bool
wideFormat bool // wide format
}
func NewRolesQuerier(configManager *config.ConfigManager) *RolesQuerier {
return &RolesQuerier{
configManager: configManager,
filter: &role_filter.RolesFilter{},
}
}
func NewListRolesCmd(configManager *config.ConfigManager) *cobra.Command {
rolesQuerier := NewRolesQuerier(configManager)
cmd := &cobra.Command{
Use: "list",
Short: "Query role info",
Long: `Query role info, support filter by role.
example:
xce roles list # query all roles
xce roles list -l # query all roles with detail
xce roles list -w # list roles with wide format`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := rolesQuerier.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := rolesQuerier.RunRolesInfo("list"); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// Add command line parameters
flags := cmd.Flags()
flags.StringVarP(&rolesQuerier.filter.RoleName, "role", "r", "", "filter by role name")
flags.StringVarP(&rolesQuerier.filter.Type, "type", "t", "", "output for all or builtin roles")
//flags.StringVarP(&rolesQuerier.fields, "output", "o", "", "output fields for roles")
flags.BoolVarP(&rolesQuerier.wideFormat, "wided", "w", false, "output in wide format")
flags.BoolVarP(&rolesQuerier.jsonOutput, "json", "", false, "output in JSON format")
flags.BoolVarP(&rolesQuerier.noHeader, "noheader", "n", false, "output no header")
return cmd
}
func (rq *RolesQuerier) ParseCmdArgs(cmd *cobra.Command, args []string) error {
// No additional processing needed, all parameters are bound in flags
if rq.filter.Type != "" && rq.filter.Type != "all" && rq.filter.Type != "builtin" {
return fmt.Errorf("type must be either 'all' or 'builtin'")
}
if len(args) > 0 {
rq.filter.RoleName = strings.Join(args, ",")
}
return nil
}
func (rq *RolesQuerier) RunRolesInfo(commandType string) error {
// Get server information
serverInfo, err := rq.configManager.GetServerInfo()
if err != nil {
return fmt.Errorf("failed to get server info: %w", err)
}
// create API client
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
// Create query parameters
params := make(map[string]string)
// Get filter string
filterStr, err := rq.filter.HandleRoleFilter()
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to get filter: [%s]", errMsg)
} else if filterStr != "" {
params["filter"] = filterStr
}
// Field mapping
// Process field selection
// for list, only display role_name column; deduplication is done client-side
if commandType == "list" {
rq.fields = "ROLE_NAME"
params["fields"] = "role_name"
} else {
fieldsMap := models.RoleFieldsMap
if rq.verbose && rq.fields == "" {
for header := range fieldsMap {
rq.fields += header + ","
}
rq.fields = strings.TrimSuffix(rq.fields, ",")
}
if rq.fields != "" {
rq.fields = request.CleanFields(rq.fields)
mappedFields, err := request.CheckAndMapFields(rq.fields, fieldsMap)
if err != nil {
return err
}
params["fields"] = strings.Join(mappedFields, ",")
}
}
if rq.filter.Type != "" {
params["type"] = rq.filter.Type
}
// create page printer
pagePrinter := output.NewPagePrinter(output.DataTypeRoles, output.WithFields(rq.fields),
output.WithFormatJson(rq.jsonOutput), output.WithNoHeader(rq.noHeader),
output.WithVerbose(rq.verbose), output.WithWideFormat(rq.wideFormat))
// Record if the first page has been processed
isFirstPage := true
var (
resp models.RolesResponse
loopCount int
pageToken string
seen map[string]struct{} // cross-page dedup state for list mode
)
if commandType == "list" {
seen = make(map[string]struct{})
}
// Loop to get all pages of data
for {
if loopCount > 0 && pageToken == "" {
break
}
resp = models.RolesResponse{} // reset to avoid stale rows from previous page
err = apiClient.GetRequestFromAPIServer(serverInfo.Token, pageToken, serverInfo.CACert, "/xce/v1/roles", &resp, params)
if err != nil {
return err
}
if resp.Data.Count == 0 {
if isFirstPage {
fmt.Println("no role found")
return nil
}
}
// For list mode, deduplicate role names client-side before printing
if commandType == "list" {
var unique []models.RoleInfo
for _, row := range resp.Data.Rows {
if _, ok := seen[row.RoleName]; !ok {
seen[row.RoleName] = struct{}{}
unique = append(unique, models.RoleInfo{RoleName: row.RoleName})
}
}
resp.Data.Rows = unique
resp.Data.Count = len(unique)
}
// Process the first page
if isFirstPage {
// Start printing process
pagePrinter.StartPrinting()
// Prepare for printing, calculate column width and table header, etc.
pagePrinter.PrepareForPrinting(&resp)
isFirstPage = false
}
// Print the current page
pagePrinter.PrintPage(&resp)
// Update page token and loop counter
pageToken = resp.Data.PageToken
loopCount++
}
// Finish printing
pagePrinter.Finish()
logger.Debug("roles response completed", zap.Int("pages", loopCount))
return nil
}
// uniqueRoleNames deduplicates role names from a RolesResponse, preserving order.
// The server returns one row per permission, so the same role_name appears multiple times.
func uniqueRoleNames(resp *models.RolesResponse) []string {
seen := make(map[string]struct{})
var result []string
for _, row := range resp.Data.Rows {
if _, ok := seen[row.RoleName]; !ok {
seen[row.RoleName] = struct{}{}
result = append(result, row.RoleName)
}
}
return result
}

View file

@ -0,0 +1,82 @@
package roles
import (
"testing"
"xcecli/models"
)
// --- dedup (already tested, regression guard) ---
func TestUniqueRoleNames_NoDuplicates(t *testing.T) {
resp := &models.RolesResponse{}
resp.Data.Rows = []models.RoleInfo{{RoleName: "admin"}, {RoleName: "basic_user"}}
if got := uniqueRoleNames(resp); len(got) != 2 {
t.Fatalf("expected 2, got %d", len(got))
}
}
func TestUniqueRoleNames_WithDuplicates(t *testing.T) {
resp := &models.RolesResponse{}
resp.Data.Rows = []models.RoleInfo{
{RoleName: "admin", PermName: "job:view"},
{RoleName: "admin", PermName: "host:view"},
{RoleName: "basic_user", PermName: "job:view"},
}
got := uniqueRoleNames(resp)
if len(got) != 2 || got[0] != "admin" || got[1] != "basic_user" {
t.Fatalf("unexpected: %v", got)
}
}
func TestUniqueRoleNames_Empty(t *testing.T) {
resp := &models.RolesResponse{}
if got := uniqueRoleNames(resp); len(got) != 0 {
t.Fatalf("expected 0, got %d", len(got))
}
}
func TestUniqueRoleNames_CrossPage(t *testing.T) {
seen := make(map[string]struct{})
dedup := func(rows []models.RoleInfo) []models.RoleInfo {
var unique []models.RoleInfo
for _, row := range rows {
if _, ok := seen[row.RoleName]; !ok {
seen[row.RoleName] = struct{}{}
unique = append(unique, models.RoleInfo{RoleName: row.RoleName})
}
}
return unique
}
p1 := dedup([]models.RoleInfo{{RoleName: "admin"}, {RoleName: "admin"}, {RoleName: "basic_user"}})
p2 := dedup([]models.RoleInfo{{RoleName: "basic_user"}, {RoleName: "operator"}})
if len(p1) != 2 {
t.Fatalf("page1: expected 2, got %d", len(p1))
}
if len(p2) != 1 || p2[0].RoleName != "operator" {
t.Fatalf("page2: expected [operator], got %v", p2)
}
}
// --- column width calculation ---
func TestUserRoleNameColumnWidth(t *testing.T) {
resp := &models.UserRolesResponse{}
resp.Data.Rows = []models.UserRoleInfo{
{UserName: "alice", UserRoleName: "cluster_admin"},
{UserName: "bob", UserRoleName: "basic_user"},
}
width := calcUserRoleNameWidth(resp)
if width < len("cluster_admin") {
t.Errorf("ROLE_NAME width %d < len(%q)=%d", width, "cluster_admin", len("cluster_admin"))
}
}
func calcUserRoleNameWidth(resp *models.UserRolesResponse) int {
w := len(models.HEADER_USERROLE_NAME)
for _, u := range resp.Data.Rows {
if n := len(u.UserRoleName); n > w {
w = n
}
}
return w
}

View file

@ -0,0 +1,31 @@
package roles
import (
"xcecli/config"
"github.com/spf13/cobra"
)
func NewRolesCmd(configManager *config.ConfigManager) *cobra.Command {
// Create the parent roles command
cmd := &cobra.Command{
Use: "roles",
GroupID: "query",
Short: "Manage roles",
Long: `Manage roles in the system.
Use 'xce roles list' to query roles list only.
Use 'xce roles view' to query roles details.
Use 'xce roles <action>' to add roles.`,
Run: func(cmd *cobra.Command, args []string) {
// Display help when no subcommand is specified
cmd.Help()
},
}
cmd.AddCommand(NewListRolesCmd(configManager))
cmd.AddCommand(NewViewRolesCmd(configManager))
cmd.AddCommand(NewAddRolesCmd(configManager))
cmd.AddCommand(NewAssignPermsCmd(configManager))
cmd.AddCommand(NewDeleteRolesCmd(configManager))
return cmd
}

View file

@ -0,0 +1,46 @@
package roles
import (
"fmt"
"strings"
"xcecli/config"
"github.com/spf13/cobra"
)
func NewViewRolesCmd(configManager *config.ConfigManager) *cobra.Command {
rolesQuerier := NewRolesQuerier(configManager)
cmd := &cobra.Command{
Use: "view",
Short: "Query role info",
Long: `Query role info, support filter by role.
example:
xce roles view # query all roles
xce roles view -w # view roles with wide format`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := rolesQuerier.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := rolesQuerier.RunRolesInfo("view"); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// Add command line parameters
flags := cmd.Flags()
flags.StringVarP(&rolesQuerier.filter.RoleName, "role", "r", "", "filter by role name")
flags.StringVarP(&rolesQuerier.filter.Type, "type", "t", "", "output for all or builtin roles")
flags.StringVarP(&rolesQuerier.filter.PermName, "permission_name", "p", "", "filter by permission name")
flags.StringVarP(&rolesQuerier.fields, "output", "o", "", "output fields for roles")
flags.BoolVarP(&rolesQuerier.wideFormat, "wided", "w", false, "output in wide format")
flags.BoolVarP(&rolesQuerier.jsonOutput, "json", "", false, "output in JSON format")
flags.BoolVarP(&rolesQuerier.noHeader, "noheader", "n", false, "output no header")
return cmd
}

View file

@ -0,0 +1,127 @@
package users
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"xcecli/common/request"
"xcecli/config"
logger "xcecli/config"
"xcecli/models"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
func NewUserSubmitter(configManager *config.ConfigManager) *UserSubmitter {
return &UserSubmitter{
configManager: configManager,
}
}
type UserSubmitter struct {
configManager *config.ConfigManager
userName string
roleName []string
}
func NewAssignUserRolesCmd(configManager *config.ConfigManager) *cobra.Command {
userSubmitter := NewUserSubmitter(configManager)
cmd := &cobra.Command{
Use: "assignrole",
Short: "Assign role to API Server",
Long: `Assign role to API Server.
Example:
xce user assignrole -u user_name -r role_name1,role_name2 # assign role to a user"`,
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {
if err := userSubmitter.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return
}
if err := userSubmitter.RunUserAssignRole(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return
}
},
}
// add the flags
flags := cmd.Flags()
//modify the flags
flags.StringVarP(&userSubmitter.userName, "user_name", "u", "", "specify the user name")
flags.StringSliceVarP(&userSubmitter.roleName, "role_name", "r", []string{}, "specify the role name")
return cmd
//
}
func (rs *UserSubmitter) ParseCmdArgs(cmd *cobra.Command, args []string) error {
if rs.userName == "" {
return fmt.Errorf("please provide the user name")
}
if len(rs.userName) > 40 {
return fmt.Errorf("error: user name cannot be longer than 40 characters")
}
if len(rs.roleName) == 0 {
return fmt.Errorf("please provide the role name")
}
if len(args) > 0 {
return fmt.Errorf("too many arguments")
}
return nil
}
func (us *UserSubmitter) RunUserAssignRole() error {
// Get server info and validate authentication
serverInfo, _ := us.configManager.GetServerInfo()
// create the API client and submit the user
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
// build the user request
userReq, err := us.BuildUserRequest()
if err != nil {
return err
}
// encode request to JSON bytes
reqData, err := json.Marshal(userReq)
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to encode request data: [%s]", errMsg)
}
url := fmt.Sprintf("/xce/v1/users/%s/roles", us.userName)
var resp models.UserAssignRoleResponse
err = apiClient.PostRequestToAPIServer(
serverInfo.Token,
serverInfo.CACert,
url,
reqData,
&resp,
)
if err != nil {
return err
}
if resp.Code != http.StatusCreated {
logger.Info("user assign role failed", zap.Any("resp", resp), zap.Error(err))
return fmt.Errorf("%s", resp.Data.Message)
}
logger.Debug("user assign role successfully", zap.String("message", resp.Data.Message))
logger.Info("user assign role successfully", zap.String("message", resp.Data.Message))
fmt.Printf("%s\n", resp.Data.Message)
return nil
}
func (us *UserSubmitter) BuildUserRequest() (*models.AddRoleForUserRequest, error) {
roleReq := &models.AddRoleForUserRequest{
UserName: us.userName,
RoleName: us.roleName,
}
return roleReq, nil
}

View file

@ -0,0 +1,252 @@
package users
import (
"fmt"
"strings"
"xcecli/common/output"
"xcecli/common/request"
"xcecli/config"
logger "xcecli/config"
userrole_filter "xcecli/filters"
"xcecli/models"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
type UserRolesQuerier struct {
configManager *config.ConfigManager
filter *userrole_filter.UserRolesFilter
fields string
jsonOutput bool
noHeader bool
verbose bool
wideFormat bool
}
func NewUserRolesQuerier(configManager *config.ConfigManager) *UserRolesQuerier {
return &UserRolesQuerier{
configManager: configManager,
filter: &userrole_filter.UserRolesFilter{},
}
}
func NewCurrentUserCmd(configManager *config.ConfigManager) *cobra.Command {
userrolesQuerier := NewUserRolesQuerier(configManager)
cmd := &cobra.Command{
Use: "whoami",
Short: "Query the current loggin user",
Long: `Query the current loggin user.
example:
xce users whoami # query the current loggin user`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := userrolesQuerier.RunCurrentUserInfo(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
return cmd
}
func NewListUserRolesCmd(configManager *config.ConfigManager) *cobra.Command {
userrolesQuerier := NewUserRolesQuerier(configManager)
cmd := &cobra.Command{
Use: "list",
Short: "Query user role info",
Long: `Query user role info, support filter by user.
example:
xce users list # query all users
xce users list -l # query all users with detail
xce users list -w # list users with wide format`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := userrolesQuerier.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := userrolesQuerier.RunUserRolesInfo("list"); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// Add command line parameters
flags := cmd.Flags()
flags.StringVarP(&userrolesQuerier.filter.UserName, "user_name", "u", "", "filter by user name, e.g. -u user1,user2")
flags.BoolVarP(&userrolesQuerier.wideFormat, "wided", "w", false, "output in wide format")
flags.BoolVarP(&userrolesQuerier.jsonOutput, "json", "", false, "output in JSON format")
flags.BoolVarP(&userrolesQuerier.noHeader, "noheader", "n", false, "output no header")
return cmd
}
func (urq *UserRolesQuerier) ParseCmdArgs(cmd *cobra.Command, args []string) error {
// No additional processing needed, all parameters are bound in flags
if len(args) > 0 {
urq.filter.UserName = strings.Join(args, ",")
}
return nil
}
func (urq *UserRolesQuerier) RunCurrentUserInfo() error {
// Get server information
serverInfo, err := urq.configManager.GetServerInfo()
if err != nil {
return fmt.Errorf("failed to get server info: %w", err)
}
// create API client
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
// Create a response structure for current user info
var currentUserResp models.UserResponse // You'll need to define this model
// Make API request to get current user info using the token
err = apiClient.GetRequestFromAPIServer(serverInfo.Token, "", serverInfo.CACert, "/xce/v1/users/current", &currentUserResp, nil)
if err != nil {
return fmt.Errorf("failed to get current user info: %w", err)
}
// Print the username
fmt.Println(currentUserResp.Data) // Adjust field name based on your model structure
return nil
}
func (urq *UserRolesQuerier) RunUserRolesInfo(commandType string) error {
// Get server information
serverInfo, err := urq.configManager.GetServerInfo()
if err != nil {
return fmt.Errorf("failed to get server info: %w", err)
}
// create API client
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
// Create query parameters
params := make(map[string]string)
// Get filter string
filterStr, err := urq.filter.HandleUserRoleFilter()
if err != nil {
errMsg := request.ExtractErrorMessage(err)
return fmt.Errorf("failed to get filter: [%s]", errMsg)
} else if filterStr != "" {
params["filter"] = filterStr
}
if commandType == "list" {
urq.fields = "USER_NAME"
params["fields"] = "user_name"
} else {
// Field mapping
fieldsMap := models.UserRoleFieldsMap
if urq.verbose && urq.fields == "" {
for header := range fieldsMap {
urq.fields += header + ","
}
urq.fields = strings.TrimSuffix(urq.fields, ",")
}
// Process field selection
if urq.fields != "" {
urq.fields = request.CleanFields(urq.fields)
mappedFields, err := request.CheckAndMapFields(urq.fields, fieldsMap)
if err != nil {
return err
}
params["fields"] = strings.Join(mappedFields, ",")
}
}
// create page printer
pagePrinter := output.NewPagePrinter(output.DataTypeUsers, output.WithFields(urq.fields),
output.WithFormatJson(urq.jsonOutput), output.WithNoHeader(urq.noHeader),
output.WithVerbose(urq.verbose), output.WithWideFormat(urq.wideFormat))
// Record if the first page has been processed
isFirstPage := true
var (
resp models.UserRolesResponse
loopCount int
pageToken string
seen map[string]struct{} // cross-page dedup state for list mode
)
if commandType == "list" {
seen = make(map[string]struct{})
}
// Loop to get all pages of data
for {
if loopCount > 0 && pageToken == "" {
break
}
resp = models.UserRolesResponse{} // reset to avoid stale rows from previous page
err = apiClient.GetRequestFromAPIServer(serverInfo.Token, pageToken, serverInfo.CACert, "/xce/v1/users", &resp, params)
if err != nil {
return err
}
if resp.Data.Count == 0 {
if isFirstPage {
fmt.Println("no user found")
return nil
}
}
// For list mode, deduplicate user names client-side before printing
if commandType == "list" {
var unique []models.UserRoleInfo
for _, row := range resp.Data.Rows {
if _, ok := seen[row.UserName]; !ok {
seen[row.UserName] = struct{}{}
unique = append(unique, models.UserRoleInfo{UserName: row.UserName})
}
}
resp.Data.Rows = unique
resp.Data.Count = len(unique)
}
// Process the first page
if isFirstPage {
// Start printing process
pagePrinter.StartPrinting()
// Prepare for printing, calculate column width and table header, etc.
pagePrinter.PrepareForPrinting(&resp)
isFirstPage = false
}
// Print the current page
pagePrinter.PrintPage(&resp)
// Update page token and loop counter
pageToken = resp.Data.PageToken
loopCount++
}
// Finish printing
pagePrinter.Finish()
logger.Debug("user role response completed", zap.Int("pages", loopCount))
return nil
}
// uniqueUserNames deduplicates user names from a UserRolesResponse, preserving order.
// The server returns one row per role assignment, so the same user_name appears multiple times.
func uniqueUserNames(resp *models.UserRolesResponse) []string {
seen := make(map[string]struct{})
var result []string
for _, row := range resp.Data.Rows {
if _, ok := seen[row.UserName]; !ok {
seen[row.UserName] = struct{}{}
result = append(result, row.UserName)
}
}
return result
}

View file

@ -0,0 +1,82 @@
package users
import (
"testing"
"xcecli/models"
)
// --- dedup (regression guard) ---
func TestUniqueUserNames_NoDuplicates(t *testing.T) {
resp := &models.UserRolesResponse{}
resp.Data.Rows = []models.UserRoleInfo{{UserName: "alice"}, {UserName: "bob"}}
if got := uniqueUserNames(resp); len(got) != 2 {
t.Fatalf("expected 2, got %d", len(got))
}
}
func TestUniqueUserNames_WithDuplicates(t *testing.T) {
resp := &models.UserRolesResponse{}
resp.Data.Rows = []models.UserRoleInfo{
{UserName: "alice", UserRoleName: "admin"},
{UserName: "alice", UserRoleName: "basic_user"},
{UserName: "bob", UserRoleName: "basic_user"},
}
got := uniqueUserNames(resp)
if len(got) != 2 || got[0] != "alice" || got[1] != "bob" {
t.Fatalf("unexpected: %v", got)
}
}
func TestUniqueUserNames_Empty(t *testing.T) {
resp := &models.UserRolesResponse{}
if got := uniqueUserNames(resp); len(got) != 0 {
t.Fatalf("expected 0, got %d", len(got))
}
}
func TestUniqueUserNames_CrossPage(t *testing.T) {
seen := make(map[string]struct{})
dedup := func(rows []models.UserRoleInfo) []models.UserRoleInfo {
var unique []models.UserRoleInfo
for _, row := range rows {
if _, ok := seen[row.UserName]; !ok {
seen[row.UserName] = struct{}{}
unique = append(unique, models.UserRoleInfo{UserName: row.UserName})
}
}
return unique
}
p1 := dedup([]models.UserRoleInfo{{UserName: "alice"}, {UserName: "alice"}, {UserName: "bob"}})
p2 := dedup([]models.UserRoleInfo{{UserName: "bob"}, {UserName: "carol"}})
if len(p1) != 2 {
t.Fatalf("page1: expected 2, got %d", len(p1))
}
if len(p2) != 1 || p2[0].UserName != "carol" {
t.Fatalf("page2: expected [carol], got %v", p2)
}
}
// --- column width ---
func TestUserRoleNameColumnWidth(t *testing.T) {
resp := &models.UserRolesResponse{}
resp.Data.Rows = []models.UserRoleInfo{
{UserName: "alice", UserRoleName: "cluster_admin"},
{UserName: "bob", UserRoleName: "basic_user"},
}
width := calcUserRoleNameWidth(resp)
if width < len("cluster_admin") {
t.Errorf("ROLE_NAME width %d < len(%q)=%d", width, "cluster_admin", len("cluster_admin"))
}
}
func calcUserRoleNameWidth(resp *models.UserRolesResponse) int {
w := len(models.HEADER_USERROLE_NAME)
for _, u := range resp.Data.Rows {
if n := len(u.UserRoleName); n > w {
w = n
}
}
return w
}

View file

@ -0,0 +1,141 @@
package users
import (
"fmt"
"strings"
"xcecli/common/output"
"xcecli/common/request"
"xcecli/config"
logger "xcecli/config"
userrole_filter "xcecli/filters"
"xcecli/models"
"github.com/spf13/cobra"
)
func NewPerms4UserQuerier(configManager *config.ConfigManager) *Perms4User {
return &Perms4User{
configManager: configManager,
}
}
type Perms4User struct {
configManager *config.ConfigManager
userName string
filter *userrole_filter.UserRolesFilter
fields string
jsonOutput bool
noHeader bool
verbose bool
wideFormat bool
}
func NewPermissions4UserCmd(configManager *config.ConfigManager) *cobra.Command {
perms4userQuerier := NewPerms4UserQuerier(configManager)
cmd := &cobra.Command{
Use: "permissions4user",
Short: "Query permissions for a user",
Long: `Query permissions for a user, by specifying the user name.
example:
xce users permissions4user -u user_name # query permissions for a user
xce users permissions4user -u user_name -w # view permissions with wide format`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := perms4userQuerier.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := perms4userQuerier.RunUserPermsInfo(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// Add command line parameters
flags := cmd.Flags()
flags.StringVarP(&perms4userQuerier.userName, "user_name", "u", "", "filter by user name, only one user name can be specified")
flags.StringVarP(&perms4userQuerier.fields, "output", "o", "", "output fields for user roles")
flags.BoolVarP(&perms4userQuerier.wideFormat, "wided", "w", false, "output in wide format")
flags.BoolVarP(&perms4userQuerier.jsonOutput, "json", "", false, "output in JSON format")
flags.BoolVarP(&perms4userQuerier.noHeader, "noheader", "n", false, "output no header")
return cmd
}
func (pu *Perms4User) ParseCmdArgs(cmd *cobra.Command, args []string) error {
if pu.userName == "" {
return fmt.Errorf("please provide the user name")
}
if len(args) > 0 {
return fmt.Errorf("too many arguments")
}
return nil
}
func (up *Perms4User) RunUserPermsInfo() error {
// Get server information
serverInfo, err := up.configManager.GetServerInfo()
if err != nil {
return fmt.Errorf("failed to get server info: %w", err)
}
// create API client
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
// Create query parameters
params := make(map[string]string)
// Field mapping
fieldsMap := models.RoleFieldsMap
if up.verbose && up.fields == "" {
for header := range fieldsMap {
up.fields += header + ","
}
up.fields = strings.TrimSuffix(up.fields, ",")
}
// Process field selection
if up.fields != "" {
up.fields = request.CleanFields(up.fields)
mappedFields, err := request.CheckAndMapFields(up.fields, fieldsMap)
if err != nil {
return err
}
params["fields"] = strings.Join(mappedFields, ",")
}
// create page printer
pagePrinter := output.NewPagePrinter(output.DataTypeRoles, output.WithFields(up.fields),
output.WithFormatJson(up.jsonOutput), output.WithNoHeader(up.noHeader),
output.WithVerbose(up.verbose), output.WithWideFormat(up.wideFormat))
var resp models.RolesResponse
// Loop to get all pages of data
url := "/xce/v1/users/" + up.userName + "/permissions"
err = apiClient.GetRequestFromAPIServer(serverInfo.Token, "", serverInfo.CACert, url, &resp, nil)
if err != nil {
return err
}
if resp.Data.Count == 0 {
fmt.Println("no role found")
return nil
}
// Start printing process
pagePrinter.StartPrinting()
// Prepare for printing, calculate column width and table header, etc.
pagePrinter.PrepareForPrinting(&resp)
// Print the current page
pagePrinter.PrintPage(&resp)
// Finish printing
pagePrinter.Finish()
logger.Debug("roles response completed")
return nil
}

View file

@ -0,0 +1,100 @@
package users
import (
"fmt"
"net/http"
"strings"
"xcecli/common/request"
"xcecli/config"
logger "xcecli/config"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
func NewUnassignUserQuerier(configManager *config.ConfigManager) *UnassignUser {
return &UnassignUser{
configManager: configManager,
}
}
type UnassignUser struct {
configManager *config.ConfigManager
userName string
roleName string
}
func DeleteUserRolesCmd(configManager *config.ConfigManager) *cobra.Command {
unassignUserQuerier := NewUnassignUserQuerier(configManager)
cmd := &cobra.Command{
Use: "unassignrole",
Short: "Unassign permissions for a user",
Long: `Unassign permissions for a user, by specifying the user name.
example:
xce users unassignrole -u user_name -r role_name # unassign role for a user`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := unassignUserQuerier.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := unassignUserQuerier.RunDeleteUserRoleInfo(); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// Add command line parameters
flags := cmd.Flags()
flags.StringVarP(&unassignUserQuerier.userName, "user_name", "u", "", "filter by user name, only one user name can be specified")
flags.StringVarP(&unassignUserQuerier.roleName, "role_name", "r", "", "filter by role name, only one role name can be specified")
return cmd
}
func (uu *UnassignUser) ParseCmdArgs(cmd *cobra.Command, args []string) error {
if uu.userName == "" {
return fmt.Errorf("please provide the user name")
}
if uu.roleName == "" {
return fmt.Errorf("please provide the role name")
}
if len(args) > 0 {
return fmt.Errorf("too many arguments")
}
return nil
}
func (uu *UnassignUser) RunDeleteUserRoleInfo() error {
// Get server info and validate authentication
serverInfo, _ := uu.configManager.GetServerInfo()
// create the API client and submit the user
apiClient := request.NewAPIClient(serverInfo.URL)
apiClient.SetRootCAs(serverInfo.CACert)
url := fmt.Sprintf("/xce/v1/users/%s/%s", uu.userName, uu.roleName)
var resp request.APIResponse
err := apiClient.DeleteRequestFromAPIServer(
serverInfo.Token,
serverInfo.CACert,
url,
[]byte(nil),
&resp,
)
if err != nil {
return err
}
if resp.Code != http.StatusOK {
logger.Info("user unassign role failed", zap.Any("resp", resp), zap.Error(err))
return fmt.Errorf("%v", resp.Data)
}
logger.Debug("user unassign role successfully", zap.Any("data", resp.Data))
logger.Info("user unassign role successfully", zap.Any("data", resp.Data))
fmt.Printf("%v\n", resp.Data)
return nil
}

View file

@ -0,0 +1,35 @@
package users
import (
"xcecli/config"
"github.com/spf13/cobra"
)
func NewUsersCmd(configManager *config.ConfigManager) *cobra.Command {
// Create the parent users command
cmd := &cobra.Command{
Use: "users",
GroupID: "query",
Short: "Manage users",
Long: `Manage users in the system.
Use 'xce users whoami' to query current user name.
Use 'xce users list' to query users list only.
Use 'xce users view' to query users details.
Use 'xce users permissions4user' to query a user's permissions.
Use 'xce users unassignrole' to unassign a role for a user.
Use 'xce users <action>' to add users.`,
Run: func(cmd *cobra.Command, args []string) {
// Display help when no subcommand is specified
cmd.Help()
},
}
cmd.AddCommand(NewCurrentUserCmd(configManager))
cmd.AddCommand(NewListUserRolesCmd(configManager))
cmd.AddCommand(NewViewUserRolesCmd(configManager))
cmd.AddCommand(NewAssignUserRolesCmd(configManager))
cmd.AddCommand(NewPermissions4UserCmd(configManager))
cmd.AddCommand(DeleteUserRolesCmd(configManager))
return cmd
}

View file

@ -0,0 +1,45 @@
package users
import (
"fmt"
"strings"
"xcecli/config"
"github.com/spf13/cobra"
)
func NewViewUserRolesCmd(configManager *config.ConfigManager) *cobra.Command {
userrolesQuerier := NewUserRolesQuerier(configManager)
cmd := &cobra.Command{
Use: "view",
Short: "Query user role info",
Long: `Query user role info, support filter by user.
example:
xce users view # query all users
xce users view -w # view users with wide format`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := userrolesQuerier.ParseCmdArgs(cmd, args); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
if err := userrolesQuerier.RunUserRolesInfo("view"); err != nil {
fmt.Println(strings.ToLower(err.Error()))
return nil
}
return nil
},
}
// Add command line parameters
flags := cmd.Flags()
flags.StringVarP(&userrolesQuerier.filter.UserName, "user_name", "u", "", "filter by user name, e.g. -u user1,user2")
flags.StringVarP(&userrolesQuerier.filter.RoleName, "role_name", "r", "", "filter by role name, e.g. -r role1,role2")
flags.StringVarP(&userrolesQuerier.fields, "output", "o", "", "output fields for user roles")
flags.BoolVarP(&userrolesQuerier.wideFormat, "wided", "w", false, "output in wide format")
flags.BoolVarP(&userrolesQuerier.jsonOutput, "json", "", false, "output in JSON format")
flags.BoolVarP(&userrolesQuerier.noHeader, "noheader", "n", false, "output no header")
return cmd
}