diff --git a/config.go b/config.go index 33663e5ef..2841b86ec 100644 --- a/config.go +++ b/config.go @@ -45,6 +45,7 @@ type GraphConfig struct { Graph string `yaml:"graph"` Resources struct { Noop []NoopRes `yaml:"noop"` + Pkg []PkgRes `yaml:"pkg"` File []FileRes `yaml:"file"` Svc []SvcRes `yaml:"svc"` Exec []ExecRes `yaml:"exec"` @@ -94,12 +95,14 @@ func ParseConfigFromFile(filename string) *GraphConfig { func UpdateGraphFromConfig(config *GraphConfig, hostname string, g *Graph, etcdO *EtcdWObject) bool { var NoopMap = make(map[string]*Vertex) + var PkgMap = make(map[string]*Vertex) var FileMap = make(map[string]*Vertex) var SvcMap = make(map[string]*Vertex) var ExecMap = make(map[string]*Vertex) var lookup = make(map[string]map[string]*Vertex) lookup["noop"] = NoopMap + lookup["pkg"] = PkgMap lookup["file"] = FileMap lookup["svc"] = SvcMap lookup["exec"] = ExecMap @@ -121,6 +124,17 @@ func UpdateGraphFromConfig(config *GraphConfig, hostname string, g *Graph, etcdO keep = append(keep, v) // append } + for _, t := range config.Resources.Pkg { + obj := NewPkgRes(t.Name, t.State, false, false, false) + v := g.GetVertexMatch(obj) + if v == nil { // no match found + v = NewVertex(obj) + g.AddVertex(v) // call standalone in case not part of an edge + } + PkgMap[obj.Name] = v // used for constructing edges + keep = append(keep, v) // append + } + for _, t := range config.Resources.File { // XXX: should we export based on a @@ prefix, or a metaparam // like exported => true || exported => (host pattern)||(other pattern?) diff --git a/examples/pkg1.yaml b/examples/pkg1.yaml new file mode 100644 index 000000000..64e176f34 --- /dev/null +++ b/examples/pkg1.yaml @@ -0,0 +1,7 @@ +--- +graph: mygraph +resources: + pkg: + - name: powertop + state: installed +edges: [] diff --git a/examples/pkg2.yaml b/examples/pkg2.yaml new file mode 100644 index 000000000..122c06312 --- /dev/null +++ b/examples/pkg2.yaml @@ -0,0 +1,7 @@ +--- +graph: mygraph +resources: + pkg: + - name: powertop + state: uninstalled +edges: [] diff --git a/misc.go b/misc.go index b3117384d..94a281fb6 100644 --- a/misc.go +++ b/misc.go @@ -21,6 +21,7 @@ import ( "bytes" "encoding/base64" "encoding/gob" + "github.com/godbus/dbus" "path" "strings" "time" @@ -120,3 +121,22 @@ func TimeAfterOrBlock(t int) <-chan time.Time { } return time.After(time.Duration(t) * time.Second) } + +// making using the private bus usable, should be upstream: +// TODO: https://github.com/godbus/dbus/issues/15 +func SystemBusPrivateUsable() (conn *dbus.Conn, err error) { + conn, err = dbus.SystemBusPrivate() + if err != nil { + return nil, err + } + if err = conn.Auth(nil); err != nil { + conn.Close() + conn = nil + return + } + if err = conn.Hello(); err != nil { + conn.Close() + conn = nil + } + return conn, nil // success +} diff --git a/packagekit.go b/packagekit.go new file mode 100644 index 000000000..2801e1698 --- /dev/null +++ b/packagekit.go @@ -0,0 +1,479 @@ +// Mgmt +// Copyright (C) 2013-2016+ 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 Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// DOCS: https://www.freedesktop.org/software/PackageKit/gtk-doc/index.html + +//package packagekit // TODO +package main + +import ( + "errors" + "fmt" + "github.com/godbus/dbus" + "log" + "strings" +) + +const ( + PK_DEBUG = false + PARANOID = false // enable if you see any ghosts +) + +const ( + PkBufferSize = 100 + PkPath = "/org/freedesktop/PackageKit" + PkIface = "org.freedesktop.PackageKit" + PkIfaceTransaction = PkIface + ".Transaction" + dbusAddMatch = "org.freedesktop.DBus.AddMatch" +) + +var ( + PkArchMap = map[string]string{ // map of PackageKit arch to GOARCH + "x86_64": "amd64", + // TODO: add more values + } +) + +//type enum_filter uint64 +// https://github.com/hughsie/PackageKit/blob/master/lib/packagekit-glib2/pk-enum.c +const ( //static const PkEnumMatch enum_filter[] + PK_FILTER_ENUM_UNKNOWN uint64 = 1 << iota // "unknown" + PK_FILTER_ENUM_NONE // "none" + PK_FILTER_ENUM_INSTALLED // "installed" + PK_FILTER_ENUM_NOT_INSTALLED // "~installed" + PK_FILTER_ENUM_DEVELOPMENT // "devel" + PK_FILTER_ENUM_NOT_DEVELOPMENT // "~devel" + PK_FILTER_ENUM_GUI // "gui" + PK_FILTER_ENUM_NOT_GUI // "~gui" + PK_FILTER_ENUM_FREE // "free" + PK_FILTER_ENUM_NOT_FREE // "~free" + PK_FILTER_ENUM_VISIBLE // "visible" + PK_FILTER_ENUM_NOT_VISIBLE // "~visible" + PK_FILTER_ENUM_SUPPORTED // "supported" + PK_FILTER_ENUM_NOT_SUPPORTED // "~supported" + PK_FILTER_ENUM_BASENAME // "basename" + PK_FILTER_ENUM_NOT_BASENAME // "~basename" + PK_FILTER_ENUM_NEWEST // "newest" + PK_FILTER_ENUM_NOT_NEWEST // "~newest" + PK_FILTER_ENUM_ARCH // "arch" + PK_FILTER_ENUM_NOT_ARCH // "~arch" + PK_FILTER_ENUM_SOURCE // "source" + PK_FILTER_ENUM_NOT_SOURCE // "~source" + PK_FILTER_ENUM_COLLECTIONS // "collections" + PK_FILTER_ENUM_NOT_COLLECTIONS // "~collections" + PK_FILTER_ENUM_APPLICATION // "application" + PK_FILTER_ENUM_NOT_APPLICATION // "~application" + PK_FILTER_ENUM_DOWNLOADED // "downloaded" + PK_FILTER_ENUM_NOT_DOWNLOADED // "~downloaded" +) + +const ( //static const PkEnumMatch enum_transaction_flag[] + PK_TRANSACTION_FLAG_ENUM_NONE uint64 = 1 << iota // "none" + PK_TRANSACTION_FLAG_ENUM_ONLY_TRUSTED // "only-trusted" + PK_TRANSACTION_FLAG_ENUM_SIMULATE // "simulate" + PK_TRANSACTION_FLAG_ENUM_ONLY_DOWNLOAD // "only-download" + PK_TRANSACTION_FLAG_ENUM_ALLOW_REINSTALL // "allow-reinstall" + PK_TRANSACTION_FLAG_ENUM_JUST_REINSTALL // "just-reinstall" + PK_TRANSACTION_FLAG_ENUM_ALLOW_DOWNGRADE // "allow-downgrade" +) + +// wrapper struct so we can pass bus connection around in the struct +type Conn struct { + conn *dbus.Conn +} + +// get a new bus connection +func NewBus() *Conn { + // if we share the bus with others, we will get each others messages!! + bus, err := SystemBusPrivateUsable() // don't share the bus connection! + if err != nil { + return nil + } + return &Conn{ + conn: bus, + } +} + +// get the dbus connection object +func (bus *Conn) GetBus() *dbus.Conn { + return bus.conn +} + +// close the dbus connection object +func (bus *Conn) Close() error { + return bus.conn.Close() +} + +// internal helper to add signal matches to the bus, should only be called once +func (bus *Conn) matchSignal(ch chan *dbus.Signal, path dbus.ObjectPath, iface string, signals []string) error { + if PK_DEBUG { + log.Printf("PackageKit: matchSignal(%v, %v, %v, %v)", ch, path, iface, signals) + } + // eg: gdbus monitor --system --dest org.freedesktop.PackageKit --object-path /org/freedesktop/PackageKit | grep + var call *dbus.Call + // TODO: if we make this call many times, we seem to receive signals + // that many times... Maybe this should be an object singleton? + obj := bus.GetBus().BusObject() + pathStr := fmt.Sprintf("%s", path) + if len(signals) == 0 { + call = obj.Call(dbusAddMatch, 0, "type='signal',path='"+pathStr+"',interface='"+iface+"'") + } else { + for _, signal := range signals { + call = obj.Call(dbusAddMatch, 0, "type='signal',path='"+pathStr+"',interface='"+iface+"',member='"+signal+"'") + if call.Err != nil { + break + } + } + } + if call.Err != nil { + return call.Err + } + if PK_DEBUG { + log.Println("PackageKit: matchSignal(): Added!") + } + // The caller has to make sure that ch is sufficiently buffered; if a + // message arrives when a write to c is not possible, it is discarded! + // This can be disastrous if we're waiting for a "Finished" signal! + bus.GetBus().Signal(ch) + if PK_DEBUG { + log.Println("PackageKit: matchSignal(): Success!") + } + return nil +} + +// get a signal anytime an event happens +func (bus *Conn) WatchChanges() (chan *dbus.Signal, error) { + ch := make(chan *dbus.Signal, PkBufferSize) + // NOTE: the TransactionListChanged signal fires much more frequently, + // but with much less specificity. If we're missing events, report the + // issue upstream! The UpdatesChanged signal is what hughsie suggested + var signal = "UpdatesChanged" + err := bus.matchSignal(ch, PkPath, PkIface, []string{signal}) + if err != nil { + return nil, err + } + if PARANOID { // TODO: this filtering might not be necessary anymore... + // try to handle the filtering inside this function! + rch := make(chan *dbus.Signal) + go func() { + loop: + for { + select { + case event := <-ch: + // "A receive from a closed channel returns the + // zero value immediately": if i get nil here, + // it means the channel was closed by someone!! + if event == nil { // shared bus issue? + log.Println("PackageKit: Hrm, channel was closed!") + break loop // TODO: continue? + } + // i think this was caused by using the shared + // bus, but we might as well leave it in for now + if event.Path != PkPath || event.Name != fmt.Sprintf("%s.%s", PkIface, signal) { + log.Println("PackageKit: Some wires have been crossed!") + continue + } + rch <- event // forward... + } + } + defer close(ch) + }() + return rch, nil + } + return ch, nil +} + +// create and return a transaction path +func (bus *Conn) CreateTransaction() (dbus.ObjectPath, error) { + if PK_DEBUG { + log.Println("PackageKit: CreateTransaction()") + } + var interfacePath dbus.ObjectPath + obj := bus.GetBus().Object(PkIface, PkPath) + call := obj.Call(fmt.Sprintf("%s.CreateTransaction", PkIface), 0).Store(&interfacePath) + if call != nil { + return "", call + } + if PK_DEBUG { + log.Printf("PackageKit: CreateTransaction(): %v", interfacePath) + } + return interfacePath, nil +} + +func (bus *Conn) ResolvePackages(packages []string, filter uint64) ([]string, error) { + packageIds := []string{} + ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :( + interfacePath, err := bus.CreateTransaction() // emits Destroy on close + if err != nil { + return []string{}, err + } + + // add signal matches for Package and Finished which will always be last + var signals = []string{"Package", "Finished", "Error", "Destroy"} + bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals) + if PK_DEBUG { + log.Printf("PackageKit: ResolvePackages(): Object(%v, %v)", PkIface, interfacePath) + } + obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path + call := obj.Call(FmtTransactionMethod("Resolve"), 0, filter, packages) + if PK_DEBUG { + log.Println("PackageKit: ResolvePackages(): Call: Success!") + } + if call.Err != nil { + return []string{}, call.Err + } +loop: + for { + // FIXME: add a timeout option to error in case signals are dropped! + select { + case signal := <-ch: + if PK_DEBUG { + log.Printf("PackageKit: ResolvePackages(): Signal: %+v", signal) + } + if signal.Path != interfacePath { + log.Println("PackageKit: Some wires have been crossed!") + continue loop + } + + if signal.Name == FmtTransactionMethod("Package") { + //pkg_int, ok := signal.Body[0].(int) + packageId, ok := signal.Body[1].(string) + // format is: name;version;arch;data + if !ok { + continue loop + } + //comment, ok := signal.Body[2].(string) + for _, p := range packageIds { + if packageId == p { + continue loop // duplicate! + } + } + packageIds = append(packageIds, packageId) + } else if signal.Name == FmtTransactionMethod("Finished") { + // TODO: should we wait for the Destroy signal? + break loop + } else if signal.Name == FmtTransactionMethod("Destroy") { + // should already be broken + break loop + } else { + return []string{}, errors.New(fmt.Sprintf("PackageKit error: %v", signal.Body)) + } + } + } + return packageIds, nil +} + +func (bus *Conn) IsInstalledList(packages []string) ([]bool, error) { + var filter uint64 = 0 + filter += PK_FILTER_ENUM_ARCH // always search in our arch + packageIds, e := bus.ResolvePackages(packages, filter) + if e != nil { + return nil, errors.New(fmt.Sprintf("ResolvePackages error: %v", e)) + } + + var m map[string]int = make(map[string]int) + for _, packageId := range packageIds { + s := strings.Split(packageId, ";") + //if len(s) != 4 { continue } // this would be a bug! + pkg := s[0] + flags := strings.Split(s[3], ":") + for _, f := range flags { + if f == "installed" { + if _, exists := m[pkg]; !exists { + m[pkg] = 0 + } + m[pkg]++ // if we see pkg installed, increment + break + } + } + } + + var r []bool + for _, p := range packages { + if value, exists := m[p]; exists { + r = append(r, value > 0) // at least 1 means installed + } else { + r = append(r, false) + } + } + return r, nil +} + +// is package installed ? +// TODO: this could be optimized by making the resolve call directly +func (bus *Conn) IsInstalled(pkg string) (bool, error) { + p, e := bus.IsInstalledList([]string{pkg}) + if len(p) != 1 { + return false, e + } + return p[0], nil +} + +// install list of packages by packageId +func (bus *Conn) InstallPackages(packageIds []string, transactionFlags uint64) error { + + ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :( + interfacePath, err := bus.CreateTransaction() // emits Destroy on close + if err != nil { + return err + } + + var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ? + bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals) + + obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path + call := obj.Call(FmtTransactionMethod("InstallPackages"), 0, transactionFlags, packageIds) + if call.Err != nil { + return call.Err + } +loop: + for { + // FIXME: add a timeout option to error in case signals are dropped! + select { + case signal := <-ch: + if signal.Path != interfacePath { + log.Println("PackageKit: Some wires have been crossed!") + continue loop + } + + if signal.Name == FmtTransactionMethod("ErrorCode") { + return errors.New(fmt.Sprintf("PackageKit error: %v", signal.Body)) + } else if signal.Name == FmtTransactionMethod("Package") { + // a package was installed... + continue loop + } else if signal.Name == FmtTransactionMethod("Finished") { + // TODO: should we wait for the Destroy signal? + break loop + } else if signal.Name == FmtTransactionMethod("Destroy") { + // should already be broken + break loop + } else { + return errors.New(fmt.Sprintf("PackageKit error: %v", signal.Body)) + } + } + } + return nil +} + +// remove list of packages +func (bus *Conn) RemovePackages(packageIds []string, transactionFlags uint64) error { + + var allowDeps bool = true // TODO: configurable + var autoremove bool = false // unsupported on GNU/Linux + ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :( + interfacePath, err := bus.CreateTransaction() // emits Destroy on close + if err != nil { + return err + } + + var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ? + bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals) + + obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path + call := obj.Call(FmtTransactionMethod("RemovePackages"), 0, transactionFlags, packageIds, allowDeps, autoremove) + if call.Err != nil { + return call.Err + } +loop: + for { + // FIXME: add a timeout option to error in case signals are dropped! + select { + case signal := <-ch: + if signal.Path != interfacePath { + log.Println("PackageKit: Some wires have been crossed!") + continue loop + } + + if signal.Name == FmtTransactionMethod("ErrorCode") { + return errors.New(fmt.Sprintf("PackageKit error: %v", signal.Body)) + } else if signal.Name == FmtTransactionMethod("Package") { + // a package was installed... + continue loop + } else if signal.Name == FmtTransactionMethod("Finished") { + // TODO: should we wait for the Destroy signal? + break loop + } else if signal.Name == FmtTransactionMethod("Destroy") { + // should already be broken + break loop + } else { + return errors.New(fmt.Sprintf("PackageKit error: %v", signal.Body)) + } + } + } + return nil +} + +// update list of packages to versions that are specified +func (bus *Conn) UpdatePackages(packageIds []string, transactionFlags uint64) error { + ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :( + interfacePath, err := bus.CreateTransaction() + if err != nil { + return err + } + + var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ? + bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals) + + obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path + call := obj.Call(FmtTransactionMethod("UpdatePackages"), 0, transactionFlags, packageIds) + if call.Err != nil { + return call.Err + } +loop: + for { + // FIXME: add a timeout option to error in case signals are dropped! + select { + case signal := <-ch: + if signal.Path != interfacePath { + log.Println("PackageKit: Some wires have been crossed!") + continue loop + } + + if signal.Name == FmtTransactionMethod("ErrorCode") { + return errors.New(fmt.Sprintf("PackageKit error: %v", signal.Body)) + } else if signal.Name == FmtTransactionMethod("Package") { + // a package was installed... + continue loop + } else if signal.Name == FmtTransactionMethod("Finished") { + // TODO: should we wait for the Destroy signal? + break loop + } else if signal.Name == FmtTransactionMethod("Destroy") { + // should already be broken + break loop + } else { + return errors.New(fmt.Sprintf("PackageKit error: %v", signal.Body)) + } + } + } + return nil +} + +// does flag exist inside data portion of packageId field? +func FlagInData(flag, data string) bool { + flags := strings.Split(data, ":") + for _, f := range flags { + if f == flag { + return true + } + } + return false +} + +// builds the transaction method string +func FmtTransactionMethod(method string) string { + return fmt.Sprintf("%s.%s", PkIfaceTransaction, method) +} diff --git a/pkg.go b/pkg.go new file mode 100644 index 000000000..dfc94a8ca --- /dev/null +++ b/pkg.go @@ -0,0 +1,302 @@ +// Mgmt +// Copyright (C) 2013-2016+ 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 Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package main + +import ( + //"packagekit" // TODO + "errors" + "fmt" + "log" + "strings" +) + +type PkgRes struct { + BaseRes `yaml:",inline"` + State string `yaml:"state"` // state: installed, uninstalled, newest, + AllowUntrusted bool `yaml:"allowuntrusted"` // allow untrusted packages to be installed? + AllowNonFree bool `yaml:"allownonfree"` // allow nonfree packages to be found? + AllowUnsupported bool `yaml:"allowunsupported"` // allow unsupported packages to be found? +} + +func NewPkgRes(name, state string, allowuntrusted, allownonfree, allowunsupported bool) *PkgRes { + return &PkgRes{ + BaseRes: BaseRes{ + Name: name, + events: make(chan Event), + vertex: nil, + }, + State: state, + AllowUntrusted: allowuntrusted, + AllowNonFree: allownonfree, + AllowUnsupported: allowunsupported, + } +} + +func (obj *PkgRes) GetRes() string { + return "Pkg" +} + +func (obj *PkgRes) Validate() bool { + + if obj.State == "" { + return false + } + + return true +} + +// use UpdatesChanged signal to watch for changes +// TODO: https://github.com/hughsie/PackageKit/issues/109 +// TODO: https://github.com/hughsie/PackageKit/issues/110 +func (obj *PkgRes) Watch() { + if obj.IsWatching() { + return + } + obj.SetWatching(true) + defer obj.SetWatching(false) + + bus := NewBus() + if bus == nil { + log.Fatal("Can't connect to PackageKit bus.") + } + defer bus.Close() + + ch, err := bus.WatchChanges() + if err != nil { + log.Fatalf("Error adding signal match: %v", err) + } + + var send = false // send event? + var exit = false + var dirty = false + + for { + if DEBUG { + log.Printf("Pkg[%v]: Watching...", obj.GetName()) + } + + obj.SetState(resStateWatching) // reset + select { + case event := <-ch: + + // FIXME: ask packagekit for info on what packages changed + if DEBUG { + log.Printf("Pkg[%v]: Event: %v", obj.GetName(), event.Name) + } + + // since the chan is buffered, remove any supplemental + // events since they would just be duplicates anyways! + for len(ch) > 0 { // we can detect pending count here! + <-ch // discard + } + + obj.SetConvergedState(resConvergedNil) + send = true + dirty = true + + case event := <-obj.events: + obj.SetConvergedState(resConvergedNil) + if exit, send = obj.ReadEvent(&event); exit { + return // exit + } + //dirty = false // these events don't invalidate state + + case _ = <-TimeAfterOrBlock(obj.ctimeout): + obj.SetConvergedState(resConvergedTimeout) + obj.converged <- true + continue + } + + // do all our event sending all together to avoid duplicate msgs + if send { + send = false + // only invalid state on certain types of events + if dirty { + dirty = false + obj.isStateOK = false // something made state dirty + } + Process(obj) // XXX: rename this function + } + } +} + +func (obj *PkgRes) CheckApply(apply bool) (stateok bool, err error) { + log.Printf("%v[%v]: CheckApply(%t)", obj.GetRes(), obj.GetName(), apply) + + if obj.State == "" { // TODO: Validate() should replace this check! + log.Fatalf("%v[%v]: Package state is undefined!", obj.GetRes(), obj.GetName()) + } + + if obj.isStateOK { // cache the state + return true, nil + } + + bus := NewBus() + if bus == nil { + return false, errors.New("Can't connect to PackageKit bus.") + } + defer bus.Close() + + var packages = []string{obj.Name} + var filter uint64 = 0 + filter += PK_FILTER_ENUM_ARCH // always search in our arch + // we're requesting latest version, or to narrow down install choices! + if obj.State == "newest" || obj.State == "installed" { + // if we add this, we'll still see older packages if installed + filter += PK_FILTER_ENUM_NEWEST // only search for newest packages + } + if !obj.AllowNonFree { + filter += PK_FILTER_ENUM_FREE + } + if !obj.AllowUnsupported { + filter += PK_FILTER_ENUM_SUPPORTED + } + if DEBUG { + log.Printf("Pkg[%v]: ResolvePackages: %v", obj.GetName(), strings.Join(packages, ", ")) + } + resolved, e := bus.ResolvePackages(packages, filter) + if e != nil { + return false, errors.New(fmt.Sprintf("Resolve error: %v", e)) + } + + var found = false + var installed = false + var version = "" + var newest = true // assume, for now + var usePackageId = "" + for _, packageId := range resolved { + //log.Printf("* %v", packageId) + // format is: name;version;arch;data + s := strings.Split(packageId, ";") + //if len(s) != 4 { continue } // this would be a bug! + pkg, ver, _, data := s[0], s[1], s[2], s[3] + //arch := s[2] // TODO: double check match on arch? + if pkg != obj.Name { // not what we're looking for + continue + } + found = true + if obj.State != "installed" && obj.State != "uninstalled" && obj.State != "newest" { // must be a ver. string + if obj.State == ver && ver != "" { // we match what we want... + usePackageId = packageId + } + } + if FlagInData("installed", data) { + installed = true + version = ver + if obj.State == "uninstalled" { + usePackageId = packageId // save for later + } + } else { // not installed... + if obj.State == "installed" || obj.State == "newest" { + usePackageId = packageId + } + } + + // if the first iteration didn't contain the installed package, + // then since the NEWEST filter was on, we're not the newest! + if !installed { + newest = false + } + } + + // package doesn't exist, this is an error! + if !found { + return false, errors.New(fmt.Sprintf("Can't find package named '%s'.", obj.Name)) + } + + //obj.State == "installed" || "uninstalled" || "newest" || "4.2-1.fc23" + switch obj.State { + case "installed": + if installed { + return true, nil // state is correct, exit! + } + case "uninstalled": + if !installed { + return true, nil + } + case "newest": + if newest { + return true, nil + } + default: // version string + if obj.State == version && version != "" { + return true, nil + } + } + + if usePackageId == "" { + return false, errors.New("Can't find package id to use.") + } + + // state is not okay, no work done, exit, but without error + if !apply { + return false, nil + } + + packageList := []string{usePackageId} + var transactionFlags uint64 = 0 + if !obj.AllowUntrusted { // allow + transactionFlags += PK_TRANSACTION_FLAG_ENUM_ONLY_TRUSTED + } + // apply correct state! + log.Printf("%v[%v]: Set: %v...", obj.GetRes(), obj.GetName(), obj.State) + switch obj.State { + case "uninstalled": // run remove + // NOTE: packageId is different than when installed, because now + // it has the "installed" flag added to the data portion if it!! + err = bus.RemovePackages(packageList, transactionFlags) + + case "newest": // TODO: isn't this the same operation as install, below? + err = bus.UpdatePackages(packageList, transactionFlags) + + case "installed": + fallthrough // same method as for "set specific version", below + default: // version string + err = bus.InstallPackages(packageList, transactionFlags) + } + if err != nil { + return false, err // fail + } + log.Printf("%v[%v]: Set: %v success!", obj.GetRes(), obj.GetName(), obj.State) + return false, nil // success +} + +func (obj *PkgRes) Compare(res Res) bool { + switch res.(type) { + case *PkgRes: + res := res.(*PkgRes) + if obj.Name != res.Name { + return false + } + if obj.State != res.State { + return false + } + if obj.AllowUntrusted != res.AllowUntrusted { + return false + } + if obj.AllowNonFree != res.AllowNonFree { + return false + } + if obj.AllowUnsupported != res.AllowUnsupported { + return false + } + default: + return false + } + return true +} diff --git a/test/omv/pkg1a.yaml b/test/omv/pkg1a.yaml new file mode 100644 index 000000000..17d739ce1 --- /dev/null +++ b/test/omv/pkg1a.yaml @@ -0,0 +1,52 @@ +--- +:domain: example.com +:network: 192.168.123.0/24 +:image: centos-7.1 +:cpus: '' +:memory: '' +:disks: 0 +:disksize: 40G +:boxurlprefix: '' +:sync: rsync +:syncdir: '' +:syncsrc: '' +:folder: ".omv" +:extern: +- type: git + repository: https://github.com/purpleidea/mgmt + directory: mgmt +:cd: '' +:puppet: false +:classes: [] +:shell: +- mkdir /tmp/mgmt/ +:docker: false +:kubernetes: false +:ansible: [] +:playbook: [] +:ansible_extras: {} +:cachier: false +:vms: +- :name: mgmt1 + :shell: + - iptables -F + - cd /vagrant/mgmt/ && make path + - cd /vagrant/mgmt/ && make deps && make build && cp mgmt ~/bin/ + - etcd -bind-addr "`hostname --ip-address`:2379" & + - cd && mgmt run --file /vagrant/mgmt/examples/pkg1.yaml --converged-timeout=5 +:namespace: omv +:count: 0 +:username: '' +:password: '' +:poolid: true +:repos: [] +:update: false +:reboot: false +:unsafe: false +:nested: false +:tests: +- omv up +- vssh root@mgmt1 -c which powertop +- omv destroy +:comment: simple package install test case +:reallyrm: false diff --git a/test/omv/pkg1b.yaml b/test/omv/pkg1b.yaml new file mode 100644 index 000000000..416503a0d --- /dev/null +++ b/test/omv/pkg1b.yaml @@ -0,0 +1,52 @@ +--- +:domain: example.com +:network: 192.168.123.0/24 +:image: debian-8 +:cpus: '' +:memory: '' +:disks: 0 +:disksize: 40G +:boxurlprefix: '' +:sync: rsync +:syncdir: '' +:syncsrc: '' +:folder: ".omv" +:extern: +- type: git + repository: https://github.com/purpleidea/mgmt + directory: mgmt +:cd: '' +:puppet: false +:classes: [] +:shell: +- mkdir /tmp/mgmt/ +:docker: false +:kubernetes: false +:ansible: [] +:playbook: [] +:ansible_extras: {} +:cachier: false +:vms: +- :name: mgmt1 + :shell: + - iptables -F + - cd /vagrant/mgmt/ && make path + - cd /vagrant/mgmt/ && make deps && make build && cp mgmt ~/bin/ + - etcd -bind-addr "`hostname --ip-address`:2379" & + - cd && mgmt run --file /vagrant/mgmt/examples/pkg1.yaml --converged-timeout=5 +:namespace: omv +:count: 0 +:username: '' +:password: '' +:poolid: true +:repos: [] +:update: false +:reboot: false +:unsafe: false +:nested: false +:tests: +- omv up +- vssh root@mgmt1 -c which powertop +- omv destroy +:comment: simple package install test case +:reallyrm: false diff --git a/test/test-omv.sh b/test/test-omv.sh index d382c5b30..d65b712ec 100755 --- a/test/test-omv.sh +++ b/test/test-omv.sh @@ -6,7 +6,10 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # dir! cd "$DIR" >/dev/null # work from test directory # vtest+ tests -vtest+ omv/helloworld.yaml +for i in omv/*.yaml; do + echo "running: vtest+ $i" + vtest+ "$i" +done # return to original dir cd "$CWD" >/dev/null