From 8c2c55216417e7f83c001ca9499943db972f2df1 Mon Sep 17 00:00:00 2001 From: Mildred Ki'Lya Date: Mon, 13 Feb 2017 23:55:17 +0100 Subject: [PATCH] resources: file: Implement file attributes Add owner which must be username or uid of the file owner, group which is the group name or gid of the file, and mode which is the octal unix file permissions. Add separate implementation for Go 1.6 and lower. --- docs/documentation.md | 9 ++ resources/file.go | 162 +++++++++++++++++++++++++++++++--- resources/file_attrs.go | 43 +++++++++ resources/file_attrs_go1.6.go | 43 +++++++++ test/shell/file-mode.sh | 19 ++++ test/shell/file-mode.yaml | 21 +++++ test/shell/file-owner.sh | 24 +++++ test/shell/file-owner.yaml | 18 ++++ 8 files changed, 327 insertions(+), 12 deletions(-) create mode 100644 resources/file_attrs.go create mode 100644 resources/file_attrs_go1.6.go create mode 100755 test/shell/file-mode.sh create mode 100644 test/shell/file-mode.yaml create mode 100755 test/shell/file-owner.sh create mode 100644 test/shell/file-owner.yaml 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