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.
This commit is contained in:
Mildred Ki'Lya
2017-02-13 23:55:17 +01:00
committed by James Shubin
parent b9976cf693
commit 8c2c552164
8 changed files with 327 additions and 12 deletions

View File

@@ -247,6 +247,15 @@ The exec resource can execute commands on your system.
The file resource manages files and directories. In `mgmt`, directories are 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. 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 #### Path
The path property specifies the file or directory that we are managing. The path property specifies the file or directory that we are managing.

View File

@@ -27,9 +27,12 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
"os/user"
"path" "path"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"syscall"
"github.com/purpleidea/mgmt/event" "github.com/purpleidea/mgmt/event"
"github.com/purpleidea/mgmt/recwatch" "github.com/purpleidea/mgmt/recwatch"
@@ -51,6 +54,9 @@ type FileRes struct {
Content *string `yaml:"content"` // nil to mark as undefined Content *string `yaml:"content"` // nil to mark as undefined
Source string `yaml:"source"` // file path for source content Source string `yaml:"source"` // file path for source content
State string `yaml:"state"` // state: exists/present?, absent, (undefined?) 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"` Recurse bool `yaml:"recurse"`
Force bool `yaml:"force"` Force bool `yaml:"force"`
path string // computed path path string // computed path
@@ -102,6 +108,20 @@ func (obj *FileRes) Validate() error {
return fmt.Errorf("Can't specify Content when creating a Dir.") 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? // XXX: should this specify that we create an empty directory instead?
//if obj.Source == "" && obj.isDir { //if obj.Source == "" && obj.isDir {
// return fmt.Errorf("Can't specify an empty source when creating a Dir.") // 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() 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. // Init runs some startup code for this resource.
func (obj *FileRes) Init() error { func (obj *FileRes) Init() error {
obj.sha256sum = "" obj.sha256sum = ""
@@ -614,6 +661,99 @@ func (obj *FileRes) contentCheckApply(apply bool) (checkOK bool, _ error) {
return checkOK, nil 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 // 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. // input is true. It returns error info and if the state check passed or not.
func (obj *FileRes) CheckApply(apply bool) (checkOK bool, _ error) { func (obj *FileRes) CheckApply(apply bool) (checkOK bool, _ error) {
@@ -638,19 +778,17 @@ func (obj *FileRes) CheckApply(apply bool) (checkOK bool, _ error) {
checkOK = false checkOK = false
} }
// TODO if c, err := obj.chmodCheckApply(apply); err != nil {
//if c, err := obj.chmodCheckApply(apply); err != nil { return false, err
// return false, err } else if !c {
//} else if !c { checkOK = false
// checkOK = false }
//}
// TODO if c, err := obj.chownCheckApply(apply); err != nil {
//if c, err := obj.chownCheckApply(apply); err != nil { return false, err
// return false, err } else if !c {
//} else if !c { checkOK = false
// checkOK = false }
//}
return checkOK, nil // w00t return checkOK, nil // w00t
} }

43
resources/file_attrs.go Normal file
View File

@@ -0,0 +1,43 @@
// Mgmt
// Copyright (C) 2013-2016+ 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 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 <http://www.gnu.org/licenses/>.
// +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)
}

View File

@@ -0,0 +1,43 @@
// Mgmt
// Copyright (C) 2013-2016+ 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 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 <http://www.gnu.org/licenses/>.
// +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)
}

19
test/shell/file-mode.sh Executable file
View File

@@ -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

21
test/shell/file-mode.yaml Normal file
View File

@@ -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'

24
test/shell/file-owner.sh Executable file
View File

@@ -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

View File

@@ -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