From 831c7e2c326e4fcbdec33f41cf7e0b2be433cdc5 Mon Sep 17 00:00:00 2001 From: Jonathan Gold Date: Sat, 9 Sep 2017 19:54:25 -0400 Subject: [PATCH] resources: Add user resource --- examples/user1.yaml | 9 ++ resources/user.go | 310 +++++++++++++++++++++++++++++++++++++++++++ yamlgraph/gconfig.go | 1 + 3 files changed, 320 insertions(+) create mode 100644 examples/user1.yaml create mode 100644 resources/user.go diff --git a/examples/user1.yaml b/examples/user1.yaml new file mode 100644 index 00000000..5246e5af --- /dev/null +++ b/examples/user1.yaml @@ -0,0 +1,9 @@ +--- +graph: mygraph +resources: + user: + - name: testuser + uid: 1002 + gid: 100 + state: exists +edges: [] diff --git a/resources/user.go b/resources/user.go new file mode 100644 index 00000000..d45ccb19 --- /dev/null +++ b/resources/user.go @@ -0,0 +1,310 @@ +// Mgmt +// Copyright (C) 2013-2017+ 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 . + +package resources + +import ( + "fmt" + "log" + "os/exec" + "os/user" + "strconv" + "syscall" + + "github.com/purpleidea/mgmt/recwatch" + + errwrap "github.com/pkg/errors" +) + +func init() { + RegisterResource("user", func() Res { return &UserRes{} }) +} + +const passwdFile = "/etc/passwd" + +// UserRes is a user account resource. +type UserRes struct { + BaseRes `yaml:",inline"` + State string `yaml:"state"` // state: exists, absent + UID *uint32 `yaml:"uid"` + GID *uint32 `yaml:"gid"` + HomeDir *string `yaml:"homedir"` + AllowDuplicateUID bool `yaml:"allowduplicateuid"` + + recWatcher *recwatch.RecWatcher +} + +// Default returns some sensible defaults for this resource. +func (obj *UserRes) Default() Res { + return &UserRes{ + BaseRes: BaseRes{ + MetaParams: DefaultMetaParams, // force a default + }, + } +} + +// Validate if the params passed in are valid data. +func (obj *UserRes) Validate() error { + if obj.State != "exists" && obj.State != "absent" { + return fmt.Errorf("State must be 'exists' or 'absent'") + } + return obj.BaseRes.Validate() +} + +// Init initializes the resource. +func (obj *UserRes) Init() error { + return obj.BaseRes.Init() // call base init, b/c we're overriding +} + +// Watch is the primary listener for this resource and it outputs events. +func (obj *UserRes) Watch() error { + var err error + obj.recWatcher, err = recwatch.NewRecWatcher(passwdFile, false) + if err != nil { + return err + } + defer obj.recWatcher.Close() + + // notify engine that we're running + if err := obj.Running(); err != nil { + return err // bubble up a NACK... + } + + var send = false // send event? + var exit *error + + for { + if obj.debug { + log.Printf("Watching: %s", passwdFile) // attempting to watch... + } + + select { + case event, ok := <-obj.recWatcher.Events(): + if !ok { // channel shutdown + return nil + } + if err := event.Error; err != nil { + return errwrap.Wrapf(err, "Unknown %s watcher error", obj) + } + if obj.debug { // don't access event.Body if event.Error isn't nil + log.Printf("%s: Event(%s): %v", obj, event.Body.Name, event.Body.Op) + } + send = true + obj.StateOK(false) // dirty + + case event := <-obj.Events(): + if exit, send = obj.ReadEvent(event); exit != nil { + return *exit // exit + } + //obj.StateOK(false) // dirty // these events don't invalidate state + } + + // do all our event sending all together to avoid duplicate msgs + if send { + send = false + obj.Event() + } + } +} + +// CheckApply method for User resource. +func (obj *UserRes) CheckApply(apply bool) (checkOK bool, err error) { + log.Printf("%s: CheckApply(%t)", obj, apply) + + var exists = true + usr, err := user.Lookup(obj.GetName()) + if err != nil { + if _, ok := err.(user.UnknownUserError); !ok { + return false, errwrap.Wrapf(err, "error looking up user") + } + log.Printf("the user: %s does not exist", obj.GetName()) + exists = false + } + + if obj.AllowDuplicateUID == false && obj.UID != nil { + existingUID, err := user.LookupId(strconv.Itoa(int(*obj.UID))) + if err != nil { + if _, ok := err.(user.UnknownUserIdError); !ok { + return false, errwrap.Wrapf(err, "error looking up UID") + } + } else if existingUID.Username != obj.GetName() { + return false, fmt.Errorf("the requested UID is already taken") + } + } + + if obj.State == "absent" && !exists { + return true, nil + } + + if usercheck := true; exists && obj.State == "exists" { + intUID, err := strconv.Atoi(usr.Uid) + if err != nil { + return false, errwrap.Wrapf(err, "error casting UID to int") + } + intGID, err := strconv.Atoi(usr.Gid) + if err != nil { + return false, errwrap.Wrapf(err, "error casting GID to int") + } + if obj.UID != nil && int(*obj.UID) != intUID { + usercheck = false + } + if obj.GID != nil && int(*obj.GID) != intGID { + usercheck = false + } + if obj.HomeDir != nil && *obj.HomeDir != usr.HomeDir { + usercheck = false + } + if usercheck { + return true, nil + } + } + + if !apply { + return false, nil + } + + var cmdName string + var args []string + if obj.State == "exists" { + if exists { + cmdName = "usermod" + log.Printf("modifying user: %s", obj.GetName()) + } else { + cmdName = "useradd" + log.Printf("adding user: %s", obj.GetName()) + } + if obj.AllowDuplicateUID { + args = append(args, "--non-unique") + } + if obj.UID != nil { + args = append(args, "-u", fmt.Sprintf("%d", *obj.UID)) + } + if obj.GID != nil { + args = append(args, "-g", fmt.Sprintf("%d", *obj.GID)) + } + if obj.HomeDir != nil { + args = append(args, "-d", *obj.HomeDir) + } + } + if obj.State == "absent" { + cmdName = "userdel" + } + + args = append(args, obj.GetName()) + + cmd := exec.Command(cmdName, args...) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + Pgid: 0, + } + if err := cmd.Run(); err != nil { + return false, errwrap.Wrapf(err, "cmd failed to run") + } + + return false, nil +} + +// UserUID is the UID struct for UserRes. +type UserUID struct { + BaseUID + name string +} + +// UIDs includes all params to make a unique identification of this object. +// Most resources only return one, although some resources can return multiple. +func (obj *UserRes) UIDs() []ResUID { + x := &UserUID{ + BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()}, + name: obj.Name, + } + return []ResUID{x} +} + +// GroupCmp returns whether two resources can be grouped together or not. +func (obj *UserRes) GroupCmp(r Res) bool { + _, ok := r.(*UserRes) + if !ok { + return false + } + return false +} + +// Compare two resources and return if they are equivalent. +func (obj *UserRes) Compare(r Res) bool { + // we can only compare UserRes to others of the same resource kind + res, ok := r.(*UserRes) + if !ok { + return false + } + if !obj.BaseRes.Compare(res) { // call base Compare + return false + } + if obj.Name != res.Name { + return false + } + if obj.State != res.State { + return false + } + if (obj.UID == nil) != (res.UID == nil) { + return false + } + if obj.UID != nil && res.UID != nil { + if *obj.UID != *res.UID { + return false + } + } + if (obj.GID == nil) != (res.GID == nil) { + return false + } + if obj.GID != nil && res.GID != nil { + if *obj.GID != *res.UID { + return false + } + } + if (obj.HomeDir == nil) != (res.HomeDir == nil) { + return false + } + if obj.HomeDir != nil && res.HomeDir != nil { + if *obj.HomeDir != *obj.HomeDir { + return false + } + } + if obj.AllowDuplicateUID != res.AllowDuplicateUID { + return false + } + return true +} + +// UnmarshalYAML is the custom unmarshal handler for this struct. +// It is primarily useful for setting the defaults. +func (obj *UserRes) UnmarshalYAML(unmarshal func(interface{}) error) error { + type rawRes UserRes // indirection to avoid infinite recursion + + def := obj.Default() // get the default + res, ok := def.(*UserRes) // put in the right format + if !ok { + return fmt.Errorf("could not convert to UserRes") + } + raw := rawRes(*res) // convert; the defaults go here + + if err := unmarshal(&raw); err != nil { + return err + } + + *obj = UserRes(raw) // restore from indirection with type conversion! + return nil +} diff --git a/yamlgraph/gconfig.go b/yamlgraph/gconfig.go index c5d92129..af7a236f 100644 --- a/yamlgraph/gconfig.go +++ b/yamlgraph/gconfig.go @@ -68,6 +68,7 @@ type Resources struct { Pkg []*resources.PkgRes `yaml:"pkg"` Svc []*resources.SvcRes `yaml:"svc"` Timer []*resources.TimerRes `yaml:"timer"` + User []*resources.UserRes `yaml:"user"` Virt []*resources.VirtRes `yaml:"virt"` }