lang: Unnested the core package from the functions dir

The core package could contain non-functions, so we might as well move
it upwards.
This commit is contained in:
James Shubin
2024-02-21 17:49:01 -05:00
parent 9329ed1e37
commit d6cf595899
97 changed files with 36 additions and 36 deletions

View File

@@ -1 +0,0 @@
generated_funcs.go

View File

@@ -1,43 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package core
import (
"github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
const (
// ConcatFuncName is the name this function is registered as.
ConcatFuncName = funcs.ConcatFuncName
)
func init() {
simple.Register(ConcatFuncName, &types.FuncValue{
T: types.NewType("func(a str, b str) str"),
V: Concat,
})
}
// Concat concatenates two strings together.
func Concat(input []types.Value) (types.Value, error) {
return &types.StrValue{
V: input[0].Str() + input[1].Str(),
}, nil
}

View File

@@ -1,23 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package convert
const (
// ModuleName is the prefix given to all the functions in this module.
ModuleName = "convert"
)

View File

@@ -1,40 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package convert
import (
"strconv"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "format_bool", &types.FuncValue{
T: types.NewType("func(a bool) str"),
V: FormatBool,
})
}
// FormatBool converts a boolean to a string representation that can be consumed
// by ParseBool. This value will be `"true"` or `"false"`.
func FormatBool(input []types.Value) (types.Value, error) {
return &types.StrValue{
V: strconv.FormatBool(input[0].Bool()),
}, nil
}

View File

@@ -1,48 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package convert
import (
"fmt"
"strconv"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "parse_bool", &types.FuncValue{
T: types.NewType("func(a str) bool"),
V: ParseBool,
})
}
// ParseBool parses a bool string and returns a boolean. It errors if you pass
// it an invalid value. Valid values match what is accepted by the golang
// strconv.ParseBool function. It's recommended to use the strings `true` or
// `false` if you are undecided about what string representation to choose.
func ParseBool(input []types.Value) (types.Value, error) {
s := input[0].Str()
b, err := strconv.ParseBool(s)
if err != nil {
return nil, fmt.Errorf("invalid bool: `%s`", s)
}
return &types.BoolValue{
V: b,
}, nil
}

View File

@@ -1,37 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package convert
import (
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "to_float", &types.FuncValue{
T: types.NewType("func(a int) float"),
V: ToFloat,
})
}
// ToFloat converts an integer to a float.
func ToFloat(input []types.Value) (types.Value, error) {
return &types.FloatValue{
V: float64(input[0].Int()),
}, nil
}

View File

@@ -1,44 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package convert
import (
"testing"
"github.com/purpleidea/mgmt/lang/types"
)
func testToFloat(t *testing.T, input int64, expected float64) {
got, err := ToFloat([]types.Value{&types.IntValue{V: input}})
if err != nil {
t.Error(err)
return
}
if got.Float() != expected {
t.Errorf("invalid output, expected %v, got %v", expected, got.Float())
return
}
}
func TestToFloat1(t *testing.T) {
testToFloat(t, 2, 2.0)
}
func TestToFloat2(t *testing.T) {
testToFloat(t, 7, 7.0)
}

View File

@@ -1,37 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package convert
import (
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "to_int", &types.FuncValue{
T: types.NewType("func(a float) int"),
V: ToInt,
})
}
// ToInt converts a float to an integer.
func ToInt(input []types.Value) (types.Value, error) {
return &types.IntValue{
V: int64(input[0].Float()),
}, nil
}

View File

@@ -1,46 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package convert
import (
"testing"
"github.com/purpleidea/mgmt/lang/types"
)
func testToInt(t *testing.T, input float64, expected int64) {
got, err := ToInt([]types.Value{&types.FloatValue{V: input}})
if err != nil {
t.Error(err)
return
}
if got.Int() != expected {
t.Errorf("invalid output, expected %v, got %v", expected, got)
return
}
}
func TestToInt1(t *testing.T) {
testToInt(t, 2.09, 2)
}
func TestToInt2(t *testing.T) {
testToInt(t, 7.8, 7)
}

View File

@@ -1,71 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package core
import (
"embed"
"io/fs"
// import so the funcs register
_ "github.com/purpleidea/mgmt/lang/funcs/core/convert"
_ "github.com/purpleidea/mgmt/lang/funcs/core/datetime"
_ "github.com/purpleidea/mgmt/lang/funcs/core/deploy"
_ "github.com/purpleidea/mgmt/lang/funcs/core/example"
_ "github.com/purpleidea/mgmt/lang/funcs/core/example/nested"
_ "github.com/purpleidea/mgmt/lang/funcs/core/fmt"
_ "github.com/purpleidea/mgmt/lang/funcs/core/iter"
_ "github.com/purpleidea/mgmt/lang/funcs/core/math"
_ "github.com/purpleidea/mgmt/lang/funcs/core/net"
_ "github.com/purpleidea/mgmt/lang/funcs/core/os"
_ "github.com/purpleidea/mgmt/lang/funcs/core/regexp"
_ "github.com/purpleidea/mgmt/lang/funcs/core/strings"
_ "github.com/purpleidea/mgmt/lang/funcs/core/sys"
_ "github.com/purpleidea/mgmt/lang/funcs/core/test"
_ "github.com/purpleidea/mgmt/lang/funcs/core/value"
_ "github.com/purpleidea/mgmt/lang/funcs/core/world"
)
// TODO: Instead of doing this one-level embed, we could give each package an
// API that it calls to "register" the private embed.FS that it wants to share.
//go:embed */*.mcl
var mcl embed.FS
// AssetNames returns a flattened list of embedded .mcl file paths.
func AssetNames() ([]string, error) {
fileSystem := mcl
paths := []string{}
if err := fs.WalkDir(fileSystem, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() { // skip the dirs
return nil
}
paths = append(paths, path)
return nil
}); err != nil {
return nil, err
}
return paths, nil
}
// Asset returns the contents of an embedded .mcl file.
func Asset(name string) ([]byte, error) {
return mcl.ReadFile(name)
}

View File

@@ -1,810 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
//go:build !root
package core
import (
"context"
"fmt"
"io/ioutil"
"os"
"reflect"
"sync"
"testing"
"github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/util"
"github.com/purpleidea/mgmt/util/errwrap"
"github.com/davecgh/go-spew/spew"
"github.com/kylelemons/godebug/pretty"
)
func TestPureFuncExec0(t *testing.T) {
type test struct { // an individual test
name string
funcname string
args []types.Value
fail bool
expect types.Value
}
testCases := []test{}
//{
// testCases = append(testCases, test{
// name: "",
// funcname: "",
// args: []types.Value{
// },
// fail: false,
// expect: nil,
// })
//}
{
testCases = append(testCases, test{
name: "strings.to_lower 0",
funcname: "strings.to_lower",
args: []types.Value{
&types.StrValue{
V: "HELLO",
},
},
fail: false,
expect: &types.StrValue{
V: "hello",
},
})
}
{
testCases = append(testCases, test{
name: "datetime.now fail",
funcname: "datetime.now",
args: nil,
fail: true,
expect: nil,
})
}
// TODO: run unification in PureFuncExec if it makes sense to do so...
//{
// testCases = append(testCases, test{
// name: "len 0",
// funcname: "len",
// args: []types.Value{
// &types.StrValue{
// V: "Hello, world!",
// },
// },
// fail: false,
// expect: &types.IntValue{
// V: 13,
// },
// })
//}
names := []string{}
for index, tc := range testCases { // run all the tests
if tc.name == "" {
t.Errorf("test #%d: not named", index)
continue
}
if util.StrInList(tc.name, names) {
t.Errorf("test #%d: duplicate sub test name of: %s", index, tc.name)
continue
}
names = append(names, tc.name)
//if index != 3 { // hack to run a subset (useful for debugging)
//if (index != 20 && index != 21) {
//if tc.name != "nil" {
// continue
//}
t.Run(fmt.Sprintf("test #%d (%s)", index, tc.name), func(t *testing.T) {
name, funcname, args, fail, expect := tc.name, tc.funcname, tc.args, tc.fail, tc.expect
t.Logf("\n\ntest #%d (%s) ----------------\n\n", index, name)
f, err := funcs.Lookup(funcname)
if err != nil {
t.Errorf("test #%d: func lookup failed with: %+v", index, err)
return
}
result, err := funcs.PureFuncExec(f, args)
if !fail && err != nil {
t.Errorf("test #%d: func failed with: %+v", index, err)
return
}
if fail && err == nil {
t.Errorf("test #%d: func passed, expected fail", index)
return
}
if !fail && result == nil {
t.Errorf("test #%d: func output was nil", index)
return
}
if reflect.DeepEqual(result, expect) {
return
}
// double check because DeepEqual is different since the func exists
diff := pretty.Compare(result, expect)
if diff == "" { // bonus
return
}
t.Errorf("test #%d: result did not match expected", index)
// TODO: consider making our own recursive print function
t.Logf("test #%d: actual: \n\n%s\n", index, spew.Sdump(result))
t.Logf("test #%d: expected: \n\n%s", index, spew.Sdump(expect))
// more details, for tricky cases:
diffable := &pretty.Config{
Diffable: true,
IncludeUnexported: true,
//PrintStringers: false,
//PrintTextMarshalers: false,
//SkipZeroFields: false,
}
t.Logf("test #%d: actual: \n\n%s\n", index, diffable.Sprint(result))
t.Logf("test #%d: expected: \n\n%s", index, diffable.Sprint(expect))
t.Logf("test #%d: diff:\n%s", index, diff)
})
}
}
// Step is used for the timeline in tests.
type Step interface {
Action() error
Expect() error
}
type manualStep struct {
action func() error
expect func() error
exit chan struct{} // exit signal, set by test harness
argch chan []types.Value // send new inputs, set by test harness
valueptrch chan int // incoming values, set by test harness
results []types.Value // all values, set by test harness
}
func (obj *manualStep) Action() error {
return obj.action()
}
func (obj *manualStep) Expect() error {
return obj.expect()
}
// NewManualStep creates a new manual step with an action and an expect test.
func NewManualStep(action, expect func() error) Step {
return &manualStep{
action: action,
expect: expect,
}
}
// NewSendInputs sends a list of inputs to the running function to populate it.
// If you send the wrong input signature, then you'll cause a failure. Testing
// this kind of failure is not a goal of these tests, since the unification code
// is meant to guarantee we always send the correct type signature.
func NewSendInputs(inputs []types.Value) Step {
return &sendInputsStep{
inputs: inputs,
}
}
type sendInputsStep struct {
inputs []types.Value
exit chan struct{} // exit signal, set by test harness
argch chan []types.Value // send new inputs, set by test harness
//valueptrch chan int // incoming values, set by test harness
//results []types.Value // all values, set by test harness
}
func (obj *sendInputsStep) Action() error {
select {
case obj.argch <- obj.inputs:
return nil
case <-obj.exit:
return fmt.Errorf("exit called")
}
}
func (obj *sendInputsStep) Expect() error { return nil }
// NewWaitForNSeconds waits this many seconds for new values from the stream. It
// can timeout if it gets bored of waiting.
func NewWaitForNSeconds(number int, timeout int) Step {
return &waitAmountStep{
timer: number, // timer seconds
timeout: timeout,
}
}
// NewWaitForNValues waits for this many values from the stream. It can timeout
// if it gets bored of waiting. If you request more values than can be produced,
// then it will block indefinitely if there's no timeout.
func NewWaitForNValues(number int, timeout int) Step {
return &waitAmountStep{
count: number, // count values
timeout: timeout,
}
}
// waitAmountStep waits for either a count of N values, or a timer of N seconds,
// or both. It also accepts a timeout which will cause it to error.
// TODO: have the timeout timer be overall instead of per step!
type waitAmountStep struct {
count int // nth count (set to a negative value to disable)
timer int // seconds (set to a negative value to disable)
timeout int // seconds to fail after
exit chan struct{} // exit signal, set by test harness
//argch chan []types.Value // send new inputs, set by test harness
valueptrch chan int // incoming values, set by test harness
results []types.Value // all values, set by test harness
}
func (obj *waitAmountStep) Action() error {
count := 0
ticked := false // did we get the timer event?
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ticker := util.TimeAfterOrBlockCtx(ctx, obj.timer)
if obj.timer < 0 { // disable timer
ticked = true
}
for {
if count >= obj.count { // got everything we wanted
if ticked {
break
}
}
select {
case <-obj.exit:
return fmt.Errorf("exit called")
case <-util.TimeAfterOrBlock(obj.timeout):
// TODO: make this overall instead of re-running it each time
return fmt.Errorf("waited too long for a value")
case n, ok := <-obj.valueptrch: // read output
if !ok {
return fmt.Errorf("unexpected close")
}
count++
_ = n // this is the index of the value we're at
case <-ticker: // received the timer event
ticker = nil
ticked = true
if obj.count > -1 {
break
}
}
}
return nil
}
func (obj *waitAmountStep) Expect() error { return nil }
// NewRangeExpect passes in an expect function which will receive the entire
// range of values ever received. This stream (list) of values can be matched on
// however you like.
func NewRangeExpect(fn func([]types.Value) error) Step {
return &rangeExpectStep{
fn: fn,
}
}
type rangeExpectStep struct {
fn func([]types.Value) error
// TODO: we could pass exit to the expect fn if we wanted in the future
exit chan struct{} // exit signal, set by test harness
//argch chan []types.Value // send new inputs, set by test harness
//valueptrch chan int // incoming values, set by test harness
results []types.Value // all values, set by test harness
}
func (obj *rangeExpectStep) Action() error { return nil }
func (obj *rangeExpectStep) Expect() error {
results := []types.Value{}
for _, v := range obj.results { // copy
value := v.Copy()
results = append(results, value)
}
return obj.fn(results) // run with a copy
}
// vog is a helper function to produce mcl values from golang equivalents that
// is only safe in tests because it panics on error.
func vog(i interface{}) types.Value {
v, err := types.ValueOfGolang(i)
if err != nil {
panic(fmt.Sprintf("unexpected error in vog: %+v", err))
}
return v
}
// rcopy is a helper to copy a list of types.Value structs.
func rcopy(input []types.Value) []types.Value {
result := []types.Value{}
for i := range input {
x := input[i].Copy()
result = append(result, x)
}
return result
}
// TestLiveFuncExec0 runs a live execution timeline on a function stream. It is
// very useful for testing function streams.
// FIXME: if the function returns a different type than what is specified by its
// signature, we might block instead of returning a useful error.
func TestLiveFuncExec0(t *testing.T) {
type args struct {
argv []types.Value
next func() // specifies we're ready for the next set of inputs
}
type test struct { // an individual test
name string
hostname string // in case we want to simulate a hostname
funcname string
// TODO: this could be a generator that keeps pushing out steps until it's done!
timeline []Step
expect func() error // function to check for expected state
startup func() error // function to run as startup
cleanup func() error // function to run as cleanup
}
timeout := -1 // default timeout (block) if not specified elsewhere
testCases := []test{}
{
count := 5
timeline := []Step{
NewWaitForNValues(count, timeout), // get 5 values
// pass in a custom validation function
NewRangeExpect(func(args []types.Value) error {
//fmt.Printf("range: %+v\n", args) // debugging
if len(args) < count {
return fmt.Errorf("no args found")
}
// check for increasing ints (ideal delta == 1)
x := args[0].Int()
for i := 1; i < count; i++ {
if args[i].Int()-x < 1 {
return fmt.Errorf("range jumps: %+v", args)
}
if args[i].Int()-x != 1 {
// if this fails, travis is just slow
return fmt.Errorf("timing error: %+v", args)
}
x = args[i].Int()
}
return nil
}),
//NewWaitForNSeconds(5, timeout), // not needed
}
testCases = append(testCases, test{
name: "simple func",
hostname: "", // not needed for this func
funcname: "datetime.now",
timeline: timeline,
expect: func() error { return nil },
startup: func() error { return nil },
cleanup: func() error { return nil },
})
}
{
timeline := []Step{
NewSendInputs([]types.Value{
vog("helloXworld"),
vog("X"), // split by this
}),
NewWaitForNValues(1, timeout), // more than 1 blocks here
// pass in a custom validation function
NewRangeExpect(func(args []types.Value) error {
//fmt.Printf("range: %+v\n", args) // debugging
if c := len(args); c != 1 {
return fmt.Errorf("wrong args count, got: %d", c)
}
if args[0].Type().Kind != types.KindList {
return fmt.Errorf("expected list, got: %+v", args[0])
}
if err := vog([]string{"hello", "world"}).Cmp(args[0]); err != nil {
return errwrap.Wrapf(err, "got different expected value: %+v", args[0])
}
return nil
}),
}
testCases = append(testCases, test{
name: "simple pure func",
hostname: "", // not needed for this func
funcname: "strings.split",
timeline: timeline,
expect: func() error { return nil },
startup: func() error { return nil },
cleanup: func() error { return nil },
})
}
{
p := "/tmp/somefiletoread"
content := "hello world!\n"
timeline := []Step{
NewSendInputs([]types.Value{
vog(p),
}),
NewWaitForNValues(1, timeout), // more than 1 blocks here
NewWaitForNSeconds(5, 10), // wait longer just to be sure
// pass in a custom validation function
NewRangeExpect(func(args []types.Value) error {
//fmt.Printf("range: %+v\n", args) // debugging
if c := len(args); c != 1 {
return fmt.Errorf("wrong args count, got: %d", c)
}
if args[0].Type().Kind != types.KindStr {
return fmt.Errorf("expected str, got: %+v", args[0])
}
if err := vog(content).Cmp(args[0]); err != nil {
return errwrap.Wrapf(err, "got different expected value: %+v", args[0])
}
return nil
}),
}
testCases = append(testCases, test{
name: "readfile",
hostname: "", // not needed for this func
funcname: "os.readfile",
timeline: timeline,
expect: func() error { return nil },
startup: func() error { return ioutil.WriteFile(p, []byte(content), 0666) },
cleanup: func() error { return os.Remove(p) },
})
}
names := []string{}
for index, tc := range testCases { // run all the tests
if tc.name == "" {
t.Errorf("test #%d: not named", index)
continue
}
if util.StrInList(tc.name, names) {
t.Errorf("test #%d: duplicate sub test name of: %s", index, tc.name)
continue
}
names = append(names, tc.name)
t.Run(fmt.Sprintf("test #%d (%s)", index, tc.name), func(t *testing.T) {
hostname, funcname, timeline, expect, startup, cleanup := tc.hostname, tc.funcname, tc.timeline, tc.expect, tc.startup, tc.cleanup
t.Logf("\n\ntest #%d: func: %+v\n", index, funcname)
defer t.Logf("test #%d: done!", index)
handle, err := funcs.Lookup(funcname) // get function...
if err != nil {
t.Errorf("test #%d: func lookup failed with: %+v", index, err)
return
}
// run validate!
if err := handle.Validate(); err != nil {
t.Errorf("test #%d: FAIL", index)
t.Errorf("test #%d: could not validate Func: %+v", index, err)
return
}
sig := handle.Info().Sig
if sig.Kind != types.KindFunc {
t.Errorf("test #%d: FAIL", index)
t.Errorf("test #%d: must be kind func: %+v", index, err)
return
}
input := make(chan types.Value) // we close this when we're done
output := make(chan types.Value) // we create it, func closes it
debug := testing.Verbose() // set via the -test.v flag to `go test`
logf := func(format string, v ...interface{}) {
t.Logf(fmt.Sprintf("test #%d: func: ", index)+format, v...)
}
init := &interfaces.Init{
Hostname: hostname,
Input: input,
Output: output,
World: nil, // TODO: add me somehow!
Debug: debug,
Logf: logf,
}
t.Logf("test #%d: running startup()", index)
if err := startup(); err != nil {
t.Errorf("test #%d: FAIL", index)
t.Errorf("test #%d: could not startup: %+v", index, err)
return
}
// run init
t.Logf("test #%d: running Init", index)
if err := handle.Init(init); err != nil {
t.Errorf("test #%d: FAIL", index)
t.Errorf("test #%d: could not init func: %+v", index, err)
return
}
defer func() {
t.Logf("test #%d: running cleanup()", index)
if err := cleanup(); err != nil {
t.Errorf("test #%d: FAIL", index)
t.Errorf("test #%d: could not cleanup: %+v", index, err)
}
}()
wg := &sync.WaitGroup{}
defer wg.Wait() // if we return early
argch := make(chan []types.Value)
errch := make(chan error)
close1 := make(chan struct{})
close2 := make(chan struct{})
kill1 := make(chan struct{})
kill2 := make(chan struct{})
//kill3 := make(chan struct{}) // future use
exit := make(chan struct{})
mutex := &sync.RWMutex{}
results := []types.Value{} // all values received so far
valueptrch := make(chan int) // which Nth value are we at?
killTimeline := make(chan struct{}) // ask timeline to exit
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// wait for close signals
wg.Add(1)
go func() {
defer wg.Done()
defer close(errch) // last one turns out the lights
select {
case <-close1:
}
select {
case <-close2:
}
}()
// wait for kill signals
wg.Add(1)
go func() {
defer wg.Done()
select {
case <-exit:
case <-kill1:
case <-kill2:
//case <-kill3: // future use
}
close(killTimeline) // main kill signal for tl
}()
// run the stream
wg.Add(1)
go func() {
defer wg.Done()
defer close(close1)
if debug {
logf("Running func")
}
err := handle.Stream(ctx) // sends to output chan
t.Logf("test #%d: stream exited with: %+v", index, err)
if debug {
logf("Exiting func")
}
if err == nil {
return
}
// we closed with an error...
select {
case errch <- errwrap.Wrapf(err, "problem streaming func"):
}
}()
// read from incoming args and send to input channel
wg.Add(1)
go func() {
defer wg.Done()
defer close(close2)
defer close(input) // close input when done
if argch == nil { // no args
return
}
si := &types.Type{
// input to functions are structs
Kind: types.KindStruct,
Map: handle.Info().Sig.Map,
Ord: handle.Info().Sig.Ord,
}
t.Logf("test #%d: func has sig: %s", index, si)
// TODO: should this be a select with an exit signal?
for args := range argch { // chan
st := types.NewStruct(si)
count := 0
for i, arg := range args {
//name := util.NumToAlpha(i) // assume (incorrectly) for now...
name := handle.Info().Sig.Ord[i] // better
if err := st.Set(name, arg); err != nil { // populate struct
select {
case errch <- errwrap.Wrapf(err, "struct set failure"):
}
close(kill1) // unblock tl and cause fail
return
}
count++
}
if count != len(si.Map) { // expect this number
select {
case errch <- fmt.Errorf("struct field count is wrong"):
}
close(kill1) // unblock tl and cause fail
return
}
t.Logf("test #%d: send to func: %s", index, args)
select {
case input <- st: // send to function (must not block)
case <-close1: // unblock the input send in case stream closed
select {
case errch <- fmt.Errorf("stream closed early"):
}
}
}
}()
// run timeline
wg.Add(1)
go func() {
t.Logf("test #%d: executing timeline", index)
defer wg.Done()
Timeline:
for ix, step := range timeline {
select {
case <-killTimeline:
break Timeline
default:
// pass
}
mutex.RLock()
// magic setting of important values...
if s, ok := step.(*manualStep); ok {
s.exit = killTimeline // kill signal
s.argch = argch // send inputs here
s.valueptrch = valueptrch // receive value ptr
s.results = rcopy(results) // all results as array
}
if s, ok := step.(*sendInputsStep); ok {
s.exit = killTimeline
s.argch = argch
//s.valueptrch = valueptrch
//s.results = rcopy(results)
}
if s, ok := step.(*waitAmountStep); ok {
s.exit = killTimeline
//s.argch = argch
s.valueptrch = valueptrch
s.results = rcopy(results)
}
if s, ok := step.(*rangeExpectStep); ok {
s.exit = killTimeline
//s.argch = argch
//s.valueptrch = valueptrch
s.results = rcopy(results)
}
mutex.RUnlock()
t.Logf("test #%d: step(%d)...", index, ix)
if err := step.Action(); err != nil {
t.Errorf("test #%d: FAIL", index)
t.Errorf("test #%d: step(%d) action failed: %s", index, ix, err.Error())
break
}
if err := step.Expect(); err != nil {
t.Errorf("test #%d: FAIL", index)
t.Errorf("test #%d: step(%d) expect failed: %s", index, ix, err.Error())
break
}
}
t.Logf("test #%d: timeline finished", index)
close(argch)
t.Logf("test #%d: running cancel", index)
cancel()
}()
// read everything
counter := 0
Loop:
for {
select {
case value, ok := <-output: // read from channel
if !ok {
output = nil
continue Loop // only exit via errch closing!
}
t.Logf("test #%d: got from func: %s", index, value)
// check return type
if err := handle.Info().Sig.Out.Cmp(value.Type()); err != nil {
t.Errorf("test #%d: FAIL", index)
t.Errorf("test #%d: unexpected return type from func: %+v", index, err)
close(kill2)
continue Loop // only exit via errch closing!
}
mutex.Lock()
results = append(results, value) // save value
mutex.Unlock()
counter++
case err, ok := <-errch: // handle possible errors
if !ok {
break Loop
}
t.Errorf("test #%d: FAIL", index)
t.Errorf("test #%d: error: %+v", index, err)
continue Loop // only exit via errch closing!
}
// send events to our timeline
select {
case valueptrch <- counter: // TODO: send value?
// TODO: add this sort of thing, but don't block everyone who doesn't read
//case <-time.After(time.Duration(globalStepReadTimeout) * time.Second):
// t.Errorf("test #%d: FAIL", index)
// t.Errorf("test #%d: timeline receiver was too slow for value", index)
// t.Errorf("test #%d: got(%d): %+v", index, counter, results[counter])
// close(kill3) // shut everything down
// continue Loop // only exit via errch closing!
}
}
t.Logf("test #%d: waiting for shutdown", index)
close(exit)
wg.Wait()
if err := expect(); err != nil {
t.Errorf("test #%d: FAIL", index)
t.Errorf("test #%d: expect failed: %s", index, err.Error())
return
}
// all done!
})
}
}

View File

@@ -1,23 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coredatetime
const (
// ModuleName is the prefix given to all the functions in this module.
ModuleName = "datetime"
)

View File

@@ -1,50 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coredatetime
import (
"fmt"
"time"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "format", &types.FuncValue{
T: types.NewType("func(a int, b str) str"),
V: Format,
})
}
// Format returns returns a textual representation of the input time. The format
// has to be defined like specified by the golang "time" package. The time is
// the number of seconds since the epoch, and matches what comes from our Now
// function. Golang documentation: https://golang.org/pkg/time/#Time.Format
func Format(input []types.Value) (types.Value, error) {
epochDelta := input[0].Int()
if epochDelta < 0 {
return nil, fmt.Errorf("epoch delta must be positive")
}
format := input[1].Str()
v := time.Unix(epochDelta, 0).Format(format)
return &types.StrValue{
V: v,
}, nil
}

View File

@@ -1,39 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
//go:build !darwin
package coredatetime
import (
"testing"
"github.com/purpleidea/mgmt/lang/types"
)
func TestFormat(t *testing.T) {
inputVal := &types.IntValue{V: 1443158163}
inputFormat := &types.StrValue{V: "2006"}
val, err := Format([]types.Value{inputVal, inputFormat})
if err != nil {
t.Error(err)
}
if val.String() != `"2015"` {
t.Errorf("invalid output, expected %s, got %s", `"2015"`, val.String())
}
}

View File

@@ -1,48 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coredatetime
import (
"fmt"
"time"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "hour", &types.FuncValue{
T: types.NewType("func(a int) int"),
V: Hour,
})
}
// Hour returns the hour of the day corresponding to the input time. The time is
// the number of seconds since the epoch, and matches what comes from our Now
// function.
func Hour(input []types.Value) (types.Value, error) {
epochDelta := input[0].Int()
if epochDelta < 0 {
return nil, fmt.Errorf("epoch delta must be positive")
}
hour := time.Unix(epochDelta, 0).Hour()
return &types.IntValue{
V: int64(hour),
}, nil
}

View File

@@ -1,101 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coredatetime
import (
"context"
"time"
"github.com/purpleidea/mgmt/lang/funcs/facts"
"github.com/purpleidea/mgmt/lang/types"
)
const (
// NowFuncName is the name this fact is registered as. It's still a Func
// Name because this is the name space the fact is actually using.
NowFuncName = "now"
)
func init() {
facts.ModuleRegister(ModuleName, NowFuncName, func() facts.Fact { return &DateTimeFact{} }) // must register the fact and name
}
// DateTimeFact is a fact which returns the current date and time.
type DateTimeFact struct {
init *facts.Init
}
// String returns a simple name for this fact. This is needed so this struct can
// satisfy the pgraph.Vertex interface.
func (obj *DateTimeFact) String() string {
return NowFuncName
}
// Validate makes sure we've built our struct properly. It is usually unused for
// normal facts that users can use directly.
//func (obj *DateTimeFact) Validate() error {
// return nil
//}
// Info returns some static info about itself.
func (obj *DateTimeFact) Info() *facts.Info {
return &facts.Info{
Output: types.NewType("int"),
}
}
// Init runs some startup code for this fact.
func (obj *DateTimeFact) Init(init *facts.Init) error {
obj.init = init
return nil
}
// Stream returns the changing values that this fact has over time.
func (obj *DateTimeFact) Stream(ctx context.Context) error {
defer close(obj.init.Output) // always signal when we're done
// XXX: this might be an interesting fact to write because:
// 1) will the sleeps from the ticker be in sync with the second ticker?
// 2) if we care about a less precise interval (eg: minute changes) can
// we set this up so it doesn't tick as often? -- Yes (make this a function or create a limit function to wrap this)
// 3) is it best to have a delta timer that wakes up before it's needed
// and calculates how much longer to sleep for?
ticker := time.NewTicker(time.Duration(1) * time.Second)
// streams must generate an initial event on startup
startChan := make(chan struct{}) // start signal
close(startChan) // kick it off!
defer ticker.Stop()
for {
select {
case <-startChan: // kick the loop once at start
startChan = nil // disable
case <-ticker.C: // received the timer event
// pass
case <-ctx.Done():
return nil
}
select {
case obj.init.Output <- &types.IntValue{ // seconds since 1970...
V: time.Now().Unix(), // .UTC() not necessary
}:
case <-ctx.Done():
return nil
}
}
}

View File

@@ -1,42 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coredatetime
import (
"fmt"
"time"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
// FIXME: consider renaming this to printf, and add in a format string?
simple.ModuleRegister(ModuleName, "print", &types.FuncValue{
T: types.NewType("func(a int) str"),
V: func(input []types.Value) (types.Value, error) {
epochDelta := input[0].Int()
if epochDelta < 0 {
return nil, fmt.Errorf("epoch delta must be positive")
}
return &types.StrValue{
V: time.Unix(epochDelta, 0).String(),
}, nil
},
})
}

View File

@@ -1,49 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coredatetime
import (
"fmt"
"strings"
"time"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "weekday", &types.FuncValue{
T: types.NewType("func(a int) str"),
V: Weekday,
})
}
// Weekday returns the lowercased day of the week corresponding to the input
// time. The time is the number of seconds since the epoch, and matches what
// comes from our Now function.
func Weekday(input []types.Value) (types.Value, error) {
epochDelta := input[0].Int()
if epochDelta < 0 {
return nil, fmt.Errorf("epoch delta must be positive")
}
weekday := time.Unix(epochDelta, 0).Weekday()
return &types.StrValue{
V: strings.ToLower(weekday.String()),
}, nil
}

View File

@@ -1,158 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coredeploy
import (
"context"
"fmt"
"strings"
"github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
)
const (
// AbsPathFuncName is the name this function is registered as.
AbsPathFuncName = "abspath"
// arg names...
absPathArgNamePath = "path"
)
func init() {
funcs.ModuleRegister(ModuleName, AbsPathFuncName, func() interfaces.Func { return &AbsPathFunc{} }) // must register the func and name
}
// AbsPathFunc is a function that returns the absolute, full path in the deploy
// from an input path that is relative to the calling file. If you pass it an
// empty string, you'll just get the absolute deploy directory path that you're
// in.
type AbsPathFunc struct {
init *interfaces.Init
data *interfaces.FuncData
last types.Value // last value received to use for diff
path *string // the active path
result *string // last calculated output
}
// String returns a simple name for this function. This is needed so this struct
// can satisfy the pgraph.Vertex interface.
func (obj *AbsPathFunc) String() string {
return AbsPathFuncName
}
// SetData is used by the language to pass our function some code-level context.
func (obj *AbsPathFunc) SetData(data *interfaces.FuncData) {
obj.data = data
}
// ArgGen returns the Nth arg name for this function.
func (obj *AbsPathFunc) ArgGen(index int) (string, error) {
seq := []string{absPathArgNamePath}
if l := len(seq); index >= l {
return "", fmt.Errorf("index %d exceeds arg length of %d", index, l)
}
return seq[index], nil
}
// Validate makes sure we've built our struct properly. It is usually unused for
// normal functions that users can use directly.
func (obj *AbsPathFunc) Validate() error {
return nil
}
// Info returns some static info about itself.
func (obj *AbsPathFunc) Info() *interfaces.Info {
return &interfaces.Info{
Pure: false, // maybe false because the file contents can change
Memo: false,
Sig: types.NewType(fmt.Sprintf("func(%s str) str", absPathArgNamePath)),
}
}
// Init runs some startup code for this function.
func (obj *AbsPathFunc) Init(init *interfaces.Init) error {
obj.init = init
if obj.data == nil {
// programming error
return fmt.Errorf("missing function data")
}
return nil
}
// Stream returns the changing values that this func has over time.
func (obj *AbsPathFunc) Stream(ctx context.Context) error {
defer close(obj.init.Output) // the sender closes
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
path := input.Struct()[absPathArgNamePath].Str()
// TODO: add validation for absolute path?
if obj.path != nil && *obj.path == path {
continue // nothing changed
}
obj.path = &path
p := strings.TrimSuffix(obj.data.Base, "/")
if p == obj.data.Base { // didn't trim, so we fail
// programming error
return fmt.Errorf("no trailing slash on Base, got: `%s`", p)
}
result := p
if *obj.path == "" {
result += "/" // add the above trailing slash back
} else if !strings.HasPrefix(*obj.path, "/") {
return fmt.Errorf("path was not absolute, got: `%s`", *obj.path)
//result += "/" // be forgiving ?
}
result += *obj.path
if obj.result != nil && *obj.result == result {
continue // result didn't change
}
obj.result = &result // store new result
case <-ctx.Done():
return nil
}
select {
case obj.init.Output <- &types.StrValue{
V: *obj.result,
}:
case <-ctx.Done():
return nil
}
}
}

View File

@@ -1,23 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coredeploy
const (
// ModuleName is the prefix given to all the functions in this module.
ModuleName = "deploy"
)

View File

@@ -1,172 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coredeploy
import (
"context"
"fmt"
"strings"
"github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/util/errwrap"
)
const (
// ReadFileFuncName is the name this function is registered as.
ReadFileFuncName = "readfile"
// arg names...
readFileArgNameFilename = "filename"
)
func init() {
funcs.ModuleRegister(ModuleName, ReadFileFuncName, func() interfaces.Func { return &ReadFileFunc{} }) // must register the func and name
}
// ReadFileFunc is a function that reads the full contents from a file in our
// deploy. The file contents can only change with a new deploy, so this is
// static. Please note that this is different from the readfile function in the
// os package.
type ReadFileFunc struct {
init *interfaces.Init
data *interfaces.FuncData
last types.Value // last value received to use for diff
filename *string // the active filename
result *string // last calculated output
}
// String returns a simple name for this function. This is needed so this struct
// can satisfy the pgraph.Vertex interface.
func (obj *ReadFileFunc) String() string {
return ReadFileFuncName
}
// SetData is used by the language to pass our function some code-level context.
func (obj *ReadFileFunc) SetData(data *interfaces.FuncData) {
obj.data = data
}
// ArgGen returns the Nth arg name for this function.
func (obj *ReadFileFunc) ArgGen(index int) (string, error) {
seq := []string{readFileArgNameFilename}
if l := len(seq); index >= l {
return "", fmt.Errorf("index %d exceeds arg length of %d", index, l)
}
return seq[index], nil
}
// 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(fmt.Sprintf("func(%s str) str", readFileArgNameFilename)),
}
}
// Init runs some startup code for this function.
func (obj *ReadFileFunc) Init(init *interfaces.Init) error {
obj.init = init
if obj.data == nil {
// programming error
return fmt.Errorf("missing function data")
}
return nil
}
// Stream returns the changing values that this func has over time.
func (obj *ReadFileFunc) Stream(ctx context.Context) error {
defer close(obj.init.Output) // the sender closes
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()[readFileArgNameFilename].Str()
// TODO: add validation for absolute path?
// TODO: add check for empty string
if obj.filename != nil && *obj.filename == filename {
continue // nothing changed
}
obj.filename = &filename
p := strings.TrimSuffix(obj.data.Base, "/")
if p == obj.data.Base { // didn't trim, so we fail
// programming error
return fmt.Errorf("no trailing slash on Base, got: `%s`", p)
}
path := p
if !strings.HasPrefix(*obj.filename, "/") {
return fmt.Errorf("filename was not absolute, got: `%s`", *obj.filename)
//path += "/" // be forgiving ?
}
path += *obj.filename
fs, err := obj.init.World.Fs(obj.data.FsURI) // open the remote file system
if err != nil {
return errwrap.Wrapf(err, "can't load code from file system `%s`", obj.data.FsURI)
}
// this is relative to the module dir the func is in!
content, err := fs.ReadFile(path) // open the remote file system
// We could use it directly, but it feels like less correct.
//content, err := obj.data.Fs.ReadFile(path) // open the remote file system
if err != nil {
return errwrap.Wrapf(err, "can't read file `%s` (%s)", *obj.filename, path)
}
result := string(content) // convert to string
if obj.result != nil && *obj.result == result {
continue // result didn't change
}
obj.result = &result // store new result
case <-ctx.Done():
return nil
}
select {
case obj.init.Output <- &types.StrValue{
V: *obj.result,
}:
case <-ctx.Done():
return nil
}
}
}

View File

@@ -1,158 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coredeploy
import (
"context"
"fmt"
"github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/util/errwrap"
)
const (
// ReadFileAbsFuncName is the name this function is registered as.
ReadFileAbsFuncName = "readfileabs"
// arg names...
readfileArgNameFilename = "filename"
)
func init() {
funcs.ModuleRegister(ModuleName, ReadFileAbsFuncName, func() interfaces.Func { return &ReadFileAbsFunc{} }) // must register the func and name
}
// ReadFileAbsFunc is a function that reads the full contents from a file in our
// deploy. The file contents can only change with a new deploy, so this is
// static. In particular, this takes an absolute path relative to the root
// deploy. In general, you should use `deploy.readfile` instead. Please note
// that this is different from the readfile function in the os package.
type ReadFileAbsFunc struct {
init *interfaces.Init
data *interfaces.FuncData
last types.Value // last value received to use for diff
filename *string // the active filename
result *string // last calculated output
}
// String returns a simple name for this function. This is needed so this struct
// can satisfy the pgraph.Vertex interface.
func (obj *ReadFileAbsFunc) String() string {
return ReadFileAbsFuncName
}
// SetData is used by the language to pass our function some code-level context.
func (obj *ReadFileAbsFunc) SetData(data *interfaces.FuncData) {
obj.data = data
}
// ArgGen returns the Nth arg name for this function.
func (obj *ReadFileAbsFunc) ArgGen(index int) (string, error) {
seq := []string{readfileArgNameFilename}
if l := len(seq); index >= l {
return "", fmt.Errorf("index %d exceeds arg length of %d", index, l)
}
return seq[index], nil
}
// Validate makes sure we've built our struct properly. It is usually unused for
// normal functions that users can use directly.
func (obj *ReadFileAbsFunc) Validate() error {
return nil
}
// Info returns some static info about itself.
func (obj *ReadFileAbsFunc) Info() *interfaces.Info {
return &interfaces.Info{
Pure: false, // maybe false because the file contents can change
Memo: false,
Sig: types.NewType(fmt.Sprintf("func(%s str) str", readfileArgNameFilename)),
}
}
// Init runs some startup code for this function.
func (obj *ReadFileAbsFunc) Init(init *interfaces.Init) error {
obj.init = init
if obj.data == nil {
// programming error
return fmt.Errorf("missing function data")
}
return nil
}
// Stream returns the changing values that this func has over time.
func (obj *ReadFileAbsFunc) Stream(ctx context.Context) error {
defer close(obj.init.Output) // the sender closes
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()[readfileArgNameFilename].Str()
// TODO: add validation for absolute path?
// TODO: add check for empty string
if obj.filename != nil && *obj.filename == filename {
continue // nothing changed
}
obj.filename = &filename
fs, err := obj.init.World.Fs(obj.data.FsURI) // open the remote file system
if err != nil {
return errwrap.Wrapf(err, "can't load code from file system `%s`", obj.data.FsURI)
}
content, err := fs.ReadFile(*obj.filename) // open the remote file system
// We could use it directly, but it feels like less correct.
//content, err := obj.data.Fs.ReadFile(*obj.filename) // open the remote file system
if err != nil {
return errwrap.Wrapf(err, "can't read file `%s`", *obj.filename)
}
result := string(content) // convert to string
if obj.result != nil && *obj.result == result {
continue // result didn't change
}
obj.result = &result // store new result
case <-ctx.Done():
return nil
}
select {
case obj.init.Output <- &types.StrValue{
V: *obj.result,
}:
case <-ctx.Done():
return nil
}
}
}

View File

@@ -1,35 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreexample
import (
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
// Answer is the Answer to Life, the Universe and Everything.
const Answer = 42
func init() {
simple.ModuleRegister(ModuleName, "answer", &types.FuncValue{
T: types.NewType("func() int"),
V: func([]types.Value) (types.Value, error) {
return &types.IntValue{V: Answer}, nil
},
})
}

View File

@@ -1,39 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreexample
import (
"fmt"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "errorbool", &types.FuncValue{
T: types.NewType("func(a bool) str"),
V: func(input []types.Value) (types.Value, error) {
if input[0].Bool() {
return nil, fmt.Errorf("we errored on request")
}
return &types.StrValue{
V: "set input to true to generate an error",
}, nil
},
})
}

View File

@@ -1,23 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreexample
const (
// ModuleName is the prefix given to all the functions in this module.
ModuleName = "example"
)

View File

@@ -1,101 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreexample
import (
"context"
"time"
"github.com/purpleidea/mgmt/lang/funcs/facts"
"github.com/purpleidea/mgmt/lang/types"
)
const (
// FlipFlopFuncName is the name this fact is registered as. It's still a
// Func Name because this is the name space the fact is actually using.
FlipFlopFuncName = "flipflop"
)
func init() {
facts.ModuleRegister(ModuleName, FlipFlopFuncName, func() facts.Fact { return &FlipFlopFact{} }) // must register the fact and name
}
// FlipFlopFact is a fact which flips a bool repeatedly. This is an example fact
// and is not meant for serious computing. This would be better served by a flip
// function which you could specify an interval for.
type FlipFlopFact struct {
init *facts.Init
value bool
}
// String returns a simple name for this fact. This is needed so this struct can
// satisfy the pgraph.Vertex interface.
func (obj *FlipFlopFact) String() string {
return FlipFlopFuncName
}
// Validate makes sure we've built our struct properly. It is usually unused for
// normal facts that users can use directly.
//func (obj *FlipFlopFact) Validate() error {
// return nil
//}
// Info returns some static info about itself.
func (obj *FlipFlopFact) Info() *facts.Info {
return &facts.Info{
Output: types.NewType("bool"),
}
}
// Init runs some startup code for this fact.
func (obj *FlipFlopFact) Init(init *facts.Init) error {
obj.init = init
return nil
}
// Stream returns the changing values that this fact has over time.
func (obj *FlipFlopFact) Stream(ctx context.Context) error {
defer close(obj.init.Output) // always signal when we're done
// TODO: don't hard code 5 sec interval
ticker := time.NewTicker(time.Duration(5) * time.Second)
// streams must generate an initial event on startup
startChan := make(chan struct{}) // start signal
close(startChan) // kick it off!
defer ticker.Stop()
for {
select {
case <-startChan: // kick the loop once at start
startChan = nil // disable
case <-ticker.C: // received the timer event
// pass
case <-ctx.Done():
return nil
}
select {
case obj.init.Output <- &types.BoolValue{ // flip
V: obj.value,
}:
case <-ctx.Done():
return nil
}
obj.value = !obj.value // flip it
}
}

View File

@@ -1,36 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreexample
import (
"fmt"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "int2str", &types.FuncValue{
T: types.NewType("func(a int) str"),
V: func(input []types.Value) (types.Value, error) {
return &types.StrValue{
V: fmt.Sprintf("%d", input[0].Int()),
}, nil
},
})
}

View File

@@ -1,21 +0,0 @@
# Mgmt
# Copyright (C) 2013-2024+ 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 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 <http:#www.gnu.org/licenses/>.
# This is a native (mcl) function.
func nativeanswer() {
42
}

View File

@@ -1,38 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package corenested
import (
coreexample "github.com/purpleidea/mgmt/lang/funcs/core/example"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(coreexample.ModuleName+"/"+ModuleName, "hello", &types.FuncValue{
T: types.NewType("func() str"),
V: Hello,
})
}
// Hello returns some string. This is just to test nesting.
func Hello(input []types.Value) (types.Value, error) {
return &types.StrValue{
V: "Hello!",
}, nil
}

View File

@@ -1,23 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package corenested
const (
// ModuleName is the prefix given to all the functions in this module.
ModuleName = "nested"
)

View File

@@ -1,38 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreexample
import (
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "plus", &types.FuncValue{
T: types.NewType("func(y str, z str) str"),
V: Plus,
})
}
// Plus returns y + z.
func Plus(input []types.Value) (types.Value, error) {
y, z := input[0].Str(), input[1].Str()
return &types.StrValue{
V: y + z,
}, nil
}

View File

@@ -1,40 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreexample
import (
"strconv"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "str2int", &types.FuncValue{
T: types.NewType("func(a str) int"),
V: func(input []types.Value) (types.Value, error) {
var i int64
if val, err := strconv.ParseInt(input[0].Str(), 10, 64); err == nil {
i = val
}
return &types.IntValue{
V: i,
}, nil
},
})
}

View File

@@ -1,31 +0,0 @@
# Mgmt
# Copyright (C) 2013-2024+ 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 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 <http:#www.gnu.org/licenses/>.
# This is a native (mcl) function.
func test1() {
42
}
# This is a native (mcl) global variable.
$test1 = 13
# This is a native (mcl) class.
class test1 {
print "inside" {
msg => "wow, cool",
}
}

View File

@@ -1,276 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreexample
import (
"context"
"fmt"
"math"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/util/errwrap"
)
const (
// VUMeterFuncName is the name this function is registered as.
VUMeterFuncName = "vumeter"
// arg names...
vuMeterArgNameSymbol = "symbol"
vuMeterArgNameMultiplier = "multiplier"
vuMeterArgNamePeak = "peak"
)
func init() {
funcs.ModuleRegister(ModuleName, VUMeterFuncName, func() interfaces.Func { return &VUMeterFunc{} }) // must register the func and name
}
// VUMeterFunc is a gimmic function to display a vu meter from the microphone.
type VUMeterFunc struct {
init *interfaces.Init
last types.Value // last value received to use for diff
symbol string
multiplier int64
peak float64
result *string // last calculated output
}
// String returns a simple name for this function. This is needed so this struct
// can satisfy the pgraph.Vertex interface.
func (obj *VUMeterFunc) String() string {
return VUMeterFuncName
}
// ArgGen returns the Nth arg name for this function.
func (obj *VUMeterFunc) ArgGen(index int) (string, error) {
seq := []string{vuMeterArgNameSymbol, vuMeterArgNameMultiplier, vuMeterArgNamePeak}
if l := len(seq); index >= l {
return "", fmt.Errorf("index %d exceeds arg length of %d", index, l)
}
return seq[index], nil
}
// Validate makes sure we've built our struct properly. It is usually unused for
// normal functions that users can use directly.
func (obj *VUMeterFunc) Validate() error {
check := func(binary string) error {
args := []string{"--help"}
prog := fmt.Sprintf("%s %s", binary, strings.Join(args, " "))
//obj.init.Logf("running: %s", prog)
p, err := filepath.EvalSymlinks(binary)
if err != nil {
return err
}
// TODO: do we need to do the ^C handling?
// XXX: is the ^C context cancellation propagating into this correctly?
cmd := exec.CommandContext(context.TODO(), p, args...)
cmd.Dir = ""
cmd.Env = []string{}
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
Pgid: 0,
}
if err := cmd.Run(); err != nil {
if e, ok := err.(*exec.Error); ok && e.Err == exec.ErrNotFound {
return fmt.Errorf("is %s in your $PATH ?", binary)
}
return errwrap.Wrapf(err, "error running: %s", prog)
}
return nil
}
// if rec is a symlink, this will error without the above EvalSymlinks!
for _, x := range []string{"/usr/bin/rec", "/usr/bin/sox"} {
if err := check(x); err != nil {
return err
}
}
return nil
}
// Info returns some static info about itself.
func (obj *VUMeterFunc) Info() *interfaces.Info {
return &interfaces.Info{
Pure: true,
Memo: false,
Sig: types.NewType(fmt.Sprintf("func(%s str, %s int, %s float) str", vuMeterArgNameSymbol, vuMeterArgNameMultiplier, vuMeterArgNamePeak)),
}
}
// Init runs some startup code for this function.
func (obj *VUMeterFunc) Init(init *interfaces.Init) error {
obj.init = init
return nil
}
// Stream returns the changing values that this func has over time.
func (obj *VUMeterFunc) Stream(ctx context.Context) error {
defer close(obj.init.Output) // the sender closes
ticker := newTicker()
defer ticker.Stop()
// FIXME: this goChan seems to work better than the ticker :)
// this is because we have a ~1sec delay in capturing the value in exec
goChan := make(chan struct{})
once := &sync.Once{}
onceFunc := func() { close(goChan) } // only run once!
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
obj.symbol = input.Struct()[vuMeterArgNameSymbol].Str()
obj.multiplier = input.Struct()[vuMeterArgNameMultiplier].Int()
obj.peak = input.Struct()[vuMeterArgNamePeak].Float()
once.Do(onceFunc)
continue // we must wrap around and go in through goChan
//case <-ticker.C: // received the timer event
case <-goChan: // triggers constantly
if obj.last == nil {
continue // still waiting for input values
}
// record for one second to a shared memory file
// rec /dev/shm/mgmt_rec.wav trim 0 1 2>/dev/null
args1 := []string{"/dev/shm/mgmt_rec.wav", "trim", "0", "1"}
cmd1 := exec.Command("/usr/bin/rec", args1...)
// XXX: arecord stopped working on newer linux...
// arecord -d 1 /dev/shm/mgmt_rec.wav 2>/dev/null
//args1 := []string{"-d", "1", "/dev/shm/mgmt_rec.wav"}
//cmd1 := exec.Command("/usr/bin/arecord", args1...)
cmd1.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
Pgid: 0,
}
// start the command
if _, err := cmd1.Output(); err != nil {
return errwrap.Wrapf(err, "cmd failed to run")
}
// sox -t .wav /dev/shm/mgmt_rec.wav -n stat 2>&1 | grep "Maximum amplitude" | cut -d ':' -f 2
args2 := []string{"-t", ".wav", "/dev/shm/mgmt_rec.wav", "-n", "stat"}
cmd2 := exec.Command("/usr/bin/sox", args2...)
cmd2.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
Pgid: 0,
}
// start the command
out, err := cmd2.CombinedOutput() // data comes on stderr
if err != nil {
return errwrap.Wrapf(err, "cmd failed to run")
}
ratio, err := extract(out)
if err != nil {
return errwrap.Wrapf(err, "failed to extract")
}
result, err := visual(obj.symbol, int(obj.multiplier), obj.peak, ratio)
if err != nil {
return errwrap.Wrapf(err, "could not generate visual")
}
if obj.result != nil && *obj.result == result {
continue // result didn't change
}
obj.result = &result // store new result
case <-ctx.Done():
return nil
}
select {
case obj.init.Output <- &types.StrValue{
V: *obj.result,
}:
case <-ctx.Done():
return nil
}
}
}
func newTicker() *time.Ticker {
return time.NewTicker(time.Duration(1) * time.Second)
}
func extract(data []byte) (float64, error) {
const prefix = "Maximum amplitude:"
str := string(data)
lines := strings.Split(str, "\n")
for _, line := range lines {
if !strings.HasPrefix(line, prefix) {
continue
}
s := strings.TrimSpace(line[len(prefix):])
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0, err
}
return f, nil
}
return 0, fmt.Errorf("could not extract any data")
}
func round(f float64) int {
return int(f + math.Copysign(0.5, f))
}
// TODO: make this fancier
func visual(symbol string, multiplier int, peak, ratio float64) (string, error) {
if ratio > 1 || ratio < 0 {
return "", fmt.Errorf("invalid ratio of %f", ratio)
}
x := strings.Repeat(symbol, round(ratio*float64(multiplier)))
if x == "" {
x += symbol // add a minimum
}
if ratio > peak {
x += " PEAK!!!"
}
return fmt.Sprintf("(%f):\n%s\n%s", ratio, x, x), nil
}

View File

@@ -1,23 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package corefmt
const (
// ModuleName is the prefix given to all the functions in this module.
ModuleName = "fmt"
)

View File

@@ -1,722 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package corefmt
import (
"context"
"fmt"
"github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/util"
"github.com/purpleidea/mgmt/util/errwrap"
)
const (
// PrintfFuncName is the name this function is registered as.
// FIXME: should this be named sprintf instead?
PrintfFuncName = "printf"
// PrintfAllowNonStaticFormat allows us to use printf when the zeroth
// argument (the format string) is not known statically at compile time.
// The downside of this is that if it changes while we are running, it
// could change from "hello %s" to "hello %d" or "%s %d...". If this
// happens we just generate ugly format strings, instead of preventing
// it all from even running at all. It's useful to allow dynamic strings
// if we were generating custom log messages (for example) where the
// format comes from a database lookup or similar. Of course if we knew
// that such a lookup could be done quickly and statically (maybe it's a
// read from a local key-value config file that's part of our deploy)
// then maybe we can do it before unification speculatively.
PrintfAllowNonStaticFormat = true
printfArgNameFormat = "format" // name of the first arg
)
func init() {
funcs.ModuleRegister(ModuleName, PrintfFuncName, func() interfaces.Func { return &PrintfFunc{} })
}
var _ interfaces.PolyFunc = &PrintfFunc{} // ensure it meets this expectation
// PrintfFunc is a static polymorphic function that compiles a format string and
// returns the output as a string. It bases its output on the values passed in
// to it. It examines the type of the arguments at compile time and then
// determines the static function signature by parsing the format string and
// using that to determine the final function signature. One consequence of this
// is that the format string must be a static string which is known at compile
// time. This is reasonable, because if it was a reactive, changing string, then
// we could expect the type signature to change, which is not allowed in our
// statically typed language.
type PrintfFunc struct {
Type *types.Type // final full type of our function
init *interfaces.Init
last types.Value // last value received to use for diff
result *string // last calculated output
}
// String returns a simple name for this function. This is needed so this struct
// can satisfy the pgraph.Vertex interface.
func (obj *PrintfFunc) String() string {
return fmt.Sprintf("%s@%p", PrintfFuncName, obj) // be more unique!
}
// ArgGen returns the Nth arg name for this function.
func (obj *PrintfFunc) ArgGen(index int) (string, error) {
if index == 0 {
return printfArgNameFormat, nil
}
// TODO: if index is big enough that it would return the string in
// `printfArgNameFormat` then we should return an error! (Nearly impossible.)
return util.NumToAlpha(index - 1), nil
}
// Unify returns the list of invariants that this func produces.
func (obj *PrintfFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) {
var invariants []interfaces.Invariant
var invar interfaces.Invariant
// func(format string, args... variant) string
formatName, err := obj.ArgGen(0)
if err != nil {
return nil, err
}
dummyFormat := &interfaces.ExprAny{} // corresponds to the format type
dummyOut := &interfaces.ExprAny{} // corresponds to the out string
// format arg type of string
invar = &interfaces.EqualsInvariant{
Expr: dummyFormat,
Type: types.TypeStr,
}
invariants = append(invariants, invar)
// return type of string
invar = &interfaces.EqualsInvariant{
Expr: dummyOut,
Type: types.TypeStr,
}
invariants = append(invariants, invar)
invar = &interfaces.EqualityInvariant{
Expr1: dummyFormat,
Expr2: dummyOut,
}
invariants = append(invariants, invar)
// dynamic generator function for when the format string is dynamic
dynamicFn := func(fnInvariants []interfaces.Invariant, solved map[interfaces.Expr]*types.Type) ([]interfaces.Invariant, error) {
for _, invariant := range fnInvariants {
// search for this special type of invariant
cfavInvar, ok := invariant.(*interfaces.CallFuncArgsValueInvariant)
if !ok {
continue
}
// did we find the mapping from us to ExprCall ?
if cfavInvar.Func != expr {
continue
}
// cfavInvar.Expr is the ExprCall! (the return pointer)
// cfavInvar.Args are the args that ExprCall uses!
if len(cfavInvar.Args) == 0 {
return nil, fmt.Errorf("unable to build function with no args")
}
var invariants []interfaces.Invariant
var invar interfaces.Invariant
// add the relationship to the format string arg
invar = &interfaces.EqualityInvariant{
Expr1: cfavInvar.Expr,
Expr2: dummyFormat,
}
invariants = append(invariants, invar)
// add the relationship to the returned value
invar = &interfaces.EqualityInvariant{
Expr1: cfavInvar.Expr,
Expr2: dummyOut,
}
invariants = append(invariants, invar)
// add the relationships to the called args
invar = &interfaces.EqualityInvariant{
Expr1: cfavInvar.Args[0],
Expr2: dummyFormat,
}
invariants = append(invariants, invar)
// first arg must be a string
invar = &interfaces.EqualsInvariant{
Expr: cfavInvar.Args[0],
Type: types.TypeStr,
}
invariants = append(invariants, invar)
// full function
mapped := make(map[string]interfaces.Expr)
ordered := []string{}
for i, x := range cfavInvar.Args {
argName, err := obj.ArgGen(i)
if err != nil {
return nil, err
}
dummyArg := &interfaces.ExprAny{}
if i == 0 {
dummyArg = dummyFormat // use parent one
}
invar = &interfaces.EqualityInvariant{
Expr1: x, // cfavInvar.Args[i]
Expr2: dummyArg,
}
invariants = append(invariants, invar)
mapped[argName] = dummyArg
ordered = append(ordered, argName)
}
invar = &interfaces.EqualityWrapFuncInvariant{
Expr1: expr, // maps directly to us!
Expr2Map: mapped,
Expr2Ord: ordered,
Expr2Out: dummyOut,
}
invariants = append(invariants, invar)
// TODO: do we return this relationship with ExprCall?
invar = &interfaces.EqualityWrapCallInvariant{
// TODO: should Expr1 and Expr2 be reversed???
Expr1: cfavInvar.Expr,
//Expr2Func: cfavInvar.Func, // same as below
Expr2Func: expr,
}
invariants = append(invariants, invar)
// TODO: are there any other invariants we should build?
return invariants, nil // generator return
}
// We couldn't tell the solver anything it didn't already know!
return nil, fmt.Errorf("couldn't generate new invariants")
}
// generator function
fn := func(fnInvariants []interfaces.Invariant, solved map[interfaces.Expr]*types.Type) ([]interfaces.Invariant, error) {
for _, invariant := range fnInvariants {
// search for this special type of invariant
cfavInvar, ok := invariant.(*interfaces.CallFuncArgsValueInvariant)
if !ok {
continue
}
// did we find the mapping from us to ExprCall ?
if cfavInvar.Func != expr {
continue
}
// cfavInvar.Expr is the ExprCall! (the return pointer)
// cfavInvar.Args are the args that ExprCall uses!
if len(cfavInvar.Args) == 0 {
return nil, fmt.Errorf("unable to build function with no args")
}
var invariants []interfaces.Invariant
var invar interfaces.Invariant
// add the relationship to the format string arg
invar = &interfaces.EqualityInvariant{
Expr1: cfavInvar.Expr,
Expr2: dummyFormat,
}
invariants = append(invariants, invar)
// add the relationship to the returned value
invar = &interfaces.EqualityInvariant{
Expr1: cfavInvar.Expr,
Expr2: dummyOut,
}
invariants = append(invariants, invar)
// add the relationships to the called args
invar = &interfaces.EqualityInvariant{
Expr1: cfavInvar.Args[0],
Expr2: dummyFormat,
}
invariants = append(invariants, invar)
// first arg must be a string
invar = &interfaces.EqualsInvariant{
Expr: cfavInvar.Args[0],
Type: types.TypeStr,
}
invariants = append(invariants, invar)
// Here we try to see if we know the format string
// statically. Perhaps more future Value() invocations
// on simple functions will also return before
// unification and before the function engine runs.
// If we happen to know the value, that's great and we
// can unify very easily. If we don't, then we can
// decide if we want to allow dynamic format strings.
value, err := cfavInvar.Args[0].Value() // is it known?
if err != nil {
if PrintfAllowNonStaticFormat {
return dynamicFn(fnInvariants, solved)
}
return nil, fmt.Errorf("format string is not known statically")
}
if k := value.Type().Kind; k != types.KindStr {
return nil, fmt.Errorf("unable to build function with 0th arg of kind: %s", k)
}
format := value.Str() // must not panic
typList, err := parseFormatToTypeList(format)
if err != nil {
return nil, errwrap.Wrapf(err, "could not parse format string")
}
// full function
mapped := make(map[string]interfaces.Expr)
ordered := []string{formatName}
mapped[formatName] = dummyFormat
for i, x := range typList {
argName, err := obj.ArgGen(i + 1) // skip 0th
if err != nil {
return nil, err
}
if argName == printfArgNameFormat {
return nil, fmt.Errorf("could not build function with %d args", i+1) // +1 for format arg
}
dummyArg := &interfaces.ExprAny{}
// if it's a variant, we can't add the invariant
if x != types.TypeVariant {
invar = &interfaces.EqualsInvariant{
Expr: dummyArg,
Type: x,
}
invariants = append(invariants, invar)
}
// catch situations like `printf("%d%d", 42)`
if len(cfavInvar.Args) <= i+1 {
return nil, fmt.Errorf("more specifiers (%d) than values (%d)", len(typList), len(cfavInvar.Args)-1)
}
// add the relationships to the called args
invar = &interfaces.EqualityInvariant{
Expr1: cfavInvar.Args[i+1],
Expr2: dummyArg,
}
invariants = append(invariants, invar)
mapped[argName] = dummyArg
ordered = append(ordered, argName)
}
invar = &interfaces.EqualityWrapFuncInvariant{
Expr1: expr, // maps directly to us!
Expr2Map: mapped,
Expr2Ord: ordered,
Expr2Out: dummyOut,
}
invariants = append(invariants, invar)
// TODO: do we return this relationship with ExprCall?
invar = &interfaces.EqualityWrapCallInvariant{
// TODO: should Expr1 and Expr2 be reversed???
Expr1: cfavInvar.Expr,
//Expr2Func: cfavInvar.Func, // same as below
Expr2Func: expr,
}
invariants = append(invariants, invar)
// TODO: are there any other invariants we should build?
return invariants, nil // generator return
}
// We couldn't tell the solver anything it didn't already know!
return nil, fmt.Errorf("couldn't generate new invariants")
}
invar = &interfaces.GeneratorInvariant{
Func: fn,
}
invariants = append(invariants, invar)
return invariants, nil
}
// Polymorphisms returns the possible type signature for this function. In this
// case, since the number of arguments can be infinite, it returns the final
// precise type if it can be gleamed from the format argument. If it cannot, it
// is because either the format argument was not known statically, or because it
// had an invalid format string.
// XXX: This version of the function does not handle any variants returned from
// the parseFormatToTypeList helper function.
func (obj *PrintfFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) {
if partialType == nil || len(partialValues) < 1 {
return nil, fmt.Errorf("first argument must be a static format string")
}
if partialType.Out != nil && partialType.Out.Cmp(types.TypeStr) != nil {
return nil, fmt.Errorf("return value of printf must be str")
}
ord := partialType.Ord
if partialType.Map != nil {
if len(ord) < 1 {
return nil, fmt.Errorf("must have at least one arg in printf func")
}
if t, exists := partialType.Map[ord[0]]; exists && t != nil {
if t.Cmp(types.TypeStr) != nil {
return nil, fmt.Errorf("first arg for printf must be an str")
}
}
}
// FIXME: we'd like to pre-compute the interpolation if we can, so that
// we can run this code properly... for now, we can't, so it's a compile
// time error...
if partialValues[0] == nil {
return nil, fmt.Errorf("could not determine type from format string")
}
format := partialValues[0].Str() // must not panic
typList, err := parseFormatToTypeList(format)
if err != nil {
return nil, errwrap.Wrapf(err, "could not parse format string")
}
typ := &types.Type{
Kind: types.KindFunc, // function type
Map: make(map[string]*types.Type),
Ord: []string{},
Out: types.TypeStr,
}
// add first arg
typ.Map[printfArgNameFormat] = types.TypeStr
typ.Ord = append(typ.Ord, printfArgNameFormat)
for i, x := range typList {
name := util.NumToAlpha(i) // start with a...
if name == printfArgNameFormat {
return nil, fmt.Errorf("could not build function with %d args", i+1) // +1 for format arg
}
// if we also had even more partial type information, check it!
if t, exists := partialType.Map[ord[i+1]]; exists && t != nil {
if err := t.Cmp(x); err != nil {
return nil, errwrap.Wrapf(err, "arg %d does not match expected type", i+1)
}
}
typ.Map[name] = x
typ.Ord = append(typ.Ord, name)
}
return []*types.Type{typ}, nil // return a list with a single possibility
}
// Build takes the now known function signature and stores it so that this
// function can appear to be static. That type is used to build our function
// statically.
func (obj *PrintfFunc) Build(typ *types.Type) (*types.Type, error) {
if typ.Kind != types.KindFunc {
return nil, fmt.Errorf("input type must be of kind func")
}
if len(typ.Ord) < 1 {
return nil, fmt.Errorf("the printf function needs at least one arg")
}
if typ.Out == nil {
return nil, fmt.Errorf("return type of function must be specified")
}
if typ.Out.Cmp(types.TypeStr) != nil {
return nil, fmt.Errorf("return type of function must be an str")
}
if typ.Map == nil {
return nil, fmt.Errorf("invalid input type")
}
t0, exists := typ.Map[typ.Ord[0]]
if !exists || t0 == nil {
return nil, fmt.Errorf("first arg must be specified")
}
if t0.Cmp(types.TypeStr) != nil {
return nil, fmt.Errorf("first arg for printf must be an str")
}
//newTyp := typ.Copy()
newTyp := &types.Type{
Kind: typ.Kind, // copy
Map: make(map[string]*types.Type), // new
Ord: []string{}, // new
Out: typ.Out, // copy
}
for i, x := range typ.Ord { // remap arg names
argName, err := obj.ArgGen(i)
if err != nil {
return nil, err
}
newTyp.Map[argName] = typ.Map[x]
newTyp.Ord = append(newTyp.Ord, argName)
}
obj.Type = newTyp // function type is now known!
return obj.Type, nil
}
// Validate makes sure we've built our struct properly. It is usually unused for
// normal functions that users can use directly.
func (obj *PrintfFunc) Validate() error {
if obj.Type == nil { // build must be run first
return fmt.Errorf("type is still unspecified")
}
return nil
}
// Info returns some static info about itself.
func (obj *PrintfFunc) Info() *interfaces.Info {
return &interfaces.Info{
Pure: true,
Memo: false,
Sig: obj.Type,
Err: obj.Validate(),
}
}
// Init runs some startup code for this function.
func (obj *PrintfFunc) Init(init *interfaces.Init) error {
obj.init = init
return nil
}
// Stream returns the changing values that this func has over time.
func (obj *PrintfFunc) Stream(ctx context.Context) error {
defer close(obj.init.Output) // the sender closes
for {
select {
case input, ok := <-obj.init.Input:
if !ok {
return nil // can't output any more
}
//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
format := input.Struct()[printfArgNameFormat].Str()
values := []types.Value{}
for _, name := range obj.Type.Ord {
if name == printfArgNameFormat { // skip format arg
continue
}
x := input.Struct()[name]
values = append(values, x)
}
result, err := compileFormatToString(format, values)
if err != nil {
return err // no errwrap needed b/c helper func
}
if obj.result != nil && *obj.result == result {
continue // result didn't change
}
obj.result = &result // store new result
case <-ctx.Done():
return nil
}
select {
case obj.init.Output <- &types.StrValue{
V: *obj.result,
}:
case <-ctx.Done():
return nil
}
}
}
// valueToString prints our values how we expect for printf.
// FIXME: if this turns out to be useful, add it to the types package.
func valueToString(value types.Value) string {
switch x := value.Type().Kind; x {
// FIXME: floats don't print nicely: https://github.com/golang/go/issues/46118
case types.KindFloat:
// TODO: use formatting flags ?
// FIXME: Our String() method in FloatValue doesn't print nicely
return value.String()
}
// FIXME: this is just an "easy-out" implementation for now...
return fmt.Sprintf("%v", value.Value())
//switch x := value.Type().Kind; x {
//case types.KindBool:
// return value.String()
//case types.KindStr:
// return value.Str() // use this since otherwise it adds " & "
//case types.KindInt:
// return value.String()
//case types.KindFloat:
// // TODO: use formatting flags ?
// return value.String()
//}
//panic("unhandled type") // TODO: not fully implemented yet
}
// parseFormatToTypeList takes a format string and returns a list of types that
// it expects to use in the order found in the format string. This can also
// handle the %v special variant type in the format string.
// FIXME: add support for more types, and add tests!
func parseFormatToTypeList(format string) ([]*types.Type, error) {
typList := []*types.Type{}
inType := false
for i := 0; i < len(format); i++ {
// some normal char...
if !inType && format[i] != '%' {
continue
}
// in a type or we're a %
if format[i] == '%' {
if inType {
// it's a %%
inType = false
} else {
// start looking for type specification!
inType = true
}
continue
}
// we must be in a type
switch format[i] {
case 't':
typList = append(typList, types.TypeBool)
case 's':
typList = append(typList, types.TypeStr)
case 'd':
typList = append(typList, types.TypeInt)
// TODO: parse fancy formats like %0.2f and stuff
case 'f':
typList = append(typList, types.TypeFloat)
// FIXME: add fancy types like: %[]s, %[]f, %{s:f}, etc...
// special!
case 'v':
typList = append(typList, types.TypeVariant)
default:
return nil, fmt.Errorf("invalid format string at %d", i)
}
inType = false // done
}
return typList, nil
}
// compileFormatToString takes a format string and a list of values and returns
// the compiled/templated output. This can also handle the %v special variant
// type in the format string. Of course the corresponding value to those %v
// entries must have a static, fixed, precise type.
// FIXME: add support for more types, and add tests!
// XXX: depending on PrintfAllowNonStaticFormat, we should NOT error if we have
// a mismatch between the format string and the available args. Return similar
// to golang's EXTRA/MISSING, eg: https://pkg.go.dev/fmt#hdr-Format_errors
func compileFormatToString(format string, values []types.Value) (string, error) {
output := ""
ix := 0
inType := false
for i := 0; i < len(format); i++ {
// some normal char...
if !inType && format[i] != '%' {
output += string(format[i])
continue
}
// in a type or we're a %
if format[i] == '%' {
if inType {
// it's a %%
output += string(format[i])
inType = false
} else {
// start looking for type specification!
inType = true
}
continue
}
// we must be in a type
var typ *types.Type
switch format[i] {
case 't':
typ = types.TypeBool
case 's':
typ = types.TypeStr
case 'd':
typ = types.TypeInt
// TODO: parse fancy formats like %0.2f and stuff
case 'f':
typ = types.TypeFloat
// FIXME: add fancy types like: %[]s, %[]f, %{s:f}, etc...
case 'v':
typ = types.TypeVariant
default:
return "", fmt.Errorf("invalid format string at %d", i)
}
inType = false // done
if ix >= len(values) {
// programming error, probably in type unification
return "", fmt.Errorf("more specifiers (%d) than values (%d)", ix+1, len(values))
}
// check the type (if not a variant) matches what we have...
if typ == types.TypeVariant {
if values[ix].Type() == nil {
return "", fmt.Errorf("unexpected nil type")
}
} else if err := typ.Cmp(values[ix].Type()); err != nil {
return "", errwrap.Wrapf(err, "unexpected type")
}
output += valueToString(values[ix])
ix++ // consume one value
}
return output, nil
}

View File

@@ -1,16 +0,0 @@
# This file is used by github.com/purpleidea/mgmt/lang/funcs/funcgen/ to
# generate mcl functions.
packages:
- name: html
- name: math
- name: math/rand
alias: rand
- name: os
- name: os/exec
alias: exec
- name: path
- name: path/filepath
alias: filepath
- name: runtime
- name: strconv
- name: strings

View File

@@ -1,23 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreiter
const (
// ModuleName is the prefix given to all the functions in this module.
ModuleName = "iter"
)

View File

@@ -1,811 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreiter
import (
"context"
"fmt"
"github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/funcs/structs"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/lang/types/full"
"github.com/purpleidea/mgmt/util"
"github.com/purpleidea/mgmt/util/errwrap"
)
const (
// MapFuncName is the name this function is registered as.
MapFuncName = "map"
// arg names...
mapArgNameInputs = "inputs"
mapArgNameFunction = "function"
)
func init() {
funcs.ModuleRegister(ModuleName, MapFuncName, func() interfaces.Func { return &MapFunc{} }) // must register the func and name
}
var _ interfaces.PolyFunc = &MapFunc{} // ensure it meets this expectation
// MapFunc is the standard map iterator function that applies a function to each
// element in a list. It returns a list with the same number of elements as the
// input list. There is no requirement that the element output type be the same
// as the input element type. This implements the signature: `func(inputs []T1,
// function func(T1) T2) []T2` instead of the alternate with the two input args
// swapped, because while the latter is more common with languages that support
// partial function application, the former variant that we implemented is much
// more readable when using an inline lambda.
// TODO: should we extend this to support iterating over map's and structs, or
// should that be a different function? I think a different function is best.
type MapFunc struct {
Type *types.Type // this is the type of the elements in our input list
RType *types.Type // this is the type of the elements in our output list
init *interfaces.Init
last types.Value // last value received to use for diff
lastFuncValue *full.FuncValue // remember the last function value
lastInputListLength int // remember the last input list length
inputListType *types.Type
outputListType *types.Type
// outputChan is an initially-nil channel from which we receive output
// lists from the subgraph. This channel is reset when the subgraph is
// recreated.
outputChan chan types.Value
}
// String returns a simple name for this function. This is needed so this struct
// can satisfy the pgraph.Vertex interface.
func (obj *MapFunc) String() string {
return MapFuncName
}
// ArgGen returns the Nth arg name for this function.
func (obj *MapFunc) ArgGen(index int) (string, error) {
seq := []string{mapArgNameInputs, mapArgNameFunction} // inverted for pretty!
if l := len(seq); index >= l {
return "", fmt.Errorf("index %d exceeds arg length of %d", index, l)
}
return seq[index], nil
}
// Unify returns the list of invariants that this func produces.
func (obj *MapFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) {
var invariants []interfaces.Invariant
var invar interfaces.Invariant
// func(inputs []T1, function func(T1) T2) []T2
inputsName, err := obj.ArgGen(0)
if err != nil {
return nil, err
}
functionName, err := obj.ArgGen(1)
if err != nil {
return nil, err
}
dummyArgList := &interfaces.ExprAny{} // corresponds to the input list
dummyArgFunc := &interfaces.ExprAny{} // corresponds to the input func
dummyOutList := &interfaces.ExprAny{} // corresponds to the output list
t1Expr := &interfaces.ExprAny{} // corresponds to the t1 type
t2Expr := &interfaces.ExprAny{} // corresponds to the t2 type
invar = &interfaces.EqualityWrapListInvariant{
Expr1: dummyArgList,
Expr2Val: t1Expr,
}
invariants = append(invariants, invar)
invar = &interfaces.EqualityWrapListInvariant{
Expr1: dummyOutList,
Expr2Val: t2Expr,
}
invariants = append(invariants, invar)
// full function
mapped := make(map[string]interfaces.Expr)
ordered := []string{inputsName, functionName}
mapped[inputsName] = dummyArgList
mapped[functionName] = dummyArgFunc
invar = &interfaces.EqualityWrapFuncInvariant{
Expr1: expr, // maps directly to us!
Expr2Map: mapped,
Expr2Ord: ordered,
Expr2Out: dummyOutList,
}
invariants = append(invariants, invar)
// relationship between t1 and t2
argName := util.NumToAlpha(0) // XXX: does the arg name matter?
invar = &interfaces.EqualityWrapFuncInvariant{
Expr1: dummyArgFunc,
Expr2Map: map[string]interfaces.Expr{
argName: t1Expr,
},
Expr2Ord: []string{argName},
Expr2Out: t2Expr,
}
invariants = append(invariants, invar)
// generator function
fn := func(fnInvariants []interfaces.Invariant, solved map[interfaces.Expr]*types.Type) ([]interfaces.Invariant, error) {
for _, invariant := range fnInvariants {
// search for this special type of invariant
cfavInvar, ok := invariant.(*interfaces.CallFuncArgsValueInvariant)
if !ok {
continue
}
// did we find the mapping from us to ExprCall ?
if cfavInvar.Func != expr {
continue
}
// cfavInvar.Expr is the ExprCall! (the return pointer)
// cfavInvar.Args are the args that ExprCall uses!
if l := len(cfavInvar.Args); l != 2 {
return nil, fmt.Errorf("unable to build function with %d args", l)
}
// we must have exactly two args
var invariants []interfaces.Invariant
var invar interfaces.Invariant
// add the relationship to the returned value
invar = &interfaces.EqualityInvariant{
Expr1: cfavInvar.Expr,
Expr2: dummyOutList,
}
invariants = append(invariants, invar)
// add the relationships to the called args
invar = &interfaces.EqualityInvariant{
Expr1: cfavInvar.Args[0],
Expr2: dummyArgList,
}
invariants = append(invariants, invar)
invar = &interfaces.EqualityInvariant{
Expr1: cfavInvar.Args[1],
Expr2: dummyArgFunc,
}
invariants = append(invariants, invar)
invar = &interfaces.EqualityWrapListInvariant{
Expr1: cfavInvar.Args[0],
Expr2Val: t1Expr,
}
invariants = append(invariants, invar)
invar = &interfaces.EqualityWrapListInvariant{
Expr1: cfavInvar.Expr,
Expr2Val: t2Expr,
}
invariants = append(invariants, invar)
var t1, t2 *types.Type // as seen in our sig's
var foundArgName string = util.NumToAlpha(0) // XXX: is this a hack?
// validateArg0 checks: inputs []T1
validateArg0 := func(typ *types.Type) error {
if typ == nil { // unknown so far
return nil
}
if typ.Kind != types.KindList {
return fmt.Errorf("input type must be of kind list")
}
if typ.Val == nil { // TODO: is this okay to add?
return nil // unknown so far
}
if t1 == nil { // t1 is not yet known, so done!
t1 = typ.Val // learn!
return nil
}
//if err := typ.Val.Cmp(t1); err != nil {
// return errwrap.Wrapf(err, "input type was inconsistent")
//}
//return nil
return errwrap.Wrapf(typ.Val.Cmp(t1), "input type was inconsistent")
}
// validateArg1 checks: func(T1) T2
validateArg1 := func(typ *types.Type) error {
if typ == nil { // unknown so far
return nil
}
if typ.Kind != types.KindFunc {
return fmt.Errorf("input type must be of kind func")
}
if len(typ.Map) != 1 || len(typ.Ord) != 1 {
return fmt.Errorf("input type func must have only one input arg")
}
arg, exists := typ.Map[typ.Ord[0]]
if !exists {
// programming error
return fmt.Errorf("input type func first arg is missing")
}
if t1 != nil {
if err := arg.Cmp(t1); err != nil {
return errwrap.Wrapf(err, "input type func arg was inconsistent")
}
}
if t2 != nil {
if err := typ.Out.Cmp(t2); err != nil {
return errwrap.Wrapf(err, "input type func output was inconsistent")
}
}
// in case they weren't set already
t1 = arg
t2 = typ.Out
foundArgName = typ.Ord[0] // we found a name!
return nil
}
if typ, err := cfavInvar.Args[0].Type(); err == nil { // is it known?
// this sets t1 and t2 on success if it learned
if err := validateArg0(typ); err != nil {
return nil, errwrap.Wrapf(err, "first input arg type is inconsistent")
}
}
if typ, exists := solved[cfavInvar.Args[0]]; exists { // alternate way to lookup type
// this sets t1 and t2 on success if it learned
if err := validateArg0(typ); err != nil {
return nil, errwrap.Wrapf(err, "first input arg type is inconsistent")
}
}
// XXX: since we might not yet have association to this
// expression (dummyArgList) yet, we could consider
// returning some of the invariants and a new generator
// and hoping we get a hit on this one the next time.
if typ, exists := solved[dummyArgList]; exists { // alternate way to lookup type
// this sets t1 and t2 on success if it learned
if err := validateArg0(typ); err != nil {
return nil, errwrap.Wrapf(err, "first input arg type is inconsistent")
}
}
if typ, err := cfavInvar.Args[1].Type(); err == nil { // is it known?
// this sets t1 and t2 on success if it learned
if err := validateArg1(typ); err != nil {
return nil, errwrap.Wrapf(err, "second input arg type is inconsistent")
}
}
if typ, exists := solved[cfavInvar.Args[1]]; exists { // alternate way to lookup type
// this sets t1 and t2 on success if it learned
if err := validateArg1(typ); err != nil {
return nil, errwrap.Wrapf(err, "second input arg type is inconsistent")
}
}
// XXX: since we might not yet have association to this
// expression (dummyArgFunc) yet, we could consider
// returning some of the invariants and a new generator
// and hoping we get a hit on this one the next time.
if typ, exists := solved[dummyArgFunc]; exists { // alternate way to lookup type
// this sets t1 and t2 on success if it learned
if err := validateArg1(typ); err != nil {
return nil, errwrap.Wrapf(err, "second input arg type is inconsistent")
}
}
// XXX: look for t1 and t2 in other places?
if t1 != nil {
invar = &interfaces.EqualsInvariant{
Expr: t1Expr,
Type: t1,
}
invariants = append(invariants, invar)
}
if t1 != nil && t2 != nil {
// TODO: if the argName matters, do it here...
_ = foundArgName
//argName := foundArgName // XXX: is this a hack?
//mapped := make(map[string]interfaces.Expr)
//ordered := []string{argName}
//mapped[argName] = t1Expr
//invar = &interfaces.EqualityWrapFuncInvariant{
// Expr1: dummyArgFunc,
// Expr2Map: mapped,
// Expr2Ord: ordered,
// Expr2Out: t2Expr,
//}
//invariants = append(invariants, invar)
}
// note, currently, we can't learn t2 without t1
if t2 != nil {
invar = &interfaces.EqualsInvariant{
Expr: t2Expr,
Type: t2,
}
invariants = append(invariants, invar)
}
// We need to require this knowledge to continue!
if t1 == nil || t2 == nil {
return nil, fmt.Errorf("not enough known about function signature")
}
// TODO: do we return this relationship with ExprCall?
invar = &interfaces.EqualityWrapCallInvariant{
// TODO: should Expr1 and Expr2 be reversed???
Expr1: cfavInvar.Expr,
//Expr2Func: cfavInvar.Func, // same as below
Expr2Func: expr,
}
invariants = append(invariants, invar)
// TODO: are there any other invariants we should build?
return invariants, nil // generator return
}
// We couldn't tell the solver anything it didn't already know!
return nil, fmt.Errorf("couldn't generate new invariants")
}
invar = &interfaces.GeneratorInvariant{
Func: fn,
}
invariants = append(invariants, invar)
return invariants, nil
}
// Polymorphisms returns the list of possible function signatures available for
// this static polymorphic function. It relies on type and value hints to limit
// the number of returned possibilities.
func (obj *MapFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) {
// XXX: double check that this works with `func([]int, func(int) str) []str` (when types change!)
// TODO: look at partialValues to gleam type information?
if partialType == nil {
return nil, fmt.Errorf("zero type information given")
}
if partialType.Kind != types.KindFunc {
return nil, fmt.Errorf("partial type must be of kind func")
}
// If we figure out both of these two types, we'll know the full type...
var t1 *types.Type // type
var t2 *types.Type // rtype
// Look at the returned "out" type if it's known.
if tOut := partialType.Out; tOut != nil {
if tOut.Kind != types.KindList {
return nil, fmt.Errorf("partial out type must be of kind list")
}
t2 = tOut.Val // found (if not nil)
}
ord := partialType.Ord
if partialType.Map != nil {
// TODO: is it okay to assume this?
//if len(ord) == 0 {
// return nil, fmt.Errorf("must have two args in func")
//}
if len(ord) != 2 {
return nil, fmt.Errorf("must have two args in func")
}
if tInputs, exists := partialType.Map[ord[0]]; exists && tInputs != nil {
if tInputs.Kind != types.KindList {
return nil, fmt.Errorf("first input arg must be of kind list")
}
t1 = tInputs.Val // found (if not nil)
}
if tFunction, exists := partialType.Map[ord[1]]; exists && tFunction != nil {
if tFunction.Kind != types.KindFunc {
return nil, fmt.Errorf("second input arg must be a func")
}
fOrd := tFunction.Ord
if fMap := tFunction.Map; fMap != nil {
if len(fOrd) != 1 {
return nil, fmt.Errorf("second input arg func, must have only one arg")
}
if fIn, exists := fMap[fOrd[0]]; exists && fIn != nil {
if err := fIn.Cmp(t1); t1 != nil && err != nil {
return nil, errwrap.Wrapf(err, "first arg function in type is inconsistent")
}
t1 = fIn // found
}
}
if fOut := tFunction.Out; fOut != nil {
if err := fOut.Cmp(t2); t2 != nil && err != nil {
return nil, errwrap.Wrapf(err, "second arg function out type is inconsistent")
}
t2 = fOut // found
}
}
}
if t1 == nil || t2 == nil {
return nil, fmt.Errorf("not enough type information given")
}
tI := types.NewType(fmt.Sprintf("[]%s", t1.String())) // in
tO := types.NewType(fmt.Sprintf("[]%s", t2.String())) // out
tF := types.NewType(fmt.Sprintf("func(%s) %s", t1.String(), t2.String()))
s := fmt.Sprintf("func(%s %s, %s %s) %s", mapArgNameInputs, tI, mapArgNameFunction, tF, tO)
typ := types.NewType(s) // yay!
// TODO: type check that the partialValues are compatible
return []*types.Type{typ}, nil // solved!
}
// Build is run to turn the polymorphic, undetermined function, into the
// specific statically typed version. It is usually run after Unify completes,
// and must be run before Info() and any of the other Func interface methods are
// used. This function is idempotent, as long as the arg isn't changed between
// runs.
func (obj *MapFunc) Build(typ *types.Type) (*types.Type, error) {
// typ is the KindFunc signature we're trying to build...
if typ.Kind != types.KindFunc {
return nil, fmt.Errorf("input type must be of kind func")
}
if len(typ.Ord) != 2 {
return nil, fmt.Errorf("the map needs exactly two args")
}
if typ.Map == nil {
return nil, fmt.Errorf("the map is nil")
}
tInputs, exists := typ.Map[typ.Ord[0]]
if !exists || tInputs == nil {
return nil, fmt.Errorf("first argument was missing")
}
tFunction, exists := typ.Map[typ.Ord[1]]
if !exists || tFunction == nil {
return nil, fmt.Errorf("second argument was missing")
}
if tInputs.Kind != types.KindList {
return nil, fmt.Errorf("first argument must be of kind list")
}
if tFunction.Kind != types.KindFunc {
return nil, fmt.Errorf("second argument must be of kind func")
}
if typ.Out == nil {
return nil, fmt.Errorf("return type must be specified")
}
if typ.Out.Kind != types.KindList {
return nil, fmt.Errorf("return argument must be a list")
}
if len(tFunction.Ord) != 1 {
return nil, fmt.Errorf("the functions map needs exactly one arg")
}
if tFunction.Map == nil {
return nil, fmt.Errorf("the functions map is nil")
}
tArg, exists := tFunction.Map[tFunction.Ord[0]]
if !exists || tArg == nil {
return nil, fmt.Errorf("the functions first argument was missing")
}
if err := tArg.Cmp(tInputs.Val); err != nil {
return nil, errwrap.Wrapf(err, "the functions arg type must match the input list contents type")
}
if tFunction.Out == nil {
return nil, fmt.Errorf("return type of function must be specified")
}
if err := tFunction.Out.Cmp(typ.Out.Val); err != nil {
return nil, errwrap.Wrapf(err, "return type of function must match returned list contents type")
}
obj.Type = tInputs.Val // or tArg
obj.RType = tFunction.Out // or typ.Out.Val
return obj.sig(), nil
}
// Validate tells us if the input struct takes a valid form.
func (obj *MapFunc) Validate() error {
if obj.Type == nil || obj.RType == nil {
return fmt.Errorf("type is not yet known")
}
return nil
}
// Info returns some static info about itself. Build must be called before this
// will return correct data.
func (obj *MapFunc) Info() *interfaces.Info {
sig := obj.sig() // helper
return &interfaces.Info{
Pure: false, // TODO: what if the input function isn't pure?
Memo: false,
Sig: sig,
Err: obj.Validate(),
}
}
// helper
func (obj *MapFunc) sig() *types.Type {
// TODO: what do we put if this is unknown?
tIi := types.TypeVariant
if obj.Type != nil {
tIi = obj.Type
}
tI := types.NewType(fmt.Sprintf("[]%s", tIi.String())) // type of 2nd arg
tOi := types.TypeVariant
if obj.RType != nil {
tOi = obj.RType
}
tO := types.NewType(fmt.Sprintf("[]%s", tOi.String())) // return type
// type of 1st arg (the function)
tF := types.NewType(fmt.Sprintf("func(%s %s) %s", "name-which-can-vary-over-time", tIi.String(), tOi.String()))
s := fmt.Sprintf("func(%s %s, %s %s) %s", mapArgNameInputs, tI, mapArgNameFunction, tF, tO)
return types.NewType(s) // yay!
}
// Init runs some startup code for this function.
func (obj *MapFunc) Init(init *interfaces.Init) error {
obj.init = init
obj.lastFuncValue = nil
obj.lastInputListLength = -1
obj.inputListType = types.NewType(fmt.Sprintf("[]%s", obj.Type))
obj.outputListType = types.NewType(fmt.Sprintf("[]%s", obj.RType))
return nil
}
// Stream returns the changing values that this func has over time.
func (obj *MapFunc) Stream(ctx context.Context) error {
// Every time the FuncValue or the length of the list changes, recreate the
// subgraph, by calling the FuncValue N times on N nodes, each of which
// extracts one of the N values in the list.
defer close(obj.init.Output) // the sender closes
// A Func to send input lists to the subgraph. The Txn.Erase() call ensures
// that this Func is not removed when the subgraph is recreated, so that the
// function graph can propagate the last list we received to the subgraph.
inputChan := make(chan types.Value)
subgraphInput := &structs.ChannelBasedSourceFunc{
Name: "subgraphInput",
Source: obj,
Chan: inputChan,
Type: obj.inputListType,
}
obj.init.Txn.AddVertex(subgraphInput)
if err := obj.init.Txn.Commit(); err != nil {
return errwrap.Wrapf(err, "commit error in Stream")
}
obj.init.Txn.Erase() // prevent the next Reverse() from removing subgraphInput
defer func() {
close(inputChan)
obj.init.Txn.Reverse()
obj.init.Txn.DeleteVertex(subgraphInput)
obj.init.Txn.Commit()
}()
obj.outputChan = nil
canReceiveMoreFuncValuesOrInputLists := true
canReceiveMoreOutputLists := true
for {
if !canReceiveMoreFuncValuesOrInputLists && !canReceiveMoreOutputLists {
//break
return nil
}
select {
case input, ok := <-obj.init.Input:
if !ok {
obj.init.Input = nil // block looping back here
canReceiveMoreFuncValuesOrInputLists = false
continue
}
if obj.last != nil && input.Cmp(obj.last) == nil {
continue // value didn't change, skip it
}
obj.last = input // store for next
value, exists := input.Struct()[mapArgNameFunction]
if !exists {
return fmt.Errorf("programming error, can't find edge")
}
newFuncValue, ok := value.(*full.FuncValue)
if !ok {
return fmt.Errorf("programming error, can't convert to *FuncValue")
}
newInputList, exists := input.Struct()[mapArgNameInputs]
if !exists {
return fmt.Errorf("programming error, can't find edge")
}
// If we have a new function or the length of the input
// list has changed, then we need to replace the
// subgraph with a new one that uses the new function
// the correct number of times.
// It's important to have this compare step to avoid
// redundant graph replacements which slow things down,
// but also cause the engine to lock, which can preempt
// the process scheduler, which can cause duplicate or
// unnecessary re-sending of values here, which causes
// the whole process to repeat ad-nauseum.
n := len(newInputList.List())
if newFuncValue != obj.lastFuncValue || n != obj.lastInputListLength {
obj.lastFuncValue = newFuncValue
obj.lastInputListLength = n
// replaceSubGraph uses the above two values
if err := obj.replaceSubGraph(subgraphInput); err != nil {
return errwrap.Wrapf(err, "could not replace subgraph")
}
canReceiveMoreOutputLists = true
}
// send the new input list to the subgraph
select {
case inputChan <- newInputList:
case <-ctx.Done():
return nil
}
case outputList, ok := <-obj.outputChan:
// send the new output list downstream
if !ok {
obj.outputChan = nil
canReceiveMoreOutputLists = false
continue
}
select {
case obj.init.Output <- outputList:
case <-ctx.Done():
return nil
}
case <-ctx.Done():
return nil
}
}
}
func (obj *MapFunc) replaceSubGraph(subgraphInput interfaces.Func) error {
// Create a subgraph which splits the input list into 'n' nodes, applies
// 'newFuncValue' to each, then combines the 'n' outputs back into a list.
//
// Here is what the subgraph looks like:
//
// digraph {
// "subgraphInput" -> "inputElemFunc0"
// "subgraphInput" -> "inputElemFunc1"
// "subgraphInput" -> "inputElemFunc2"
//
// "inputElemFunc0" -> "outputElemFunc0"
// "inputElemFunc1" -> "outputElemFunc1"
// "inputElemFunc2" -> "outputElemFunc2"
//
// "outputElemFunc0" -> "outputListFunc"
// "outputElemFunc1" -> "outputListFunc"
// "outputElemFunc1" -> "outputListFunc"
//
// "outputListFunc" -> "subgraphOutput"
// }
const channelBasedSinkFuncArgNameEdgeName = structs.ChannelBasedSinkFuncArgName // XXX: not sure if the specific name matters.
// delete the old subgraph
if err := obj.init.Txn.Reverse(); err != nil {
return errwrap.Wrapf(err, "could not Reverse")
}
// create the new subgraph
obj.outputChan = make(chan types.Value)
subgraphOutput := &structs.ChannelBasedSinkFunc{
Name: "subgraphOutput",
Target: obj,
EdgeName: channelBasedSinkFuncArgNameEdgeName,
Chan: obj.outputChan,
Type: obj.outputListType,
}
obj.init.Txn.AddVertex(subgraphOutput)
m := make(map[string]*types.Type)
ord := []string{}
for i := 0; i < obj.lastInputListLength; i++ {
argName := fmt.Sprintf("outputElem%d", i)
m[argName] = obj.RType
ord = append(ord, argName)
}
typ := &types.Type{
Kind: types.KindFunc,
Map: m,
Ord: ord,
Out: obj.outputListType,
}
outputListFunc := structs.SimpleFnToDirectFunc(
"mapOutputList",
&types.FuncValue{
V: func(args []types.Value) (types.Value, error) {
listValue := &types.ListValue{
V: args,
T: obj.outputListType,
}
return listValue, nil
},
T: typ,
},
)
obj.init.Txn.AddVertex(outputListFunc)
obj.init.Txn.AddEdge(outputListFunc, subgraphOutput, &interfaces.FuncEdge{
Args: []string{channelBasedSinkFuncArgNameEdgeName},
})
for i := 0; i < obj.lastInputListLength; i++ {
i := i
inputElemFunc := structs.SimpleFnToDirectFunc(
fmt.Sprintf("mapInputElem[%d]", i),
&types.FuncValue{
V: func(args []types.Value) (types.Value, error) {
if len(args) != 1 {
return nil, fmt.Errorf("inputElemFunc: expected a single argument")
}
arg := args[0]
list, ok := arg.(*types.ListValue)
if !ok {
return nil, fmt.Errorf("inputElemFunc: expected a ListValue argument")
}
return list.List()[i], nil
},
T: types.NewType(fmt.Sprintf("func(inputList %s) %s", obj.inputListType, obj.Type)),
},
)
obj.init.Txn.AddVertex(inputElemFunc)
outputElemFunc, err := obj.lastFuncValue.Call(obj.init.Txn, []interfaces.Func{inputElemFunc})
if err != nil {
return errwrap.Wrapf(err, "could not call obj.lastFuncValue.Call()")
}
obj.init.Txn.AddEdge(subgraphInput, inputElemFunc, &interfaces.FuncEdge{
Args: []string{"inputList"},
})
obj.init.Txn.AddEdge(outputElemFunc, outputListFunc, &interfaces.FuncEdge{
Args: []string{fmt.Sprintf("outputElem%d", i)},
})
}
return obj.init.Txn.Commit()
}

View File

@@ -1,64 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package core
import (
"fmt"
"github.com/purpleidea/mgmt/lang/funcs/simplepoly"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simplepoly.Register("len", []*types.FuncValue{
{
T: types.NewType("func(str) int"),
V: Len,
},
{
T: types.NewType("func([]variant) int"),
V: Len,
},
{
T: types.NewType("func(map{variant: variant}) int"),
V: Len,
},
// TODO: should we add support for struct or func lengths?
})
}
// Len returns the number of elements in a list or the number of key pairs in a
// map. It can operate on either of these types.
func Len(input []types.Value) (types.Value, error) {
var length int
switch k := input[0].Type().Kind; k {
case types.KindStr:
length = len(input[0].Str())
case types.KindList:
length = len(input[0].List())
case types.KindMap:
length = len(input[0].Map())
default:
return nil, fmt.Errorf("unsupported kind: %+v", k)
}
return &types.IntValue{
V: int64(length),
}, nil
}

View File

@@ -1,69 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coremath
import (
"fmt"
"github.com/purpleidea/mgmt/lang/funcs/simplepoly"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
typInt := types.NewType("func() int")
typFloat := types.NewType("func() float")
simplepoly.ModuleRegister(ModuleName, "fortytwo", []*types.FuncValue{
{
T: typInt,
V: fortyTwo(typInt), // generate the correct function here
},
{
T: typFloat,
V: fortyTwo(typFloat),
},
})
}
// fortyTwo is a helper function to build the correct function for the desired
// signature, because the simplepoly API doesn't tell the implementing function
// what its signature should be! In the next version of this API, we could pass
// in a sig field, like how we demonstrate in the implementation of FortyTwo. If
// the API doesn't change, then this is an example of how to build this as a
// wrapper.
func fortyTwo(sig *types.Type) func([]types.Value) (types.Value, error) {
return func(input []types.Value) (types.Value, error) {
return FortyTwo(sig, input)
}
}
// FortyTwo returns 42 as either an int or a float. This is not especially
// useful, but was built for a fun test case of a simple poly function with two
// different return types, but no input args.
func FortyTwo(sig *types.Type, input []types.Value) (types.Value, error) {
if sig.Out.Kind == types.KindInt {
return &types.IntValue{
V: 42,
}, nil
}
if sig.Out.Kind == types.KindFloat {
return &types.FloatValue{
V: 42.0,
}, nil
}
return nil, fmt.Errorf("unknown output type: %+v", sig.Out)
}

View File

@@ -1,23 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coremath
const (
// ModuleName is the prefix given to all the functions in this module.
ModuleName = "math"
)

View File

@@ -1,38 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coremath
import (
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "minus1", &types.FuncValue{
T: types.NewType("func(x int) int"),
V: Minus1,
})
}
// Minus1 takes an int and subtracts one from it.
func Minus1(input []types.Value) (types.Value, error) {
// TODO: check for overflow
return &types.IntValue{
V: input[0].Int() - 1,
}, nil
}

View File

@@ -1,78 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coremath
import (
"fmt"
"math"
"github.com/purpleidea/mgmt/lang/funcs/simplepoly"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simplepoly.ModuleRegister(ModuleName, "mod", []*types.FuncValue{
{
T: types.NewType("func(int, int) int"),
V: Mod,
},
{
T: types.NewType("func(float, float) float"),
V: Mod,
},
})
}
// Mod returns mod(x, y), the remainder of x/y. The two values must be either
// both of KindInt or both of KindFloat, and it will return the same kind. If
// you pass in a divisor of zero, this will error, eg: mod(x, 0) = NaN.
// TODO: consider returning zero instead of erroring?
func Mod(input []types.Value) (types.Value, error) {
var x, y float64
var float bool
k := input[0].Type().Kind
switch k {
case types.KindFloat:
float = true
x = input[0].Float()
y = input[1].Float()
case types.KindInt:
x = float64(input[0].Int())
y = float64(input[1].Int())
default:
return nil, fmt.Errorf("unexpected kind: %s", k)
}
z := math.Mod(x, y)
if math.IsNaN(z) {
return nil, fmt.Errorf("result is not a number")
}
if math.IsInf(z, 1) {
return nil, fmt.Errorf("unexpected positive infinity")
}
if math.IsInf(z, -1) {
return nil, fmt.Errorf("unexpected negative infinity")
}
if float {
return &types.FloatValue{
V: z,
}, nil
}
return &types.IntValue{
V: int64(z), // XXX: does this truncate?
}, nil
}

View File

@@ -1,54 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coremath
import (
"fmt"
"math"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "pow", &types.FuncValue{
T: types.NewType("func(x float, y float) float"),
V: Pow,
})
}
// Pow returns x ^ y, the base-x exponential of y.
func Pow(input []types.Value) (types.Value, error) {
x, y := input[0].Float(), input[1].Float()
// FIXME: check for overflow
z := math.Pow(x, y)
if math.IsNaN(z) {
return nil, fmt.Errorf("result is not a number")
}
if math.IsInf(z, 1) {
return nil, fmt.Errorf("result is positive infinity")
}
if math.IsInf(z, -1) {
return nil, fmt.Errorf("result is negative infinity")
}
// TODO: consider only returning floats, and adding isinf and
// isnan functions so that users can decide for themselves...
return &types.FloatValue{
V: z,
}, nil
}

View File

@@ -1,51 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coremath
import (
"fmt"
"math"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "sqrt", &types.FuncValue{
T: types.NewType("func(x float) float"),
V: Sqrt,
})
}
// Sqrt returns sqrt(x), the square root of x.
func Sqrt(input []types.Value) (types.Value, error) {
x := input[0].Float()
y := math.Sqrt(x)
if math.IsNaN(y) {
return nil, fmt.Errorf("result is not a number")
}
if math.IsInf(y, 1) {
return nil, fmt.Errorf("result is positive infinity")
}
if math.IsInf(y, -1) {
return nil, fmt.Errorf("result is negative infinity")
}
return &types.FloatValue{
V: y,
}, nil
}

View File

@@ -1,72 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coremath
import (
"fmt"
"math"
"testing"
"github.com/purpleidea/mgmt/lang/types"
)
func testSqrtSuccess(input, sqrt float64) error {
inputVal := &types.FloatValue{V: input}
val, err := Sqrt([]types.Value{inputVal})
if err != nil {
return err
}
if val.Float() != sqrt {
return fmt.Errorf("invalid output, expected %f, got %f", sqrt, val.Float())
}
return nil
}
func testSqrtError(input float64) error {
inputVal := &types.FloatValue{V: input}
_, err := Sqrt([]types.Value{inputVal})
if err == nil {
return fmt.Errorf("expected error for input %f, got nil", input)
}
return nil
}
func TestSqrtValidInput(t *testing.T) {
values := map[float64]float64{
4.0: 2.0,
16.0: 4.0,
2.0: math.Sqrt(2.0),
}
for input, sqrt := range values {
if err := testSqrtSuccess(input, sqrt); err != nil {
t.Error(err)
}
}
}
func TestSqrtInvalidInput(t *testing.T) {
values := []float64{-1.0}
for _, input := range values {
if err := testSqrtError(input); err != nil {
t.Error(err)
}
}
}

View File

@@ -1,45 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package corenet
import (
"net"
"strings"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "cidr_to_ip", &types.FuncValue{
T: types.NewType("func(a str) str"),
V: CidrToIP,
})
}
// CidrToIP returns the IP from a CIDR address
func CidrToIP(input []types.Value) (types.Value, error) {
cidr := input[0].Str()
ip, _, err := net.ParseCIDR(strings.TrimSpace(cidr))
if err != nil {
return &types.StrValue{}, err
}
return &types.StrValue{
V: ip.String(),
}, nil
}

View File

@@ -1,71 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package corenet
import (
"fmt"
"testing"
"github.com/purpleidea/mgmt/lang/types"
)
func TestCidrToIP(t *testing.T) {
cidrtests := []struct {
name string
input string
expected string
err error
}{
// IPv4 success.
{"IPv4 cidr", "192.0.2.12/24", "192.0.2.12", nil},
{"spaced IPv4 cidr ", " 192.168.42.13/24 ", "192.168.42.13", nil},
// IPv4 failure - tests error.
{"invalid IPv4 cidr", "192.168.42.13/33", "", fmt.Errorf("invalid CIDR address: 192.168.42.13/33")},
// IPV6 success.
{"IPv6 cidr", "2001:db8::/32", "2001:db8::", nil},
{"spaced IPv6 cidr ", " 2001:db8::/32 ", "2001:db8::", nil},
// IPv6 failure - tests error.
{"invalid IPv6 cidr", "2001:db8::/333", "", fmt.Errorf("invalid CIDR address: 2001:db8::/333")},
}
for _, ts := range cidrtests {
test := ts
t.Run(test.name, func(t *testing.T) {
output, err := CidrToIP([]types.Value{&types.StrValue{V: test.input}})
expectedStr := &types.StrValue{V: test.expected}
if test.err != nil && err.Error() != test.err.Error() {
t.Errorf("input: %s, expected error: %q, got: %q", test.input, test.err, err)
return
} else if test.err != nil && err == nil {
t.Errorf("input: %s, expected error: %v, but got nil", test.input, test.err)
return
} else if test.err == nil && err != nil {
t.Errorf("input: %s, did not expect error but got: %#v", test.input, err)
return
}
if err := output.Cmp(expectedStr); err != nil {
t.Errorf("input: %s, expected: %s, got: %s", test.input, expectedStr, output)
return
}
})
}
}

View File

@@ -1,76 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package corenet
import (
"fmt"
"net"
"strings"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "macfmt", &types.FuncValue{
T: types.NewType("func(a str) str"),
V: MacFmt,
})
simple.ModuleRegister(ModuleName, "oldmacfmt", &types.FuncValue{
T: types.NewType("func(a str) str"),
V: OldMacFmt,
})
}
// MacFmt takes a MAC address with hyphens and converts it to a format with
// colons.
func MacFmt(input []types.Value) (types.Value, error) {
mac := input[0].Str()
// Check if the MAC address is valid.
if len(mac) != len("00:00:00:00:00:00") {
return nil, fmt.Errorf("invalid MAC address length: %s", mac)
}
_, err := net.ParseMAC(mac)
if err != nil {
return nil, err
}
return &types.StrValue{
V: strings.Replace(mac, "-", ":", -1),
}, nil
}
// OldMacFmt takes a MAC address with colons and converts it to a format with
// hyphens. This is the old deprecated style that nobody likes.
func OldMacFmt(input []types.Value) (types.Value, error) {
mac := input[0].Str()
// Check if the MAC address is valid.
if len(mac) != len("00:00:00:00:00:00") {
return nil, fmt.Errorf("invalid MAC address length: %s", mac)
}
_, err := net.ParseMAC(mac)
if err != nil {
return nil, err
}
return &types.StrValue{
V: strings.Replace(mac, ":", "-", -1),
}, nil
}

View File

@@ -1,84 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package corenet
import (
"testing"
"github.com/purpleidea/mgmt/lang/types"
)
func TestMacFmt(t *testing.T) {
var tests = []struct {
name string
in string
out string
wantErr bool
}{
{"Valid mac with hyphens", "01-23-45-67-89-AB", "01:23:45:67:89:AB", false},
{"Valid mac with colons", "01:23:45:67:89:AB", "01:23:45:67:89:AB", false},
{"Incorrect mac length with colons", "01:23:45:67:89:AB:01:23:45:67:89:AB", "01:23:45:67:89:AB:01:23:45:67:89:AB", true},
{"Invalid mac", "", "", true},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
m, err := MacFmt([]types.Value{&types.StrValue{V: tt.in}})
if (err != nil) != tt.wantErr {
t.Errorf("func MacFmt() error = %v, wantErr %v", err, tt.wantErr)
return
}
if m != nil {
if err := m.Cmp(&types.StrValue{V: tt.out}); err != nil {
t.Errorf("got %q, want %q", m.Value(), tt.out)
}
}
})
}
}
func TestOldMacFmt(t *testing.T) {
var tests = []struct {
name string
in string
out string
wantErr bool
}{
{"Valid mac with hyphens", "01:23:45:67:89:AB", "01-23-45-67-89-AB", false},
{"Valid mac with colons", "01-23-45-67-89-AB", "01-23-45-67-89-AB", false},
{"Incorrect mac length with hyphens", "01-23-45-67-89-AB-01-23-45-67-89-AB", "01-23-45-67-89-AB-01-23-45-67-89-AB", true},
{"Invalid mac", "", "", true},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
m, err := OldMacFmt([]types.Value{&types.StrValue{V: tt.in}})
if (err != nil) != tt.wantErr {
t.Errorf("func MacFmt() error = %v, wantErr %v", err, tt.wantErr)
return
}
if m != nil {
if err := m.Cmp(&types.StrValue{V: tt.out}); err != nil {
t.Errorf("got %q, want %q", m.Value(), tt.out)
}
}
})
}
}

View File

@@ -1,23 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package corenet
const (
// ModuleName is the prefix given to all the functions in this module.
ModuleName = "net"
)

View File

@@ -1,47 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreos
import (
"os"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "args", &types.FuncValue{
T: types.NewType("func() []str"),
V: Args,
})
}
// Args returns the `argv` of the current process. Keep in mind that this will
// return different values depending on how this is deployed, so don't expect a
// result on your deploy client to behave the same as a server receiving code.
// FIXME: Sanitize any command-line secrets we might pass in by cli.
func Args([]types.Value) (types.Value, error) {
values := []types.Value{}
for _, s := range os.Args {
values = append(values, &types.StrValue{V: s})
}
return &types.ListValue{
V: values,
T: types.NewType("[]str"),
}, nil
}

View File

@@ -1,80 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreos
import (
"os"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
// TODO: Create a family method that will return a giant struct.
simple.ModuleRegister(ModuleName, "is_debian", &types.FuncValue{
T: types.NewType("func() bool"),
V: IsDebian,
})
simple.ModuleRegister(ModuleName, "is_redhat", &types.FuncValue{
T: types.NewType("func() bool"),
V: IsRedHat,
})
simple.ModuleRegister(ModuleName, "is_archlinux", &types.FuncValue{
T: types.NewType("func() bool"),
V: IsArchLinux,
})
}
// IsDebian detects if the os family is debian.
// TODO: Detect OS changes.
func IsDebian(input []types.Value) (types.Value, error) {
exists := true
_, err := os.Stat("/etc/debian_version")
if os.IsNotExist(err) {
exists = false
}
return &types.BoolValue{
V: exists,
}, nil
}
// IsRedHat detects if the os family is redhat.
// TODO: Detect OS changes.
func IsRedHat(input []types.Value) (types.Value, error) {
exists := true
_, err := os.Stat("/etc/redhat-release")
if os.IsNotExist(err) {
exists = false
}
return &types.BoolValue{
V: exists,
}, nil
}
// IsArchLinux detects if the os family is archlinux.
// TODO: Detect OS changes.
func IsArchLinux(input []types.Value) (types.Value, error) {
exists := true
_, err := os.Stat("/etc/arch-release")
if os.IsNotExist(err) {
exists = false
}
return &types.BoolValue{
V: exists,
}, nil
}

View File

@@ -1,23 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreos
const (
// ModuleName is the prefix given to all the functions in this module.
ModuleName = "os"
)

View File

@@ -1,229 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreos
import (
"context"
"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"
"github.com/purpleidea/mgmt/util/errwrap"
)
const (
// ReadFileFuncName is the name this function is registered as.
ReadFileFuncName = "readfile"
// arg names...
readFileArgNameFilename = "filename"
)
func init() {
funcs.ModuleRegister(ModuleName, ReadFileFuncName, 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.
// Please note that this is different from the readfile function in the deploy
// package.
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
}
// String returns a simple name for this function. This is needed so this struct
// can satisfy the pgraph.Vertex interface.
func (obj *ReadFileFunc) String() string {
return ReadFileFuncName
}
// ArgGen returns the Nth arg name for this function.
func (obj *ReadFileFunc) ArgGen(index int) (string, error) {
seq := []string{readFileArgNameFilename}
if l := len(seq); index >= l {
return "", fmt.Errorf("index %d exceeds arg length of %d", index, l)
}
return seq[index], nil
}
// 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(fmt.Sprintf("func(%s str) str", readFileArgNameFilename)),
}
}
// 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{}
return nil
}
// Stream returns the changing values that this func has over time.
func (obj *ReadFileFunc) Stream(ctx context.Context) error {
defer close(obj.init.Output) // the sender closes
defer close(obj.events) // clean up for fun
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()[readFileArgNameFilename].Str()
// TODO: add validation for absolute path?
// TODO: add check for empty string
if obj.filename != nil && *obj.filename == 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 <-ctx.Done():
// don't block here on shutdown
return
}
//err = nil // reset
}
}()
continue // wait for an actual event or we'd send empty!
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 != nil && *obj.result == result {
continue // result didn't change
}
obj.result = &result // store new result
case <-ctx.Done():
return nil
}
select {
case obj.init.Output <- &types.StrValue{
V: *obj.result,
}:
case <-ctx.Done():
return nil
}
}
}

View File

@@ -1,213 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreos
import (
"bufio"
"context"
"fmt"
"os/exec"
"sync"
"github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
)
const (
// SystemFuncName is the name this function is registered as.
SystemFuncName = "system"
// arg names...
systemArgNameCmd = "cmd"
)
func init() {
funcs.ModuleRegister(ModuleName, SystemFuncName, func() interfaces.Func { return &SystemFunc{} })
}
// SystemFunc runs a string as a shell command, then produces each line from
// stdout. If the input string changes, then the commands are executed one after
// the other and the concatenation of their outputs is produced line by line.
//
// Note that in the likely case in which the process emits several lines one
// after the other, the downstream resources might not run for every line unless
// the "Meta:realize" metaparam is set to true.
type SystemFunc struct {
init *interfaces.Init
cancel context.CancelFunc
}
// String returns a simple name for this function. This is needed so this struct
// can satisfy the pgraph.Vertex interface.
func (obj *SystemFunc) String() string {
return SystemFuncName
}
// ArgGen returns the Nth arg name for this function.
func (obj *SystemFunc) ArgGen(index int) (string, error) {
seq := []string{systemArgNameCmd}
if l := len(seq); index >= l {
return "", fmt.Errorf("index %d exceeds arg length of %d", index, l)
}
return seq[index], nil
}
// Validate makes sure we've built our struct properly. It is usually unused for
// normal functions that users can use directly.
func (obj *SystemFunc) Validate() error {
return nil
}
// Info returns some static info about itself.
func (obj *SystemFunc) Info() *interfaces.Info {
return &interfaces.Info{
Pure: false, // definitely false
Memo: false,
Sig: types.NewType(fmt.Sprintf("func(%s str) str", systemArgNameCmd)),
Err: obj.Validate(),
}
}
// Init runs some startup code for this function.
func (obj *SystemFunc) Init(init *interfaces.Init) error {
obj.init = init
return nil
}
// Stream returns the changing values that this func has over time.
func (obj *SystemFunc) Stream(ctx context.Context) error {
// XXX: this implementation is a bit awkward especially with the port to
// the Stream(context.Context) signature change. This is a straight port
// but we could refactor this eventually.
// Close the output chan to signal that no more values are coming.
defer close(obj.init.Output)
// A channel which closes when the current process exits, on its own
// or due to cancel(). The channel is only closed once all the pending
// stdout and stderr lines have been processed.
//
// The channel starts closed because no process is running yet. A new
// channel is created each time a new process is started. We never run
// more than one process at a time.
processedChan := make(chan struct{})
close(processedChan)
// Wait for the current process to exit, if any.
defer func() {
<-processedChan
}()
// Kill the current process, if any. A new cancel function is created
// each time a new process is started.
var innerCtx context.Context
defer func() {
if obj.cancel == nil {
return
}
obj.cancel()
}()
for {
select {
case input, more := <-obj.init.Input:
if !more {
// Wait until the current process exits and all of its
// stdout is sent downstream.
select {
case <-processedChan:
return nil
case <-ctx.Done():
return nil
}
}
shellCommand := input.Struct()[systemArgNameCmd].Str()
// Kill the previous command, if any.
if obj.cancel != nil {
obj.cancel()
}
<-processedChan
// Run the command, connecting it to ctx so we can kill
// it if needed, and to two Readers so we can read its
// stdout and stderr.
innerCtx, obj.cancel = context.WithCancel(context.Background())
cmd := exec.CommandContext(innerCtx, "sh", "-c", shellCommand)
stdoutReader, err := cmd.StdoutPipe()
if err != nil {
return err
}
stderrReader, err := cmd.StderrPipe()
if err != nil {
return err
}
if err = cmd.Start(); err != nil {
return err
}
// We will now start several goroutines:
// 1. To process stdout
// 2. To process stderr
// 3. To wait for (1) and (2) to terminate and close processedChan
//
// This WaitGroup is used by (3) to wait for (1) and (2).
wg := &sync.WaitGroup{}
// Emit one value downstream for each line from stdout.
// Terminates when the process exits, on its own or due
// to cancel().
wg.Add(1)
go func() {
defer wg.Done()
stdoutScanner := bufio.NewScanner(stdoutReader)
for stdoutScanner.Scan() {
outputValue := &types.StrValue{V: stdoutScanner.Text()}
obj.init.Output <- outputValue
}
}()
// Log the lines from stderr, to help the user debug.
// Terminates when the process exits, on its own or
// due to cancel().
wg.Add(1)
go func() {
defer wg.Done()
stderrScanner := bufio.NewScanner(stderrReader)
for stderrScanner.Scan() {
obj.init.Logf("system: \"%v\": stderr: %v\n", shellCommand, stderrScanner.Text())
}
}()
// Closes processedChan after the previous two
// goroutines terminate. Thus, this goroutine also
// terminates when the process exits, on its own or due
// to cancel().
processedChan = make(chan struct{})
go func() {
wg.Wait()
close(processedChan)
}()
case <-ctx.Done():
return nil
}
}
}

View File

@@ -1,60 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package core
import (
"fmt"
"github.com/purpleidea/mgmt/lang/funcs/simplepoly"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simplepoly.Register("panic", []*types.FuncValue{
{
T: types.NewType("func(x bool) bool"),
V: Panic,
},
{
T: types.NewType("func(x str) bool"),
V: Panic,
},
})
}
// Panic returns an error when it receives a non-empty string or a true boolean.
// The error should cause the function engine to shutdown. If there's no error,
// it returns false.
func Panic(input []types.Value) (types.Value, error) {
switch k := input[0].Type().Kind; k {
case types.KindBool:
if input[0].Bool() {
return nil, fmt.Errorf("bool panic occurred")
}
case types.KindStr:
if s := input[0].Str(); s != "" {
return nil, fmt.Errorf("str panic occurred: %s", s)
}
default:
return nil, fmt.Errorf("unsupported kind: %+v", k)
}
return &types.BoolValue{
V: false,
}, nil
}

View File

@@ -1,169 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package core // TODO: should this be in its own individual package?
import (
"context"
"crypto/rand"
"fmt"
"math/big"
"github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/util/errwrap"
)
const (
// Random1FuncName is the name this function is registered as.
Random1FuncName = "random1"
// arg names...
random1ArgNameLength = "length"
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
func init() {
funcs.Register(Random1FuncName, func() interfaces.Func { return &Random1Func{} })
}
// Random1Func returns one random string of a certain length.
// XXX: return a stream instead, and combine this with a first(?) function which
// takes the first value and then puts backpressure on the stream. This should
// notify parent functions somehow that their values are no longer required so
// that they can shutdown if possible. Maybe it should be returning a stream of
// floats [0,1] as well, which someone can later map to the alphabet that they
// want. Should random() take an interval to know how often to spit out values?
// It could also just do it once per second, and we could filter for less. If we
// want something high precision, we could add that in the future... We could
// name that "random" and this one can be "random1" until we deprecate it.
type Random1Func struct {
init *interfaces.Init
finished bool // did we send the random string?
}
// String returns a simple name for this function. This is needed so this struct
// can satisfy the pgraph.Vertex interface.
func (obj *Random1Func) String() string {
return Random1FuncName
}
// ArgGen returns the Nth arg name for this function.
func (obj *Random1Func) ArgGen(index int) (string, error) {
seq := []string{random1ArgNameLength}
if l := len(seq); index >= l {
return "", fmt.Errorf("index %d exceeds arg length of %d", index, l)
}
return seq[index], nil
}
// Validate makes sure we've built our struct properly. It is usually unused for
// normal functions that users can use directly.
func (obj *Random1Func) Validate() error {
return nil
}
// Info returns some static info about itself.
func (obj *Random1Func) Info() *interfaces.Info {
return &interfaces.Info{
Pure: false,
Sig: types.NewType(fmt.Sprintf("func(%s int) str", random1ArgNameLength)),
Err: obj.Validate(),
}
}
// generate generates a random string.
func generate(length uint16) (string, error) {
max := len(alphabet) - 1 // last index
output := ""
// FIXME: have someone verify this is cryptographically secure & correct
for i := uint16(0); i < length; i++ {
big, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
if err != nil {
return "", errwrap.Wrapf(err, "could not generate random string")
}
ix := big.Int64()
output += string(alphabet[ix])
}
if length != 0 && output == "" { // safety against empty strings
return "", fmt.Errorf("string is empty")
}
if uint16(len(output)) != length { // safety against weird bugs
return "", fmt.Errorf("random string is too short") // bug!
}
return output, nil
}
// Init runs some startup code for this function.
func (obj *Random1Func) Init(init *interfaces.Init) error {
obj.init = init
return nil
}
// Stream returns the single value that was generated and then closes.
func (obj *Random1Func) Stream(ctx context.Context) error {
defer close(obj.init.Output) // the sender closes
var result string
for {
select {
case input, ok := <-obj.init.Input:
if !ok {
return nil // can't output any more
}
//if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil {
// return errwrap.Wrapf(err, "wrong function input")
//}
if obj.finished {
// TODO: continue instead?
return fmt.Errorf("you can only pass a single input to random")
}
length := input.Struct()[random1ArgNameLength].Int()
// TODO: if negative, randomly pick a length ?
if length < 0 {
return fmt.Errorf("can't generate a negative length")
}
var err error
if result, err = generate(uint16(length)); err != nil {
return err // no errwrap needed b/c helper func
}
case <-ctx.Done():
return nil
}
select {
case obj.init.Output <- &types.StrValue{
V: result,
}:
// we only send one value, then wait for input to close
obj.finished = true
case <-ctx.Done():
return nil
}
}
}

View File

@@ -1,51 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreregexp
import (
"regexp"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/util/errwrap"
)
func init() {
simple.ModuleRegister(ModuleName, "match", &types.FuncValue{
T: types.NewType("func(pattern str, s str) bool"),
V: Match,
})
}
// Match matches whether a string matches the regexp pattern.
func Match(input []types.Value) (types.Value, error) {
pattern := input[0].Str()
s := input[1].Str()
// TODO: We could make this more efficient with the regular function API
// by only compiling the pattern when it changes.
re, err := regexp.Compile(pattern)
if err != nil {
return nil, errwrap.Wrapf(err, "pattern did not compile")
}
result := re.MatchString(s)
return &types.BoolValue{
V: result,
}, nil
}

View File

@@ -1,75 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreregexp
import (
"testing"
"github.com/purpleidea/mgmt/lang/types"
)
func TestMatch0(t *testing.T) {
values := []struct {
pattern string
s string
expected bool
}{
{
"(mgmt){2}",
"mgmt",
false,
},
{
"(mgmt){2}",
"mgmtmgmt",
true,
},
{
"(mgmt){2}",
"mgmtmgmtmgmt",
true,
},
{
`^db\d+\.example\.com$`,
"db1.example.com",
true,
},
{
`^db\d+\.example\.com$`,
"dbX.example.com",
false,
},
{
`^db\d+\.example\.com$`,
"db1.exampleXcom",
false,
},
}
for i, x := range values {
pattern := &types.StrValue{V: x.pattern}
s := &types.StrValue{V: x.s}
val, err := Match([]types.Value{pattern, s})
if err != nil {
t.Errorf("test index %d failed with: %+v", i, err)
}
if a, b := x.expected, val.Bool(); a != b {
t.Errorf("test index %d expected %t, got %t", i, a, b)
}
}
}

View File

@@ -1,23 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreregexp
const (
// ModuleName is the prefix given to all the functions in this module.
ModuleName = "regexp"
)

View File

@@ -1,50 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package corestrings
import (
"strings"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "split", &types.FuncValue{
T: types.NewType("func(a str, b str) []str"),
V: Split,
})
}
// Split splits the input string using the separator and returns the segments as
// a list.
func Split(input []types.Value) (types.Value, error) {
str, sep := input[0].Str(), input[1].Str()
segments := strings.Split(str, sep)
listVal := types.NewList(types.NewType("[]str"))
for _, segment := range segments {
listVal.Add(&types.StrValue{
V: segment,
})
}
return listVal, nil
}

View File

@@ -1,64 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package corestrings
import (
"fmt"
"testing"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/util"
)
func testSplit(input, sep string, output []string) error {
inputVal, sepVal := &types.StrValue{V: input}, &types.StrValue{V: sep}
val, err := Split([]types.Value{inputVal, sepVal})
if err != nil {
return err
}
listVal, ok := val.(*types.ListValue)
if !ok {
return fmt.Errorf("split did not return a list")
}
for _, segment := range output {
if _, ok := listVal.Contains(&types.StrValue{V: segment}); !ok {
return fmt.Errorf("output does not contained expected segment %s", segment)
}
}
for _, segment := range listVal.V {
if !util.StrInList(segment.Str(), output) {
return fmt.Errorf("output contains unexpected segment %s", segment.Str())
}
}
return nil
}
func TestSplit(t *testing.T) {
values := map[string][]string{
"hello,world": {"hello", "world"},
"hello world": {"hello world"},
"hello;world": {"hello;world"},
}
for input, output := range values {
if err := testSplit(input, ",", output); err != nil {
t.Error(err)
}
}
}

View File

@@ -1,23 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package corestrings
const (
// ModuleName is the prefix given to all the functions in this module.
ModuleName = "strings"
)

View File

@@ -1,39 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package corestrings
import (
"strings"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "to_lower", &types.FuncValue{
T: types.NewType("func(a str) str"),
V: ToLower,
})
}
// ToLower turns a string to lowercase.
func ToLower(input []types.Value) (types.Value, error) {
return &types.StrValue{
V: strings.ToLower(input[0].Str()),
}, nil
}

View File

@@ -1,44 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package corestrings
import (
"testing"
"github.com/purpleidea/mgmt/lang/types"
)
func testToLower(t *testing.T, input, expected string) {
inputStr := &types.StrValue{V: input}
value, err := ToLower([]types.Value{inputStr})
if err != nil {
t.Error(err)
return
}
if value.Str() != expected {
t.Errorf("invalid output, expected %s, got %s", expected, value.Str())
}
}
func TestToLowerSimple(t *testing.T) {
testToLower(t, "Hello", "hello")
}
func TestToLowerSameString(t *testing.T) {
testToLower(t, "HELLO 22", "hello 22")
}

View File

@@ -1,229 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
//go:build !darwin
package coresys
import (
"context"
"io/ioutil"
"regexp"
"strconv"
"strings"
"sync"
"github.com/purpleidea/mgmt/lang/funcs/facts"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/util/errwrap"
"github.com/purpleidea/mgmt/util/socketset"
"golang.org/x/sys/unix"
)
const (
// CPUCountFuncName is the name this fact is registered as. It's still a
// Func Name because this is the name space the fact is actually using.
CPUCountFuncName = "cpu_count"
rtmGrps = 0x1 // make me a multicast receiver
socketFile = "pipe.sock"
cpuDevpathRegex = "/devices/system/cpu/cpu[0-9]"
)
func init() {
facts.ModuleRegister(ModuleName, CPUCountFuncName, func() facts.Fact { return &CPUCountFact{} }) // must register the fact and name
}
// CPUCountFact is a fact that returns the current CPU count.
type CPUCountFact struct {
init *facts.Init
}
// String returns a simple name for this fact. This is needed so this struct can
// satisfy the pgraph.Vertex interface.
func (obj *CPUCountFact) String() string {
return CPUCountFuncName
}
// Info returns static typing info about what the fact returns.
func (obj *CPUCountFact) Info() *facts.Info {
return &facts.Info{
Output: types.NewType("int"),
}
}
// Init runs startup code for this fact and sets the facts.Init variable.
func (obj *CPUCountFact) Init(init *facts.Init) error {
obj.init = init
return nil
}
// Stream returns the changing values that this fact has over time. It will
// first poll sysfs to get the initial cpu count, and then receives UEvents from
// the kernel as CPUs are added/removed.
func (obj CPUCountFact) Stream(ctx context.Context) error {
defer close(obj.init.Output) // signal when we're done
ss, err := socketset.NewSocketSet(rtmGrps, socketFile, unix.NETLINK_KOBJECT_UEVENT)
if err != nil {
return errwrap.Wrapf(err, "error creating socket set")
}
// waitgroup for netlink receive goroutine
wg := &sync.WaitGroup{}
defer ss.Close()
// We must wait for the Shutdown() AND the select inside of SocketSet to
// complete before we Close, since the unblocking in SocketSet is not a
// synchronous operation.
defer wg.Wait()
defer ss.Shutdown() // close the netlink socket and unblock conn.receive()
eventChan := make(chan *nlChanEvent) // updated in goroutine when we receive uevent
closeChan := make(chan struct{}) // channel to unblock selects in goroutine
defer close(closeChan)
var once bool // did we send at least once?
// wait for kernel to poke us about new device changes on the system
wg.Add(1)
go func() {
defer wg.Done()
defer close(eventChan)
for {
uevent, err := ss.ReceiveUEvent() // calling Shutdown will stop this from blocking
if obj.init.Debug {
obj.init.Logf("sending uevent SEQNUM: %s", uevent.Data["SEQNUM"])
}
select {
case eventChan <- &nlChanEvent{
uevent: uevent,
err: err,
}:
case <-closeChan:
return
}
}
}()
startChan := make(chan struct{})
close(startChan) // trigger the first event
var cpuCount, newCount int64 = 0, -1
for {
select {
case <-startChan:
startChan = nil // disable
newCount, err = getCPUCount()
if err != nil {
obj.init.Logf("Could not get initial CPU count. Setting to zero.")
}
// TODO: would we rather error instead of sending zero?
case event, ok := <-eventChan:
if !ok {
continue
}
if event.err != nil {
return errwrap.Wrapf(event.err, "error receiving uevent")
}
if obj.init.Debug {
obj.init.Logf("received uevent SEQNUM: %s", event.uevent.Data["SEQNUM"])
}
if isCPUEvent(event.uevent) {
newCount, err = getCPUCount()
if err != nil {
obj.init.Logf("could not getCPUCount: %e", err)
continue
}
}
case <-ctx.Done():
return nil
}
if once && newCount == cpuCount {
continue
}
cpuCount = newCount
select {
case obj.init.Output <- &types.IntValue{
V: cpuCount,
}:
once = true
// send
case <-ctx.Done():
return nil
}
}
}
// getCPUCount looks in sysfs to get the number of CPUs that are online.
func getCPUCount() (int64, error) {
dat, err := ioutil.ReadFile("/sys/devices/system/cpu/online")
if err != nil {
return 0, err
}
return parseCPUList(string(dat))
}
// Parses a line of the form X,Y,Z,... where X,Y,Z can be either a single CPU or
// a contiguous range of CPUs. e.g. "2,4-31,32-63". If there is an error parsing
// the line the function will return 0.
func parseCPUList(list string) (int64, error) {
var count int64
for _, rg := range strings.Split(list, ",") {
cpuRange := strings.SplitN(rg, "-", 2)
if len(cpuRange) == 1 {
count++
} else if len(cpuRange) == 2 {
lo, err := strconv.ParseInt(cpuRange[0], 10, 64)
if err != nil {
return 0, err
}
hi, err := strconv.ParseInt(strings.TrimRight(cpuRange[1], "\n"), 10, 64)
if err != nil {
return 0, err
}
count += hi - lo + 1
}
}
return count, nil
}
// When we receive a udev event, we filter only those that indicate a CPU is
// being added or removed, or being taken online or offline.
func isCPUEvent(event *socketset.UEvent) bool {
if event.Subsystem != "cpu" {
return false
}
// is this a valid cpu path in sysfs?
m, err := regexp.MatchString(cpuDevpathRegex, event.Devpath)
if !m || err != nil {
return false
}
if event.Action == "add" || event.Action == "remove" || event.Action == "online" || event.Action == "offline" {
return true
}
return false
}
// nlChanEvent defines the channel used to send netlink messages and errors to
// the event processing loop in Stream.
type nlChanEvent struct {
uevent *socketset.UEvent
err error
}

View File

@@ -1,105 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
//go:build !darwin
package coresys
import (
"context"
"testing"
"github.com/purpleidea/mgmt/lang/funcs/facts"
"github.com/purpleidea/mgmt/lang/types"
)
func TestSimple(t *testing.T) {
fact := &CPUCountFact{}
output := make(chan types.Value)
err := fact.Init(&facts.Init{
Output: output,
Logf: func(format string, v ...interface{}) {
t.Logf("cpucount_fact_test: "+format, v...)
},
})
if err != nil {
t.Errorf("could not init CPUCountFact")
return
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
defer cancel()
Loop:
for {
select {
case cpus := <-output:
t.Logf("CPUS: %d\n", cpus.Int())
break Loop
}
}
}()
// now start the stream
if err := fact.Stream(ctx); err != nil {
t.Error(err)
}
}
func TestParseCPUList(t *testing.T) {
var cpulistTests = []struct {
desc string
list string
result int64
}{
{
desc: "single CPU",
list: "1",
result: 1,
},
{
desc: "cpu range",
list: "0-31",
result: 32,
},
{
desc: "range and single",
list: "0-1,3",
result: 3,
},
{
desc: "single, two ranges",
list: "2,4-8,10-16",
result: 13,
},
}
for _, tt := range cpulistTests {
t.Run(tt.list, func(t *testing.T) {
cpuCount, err := parseCPUList(tt.list)
if err != nil {
t.Errorf("could not parseCPUList: %+v", err)
return
}
if cpuCount != tt.result {
t.Errorf("expected %d, got %d", tt.result, cpuCount)
return
}
})
}
}

View File

@@ -1,87 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coresys
import (
"os"
"strings"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
simple.ModuleRegister(ModuleName, "getenv", &types.FuncValue{
T: types.NewType("func(str) str"),
V: GetEnv,
})
simple.ModuleRegister(ModuleName, "defaultenv", &types.FuncValue{
T: types.NewType("func(str, str) str"),
V: DefaultEnv,
})
simple.ModuleRegister(ModuleName, "hasenv", &types.FuncValue{
T: types.NewType("func(str) bool"),
V: HasEnv,
})
simple.ModuleRegister(ModuleName, "env", &types.FuncValue{
T: types.NewType("func() map{str: str}"),
V: Env,
})
}
// GetEnv gets environment variable by name or returns empty string if non
// existing.
func GetEnv(input []types.Value) (types.Value, error) {
return &types.StrValue{
V: os.Getenv(input[0].Str()),
}, nil
}
// DefaultEnv gets environment variable by name or returns default if non
// existing.
func DefaultEnv(input []types.Value) (types.Value, error) {
value, exists := os.LookupEnv(input[0].Str())
if !exists {
value = input[1].Str()
}
return &types.StrValue{
V: value,
}, nil
}
// HasEnv returns true if environment variable exists.
func HasEnv(input []types.Value) (types.Value, error) {
_, exists := os.LookupEnv(input[0].Str())
return &types.BoolValue{
V: exists,
}, nil
}
// Env returns a map of all keys and their values.
func Env(input []types.Value) (types.Value, error) {
environ := make(map[types.Value]types.Value)
for _, keyval := range os.Environ() {
if i := strings.IndexRune(keyval, '='); i != -1 {
environ[&types.StrValue{V: keyval[:i]}] = &types.StrValue{V: keyval[i+1:]}
}
}
return &types.MapValue{
T: types.NewType("map{str: str}"),
V: environ,
}, nil
}

View File

@@ -1,80 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coresys
import (
"context"
"github.com/purpleidea/mgmt/lang/funcs/facts"
"github.com/purpleidea/mgmt/lang/types"
)
const (
// HostnameFuncName is the name this fact is registered as. It's still a
// Func Name because this is the name space the fact is actually using.
HostnameFuncName = "hostname"
)
func init() {
facts.ModuleRegister(ModuleName, HostnameFuncName, func() facts.Fact { return &HostnameFact{} }) // must register the fact and name
}
// HostnameFact is a function that returns the hostname.
// TODO: support hostnames that change in the future.
type HostnameFact struct {
init *facts.Init
}
// String returns a simple name for this fact. This is needed so this struct can
// satisfy the pgraph.Vertex interface.
func (obj *HostnameFact) String() string {
return HostnameFuncName
}
// Validate makes sure we've built our struct properly. It is usually unused for
// normal facts that users can use directly.
//func (obj *HostnameFact) Validate() error {
// return nil
//}
// Info returns some static info about itself.
func (obj *HostnameFact) Info() *facts.Info {
return &facts.Info{
Output: types.NewType("str"),
}
}
// Init runs some startup code for this fact.
func (obj *HostnameFact) Init(init *facts.Init) error {
obj.init = init
return nil
}
// Stream returns the single value that this fact has, and then closes.
func (obj *HostnameFact) Stream(ctx context.Context) error {
select {
case obj.init.Output <- &types.StrValue{
V: obj.init.Hostname,
}:
// pass
case <-ctx.Done():
return nil
}
close(obj.init.Output) // signal that we're done sending
return nil
}

View File

@@ -1,113 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coresys
import (
"context"
"time"
"github.com/purpleidea/mgmt/lang/funcs/facts"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/util/errwrap"
)
const (
// LoadFuncName is the name this fact is registered as. It's still a
// Func Name because this is the name space the fact is actually using.
LoadFuncName = "load"
loadSignature = "struct{x1 float; x5 float; x15 float}"
)
func init() {
facts.ModuleRegister(ModuleName, LoadFuncName, func() facts.Fact { return &LoadFact{} }) // must register the fact and name
}
// LoadFact is a fact which returns the current system load.
type LoadFact struct {
init *facts.Init
}
// String returns a simple name for this fact. This is needed so this struct can
// satisfy the pgraph.Vertex interface.
func (obj *LoadFact) String() string {
return LoadFuncName
}
// Validate makes sure we've built our struct properly. It is usually unused for
// normal facts that users can use directly.
//func (obj *LoadFact) Validate() error {
// return nil
//}
// Info returns some static info about itself.
func (obj *LoadFact) Info() *facts.Info {
return &facts.Info{
Output: types.NewType(loadSignature),
}
}
// Init runs some startup code for this fact.
func (obj *LoadFact) Init(init *facts.Init) error {
obj.init = init
return nil
}
// Stream returns the changing values that this fact has over time.
func (obj *LoadFact) Stream(ctx context.Context) error {
defer close(obj.init.Output) // always signal when we're done
// it seems the different values only update once every 5
// seconds, so that's as often as we need to refresh this!
// TODO: lookup this value if it's something configurable
ticker := time.NewTicker(time.Duration(5) * time.Second)
// streams must generate an initial event on startup
startChan := make(chan struct{}) // start signal
close(startChan) // kick it off!
defer ticker.Stop()
for {
select {
case <-startChan: // kick the loop once at start
startChan = nil // disable
case <-ticker.C: // received the timer event
// pass
case <-ctx.Done():
return nil
}
x1, x5, x15, err := load()
if err != nil {
return errwrap.Wrapf(err, "could not read load values")
}
st := types.NewStruct(types.NewType(loadSignature))
for k, v := range map[string]float64{"x1": x1, "x5": x5, "x15": x15} {
if err := st.Set(k, &types.FloatValue{V: v}); err != nil {
return errwrap.Wrapf(err, "struct could not set key: `%s`", k)
}
}
select {
case obj.init.Output <- st:
// send
case <-ctx.Done():
return nil
}
}
}

View File

@@ -1,38 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
//go:build darwin
package coresys
/*
#include <stdlib.h>
*/
import "C"
// macOS/Darwin specific implementation to get load.
func load() (one, five, fifteen float64, err error) {
avg := []C.double{0, 0, 0}
C.getloadavg(&avg[0], C.int(len(avg)))
one = float64(avg[0])
five = float64(avg[1])
fifteen = float64(avg[2])
return
}

View File

@@ -1,45 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
//go:build !darwin
package coresys
import (
"syscall"
)
const (
// LoadScale factor scales the output from sysinfo to the correct float
// value.
LoadScale = 65536 // XXX: is this correct or should it be 65535?
)
// load returns the system load averages for the last minute, five minutes and
// fifteen minutes. Calling this more often than once every five seconds seems
// to be unnecessary, since the kernel only updates these values that often.
// TODO: is the kernel update interval configurable?
func load() (one, five, fifteen float64, err error) {
var sysinfo syscall.Sysinfo_t
if err = syscall.Sysinfo(&sysinfo); err != nil {
return
}
one = float64(sysinfo.Loads[0]) / LoadScale
five = float64(sysinfo.Loads[1]) / LoadScale
fifteen = float64(sysinfo.Loads[2]) / LoadScale
return
}

View File

@@ -1,23 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coresys
const (
// ModuleName is the prefix given to all the functions in this module.
ModuleName = "sys"
)

View File

@@ -1,93 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coresys
import (
"context"
"time"
"github.com/purpleidea/mgmt/lang/funcs/facts"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/util/errwrap"
)
const (
// UptimeFuncName is the name this fact is registered as. It's still a
// Func Name because this is the name space the fact is actually using.
UptimeFuncName = "uptime"
)
func init() {
facts.ModuleRegister(ModuleName, UptimeFuncName, func() facts.Fact { return &UptimeFact{} })
}
// UptimeFact is a fact which returns the current uptime of your system.
type UptimeFact struct {
init *facts.Init
}
// String returns a simple name for this fact. This is needed so this struct can
// satisfy the pgraph.Vertex interface.
func (obj *UptimeFact) String() string {
return UptimeFuncName
}
// Info returns some static info about itself.
func (obj *UptimeFact) Info() *facts.Info {
return &facts.Info{
Output: types.TypeInt,
}
}
// Init runs some startup code for this fact.
func (obj *UptimeFact) Init(init *facts.Init) error {
obj.init = init
return nil
}
// Stream returns the changing values that this fact has over time.
func (obj *UptimeFact) Stream(ctx context.Context) error {
defer close(obj.init.Output)
ticker := time.NewTicker(time.Duration(1) * time.Second)
startChan := make(chan struct{})
close(startChan)
defer ticker.Stop()
for {
select {
case <-startChan:
startChan = nil
case <-ticker.C:
// send
case <-ctx.Done():
return nil
}
uptime, err := uptime()
if err != nil {
return errwrap.Wrapf(err, "could not read uptime value")
}
select {
case obj.init.Output <- &types.IntValue{V: uptime}:
// send
case <-ctx.Done():
return nil
}
}
}

View File

@@ -1,47 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
//go:build darwin
package coresys
import (
"fmt"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/unix"
)
func uptime() (int64, error) {
// get time of boot struct
rawBoottime, err := unix.SysctlRaw("kern.boottime")
if err != nil {
return 0, err
}
// make sure size is correct
var boottime syscall.Timeval
if len(rawBoottime) != int(unsafe.Sizeof(boottime)) {
return 0, fmt.Errorf("invalid boottime encountered while calculating uptime")
}
// uptime is difference between current time and boot time
boottime = *(*syscall.Timeval)(unsafe.Pointer(&rawBoottime[0]))
return time.Now().Unix() - boottime.Sec, nil
}

View File

@@ -1,32 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
//go:build !darwin
package coresys
import (
"golang.org/x/sys/unix"
)
func uptime() (int64, error) {
var sysinfo unix.Sysinfo_t
if err := unix.Sysinfo(&sysinfo); err != nil {
return 0, err
}
return sysinfo.Uptime, nil
}

View File

@@ -1,632 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package core // TODO: should this be in its own individual package?
import (
"bytes"
"context"
"fmt"
"reflect"
"strings"
"text/template"
"github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/util/errwrap"
)
const (
// TemplateFuncName is the name this function is registered as.
TemplateFuncName = "template"
// TemplateName is the name of our template as required by the template
// library.
TemplateName = "template"
// arg names...
templateArgNameTemplate = "template"
templateArgNameVars = "vars"
)
var (
// errorType represents a reflection type of error as seen in:
// https://github.com/golang/go/blob/ec62ee7f6d3839fe69aeae538dadc1c9dc3bf020/src/text/template/exec.go#L612
errorType = reflect.TypeOf((*error)(nil)).Elem()
)
func init() {
funcs.Register(TemplateFuncName, func() interfaces.Func { return &TemplateFunc{} })
}
var _ interfaces.PolyFunc = &TemplateFunc{} // ensure it meets this expectation
// TemplateFunc is a static polymorphic function that compiles a template and
// returns the output as a string. It bases its output on the values passed in
// to it. It examines the type of the second argument (the input data vars) at
// compile time and then determines the static functions signature by including
// that in the overall signature.
// TODO: We *might* need to add events for internal function changes over time,
// but only if they are not pure. We currently only use simple, pure functions.
type TemplateFunc struct {
// Type is the type of the input vars (2nd) arg if one is specified. Nil
// is the special undetermined value that is used before type is known.
Type *types.Type // type of vars
built bool // was this function built yet?
init *interfaces.Init
last types.Value // last value received to use for diff
result *string // last calculated output
}
// String returns a simple name for this function. This is needed so this struct
// can satisfy the pgraph.Vertex interface.
func (obj *TemplateFunc) String() string {
return TemplateFuncName
}
// ArgGen returns the Nth arg name for this function.
func (obj *TemplateFunc) ArgGen(index int) (string, error) {
seq := []string{templateArgNameTemplate, templateArgNameVars}
if l := len(seq); index >= l {
return "", fmt.Errorf("index %d exceeds arg length of %d", index, l)
}
return seq[index], nil
}
// Unify returns the list of invariants that this func produces.
func (obj *TemplateFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) {
var invariants []interfaces.Invariant
var invar interfaces.Invariant
// func(format string) string
// OR
// func(format string, arg variant) string
formatName, err := obj.ArgGen(0)
if err != nil {
return nil, err
}
dummyFormat := &interfaces.ExprAny{} // corresponds to the format type
dummyOut := &interfaces.ExprAny{} // corresponds to the out string
// format arg type of string
invar = &interfaces.EqualsInvariant{
Expr: dummyFormat,
Type: types.TypeStr,
}
invariants = append(invariants, invar)
// return type of string
invar = &interfaces.EqualsInvariant{
Expr: dummyOut,
Type: types.TypeStr,
}
invariants = append(invariants, invar)
// generator function
fn := func(fnInvariants []interfaces.Invariant, solved map[interfaces.Expr]*types.Type) ([]interfaces.Invariant, error) {
for _, invariant := range fnInvariants {
// search for this special type of invariant
cfavInvar, ok := invariant.(*interfaces.CallFuncArgsValueInvariant)
if !ok {
continue
}
// did we find the mapping from us to ExprCall ?
if cfavInvar.Func != expr {
continue
}
// cfavInvar.Expr is the ExprCall! (the return pointer)
// cfavInvar.Args are the args that ExprCall uses!
if len(cfavInvar.Args) == 0 {
return nil, fmt.Errorf("unable to build function with no args")
}
if l := len(cfavInvar.Args); l > 2 {
return nil, fmt.Errorf("unable to build function with %d args", l)
}
// we can either have one arg or two
var invariants []interfaces.Invariant
var invar interfaces.Invariant
// add the relationship to the returned value
invar = &interfaces.EqualityInvariant{
Expr1: cfavInvar.Expr,
Expr2: dummyOut,
}
invariants = append(invariants, invar)
// add the relationships to the called args
invar = &interfaces.EqualityInvariant{
Expr1: cfavInvar.Args[0],
Expr2: dummyFormat,
}
invariants = append(invariants, invar)
// first arg must be a string
invar = &interfaces.EqualsInvariant{
Expr: cfavInvar.Args[0],
Type: types.TypeStr,
}
invariants = append(invariants, invar)
// TODO: if the template is known statically, we could
// parse it to check for variable safety if we wanted!
//value, err := cfavInvar.Args[0].Value() // is it known?
//if err != nil {
//}
// full function
mapped := make(map[string]interfaces.Expr)
ordered := []string{formatName}
mapped[formatName] = dummyFormat
if len(cfavInvar.Args) == 2 { // two args is more complex
argName, err := obj.ArgGen(1) // 1st arg after 0
if err != nil {
return nil, err
}
if argName == templateArgNameTemplate {
return nil, fmt.Errorf("could not build function with %d args", 1)
}
dummyArg := &interfaces.ExprAny{}
// speculate about the type? (maybe redundant)
if typ, err := cfavInvar.Args[1].Type(); err == nil {
invar := &interfaces.EqualsInvariant{
Expr: dummyArg,
Type: typ,
}
invariants = append(invariants, invar)
}
if typ, exists := solved[cfavInvar.Args[1]]; exists { // alternate way to lookup type
invar := &interfaces.EqualsInvariant{
Expr: dummyArg,
Type: typ,
}
invariants = append(invariants, invar)
}
// expression must match type of the input arg
invar := &interfaces.EqualityInvariant{
Expr1: dummyArg,
Expr2: cfavInvar.Args[1],
}
invariants = append(invariants, invar)
mapped[argName] = dummyArg
ordered = append(ordered, argName)
}
invar = &interfaces.EqualityWrapFuncInvariant{
Expr1: expr, // maps directly to us!
Expr2Map: mapped,
Expr2Ord: ordered,
Expr2Out: dummyOut,
}
invariants = append(invariants, invar)
// TODO: do we return this relationship with ExprCall?
invar = &interfaces.EqualityWrapCallInvariant{
// TODO: should Expr1 and Expr2 be reversed???
Expr1: cfavInvar.Expr,
//Expr2Func: cfavInvar.Func, // same as below
Expr2Func: expr,
}
invariants = append(invariants, invar)
// TODO: are there any other invariants we should build?
return invariants, nil // generator return
}
// We couldn't tell the solver anything it didn't already know!
return nil, fmt.Errorf("couldn't generate new invariants")
}
invar = &interfaces.GeneratorInvariant{
Func: fn,
}
invariants = append(invariants, invar)
return invariants, nil
}
// Polymorphisms returns the possible type signatures for this template. In this
// case, since the second argument can be an infinite number of values, it
// instead returns either the final precise type (if it can be gleamed from the
// input partials) or if it cannot, it returns a single entry with the complete
// type but with the variable second argument specified as a `variant` type. If
// it encounters any partial type specifications which are not possible, then it
// errors out. This could happen if you specified a non string template arg.
// XXX: is there a better API than returning a buried `variant` type?
func (obj *TemplateFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) {
// TODO: return `variant` as second arg for now -- maybe there's a better way?
str := fmt.Sprintf("func(%s str, %s variant) str", templateArgNameTemplate, templateArgNameVars)
variant := []*types.Type{types.NewType(str)}
if partialType == nil {
return variant, nil
}
if partialType.Out != nil && partialType.Out.Cmp(types.TypeStr) != nil {
return nil, fmt.Errorf("return value of template must be str")
}
ord := partialType.Ord
if partialType.Map != nil {
if len(ord) != 2 && len(ord) != 1 {
return nil, fmt.Errorf("must have exactly one or two args in template func")
}
if t, exists := partialType.Map[ord[0]]; exists && t != nil {
if t.Cmp(types.TypeStr) != nil {
return nil, fmt.Errorf("first arg for template must be an str")
}
}
if len(ord) == 1 { // no args being passed in (boring template)
return []*types.Type{types.NewType(fmt.Sprintf("func(%s str) str", templateArgNameTemplate))}, nil
} else if t, exists := partialType.Map[ord[1]]; exists && t != nil {
// known vars type! w00t!
return []*types.Type{types.NewType(fmt.Sprintf("func(%s str, %s %s) str", templateArgNameTemplate, templateArgNameVars, t.String()))}, nil
}
}
return variant, nil
}
// Build takes the now known function signature and stores it so that this
// function can appear to be static. It extracts the type of the vars argument,
// which is the dynamic part which can change. That type is used to build our
// function statically.
func (obj *TemplateFunc) Build(typ *types.Type) (*types.Type, error) {
if typ.Kind != types.KindFunc {
return nil, fmt.Errorf("input type must be of kind func")
}
if len(typ.Ord) != 2 && len(typ.Ord) != 1 {
return nil, fmt.Errorf("the template function needs exactly one or two args")
}
if typ.Out == nil {
return nil, fmt.Errorf("return type of function must be specified")
}
if typ.Out.Cmp(types.TypeStr) != nil {
return nil, fmt.Errorf("return type of function must be an str")
}
if typ.Map == nil {
return nil, fmt.Errorf("invalid input type")
}
t0, exists := typ.Map[typ.Ord[0]]
if !exists || t0 == nil {
return nil, fmt.Errorf("first arg must be specified")
}
if t0.Cmp(types.TypeStr) != nil {
return nil, fmt.Errorf("first arg for template must be an str")
}
if len(typ.Ord) == 1 { // no args being passed in (boring template)
obj.built = true
return obj.sig(), nil
}
t1, exists := typ.Map[typ.Ord[1]]
if !exists || t1 == nil {
return nil, fmt.Errorf("second arg must be specified")
}
obj.Type = t1 // extracted vars type is now known!
obj.built = true
return obj.sig(), nil
}
// Validate makes sure we've built our struct properly. It is usually unused for
// normal functions that users can use directly.
func (obj *TemplateFunc) Validate() error {
if !obj.built {
return fmt.Errorf("function wasn't built yet")
}
return nil
}
// Info returns some static info about itself.
func (obj *TemplateFunc) Info() *interfaces.Info {
var sig *types.Type
if obj.built {
sig = obj.sig() // helper
}
return &interfaces.Info{
Pure: true,
Memo: false,
Sig: sig,
Err: obj.Validate(),
}
}
// helper
func (obj *TemplateFunc) sig() *types.Type {
if obj.Type != nil { // don't panic if called speculatively
str := fmt.Sprintf("func(%s str, %s %s) str", templateArgNameTemplate, templateArgNameVars, obj.Type.String())
return types.NewType(str)
}
str := fmt.Sprintf("func(%s str) str", templateArgNameTemplate)
return types.NewType(str)
}
// Init runs some startup code for this function.
func (obj *TemplateFunc) Init(init *interfaces.Init) error {
obj.init = init
return nil
}
// run runs a template and returns the result.
func (obj *TemplateFunc) run(templateText string, vars types.Value) (string, error) {
// see: https://golang.org/pkg/text/template/#FuncMap for more info
// note: we can override any other functions by adding them here...
funcMap := map[string]interface{}{
//"test1": func(in interface{}) (interface{}, error) { // ok
// return fmt.Sprintf("got(%T): %+v", in, in), nil
//},
//"test2": func(in interface{}) interface{} { // NOT ok
// panic("panic") // a panic here brings down everything!
//},
//"test3": func(foo int64) (string, error) { // ok, but errors
// return "", fmt.Errorf("i am an error")
//},
//"test4": func(in1, in2 reflect.Value) (reflect.Value, error) { // ok
// s := fmt.Sprintf("got: %+v and: %+v", in1, in2)
// return reflect.ValueOf(s), nil
//},
}
// FIXME: should we do this once in init() instead, or in the Register
// function in the simple package?
// TODO: loop through this map in a sorted, deterministic order
// XXX: should this use the scope instead (so imports are used properly) ?
for name, fn := range simple.RegisteredFuncs {
name = safename(name) // TODO: rename since we can't include dot
if _, exists := funcMap[name]; exists {
obj.init.Logf("warning, existing function named: `%s` exists", name)
continue
}
// When template execution invokes a function with an argument
// list, that list must be assignable to the function's
// parameter types. Functions meant to apply to arguments of
// arbitrary type can use parameters of type interface{} or of
// type reflect.Value.
f := wrap(name, fn) // wrap it so that it meets API expectations
funcMap[name] = f // add it
}
var err error
tmpl := template.New(TemplateName)
tmpl = tmpl.Funcs(funcMap)
tmpl, err = tmpl.Parse(templateText)
if err != nil {
return "", errwrap.Wrapf(err, "template: parse error")
}
buf := new(bytes.Buffer)
if vars == nil {
// run the template
if err := tmpl.Execute(buf, nil); err != nil {
return "", errwrap.Wrapf(err, "template: execution error")
}
return buf.String(), nil
}
// NOTE: any objects in here can have their methods called by the template!
var data interface{} // can be many types, eg a struct!
v := vars.Copy() // make a copy since we make modifications to it...
Loop:
// TODO: simplify with Type.Underlying()
for {
switch x := v.Type().Kind; x {
case types.KindBool:
fallthrough
case types.KindStr:
fallthrough
case types.KindInt:
fallthrough
case types.KindFloat:
// standalone values can be used in templates with a dot
data = v.Value()
break Loop
case types.KindList:
// TODO: can we improve on this to expose indexes?
data = v.Value()
break Loop
case types.KindMap:
if v.Type().Key.Cmp(types.TypeStr) != nil {
return "", errwrap.Wrapf(err, "template: map keys must be str")
}
m := make(map[string]interface{})
for k, v := range v.Map() { // map[Value]Value
m[k.Str()] = v.Value()
}
data = m
break Loop
case types.KindStruct:
m := make(map[string]interface{})
for k, v := range v.Struct() { // map[string]Value
m[k] = v.Value()
}
data = m
break Loop
// TODO: should we allow functions here?
//case types.KindFunc:
case types.KindVariant:
v = v.(*types.VariantValue).V // un-nest and recurse
continue Loop
default:
return "", fmt.Errorf("can't use `%+v` as vars input", x)
}
}
// run the template
if err := tmpl.Execute(buf, data); err != nil {
return "", errwrap.Wrapf(err, "template: execution error")
}
return buf.String(), nil
}
// Stream returns the changing values that this func has over time.
func (obj *TemplateFunc) Stream(ctx context.Context) error {
defer close(obj.init.Output) // the sender closes
for {
select {
case input, ok := <-obj.init.Input:
if !ok {
return nil // can't output any more
}
//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
st := input.Struct()
tmpl := st[templateArgNameTemplate].Str()
vars, exists := st[templateArgNameVars]
if !exists {
vars = nil
}
result, err := obj.run(tmpl, vars)
if err != nil {
return err // no errwrap needed b/c helper func
}
if obj.result != nil && *obj.result == result {
continue // result didn't change
}
obj.result = &result // store new result
case <-ctx.Done():
return nil
}
select {
case obj.init.Output <- &types.StrValue{
V: *obj.result,
}:
case <-ctx.Done():
return nil
}
}
}
// safename renames the functions so they're valid inside the template. This is
// a limitation of the template library, and it might be worth moving to a new
// one.
func safename(name string) string {
// TODO: should we pick a different replacement char?
char := funcs.ReplaceChar // can't be any of: .-#
result := strings.Replace(name, funcs.ModuleSep, char, -1)
result = strings.Replace(result, "/", char, -1) // nested imports
if result == name {
// No change, so add a prefix for package-less functions... This
// prevents conflicts from sys.func1 -> sys_func1 which would be
// a conflict with a top-level function named sys_func1 which is
// now renamed to _sys_func1.
return char + name
}
return result
}
// wrap builds a function in the format expected by the template engine, and
// returns it as an interface{}. It does so by wrapping our type system and
// function API with what is expected from the reflection API. It returns a
// version that includes the optional second error return value so that our
// functions can return errors without causing a panic.
func wrap(name string, fn *types.FuncValue) interface{} {
if fn.T.Map == nil {
panic("malformed func type")
}
if len(fn.T.Map) != len(fn.T.Ord) {
panic("malformed func length")
}
in := []reflect.Type{}
for _, k := range fn.T.Ord {
t, ok := fn.T.Map[k]
if !ok {
panic("malformed func order")
}
if t == nil {
panic("malformed func arg")
}
in = append(in, t.Reflect())
}
out := []reflect.Type{fn.T.Out.Reflect(), errorType}
var variadic = false // currently not supported in our function value
typ := reflect.FuncOf(in, out, variadic)
// wrap our function with the translation that is necessary
f := func(args []reflect.Value) (results []reflect.Value) { // build
innerArgs := []types.Value{}
zeroValue := reflect.Zero(fn.T.Out.Reflect()) // zero value of return type
for _, x := range args {
v, err := types.ValueOf(x) // reflect.Value -> Value
if err != nil {
r := reflect.ValueOf(errwrap.Wrapf(err, "function `%s` errored", name))
if !r.Type().ConvertibleTo(errorType) { // for fun!
r = reflect.ValueOf(fmt.Errorf("function `%s` errored: %+v", name, err))
}
e := r.Convert(errorType) // must be seen as an `error`
return []reflect.Value{zeroValue, e}
}
innerArgs = append(innerArgs, v)
}
result, err := fn.Call(innerArgs) // call it
if err != nil { // function errored :(
// errwrap is a better way to report errors, if allowed!
r := reflect.ValueOf(errwrap.Wrapf(err, "function `%s` errored", name))
if !r.Type().ConvertibleTo(errorType) { // for fun!
r = reflect.ValueOf(fmt.Errorf("function `%s` errored: %+v", name, err))
}
e := r.Convert(errorType) // must be seen as an `error`
return []reflect.Value{zeroValue, e}
} else if result == nil { // someone wrote a bad function
r := reflect.ValueOf(fmt.Errorf("function `%s` returned nil", name))
e := r.Convert(errorType) // must be seen as an `error`
return []reflect.Value{zeroValue, e}
}
nilError := reflect.Zero(errorType)
return []reflect.Value{reflect.ValueOf(result.Value()), nilError}
}
val := reflect.MakeFunc(typ, f)
return val.Interface()
}

View File

@@ -1,84 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coretest
import (
"context"
"github.com/purpleidea/mgmt/lang/funcs/facts"
"github.com/purpleidea/mgmt/lang/types"
)
const (
// FastCountFuncName is the name this fact is registered as. It's still
// a Func Name because this is the name space the fact is actually
// using.
FastCountFuncName = "fastcount"
)
func init() {
facts.ModuleRegister(ModuleName, FastCountFuncName, func() facts.Fact { return &FastCountFact{} }) // must register the fact and name
}
// FastCountFact is a fact that counts up as fast as possible from zero forever.
type FastCountFact struct {
init *facts.Init
}
// String returns a simple name for this fact. This is needed so this struct can
// satisfy the pgraph.Vertex interface.
func (obj *FastCountFact) String() string {
return FastCountFuncName
}
// Validate makes sure we've built our struct properly. It is usually unused for
// normal facts that users can use directly.
//func (obj *FastCountFact) Validate() error {
// return nil
//}
// Info returns some static info about itself.
func (obj *FastCountFact) Info() *facts.Info {
return &facts.Info{
Output: types.NewType("int"),
}
}
// Init runs some startup code for this fact.
func (obj *FastCountFact) Init(init *facts.Init) error {
obj.init = init
return nil
}
// Stream returns the changing values that this fact has over time.
func (obj *FastCountFact) Stream(ctx context.Context) error {
defer close(obj.init.Output) // always signal when we're done
count := int64(0)
// streams must generate an initial event on startup
for {
select {
case obj.init.Output <- &types.IntValue{V: count}:
count++
case <-ctx.Done():
return nil
}
}
}

View File

@@ -1,239 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coretest
import (
"context"
"sync"
"github.com/purpleidea/mgmt/lang/funcs/facts"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
const (
// OneInstanceAFuncName is the name this fact is registered as. It's
// still a Func Name because this is the name space the fact is actually
// using.
OneInstanceAFuncName = "one_instance_a"
// OneInstanceBFuncName is the name this fact is registered as. It's
// still a Func Name because this is the name space the fact is actually
// using.
OneInstanceBFuncName = "one_instance_b"
// OneInstanceCFuncName is the name this fact is registered as. It's
// still a Func Name because this is the name space the fact is actually
// using.
OneInstanceCFuncName = "one_instance_c"
// OneInstanceDFuncName is the name this fact is registered as. It's
// still a Func Name because this is the name space the fact is actually
// using.
OneInstanceDFuncName = "one_instance_d"
// OneInstanceEFuncName is the name this fact is registered as. It's
// still a Func Name because this is the name space the fact is actually
// using.
OneInstanceEFuncName = "one_instance_e"
// OneInstanceFFuncName is the name this fact is registered as. It's
// still a Func Name because this is the name space the fact is actually
// using.
OneInstanceFFuncName = "one_instance_f"
// OneInstanceGFuncName is the name this fact is registered as. It's
// still a Func Name because this is the name space the fact is actually
// using.
OneInstanceGFuncName = "one_instance_g"
// OneInstanceHFuncName is the name this fact is registered as. It's
// still a Func Name because this is the name space the fact is actually
// using.
OneInstanceHFuncName = "one_instance_h"
msg = "hello"
)
func init() {
oneInstanceAMutex = &sync.Mutex{}
oneInstanceBMutex = &sync.Mutex{}
oneInstanceCMutex = &sync.Mutex{}
oneInstanceDMutex = &sync.Mutex{}
oneInstanceEMutex = &sync.Mutex{}
oneInstanceFMutex = &sync.Mutex{}
oneInstanceGMutex = &sync.Mutex{}
oneInstanceHMutex = &sync.Mutex{}
facts.ModuleRegister(ModuleName, OneInstanceAFuncName, func() facts.Fact {
return &OneInstanceFact{
Name: OneInstanceAFuncName,
Mutex: oneInstanceAMutex,
Flag: &oneInstanceAFlag,
}
}) // must register the fact and name
facts.ModuleRegister(ModuleName, OneInstanceCFuncName, func() facts.Fact {
return &OneInstanceFact{
Name: OneInstanceCFuncName,
Mutex: oneInstanceCMutex,
Flag: &oneInstanceCFlag,
}
})
facts.ModuleRegister(ModuleName, OneInstanceEFuncName, func() facts.Fact {
return &OneInstanceFact{
Name: OneInstanceEFuncName,
Mutex: oneInstanceEMutex,
Flag: &oneInstanceEFlag,
}
})
facts.ModuleRegister(ModuleName, OneInstanceGFuncName, func() facts.Fact {
return &OneInstanceFact{
Name: OneInstanceGFuncName,
Mutex: oneInstanceGMutex,
Flag: &oneInstanceGFlag,
}
})
simple.ModuleRegister(ModuleName, OneInstanceBFuncName, &types.FuncValue{
T: types.NewType("func() str"),
V: func([]types.Value) (types.Value, error) {
oneInstanceBMutex.Lock()
if oneInstanceBFlag {
panic("should not get called twice")
}
oneInstanceBFlag = true
oneInstanceBMutex.Unlock()
return &types.StrValue{V: msg}, nil
},
})
simple.ModuleRegister(ModuleName, OneInstanceDFuncName, &types.FuncValue{
T: types.NewType("func() str"),
V: func([]types.Value) (types.Value, error) {
oneInstanceDMutex.Lock()
if oneInstanceDFlag {
panic("should not get called twice")
}
oneInstanceDFlag = true
oneInstanceDMutex.Unlock()
return &types.StrValue{V: msg}, nil
},
})
simple.ModuleRegister(ModuleName, OneInstanceFFuncName, &types.FuncValue{
T: types.NewType("func() str"),
V: func([]types.Value) (types.Value, error) {
oneInstanceFMutex.Lock()
if oneInstanceFFlag {
panic("should not get called twice")
}
oneInstanceFFlag = true
oneInstanceFMutex.Unlock()
return &types.StrValue{V: msg}, nil
},
})
simple.ModuleRegister(ModuleName, OneInstanceHFuncName, &types.FuncValue{
T: types.NewType("func() str"),
V: func([]types.Value) (types.Value, error) {
oneInstanceHMutex.Lock()
if oneInstanceHFlag {
panic("should not get called twice")
}
oneInstanceHFlag = true
oneInstanceHMutex.Unlock()
return &types.StrValue{V: msg}, nil
},
})
}
var (
oneInstanceAFlag bool
oneInstanceBFlag bool
oneInstanceCFlag bool
oneInstanceDFlag bool
oneInstanceEFlag bool
oneInstanceFFlag bool
oneInstanceGFlag bool
oneInstanceHFlag bool
oneInstanceAMutex *sync.Mutex
oneInstanceBMutex *sync.Mutex
oneInstanceCMutex *sync.Mutex
oneInstanceDMutex *sync.Mutex
oneInstanceEMutex *sync.Mutex
oneInstanceFMutex *sync.Mutex
oneInstanceGMutex *sync.Mutex
oneInstanceHMutex *sync.Mutex
)
// OneInstanceFact is a fact which flips a bool repeatedly. This is an example
// fact and is not meant for serious computing. This would be better served by a
// flip function which you could specify an interval for.
type OneInstanceFact struct {
init *facts.Init
Name string
Mutex *sync.Mutex
Flag *bool
}
// String returns a simple name for this fact. This is needed so this struct can
// satisfy the pgraph.Vertex interface.
func (obj *OneInstanceFact) String() string {
return obj.Name
}
// Validate makes sure we've built our struct properly. It is usually unused for
// normal facts that users can use directly.
//func (obj *OneInstanceFact) Validate() error {
// return nil
//}
// Info returns some static info about itself.
func (obj *OneInstanceFact) Info() *facts.Info {
return &facts.Info{
Output: types.NewType("str"),
}
}
// Init runs some startup code for this fact.
func (obj *OneInstanceFact) Init(init *facts.Init) error {
obj.init = init
obj.init.Logf("Init of `%s` @ %p", obj.Name, obj)
obj.Mutex.Lock()
if *obj.Flag {
panic("should not get called twice")
}
b := true
obj.Flag = &b
obj.Mutex.Unlock()
return nil
}
// Stream returns the changing values that this fact has over time.
func (obj *OneInstanceFact) Stream(ctx context.Context) error {
obj.init.Logf("Stream of `%s` @ %p", obj.Name, obj)
defer close(obj.init.Output) // always signal when we're done
select {
case obj.init.Output <- &types.StrValue{
V: msg,
}:
case <-ctx.Done():
return nil
}
return nil
}

View File

@@ -1,23 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coretest
const (
// ModuleName is the prefix given to all the functions in this module.
ModuleName = "test"
)

View File

@@ -1,545 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package corevalue
import (
"context"
"fmt"
"github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/util/errwrap"
)
const (
// GetFuncName is the name this function is registered as. This variant
// is the fanciest version, although type unification is much more
// difficult when using this.
// XXX: type unification doesn't work perfectly here yet... maybe a bug with returned structs?
GetFuncName = "get"
// GetBoolFuncName is the name this function is registered as. This
// variant can only pull in values of type bool.
GetBoolFuncName = "get_bool"
// GetStrFuncName is the name this function is registered as. This
// variant can only pull in values of type str.
GetStrFuncName = "get_str"
// GetIntFuncName is the name this function is registered as. This
// variant can only pull in values of type int.
GetIntFuncName = "get_int"
// GetFloatFuncName is the name this function is registered as. This
// variant can only pull in values of type float.
GetFloatFuncName = "get_float"
// arg names...
getArgNameKey = "key"
// struct field names...
getFieldNameValue = "value"
getFieldNameReady = "ready"
)
func init() {
funcs.ModuleRegister(ModuleName, GetFuncName, func() interfaces.Func { return &GetFunc{} })
funcs.ModuleRegister(ModuleName, GetBoolFuncName, func() interfaces.Func { return &GetFunc{Type: types.TypeBool} })
funcs.ModuleRegister(ModuleName, GetStrFuncName, func() interfaces.Func { return &GetFunc{Type: types.TypeStr} })
funcs.ModuleRegister(ModuleName, GetIntFuncName, func() interfaces.Func { return &GetFunc{Type: types.TypeInt} })
funcs.ModuleRegister(ModuleName, GetFloatFuncName, func() interfaces.Func { return &GetFunc{Type: types.TypeFloat} })
}
// GetFunc is special function which looks up the stored `Any` field in the
// value resource that it gets it from. If it is initialized with a fixed Type
// field, then it becomes a statically typed version that can only return keys
// of that type. It is instead recommended to use the Get* functions that are
// more strictly typed.
type GetFunc struct {
// Type is the actual type being used for the value we are looking up.
Type *types.Type
init *interfaces.Init
key string
last types.Value
result types.Value // last calculated output
watchChan chan struct{}
}
// String returns a simple name for this function. This is needed so this struct
// can satisfy the pgraph.Vertex interface.
func (obj *GetFunc) String() string {
return GetFuncName
}
// ArgGen returns the Nth arg name for this function.
func (obj *GetFunc) ArgGen(index int) (string, error) {
seq := []string{getArgNameKey}
if l := len(seq); index >= l {
return "", fmt.Errorf("index %d exceeds arg length of %d", index, l)
}
return seq[index], nil
}
// Unify returns the list of invariants that this func produces.
func (obj *GetFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) {
var invariants []interfaces.Invariant
var invar interfaces.Invariant
if obj.Type != nil { // if we set the type statically, unify is simple
sig := obj.sig() // helper
invar = &interfaces.EqualsInvariant{
Expr: expr,
Type: sig,
}
invariants = append(invariants, invar)
return invariants, nil
}
// func(key str) struct{value T1; ready bool}
keyName, err := obj.ArgGen(0)
if err != nil {
return nil, err
}
dummyKey := &interfaces.ExprAny{} // corresponds to the key type
dummyOut := &interfaces.ExprAny{} // corresponds to the out struct
dummyValue := &interfaces.ExprAny{} // corresponds to the value type
dummyReady := &interfaces.ExprAny{} // corresponds to the ready type
// the known types...
invar = &interfaces.EqualsInvariant{
Expr: dummyKey,
Type: types.TypeStr,
}
invariants = append(invariants, invar)
invar = &interfaces.EqualsInvariant{
Expr: dummyReady,
Type: types.TypeBool,
}
invariants = append(invariants, invar)
// relationship between Out and T1
// TODO: do the precise field string names matter or can we cmp anyways?
mapped := make(map[string]interfaces.Expr)
ordered := []string{getFieldNameValue, getFieldNameReady}
mapped[getFieldNameValue] = dummyValue
mapped[getFieldNameReady] = dummyReady
invar = &interfaces.EqualityWrapStructInvariant{
Expr1: dummyOut, // unique id for this expression (a pointer)
Expr2Map: mapped,
Expr2Ord: ordered,
}
invariants = append(invariants, invar)
// full function
invar = &interfaces.EqualityWrapFuncInvariant{
Expr1: expr, // maps directly to us!
Expr2Map: map[string]interfaces.Expr{keyName: dummyKey},
Expr2Ord: []string{keyName},
Expr2Out: dummyOut,
}
invariants = append(invariants, invar)
// generator function
fn := func(fnInvariants []interfaces.Invariant, solved map[interfaces.Expr]*types.Type) ([]interfaces.Invariant, error) {
for _, invariant := range fnInvariants {
// search for this special type of invariant
cfavInvar, ok := invariant.(*interfaces.CallFuncArgsValueInvariant)
if !ok {
continue
}
// did we find the mapping from us to ExprCall ?
if cfavInvar.Func != expr {
continue
}
// cfavInvar.Expr is the ExprCall! (the return pointer)
// cfavInvar.Args are the args that ExprCall uses!
if l := len(cfavInvar.Args); l != 1 {
return nil, fmt.Errorf("unable to build function with %d args", l)
}
var invariants []interfaces.Invariant
var invar interfaces.Invariant
// add the relationship to the returned value
invar = &interfaces.EqualityInvariant{
Expr1: cfavInvar.Expr,
Expr2: dummyOut,
}
invariants = append(invariants, invar)
// add the relationships to the called args
invar = &interfaces.EqualityInvariant{
Expr1: cfavInvar.Args[0],
Expr2: dummyKey,
}
invariants = append(invariants, invar)
// If we figure out this type, we'll know the full type!
var t1 *types.Type // value type
// validateArg0 checks: key input
validateArg0 := func(typ *types.Type) error {
if typ == nil { // unknown so far
return nil
}
if typ.Kind != types.KindStr {
return errwrap.Wrapf(err, "input index type was inconsistent")
}
return nil
}
// validateOut checks: T1
validateOut := func(typ *types.Type) error {
if typ == nil { // unknown so far
return nil
}
// we happen to have a struct!
if k := typ.Kind; k != types.KindStruct {
return fmt.Errorf("unable to build function with return type of kind: %s", k)
}
if typ.Map == nil || len(typ.Ord) == 0 {
// programming error
return fmt.Errorf("return struct is missing type")
}
// TODO: do the precise field string names
// matter or can we cmp anyways?
tReady, exists := typ.Map[getFieldNameReady]
if !exists {
return fmt.Errorf("return struct is missing ready field")
}
if tReady.Kind != types.KindBool {
return fmt.Errorf("return struct ready field must be bool kind")
}
tValue, exists := typ.Map[getFieldNameValue]
if !exists {
return fmt.Errorf("return struct is missing value field")
}
if err := tValue.Cmp(t1); t1 != nil && err != nil {
return errwrap.Wrapf(err, "value type was inconsistent")
}
// learn!
t1 = tValue
return nil
}
if typ, err := cfavInvar.Args[0].Type(); err == nil { // is it known?
// this only checks if this is an str
if err := validateArg0(typ); err != nil {
return nil, errwrap.Wrapf(err, "first list arg type is inconsistent")
}
}
if typ, exists := solved[cfavInvar.Args[0]]; exists { // alternate way to lookup type
// this only checks if this is an str
if err := validateArg0(typ); err != nil {
return nil, errwrap.Wrapf(err, "first list arg type is inconsistent")
}
}
// return type...
if typ, err := cfavInvar.Expr.Type(); err == nil { // is it known?
if err := validateOut(typ); err != nil {
return nil, errwrap.Wrapf(err, "return type is inconsistent")
}
}
if typ, exists := solved[cfavInvar.Expr]; exists { // alternate way to lookup type
if err := validateOut(typ); err != nil {
return nil, errwrap.Wrapf(err, "return type is inconsistent")
}
}
// XXX: We need to add a relationship somehow here or
// elsewhere between dummyValue and the type we are
// expecting.
// (1) we shouldn't look on disk in the cached storage.
// (2) how can we match on function send/recv values and
// resource fields???
// (3) worst case scenario we just hope for the best,
// and hope we can infer the type some other way...
// XXX: if the types aren't know statically?
if t1 != nil {
invar := &interfaces.EqualsInvariant{
Expr: dummyValue,
Type: t1,
}
invariants = append(invariants, invar)
}
// XXX: if t1 is missing, we could also return a new
// generator for later if we learn new information, but
// we'd have to be careful to not do it infinitely.
// TODO: do we return this relationship with ExprCall?
invar = &interfaces.EqualityWrapCallInvariant{
// TODO: should Expr1 and Expr2 be reversed???
Expr1: cfavInvar.Expr,
//Expr2Func: cfavInvar.Func, // same as below
Expr2Func: expr,
}
invariants = append(invariants, invar)
// TODO: are there any other invariants we should build?
return invariants, nil // generator return
}
// We couldn't tell the solver anything it didn't already know!
return nil, fmt.Errorf("couldn't generate new invariants")
}
invar = &interfaces.GeneratorInvariant{
Func: fn,
}
invariants = append(invariants, invar)
return invariants, nil
}
// Build is run to turn the polymorphic, undetermined function, into the
// specific statically typed version. It is usually run after Unify completes,
// and must be run before Info() and any of the other Func interface methods are
// used. This function is idempotent, as long as the arg isn't changed between
// runs.
func (obj *GetFunc) Build(typ *types.Type) (*types.Type, error) {
// typ is the KindFunc signature we're trying to build...
if typ.Kind != types.KindFunc {
return nil, fmt.Errorf("input type must be of kind func")
}
if typ.Map == nil {
return nil, fmt.Errorf("invalid input type")
}
if len(typ.Ord) != 1 {
return nil, fmt.Errorf("the function needs exactly one arg")
}
if typ.Out == nil {
return nil, fmt.Errorf("return type of function must be specified")
}
tKey, exists := typ.Map[typ.Ord[0]]
if !exists || tKey == nil {
return nil, fmt.Errorf("first arg must be specified")
}
if tKey.Kind != types.KindStr {
return nil, fmt.Errorf("key must be str kind")
}
if typ.Out.Kind != types.KindStruct {
return nil, fmt.Errorf("return must be kind struct")
}
if typ.Out.Map == nil {
return nil, fmt.Errorf("invalid return type")
}
if len(typ.Out.Ord) != 2 {
return nil, fmt.Errorf("invalid return type")
}
tValue, exists := typ.Out.Map[typ.Out.Ord[0]]
if !exists || tValue == nil {
return nil, fmt.Errorf("first struct field must be specified")
}
tReady, exists := typ.Out.Map[typ.Out.Ord[1]]
if !exists || tReady == nil {
return nil, fmt.Errorf("second struct field must be specified")
}
if tReady.Kind != types.KindBool {
return nil, fmt.Errorf("second struct field must be bool kind")
}
obj.Type = tValue // type of our value
return obj.sig(), nil
}
// Validate makes sure we've built our struct properly. It is usually unused for
// normal functions that users can use directly.
func (obj *GetFunc) Validate() error {
return nil
}
// Info returns some static info about itself.
func (obj *GetFunc) Info() *interfaces.Info {
var sig *types.Type
if obj.Type != nil { // don't panic if called speculatively
sig = obj.sig() // helper
}
return &interfaces.Info{
Pure: false, // definitely false
Memo: false,
Sig: sig,
Err: obj.Validate(),
}
}
// helper
func (obj *GetFunc) sig() *types.Type {
// output is a struct with two fields:
// value is the zero value if not ready. A bool for that in other field.
return types.NewType(fmt.Sprintf("func(%s str) struct{%s %s; %s bool}", getArgNameKey, getFieldNameValue, obj.Type.String(), getFieldNameReady))
}
// Init runs some startup code for this function.
func (obj *GetFunc) Init(init *interfaces.Init) error {
obj.init = init
obj.watchChan = make(chan struct{}) // sender closes this when Stream ends
return nil
}
// Stream returns the changing values that this func has over time.
func (obj *GetFunc) Stream(ctx context.Context) error {
defer close(obj.init.Output) // the sender closes
ctx, cancel := context.WithCancel(ctx)
defer cancel() // important so that we cleanup the watch when exiting
for {
select {
// TODO: should this first chan be run as a priority channel to
// avoid some sort of glitch? is that even possible? can our
// hostname check with reality (below) fix that?
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
key := input.Struct()[getArgNameKey].Str()
if key == "" {
return fmt.Errorf("can't use an empty key")
}
if obj.init.Debug {
obj.init.Logf("key: %s", key)
}
// We don't support changing the key over time, since it
// might cause the type to need to be changed.
if obj.key == "" {
obj.key = key // store it
var err error
// Don't send a value right away, wait for the
// first ValueWatch startup event to get one!
obj.watchChan, err = obj.init.Local.ValueWatch(ctx, obj.key) // watch for var changes
if err != nil {
return err
}
} else if obj.key != key {
return fmt.Errorf("can't change key, previously: `%s`", obj.key)
}
continue // we get values on the watch chan, not here!
case _, ok := <-obj.watchChan:
if !ok { // closed
return nil
}
//if err != nil {
// return errwrap.Wrapf(err, "channel watch failed on `%s`", obj.key)
//}
result, err := obj.getValue(ctx) // get the value...
if err != nil {
return err
}
// if the result is still the same, skip sending an update...
if obj.result != nil && result.Cmp(obj.result) == nil {
continue // result didn't change
}
obj.result = result // store new result
case <-ctx.Done():
return nil
}
select {
case obj.init.Output <- obj.result: // send
// pass
case <-ctx.Done():
return nil
}
}
}
// getValue gets the value we're looking for.
func (obj *GetFunc) getValue(ctx context.Context) (types.Value, error) {
typ, exists := obj.Info().Sig.Out.Map[getFieldNameValue] // type of value field
if !exists || typ == nil {
// programming error
return nil, fmt.Errorf("missing type for %s field", getFieldNameValue)
}
// The API will pull from the on-disk stored cache if present... This
// value comes from the field in the Value resource... We only have an
// on-disk cache because since functions load before resources do, we'd
// like to warm the cache with the right value before the resource can
// issue a new one to our in-memory store. This avoids a re-provisioning
// step that might be needed if the value started out empty...
// TODO: We could even add a stored: bool field in the returned struct!
isReady := true // assume true
val, err := obj.init.Local.ValueGet(ctx, obj.key)
if err != nil {
return nil, errwrap.Wrapf(err, "channel read failed on `%s`", obj.key)
}
if val == nil { // val doesn't exist
isReady = false
}
ready := &types.BoolValue{V: isReady}
value := typ.New() // new zero value of that typ
if isReady {
value, err = types.ValueOfGolang(val) // interface{} -> types.Value
if err != nil {
// programming error
return nil, errwrap.Wrapf(err, "invalid value")
}
if err := value.Type().Cmp(typ); err != nil {
// XXX: when we run get_int, but the resource value is
// an str for example, this error happens... Do we want
// to: (1) coerce? -- no; (2) error? -- yep for now; (3)
// improve type unification? -- if it's possible, yes.
return nil, errwrap.Wrapf(err, "type mismatch, check type in Value[%s]", obj.key)
}
}
st := types.NewStruct(obj.Info().Sig.Out)
if err := st.Set(getFieldNameValue, value); err != nil {
return nil, errwrap.Wrapf(err, "struct could not add field `%s`, val: `%s`", getFieldNameValue, value)
}
if err := st.Set(getFieldNameReady, ready); err != nil {
return nil, errwrap.Wrapf(err, "struct could not add field `%s`, val: `%s`", getFieldNameReady, ready)
}
return st, nil // put struct into interface type
}

View File

@@ -1,23 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package corevalue
const (
// ModuleName is the prefix given to all the functions in this module.
ModuleName = "value"
)

View File

@@ -1,200 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreworld
import (
"context"
"fmt"
"github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/util/errwrap"
)
const (
// ExchangeFuncName is the name this function is registered as.
ExchangeFuncName = "exchange"
// arg names...
exchangeArgNameNamespace = "namespace"
exchangeArgNameValue = "value"
)
func init() {
funcs.ModuleRegister(ModuleName, ExchangeFuncName, func() interfaces.Func { return &ExchangeFunc{} })
}
// ExchangeFunc is special function which returns all the values of a given key
// in the exposed world, and sets it's own.
type ExchangeFunc struct {
init *interfaces.Init
namespace string
last types.Value
result types.Value // last calculated output
watchChan chan error
}
// String returns a simple name for this function. This is needed so this struct
// can satisfy the pgraph.Vertex interface.
func (obj *ExchangeFunc) String() string {
return ExchangeFuncName
}
// ArgGen returns the Nth arg name for this function.
func (obj *ExchangeFunc) ArgGen(index int) (string, error) {
seq := []string{exchangeArgNameNamespace, exchangeArgNameValue}
if l := len(seq); index >= l {
return "", fmt.Errorf("index %d exceeds arg length of %d", index, l)
}
return seq[index], nil
}
// Validate makes sure we've built our struct properly. It is usually unused for
// normal functions that users can use directly.
func (obj *ExchangeFunc) Validate() error {
return nil
}
// Info returns some static info about itself.
func (obj *ExchangeFunc) Info() *interfaces.Info {
return &interfaces.Info{
Pure: false, // definitely false
Memo: false,
// TODO: do we want to allow this to be statically polymorphic,
// and have value be any type we might want?
// output is map of: hostname => value
Sig: types.NewType(fmt.Sprintf("func(%s str, %s str) map{str: str}", exchangeArgNameNamespace, exchangeArgNameValue)),
Err: obj.Validate(),
}
}
// Init runs some startup code for this function.
func (obj *ExchangeFunc) Init(init *interfaces.Init) error {
obj.init = init
obj.watchChan = make(chan error) // XXX: sender should close this, but did I implement that part yet???
return nil
}
// Stream returns the changing values that this func has over time.
func (obj *ExchangeFunc) Stream(ctx context.Context) error {
defer close(obj.init.Output) // the sender closes
ctx, cancel := context.WithCancel(ctx)
defer cancel()
for {
select {
// TODO: should this first chan be run as a priority channel to
// avoid some sort of glitch? is that even possible? can our
// hostname check with reality (below) fix that?
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
namespace := input.Struct()[exchangeArgNameNamespace].Str()
if namespace == "" {
return fmt.Errorf("can't use an empty namespace")
}
if obj.init.Debug {
obj.init.Logf("namespace: %s", namespace)
}
// TODO: support changing the namespace over time...
// TODO: possibly removing our stored value there first!
if obj.namespace == "" {
obj.namespace = namespace // store it
var err error
obj.watchChan, err = obj.init.World.StrMapWatch(ctx, obj.namespace) // watch for var changes
if err != nil {
return err
}
} else if obj.namespace != namespace {
return fmt.Errorf("can't change namespace, previously: `%s`", obj.namespace)
}
value := input.Struct()[exchangeArgNameValue].Str()
if obj.init.Debug {
obj.init.Logf("value: %+v", value)
}
if err := obj.init.World.StrMapSet(ctx, obj.namespace, value); err != nil {
return errwrap.Wrapf(err, "namespace write error of `%s` to `%s`", value, obj.namespace)
}
continue // we get values on the watch chan, not here!
case err, ok := <-obj.watchChan:
if !ok { // closed
// XXX: if we close, perhaps the engine is
// switching etcd hosts and we should retry?
// maybe instead we should get an "etcd
// reconnect" signal, and the lang will restart?
return nil
}
if err != nil {
return errwrap.Wrapf(err, "channel watch failed on `%s`", obj.namespace)
}
keyMap, err := obj.init.World.StrMapGet(ctx, obj.namespace)
if err != nil {
return errwrap.Wrapf(err, "channel read failed on `%s`", obj.namespace)
}
var result types.Value
d := types.NewMap(obj.Info().Sig.Out)
for k, v := range keyMap {
key := &types.StrValue{V: k}
val := &types.StrValue{V: v}
if err := d.Add(key, val); err != nil {
return errwrap.Wrapf(err, "map could not add key `%s`, val: `%s`", k, v)
}
}
result = d // put map into interface type
// if the result is still the same, skip sending an update...
if obj.result != nil && result.Cmp(obj.result) == nil {
continue // result didn't change
}
obj.result = result // store new result
case <-ctx.Done():
return nil
}
select {
case obj.init.Output <- obj.result: // send
// pass
case <-ctx.Done():
return nil
}
}
}

View File

@@ -1,205 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreworld
import (
"context"
"fmt"
"github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/util/errwrap"
)
const (
// GetValFuncName is the name this function is registered as.
GetValFuncName = "getval"
// arg names...
getValArgNameKey = "key"
// struct field names...
getValFieldNameValue = "value"
getValFieldNameExists = "exists"
)
func init() {
funcs.ModuleRegister(ModuleName, GetValFuncName, func() interfaces.Func { return &GetValFunc{} })
}
// GetValFunc is special function which returns the value of a given key in the
// exposed world.
type GetValFunc struct {
init *interfaces.Init
key string
last types.Value
result types.Value // last calculated output
watchChan chan error
}
// String returns a simple name for this function. This is needed so this struct
// can satisfy the pgraph.Vertex interface.
func (obj *GetValFunc) String() string {
return GetValFuncName
}
// ArgGen returns the Nth arg name for this function.
func (obj *GetValFunc) ArgGen(index int) (string, error) {
seq := []string{getValArgNameKey}
if l := len(seq); index >= l {
return "", fmt.Errorf("index %d exceeds arg length of %d", index, l)
}
return seq[index], nil
}
// Validate makes sure we've built our struct properly. It is usually unused for
// normal functions that users can use directly.
func (obj *GetValFunc) Validate() error {
return nil
}
// Info returns some static info about itself.
func (obj *GetValFunc) Info() *interfaces.Info {
return &interfaces.Info{
Pure: false, // definitely false
Memo: false,
// output is a struct with two fields:
// value is the zero value if not exists. A bool for that in other field.
Sig: types.NewType(fmt.Sprintf("func(%s str) struct{%s str; %s bool}", getValArgNameKey, getValFieldNameValue, getValFieldNameExists)),
Err: obj.Validate(),
}
}
// Init runs some startup code for this function.
func (obj *GetValFunc) Init(init *interfaces.Init) error {
obj.init = init
obj.watchChan = make(chan error) // XXX: sender should close this, but did I implement that part yet???
return nil
}
// Stream returns the changing values that this func has over time.
func (obj *GetValFunc) Stream(ctx context.Context) error {
defer close(obj.init.Output) // the sender closes
ctx, cancel := context.WithCancel(ctx)
defer cancel() // important so that we cleanup the watch when exiting
for {
select {
// TODO: should this first chan be run as a priority channel to
// avoid some sort of glitch? is that even possible? can our
// hostname check with reality (below) fix that?
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
key := input.Struct()[getValArgNameKey].Str()
if key == "" {
return fmt.Errorf("can't use an empty key")
}
if obj.init.Debug {
obj.init.Logf("key: %s", key)
}
// TODO: support changing the key over time...
if obj.key == "" {
obj.key = key // store it
var err error
// Don't send a value right away, wait for the
// first ValueWatch startup event to get one!
obj.watchChan, err = obj.init.World.StrWatch(ctx, obj.key) // watch for var changes
if err != nil {
return err
}
} else if obj.key != key {
return fmt.Errorf("can't change key, previously: `%s`", obj.key)
}
continue // we get values on the watch chan, not here!
case err, ok := <-obj.watchChan:
if !ok { // closed
// XXX: if we close, perhaps the engine is
// switching etcd hosts and we should retry?
// maybe instead we should get an "etcd
// reconnect" signal, and the lang will restart?
return nil
}
if err != nil {
return errwrap.Wrapf(err, "channel watch failed on `%s`", obj.key)
}
result, err := obj.getValue(ctx) // get the value...
if err != nil {
return err
}
// if the result is still the same, skip sending an update...
if obj.result != nil && result.Cmp(obj.result) == nil {
continue // result didn't change
}
obj.result = result // store new result
case <-ctx.Done():
return nil
}
select {
case obj.init.Output <- obj.result: // send
// pass
case <-ctx.Done():
return nil
}
}
}
// getValue gets the value we're looking for.
func (obj *GetValFunc) getValue(ctx context.Context) (types.Value, error) {
exists := true // assume true
val, err := obj.init.World.StrGet(ctx, obj.key)
if err != nil && obj.init.World.StrIsNotExist(err) {
exists = false // val doesn't exist
} else if err != nil {
return nil, errwrap.Wrapf(err, "channel read failed on `%s`", obj.key)
}
s := &types.StrValue{V: val}
b := &types.BoolValue{V: exists}
st := types.NewStruct(obj.Info().Sig.Out)
if err := st.Set(getValFieldNameValue, s); err != nil {
return nil, errwrap.Wrapf(err, "struct could not add field `%s`, val: `%s`", getValFieldNameValue, s)
}
if err := st.Set(getValFieldNameExists, b); err != nil {
return nil, errwrap.Wrapf(err, "struct could not add field `%s`, val: `%s`", getValFieldNameExists, b)
}
return st, nil // put struct into interface type
}

View File

@@ -1,205 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreworld
import (
"context"
"fmt"
"github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/util/errwrap"
)
const (
// KVLookupFuncName is the name this function is registered as.
KVLookupFuncName = "kvlookup"
// arg names...
kvLookupArgNameNamespace = "namespace"
)
func init() {
funcs.ModuleRegister(ModuleName, KVLookupFuncName, func() interfaces.Func { return &KVLookupFunc{} })
}
// KVLookupFunc is special function which returns all the values of a given key
// in the exposed world. It is similar to exchange, but it does not set a key.
type KVLookupFunc struct {
init *interfaces.Init
namespace string
last types.Value
result types.Value // last calculated output
watchChan chan error
}
// String returns a simple name for this function. This is needed so this struct
// can satisfy the pgraph.Vertex interface.
func (obj *KVLookupFunc) String() string {
return KVLookupFuncName
}
// ArgGen returns the Nth arg name for this function.
func (obj *KVLookupFunc) ArgGen(index int) (string, error) {
seq := []string{kvLookupArgNameNamespace}
if l := len(seq); index >= l {
return "", fmt.Errorf("index %d exceeds arg length of %d", index, l)
}
return seq[index], nil
}
// Validate makes sure we've built our struct properly. It is usually unused for
// normal functions that users can use directly.
func (obj *KVLookupFunc) Validate() error {
return nil
}
// Info returns some static info about itself.
func (obj *KVLookupFunc) Info() *interfaces.Info {
return &interfaces.Info{
Pure: false, // definitely false
Memo: false,
// output is map of: hostname => value
Sig: types.NewType(fmt.Sprintf("func(%s str) map{str: str}", kvLookupArgNameNamespace)),
Err: obj.Validate(),
}
}
// Init runs some startup code for this function.
func (obj *KVLookupFunc) Init(init *interfaces.Init) error {
obj.init = init
obj.watchChan = make(chan error) // XXX: sender should close this, but did I implement that part yet???
return nil
}
// Stream returns the changing values that this func has over time.
func (obj *KVLookupFunc) Stream(ctx context.Context) error {
defer close(obj.init.Output) // the sender closes
ctx, cancel := context.WithCancel(ctx)
defer cancel()
for {
select {
// TODO: should this first chan be run as a priority channel to
// avoid some sort of glitch? is that even possible? can our
// hostname check with reality (below) fix that?
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
namespace := input.Struct()[kvLookupArgNameNamespace].Str()
if namespace == "" {
return fmt.Errorf("can't use an empty namespace")
}
if obj.init.Debug {
obj.init.Logf("namespace: %s", namespace)
}
// TODO: support changing the namespace over time...
// TODO: possibly removing our stored value there first!
if obj.namespace == "" {
obj.namespace = namespace // store it
var err error
obj.watchChan, err = obj.init.World.StrMapWatch(ctx, obj.namespace) // watch for var changes
if err != nil {
return err
}
result, err := obj.buildMap(ctx) // build the map...
if err != nil {
return err
}
select {
case obj.init.Output <- result: // send one!
// pass
case <-ctx.Done():
return nil
}
} else if obj.namespace != namespace {
return fmt.Errorf("can't change namespace, previously: `%s`", obj.namespace)
}
continue // we get values on the watch chan, not here!
case err, ok := <-obj.watchChan:
if !ok { // closed
// XXX: if we close, perhaps the engine is
// switching etcd hosts and we should retry?
// maybe instead we should get an "etcd
// reconnect" signal, and the lang will restart?
return nil
}
if err != nil {
return errwrap.Wrapf(err, "channel watch failed on `%s`", obj.namespace)
}
result, err := obj.buildMap(ctx) // build the map...
if err != nil {
return err
}
// if the result is still the same, skip sending an update...
if obj.result != nil && result.Cmp(obj.result) == nil {
continue // result didn't change
}
obj.result = result // store new result
case <-ctx.Done():
return nil
}
select {
case obj.init.Output <- obj.result: // send
// pass
case <-ctx.Done():
return nil
}
}
}
// buildMap builds the result map which we'll need. It uses struct variables.
func (obj *KVLookupFunc) buildMap(ctx context.Context) (types.Value, error) {
keyMap, err := obj.init.World.StrMapGet(ctx, obj.namespace)
if err != nil {
return nil, errwrap.Wrapf(err, "channel read failed on `%s`", obj.namespace)
}
d := types.NewMap(obj.Info().Sig.Out)
for k, v := range keyMap {
key := &types.StrValue{V: k}
val := &types.StrValue{V: v}
if err := d.Add(key, val); err != nil {
return nil, errwrap.Wrapf(err, "map could not add key `%s`, val: `%s`", k, v)
}
}
return d, nil // put map into interface type
}

View File

@@ -1,718 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
// test with:
// time ./mgmt run --hostname h1 --tmp-prefix --no-pgp lang examples/lang/schedule0.mcl
// time ./mgmt run --hostname h2 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2381 --server-urls http://127.0.0.1:2382 --tmp-prefix --no-pgp lang examples/lang/schedule0.mcl
// time ./mgmt run --hostname h3 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2383 --server-urls http://127.0.0.1:2384 --tmp-prefix --no-pgp lang examples/lang/schedule0.mcl
// kill h2 (should see h1 and h3 pick [h1, h3] instead)
// restart h2 (should see [h1, h3] as before)
// kill h3 (should see h1 and h2 pick [h1, h2] instead)
// restart h3 (should see [h1, h2] as before)
// kill h3
// kill h2
// kill h1... all done!
package coreworld
import (
"context"
"fmt"
"sort"
"github.com/purpleidea/mgmt/etcd/scheduler" // TODO: is it okay to import this without abstraction?
"github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/util/errwrap"
)
const (
// ScheduleFuncName is the name this function is registered as.
ScheduleFuncName = "schedule"
// DefaultStrategy is the strategy to use if none has been specified.
DefaultStrategy = "rr"
// StrictScheduleOpts specifies whether the opts passed into the
// scheduler must be strictly what we're expecting, and nothing more.
// If this was false, then we'd allow an opts struct that had a field
// that wasn't used by the scheduler. This could be useful if we need to
// migrate to a newer version of the function. It's probably best to
// keep this strict.
StrictScheduleOpts = true
// arg names...
scheduleArgNameNamespace = "namespace"
scheduleArgNameOpts = "opts"
)
func init() {
funcs.ModuleRegister(ModuleName, ScheduleFuncName, func() interfaces.Func { return &ScheduleFunc{} })
}
var _ interfaces.PolyFunc = &ScheduleFunc{} // ensure it meets this expectation
// ScheduleFunc is special function which determines where code should run in
// the cluster.
type ScheduleFunc struct {
Type *types.Type // this is the type of opts used if specified
built bool // was this function built yet?
init *interfaces.Init
namespace string
scheduler *scheduler.Result
last types.Value
result types.Value // last calculated output
watchChan chan *schedulerResult
}
// String returns a simple name for this function. This is needed so this struct
// can satisfy the pgraph.Vertex interface.
func (obj *ScheduleFunc) String() string {
return ScheduleFuncName
}
// validOpts returns the available mapping of valid opts fields to types.
func (obj *ScheduleFunc) validOpts() map[string]*types.Type {
return map[string]*types.Type{
"strategy": types.TypeStr,
"max": types.TypeInt,
"reuse": types.TypeBool,
"ttl": types.TypeInt,
}
}
// ArgGen returns the Nth arg name for this function.
func (obj *ScheduleFunc) ArgGen(index int) (string, error) {
seq := []string{scheduleArgNameNamespace, scheduleArgNameOpts} // 2nd arg is optional
if l := len(seq); index >= l {
return "", fmt.Errorf("index %d exceeds arg length of %d", index, l)
}
return seq[index], nil
}
// Unify returns the list of invariants that this func produces.
func (obj *ScheduleFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) {
var invariants []interfaces.Invariant
var invar interfaces.Invariant
// func(namespace str) []str
// OR
// func(namespace str, opts T1) []str
namespaceName, err := obj.ArgGen(0)
if err != nil {
return nil, err
}
dummyNamespace := &interfaces.ExprAny{} // corresponds to the namespace type
dummyOut := &interfaces.ExprAny{} // corresponds to the out string
// namespace arg type of string
invar = &interfaces.EqualsInvariant{
Expr: dummyNamespace,
Type: types.TypeStr,
}
invariants = append(invariants, invar)
// return type of []string
invar = &interfaces.EqualsInvariant{
Expr: dummyOut,
Type: types.NewType("[]str"),
}
invariants = append(invariants, invar)
// generator function
fn := func(fnInvariants []interfaces.Invariant, solved map[interfaces.Expr]*types.Type) ([]interfaces.Invariant, error) {
for _, invariant := range fnInvariants {
// search for this special type of invariant
cfavInvar, ok := invariant.(*interfaces.CallFuncArgsValueInvariant)
if !ok {
continue
}
// did we find the mapping from us to ExprCall ?
if cfavInvar.Func != expr {
continue
}
// cfavInvar.Expr is the ExprCall! (the return pointer)
// cfavInvar.Args are the args that ExprCall uses!
if len(cfavInvar.Args) == 0 {
return nil, fmt.Errorf("unable to build function with no args")
}
if l := len(cfavInvar.Args); l > 2 {
return nil, fmt.Errorf("unable to build function with %d args", l)
}
// we can either have one arg or two
var invariants []interfaces.Invariant
var invar interfaces.Invariant
// add the relationship to the returned value
invar = &interfaces.EqualityInvariant{
Expr1: cfavInvar.Expr,
Expr2: dummyOut,
}
invariants = append(invariants, invar)
// add the relationships to the called args
invar = &interfaces.EqualityInvariant{
Expr1: cfavInvar.Args[0],
Expr2: dummyNamespace,
}
invariants = append(invariants, invar)
// first arg must be a string
invar = &interfaces.EqualsInvariant{
Expr: cfavInvar.Args[0],
Type: types.TypeStr,
}
invariants = append(invariants, invar)
// full function
mapped := make(map[string]interfaces.Expr)
ordered := []string{namespaceName}
mapped[namespaceName] = dummyNamespace
if len(cfavInvar.Args) == 2 { // two args is more complex
dummyOpts := &interfaces.ExprAny{}
optsTypeKnown := false
// speculate about the type?
if typ, exists := solved[cfavInvar.Args[1]]; exists {
optsTypeKnown = true
if typ.Kind != types.KindStruct {
return nil, fmt.Errorf("second arg must be of kind struct")
}
// XXX: the problem is that I can't
// currently express the opts struct as
// an invariant, without building a big
// giant, unusable exclusive...
validOpts := obj.validOpts()
if StrictScheduleOpts {
// strict opts field checking!
for _, name := range typ.Ord {
t := typ.Map[name]
value, exists := validOpts[name]
if !exists {
return nil, fmt.Errorf("unexpected opts field: `%s`", name)
}
if err := t.Cmp(value); err != nil {
return nil, errwrap.Wrapf(err, "expected different type for opts field: `%s`", name)
}
}
} else {
// permissive field checking...
validOptsSorted := []string{}
for name := range validOpts {
validOptsSorted = append(validOptsSorted, name)
}
sort.Strings(validOptsSorted)
for _, name := range validOptsSorted {
value := validOpts[name] // type
t, exists := typ.Map[name]
if !exists {
continue // ignore it
}
// if it exists, check the type
if err := t.Cmp(value); err != nil {
return nil, errwrap.Wrapf(err, "expected different type for opts field: `%s`", name)
}
}
}
invar := &interfaces.EqualsInvariant{
Expr: dummyOpts,
Type: typ,
}
invariants = append(invariants, invar)
}
// redundant?
if typ, err := cfavInvar.Args[1].Type(); err == nil {
invar := &interfaces.EqualsInvariant{
Expr: cfavInvar.Args[1],
Type: typ,
}
invariants = append(invariants, invar)
}
// If we're strict, require it, otherwise let
// in whatever, and let Build() deal with it.
if StrictScheduleOpts && !optsTypeKnown {
return nil, fmt.Errorf("the type of the opts struct is not known")
}
// expression must match type of the input arg
invar := &interfaces.EqualityInvariant{
Expr1: dummyOpts,
Expr2: cfavInvar.Args[1],
}
invariants = append(invariants, invar)
mapped[scheduleArgNameOpts] = dummyOpts
ordered = append(ordered, scheduleArgNameOpts)
}
invar = &interfaces.EqualityWrapFuncInvariant{
Expr1: expr, // maps directly to us!
Expr2Map: mapped,
Expr2Ord: ordered,
Expr2Out: dummyOut,
}
invariants = append(invariants, invar)
// TODO: do we return this relationship with ExprCall?
invar = &interfaces.EqualityWrapCallInvariant{
// TODO: should Expr1 and Expr2 be reversed???
Expr1: cfavInvar.Expr,
//Expr2Func: cfavInvar.Func, // same as below
Expr2Func: expr,
}
invariants = append(invariants, invar)
// TODO: are there any other invariants we should build?
return invariants, nil // generator return
}
// We couldn't tell the solver anything it didn't already know!
return nil, fmt.Errorf("couldn't generate new invariants")
}
invar = &interfaces.GeneratorInvariant{
Func: fn,
}
invariants = append(invariants, invar)
return invariants, nil
}
// Polymorphisms returns the list of possible function signatures available for
// this static polymorphic function. It relies on type and value hints to limit
// the number of returned possibilities.
func (obj *ScheduleFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) {
// TODO: technically, we could generate all permutations of the struct!
//variant := []*types.Type{}
//t0 := types.NewType("func(namespace str) []str")
//variant = append(variant, t0)
//validOpts := obj.validOpts()
//for ? := ? range { // generate all permutations of the struct...
// t := types.NewType(fmt.Sprintf("func(namespace str, opts %s) []str", ?))
// variant = append(variant, t)
//}
//if partialType == nil {
// return variant, nil
//}
if partialType == nil {
return nil, fmt.Errorf("zero type information given")
}
var typ *types.Type
if tOut := partialType.Out; tOut != nil {
if err := tOut.Cmp(types.NewType("[]str")); err != nil {
return nil, errwrap.Wrapf(err, "return type must be a list of strings")
}
}
ord := partialType.Ord
if partialType.Map != nil {
if len(ord) == 0 {
return nil, fmt.Errorf("must have at least one arg in schedule func")
}
if tNamespace, exists := partialType.Map[ord[0]]; exists && tNamespace != nil {
if err := tNamespace.Cmp(types.TypeStr); err != nil {
return nil, errwrap.Wrapf(err, "first arg must be an str")
}
}
if len(ord) == 1 {
return []*types.Type{types.NewType("func(namespace str) []str")}, nil // done!
}
if len(ord) != 2 {
return nil, fmt.Errorf("must have either one or two args in schedule func")
}
if tOpts, exists := partialType.Map[ord[1]]; exists {
if tOpts == nil { // usually a `struct{}`
typFunc := types.NewType("func(namespace str, opts variant) []str")
return []*types.Type{typFunc}, nil // solved!
}
if tOpts.Kind != types.KindStruct {
return nil, fmt.Errorf("second arg must be of kind struct")
}
validOpts := obj.validOpts()
for _, name := range tOpts.Ord {
t := tOpts.Map[name]
value, exists := validOpts[name]
if !exists {
return nil, fmt.Errorf("unexpected opts field: `%s`", name)
}
if err := t.Cmp(value); err != nil {
return nil, errwrap.Wrapf(err, "expected different type for opts field: `%s`", name)
}
}
typ = tOpts // solved
}
}
if typ == nil {
return nil, fmt.Errorf("not enough type information")
}
typFunc := types.NewType(fmt.Sprintf("func(namespace str, opts %s) []str", typ.String()))
// TODO: type check that the partialValues are compatible
return []*types.Type{typFunc}, nil // solved!
}
// Build is run to turn the polymorphic, undetermined function, into the
// specific statically typed version. It is usually run after Unify completes,
// and must be run before Info() and any of the other Func interface methods are
// used. This function is idempotent, as long as the arg isn't changed between
// runs.
func (obj *ScheduleFunc) Build(typ *types.Type) (*types.Type, error) {
// typ is the KindFunc signature we're trying to build...
if typ.Kind != types.KindFunc {
return nil, fmt.Errorf("input type must be of kind func")
}
if len(typ.Ord) != 1 && len(typ.Ord) != 2 {
return nil, fmt.Errorf("the schedule function needs either one or two args")
}
if typ.Out == nil {
return nil, fmt.Errorf("return type of function must be specified")
}
if typ.Map == nil {
return nil, fmt.Errorf("invalid input type")
}
if err := typ.Out.Cmp(types.NewType("[]str")); err != nil {
return nil, errwrap.Wrapf(err, "return type must be a list of strings")
}
tNamespace, exists := typ.Map[typ.Ord[0]]
if !exists || tNamespace == nil {
return nil, fmt.Errorf("first arg must be specified")
}
if len(typ.Ord) == 1 {
obj.Type = nil
obj.built = true
return obj.sig(), nil // done early, 2nd arg is absent!
}
tOpts, exists := typ.Map[typ.Ord[1]]
if !exists || tOpts == nil {
return nil, fmt.Errorf("second argument was missing")
}
if tOpts.Kind != types.KindStruct {
return nil, fmt.Errorf("second argument must be of kind struct")
}
validOpts := obj.validOpts()
if StrictScheduleOpts {
// strict opts field checking!
for _, name := range tOpts.Ord {
t := tOpts.Map[name]
value, exists := validOpts[name]
if !exists {
return nil, fmt.Errorf("unexpected opts field: `%s`", name)
}
if err := t.Cmp(value); err != nil {
return nil, errwrap.Wrapf(err, "expected different type for opts field: `%s`", name)
}
}
} else {
// permissive field checking...
validOptsSorted := []string{}
for name := range validOpts {
validOptsSorted = append(validOptsSorted, name)
}
sort.Strings(validOptsSorted)
for _, name := range validOptsSorted {
value := validOpts[name] // type
t, exists := tOpts.Map[name]
if !exists {
continue // ignore it
}
// if it exists, check the type
if err := t.Cmp(value); err != nil {
return nil, errwrap.Wrapf(err, "expected different type for opts field: `%s`", name)
}
}
}
obj.Type = tOpts // type of opts struct, even an empty: `struct{}`
obj.built = true
return obj.sig(), nil
}
// Validate tells us if the input struct takes a valid form.
func (obj *ScheduleFunc) Validate() error {
if !obj.built {
return fmt.Errorf("function wasn't built yet")
}
// obj.Type can be nil if no 2nd arg is given, or a struct (even empty!)
if obj.Type != nil && obj.Type.Kind != types.KindStruct { // build must be run first
return fmt.Errorf("type must be nil or a struct")
}
return nil
}
// Info returns some static info about itself. Build must be called before this
// will return correct data.
func (obj *ScheduleFunc) Info() *interfaces.Info {
// It's important that you don't return a non-nil sig if this is called
// before you're built. Type unification may call it opportunistically.
var sig *types.Type
if obj.built {
sig = obj.sig() // helper
}
return &interfaces.Info{
Pure: false, // definitely false
Memo: false,
// output is list of hostnames chosen
Sig: sig, // func kind
Err: obj.Validate(),
}
}
// helper
func (obj *ScheduleFunc) sig() *types.Type {
sig := types.NewType(fmt.Sprintf("func(%s str) []str", scheduleArgNameNamespace)) // simplest form
if obj.Type != nil {
sig = types.NewType(fmt.Sprintf("func(%s str, %s %s) []str", scheduleArgNameNamespace, scheduleArgNameOpts, obj.Type.String()))
}
return sig
}
// Init runs some startup code for this function.
func (obj *ScheduleFunc) Init(init *interfaces.Init) error {
obj.init = init
obj.watchChan = make(chan *schedulerResult)
//obj.init.Debug = true // use this for local debugging
return nil
}
// Stream returns the changing values that this func has over time.
func (obj *ScheduleFunc) Stream(ctx context.Context) error {
defer close(obj.init.Output) // the sender closes
for {
select {
// TODO: should this first chan be run as a priority channel to
// avoid some sort of glitch? is that even possible? can our
// hostname check with reality (below) fix that?
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
namespace := input.Struct()[scheduleArgNameNamespace].Str()
if namespace == "" {
return fmt.Errorf("can't use an empty namespace")
}
opts := make(map[string]types.Value) // empty "struct"
if val, exists := input.Struct()[scheduleArgNameOpts]; exists {
opts = val.Struct()
}
if obj.init.Debug {
obj.init.Logf("namespace: %s", namespace)
}
schedulerOpts := []scheduler.Option{}
// don't add bad or zero-value options
defaultStrategy := true
if val, exists := opts["strategy"]; exists {
if strategy := val.Str(); strategy != "" {
if obj.init.Debug {
obj.init.Logf("opts: strategy: %s", strategy)
}
defaultStrategy = false
schedulerOpts = append(schedulerOpts, scheduler.StrategyKind(strategy))
}
}
if defaultStrategy { // we always need to add one!
schedulerOpts = append(schedulerOpts, scheduler.StrategyKind(DefaultStrategy))
}
if val, exists := opts["max"]; exists {
// TODO: check for overflow
if max := int(val.Int()); max > 0 {
if obj.init.Debug {
obj.init.Logf("opts: max: %d", max)
}
schedulerOpts = append(schedulerOpts, scheduler.MaxCount(max))
}
}
if val, exists := opts["reuse"]; exists {
reuse := val.Bool()
if obj.init.Debug {
obj.init.Logf("opts: reuse: %t", reuse)
}
schedulerOpts = append(schedulerOpts, scheduler.ReuseLease(reuse))
}
if val, exists := opts["ttl"]; exists {
// TODO: check for overflow
if ttl := int(val.Int()); ttl > 0 {
if obj.init.Debug {
obj.init.Logf("opts: ttl: %d", ttl)
}
schedulerOpts = append(schedulerOpts, scheduler.SessionTTL(ttl))
}
}
// TODO: support changing the namespace over time...
// TODO: possibly removing our stored value there first!
if obj.namespace == "" {
obj.namespace = namespace // store it
if obj.init.Debug {
obj.init.Logf("starting scheduler...")
}
var err error
obj.scheduler, err = obj.init.World.Scheduler(obj.namespace, schedulerOpts...)
if err != nil {
return errwrap.Wrapf(err, "can't create scheduler")
}
// process the stream of scheduling output...
go func() {
defer close(obj.watchChan)
// XXX: maybe we could share the parent
// ctx, but I have to work out the
// ordering logic first. For now this is
// just a port of what it was before.
newCtx, cancel := context.WithCancel(context.Background())
go func() {
defer cancel() // unblock Next()
defer obj.scheduler.Shutdown()
select {
case <-ctx.Done():
return
}
}()
for {
hosts, err := obj.scheduler.Next(newCtx)
select {
case obj.watchChan <- &schedulerResult{
hosts: hosts,
err: err,
}:
case <-ctx.Done():
return
}
}
}()
} else if obj.namespace != namespace {
return fmt.Errorf("can't change namespace, previously: `%s`", obj.namespace)
}
continue // we send values on the watch chan, not here!
case schedulerResult, ok := <-obj.watchChan:
if !ok { // closed
// XXX: maybe etcd reconnected? (fix etcd implementation)
// XXX: if we close, perhaps the engine is
// switching etcd hosts and we should retry?
// maybe instead we should get an "etcd
// reconnect" signal, and the lang will restart?
return nil
}
if err := schedulerResult.err; err != nil {
if err == scheduler.ErrEndOfResults {
//return nil // TODO: we should probably fix the reconnect issue and use this here
return fmt.Errorf("scheduler shutdown, reconnect bug?") // XXX: fix etcd reconnects
}
return errwrap.Wrapf(err, "channel watch failed on `%s`", obj.namespace)
}
if obj.init.Debug {
obj.init.Logf("got hosts: %+v", schedulerResult.hosts)
}
var result types.Value
l := types.NewList(obj.Info().Sig.Out)
for _, val := range schedulerResult.hosts {
if err := l.Add(&types.StrValue{V: val}); err != nil {
return errwrap.Wrapf(err, "list could not add val: `%s`", val)
}
}
result = l // set list as result
if obj.init.Debug {
obj.init.Logf("result: %+v", result)
}
// if the result is still the same, skip sending an update...
if obj.result != nil && result.Cmp(obj.result) == nil {
continue // result didn't change
}
obj.result = result // store new result
case <-ctx.Done():
return nil
}
select {
case obj.init.Output <- obj.result: // send
// pass
case <-ctx.Done():
return nil
}
}
}
// schedulerResult combines our internal events into a single message packet.
type schedulerResult struct {
hosts []string
err error
}

View File

@@ -1,23 +0,0 @@
// Mgmt
// Copyright (C) 2013-2024+ 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 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 <http://www.gnu.org/licenses/>.
package coreworld
const (
// ModuleName is the prefix given to all the functions in this module.
ModuleName = "world"
)

View File

@@ -24,7 +24,7 @@ import (
)
var (
pkg = flag.String("package", "lang/funcs/core", "path to the package")
pkg = flag.String("package", "lang/core", "path to the package")
filename = flag.String("filename", "funcgen.yaml", "path to the config")
templates = flag.String("templates", "lang/funcs/funcgen/templates/*.tpl", "path to the templates")
)