// Mgmt // Copyright (C) James Shubin and the project contributors // Written by James Shubin and the project contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // // Additional permission under GNU GPL version 3 section 7 // // If you modify this program, or any covered work, by linking or combining it // with embedded mcl code and modules (and that the embedded mcl code and // modules which link with this program, contain a copy of their source code in // the authoritative form) containing parts covered by the terms of any other // license, the licensors of this program grant you additional permission to // convey the resulting work. Furthermore, the licensors of this program grant // the original author, James Shubin, additional permission to update this // additional permission if he deems it necessary to achieve the goals of this // additional permission. package resources import ( "context" "fmt" "net/url" "strconv" "sync" "github.com/purpleidea/mgmt/engine" "github.com/purpleidea/mgmt/engine/resources/http_server_ui/common" "github.com/purpleidea/mgmt/engine/traits" "github.com/purpleidea/mgmt/util/errwrap" ) const ( httpServerUIInputKind = httpServerUIKind + ":input" httpServerUIInputStoreKey = "key" httpServerUIInputStoreSchemeLocal = "local" httpServerUIInputStoreSchemeWorld = "world" httpServerUIInputTypeText = common.HTTPServerUIInputTypeText // "text" httpServerUIInputTypeRange = common.HTTPServerUIInputTypeRange // "range" ) func init() { engine.RegisterResource(httpServerUIInputKind, func() engine.Res { return &HTTPServerUIInputRes{} }) } var _ HTTPServerUIGroupableRes = &HTTPServerUIInputRes{} // compile time check // HTTPServerUIInputRes is a form element that exists within a http:server:ui // resource, which exists within an http server. The name is used as the unique // id of the field, unless the id field is specified, and in that case it is // used instead. The way this works is that it autogroups at runtime with an // existing http:server:ui resource, and in doing so makes the form field // associated with this resource available as part of that ui which is itself // grouped and served from the http server resource. type HTTPServerUIInputRes struct { traits.Base // add the base methods without re-implementation traits.Edgeable // XXX: add autoedge support traits.Groupable // can be grouped into HTTPServerUIRes traits.Sendable init *engine.Init // Path is the name of the http ui resource to group this into. If it is // omitted, and there is only a single http ui resource, then it will // be grouped into it automatically. If there is more than one main http // ui resource being used, then the grouping behaviour is *undefined* // when this is not specified, and it is not recommended to leave this // blank! Path string `lang:"path" yaml:"path"` // ID is the unique id for this element. It is used in form fields and // should not be a private identifier. It must be unique within a given // http ui. ID string `lang:"id" yaml:"id"` // Value is the default value to use for the form field. If you change // it, then the resource graph will change and we'll rebuild and have // the new value visible. You can use either this or the Store field. // XXX: If we ever add our resource mutate API, we might not need to // swap to a new resource graph, and maybe Store is not needed? Value string `lang:"value" yaml:"value"` // Store the data in this source. It will also read in a default value // from there if one is present. It will watch it for changes as well, // and update the displayed value if it's changed from another source. // This cannot be used at the same time as the Value field. Store string `lang:"store" yaml:"store"` // Type specifies the type of input field this is, and some information // about it. // XXX: come up with a format such as "multiline://?max=60&style=foo" Type string `lang:"type" yaml:"type"` // Sort is a string that you can use to determine the global sorted // display order of all the elements in a ui. Sort string `lang:"sort" yaml:"sort"` scheme string // the scheme we're using with Store, cached for later key string // the key we're using with Store, cached for later typeURL *url.URL // the type data, cached for later typeURLValues url.Values // the type data, cached for later last *string // the last value we sent value string // what we've last received from SetValue storeEvent bool // did a store event happen? mutex *sync.Mutex // guards storeEvent and value event chan struct{} // local event that the setValue sends } // Default returns some sensible defaults for this resource. func (obj *HTTPServerUIInputRes) Default() engine.Res { return &HTTPServerUIInputRes{ Type: "text://", } } // Validate checks if the resource data structure was populated correctly. func (obj *HTTPServerUIInputRes) Validate() error { if obj.GetID() == "" { return fmt.Errorf("empty id") } if obj.Value != "" && obj.Store != "" { return fmt.Errorf("may only use either Value or Store") } if obj.Value != "" { if err := obj.checkValue(obj.Value); err != nil { return errwrap.Wrapf(err, "the Value field is invalid") } } if obj.Store != "" { // XXX: check the URI format } return nil } // Init runs some startup code for this resource. func (obj *HTTPServerUIInputRes) Init(init *engine.Init) error { obj.init = init // save for later u, err := url.Parse(obj.Type) if err != nil { return err } if u == nil { return fmt.Errorf("can't parse Type") } if u.Scheme != httpServerUIInputTypeText && u.Scheme != httpServerUIInputTypeRange { return fmt.Errorf("unknown scheme: %s", u.Scheme) } values, err := url.ParseQuery(u.RawQuery) if err != nil { return err } obj.typeURL = u obj.typeURLValues = values if obj.Store != "" { u, err := url.Parse(obj.Store) if err != nil { return err } if u == nil { return fmt.Errorf("can't parse Store") } if u.Scheme != httpServerUIInputStoreSchemeLocal && u.Scheme != httpServerUIInputStoreSchemeWorld { return fmt.Errorf("unknown scheme: %s", u.Scheme) } values, err := url.ParseQuery(u.RawQuery) if err != nil { return err } obj.scheme = u.Scheme // cache for later obj.key = obj.Name() // default x, exists := values[httpServerUIInputStoreKey] if exists && len(x) > 0 && x[0] != "" { // ignore absent or broken keys obj.key = x[0] } } // populate our obj.value cache somehow, so we don't mutate obj.Value obj.value = obj.Value // copy obj.mutex = &sync.Mutex{} obj.event = make(chan struct{}, 1) // buffer to avoid blocks or deadlock return nil } // Cleanup is run by the engine to clean up after the resource is done. func (obj *HTTPServerUIInputRes) Cleanup() error { return nil } // getKey returns the key to be used for this resource. If the Store field is // specified, it will use that parsed part, otherwise it uses the Name. func (obj *HTTPServerUIInputRes) getKey() string { if obj.Store != "" { return obj.key } return obj.Name() } // ParentName is used to limit which resources autogroup into this one. If it's // empty then it's ignored, otherwise it must match the Name of the parent to // get grouped. func (obj *HTTPServerUIInputRes) ParentName() string { return obj.Path } // GetKind returns the kind of this resource. func (obj *HTTPServerUIInputRes) GetKind() string { // NOTE: We don't *need* to return such a specific string, and "input" // would be enough, but we might as well use this because we have it. return httpServerUIInputKind } // GetID returns the actual ID we respond to. When ID is not specified, we use // the Name. func (obj *HTTPServerUIInputRes) GetID() string { if obj.ID != "" { return obj.ID } return obj.Name() } // SetValue stores the new value field that was obtained from submitting the // form. This receives the raw, unsafe value that you must validate first. func (obj *HTTPServerUIInputRes) SetValue(ctx context.Context, vs []string) error { if len(vs) != 1 { return fmt.Errorf("unexpected length of %d", len(vs)) } value := vs[0] if err := obj.checkValue(value); err != nil { return err } obj.mutex.Lock() obj.setValue(ctx, value) // also sends an event obj.mutex.Unlock() return nil } // setValue is the helper version where the caller must provide the mutex. func (obj *HTTPServerUIInputRes) setValue(ctx context.Context, val string) error { obj.value = val select { case obj.event <- struct{}{}: default: } return nil } func (obj *HTTPServerUIInputRes) checkValue(value string) error { // XXX: validate based on obj.Type // XXX: validate what kind of values are allowed, probably no \n, etc... return nil } // GetValue gets a string representation for the form value, that we'll use in // our html form. func (obj *HTTPServerUIInputRes) GetValue(ctx context.Context) (string, error) { obj.mutex.Lock() defer obj.mutex.Unlock() if obj.storeEvent { val, exists, err := obj.storeGet(ctx, obj.getKey()) if err != nil { return "", errwrap.Wrapf(err, "error during get") } if !exists { return "", nil // default } return val, nil } return obj.value, nil } // GetType returns a map that you can use to build the input field in the ui. func (obj *HTTPServerUIInputRes) GetType() map[string]string { m := make(map[string]string) if obj.typeURL.Scheme == httpServerUIInputTypeRange { m = obj.rangeGetType() } m[common.HTTPServerUIInputType] = obj.typeURL.Scheme return m } func (obj *HTTPServerUIInputRes) rangeGetType() map[string]string { m := make(map[string]string) base := 10 bits := 64 if sa, exists := obj.typeURLValues[common.HTTPServerUIInputTypeRangeMin]; exists && len(sa) > 0 { if x, err := strconv.ParseInt(sa[0], base, bits); err == nil { m[common.HTTPServerUIInputTypeRangeMin] = strconv.FormatInt(x, base) } } if sa, exists := obj.typeURLValues[common.HTTPServerUIInputTypeRangeMax]; exists && len(sa) > 0 { if x, err := strconv.ParseInt(sa[0], base, bits); err == nil { m[common.HTTPServerUIInputTypeRangeMax] = strconv.FormatInt(x, base) } } if sa, exists := obj.typeURLValues[common.HTTPServerUIInputTypeRangeStep]; exists && len(sa) > 0 { if x, err := strconv.ParseInt(sa[0], base, bits); err == nil { m[common.HTTPServerUIInputTypeRangeStep] = strconv.FormatInt(x, base) } } return m } // GetSort returns a string that you can use to determine the global sorted // display order of all the elements in a ui. func (obj *HTTPServerUIInputRes) GetSort() string { return obj.Sort } // Watch is the primary listener for this resource and it outputs events. This // particular one does absolutely nothing but block until we've received a done // signal. func (obj *HTTPServerUIInputRes) Watch(ctx context.Context) error { if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeLocal { return obj.localWatch(ctx) } if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeWorld { return obj.worldWatch(ctx) } obj.init.Running() // when started, notify engine that we're running // XXX: do we need to watch on obj.event for normal .Value stuff? select { case <-ctx.Done(): // closed by the engine to signal shutdown } //obj.init.Event() // notify engine of an event (this can block) return nil } func (obj *HTTPServerUIInputRes) localWatch(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) defer cancel() ch, err := obj.init.Local.ValueWatch(ctx, obj.getKey()) // get possible events! if err != nil { return errwrap.Wrapf(err, "error during watch") } obj.init.Running() // when started, notify engine that we're running for { select { case _, ok := <-ch: if !ok { // channel shutdown return nil } obj.mutex.Lock() obj.storeEvent = true obj.mutex.Unlock() case <-obj.event: case <-ctx.Done(): // closed by the engine to signal shutdown return nil } if obj.init.Debug { obj.init.Logf("event!") } obj.init.Event() // notify engine of an event (this can block) } } func (obj *HTTPServerUIInputRes) worldWatch(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) defer cancel() ch, err := obj.init.World.StrWatch(ctx, obj.getKey()) // get possible events! if err != nil { return errwrap.Wrapf(err, "error during watch") } obj.init.Running() // when started, notify engine that we're running for { select { case err, ok := <-ch: if !ok { // channel shutdown return nil } if err != nil { return errwrap.Wrapf(err, "unknown %s watcher error", obj) } obj.mutex.Lock() obj.storeEvent = true obj.mutex.Unlock() case <-obj.event: case <-ctx.Done(): // closed by the engine to signal shutdown return nil } if obj.init.Debug { obj.init.Logf("event!") } obj.init.Event() // notify engine of an event (this can block) } } // CheckApply performs the send/recv portion of this autogrouped resources. That // can fail, but only if the send portion fails for some reason. If we're using // the Store feature, then it also reads and writes to and from that store. func (obj *HTTPServerUIInputRes) CheckApply(ctx context.Context, apply bool) (bool, error) { if obj.init.Debug { obj.init.Logf("CheckApply") } // If we're in ".Value" mode, we want to look at the incoming value, and // send it onwards. This function mostly exists as a stub in this case. // The private value gets set by obj.SetValue from the http:server:ui // parent. If we're in ".Store" mode, then we're reconciling between the // "World" and the http:server:ui "Web". if obj.Store != "" { return obj.storeCheckApply(ctx, apply) } return obj.valueCheckApply(ctx, apply) } func (obj *HTTPServerUIInputRes) valueCheckApply(ctx context.Context, apply bool) (bool, error) { obj.mutex.Lock() value := obj.value // gets set by obj.SetValue obj.mutex.Unlock() if obj.last != nil && *obj.last == value { if err := obj.init.Send(&HTTPServerUIInputSends{ Value: &value, }); err != nil { return false, err } return true, nil // expected value has already been sent } if !apply { if err := obj.init.Send(&HTTPServerUIInputSends{ Value: &value, // XXX: arbitrary since we're in noop mode }); err != nil { return false, err } return false, nil } s := value // copy obj.last = &s // cache // XXX: This is getting called twice, what's the bug? obj.init.Logf("sending: %s", value) // send if err := obj.init.Send(&HTTPServerUIInputSends{ Value: &value, }); err != nil { return false, err } return false, nil //return true, nil // always succeeds, with nothing to do! } // storeCheckApply is a tricky function where we attempt to reconcile the state // between a third-party changing the value in the World database, and a recent // "http:server:ui" change by an end user. Basically whoever runs last is the // "right" value that we want to use. We know who sent the event from reading // the storeEvent variable, and if it was the World, we want to cache it // locally, and if it was the Web, then we want to push it up to the store. func (obj *HTTPServerUIInputRes) storeCheckApply(ctx context.Context, apply bool) (bool, error) { v1, exists, err := obj.storeGet(ctx, obj.getKey()) if err != nil { return false, errwrap.Wrapf(err, "error during get") } obj.mutex.Lock() v2 := obj.value // gets set by obj.SetValue storeEvent := obj.storeEvent obj.storeEvent = false // reset it obj.mutex.Unlock() if exists && v1 == v2 { // both sides are happy if err := obj.init.Send(&HTTPServerUIInputSends{ Value: &v2, }); err != nil { return false, err } return true, nil } if !apply { if err := obj.init.Send(&HTTPServerUIInputSends{ Value: &v2, // XXX: arbitrary since we're in noop mode }); err != nil { return false, err } return false, nil } obj.mutex.Lock() if storeEvent { // event from World, pull down the value err = obj.setValue(ctx, v1) // also sends an event } value := obj.value obj.mutex.Unlock() if err != nil { return false, err } if !exists || !storeEvent { // event from web, push up the value if err := obj.storeSet(ctx, obj.getKey(), value); err != nil { return false, errwrap.Wrapf(err, "error during set") } } obj.init.Logf("sending: %s", value) // send if err := obj.init.Send(&HTTPServerUIInputSends{ Value: &value, }); err != nil { return false, err } return false, nil } func (obj *HTTPServerUIInputRes) storeGet(ctx context.Context, key string) (string, bool, error) { if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeLocal { val, err := obj.init.Local.ValueGet(ctx, key) if err != nil { return "", false, err // real error } if val == nil { // if val is nil, and no error then it doesn't exist return "", false, nil // val doesn't exist } s, ok := val.(string) if !ok { // TODO: support different types perhaps? return "", false, fmt.Errorf("not a string") // real error } return s, true, nil } if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeWorld { val, err := obj.init.World.StrGet(ctx, key) if err != nil && obj.init.World.StrIsNotExist(err) { return "", false, nil // val doesn't exist } if err != nil { return "", false, err // real error } return val, true, nil } return "", false, nil // something else } func (obj *HTTPServerUIInputRes) storeSet(ctx context.Context, key, val string) error { if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeLocal { return obj.init.Local.ValueSet(ctx, key, val) } if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeWorld { return obj.init.World.StrSet(ctx, key, val) } return nil // something else } // Cmp compares two resources and returns an error if they are not equivalent. func (obj *HTTPServerUIInputRes) Cmp(r engine.Res) error { // we can only compare HTTPServerUIInputRes to others of the same resource kind res, ok := r.(*HTTPServerUIInputRes) if !ok { return fmt.Errorf("res is not the same kind") } if obj.Path != res.Path { return fmt.Errorf("the Path differs") } if obj.ID != res.ID { return fmt.Errorf("the ID differs") } if obj.Value != res.Value { return fmt.Errorf("the Value differs") } if obj.Store != res.Store { return fmt.Errorf("the Store differs") } if obj.Type != res.Type { return fmt.Errorf("the Type differs") } if obj.Sort != res.Sort { return fmt.Errorf("the Sort differs") } return nil } // UnmarshalYAML is the custom unmarshal handler for this struct. It is // primarily useful for setting the defaults. func (obj *HTTPServerUIInputRes) UnmarshalYAML(unmarshal func(interface{}) error) error { type rawRes HTTPServerUIInputRes // indirection to avoid infinite recursion def := obj.Default() // get the default res, ok := def.(*HTTPServerUIInputRes) // put in the right format if !ok { return fmt.Errorf("could not convert to HTTPServerUIInputRes") } raw := rawRes(*res) // convert; the defaults go here if err := unmarshal(&raw); err != nil { return err } *obj = HTTPServerUIInputRes(raw) // restore from indirection with type conversion! return nil } // HTTPServerUIInputSends is the struct of data which is sent after a successful // Apply. type HTTPServerUIInputSends struct { // Value is the text element value being sent. Value *string `lang:"value"` } // Sends represents the default struct of values we can send using Send/Recv. func (obj *HTTPServerUIInputRes) Sends() interface{} { return &HTTPServerUIInputSends{ Value: nil, } }