From 5a531b7948a0b086b6457972a138bf078e73ce30 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Fri, 1 Feb 2019 05:20:22 -0500 Subject: [PATCH] lang: funcs: Add a new readfile function This adds a new function that reads files from the local host. --- examples/lang/readfile1.mcl | 6 + lang/funcs/core/core.go | 1 + lang/funcs/core/coreos/coreos.go | 23 +++ lang/funcs/core/coreos/readfile_func.go | 211 ++++++++++++++++++++++++ 4 files changed, 241 insertions(+) create mode 100644 examples/lang/readfile1.mcl create mode 100644 lang/funcs/core/coreos/coreos.go create mode 100644 lang/funcs/core/coreos/readfile_func.go diff --git a/examples/lang/readfile1.mcl b/examples/lang/readfile1.mcl new file mode 100644 index 00000000..fb410281 --- /dev/null +++ b/examples/lang/readfile1.mcl @@ -0,0 +1,6 @@ +import "os" + +# this copies the contents from /tmp/foo and puts them in /tmp/output +file "/tmp/output" { + content => os.readfile("/tmp/foo"), +} diff --git a/lang/funcs/core/core.go b/lang/funcs/core/core.go index 68d8a48f..7a1e2932 100644 --- a/lang/funcs/core/core.go +++ b/lang/funcs/core/core.go @@ -23,5 +23,6 @@ import ( _ "github.com/purpleidea/mgmt/lang/funcs/core/coreexample" _ "github.com/purpleidea/mgmt/lang/funcs/core/corefmt" _ "github.com/purpleidea/mgmt/lang/funcs/core/coremath" + _ "github.com/purpleidea/mgmt/lang/funcs/core/coreos" _ "github.com/purpleidea/mgmt/lang/funcs/core/coresys" ) diff --git a/lang/funcs/core/coreos/coreos.go b/lang/funcs/core/coreos/coreos.go new file mode 100644 index 00000000..8d6b712e --- /dev/null +++ b/lang/funcs/core/coreos/coreos.go @@ -0,0 +1,23 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package coreos + +const ( + // moduleName is the prefix given to all the functions in this module. + moduleName = "os" +) diff --git a/lang/funcs/core/coreos/readfile_func.go b/lang/funcs/core/coreos/readfile_func.go new file mode 100644 index 00000000..555064a1 --- /dev/null +++ b/lang/funcs/core/coreos/readfile_func.go @@ -0,0 +1,211 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package coreos + +import ( + "fmt" + "io/ioutil" + "sync" + + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + "github.com/purpleidea/mgmt/recwatch" + + errwrap "github.com/pkg/errors" +) + +func init() { + funcs.ModuleRegister(moduleName, "readfile", func() interfaces.Func { return &ReadFileFunc{} }) // must register the func and name +} + +// ReadFileFunc is a function that reads the full contents from a local file. If +// the file contents change or the file path changes, a new string will be sent. +type ReadFileFunc struct { + init *interfaces.Init + last types.Value // last value received to use for diff + + filename string // the active filename + recWatcher *recwatch.RecWatcher + events chan error // internal events + wg *sync.WaitGroup + + result string // last calculated output + + closeChan chan struct{} +} + +// Validate makes sure we've built our struct properly. It is usually unused for +// normal functions that users can use directly. +func (obj *ReadFileFunc) Validate() error { + return nil +} + +// Info returns some static info about itself. +func (obj *ReadFileFunc) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: false, // maybe false because the file contents can change + Memo: false, + Sig: types.NewType("func(filename str) str"), + } +} + +// Init runs some startup code for this function. +func (obj *ReadFileFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.events = make(chan error) + obj.wg = &sync.WaitGroup{} + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the changing values that this func has over time. +func (obj *ReadFileFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + defer obj.wg.Wait() + defer func() { + if obj.recWatcher != nil { + obj.recWatcher.Close() // close previous watcher + obj.wg.Wait() + } + }() + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + obj.init.Input = nil // don't infinite loop back + continue // no more inputs, but don't return! + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + + if obj.last != nil && input.Cmp(obj.last) == nil { + continue // value didn't change, skip it + } + obj.last = input // store for next + + filename := input.Struct()["filename"].Str() + // TODO: add validation for absolute path? + if filename == obj.filename { + continue // nothing changed + } + obj.filename = filename + + if obj.recWatcher != nil { + obj.recWatcher.Close() // close previous watcher + obj.wg.Wait() + } + // create new watcher + obj.recWatcher = &recwatch.RecWatcher{ + Path: obj.filename, + Recurse: false, + Flags: recwatch.Flags{ + // TODO: add Logf + Debug: obj.init.Debug, + }, + } + if err := obj.recWatcher.Init(); err != nil { + obj.recWatcher = nil + // TODO: should we ignore the error and send ""? + return errwrap.Wrapf(err, "could not watch file") + } + + // FIXME: instead of sending one event here, the recwatch + // library should send one initial event at startup... + startup := make(chan struct{}) + close(startup) + + // watch recwatch events in a proxy goroutine, since + // changing the recwatch object would panic the main + // select when it's nil... + obj.wg.Add(1) + go func() { + defer obj.wg.Done() + for { + var err error + select { + case <-startup: + startup = nil + // send an initial event + + case event, ok := <-obj.recWatcher.Events(): + if !ok { + return // file watcher shut down + } + if err = event.Error; err != nil { + err = errwrap.Wrapf(err, "error event received") + } + } + + select { + case obj.events <- err: + // send event... + + case <-obj.closeChan: + // don't block here on shutdown + return + } + //err = nil // reset + } + }() + + case err, ok := <-obj.events: + if !ok { + return fmt.Errorf("no more events") + } + if err != nil { + return errwrap.Wrapf(err, "error event received") + } + + if obj.last == nil { + continue // still waiting for input values + } + + // read file... + content, err := ioutil.ReadFile(obj.filename) + if err != nil { + return errwrap.Wrapf(err, "error reading file") + } + result := string(content) // convert to string + + if obj.result == result { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- &types.StrValue{ + V: obj.result, + }: + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this function and turns off the stream. +func (obj *ReadFileFunc) Close() error { + close(obj.events) // clean up for fun + close(obj.closeChan) + return nil +}