package cmd import ( "os" "fmt" "log" "errors" "reflect" "strings" "io/ioutil" "gocage/jail" "encoding/json" "github.com/spf13/viper" ) // Recurse into structure, returning reflect.Value of wanted field. // Nested fields are named with a dot (ex "MyStruct.MyField") func getStructFieldValue(parentStruct interface{}, fieldName string) (reflect.Value, string, error) { v := reflect.ValueOf(parentStruct) if v.Kind() == reflect.Ptr { v = v.Elem() } if false { for i := 0 ; i < v.NumField(); i++ { f := v.Field(i) if f.Kind() == reflect.String { fmt.Printf("%v=%v\n", v.Type().Field(i).Name, f.Interface()) } } } if strings.Contains(fieldName, ".") { fs := strings.Split(fieldName, ".") f := v.FieldByName(fs[0]) if f.Kind() == reflect.Struct { return getStructFieldValue(f.Interface(), strings.Join(fs[1:], ".")) } else { log.Fatalln(fmt.Sprintf("%s is not a struct: %s\n", fs[0], f.Kind().String())) } } else { f := v.FieldByName(fieldName) if f.IsValid() { return f, fieldName, nil } else { return v, fieldName, errors.New(fmt.Sprintf("Field not found: %s", fieldName)) } } return v, fieldName, nil } // TODO : Replace by getStructFieldValue // Recurse into structure, returning reflect.Value of wanted field. // Nested fields are named with a dot (ex "MyStruct.MyField") func getStructField(parentStruct interface{}, fieldName string) (reflect.Value, string) { v := reflect.ValueOf(parentStruct) if v.Kind() == reflect.Ptr { v = v.Elem() } if false { for i := 0 ; i < v.NumField(); i++ { f := v.Field(i) if f.Kind() == reflect.String { fmt.Printf("%v=%v\n", v.Type().Field(i).Name, f.Interface()) } } } if strings.Contains(fieldName, ".") { fs := strings.Split(fieldName, ".") f := v.FieldByName(fs[0]) if f.Kind() == reflect.Struct { return getStructField(f.Interface(), strings.Join(fs[1:], ".")) } else { log.Fatalln(fmt.Sprintf("%s is not a struct: %s\n", fs[0], f.Kind().String())) } } return v, fieldName } /* Pretty display of jails field Fields to show are given in a string array parameter Ex. : displayJails(["Name", "JID", "RootPath"]) */ func displayStructFields(jails []Jail, valsToDisplay []string) { /* A line is defined by : Nr of fields For each field : Its name Its max length Its value */ type Field struct { Name string MaxLen int Value string } type Line []Field type Output []Line var out Output //fmt.Printf("%d fields in a %d items structure\n", len(valsToDisplay), len(jails)) // Browse structure to get max length and values for _, j := range jails { // Have to use a pointer, else reflect.Value.Elem() will panic : https://pkg.go.dev/reflect#Value.Elem tj := &j line := make([]Field, len(valsToDisplay)) for i, f := range valsToDisplay { a, f := getStructField(tj, f) field := Field { Name: f, } if a.FieldByName(f).IsValid() { // For now this just contains this item length, will adjust later // We need to divide length by number of items in string fields (because we can have more than 1 ip4_addr) if reflect.TypeOf(a.FieldByName(f).Interface()).Kind() == reflect.String { itnr := len(strings.Split(string(a.FieldByName(f).Interface().(string)), ",")) field.MaxLen = len(fmt.Sprintf("%v", a.FieldByName(f).Interface())) / itnr } else { field.MaxLen = len(fmt.Sprintf("%v", a.FieldByName(f).Interface())) } field.Value = fmt.Sprintf("%v", a.FieldByName(f).Interface()) } else { fmt.Printf("Invalid field name: %s\n", f) } line[i] = field } out = append(out, line) } if len(out) == 0 { fmt.Printf("Nothing to see here!\n") return } // Get real maximum length maxlen := make([]int, len(valsToDisplay)) for i := 0; i< len(valsToDisplay); i++ { maxlen[i] = len(valsToDisplay[i]) } for _, l := range out { for i, f := range l { if f.MaxLen > maxlen[i] { maxlen[i] = f.MaxLen } } } // Align maxlen to the real maximum length for io, l := range out { for ii, _ := range l { // We need to access slice by index if we want to modify content out[io][ii].MaxLen = maxlen[ii] } } totalLen := 0 // For each field, add separator and 2 blank spaces for _, f := range out[0] { totalLen += f.MaxLen + 3 } // Then add starting delimiter totalLen += 1 Debug := false if Debug == true { for _, f := range out[0] { fmt.Printf("%s max length : %d\n", f.Name, f.MaxLen) } } // Lets draw things on the screen! // First, headers: 1st separator line for i, f := range out[0] { if i == 0 { fmt.Printf("+") } for i := 0 ; i < f.MaxLen+2 ; i++ { fmt.Printf("=") } fmt.Printf("+") } fmt.Printf("\n") // Column names for i, f := range out[0] { if i == 0 { fmt.Printf("|") } /* Use vlsToDisplay to get real name (with "Config.") fmt.Printf(" %s", f.Name) for i := len(f.Name)+1 ; i < f.MaxLen+1 ; i++ { */ fmt.Printf(" %s", valsToDisplay[i]) for i := len(valsToDisplay[i])+1 ; i < f.MaxLen+1 ; i++ { fmt.Printf(" ") } fmt.Printf(" |") } // Finally separator line fmt.Printf("\n") for i, f := range out[0] { if i == 0 { fmt.Printf("+") } for i := 0 ; i < f.MaxLen+2 ; i++ { fmt.Printf("=") } fmt.Printf("+") } fmt.Printf("\n") // Then display data // Loop through lines for _, l := range out { // Loop through fields // In case we need to add a line for a 2nd IP, or whatever object var supplines = make(map[string]string) for i, f := range l { if i == 0 { fmt.Printf("|") } // Special cases of value displaying if f.Name == "JID" && f.Value == "0" { fmt.Printf(" ") } else if f.Name == "Ip4_addr" { ia := strings.Split(f.Value, ",") // If we have more than 1 value we need to finish this line, and store value for writing at the end of line loop for i, inter := range ia { if i > 0 { supplines[f.Name] = inter } else { fmt.Printf(" %s", inter) } } //fmt.Printf(" %s", strings.Split(strings.Split(f.Value, "|")[1], "/")[0]) } else { fmt.Printf(" %s", f.Value) } // Complete with spaces to the max length for i := len(f.Value)+1 ; i < f.MaxLen+1 ; i++ { fmt.Printf(" ") } fmt.Printf(" |") } // Draw supplementary lines if len(supplines) > 0 { for i, f := range l { if i == 0 { fmt.Printf("\n|") } // Overwrite value in this scope v, exists := supplines[f.Name] if exists { fmt.Printf(" %s", v) } else { // 1st option : Do not redisplay precedent line values fmt.Printf(" ") // 2nd option : Redisplay precedenet line values //fmt.Printf(" %s", f.Value) } // Complete with spaces to the max length for i := len(v)+1 ; i < f.MaxLen+1 ; i++ { fmt.Printf(" ") } fmt.Printf(" |") } } // Draw line separator between jails if !gNoLineSep { fmt.Printf("\n") for i, f := range out[0] { if i == 0 { fmt.Printf("+") } for i := 0 ; i < f.MaxLen+2 ; i++ { fmt.Printf("-") } fmt.Printf("+") } } fmt.Printf("\n") } if gNoLineSep { for i, f := range out[0] { if i == 0 { fmt.Printf("+") } for i := 0 ; i < f.MaxLen+2 ; i++ { fmt.Printf("-") } fmt.Printf("+") } } fmt.Printf("\n") } /* Get Jails from datastores. Store config and running metadata into gJails global var */ func ListJails(args []string, display bool) { fields := strings.Split(gDisplayColumns, ",") for _, d := range viper.GetStringSlice("datastore") { listJailsFromDatastore(d) } // This is the structure we will filter, then display var jails []Jail /************************************************************************************ / Filter jails with "filter" options /***********************************************************************************/ if len(gFilterJails) > 0 && gFilterJails != "none" { flts := make(map[string]string) for _, flt := range strings.Split(gFilterJails, ",") { sa := strings.Split(flt, "=") if len(sa) != 2 { fmt.Printf("Invalid format for filter %s\n", gFilterJails) return } flts[sa[0]] = sa[1] } if len(flts) > 0 { // Browse global gJails array to get filtered results for _, j := range gJails { for key, val := range flts { v, _, err := getStructFieldValue(&j, key) if err != nil { fmt.Printf("ERROR: Field %s not found\n", key) return } if strings.EqualFold(fmt.Sprintf("%v", v.Interface()), val) { jails = append(jails, j) } } } } } else { jails = gJails } /************************************************************************************ / Filter jails by names given on command line /***********************************************************************************/ if len(args) > 0 { var js []Jail for _, a := range args { for _, j := range jails { if j.Name == a { js = append(js, j) break } } } jails = js } /************************************************************************************ / Sort jails / We support 3 sort criteria max /***********************************************************************************/ if len(gSortFields) > 0 { js := initSortStruct() // The way we manage criteria quantity is not very elegant... var fct1, fct2, fct3 reflect.Value for i, c := range strings.Split(gSortFields, ",") { var fctName string if strings.HasPrefix(c, "-") { fctName = fmt.Sprintf("%sDec", strings.Replace(c, "-", "", 1)) } else { // Par defaut (pas de prefix +/-) on considere un tri incremental fctName = fmt.Sprintf("%sInc", strings.Replace(c, "+", "", 1)) } // Get function by its name fct, _, err := getStructFieldValue(js, fctName) if err != nil { fieldName := strings.Replace(strings.Replace(c, "-", "", 1), "+", "", 1) fmt.Printf("ERROR getting JailSort struct field %s. Please check the field name: %s\n", fctName, fieldName) return } switch i+1 { case 1: fct1 = fct case 2: fct2 = fct case 3: fct3 = fct } } switch len(strings.Split(gSortFields, ",")) { case 1: OrderedBy(fct1.Interface().(lessFunc)).Sort(jails) case 2: OrderedBy(fct1.Interface().(lessFunc), fct2.Interface().(lessFunc)).Sort(jails) case 3: OrderedBy(fct1.Interface().(lessFunc), fct2.Interface().(lessFunc), fct3.Interface().(lessFunc)).Sort(jails) } } /************************************************************************************ / Finally, display jails /***********************************************************************************/ if display { displayStructFields(jails, fields) } } func listJailsFromDatastore(datastore string) { fileInfo, err := os.Stat(datastore) if err != nil { log.Fatalln(fmt.Sprintf("Unable to access %s, check path and/or rights", datastore)) } if fileInfo.IsDir() == false { log.Fatalln(fmt.Sprintf("%s is not a directory", datastore)) } // A datastore have to contain a "jails" directory jailsDir := fmt.Sprintf("%s/jails", datastore) fileInfo, err = os.Stat(jailsDir) if err != nil { log.Fatalln(fmt.Sprintf("Unable to access %s, check path and/or rights", jailsDir)) } if fileInfo.IsDir() == false { log.Fatalln(fmt.Sprintf("%s is not a directory", jailsDir)) } listJailsFromDirectory(jailsDir) } func listJailsFromDirectory(dir string) ([]Jail) { files, err := ioutil.ReadDir(dir) if err != nil { log.Fatalln(fmt.Sprintf("Unable to browse %s, check path and/or rights", dir)) } for _, fi := range files { if fi.IsDir() == true { // 1. Get conf from config.json jailConfPath := fmt.Sprintf("%s/%s/%s", dir, fi.Name(), "config.json") jailConf, err := getJailConfig(jailConfPath) if err != nil { log.Println("ERROR reading jail config for %s", jailConfPath) } // 2. Build jail object from config jailRootPath := fmt.Sprintf("%s/%s/%s", dir, fi.Name(), "root") j := Jail{ Name: jailConf.Host_hostname, Config: jailConf, ConfigPath: jailConfPath, RootPath: jailRootPath, Running: false, } // 3. Add current running informations rjails, err := jail.GetJails() if err != nil { log.Fatalln("Unable to list running jails") } for _, rj := range rjails { if rj.Path == j.RootPath { j.JID = rj.Jid j.Running = true j.InternalName = rj.Name break } } /* This op take some 600ms for ~40 jails :^( */ out, err := executeCommand(fmt.Sprintf("zfs list -H -o name %s", j.RootPath)) if err != nil { fmt.Printf("ERROR getting dataset from %s: %s\n", j.RootPath, err.Error()) } else { j.Zpool = strings.Split(strings.TrimSuffix(out, "\n"), "/")[0] } gJails = append(gJails, j) } } return gJails } func getJailConfig(jailConfigPath string) (JailConfig, error) { content, err := ioutil.ReadFile(jailConfigPath) if err != nil { log.Fatalln(fmt.Sprintf("Unable to read %s, check path and/or rights", jailConfigPath)) } var jc JailConfig err = json.Unmarshal([]byte(content), &jc) if err != nil { log.Fatalln(fmt.Sprintf("Error occured during unmarshaling %s: %s", jailConfigPath, err.Error())) } return jc, err }