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:
committed by
James Shubin
parent
b9976cf693
commit
8c2c552164
@@ -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.
|
||||||
|
|||||||
@@ -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
43
resources/file_attrs.go
Normal 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)
|
||||||
|
}
|
||||||
43
resources/file_attrs_go1.6.go
Normal file
43
resources/file_attrs_go1.6.go
Normal 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
19
test/shell/file-mode.sh
Executable 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
21
test/shell/file-mode.yaml
Normal 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
24
test/shell/file-owner.sh
Executable 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
|
||||||
18
test/shell/file-owner.yaml
Normal file
18
test/shell/file-owner.yaml
Normal 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
|
||||||
Reference in New Issue
Block a user