From 37942f16a6ac77a13b3a652537a395ceb6fefc7e Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Mon, 31 Aug 2015 15:27:04 -0400 Subject: [PATCH] Add a mockable dbus interface to pkg/util --- pkg/util/dbus/dbus.go | 133 ++++++++++++++++++++ pkg/util/dbus/dbus_test.go | 249 +++++++++++++++++++++++++++++++++++++ pkg/util/dbus/doc.go | 18 +++ pkg/util/dbus/fake_dbus.go | 135 ++++++++++++++++++++ 4 files changed, 535 insertions(+) create mode 100644 pkg/util/dbus/dbus.go create mode 100644 pkg/util/dbus/dbus_test.go create mode 100644 pkg/util/dbus/doc.go create mode 100644 pkg/util/dbus/fake_dbus.go diff --git a/pkg/util/dbus/dbus.go b/pkg/util/dbus/dbus.go new file mode 100644 index 00000000000..3858aec28f5 --- /dev/null +++ b/pkg/util/dbus/dbus.go @@ -0,0 +1,133 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dbus + +import ( + godbus "github.com/godbus/dbus" +) + +// Interface is an interface that presents a subset of the godbus/dbus API. Use this +// when you want to inject fakeable/mockable D-Bus behavior. +type Interface interface { + // SystemBus returns a connection to the system bus, connecting to it + // first if necessary + SystemBus() (Connection, error) + // SessionBus returns a connection to the session bus, connecting to it + // first if necessary + SessionBus() (Connection, error) +} + +// Connection represents a D-Bus connection +type Connection interface { + // Returns an Object representing the bus itself + BusObject() Object + + // Object creates a representation of a remote D-Bus object + Object(name, path string) Object + + // Signal registers or unregisters a channel to receive D-Bus signals + Signal(ch chan<- *godbus.Signal) +} + +// Object represents a remote D-Bus object +type Object interface { + // Call synchronously calls a D-Bus method + Call(method string, flags godbus.Flags, args ...interface{}) Call +} + +// Call represents a pending or completed D-Bus method call +type Call interface { + // Store returns a completed call's return values, or an error + Store(retvalues ...interface{}) error +} + +// Implements Interface in terms of actually talking to D-Bus +type dbusImpl struct { + systemBus *connImpl + sessionBus *connImpl +} + +// Implements Connection as a godbus.Conn +type connImpl struct { + conn *godbus.Conn +} + +// Implements Object as a godbus.Object +type objectImpl struct { + object *godbus.Object +} + +// Implements Call as a godbus.Call +type callImpl struct { + call *godbus.Call +} + +// New returns a new Interface which will use godbus to talk to D-Bus +func New() Interface { + return &dbusImpl{} +} + +// SystemBus is part of Interface +func (db *dbusImpl) SystemBus() (Connection, error) { + if db.systemBus == nil { + bus, err := godbus.SystemBus() + if err != nil { + return nil, err + } + db.systemBus = &connImpl{bus} + } + + return db.systemBus, nil +} + +// SessionBus is part of Interface +func (db *dbusImpl) SessionBus() (Connection, error) { + if db.sessionBus == nil { + bus, err := godbus.SessionBus() + if err != nil { + return nil, err + } + db.sessionBus = &connImpl{bus} + } + + return db.sessionBus, nil +} + +// BusObject is part of the Connection interface +func (conn *connImpl) BusObject() Object { + return &objectImpl{conn.conn.BusObject()} +} + +// Object is part of the Connection interface +func (conn *connImpl) Object(name, path string) Object { + return &objectImpl{conn.conn.Object(name, godbus.ObjectPath(path))} +} + +// Signal is part of the Connection interface +func (conn *connImpl) Signal(ch chan<- *godbus.Signal) { + conn.conn.Signal(ch) +} + +// Call is part of the Object interface +func (obj *objectImpl) Call(method string, flags godbus.Flags, args ...interface{}) Call { + return &callImpl{obj.object.Call(method, flags, args...)} +} + +// Store is part of the Call interface +func (call *callImpl) Store(retvalues ...interface{}) error { + return call.call.Store(retvalues...) +} diff --git a/pkg/util/dbus/dbus_test.go b/pkg/util/dbus/dbus_test.go new file mode 100644 index 00000000000..3cf52263af1 --- /dev/null +++ b/pkg/util/dbus/dbus_test.go @@ -0,0 +1,249 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dbus + +import ( + "fmt" + "os" + "testing" + + godbus "github.com/godbus/dbus" +) + +const ( + DBusNameFlagAllowReplacement uint32 = 1 << (iota + 1) + DBusNameFlagReplaceExisting + DBusNameFlagDoNotQueue +) + +const ( + DBusRequestNameReplyPrimaryOwner uint32 = iota + 1 + DBusRequestNameReplyInQueue + DBusRequestNameReplyExists + DBusRequestNameReplyAlreadyOwner +) + +const ( + DBusReleaseNameReplyReleased uint32 = iota + 1 + DBusReleaseNameReplyNonExistent + DBusReleaseNameReplyNotOwner +) + +func doDBusTest(t *testing.T, dbus Interface, real bool) { + bus, err := dbus.SystemBus() + if err != nil { + if !real { + t.Errorf("dbus.SystemBus() failed with fake Interface") + } + t.Skipf("D-Bus is not running: %v", err) + } + busObj := bus.BusObject() + + id := "" + err = busObj.Call("org.freedesktop.DBus.GetId", 0).Store(&id) + if err != nil { + t.Errorf("expected success, got %v", err) + } + if len(id) == 0 { + t.Errorf("expected non-empty Id, got \"\"") + } + + // Switch to the session bus for the rest, since the system bus is more + // locked down (and thus harder to trick into emitting signals). + + bus, err = dbus.SessionBus() + if err != nil { + if !real { + t.Errorf("dbus.SystemBus() failed with fake Interface") + } + t.Skipf("D-Bus session bus is not available: %v", err) + } + busObj = bus.BusObject() + + name := fmt.Sprintf("io.kubernetes.dbus_test_%d", os.Getpid()) + owner := "" + err = busObj.Call("org.freedesktop.DBus.GetNameOwner", 0, name).Store(&owner) + if err == nil { + t.Errorf("expected '%s' to be un-owned, but found owner %s", name, owner) + } + dbuserr, ok := err.(godbus.Error) + if !ok { + t.Errorf("expected godbus.Error, but got %#v", err) + } + if dbuserr.Name != "org.freedesktop.DBus.Error.NameHasNoOwner" { + t.Errorf("expected NameHasNoOwner error but got %v", err) + } + + sigchan := make(chan *godbus.Signal, 10) + bus.Signal(sigchan) + + rule := fmt.Sprintf("type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',path='/org/freedesktop/DBus',sender='org.freedesktop.DBus',arg0='%s'", name) + err = busObj.Call("org.freedesktop.DBus.AddMatch", 0, rule).Store() + if err != nil { + t.Errorf("expected success, got %v", err) + } + + var ret uint32 + err = busObj.Call("org.freedesktop.DBus.RequestName", 0, name, DBusNameFlagDoNotQueue).Store(&ret) + if err != nil { + t.Errorf("expected success, got %v", err) + } + if ret != DBusRequestNameReplyPrimaryOwner { + t.Errorf("expected %v, got %v", DBusRequestNameReplyPrimaryOwner, ret) + } + + err = busObj.Call("org.freedesktop.DBus.GetNameOwner", 0, name).Store(&owner) + if err != nil { + t.Errorf("expected success, got %v", err) + } + + var changedSignal, acquiredSignal, lostSignal *godbus.Signal + + sig1 := <-sigchan + sig2 := <-sigchan + // We get two signals, but the order isn't guaranteed + if sig1.Name == "org.freedesktop.DBus.NameOwnerChanged" { + changedSignal = sig1 + acquiredSignal = sig2 + } else { + acquiredSignal = sig1 + changedSignal = sig2 + } + + if acquiredSignal.Sender != "org.freedesktop.DBus" || acquiredSignal.Name != "org.freedesktop.DBus.NameAcquired" { + t.Errorf("expected NameAcquired signal, got %v", acquiredSignal) + } + acquiredName := acquiredSignal.Body[0].(string) + if acquiredName != name { + t.Errorf("unexpected NameAcquired arguments: %v", acquiredSignal) + } + + if changedSignal.Sender != "org.freedesktop.DBus" || changedSignal.Name != "org.freedesktop.DBus.NameOwnerChanged" { + t.Errorf("expected NameOwnerChanged signal, got %v", changedSignal) + } + + changedName := changedSignal.Body[0].(string) + oldOwner := changedSignal.Body[1].(string) + newOwner := changedSignal.Body[2].(string) + if changedName != name || oldOwner != "" || newOwner != owner { + t.Errorf("unexpected NameOwnerChanged arguments: %v", changedSignal) + } + + err = busObj.Call("org.freedesktop.DBus.ReleaseName", 0, name).Store(&ret) + if err != nil { + t.Errorf("expected success, got %v", err) + } + if ret != DBusReleaseNameReplyReleased { + t.Errorf("expected %v, got %v", DBusReleaseNameReplyReleased, ret) + } + + sig1 = <-sigchan + sig2 = <-sigchan + if sig1.Name == "org.freedesktop.DBus.NameOwnerChanged" { + changedSignal = sig1 + lostSignal = sig2 + } else { + lostSignal = sig1 + changedSignal = sig2 + } + + if lostSignal.Sender != "org.freedesktop.DBus" || lostSignal.Name != "org.freedesktop.DBus.NameLost" { + t.Errorf("expected NameLost signal, got %v", lostSignal) + } + lostName := lostSignal.Body[0].(string) + if lostName != name { + t.Errorf("unexpected NameLost arguments: %v", lostSignal) + } + + if changedSignal.Sender != "org.freedesktop.DBus" || changedSignal.Name != "org.freedesktop.DBus.NameOwnerChanged" { + t.Errorf("expected NameOwnerChanged signal, got %v", changedSignal) + } + + changedName = changedSignal.Body[0].(string) + oldOwner = changedSignal.Body[1].(string) + newOwner = changedSignal.Body[2].(string) + if changedName != name || oldOwner != owner || newOwner != "" { + t.Errorf("unexpected NameOwnerChanged arguments: %v", changedSignal) + } + + if len(sigchan) != 0 { + t.Errorf("unexpected extra signals (%d)", len(sigchan)) + } + + // Unregister sigchan + bus.Signal(sigchan) +} + +func TestRealDBus(t *testing.T) { + dbus := New() + doDBusTest(t, dbus, true) +} + +func TestFakeDBus(t *testing.T) { + uniqueName := ":1.1" + ownedName := "" + + fakeSystem := NewFakeConnection() + fakeSystem.SetBusObject( + func(method string, args ...interface{}) ([]interface{}, error) { + if method == "org.freedesktop.DBus.GetId" { + return []interface{}{"foo"}, nil + } + return nil, fmt.Errorf("unexpected method call '%s'", method) + }, + ) + + fakeSession := NewFakeConnection() + fakeSession.SetBusObject( + func(method string, args ...interface{}) ([]interface{}, error) { + if method == "org.freedesktop.DBus.GetNameOwner" { + checkName := args[0].(string) + if checkName != ownedName { + return nil, godbus.Error{"org.freedesktop.DBus.Error.NameHasNoOwner", nil} + } else { + return []interface{}{uniqueName}, nil + } + } else if method == "org.freedesktop.DBus.RequestName" { + reqName := args[0].(string) + _ = args[1].(uint32) + if ownedName != "" { + return []interface{}{DBusRequestNameReplyAlreadyOwner}, nil + } + ownedName = reqName + fakeSession.EmitSignal("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "NameAcquired", reqName) + fakeSession.EmitSignal("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "NameOwnerChanged", reqName, "", uniqueName) + return []interface{}{DBusRequestNameReplyPrimaryOwner}, nil + } else if method == "org.freedesktop.DBus.ReleaseName" { + reqName := args[0].(string) + if reqName != ownedName { + return []interface{}{DBusReleaseNameReplyNotOwner}, nil + } + ownedName = "" + fakeSession.EmitSignal("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "NameOwnerChanged", reqName, uniqueName, "") + fakeSession.EmitSignal("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "NameLost", reqName) + return []interface{}{DBusReleaseNameReplyReleased}, nil + } else if method == "org.freedesktop.DBus.AddMatch" { + return nil, nil + } else { + return nil, fmt.Errorf("unexpected method call '%s'", method) + } + }, + ) + + dbus := NewFake(fakeSystem, fakeSession) + doDBusTest(t, dbus, false) +} diff --git a/pkg/util/dbus/doc.go b/pkg/util/dbus/doc.go new file mode 100644 index 00000000000..59bec0e5606 --- /dev/null +++ b/pkg/util/dbus/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package dbus provides an injectable interface and implementations for D-Bus communication +package dbus diff --git a/pkg/util/dbus/fake_dbus.go b/pkg/util/dbus/fake_dbus.go new file mode 100644 index 00000000000..eb97febae49 --- /dev/null +++ b/pkg/util/dbus/fake_dbus.go @@ -0,0 +1,135 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dbus + +import ( + "fmt" + + godbus "github.com/godbus/dbus" +) + +// DBusFake is a simple fake Interface type. +type DBusFake struct { + systemBus *DBusFakeConnection + sessionBus *DBusFakeConnection +} + +// DBusFakeConnection represents a fake D-Bus connection +type DBusFakeConnection struct { + busObject *fakeObject + objects map[string]*fakeObject + signalHandlers []chan<- *godbus.Signal +} + +// DBusFakeHandler is used to handle fake D-Bus method calls +type DBusFakeHandler func(method string, args ...interface{}) ([]interface{}, error) + +type fakeObject struct { + handler DBusFakeHandler +} + +type fakeCall struct { + ret []interface{} + err error +} + +// NewFake returns a new Interface which will fake talking to D-Bus +func NewFake(systemBus *DBusFakeConnection, sessionBus *DBusFakeConnection) *DBusFake { + return &DBusFake{systemBus, sessionBus} +} + +func NewFakeConnection() *DBusFakeConnection { + return &DBusFakeConnection{ + objects: make(map[string]*fakeObject), + } +} + +// SystemBus is part of Interface +func (db *DBusFake) SystemBus() (Connection, error) { + if db.systemBus != nil { + return db.systemBus, nil + } else { + return nil, fmt.Errorf("DBus is not running") + } +} + +// SessionBus is part of Interface +func (db *DBusFake) SessionBus() (Connection, error) { + if db.sessionBus != nil { + return db.sessionBus, nil + } else { + return nil, fmt.Errorf("DBus is not running") + } +} + +// BusObject is part of the Connection interface +func (conn *DBusFakeConnection) BusObject() Object { + return conn.busObject +} + +// Object is part of the Connection interface +func (conn *DBusFakeConnection) Object(name, path string) Object { + return conn.objects[name+path] +} + +// Signal is part of the Connection interface +func (conn *DBusFakeConnection) Signal(ch chan<- *godbus.Signal) { + for i := range conn.signalHandlers { + if conn.signalHandlers[i] == ch { + conn.signalHandlers = append(conn.signalHandlers[:i], conn.signalHandlers[i+1:]...) + return + } + } + conn.signalHandlers = append(conn.signalHandlers, ch) +} + +// SetBusObject sets the handler for the BusObject of conn +func (conn *DBusFakeConnection) SetBusObject(handler DBusFakeHandler) { + conn.busObject = &fakeObject{handler} +} + +// AddObject adds a handler for the Object at name and path +func (conn *DBusFakeConnection) AddObject(name, path string, handler DBusFakeHandler) { + conn.objects[name+path] = &fakeObject{handler} +} + +// EmitSignal emits a signal on conn +func (conn *DBusFakeConnection) EmitSignal(name, path, iface, signal string, args ...interface{}) { + sig := &godbus.Signal{ + Sender: name, + Path: godbus.ObjectPath(path), + Name: iface + "." + signal, + Body: args, + } + for _, ch := range conn.signalHandlers { + ch <- sig + } +} + +// Call is part of the Object interface +func (obj *fakeObject) Call(method string, flags godbus.Flags, args ...interface{}) Call { + ret, err := obj.handler(method, args...) + return &fakeCall{ret, err} +} + +// Store is part of the Call interface +func (call *fakeCall) Store(retvalues ...interface{}) error { + if call.err != nil { + return call.err + } + return godbus.Store(call.ret, retvalues...) +}