diff --git a/docs/documentation.md b/docs/documentation.md index e2e33a56..86761dbc 100644 --- a/docs/documentation.md +++ b/docs/documentation.md @@ -247,6 +247,15 @@ The exec resource can execute commands on your system. The file resource manages files and directories. In `mgmt`, directories are identified by a trailing slash in their path name. File have no such slash. +It has the following properties: + +- `path`: file path (directories have a trailing slash here) +- `content`: raw file content +- `state`: either `exists` (the default value) or `absent` +- `mode`: octal unix file permissions +- `owner`: username or uid for the file owner +- `group`: group name or gid for the file group + #### Path The path property specifies the file or directory that we are managing. diff --git a/resources/file.go b/resources/file.go index 8e2b88a7..c8f586d4 100644 --- a/resources/file.go +++ b/resources/file.go @@ -27,9 +27,12 @@ import ( "io/ioutil" "log" "os" + "os/user" "path" "path/filepath" + "strconv" "strings" + "syscall" "github.com/purpleidea/mgmt/event" "github.com/purpleidea/mgmt/recwatch" @@ -51,6 +54,9 @@ type FileRes struct { Content *string `yaml:"content"` // nil to mark as undefined Source string `yaml:"source"` // file path for source content State string `yaml:"state"` // state: exists/present?, absent, (undefined?) + Owner string `yaml:"owner"` + Group string `yaml:"group"` + Mode string `yaml:"mode"` Recurse bool `yaml:"recurse"` Force bool `yaml:"force"` path string // computed path @@ -102,6 +108,20 @@ func (obj *FileRes) Validate() error { return fmt.Errorf("Can't specify Content when creating a Dir.") } + if obj.Mode != "" { + if _, err := obj.mode(); err != nil { + return err + } + } + + if _, err := obj.uid(); obj.Owner != "" && err != nil { + return err + } + + if _, err := obj.gid(); obj.Group != "" && err != nil { + return err + } + // XXX: should this specify that we create an empty directory instead? //if obj.Source == "" && obj.isDir { // return fmt.Errorf("Can't specify an empty source when creating a Dir.") @@ -110,6 +130,33 @@ func (obj *FileRes) Validate() error { return obj.BaseRes.Validate() } +// mode returns the file permission specified on the graph. It doesn't handle +// the case where the mode is not specified. The caller should check obj.Mode is +// not empty. +func (obj *FileRes) mode() (os.FileMode, error) { + m, err := strconv.ParseInt(obj.Mode, 8, 32) + if err != nil { + return os.FileMode(0), errwrap.Wrapf(err, "Mode should be an octal number (%s)", obj.Mode) + } + return os.FileMode(m), nil +} + +// uid returns the user id for the owner specified in the yaml file graph. +// Caller should first check obj.Owner is not empty +func (obj *FileRes) uid() (int, error) { + u2, err2 := user.LookupId(obj.Owner) + if err2 == nil { + return strconv.Atoi(u2.Uid) + } + + u, err := user.Lookup(obj.Owner) + if err == nil { + return strconv.Atoi(u.Uid) + } + + return -1, errwrap.Wrapf(err, "Owner lookup error (%s)", obj.Owner) +} + // Init runs some startup code for this resource. func (obj *FileRes) Init() error { obj.sha256sum = "" @@ -614,6 +661,99 @@ func (obj *FileRes) contentCheckApply(apply bool) (checkOK bool, _ error) { return checkOK, nil } +// chmodCheckApply performs a CheckApply for the file permissions. +func (obj *FileRes) chmodCheckApply(apply bool) (checkOK bool, _ error) { + log.Printf("%s[%s]: chmodCheckApply(%t)", obj.Kind(), obj.GetName(), apply) + + if obj.State == "absent" { + // File is absent + return true, nil + } + + if obj.Mode == "" { + // No mode specified, everything is ok + return true, nil + } + + mode, err := obj.mode() + if err != nil { + return false, err + } + + st, err := os.Stat(obj.Path) + if err != nil { + return false, err + } + + // Nothing to do + if st.Mode() == mode { + return true, nil + } + + // Not clean but don't apply + if !apply { + return false, nil + } + + err = os.Chmod(obj.Path, mode) + return false, err +} + +// chownCheckApply performs a CheckApply for the file ownership. +func (obj *FileRes) chownCheckApply(apply bool) (checkOK bool, _ error) { + var expectedUID, expectedGID int + log.Printf("%s[%s]: chownCheckApply(%t)", obj.Kind(), obj.GetName(), apply) + + if obj.State == "absent" { + // File is absent or no owner specified + return true, nil + } + + st, err := os.Stat(obj.Path) + if err != nil { + return false, err + } + + stUnix, ok := st.Sys().(*syscall.Stat_t) + if !ok { + // Not unix + panic("No support for your platform") + } + + if obj.Owner != "" { + expectedUID, err = obj.uid() + if err != nil { + return false, err + } + } else { + // Nothing specified, no changes to be made, expect same as actual + expectedUID = int(stUnix.Uid) + } + + if obj.Group != "" { + expectedGID, err = obj.gid() + if err != nil { + return false, err + } + } else { + // Nothing specified, no changes to be made, expect same as actual + expectedGID = int(stUnix.Gid) + } + + // Nothing to do + if int(stUnix.Uid) == expectedUID && int(stUnix.Gid) == expectedGID { + return true, nil + } + + // Not clean, but don't apply + if !apply { + return false, nil + } + + err = os.Chown(obj.Path, expectedUID, expectedGID) + return false, err +} + // 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 *FileRes) CheckApply(apply bool) (checkOK bool, _ error) { @@ -638,19 +778,17 @@ func (obj *FileRes) CheckApply(apply bool) (checkOK bool, _ error) { checkOK = false } - // TODO - //if c, err := obj.chmodCheckApply(apply); err != nil { - // return false, err - //} else if !c { - // checkOK = false - //} + if c, err := obj.chmodCheckApply(apply); err != nil { + return false, err + } else if !c { + checkOK = false + } - // TODO - //if c, err := obj.chownCheckApply(apply); err != nil { - // return false, err - //} else if !c { - // checkOK = false - //} + if c, err := obj.chownCheckApply(apply); err != nil { + return false, err + } else if !c { + checkOK = false + } return checkOK, nil // w00t } diff --git a/resources/file_attrs.go b/resources/file_attrs.go new file mode 100644 index 00000000..45a7202c --- /dev/null +++ b/resources/file_attrs.go @@ -0,0 +1,43 @@ +// Mgmt +// Copyright (C) 2013-2016+ 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 Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// +build go1.7 + +package resources + +import ( + "os/user" + "strconv" + + errwrap "github.com/pkg/errors" +) + +// gid returns the group id for the group specified in the yaml file graph. +// Caller should first check obj.Group is not empty +func (obj *FileRes) gid() (int, error) { + g2, err2 := user.LookupGroupId(obj.Group) + if err2 == nil { + return strconv.Atoi(g2.Gid) + } + + g, err := user.LookupGroup(obj.Group) + if err == nil { + return strconv.Atoi(g.Gid) + } + + return -1, errwrap.Wrapf(err, "Group lookup error (%s)", obj.Group) +} diff --git a/resources/file_attrs_go1.6.go b/resources/file_attrs_go1.6.go new file mode 100644 index 00000000..b0983f28 --- /dev/null +++ b/resources/file_attrs_go1.6.go @@ -0,0 +1,43 @@ +// Mgmt +// Copyright (C) 2013-2016+ 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 Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// +build !go1.7 + +package resources + +import ( + "strconv" + + group "github.com/hnakamur/group" + errwrap "github.com/pkg/errors" +) + +// gid returns the group id for the group specified in the yaml file graph. +// Caller should first check obj.Group is not empty +func (obj *FileRes) gid() (int, error) { + g2, err2 := group.LookupId(obj.Group) + if err2 == nil { + return strconv.Atoi(g2.Gid) + } + + g, err := group.Lookup(obj.Group) + if err == nil { + return strconv.Atoi(g.Gid) + } + + return -1, errwrap.Wrapf(err, "Group lookup error (%s)", obj.Group) +} diff --git a/test/shell/file-mode.sh b/test/shell/file-mode.sh new file mode 100755 index 00000000..07ddc7dc --- /dev/null +++ b/test/shell/file-mode.sh @@ -0,0 +1,19 @@ +#!/bin/bash -e + +set -x + +# run till completion +timeout --kill-after=20s 15s ./mgmt run --yaml file-mode.yaml --converged-timeout=5 --no-watch --tmp-prefix & +pid=$! +wait $pid # get exit status +e=$? + +ls -l /tmp/mgmt + +test -e /tmp/mgmt/f1 +test -e /tmp/mgmt/f2 +test -e /tmp/mgmt/f3 +test $(stat -c%a /tmp/mgmt/f2) = 741 +test $(stat -c%a /tmp/mgmt/f3) = 614 + +exit $e diff --git a/test/shell/file-mode.yaml b/test/shell/file-mode.yaml new file mode 100644 index 00000000..d1fe12b3 --- /dev/null +++ b/test/shell/file-mode.yaml @@ -0,0 +1,21 @@ +--- +graph: mygraph +resources: + file: + - name: file1 + path: "/tmp/mgmt/f1" + content: | + i am f1 + state: exists + - name: file2 + path: "/tmp/mgmt/f2" + content: | + i am f2 + state: exists + mode: 0741 + - name: file3 + path: "/tmp/mgmt/f3" + content: | + i am f3 + state: exists + mode: '0614' diff --git a/test/shell/file-owner.sh b/test/shell/file-owner.sh new file mode 100755 index 00000000..320004ff --- /dev/null +++ b/test/shell/file-owner.sh @@ -0,0 +1,24 @@ +#!/bin/bash -e +# vim: noet:ts=8:sts=8:sw=8 + +set -x + +if ! timeout 1s sudo -A true; then + echo "sudo disabled: not checking file owner and group" + exit +fi + +# run till completion +timeout --kill-after=15s 10s sudo -A ./mgmt run --yaml file-owner.yaml --converged-timeout=5 --no-watch --tmp-prefix & +pid=$! +wait $pid # get exit status +e=$? + +ls -l /tmp/mgmt + +test -e /tmp/mgmt/f1 +test -e /tmp/mgmt/f2 +test $(stat -c%U:%G /tmp/mgmt/f1) = root:root +test $(stat -c%u:%g /tmp/mgmt/f2) = 1:2 + +exit $e diff --git a/test/shell/file-owner.yaml b/test/shell/file-owner.yaml new file mode 100644 index 00000000..5b53a08a --- /dev/null +++ b/test/shell/file-owner.yaml @@ -0,0 +1,18 @@ +--- +graph: mygraph +resources: + file: + - name: file1 + path: "/tmp/mgmt/f1" + content: | + i am f1 + state: exists + owner: root + group: root + - name: file2 + path: "/tmp/mgmt/f2" + content: | + i am f2 + state: exists + owner: 1 + group: 2