// Mgmt // Copyright (C) James Shubin and the project contributors // Written by James Shubin and the project contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // // Additional permission under GNU GPL version 3 section 7 // // If you modify this program, or any covered work, by linking or combining it // with embedded mcl code and modules (and that the embedded mcl code and // modules which link with this program, contain a copy of their source code in // the authoritative form) containing parts covered by the terms of any other // license, the licensors of this program grant you additional permission to // convey the resulting work. Furthermore, the licensors of this program grant // the original author, James Shubin, additional permission to update this // additional permission if he deems it necessary to achieve the goals of this // additional permission. package resources import ( "bytes" "context" "fmt" "os" "path" "strings" "sync" "github.com/purpleidea/mgmt/engine" "github.com/purpleidea/mgmt/engine/traits" "github.com/purpleidea/mgmt/util/errwrap" "github.com/purpleidea/mgmt/util/recwatch" ) func init() { engine.RegisterResource("sysctl", func() engine.Res { return &SysctlRes{} }) if !strings.HasPrefix(SysctlConfDir, "/") { panic("the SysctlConfDir does not start with a slash") } if !strings.HasSuffix(SysctlConfDir, "/") { panic("the SysctlConfDir does not end with a slash") } } const ( // SysctlConfDir is the directory to store persistent sysctl files in. SysctlConfDir = "/etc/sysctl.d/" // SysctlConfPrefix is the prefix we prepend to any automatically chosen // filename that we put in the /etc/sysctl.d/ directory. // TODO: What prefix should we use if any? SysctlConfPrefix = "99-" ) // SysctlRes is a resource for setting kernel parameters. // TODO: Add a sysctl:clean resource that removes any unmanaged files from // /etc/sysctl.d/ and optionally blanks out the stock /etc/sysctl.conf file too. type SysctlRes struct { traits.Base // add the base methods without re-implementation init *engine.Init // Value is the string value to set. Make sure you specify it in the // same format that the kernel parses it as to avoid automation // "flapping". You can test this by writing a value to the correct // /proc/sys/ path entry with `echo foo >` and then reading it back out // and seeing what the "parsed" correct format is. You must not include // the trailing newline which is present in the readback for all values. Value string `lang:"value" yaml:"value"` // Runtime specifies whether this value should be set immediately. It // defaults to true. If this is not set, then the value must be set in a // file and the machine will have to reboot for the setting to take // effect. Runtime bool `lang:"runtime" yaml:"runtime"` // Persist specifies whether this value should be stored on disk where // it will persist across reboots. It defaults to true. Keep in mind, // that if this is not used, but `Runtime` is true, then the value will // be restored anyways if `mgmt` runs on boot, which may be what you // want anyways. Persist bool `lang:"persist" yaml:"persist"` // Filename is the full path for the persistence file which is usually // read on boot. We usually use entries in the /etc/sysctl.d/ directory. // By convention, they end in .conf and start with a numeric prefix and // a dash. For example: /etc/sysctl.d/10-dmesg.conf for example. If this // is omitted, the filename will be chosen automatically. Filename string `lang:"path" yaml:"path"` } // toPath converts our name into the magic kernel path. It does not validate // that the name is in a valid format. func (obj *SysctlRes) toPath() string { return path.Join("/proc/sys/", strings.ReplaceAll(obj.Name(), ".", "/")) } // getFilename returns the filename of the config that we would use if we're // setting one. This does not look at the is persistent aspect. func (obj *SysctlRes) getFilename() string { if obj.Filename != "" { return obj.Filename } return SysctlConfDir + SysctlConfPrefix + obj.Name() + ".conf" } // Default returns some sensible defaults for this resource. func (obj *SysctlRes) Default() engine.Res { return &SysctlRes{ Runtime: true, Persist: true, } } // Validate reports any problems with the struct definition. func (obj *SysctlRes) Validate() error { if strings.Contains(obj.Name(), "/") { // We do this to avoid having two resources which "fight" with // each other by using the alternative representation. return fmt.Errorf("name contains slashes, use the dotted representation") } if strings.Contains(obj.Name(), "=") { return fmt.Errorf("name contains equals sign, this is illegal") } if strings.HasPrefix(obj.Name(), ".") || strings.HasSuffix(obj.Name(), ".") { return fmt.Errorf("name contains leading or trailing periods") } if obj.Name() != strings.TrimSpace(obj.Name()) { return fmt.Errorf("name has leading or trailing whitespace") } if obj.Value == "" { return fmt.Errorf("value is empty") } if strings.TrimSpace(obj.Value) != obj.Value { return fmt.Errorf("value contains leading or trailing whitespace") } // TODO: We could probably relax this check I suppose. if !obj.Runtime && !obj.Persist { return fmt.Errorf("you must either set the value at runtime or you must persist") } // Parse the Name() and see if it's a valid path under /proc/sys/ dir. if _, err := os.Stat(obj.toPath()); err != nil && !os.IsNotExist(err) { // system or permissions error? return errwrap.Wrapf(err, "unknown stat error") } else if err != nil { // TODO: Could there be a kernel that doesn't show this path? return fmt.Errorf("name is not a valid kernel path: %s", obj.toPath()) } if obj.Persist && !strings.HasSuffix(obj.getFilename(), ".conf") { return fmt.Errorf("filename must end with .conf") } if obj.Persist && !strings.HasPrefix(obj.getFilename(), "/") { return fmt.Errorf("filename must be absolute and start with slash") } return nil } // Init runs some startup code for this resource. func (obj *SysctlRes) Init(init *engine.Init) error { obj.init = init // save for later return nil } // Cleanup is run by the engine to clean up after the resource is done. func (obj *SysctlRes) Cleanup() error { return nil } // Watch is the primary listener for this resource and it outputs events. This // one watches the on disk filename if it creates one, as well as the runtime // value the kernel has stored! func (obj *SysctlRes) Watch(ctx context.Context) error { wg := &sync.WaitGroup{} defer wg.Wait() recurse := false // single file var events1, events2 chan recwatch.Event if obj.Runtime { recWatcher, err := recwatch.NewRecWatcher(obj.toPath(), recurse) if err != nil { return err } defer recWatcher.Close() events1 = recWatcher.Events() } if obj.Persist { recWatcher, err := recwatch.NewRecWatcher(obj.getFilename(), recurse) if err != nil { return err } defer recWatcher.Close() events2 = recWatcher.Events() } obj.init.Running() // when started, notify engine that we're running var send = false // send event? for { select { case event, ok := <-events1: if !ok { // channel shutdown return fmt.Errorf("unexpected close") } if err := event.Error; err != nil { return err } if obj.init.Debug { // don't access event.Body if event.Error isn't nil obj.init.Logf("event(%s): %v", event.Body.Name, event.Body.Op) } send = true case event, ok := <-events2: if !ok { // channel shutdown return fmt.Errorf("unexpected close") } if err := event.Error; err != nil { return err } if obj.init.Debug { // don't access event.Body if event.Error isn't nil obj.init.Logf("event(%s): %v", event.Body.Name, event.Body.Op) } send = true case <-ctx.Done(): // closed by the engine to signal shutdown return nil } // do all our event sending all together to avoid duplicate msgs if send { send = false obj.init.Event() // notify engine of an event (this can block) } } } // CheckApply checks the resource state and applies the resource if the bool // input is true. It returns error info and if the state check passed or not. func (obj *SysctlRes) CheckApply(ctx context.Context, apply bool) (bool, error) { checkOK := true // TODO: If there any reason to do one of these before the other? At // least right now, if the runtime change causes a kernel panic, the // machine will have a better chance of coming back online without the // persisted value being stored. if c, err := obj.runtimeCheckApply(ctx, apply); err != nil { return false, err } else if !c { checkOK = false } if c, err := obj.persistCheckApply(ctx, apply); err != nil { return false, err } else if !c { checkOK = false } return checkOK, nil // w00t } // runtimeCheckApply checks the runtime value in the kernel, and modifies it if // needed. func (obj *SysctlRes) runtimeCheckApply(ctx context.Context, apply bool) (bool, error) { if !obj.Runtime { return true, nil } // Clean off any whitespace it comes with and then always add a newline. expected := []byte(obj.Value + "\n") b, err := os.ReadFile(obj.toPath()) if err != nil && !os.IsNotExist(err) { // system or permissions error? return false, nil } if err == nil && bytes.Equal(expected, b) { return true, nil // we match! } // Down here, file does not exist or does not match... if !apply { return false, nil } if err := os.WriteFile(obj.toPath(), expected, 0644); err != nil { return false, err } obj.init.Logf("runtime `%s` to: %s\n", obj.Value, obj.toPath()) return false, err } // persistCheckApply checks the on-disk value for the kernel, and modifies it if // needed. func (obj *SysctlRes) persistCheckApply(ctx context.Context, apply bool) (bool, error) { if !obj.Persist { return true, nil } // Clean off any whitespace and put it in the standard format. // TODO: Should we add a "last managed by mgmt on $date" line ? s := fmt.Sprintf("%s = %s\n", obj.Name(), obj.Value) expected := []byte(s) b, err := os.ReadFile(obj.getFilename()) if err != nil && !os.IsNotExist(err) { // system or permissions error? return false, nil } if err == nil && bytes.Equal(expected, b) { return true, nil // we match! } // Down here, file does not exist or does not match... if !apply { return false, nil } if err := os.WriteFile(obj.getFilename(), expected, 0644); err != nil { return false, err } obj.init.Logf("persist `%s` to: %s\n", obj.Value, obj.getFilename()) return false, nil } // Cmp compares two resources and returns an error if they are not equivalent. func (obj *SysctlRes) Cmp(r engine.Res) error { // we can only compare SysctlRes to others of the same resource kind res, ok := r.(*SysctlRes) if !ok { return fmt.Errorf("not a %s", obj.Kind()) } if obj.Value != res.Value { return fmt.Errorf("the Value differs") } if obj.Runtime != res.Runtime { return fmt.Errorf("the Runtime value differs") } if obj.Persist != res.Persist { return fmt.Errorf("the Persist value differs") } // TODO: We could compare the actual resultant Filename if we're using // it, even if it comes from different representations, eg: specified vs // chosen automatically. If they don't differ, it's fine with us! if obj.Filename != res.Filename { return fmt.Errorf("the contents of Filename differ") } return nil } // UnmarshalYAML is the custom unmarshal handler for this struct. It is // primarily useful for setting the defaults. func (obj *SysctlRes) UnmarshalYAML(unmarshal func(interface{}) error) error { type rawRes SysctlRes // indirection to avoid infinite recursion def := obj.Default() // get the default res, ok := def.(*SysctlRes) // put in the right format if !ok { return fmt.Errorf("could not convert to SysctlRes") } raw := rawRes(*res) // convert; the defaults go here if err := unmarshal(&raw); err != nil { return err } *obj = SysctlRes(raw) // restore from indirection with type conversion! return nil }