The indegree code added a regression because elements with an indegree would not be unpaused! This is now corrected. Time to add more tests :)
412 lines
10 KiB
Go
412 lines
10 KiB
Go
// 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/>.
|
|
|
|
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"gopkg.in/fsnotify.v1"
|
|
//"github.com/go-fsnotify/fsnotify" // git master of "gopkg.in/fsnotify.v1"
|
|
"io"
|
|
"log"
|
|
"math"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
type FileType struct {
|
|
BaseType `yaml:",inline"`
|
|
Path string `yaml:"path"` // path variable (should default to name)
|
|
Dirname string `yaml:"dirname"`
|
|
Basename string `yaml:"basename"`
|
|
Content string `yaml:"content"`
|
|
State string `yaml:"state"` // state: exists/present?, absent, (undefined?)
|
|
sha256sum string
|
|
}
|
|
|
|
func NewFileType(name, path, dirname, basename, content, state string) *FileType {
|
|
// FIXME if path = nil, path = name ...
|
|
return &FileType{
|
|
BaseType: BaseType{
|
|
Name: name,
|
|
events: make(chan Event),
|
|
vertex: nil,
|
|
},
|
|
Path: path,
|
|
Dirname: dirname,
|
|
Basename: basename,
|
|
Content: content,
|
|
State: state,
|
|
sha256sum: "",
|
|
}
|
|
}
|
|
|
|
func (obj *FileType) GetType() string {
|
|
return "File"
|
|
}
|
|
|
|
// validate if the params passed in are valid data
|
|
func (obj *FileType) Validate() bool {
|
|
if obj.Dirname != "" {
|
|
// must end with /
|
|
if obj.Dirname[len(obj.Dirname)-1:] != "/" {
|
|
return false
|
|
}
|
|
}
|
|
if obj.Basename != "" {
|
|
// must not start with /
|
|
if obj.Basename[0:1] == "/" {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (obj *FileType) GetPath() string {
|
|
d := Dirname(obj.Path)
|
|
b := Basename(obj.Path)
|
|
if !obj.Validate() || (obj.Dirname == "" && obj.Basename == "") {
|
|
return obj.Path
|
|
} else if obj.Dirname == "" {
|
|
return d + obj.Basename
|
|
} else if obj.Basename == "" {
|
|
return obj.Dirname + b
|
|
} else { // if obj.dirname != "" && obj.basename != "" {
|
|
return obj.Dirname + obj.Basename
|
|
}
|
|
}
|
|
|
|
// File watcher for files and directories
|
|
// Modify with caution, probably important to write some test cases first!
|
|
// obj.GetPath(): file or directory
|
|
func (obj *FileType) Watch() {
|
|
if obj.IsWatching() {
|
|
return
|
|
}
|
|
obj.SetWatching(true)
|
|
defer obj.SetWatching(false)
|
|
|
|
//var recursive bool = false
|
|
//var isdir = (obj.GetPath()[len(obj.GetPath())-1:] == "/") // dirs have trailing slashes
|
|
//log.Printf("IsDirectory: %v", isdir)
|
|
//vertex := obj.GetVertex() // stored with SetVertex
|
|
var safename = path.Clean(obj.GetPath()) // no trailing slash
|
|
|
|
watcher, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer watcher.Close()
|
|
|
|
patharray := PathSplit(safename) // tokenize the path
|
|
var index = len(patharray) // starting index
|
|
var current string // current "watcher" location
|
|
var delta_depth int // depth delta between watcher and event
|
|
var send = false // send event?
|
|
var exit = false
|
|
var dirty = false
|
|
|
|
for {
|
|
current = strings.Join(patharray[0:index], "/")
|
|
if current == "" { // the empty string top is the root dir ("/")
|
|
current = "/"
|
|
}
|
|
if DEBUG {
|
|
log.Printf("File[%v]: Watching: %v", obj.GetName(), current) // attempting to watch...
|
|
}
|
|
// initialize in the loop so that we can reset on rm-ed handles
|
|
err = watcher.Add(current)
|
|
if err != nil {
|
|
if DEBUG {
|
|
log.Printf("File[%v]: watcher.Add(%v): Error: %v", obj.GetName(), current, err)
|
|
}
|
|
if err == syscall.ENOENT {
|
|
index-- // usually not found, move up one dir
|
|
} else if err == syscall.ENOSPC {
|
|
// XXX: occasionally: no space left on device,
|
|
// XXX: probably due to lack of inotify watches
|
|
log.Printf("Lack of watches for file[%v] error: %+v", obj.Name, err.Error) // 0x408da0
|
|
log.Fatal(err)
|
|
} else {
|
|
log.Printf("Unknown file[%v] error:", obj.Name)
|
|
log.Fatal(err)
|
|
}
|
|
index = int(math.Max(1, float64(index)))
|
|
continue
|
|
}
|
|
|
|
obj.SetState(typeWatching) // reset
|
|
select {
|
|
case event := <-watcher.Events:
|
|
if DEBUG {
|
|
log.Printf("File[%v]: Watch(%v), Event(%v): %v", obj.GetName(), current, event.Name, event.Op)
|
|
}
|
|
obj.SetConvergedState(typeConvergedNil) // XXX: technically i can detect if the event is erroneous or not first
|
|
// the deeper you go, the bigger the delta_depth is...
|
|
// this is the difference between what we're watching,
|
|
// and the event... doesn't mean we can't watch deeper
|
|
if current == event.Name {
|
|
delta_depth = 0 // i was watching what i was looking for
|
|
|
|
} else if HasPathPrefix(event.Name, current) {
|
|
delta_depth = len(PathSplit(current)) - len(PathSplit(event.Name)) // -1 or less
|
|
|
|
} else if HasPathPrefix(current, event.Name) {
|
|
delta_depth = len(PathSplit(event.Name)) - len(PathSplit(current)) // +1 or more
|
|
|
|
} else {
|
|
// TODO different watchers get each others events!
|
|
// https://github.com/go-fsnotify/fsnotify/issues/95
|
|
// this happened with two values such as:
|
|
// event.Name: /tmp/mgmt/f3 and current: /tmp/mgmt/f2
|
|
continue
|
|
}
|
|
//log.Printf("The delta depth is: %v", delta_depth)
|
|
|
|
// if we have what we wanted, awesome, send an event...
|
|
if event.Name == safename {
|
|
//log.Println("Event!")
|
|
// FIXME: should all these below cases trigger?
|
|
send = true
|
|
dirty = true
|
|
|
|
// file removed, move the watch upwards
|
|
if delta_depth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) {
|
|
//log.Println("Removal!")
|
|
watcher.Remove(current)
|
|
index--
|
|
}
|
|
|
|
// we must be a parent watcher, so descend in
|
|
if delta_depth < 0 {
|
|
watcher.Remove(current)
|
|
index++
|
|
}
|
|
|
|
// if safename starts with event.Name, we're above, and no event should be sent
|
|
} else if HasPathPrefix(safename, event.Name) {
|
|
//log.Println("Above!")
|
|
|
|
if delta_depth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) {
|
|
log.Println("Removal!")
|
|
watcher.Remove(current)
|
|
index--
|
|
}
|
|
|
|
if delta_depth < 0 {
|
|
log.Println("Parent!")
|
|
if PathPrefixDelta(safename, event.Name) == 1 { // we're the parent dir
|
|
send = true
|
|
dirty = true
|
|
}
|
|
watcher.Remove(current)
|
|
index++
|
|
}
|
|
|
|
// if event.Name startswith safename, send event, we're already deeper
|
|
} else if HasPathPrefix(event.Name, safename) {
|
|
//log.Println("Event2!")
|
|
send = true
|
|
dirty = true
|
|
}
|
|
|
|
case err := <-watcher.Errors:
|
|
obj.SetConvergedState(typeConvergedNil) // XXX ?
|
|
log.Println("error:", err)
|
|
log.Fatal(err)
|
|
//obj.events <- fmt.Sprintf("file: %v", "error") // XXX: how should we handle errors?
|
|
|
|
case event := <-obj.events:
|
|
obj.SetConvergedState(typeConvergedNil)
|
|
if exit, send = obj.ReadEvent(&event); exit {
|
|
return // exit
|
|
}
|
|
//dirty = false // these events don't invalidate state
|
|
|
|
case _ = <-TimeAfterOrBlock(obj.ctimeout):
|
|
obj.SetConvergedState(typeConvergedTimeout)
|
|
obj.converged <- true
|
|
continue
|
|
}
|
|
|
|
// do all our event sending all together to avoid duplicate msgs
|
|
if send {
|
|
send = false
|
|
// only invalid state on certain types of events
|
|
if dirty {
|
|
dirty = false
|
|
obj.isStateOK = false // something made state dirty
|
|
}
|
|
Process(obj) // XXX: rename this function
|
|
}
|
|
}
|
|
}
|
|
|
|
func (obj *FileType) HashSHA256fromContent() string {
|
|
if obj.sha256sum != "" { // return if already computed
|
|
return obj.sha256sum
|
|
}
|
|
|
|
hash := sha256.New()
|
|
hash.Write([]byte(obj.Content))
|
|
obj.sha256sum = hex.EncodeToString(hash.Sum(nil))
|
|
return obj.sha256sum
|
|
}
|
|
|
|
// FIXME: add the obj.CleanState() calls all over the true returns!
|
|
func (obj *FileType) StateOK() bool {
|
|
if obj.isStateOK { // cache the state
|
|
return true
|
|
}
|
|
|
|
if _, err := os.Stat(obj.GetPath()); os.IsNotExist(err) {
|
|
// no such file or directory
|
|
if obj.State == "absent" {
|
|
return obj.CleanState() // missing file should be missing, phew :)
|
|
} else {
|
|
// state invalid, skip expensive checksums
|
|
return false
|
|
}
|
|
}
|
|
|
|
// TODO: add file mode check here...
|
|
|
|
if PathIsDir(obj.GetPath()) {
|
|
return obj.StateOKDir()
|
|
} else {
|
|
return obj.StateOKFile()
|
|
}
|
|
}
|
|
|
|
func (obj *FileType) StateOKFile() bool {
|
|
if PathIsDir(obj.GetPath()) {
|
|
log.Fatal("This should only be called on a File type.")
|
|
}
|
|
|
|
// run a diff, and return true if needs changing
|
|
|
|
hash := sha256.New()
|
|
|
|
f, err := os.Open(obj.GetPath())
|
|
if err != nil {
|
|
//log.Fatal(err)
|
|
return false
|
|
}
|
|
defer f.Close()
|
|
|
|
if _, err := io.Copy(hash, f); err != nil {
|
|
//log.Fatal(err)
|
|
return false
|
|
}
|
|
|
|
sha256sum := hex.EncodeToString(hash.Sum(nil))
|
|
//log.Printf("sha256sum: %v", sha256sum)
|
|
|
|
if obj.HashSHA256fromContent() == sha256sum {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (obj *FileType) StateOKDir() bool {
|
|
if !PathIsDir(obj.GetPath()) {
|
|
log.Fatal("This should only be called on a Dir type.")
|
|
}
|
|
|
|
// XXX: not implemented
|
|
log.Fatal("Not implemented!")
|
|
return false
|
|
}
|
|
|
|
func (obj *FileType) Apply() bool {
|
|
log.Printf("%v[%v]: Apply", obj.GetType(), obj.GetName())
|
|
|
|
if PathIsDir(obj.GetPath()) {
|
|
return obj.ApplyDir()
|
|
} else {
|
|
return obj.ApplyFile()
|
|
}
|
|
}
|
|
|
|
func (obj *FileType) ApplyFile() bool {
|
|
|
|
if PathIsDir(obj.GetPath()) {
|
|
log.Fatal("This should only be called on a File type.")
|
|
}
|
|
|
|
if obj.State == "absent" {
|
|
log.Printf("About to remove: %v", obj.GetPath())
|
|
err := os.Remove(obj.GetPath())
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
//log.Println("writing: " + filename)
|
|
f, err := os.Create(obj.GetPath())
|
|
if err != nil {
|
|
log.Println("error:", err)
|
|
return false
|
|
}
|
|
defer f.Close()
|
|
|
|
_, err = io.WriteString(f, obj.Content)
|
|
if err != nil {
|
|
log.Println("error:", err)
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (obj *FileType) ApplyDir() bool {
|
|
if !PathIsDir(obj.GetPath()) {
|
|
log.Fatal("This should only be called on a Dir type.")
|
|
}
|
|
|
|
// XXX: not implemented
|
|
log.Fatal("Not implemented!")
|
|
return true
|
|
}
|
|
|
|
func (obj *FileType) Compare(typ Type) bool {
|
|
switch typ.(type) {
|
|
case *FileType:
|
|
typ := typ.(*FileType)
|
|
if obj.Name != typ.Name {
|
|
return false
|
|
}
|
|
if obj.GetPath() != typ.Path {
|
|
return false
|
|
}
|
|
if obj.Content != typ.Content {
|
|
return false
|
|
}
|
|
if obj.State != typ.State {
|
|
return false
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
return true
|
|
}
|