github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/client/discovery/cli_helper.go (about) 1 // Copyright (c) 2021, R.I. Pienaar and the Choria Project contributors 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package discovery 6 7 import ( 8 "bytes" 9 "context" 10 "fmt" 11 "io" 12 "os" 13 "path/filepath" 14 "strings" 15 "time" 16 17 "github.com/choria-io/go-choria/config" 18 "github.com/choria-io/go-choria/filter" 19 "github.com/choria-io/go-choria/inter" 20 "github.com/choria-io/go-choria/protocol" 21 "github.com/choria-io/go-choria/providers/discovery/broadcast" 22 "github.com/choria-io/go-choria/providers/discovery/external" 23 "github.com/choria-io/go-choria/providers/discovery/flatfile" 24 "github.com/choria-io/go-choria/providers/discovery/inventory" 25 "github.com/choria-io/go-choria/providers/discovery/puppetdb" 26 log "github.com/sirupsen/logrus" 27 ) 28 29 type StandardOptions struct { 30 Collective string `json:"collective"` 31 FactFilter []string `json:"facts"` 32 AgentFilter []string `json:"agents"` 33 ClassFilter []string `json:"classes"` 34 IdentityFilter []string `json:"identities"` 35 CombinedFilter []string `json:"combined"` 36 CompoundFilter string `json:"compound"` 37 DiscoveryMethod string `json:"discovery_method"` 38 DiscoveryTimeout int `json:"discovery_timeout"` 39 DynamicDiscoveryTimeout bool `json:"dynamic_discovery_timeout"` 40 NodesFile string `json:"nodes_file"` 41 DiscoveryOptions map[string]string `json:"discovery_options"` 42 43 unsetMethod bool 44 } 45 46 // NewStandardOptions creates a new CLI options helper 47 func NewStandardOptions() *StandardOptions { 48 return &StandardOptions{ 49 FactFilter: []string{}, 50 AgentFilter: []string{}, 51 ClassFilter: []string{}, 52 IdentityFilter: []string{}, 53 CombinedFilter: []string{}, 54 DiscoveryOptions: make(map[string]string), 55 } 56 } 57 58 // Merge merges opt with the settings here, when a basic setting is 59 // set in opt it will overwrite the one here, when its a filter like 60 // a list or map it will extend ours with its values. 61 func (o *StandardOptions) Merge(opt *StandardOptions) { 62 if opt.Collective != "" { 63 o.Collective = opt.Collective 64 } 65 o.FactFilter = append(o.FactFilter, opt.FactFilter...) 66 o.AgentFilter = append(o.AgentFilter, opt.AgentFilter...) 67 o.ClassFilter = append(o.ClassFilter, opt.ClassFilter...) 68 o.IdentityFilter = append(o.IdentityFilter, opt.IdentityFilter...) 69 o.CombinedFilter = append(o.CombinedFilter, opt.CombinedFilter...) 70 if opt.CompoundFilter != "" { 71 o.CompoundFilter = opt.CompoundFilter 72 } 73 if opt.DiscoveryMethod != "" { 74 o.DiscoveryMethod = opt.DiscoveryMethod 75 } 76 if opt.DiscoveryTimeout > 0 { 77 o.DiscoveryTimeout = opt.DiscoveryTimeout 78 } 79 if opt.NodesFile != "" { 80 o.NodesFile = opt.NodesFile 81 } 82 for k, v := range opt.DiscoveryOptions { 83 o.DiscoveryOptions[k] = v 84 } 85 } 86 87 // AddSelectionFlags adds the --dm and --discovery-timeout options 88 func (o *StandardOptions) AddSelectionFlags(app inter.FlagApp) { 89 app.Flag("dm", "Sets a discovery method (mc, choria, file, external, inventory)").EnumVar(&o.DiscoveryMethod, "broadcast", "choria", "mc", "file", "flatfile", "external", "inventory") 90 app.Flag("discovery-timeout", "Timeout for doing discovery").PlaceHolder("SECONDS").IntVar(&o.DiscoveryTimeout) 91 app.Flag("discovery-window", "Enables a sliding window based dynamic discovery timeout (experimental)").UnNegatableBoolVar(&o.DynamicDiscoveryTimeout) 92 } 93 94 // AddFilterFlags adds the various flags like -W, -S, -T etc 95 func (o *StandardOptions) AddFilterFlags(app inter.FlagApp) { 96 app.Flag("wf", "Match hosts with a certain fact").Short('F').StringsVar(&o.FactFilter) 97 app.Flag("wc", "Match hosts with a certain configuration management class").Short('C').StringsVar(&o.ClassFilter) 98 app.Flag("wa", "Match hosts with a certain Choria agent").Short('A').StringsVar(&o.AgentFilter) 99 app.Flag("wi", "Match hosts with a certain Choria identity").Short('I').StringsVar(&o.IdentityFilter) 100 app.Flag("with", "Combined classes and facts filter").Short('W').PlaceHolder("FILTER").StringsVar(&o.CombinedFilter) 101 app.Flag("select", "Match hosts using a expr compound filter").Short('S').PlaceHolder("EXPR").StringVar(&o.CompoundFilter) 102 app.Flag("target", "Target a specific sub collective").Short('T').StringVar(&o.Collective) 103 app.Flag("do", "Options for the chosen discovery method").PlaceHolder("K=V").StringMapVar(&o.DiscoveryOptions) 104 } 105 106 // AddFlatFileFlags adds the flags to select nodes using --nodes in text, json and yaml formats 107 func (o *StandardOptions) AddFlatFileFlags(app inter.FlagApp) { 108 app.Flag("nodes", "List of nodes to interact with in JSON, YAML or TEXT formats").ExistingFileVar(&o.NodesFile) 109 } 110 111 func (o *StandardOptions) Discover(ctx context.Context, fw inter.Framework, agent string, supportStdin bool, progress bool, logger *log.Entry) ([]string, time.Duration, error) { 112 var ( 113 fformat flatfile.SourceFormat 114 sourceFile io.Reader 115 nodes []string 116 to = time.Second * time.Duration(o.DiscoveryTimeout) 117 ) 118 119 filter, err := o.NewFilter(agent) 120 if err != nil { 121 return nil, 0, err 122 } 123 124 switch { 125 case o.NodesFile != "": 126 o.DiscoveryMethod = "flatfile" 127 128 switch filepath.Ext(o.NodesFile) { 129 case ".json": 130 logger.Debugf("Using %q as JSON format file", o.NodesFile) 131 fformat = flatfile.JSONFormat 132 case ".yaml", ".yml": 133 logger.Debugf("Using %q as YAML format file", o.NodesFile) 134 fformat = flatfile.YAMLFormat 135 default: 136 logger.Debugf("Using %q as TEXT format file", o.NodesFile) 137 fformat = flatfile.TextFormat 138 } 139 140 sourceFile, err = os.Open(o.NodesFile) 141 if err != nil { 142 return nil, 0, err 143 } 144 145 case len(filter.Compound) > 0 && o.DiscoveryMethod != "broadcast" && o.DiscoveryMethod != "inventory" && o.DiscoveryMethod != "mc": 146 o.DiscoveryMethod = "broadcast" 147 logger.Debugf("Forcing discovery mode to broadcast to support compound filters") 148 149 case supportStdin && o.isPiped() && (o.DiscoveryMethod == "" || o.unsetMethod): 150 stdin, err := io.ReadAll(os.Stdin) 151 stdin = bytes.TrimSpace(stdin) 152 sourceFile = bytes.NewReader(stdin) 153 154 if err != nil { 155 logger.Debugf("Could not read STDIN to detect flatfile override") 156 break 157 } 158 159 if len(stdin) == 0 { 160 logger.Debugf("No data on STDIN found, not forcing flatfile override") 161 break 162 } 163 164 if !(bytes.HasPrefix(stdin, []byte("{")) && bytes.HasSuffix(stdin, []byte("}"))) { 165 logger.Debugf("Found non JSON data on STDIN, not forcing flatfile override") 166 break 167 } 168 169 o.DiscoveryMethod = "flatfile" 170 fformat = flatfile.ChoriaResponsesFormat 171 logger.Debugf("Forcing discovery mode to flatfile with Choria responses on STDIN") 172 } 173 174 if o.DiscoveryMethod == "flatfile" && (fformat == 0 || sourceFile == nil) && len(o.DiscoveryOptions) == 0 { 175 return nil, 0, fmt.Errorf("could not determine file to use as discovery source") 176 } 177 178 if progress { 179 fmt.Printf("Discovering nodes using the %s method .... ", o.DiscoveryMethod) 180 } 181 182 start := time.Now() 183 switch o.DiscoveryMethod { 184 case "mc", "broadcast": 185 opts := []broadcast.DiscoverOption{broadcast.Filter(filter), broadcast.Collective(o.Collective), broadcast.Timeout(to)} 186 if o.DynamicDiscoveryTimeout { 187 opts = append(opts, broadcast.SlidingWindow()) 188 } 189 190 nodes, err = broadcast.New(fw).Discover(ctx, opts...) 191 case "choria", "puppetdb": 192 nodes, err = puppetdb.New(fw).Discover(ctx, puppetdb.Filter(filter), puppetdb.Collective(o.Collective), puppetdb.Timeout(to)) 193 case "external": 194 nodes, err = external.New(fw).Discover(ctx, external.Filter(filter), external.Timeout(to), external.Collective(o.Collective), external.DiscoveryOptions(o.DiscoveryOptions)) 195 case "flatfile", "file": 196 nodes, err = flatfile.New(fw).Discover(ctx, flatfile.Reader(sourceFile), flatfile.Format(fformat), flatfile.DiscoveryOptions(o.DiscoveryOptions)) 197 case "inventory": 198 nodes, err = inventory.New(fw).Discover(ctx, inventory.Filter(filter), inventory.Collective(o.Collective), inventory.DiscoveryOptions(o.DiscoveryOptions)) 199 default: 200 return nil, 0, fmt.Errorf("unsupported discovery method %q", o.DiscoveryMethod) 201 } 202 203 if progress { 204 fmt.Printf("%d\n", len(nodes)) 205 } 206 207 return nodes, time.Since(start), err 208 } 209 210 func (o *StandardOptions) isPiped() bool { 211 fi, err := os.Stdin.Stat() 212 if err != nil { 213 return false 214 } 215 216 return (fi.Mode() & os.ModeCharDevice) == 0 217 } 218 219 // SetDefaultsFromChoria sets the defaults based on cfg 220 func (o *StandardOptions) SetDefaultsFromChoria(fw inter.Framework) { 221 o.SetDefaultsFromConfig(fw.Configuration()) 222 } 223 224 // SetDefaultsFromConfig sets the defaults based on cfg 225 func (o *StandardOptions) SetDefaultsFromConfig(cfg *config.Config) { 226 if o.DiscoveryMethod == "" { 227 o.DiscoveryMethod = cfg.DefaultDiscoveryMethod 228 o.unsetMethod = true 229 } 230 231 if o.Collective == "" { 232 o.Collective = cfg.MainCollective 233 } 234 235 if o.DiscoveryTimeout == 0 { 236 o.DiscoveryTimeout = cfg.DiscoveryTimeout 237 } 238 239 if len(o.DiscoveryOptions) == 0 { 240 for _, val := range cfg.DefaultDiscoveryOptions { 241 parts := strings.Split(val, "=") 242 if len(parts) == 2 { 243 o.DiscoveryOptions[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) 244 } 245 } 246 } 247 } 248 249 // NewFilter creates a new filter based on the options supplied, additionally agent will be added to the list 250 func (o *StandardOptions) NewFilter(agent string) (*protocol.Filter, error) { 251 return filter.NewFilter( 252 filter.FactFilter(o.FactFilter...), 253 filter.AgentFilter(o.AgentFilter...), 254 filter.ClassFilter(o.ClassFilter...), 255 filter.IdentityFilter(o.IdentityFilter...), 256 filter.CombinedFilter(o.CombinedFilter...), 257 filter.CompoundFilter(o.CompoundFilter), 258 filter.AgentFilter(agent), 259 ) 260 }