diff --git a/README.md b/README.md index 3db1693..b093a47 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,65 @@ # gocage -Jail management tool for FreeBSD, written in Go. -Support iocage jails, so they can coexist. -Gocage is meant to be a complete jail management tool with network, snapshots, jail cloning support and a web interface. \ No newline at end of file +Jail management tool for FreeBSD, written in Go. +Support iocage jails, so they can coexist. +Gocage is meant to be a complete jail management tool with network, snapshots, jail cloning support and a web interface. + +# List jails +Nothing fancy, use + gocage list + +## Specify fields to display +Use -o to specify which fields you want to display: + gocage list -o JID,Name,Running,Config.Boot,Config.Comment + +=====+==========+=========+=============+================+ + | JID | Name | Running | Config.Boot | Config.Comment | + +=====+==========+=========+=============+================+ + | 183 | test | true | 1 | none | + +-----+----------+---------+-------------+----------------+ + | 29 | srv-irc | true | 1 | | + +-----+----------+---------+-------------+----------------+ + | | srv-web | false | 0 | | + +-----+----------+---------+-------------+----------------+ + | 22 | srv-dns1 | true | 1 | | + +-----+----------+---------+-------------+----------------+ + +## Filter jails +### By name +Just add name on gocage list command : + gocage list srv-bdd srv-web + +=====+=========+=================+=======================+=========+ + | JID | Name | Config.Release | Config.Ip4_addr | Running | + +=====+=========+=================+=======================+=========+ + | 98 | srv-db | 13.0-RELEASE-p5 | vnet0|192.168.1.56/24 | true | + +-----+---------+-----------------+-----------------------+---------+ + | 41 | srv-web | 13.0-RELEASE-p4 | vnet0|192.168.1.26/24 | true | + +-----+---------+-----------------+-----------------------+---------+ + +### By field value +You can filter jails with -f option, followed by key=value. Suppose you want to see only active at boot jails: + gocage list -f Config.Boot=1 -o JID,Name,Running,Config.Boot,Config.Comment + +=====+==========+=========+=============+================+ + | JID | Name | Running | Config.Boot | Config.Comment | + +=====+==========+=========+=============+================+ + | 183 | test | true | 1 | none | + +-----+----------+---------+-------------+----------------+ + | 29 | srv-irc | true | 1 | | + +-----+----------+---------+-------------+----------------+ + | | srv-db | false | 1 | none | + +-----+----------+---------+-------------+----------------+ + | 22 | srv-dns1 | true | 1 | | + +-----+----------+---------+-------------+----------------+ + +Now, only active at boot and running : + gocage list -f Config.Boot=1,Running=true -o JID,Name,Running,Config.Boot + +=====+==========+=========+=============+ + | JID | Name | Running | Config.Boot | + +=====+==========+=========+=============+ + | 183 | test | true | 1 | + +-----+----------+---------+-------------+ + | 29 | srv-irc | true | 1 | + +-----+----------+---------+-------------+ + | 22 | srv-dns1 | true | 1 | + +-----+----------+---------+-------------+ + + diff --git a/cmd/list.go b/cmd/list.go index c7c2532..fc713c5 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -4,6 +4,7 @@ import ( "os" "fmt" "log" + "errors" "reflect" "strings" "io/ioutil" @@ -13,6 +14,46 @@ import ( ) +// 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) { @@ -101,6 +142,11 @@ func displayStructFields(jails []Jail, valsToDisplay []string) { 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++ { @@ -173,6 +219,7 @@ func displayStructFields(jails []Jail, valsToDisplay []string) { // 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) @@ -251,29 +298,67 @@ func displayStructFields(jails []Jail, valsToDisplay []string) { /* Get Jails from datastores. Store config and running metadata into gJails global var */ func ListJails(args []string, display bool) { - //fields := []string{"JID", "Name", "Config.Release", "Config.Ip4_addr", "RootPath", "Running", "Config.Jail_zfs", "Config.Jail_zfs_dataset", "Zpool"} - //fields := []string{"JID", "Name", "Config.Release", "Config.Ip4_addr", "Running", "Zpool"} fields := strings.Split(gDisplayColumns, ",") for _, d := range viper.GetStringSlice("datastore") { listJailsFromDatastore(d) } - if display { - if len(args) > 0 { - var js []Jail - for _, a := range args { - for _, j := range gJails { - if j.Name == a { - js = append(js, j) - break + // 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) } } } - displayStructFields(js, fields) - } else { - displayStructFields(gJails, fields) } + } 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 + } + + if display { + displayStructFields(jails, fields) } } diff --git a/cmd/root.go b/cmd/root.go index be949ee..9d2987e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -20,6 +20,8 @@ var ( gConfigFile string gDisplayColumns string + gFilterJails string + gSortFields string gNoLineSep bool @@ -73,11 +75,13 @@ func init() { // Global switches rootCmd.PersistentFlags().StringVarP(&gConfigFile, "config", "c", "/usr/local/etc/gocage.conf.yml", "GoCage configuration file") - rootCmd.PersistentFlags().BoolVarP(&gUseSudo, "sudo", "s", false, "Use sudo to run commands") + rootCmd.PersistentFlags().BoolVarP(&gUseSudo, "sudo", "u", false, "Use sudo to run commands") // Command dependant switches listCmd.PersistentFlags().StringVarP(&gDisplayColumns, "outcol", "o", "JID,Name,Config.Release,Config.Ip4_addr,Running", "Show these columns in output") listCmd.PersistentFlags().BoolVarP(&gNoLineSep, "nolinesep", "l", false, "Do not display line separator between jails") + listCmd.PersistentFlags().StringVarP(&gFilterJails, "filter", "f", "none", "Only display jails with these values. Ex: \"gocage list -f Config.Boot=1\" will only list started on boot jails") + listCmd.PersistentFlags().StringVarP(&gSortFields, "sort", "s", "none", "Display jails sorted by field values. Ex: \"gocage list -s Config.Priority\" will sort jails by their start priority. NOT IMPLEMENTED YET") // Now declare commands rootCmd.AddCommand(versionCmd) @@ -114,6 +118,12 @@ func initConfig() { if listCmd.Flags().Lookup("nolinesep") != nil && false == listCmd.Flags().Lookup("nolinesep").Changed { gNoLineSep = viper.GetBool("nolinesep") } + if listCmd.Flags().Lookup("filter") != nil && false == listCmd.Flags().Lookup("filter").Changed { + gFilterJails = viper.GetString("filter") + } + if listCmd.Flags().Lookup("sort") != nil && false == listCmd.Flags().Lookup("sort").Changed { + gSortFields = viper.GetString("sort") + } } func Execute() {