Add package (pkg) resource
This is based on PackageKit, which means events, *and* we automatically get support for any of the backends that PackageKit supports. This means dpkg, and rpm are both first class citizens! Many other backends will surely work, although thorough testing is left as an exercise to the reader, or to someone who would like to write more test cases! Unfortunately at the moment, there are a few upstream PackageKit bugs which cause us issues, but those have been apparently resolved upstream. If you experience issues with an old version of PackageKit, test if it is working correctly before blaming mgmt :) In parallel, mgmt might increase the testing surface for PackageKit, so hopefully this makes it more robust for everyone involved! Lastly, I'd like to point out that many great things that are typically used for servers do start in the GNOME desktop world. Help support your GNOME GNU/Linux desktop today!
This commit is contained in:
14
config.go
14
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?)
|
||||
|
||||
7
examples/pkg1.yaml
Normal file
7
examples/pkg1.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
graph: mygraph
|
||||
resources:
|
||||
pkg:
|
||||
- name: powertop
|
||||
state: installed
|
||||
edges: []
|
||||
7
examples/pkg2.yaml
Normal file
7
examples/pkg2.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
graph: mygraph
|
||||
resources:
|
||||
pkg:
|
||||
- name: powertop
|
||||
state: uninstalled
|
||||
edges: []
|
||||
20
misc.go
20
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
|
||||
}
|
||||
|
||||
479
packagekit.go
Normal file
479
packagekit.go
Normal file
@@ -0,0 +1,479 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// 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 <signal>
|
||||
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)
|
||||
}
|
||||
302
pkg.go
Normal file
302
pkg.go
Normal file
@@ -0,0 +1,302 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
//"packagekit" // TODO
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PkgRes struct {
|
||||
BaseRes `yaml:",inline"`
|
||||
State string `yaml:"state"` // state: installed, uninstalled, newest, <version>
|
||||
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
|
||||
}
|
||||
52
test/omv/pkg1a.yaml
Normal file
52
test/omv/pkg1a.yaml
Normal file
@@ -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
|
||||
52
test/omv/pkg1b.yaml
Normal file
52
test/omv/pkg1b.yaml
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user