engine: resources: Add a gsettings resource
This adds a way to run the gsettings command for configuring dconf settings usually used by GNOME applications.
This commit is contained in:
391
engine/resources/gsettings.go
Normal file
391
engine/resources/gsettings.go
Normal file
@@ -0,0 +1,391 @@
|
||||
// Mgmt
|
||||
// Copyright (C) James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> 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 <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
engine.RegisterResource("gsettings", func() engine.Res { return &GsettingsRes{} })
|
||||
}
|
||||
|
||||
const (
|
||||
gsettingsTmpl = "gsettings@%s"
|
||||
)
|
||||
|
||||
// GsettingsRes is a resource for setting dconf values through gsettings. The
|
||||
// ideal scenario is that this runs as the same user that wants settings set.
|
||||
// This should be done by a local user-specific mgmt daemon. As a special case,
|
||||
// we can run as root (or anyone with permission) which launches a subprocess
|
||||
// which setuid/setgid's to that user to run the needed operations. To specify
|
||||
// the schema and key, set the resource name as "schema key" (separated by a
|
||||
// single space character) or use the parameters.
|
||||
type GsettingsRes struct {
|
||||
// XXX: add a dbus version of this-- it will require running as the user
|
||||
// directly since in that scenario we can't spawn a process of the right
|
||||
// uid/gid, and if we set either of those we would interfere with all of
|
||||
// the normal mgmt stuff running inside this process.
|
||||
|
||||
traits.Base // add the base methods without re-implementation
|
||||
|
||||
init *engine.Init
|
||||
|
||||
// Schema is the schema to use in. This can be schema:path if the schema
|
||||
// doesn't have a fixed path. See the `gsettings` manual for more info.
|
||||
Schema string `lang:"schema" yaml:"schema"`
|
||||
|
||||
// Key is the key to set.
|
||||
Key string `lang:"key" yaml:"key"`
|
||||
|
||||
// Type is the type value to set. This can be "bool", "str", "int", or
|
||||
// "custom".
|
||||
// XXX: add support for [][]str and so on...
|
||||
Type string `lang:"type" yaml:"type"`
|
||||
|
||||
// Value is the value to set. It is interface{} because it can hold any
|
||||
// value type.
|
||||
// XXX: Add resource unification to this key
|
||||
Value interface{} `lang:"value" yaml:"value"`
|
||||
|
||||
// User is the (optional) user to use to execute the command. It is used
|
||||
// for any command being run.
|
||||
User string `lang:"user" yaml:"user"`
|
||||
|
||||
// Group is the (optional) group to use to execute the command. It is
|
||||
// used for any command being run.
|
||||
Group string `lang:"group" yaml:"group"`
|
||||
|
||||
// XXX: We should have a "once" functionality if this param is set true.
|
||||
// XXX: Basically it would change that field once, and store a "tag"
|
||||
// file to say it was done.
|
||||
// XXX: Maybe that should be a metaparam called Once that works anywhere.
|
||||
// XXX: Maybe there should be a way to reset the "once" tag too...
|
||||
//Once string `lang:"once" yaml:"once"`
|
||||
|
||||
// We're using the exec resource to build the resources because it's all
|
||||
// done through exec.
|
||||
exec *ExecRes
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *GsettingsRes) Default() engine.Res {
|
||||
return &GsettingsRes{}
|
||||
}
|
||||
|
||||
// parse is a helper to pull out the correct schema and key to use.
|
||||
func (obj *GsettingsRes) parse() (string, string, error) {
|
||||
schema := obj.Schema
|
||||
key := obj.Key
|
||||
|
||||
sp := strings.Split(obj.Name(), " ")
|
||||
if len(sp) == 2 && obj.Schema == "" && obj.Key == "" {
|
||||
schema = sp[0]
|
||||
key = sp[1]
|
||||
}
|
||||
|
||||
if schema == "" {
|
||||
return "", "", fmt.Errorf("empty schema")
|
||||
}
|
||||
if key == "" {
|
||||
return "", "", fmt.Errorf("empty key")
|
||||
}
|
||||
|
||||
return schema, key, nil
|
||||
}
|
||||
|
||||
// value is a helper to pull out the value in the correct format to use.
|
||||
func (obj *GsettingsRes) value() (string, error) {
|
||||
if obj.Type == "bool" {
|
||||
v, ok := obj.Value.(bool)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid bool")
|
||||
}
|
||||
if v {
|
||||
return "true", nil
|
||||
}
|
||||
return "false", nil
|
||||
}
|
||||
|
||||
if obj.Type == "str" {
|
||||
v, ok := obj.Value.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid str")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
if obj.Type == "int" {
|
||||
v, ok := obj.Value.(int)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid int")
|
||||
}
|
||||
return strconv.Itoa(v), nil
|
||||
}
|
||||
|
||||
if obj.Type == "custom" {
|
||||
v, ok := obj.Value.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid custom")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// XXX: add proper type parsing
|
||||
|
||||
return "", fmt.Errorf("invalid type: %s", obj.Type)
|
||||
}
|
||||
|
||||
// uid is a helper to get the correct uid.
|
||||
func (obj *GsettingsRes) uid() (int, error) {
|
||||
uid := obj.User // something or empty
|
||||
if obj.User == "" {
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
uid = u.Uid
|
||||
}
|
||||
|
||||
out, err := engineUtil.GetUID(uid)
|
||||
if err != nil {
|
||||
return -1, errwrap.Wrapf(err, "error looking up uid for %s", uid)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// makeComposite creates a pointer to a ExecRes. The pointer is used to validate
|
||||
// and initialize the nested exec.
|
||||
func (obj *GsettingsRes) makeComposite() (*ExecRes, error) {
|
||||
cmd, err := exec.LookPath("gsettings")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
schema, key, err := obj.parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val, err := obj.value()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uid, err := obj.uid()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := engine.NewNamedResource("exec", fmt.Sprintf(gsettingsTmpl, obj.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exec := res.(*ExecRes)
|
||||
|
||||
exec.Cmd = cmd
|
||||
exec.Args = []string{
|
||||
"set",
|
||||
schema,
|
||||
key,
|
||||
val,
|
||||
}
|
||||
exec.Cwd = "/"
|
||||
|
||||
exec.IfCmd = fmt.Sprintf("%s get %s %s", cmd, schema, key)
|
||||
exec.IfCwd = "/"
|
||||
expected := val + "\n" // value comes with a trailing newline
|
||||
exec.IfEquals = &expected
|
||||
|
||||
exec.WatchCmd = fmt.Sprintf("%s monitor %s %s", cmd, schema, key)
|
||||
exec.WatchCwd = "/"
|
||||
|
||||
exec.User = obj.User
|
||||
exec.Group = obj.Group
|
||||
|
||||
exec.Env = map[string]string{
|
||||
// Either of these will work, so we'll include both for fun.
|
||||
"DBUS_SESSION_BUS_ADDRESS": fmt.Sprintf("unix:path=/run/user/%d/bus", uid),
|
||||
"XDG_RUNTIME_DIR": fmt.Sprintf("/run/user/%d/", uid),
|
||||
}
|
||||
//exec.Timeout = ? // TODO: should we have a timeout to prevent blocking?
|
||||
|
||||
return exec, nil
|
||||
}
|
||||
|
||||
// Validate reports any problems with the struct definition.
|
||||
func (obj *GsettingsRes) Validate() error {
|
||||
if _, _, err := obj.parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
// validation of obj.Type happens in this function.
|
||||
if _, err := obj.value(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exec, err := obj.makeComposite()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "makeComposite failed in validate")
|
||||
}
|
||||
if err := exec.Validate(); err != nil { // composite resource
|
||||
return errwrap.Wrapf(err, "validate failed for embedded exec: %s", exec)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *GsettingsRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
exec, err := obj.makeComposite()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "makeComposite failed in init")
|
||||
}
|
||||
obj.exec = exec
|
||||
|
||||
newInit := obj.init.Copy()
|
||||
newInit.Send = func(interface{}) error { // override so exec can't send
|
||||
return nil
|
||||
}
|
||||
newInit.Logf = func(format string, v ...interface{}) {
|
||||
//if format == "cmd out empty!" {
|
||||
// return
|
||||
//}
|
||||
//obj.init.Logf("exec: "+format, v...)
|
||||
}
|
||||
|
||||
return obj.exec.Init(newInit)
|
||||
}
|
||||
|
||||
// Cleanup is run by the engine to clean up after the resource is done.
|
||||
func (obj *GsettingsRes) Cleanup() error {
|
||||
if obj.exec != nil {
|
||||
return obj.exec.Cleanup()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
func (obj *GsettingsRes) Watch(ctx context.Context) error {
|
||||
return obj.exec.Watch(ctx)
|
||||
}
|
||||
|
||||
// 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 *GsettingsRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
||||
obj.init.Logf("%s", obj.exec.IfCmd) // "gsettings get"
|
||||
|
||||
checkOK, err := obj.exec.CheckApply(ctx, apply)
|
||||
if err != nil {
|
||||
return checkOK, err
|
||||
}
|
||||
|
||||
if !checkOK {
|
||||
// "gsettings set"
|
||||
obj.init.Logf("%s %s", obj.exec.Cmd, strings.Join(obj.exec.Args, " "))
|
||||
}
|
||||
|
||||
return checkOK, nil
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *GsettingsRes) Cmp(r engine.Res) error {
|
||||
// we can only compare GsettingsRes to others of the same resource kind
|
||||
res, ok := r.(*GsettingsRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.Schema != res.Schema {
|
||||
return fmt.Errorf("the Schema differs")
|
||||
}
|
||||
if obj.Key != res.Key {
|
||||
return fmt.Errorf("the Key differs")
|
||||
}
|
||||
if obj.Type != res.Type {
|
||||
return fmt.Errorf("the Type differs")
|
||||
}
|
||||
|
||||
//if obj.Value != res.Value {
|
||||
// return fmt.Errorf("the Value differs")
|
||||
//}
|
||||
if !reflect.DeepEqual(obj.Value, res.Value) {
|
||||
return fmt.Errorf("the Value field differs")
|
||||
}
|
||||
|
||||
if obj.User != res.User {
|
||||
return fmt.Errorf("the User differs")
|
||||
}
|
||||
if obj.Group != res.Group {
|
||||
return fmt.Errorf("the Group differs")
|
||||
}
|
||||
|
||||
// TODO: why is res.exec ever nil?
|
||||
if (obj.exec == nil) != (res.exec == nil) { // xor
|
||||
return fmt.Errorf("the exec differs")
|
||||
}
|
||||
if obj.exec != nil && res.exec != nil {
|
||||
if err := obj.exec.Cmp(res.exec); err != nil {
|
||||
return errwrap.Wrapf(err, "the exec differs")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *GsettingsRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes GsettingsRes // indirection to avoid infinite recursion
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*GsettingsRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to GsettingsRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = GsettingsRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
11
examples/lang/gsettings.mcl
Normal file
11
examples/lang/gsettings.mcl
Normal file
@@ -0,0 +1,11 @@
|
||||
gsettings "org.gnome.desktop.interface clock-show-seconds" {
|
||||
type => "bool",
|
||||
value => true,
|
||||
}
|
||||
|
||||
gsettings "org.gnome.desktop.input-sources sources" { # happens to match schema/key args
|
||||
schema => "org.gnome.desktop.input-sources",
|
||||
key => "sources",
|
||||
type => "custom",
|
||||
value => "[('xkb', 'ca+eng'), ('xkb', 'ca'), ('xkb', 'us')]",
|
||||
}
|
||||
Reference in New Issue
Block a user