Add GO api for openvpn control

This commit is contained in:
Praneeth Bodduluri 2015-09-24 21:21:33 +05:30 committed by Pablo Carranza Vélez
parent 98c9ee2c21
commit cd9daa8a8b
15 changed files with 1881 additions and 2 deletions

View File

@ -1,10 +1,15 @@
{
"ImportPath": "resin-supervisor/gosuper",
"GoVersion": "go1.3.3",
"GoVersion": "go1.5",
"Packages": [
"./..."
],
"Deps": [
{
"ImportPath": "github.com/coreos/go-systemd/dbus",
"Comment": "v3-13-g392c689",
"Rev": "392c689e425689acb51976419a8d62878966511f"
},
{
"ImportPath": "github.com/coreos/go-systemd/login1",
"Comment": "v3-13-g392c689",

View File

@ -0,0 +1,187 @@
// Copyright 2015 CoreOS, Inc.
//
// 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.
// Integration with the systemd D-Bus API. See http://www.freedesktop.org/wiki/Software/systemd/dbus/
package dbus
import (
"fmt"
"os"
"strconv"
"strings"
"sync"
"resin-supervisor/gosuper/Godeps/_workspace/src/github.com/godbus/dbus"
)
const (
alpha = `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`
num = `0123456789`
alphanum = alpha + num
signalBuffer = 100
)
// needsEscape checks whether a byte in a potential dbus ObjectPath needs to be escaped
func needsEscape(i int, b byte) bool {
// Escape everything that is not a-z-A-Z-0-9
// Also escape 0-9 if it's the first character
return strings.IndexByte(alphanum, b) == -1 ||
(i == 0 && strings.IndexByte(num, b) != -1)
}
// PathBusEscape sanitizes a constituent string of a dbus ObjectPath using the
// rules that systemd uses for serializing special characters.
func PathBusEscape(path string) string {
// Special case the empty string
if len(path) == 0 {
return "_"
}
n := []byte{}
for i := 0; i < len(path); i++ {
c := path[i]
if needsEscape(i, c) {
e := fmt.Sprintf("_%x", c)
n = append(n, []byte(e)...)
} else {
n = append(n, c)
}
}
return string(n)
}
// Conn is a connection to systemd's dbus endpoint.
type Conn struct {
// sysconn/sysobj are only used to call dbus methods
sysconn *dbus.Conn
sysobj dbus.BusObject
// sigconn/sigobj are only used to receive dbus signals
sigconn *dbus.Conn
sigobj dbus.BusObject
jobListener struct {
jobs map[dbus.ObjectPath]chan<- string
sync.Mutex
}
subscriber struct {
updateCh chan<- *SubStateUpdate
errCh chan<- error
sync.Mutex
ignore map[dbus.ObjectPath]int64
cleanIgnore int64
}
}
// New establishes a connection to the system bus and authenticates.
// Callers should call Close() when done with the connection.
func New() (*Conn, error) {
return newConnection(func() (*dbus.Conn, error) {
return dbusAuthHelloConnection(dbus.SystemBusPrivate)
})
}
// NewUserConnection establishes a connection to the session bus and
// authenticates. This can be used to connect to systemd user instances.
// Callers should call Close() when done with the connection.
func NewUserConnection() (*Conn, error) {
return newConnection(func() (*dbus.Conn, error) {
return dbusAuthHelloConnection(dbus.SessionBusPrivate)
})
}
// NewSystemdConnection establishes a private, direct connection to systemd.
// This can be used for communicating with systemd without a dbus daemon.
// Callers should call Close() when done with the connection.
func NewSystemdConnection() (*Conn, error) {
return newConnection(func() (*dbus.Conn, error) {
// We skip Hello when talking directly to systemd.
return dbusAuthConnection(func() (*dbus.Conn, error) {
return dbus.Dial("unix:path=/run/systemd/private")
})
})
}
// Close closes an established connection
func (c *Conn) Close() {
c.sysconn.Close()
c.sigconn.Close()
}
func newConnection(createBus func() (*dbus.Conn, error)) (*Conn, error) {
sysconn, err := createBus()
if err != nil {
return nil, err
}
sigconn, err := createBus()
if err != nil {
sysconn.Close()
return nil, err
}
c := &Conn{
sysconn: sysconn,
sysobj: systemdObject(sysconn),
sigconn: sigconn,
sigobj: systemdObject(sigconn),
}
c.subscriber.ignore = make(map[dbus.ObjectPath]int64)
c.jobListener.jobs = make(map[dbus.ObjectPath]chan<- string)
// Setup the listeners on jobs so that we can get completions
c.sigconn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0,
"type='signal', interface='org.freedesktop.systemd1.Manager', member='JobRemoved'")
c.dispatch()
return c, nil
}
func dbusAuthConnection(createBus func() (*dbus.Conn, error)) (*dbus.Conn, error) {
conn, err := createBus()
if err != nil {
return nil, err
}
// Only use EXTERNAL method, and hardcode the uid (not username)
// to avoid a username lookup (which requires a dynamically linked
// libc)
methods := []dbus.Auth{dbus.AuthExternal(strconv.Itoa(os.Getuid()))}
err = conn.Auth(methods)
if err != nil {
conn.Close()
return nil, err
}
return conn, nil
}
func dbusAuthHelloConnection(createBus func() (*dbus.Conn, error)) (*dbus.Conn, error) {
conn, err := dbusAuthConnection(createBus)
if err != nil {
return nil, err
}
if err = conn.Hello(); err != nil {
conn.Close()
return nil, err
}
return conn, nil
}
func systemdObject(conn *dbus.Conn) dbus.BusObject {
return conn.Object("org.freedesktop.systemd1", dbus.ObjectPath("/org/freedesktop/systemd1"))
}

View File

@ -0,0 +1,77 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 (
"testing"
)
func TestNeedsEscape(t *testing.T) {
// Anything not 0-9a-zA-Z should always be escaped
for want, vals := range map[bool][]byte{
false: []byte{'a', 'b', 'z', 'A', 'Q', '1', '4', '9'},
true: []byte{'#', '%', '$', '!', '.', '_', '-', '%', '\\'},
} {
for i := 1; i < 10; i++ {
for _, b := range vals {
got := needsEscape(i, b)
if got != want {
t.Errorf("needsEscape(%d, %c) returned %t, want %t", i, b, got, want)
}
}
}
}
// 0-9 in position 0 should be escaped
for want, vals := range map[bool][]byte{
false: []byte{'A', 'a', 'e', 'x', 'Q', 'Z'},
true: []byte{'0', '4', '5', '9'},
} {
for _, b := range vals {
got := needsEscape(0, b)
if got != want {
t.Errorf("needsEscape(0, %c) returned %t, want %t", b, got, want)
}
}
}
}
func TestPathBusEscape(t *testing.T) {
for in, want := range map[string]string{
"": "_",
"foo.service": "foo_2eservice",
"foobar": "foobar",
"woof@woof.service": "woof_40woof_2eservice",
"0123456": "_30123456",
"account_db.service": "account_5fdb_2eservice",
"got-dashes": "got_2ddashes",
} {
got := PathBusEscape(in)
if got != want {
t.Errorf("bad result for PathBusEscape(%s): got %q, want %q", in, got, want)
}
}
}
// TestNew ensures that New() works without errors.
func TestNew(t *testing.T) {
_, err := New()
if err != nil {
t.Fatal(err)
}
}

View File

@ -0,0 +1,410 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 (
"errors"
"path"
"strconv"
"resin-supervisor/gosuper/Godeps/_workspace/src/github.com/godbus/dbus"
)
func (c *Conn) jobComplete(signal *dbus.Signal) {
var id uint32
var job dbus.ObjectPath
var unit string
var result string
dbus.Store(signal.Body, &id, &job, &unit, &result)
c.jobListener.Lock()
out, ok := c.jobListener.jobs[job]
if ok {
out <- result
delete(c.jobListener.jobs, job)
}
c.jobListener.Unlock()
}
func (c *Conn) startJob(ch chan<- string, job string, args ...interface{}) (int, error) {
if ch != nil {
c.jobListener.Lock()
defer c.jobListener.Unlock()
}
var p dbus.ObjectPath
err := c.sysobj.Call(job, 0, args...).Store(&p)
if err != nil {
return 0, err
}
if ch != nil {
c.jobListener.jobs[p] = ch
}
// ignore error since 0 is fine if conversion fails
jobID, _ := strconv.Atoi(path.Base(string(p)))
return jobID, nil
}
// StartUnit enqueues a start job and depending jobs, if any (unless otherwise
// specified by the mode string).
//
// Takes the unit to activate, plus a mode string. The mode needs to be one of
// replace, fail, isolate, ignore-dependencies, ignore-requirements. If
// "replace" the call will start the unit and its dependencies, possibly
// replacing already queued jobs that conflict with this. If "fail" the call
// will start the unit and its dependencies, but will fail if this would change
// an already queued job. If "isolate" the call will start the unit in question
// and terminate all units that aren't dependencies of it. If
// "ignore-dependencies" it will start a unit but ignore all its dependencies.
// If "ignore-requirements" it will start a unit but only ignore the
// requirement dependencies. It is not recommended to make use of the latter
// two options.
//
// If the provided channel is non-nil, a result string will be sent to it upon
// job completion: one of done, canceled, timeout, failed, dependency, skipped.
// done indicates successful execution of a job. canceled indicates that a job
// has been canceled before it finished execution. timeout indicates that the
// job timeout was reached. failed indicates that the job failed. dependency
// indicates that a job this job has been depending on failed and the job hence
// has been removed too. skipped indicates that a job was skipped because it
// didn't apply to the units current state.
//
// If no error occurs, the ID of the underlying systemd job will be returned. There
// does exist the possibility for no error to be returned, but for the returned job
// ID to be 0. In this case, the actual underlying ID is not 0 and this datapoint
// should not be considered authoritative.
//
// If an error does occur, it will be returned to the user alongside a job ID of 0.
func (c *Conn) StartUnit(name string, mode string, ch chan<- string) (int, error) {
return c.startJob(ch, "org.freedesktop.systemd1.Manager.StartUnit", name, mode)
}
// StopUnit is similar to StartUnit but stops the specified unit rather
// than starting it.
func (c *Conn) StopUnit(name string, mode string, ch chan<- string) (int, error) {
return c.startJob(ch, "org.freedesktop.systemd1.Manager.StopUnit", name, mode)
}
// ReloadUnit reloads a unit. Reloading is done only if the unit is already running and fails otherwise.
func (c *Conn) ReloadUnit(name string, mode string, ch chan<- string) (int, error) {
return c.startJob(ch, "org.freedesktop.systemd1.Manager.ReloadUnit", name, mode)
}
// RestartUnit restarts a service. If a service is restarted that isn't
// running it will be started.
func (c *Conn) RestartUnit(name string, mode string, ch chan<- string) (int, error) {
return c.startJob(ch, "org.freedesktop.systemd1.Manager.RestartUnit", name, mode)
}
// TryRestartUnit is like RestartUnit, except that a service that isn't running
// is not affected by the restart.
func (c *Conn) TryRestartUnit(name string, mode string, ch chan<- string) (int, error) {
return c.startJob(ch, "org.freedesktop.systemd1.Manager.TryRestartUnit", name, mode)
}
// ReloadOrRestart attempts a reload if the unit supports it and use a restart
// otherwise.
func (c *Conn) ReloadOrRestartUnit(name string, mode string, ch chan<- string) (int, error) {
return c.startJob(ch, "org.freedesktop.systemd1.Manager.ReloadOrRestartUnit", name, mode)
}
// ReloadOrTryRestart attempts a reload if the unit supports it and use a "Try"
// flavored restart otherwise.
func (c *Conn) ReloadOrTryRestartUnit(name string, mode string, ch chan<- string) (int, error) {
return c.startJob(ch, "org.freedesktop.systemd1.Manager.ReloadOrTryRestartUnit", name, mode)
}
// StartTransientUnit() may be used to create and start a transient unit, which
// will be released as soon as it is not running or referenced anymore or the
// system is rebooted. name is the unit name including suffix, and must be
// unique. mode is the same as in StartUnit(), properties contains properties
// of the unit.
func (c *Conn) StartTransientUnit(name string, mode string, properties []Property, ch chan<- string) (int, error) {
return c.startJob(ch, "org.freedesktop.systemd1.Manager.StartTransientUnit", name, mode, properties, make([]PropertyCollection, 0))
}
// KillUnit takes the unit name and a UNIX signal number to send. All of the unit's
// processes are killed.
func (c *Conn) KillUnit(name string, signal int32) {
c.sysobj.Call("org.freedesktop.systemd1.Manager.KillUnit", 0, name, "all", signal).Store()
}
// ResetFailedUnit resets the "failed" state of a specific unit.
func (c *Conn) ResetFailedUnit(name string) error {
return c.sysobj.Call("org.freedesktop.systemd1.Manager.ResetFailedUnit", 0, name).Store()
}
// getProperties takes the unit name and returns all of its dbus object properties, for the given dbus interface
func (c *Conn) getProperties(unit string, dbusInterface string) (map[string]interface{}, error) {
var err error
var props map[string]dbus.Variant
path := unitPath(unit)
if !path.IsValid() {
return nil, errors.New("invalid unit name: " + unit)
}
obj := c.sysconn.Object("org.freedesktop.systemd1", path)
err = obj.Call("org.freedesktop.DBus.Properties.GetAll", 0, dbusInterface).Store(&props)
if err != nil {
return nil, err
}
out := make(map[string]interface{}, len(props))
for k, v := range props {
out[k] = v.Value()
}
return out, nil
}
// GetUnitProperties takes the unit name and returns all of its dbus object properties.
func (c *Conn) GetUnitProperties(unit string) (map[string]interface{}, error) {
return c.getProperties(unit, "org.freedesktop.systemd1.Unit")
}
func (c *Conn) getProperty(unit string, dbusInterface string, propertyName string) (*Property, error) {
var err error
var prop dbus.Variant
path := unitPath(unit)
if !path.IsValid() {
return nil, errors.New("invalid unit name: " + unit)
}
obj := c.sysconn.Object("org.freedesktop.systemd1", path)
err = obj.Call("org.freedesktop.DBus.Properties.Get", 0, dbusInterface, propertyName).Store(&prop)
if err != nil {
return nil, err
}
return &Property{Name: propertyName, Value: prop}, nil
}
func (c *Conn) GetUnitProperty(unit string, propertyName string) (*Property, error) {
return c.getProperty(unit, "org.freedesktop.systemd1.Unit", propertyName)
}
// GetUnitTypeProperties returns the extra properties for a unit, specific to the unit type.
// Valid values for unitType: Service, Socket, Target, Device, Mount, Automount, Snapshot, Timer, Swap, Path, Slice, Scope
// return "dbus.Error: Unknown interface" if the unitType is not the correct type of the unit
func (c *Conn) GetUnitTypeProperties(unit string, unitType string) (map[string]interface{}, error) {
return c.getProperties(unit, "org.freedesktop.systemd1."+unitType)
}
// SetUnitProperties() may be used to modify certain unit properties at runtime.
// Not all properties may be changed at runtime, but many resource management
// settings (primarily those in systemd.cgroup(5)) may. The changes are applied
// instantly, and stored on disk for future boots, unless runtime is true, in which
// case the settings only apply until the next reboot. name is the name of the unit
// to modify. properties are the settings to set, encoded as an array of property
// name and value pairs.
func (c *Conn) SetUnitProperties(name string, runtime bool, properties ...Property) error {
return c.sysobj.Call("org.freedesktop.systemd1.Manager.SetUnitProperties", 0, name, runtime, properties).Store()
}
func (c *Conn) GetUnitTypeProperty(unit string, unitType string, propertyName string) (*Property, error) {
return c.getProperty(unit, "org.freedesktop.systemd1."+unitType, propertyName)
}
// ListUnits returns an array with all currently loaded units. Note that
// units may be known by multiple names at the same time, and hence there might
// be more unit names loaded than actual units behind them.
func (c *Conn) ListUnits() ([]UnitStatus, error) {
result := make([][]interface{}, 0)
err := c.sysobj.Call("org.freedesktop.systemd1.Manager.ListUnits", 0).Store(&result)
if err != nil {
return nil, err
}
resultInterface := make([]interface{}, len(result))
for i := range result {
resultInterface[i] = result[i]
}
status := make([]UnitStatus, len(result))
statusInterface := make([]interface{}, len(status))
for i := range status {
statusInterface[i] = &status[i]
}
err = dbus.Store(resultInterface, statusInterface...)
if err != nil {
return nil, err
}
return status, nil
}
type UnitStatus struct {
Name string // The primary unit name as string
Description string // The human readable description string
LoadState string // The load state (i.e. whether the unit file has been loaded successfully)
ActiveState string // The active state (i.e. whether the unit is currently started or not)
SubState string // The sub state (a more fine-grained version of the active state that is specific to the unit type, which the active state is not)
Followed string // A unit that is being followed in its state by this unit, if there is any, otherwise the empty string.
Path dbus.ObjectPath // The unit object path
JobId uint32 // If there is a job queued for the job unit the numeric job id, 0 otherwise
JobType string // The job type as string
JobPath dbus.ObjectPath // The job object path
}
type LinkUnitFileChange EnableUnitFileChange
// LinkUnitFiles() links unit files (that are located outside of the
// usual unit search paths) into the unit search path.
//
// It takes a list of absolute paths to unit files to link and two
// booleans. The first boolean controls whether the unit shall be
// enabled for runtime only (true, /run), or persistently (false,
// /etc).
// The second controls whether symlinks pointing to other units shall
// be replaced if necessary.
//
// This call returns a list of the changes made. The list consists of
// structures with three strings: the type of the change (one of symlink
// or unlink), the file name of the symlink and the destination of the
// symlink.
func (c *Conn) LinkUnitFiles(files []string, runtime bool, force bool) ([]LinkUnitFileChange, error) {
result := make([][]interface{}, 0)
err := c.sysobj.Call("org.freedesktop.systemd1.Manager.LinkUnitFiles", 0, files, runtime, force).Store(&result)
if err != nil {
return nil, err
}
resultInterface := make([]interface{}, len(result))
for i := range result {
resultInterface[i] = result[i]
}
changes := make([]LinkUnitFileChange, len(result))
changesInterface := make([]interface{}, len(changes))
for i := range changes {
changesInterface[i] = &changes[i]
}
err = dbus.Store(resultInterface, changesInterface...)
if err != nil {
return nil, err
}
return changes, nil
}
// EnableUnitFiles() may be used to enable one or more units in the system (by
// creating symlinks to them in /etc or /run).
//
// It takes a list of unit files to enable (either just file names or full
// absolute paths if the unit files are residing outside the usual unit
// search paths), and two booleans: the first controls whether the unit shall
// be enabled for runtime only (true, /run), or persistently (false, /etc).
// The second one controls whether symlinks pointing to other units shall
// be replaced if necessary.
//
// This call returns one boolean and an array with the changes made. The
// boolean signals whether the unit files contained any enablement
// information (i.e. an [Install]) section. The changes list consists of
// structures with three strings: the type of the change (one of symlink
// or unlink), the file name of the symlink and the destination of the
// symlink.
func (c *Conn) EnableUnitFiles(files []string, runtime bool, force bool) (bool, []EnableUnitFileChange, error) {
var carries_install_info bool
result := make([][]interface{}, 0)
err := c.sysobj.Call("org.freedesktop.systemd1.Manager.EnableUnitFiles", 0, files, runtime, force).Store(&carries_install_info, &result)
if err != nil {
return false, nil, err
}
resultInterface := make([]interface{}, len(result))
for i := range result {
resultInterface[i] = result[i]
}
changes := make([]EnableUnitFileChange, len(result))
changesInterface := make([]interface{}, len(changes))
for i := range changes {
changesInterface[i] = &changes[i]
}
err = dbus.Store(resultInterface, changesInterface...)
if err != nil {
return false, nil, err
}
return carries_install_info, changes, nil
}
type EnableUnitFileChange struct {
Type string // Type of the change (one of symlink or unlink)
Filename string // File name of the symlink
Destination string // Destination of the symlink
}
// DisableUnitFiles() may be used to disable one or more units in the system (by
// removing symlinks to them from /etc or /run).
//
// It takes a list of unit files to disable (either just file names or full
// absolute paths if the unit files are residing outside the usual unit
// search paths), and one boolean: whether the unit was enabled for runtime
// only (true, /run), or persistently (false, /etc).
//
// This call returns an array with the changes made. The changes list
// consists of structures with three strings: the type of the change (one of
// symlink or unlink), the file name of the symlink and the destination of the
// symlink.
func (c *Conn) DisableUnitFiles(files []string, runtime bool) ([]DisableUnitFileChange, error) {
result := make([][]interface{}, 0)
err := c.sysobj.Call("org.freedesktop.systemd1.Manager.DisableUnitFiles", 0, files, runtime).Store(&result)
if err != nil {
return nil, err
}
resultInterface := make([]interface{}, len(result))
for i := range result {
resultInterface[i] = result[i]
}
changes := make([]DisableUnitFileChange, len(result))
changesInterface := make([]interface{}, len(changes))
for i := range changes {
changesInterface[i] = &changes[i]
}
err = dbus.Store(resultInterface, changesInterface...)
if err != nil {
return nil, err
}
return changes, nil
}
type DisableUnitFileChange struct {
Type string // Type of the change (one of symlink or unlink)
Filename string // File name of the symlink
Destination string // Destination of the symlink
}
// Reload instructs systemd to scan for and reload unit files. This is
// equivalent to a 'systemctl daemon-reload'.
func (c *Conn) Reload() error {
return c.sysobj.Call("org.freedesktop.systemd1.Manager.Reload", 0).Store()
}
func unitPath(name string) dbus.ObjectPath {
return dbus.ObjectPath("/org/freedesktop/systemd1/unit/" + PathBusEscape(name))
}

View File

@ -0,0 +1,345 @@
// Copyright 2015 CoreOS, Inc.
//
// 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"
"math/rand"
"os"
"path/filepath"
"reflect"
"testing"
"resin-supervisor/gosuper/Godeps/_workspace/src/github.com/godbus/dbus"
)
func setupConn(t *testing.T) *Conn {
conn, err := New()
if err != nil {
t.Fatal(err)
}
return conn
}
func findFixture(target string, t *testing.T) string {
abs, err := filepath.Abs("../fixtures/" + target)
if err != nil {
t.Fatal(err)
}
return abs
}
func setupUnit(target string, conn *Conn, t *testing.T) {
// Blindly stop the unit in case it is running
conn.StopUnit(target, "replace", nil)
// Blindly remove the symlink in case it exists
targetRun := filepath.Join("/run/systemd/system/", target)
os.Remove(targetRun)
}
func linkUnit(target string, conn *Conn, t *testing.T) {
abs := findFixture(target, t)
fixture := []string{abs}
changes, err := conn.LinkUnitFiles(fixture, true, true)
if err != nil {
t.Fatal(err)
}
if len(changes) < 1 {
t.Fatalf("Expected one change, got %v", changes)
}
runPath := filepath.Join("/run/systemd/system/", target)
if changes[0].Filename != runPath {
t.Fatal("Unexpected target filename")
}
}
// Ensure that basic unit starting and stopping works.
func TestStartStopUnit(t *testing.T) {
target := "start-stop.service"
conn := setupConn(t)
setupUnit(target, conn, t)
linkUnit(target, conn, t)
// 2. Start the unit
reschan := make(chan string)
_, err := conn.StartUnit(target, "replace", reschan)
if err != nil {
t.Fatal(err)
}
job := <-reschan
if job != "done" {
t.Fatal("Job is not done:", job)
}
units, err := conn.ListUnits()
var unit *UnitStatus
for _, u := range units {
if u.Name == target {
unit = &u
}
}
if unit == nil {
t.Fatalf("Test unit not found in list")
}
if unit.ActiveState != "active" {
t.Fatalf("Test unit not active")
}
// 3. Stop the unit
_, err = conn.StopUnit(target, "replace", reschan)
if err != nil {
t.Fatal(err)
}
// wait for StopUnit job to complete
<-reschan
units, err = conn.ListUnits()
unit = nil
for _, u := range units {
if u.Name == target {
unit = &u
}
}
if unit != nil {
t.Fatalf("Test unit found in list, should be stopped")
}
}
// Enables a unit and then immediately tears it down
func TestEnableDisableUnit(t *testing.T) {
target := "enable-disable.service"
conn := setupConn(t)
setupUnit(target, conn, t)
abs := findFixture(target, t)
runPath := filepath.Join("/run/systemd/system/", target)
// 1. Enable the unit
install, changes, err := conn.EnableUnitFiles([]string{abs}, true, true)
if err != nil {
t.Fatal(err)
}
if install != false {
t.Fatal("Install was true")
}
if len(changes) < 1 {
t.Fatalf("Expected one change, got %v", changes)
}
if changes[0].Filename != runPath {
t.Fatal("Unexpected target filename")
}
// 2. Disable the unit
dChanges, err := conn.DisableUnitFiles([]string{abs}, true)
if err != nil {
t.Fatal(err)
}
if len(dChanges) != 1 {
t.Fatalf("Changes should include the path, %v", dChanges)
}
if dChanges[0].Filename != runPath {
t.Fatalf("Change should include correct filename, %+v", dChanges[0])
}
if dChanges[0].Destination != "" {
t.Fatalf("Change destination should be empty, %+v", dChanges[0])
}
}
// TestGetUnitProperties reads the `-.mount` which should exist on all systemd
// systems and ensures that one of its properties is valid.
func TestGetUnitProperties(t *testing.T) {
conn := setupConn(t)
unit := "-.mount"
info, err := conn.GetUnitProperties(unit)
if err != nil {
t.Fatal(err)
}
names := info["Wants"].([]string)
if len(names) < 1 {
t.Fatal("/ is unwanted")
}
if names[0] != "system.slice" {
t.Fatal("unexpected wants for /")
}
prop, err := conn.GetUnitProperty(unit, "Wants")
if err != nil {
t.Fatal(err)
}
if prop.Name != "Wants" {
t.Fatal("unexpected property name")
}
val := prop.Value.Value().([]string)
if !reflect.DeepEqual(val, names) {
t.Fatal("unexpected property value")
}
}
// TestGetUnitPropertiesRejectsInvalidName attempts to get the properties for a
// unit with an invalid name. This test should be run with --test.timeout set,
// as a fail will manifest as GetUnitProperties hanging indefinitely.
func TestGetUnitPropertiesRejectsInvalidName(t *testing.T) {
conn := setupConn(t)
unit := "//invalid#$^/"
_, err := conn.GetUnitProperties(unit)
if err == nil {
t.Fatal("Expected an error, got nil")
}
_, err = conn.GetUnitProperty(unit, "Wants")
if err == nil {
t.Fatal("Expected an error, got nil")
}
}
// TestSetUnitProperties changes a cgroup setting on the `tmp.mount`
// which should exist on all systemd systems and ensures that the
// property was set.
func TestSetUnitProperties(t *testing.T) {
conn := setupConn(t)
unit := "tmp.mount"
if err := conn.SetUnitProperties(unit, true, Property{"CPUShares", dbus.MakeVariant(uint64(1023))}); err != nil {
t.Fatal(err)
}
info, err := conn.GetUnitTypeProperties(unit, "Mount")
if err != nil {
t.Fatal(err)
}
value := info["CPUShares"].(uint64)
if value != 1023 {
t.Fatal("CPUShares of unit is not 1023:", value)
}
}
// Ensure that basic transient unit starting and stopping works.
func TestStartStopTransientUnit(t *testing.T) {
conn := setupConn(t)
props := []Property{
PropExecStart([]string{"/bin/sleep", "400"}, false),
}
target := fmt.Sprintf("testing-transient-%d.service", rand.Int())
// Start the unit
reschan := make(chan string)
_, err := conn.StartTransientUnit(target, "replace", props, reschan)
if err != nil {
t.Fatal(err)
}
job := <-reschan
if job != "done" {
t.Fatal("Job is not done:", job)
}
units, err := conn.ListUnits()
var unit *UnitStatus
for _, u := range units {
if u.Name == target {
unit = &u
}
}
if unit == nil {
t.Fatalf("Test unit not found in list")
}
if unit.ActiveState != "active" {
t.Fatalf("Test unit not active")
}
// 3. Stop the unit
_, err = conn.StopUnit(target, "replace", reschan)
if err != nil {
t.Fatal(err)
}
// wait for StopUnit job to complete
<-reschan
units, err = conn.ListUnits()
unit = nil
for _, u := range units {
if u.Name == target {
unit = &u
}
}
if unit != nil {
t.Fatalf("Test unit found in list, should be stopped")
}
}
func TestConnJobListener(t *testing.T) {
target := "start-stop.service"
conn := setupConn(t)
setupUnit(target, conn, t)
linkUnit(target, conn, t)
jobSize := len(conn.jobListener.jobs)
reschan := make(chan string)
_, err := conn.StartUnit(target, "replace", reschan)
if err != nil {
t.Fatal(err)
}
<-reschan
_, err = conn.StopUnit(target, "replace", reschan)
if err != nil {
t.Fatal(err)
}
<-reschan
currentJobSize := len(conn.jobListener.jobs)
if jobSize != currentJobSize {
t.Fatal("JobListener jobs leaked")
}
}

View File

@ -0,0 +1,218 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 (
"resin-supervisor/gosuper/Godeps/_workspace/src/github.com/godbus/dbus"
)
// From the systemd docs:
//
// The properties array of StartTransientUnit() may take many of the settings
// that may also be configured in unit files. Not all parameters are currently
// accepted though, but we plan to cover more properties with future release.
// Currently you may set the Description, Slice and all dependency types of
// units, as well as RemainAfterExit, ExecStart for service units,
// TimeoutStopUSec and PIDs for scope units, and CPUAccounting, CPUShares,
// BlockIOAccounting, BlockIOWeight, BlockIOReadBandwidth,
// BlockIOWriteBandwidth, BlockIODeviceWeight, MemoryAccounting, MemoryLimit,
// DevicePolicy, DeviceAllow for services/scopes/slices. These fields map
// directly to their counterparts in unit files and as normal D-Bus object
// properties. The exception here is the PIDs field of scope units which is
// used for construction of the scope only and specifies the initial PIDs to
// add to the scope object.
type Property struct {
Name string
Value dbus.Variant
}
type PropertyCollection struct {
Name string
Properties []Property
}
type execStart struct {
Path string // the binary path to execute
Args []string // an array with all arguments to pass to the executed command, starting with argument 0
UncleanIsFailure bool // a boolean whether it should be considered a failure if the process exits uncleanly
}
// PropExecStart sets the ExecStart service property. The first argument is a
// slice with the binary path to execute followed by the arguments to pass to
// the executed command. See
// http://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart=
func PropExecStart(command []string, uncleanIsFailure bool) Property {
execStarts := []execStart{
execStart{
Path: command[0],
Args: command,
UncleanIsFailure: uncleanIsFailure,
},
}
return Property{
Name: "ExecStart",
Value: dbus.MakeVariant(execStarts),
}
}
// PropRemainAfterExit sets the RemainAfterExit service property. See
// http://www.freedesktop.org/software/systemd/man/systemd.service.html#RemainAfterExit=
func PropRemainAfterExit(b bool) Property {
return Property{
Name: "RemainAfterExit",
Value: dbus.MakeVariant(b),
}
}
// PropDescription sets the Description unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit#Description=
func PropDescription(desc string) Property {
return Property{
Name: "Description",
Value: dbus.MakeVariant(desc),
}
}
func propDependency(name string, units []string) Property {
return Property{
Name: name,
Value: dbus.MakeVariant(units),
}
}
// PropRequires sets the Requires unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Requires=
func PropRequires(units ...string) Property {
return propDependency("Requires", units)
}
// PropRequiresOverridable sets the RequiresOverridable unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequiresOverridable=
func PropRequiresOverridable(units ...string) Property {
return propDependency("RequiresOverridable", units)
}
// PropRequisite sets the Requisite unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Requisite=
func PropRequisite(units ...string) Property {
return propDependency("Requisite", units)
}
// PropRequisiteOverridable sets the RequisiteOverridable unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequisiteOverridable=
func PropRequisiteOverridable(units ...string) Property {
return propDependency("RequisiteOverridable", units)
}
// PropWants sets the Wants unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Wants=
func PropWants(units ...string) Property {
return propDependency("Wants", units)
}
// PropBindsTo sets the BindsTo unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#BindsTo=
func PropBindsTo(units ...string) Property {
return propDependency("BindsTo", units)
}
// PropRequiredBy sets the RequiredBy unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequiredBy=
func PropRequiredBy(units ...string) Property {
return propDependency("RequiredBy", units)
}
// PropRequiredByOverridable sets the RequiredByOverridable unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequiredByOverridable=
func PropRequiredByOverridable(units ...string) Property {
return propDependency("RequiredByOverridable", units)
}
// PropWantedBy sets the WantedBy unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#WantedBy=
func PropWantedBy(units ...string) Property {
return propDependency("WantedBy", units)
}
// PropBoundBy sets the BoundBy unit property. See
// http://www.freedesktop.org/software/systemd/main/systemd.unit.html#BoundBy=
func PropBoundBy(units ...string) Property {
return propDependency("BoundBy", units)
}
// PropConflicts sets the Conflicts unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Conflicts=
func PropConflicts(units ...string) Property {
return propDependency("Conflicts", units)
}
// PropConflictedBy sets the ConflictedBy unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#ConflictedBy=
func PropConflictedBy(units ...string) Property {
return propDependency("ConflictedBy", units)
}
// PropBefore sets the Before unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Before=
func PropBefore(units ...string) Property {
return propDependency("Before", units)
}
// PropAfter sets the After unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#After=
func PropAfter(units ...string) Property {
return propDependency("After", units)
}
// PropOnFailure sets the OnFailure unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#OnFailure=
func PropOnFailure(units ...string) Property {
return propDependency("OnFailure", units)
}
// PropTriggers sets the Triggers unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Triggers=
func PropTriggers(units ...string) Property {
return propDependency("Triggers", units)
}
// PropTriggeredBy sets the TriggeredBy unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#TriggeredBy=
func PropTriggeredBy(units ...string) Property {
return propDependency("TriggeredBy", units)
}
// PropPropagatesReloadTo sets the PropagatesReloadTo unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#PropagatesReloadTo=
func PropPropagatesReloadTo(units ...string) Property {
return propDependency("PropagatesReloadTo", units)
}
// PropRequiresMountsFor sets the RequiresMountsFor unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequiresMountsFor=
func PropRequiresMountsFor(units ...string) Property {
return propDependency("RequiresMountsFor", units)
}
// PropSlice sets the Slice unit property. See
// http://www.freedesktop.org/software/systemd/man/systemd.resource-control.html#Slice=
func PropSlice(slice string) Property {
return Property{
Name: "Slice",
Value: dbus.MakeVariant(slice),
}
}

View File

@ -0,0 +1,47 @@
// Copyright 2015 CoreOS, Inc.
//
// 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
type set struct {
data map[string]bool
}
func (s *set) Add(value string) {
s.data[value] = true
}
func (s *set) Remove(value string) {
delete(s.data, value)
}
func (s *set) Contains(value string) (exists bool) {
_, exists = s.data[value]
return
}
func (s *set) Length() int {
return len(s.data)
}
func (s *set) Values() (values []string) {
for val, _ := range s.data {
values = append(values, val)
}
return
}
func newSet() *set {
return &set{make(map[string]bool)}
}

View File

@ -0,0 +1,53 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 (
"testing"
)
// TestBasicSetActions asserts that Add & Remove behavior is correct
func TestBasicSetActions(t *testing.T) {
s := newSet()
if s.Contains("foo") {
t.Fatal("set should not contain 'foo'")
}
s.Add("foo")
if !s.Contains("foo") {
t.Fatal("set should contain 'foo'")
}
v := s.Values()
if len(v) != 1 {
t.Fatal("set.Values did not report correct number of values")
}
if v[0] != "foo" {
t.Fatal("set.Values did not report value")
}
s.Remove("foo")
if s.Contains("foo") {
t.Fatal("set should not contain 'foo'")
}
v = s.Values()
if len(v) != 0 {
t.Fatal("set.Values did not report correct number of values")
}
}

View File

@ -0,0 +1,250 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 (
"errors"
"time"
"resin-supervisor/gosuper/Godeps/_workspace/src/github.com/godbus/dbus"
)
const (
cleanIgnoreInterval = int64(10 * time.Second)
ignoreInterval = int64(30 * time.Millisecond)
)
// Subscribe sets up this connection to subscribe to all systemd dbus events.
// This is required before calling SubscribeUnits. When the connection closes
// systemd will automatically stop sending signals so there is no need to
// explicitly call Unsubscribe().
func (c *Conn) Subscribe() error {
c.sigconn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0,
"type='signal',interface='org.freedesktop.systemd1.Manager',member='UnitNew'")
c.sigconn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0,
"type='signal',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'")
err := c.sigobj.Call("org.freedesktop.systemd1.Manager.Subscribe", 0).Store()
if err != nil {
return err
}
return nil
}
// Unsubscribe this connection from systemd dbus events.
func (c *Conn) Unsubscribe() error {
err := c.sigobj.Call("org.freedesktop.systemd1.Manager.Unsubscribe", 0).Store()
if err != nil {
return err
}
return nil
}
func (c *Conn) dispatch() {
ch := make(chan *dbus.Signal, signalBuffer)
c.sigconn.Signal(ch)
go func() {
for {
signal, ok := <-ch
if !ok {
return
}
if signal.Name == "org.freedesktop.systemd1.Manager.JobRemoved" {
c.jobComplete(signal)
}
if c.subscriber.updateCh == nil {
continue
}
var unitPath dbus.ObjectPath
switch signal.Name {
case "org.freedesktop.systemd1.Manager.JobRemoved":
unitName := signal.Body[2].(string)
c.sysobj.Call("org.freedesktop.systemd1.Manager.GetUnit", 0, unitName).Store(&unitPath)
case "org.freedesktop.systemd1.Manager.UnitNew":
unitPath = signal.Body[1].(dbus.ObjectPath)
case "org.freedesktop.DBus.Properties.PropertiesChanged":
if signal.Body[0].(string) == "org.freedesktop.systemd1.Unit" {
unitPath = signal.Path
}
}
if unitPath == dbus.ObjectPath("") {
continue
}
c.sendSubStateUpdate(unitPath)
}
}()
}
// Returns two unbuffered channels which will receive all changed units every
// interval. Deleted units are sent as nil.
func (c *Conn) SubscribeUnits(interval time.Duration) (<-chan map[string]*UnitStatus, <-chan error) {
return c.SubscribeUnitsCustom(interval, 0, func(u1, u2 *UnitStatus) bool { return *u1 != *u2 }, nil)
}
// SubscribeUnitsCustom is like SubscribeUnits but lets you specify the buffer
// size of the channels, the comparison function for detecting changes and a filter
// function for cutting down on the noise that your channel receives.
func (c *Conn) SubscribeUnitsCustom(interval time.Duration, buffer int, isChanged func(*UnitStatus, *UnitStatus) bool, filterUnit func(string) bool) (<-chan map[string]*UnitStatus, <-chan error) {
old := make(map[string]*UnitStatus)
statusChan := make(chan map[string]*UnitStatus, buffer)
errChan := make(chan error, buffer)
go func() {
for {
timerChan := time.After(interval)
units, err := c.ListUnits()
if err == nil {
cur := make(map[string]*UnitStatus)
for i := range units {
if filterUnit != nil && filterUnit(units[i].Name) {
continue
}
cur[units[i].Name] = &units[i]
}
// add all new or changed units
changed := make(map[string]*UnitStatus)
for n, u := range cur {
if oldU, ok := old[n]; !ok || isChanged(oldU, u) {
changed[n] = u
}
delete(old, n)
}
// add all deleted units
for oldN := range old {
changed[oldN] = nil
}
old = cur
if len(changed) != 0 {
statusChan <- changed
}
} else {
errChan <- err
}
<-timerChan
}
}()
return statusChan, errChan
}
type SubStateUpdate struct {
UnitName string
SubState string
}
// SetSubStateSubscriber writes to updateCh when any unit's substate changes.
// Although this writes to updateCh on every state change, the reported state
// may be more recent than the change that generated it (due to an unavoidable
// race in the systemd dbus interface). That is, this method provides a good
// way to keep a current view of all units' states, but is not guaranteed to
// show every state transition they go through. Furthermore, state changes
// will only be written to the channel with non-blocking writes. If updateCh
// is full, it attempts to write an error to errCh; if errCh is full, the error
// passes silently.
func (c *Conn) SetSubStateSubscriber(updateCh chan<- *SubStateUpdate, errCh chan<- error) {
c.subscriber.Lock()
defer c.subscriber.Unlock()
c.subscriber.updateCh = updateCh
c.subscriber.errCh = errCh
}
func (c *Conn) sendSubStateUpdate(path dbus.ObjectPath) {
c.subscriber.Lock()
defer c.subscriber.Unlock()
if c.shouldIgnore(path) {
return
}
info, err := c.GetUnitProperties(string(path))
if err != nil {
select {
case c.subscriber.errCh <- err:
default:
}
}
name := info["Id"].(string)
substate := info["SubState"].(string)
update := &SubStateUpdate{name, substate}
select {
case c.subscriber.updateCh <- update:
default:
select {
case c.subscriber.errCh <- errors.New("update channel full!"):
default:
}
}
c.updateIgnore(path, info)
}
// The ignore functions work around a wart in the systemd dbus interface.
// Requesting the properties of an unloaded unit will cause systemd to send a
// pair of UnitNew/UnitRemoved signals. Because we need to get a unit's
// properties on UnitNew (as that's the only indication of a new unit coming up
// for the first time), we would enter an infinite loop if we did not attempt
// to detect and ignore these spurious signals. The signal themselves are
// indistinguishable from relevant ones, so we (somewhat hackishly) ignore an
// unloaded unit's signals for a short time after requesting its properties.
// This means that we will miss e.g. a transient unit being restarted
// *immediately* upon failure and also a transient unit being started
// immediately after requesting its status (with systemctl status, for example,
// because this causes a UnitNew signal to be sent which then causes us to fetch
// the properties).
func (c *Conn) shouldIgnore(path dbus.ObjectPath) bool {
t, ok := c.subscriber.ignore[path]
return ok && t >= time.Now().UnixNano()
}
func (c *Conn) updateIgnore(path dbus.ObjectPath, info map[string]interface{}) {
c.cleanIgnore()
// unit is unloaded - it will trigger bad systemd dbus behavior
if info["LoadState"].(string) == "not-found" {
c.subscriber.ignore[path] = time.Now().UnixNano() + ignoreInterval
}
}
// without this, ignore would grow unboundedly over time
func (c *Conn) cleanIgnore() {
now := time.Now().UnixNano()
if c.subscriber.cleanIgnore < now {
c.subscriber.cleanIgnore = now + cleanIgnoreInterval
for p, t := range c.subscriber.ignore {
if t < now {
delete(c.subscriber.ignore, p)
}
}
}
}

View File

@ -0,0 +1,57 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 (
"time"
)
// SubscriptionSet returns a subscription set which is like conn.Subscribe but
// can filter to only return events for a set of units.
type SubscriptionSet struct {
*set
conn *Conn
}
func (s *SubscriptionSet) filter(unit string) bool {
return !s.Contains(unit)
}
// Subscribe starts listening for dbus events for all of the units in the set.
// Returns channels identical to conn.SubscribeUnits.
func (s *SubscriptionSet) Subscribe() (<-chan map[string]*UnitStatus, <-chan error) {
// TODO: Make fully evented by using systemd 209 with properties changed values
return s.conn.SubscribeUnitsCustom(time.Second, 0,
mismatchUnitStatus,
func(unit string) bool { return s.filter(unit) },
)
}
// NewSubscriptionSet returns a new subscription set.
func (conn *Conn) NewSubscriptionSet() *SubscriptionSet {
return &SubscriptionSet{newSet(), conn}
}
// mismatchUnitStatus returns true if the provided UnitStatus objects
// are not equivalent. false is returned if the objects are equivalent.
// Only the Name, Description and state-related fields are used in
// the comparison.
func mismatchUnitStatus(u1, u2 *UnitStatus) bool {
return u1.Name != u2.Name ||
u1.Description != u2.Description ||
u1.LoadState != u2.LoadState ||
u1.ActiveState != u2.ActiveState ||
u1.SubState != u2.SubState
}

View File

@ -0,0 +1,82 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 (
"testing"
"time"
)
// TestSubscribeUnit exercises the basics of subscription of a particular unit.
func TestSubscriptionSetUnit(t *testing.T) {
target := "subscribe-events-set.service"
conn, err := New()
if err != nil {
t.Fatal(err)
}
err = conn.Subscribe()
if err != nil {
t.Fatal(err)
}
subSet := conn.NewSubscriptionSet()
evChan, errChan := subSet.Subscribe()
subSet.Add(target)
setupUnit(target, conn, t)
linkUnit(target, conn, t)
reschan := make(chan string)
_, err = conn.StartUnit(target, "replace", reschan)
if err != nil {
t.Fatal(err)
}
job := <-reschan
if job != "done" {
t.Fatal("Couldn't start", target)
}
timeout := make(chan bool, 1)
go func() {
time.Sleep(3 * time.Second)
close(timeout)
}()
for {
select {
case changes := <-evChan:
tCh, ok := changes[target]
if !ok {
t.Fatal("Unexpected event:", changes)
}
if tCh.ActiveState == "active" && tCh.Name == target {
goto success
}
case err = <-errChan:
t.Fatal(err)
case <-timeout:
t.Fatal("Reached timeout")
}
}
success:
return
}

View File

@ -0,0 +1,105 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 (
"testing"
"time"
)
// TestSubscribe exercises the basics of subscription
func TestSubscribe(t *testing.T) {
conn, err := New()
if err != nil {
t.Fatal(err)
}
err = conn.Subscribe()
if err != nil {
t.Fatal(err)
}
err = conn.Unsubscribe()
if err != nil {
t.Fatal(err)
}
}
// TestSubscribeUnit exercises the basics of subscription of a particular unit.
func TestSubscribeUnit(t *testing.T) {
target := "subscribe-events.service"
conn, err := New()
if err != nil {
t.Fatal(err)
}
err = conn.Subscribe()
if err != nil {
t.Fatal(err)
}
err = conn.Unsubscribe()
if err != nil {
t.Fatal(err)
}
evChan, errChan := conn.SubscribeUnits(time.Second)
setupUnit(target, conn, t)
linkUnit(target, conn, t)
reschan := make(chan string)
_, err = conn.StartUnit(target, "replace", reschan)
if err != nil {
t.Fatal(err)
}
job := <-reschan
if job != "done" {
t.Fatal("Couldn't start", target)
}
timeout := make(chan bool, 1)
go func() {
time.Sleep(3 * time.Second)
close(timeout)
}()
for {
select {
case changes := <-evChan:
tCh, ok := changes[target]
// Just continue until we see our event.
if !ok {
continue
}
if tCh.ActiveState == "active" && tCh.Name == target {
goto success
}
case err = <-errChan:
t.Fatal(err)
case <-timeout:
t.Fatal("Reached timeout")
}
}
success:
return
}

View File

@ -18,7 +18,7 @@ import (
// Use raw strings to avoid having to quote the backslashes.
var dockerMatch = regexp.MustCompile(`(docker[0-9]+)|(rce[0-9]+)`)
// API response sent from gosupervisor
// APIResponse sent from gosupervisor
type APIResponse struct {
Data interface{}
Error string
@ -28,6 +28,11 @@ type PurgeBody struct {
ApplicationId interface{}
}
// VPNBody interface for post request received by VPN control end point
type VPNBody struct {
Enable bool
}
func jsonResponse(writer http.ResponseWriter, response interface{}, status int) {
jsonBody, err := json.Marshal(response)
if err != nil {
@ -173,3 +178,32 @@ func IPAddressHandler(writer http.ResponseWriter, request *http.Request) {
sendResponse(payload, "", http.StatusOK)
}
}
//VPNControl is used to control VPN service status with dbus
func VPNControl(writer http.ResponseWriter, request *http.Request) {
sendResponse := responseSender(writer)
sendError := func(err string) {
sendResponse("Error", err, http.StatusInternalServerError)
}
var body VPNBody
if err := parseJsonBody(&body, request); err != nil {
sendError(string(err.Error()))
return
}
if body.Enable {
ch := make(chan string)
_, err := systemd.Dbus.StartUnit("openvpn-resin.service", "fail", ch)
if err != nil {
sendError(string(err.Error()))
return
}
} else {
ch := make(chan string)
_, err := systemd.Dbus.StopUnit("openvpn-resin.service", "fail", ch)
if err != nil {
sendError(string(err.Error()))
return
}
}
}

View File

@ -22,6 +22,7 @@ func setupApi(router *mux.Router) {
apiv1.HandleFunc("/purge", PurgeHandler).Methods("POST")
apiv1.HandleFunc("/reboot", RebootHandler).Methods("POST")
apiv1.HandleFunc("/shutdown", ShutdownHandler).Methods("POST")
apiv1.HandleFunc("/vpncontrol", VPNControl).Methods("POST")
}
func startApi(listenAddress string, router *mux.Router) {

View File

@ -3,14 +3,22 @@ package systemd
import (
"log"
"resin-supervisor/gosuper/Godeps/_workspace/src/github.com/coreos/go-systemd/dbus"
"resin-supervisor/gosuper/Godeps/_workspace/src/github.com/coreos/go-systemd/login1"
)
// Logind Systemd Login1 connection
var Logind *login1.Conn
// Dbus Systems Dbus connection
var Dbus *dbus.Conn
func init() {
var err error
if Logind, err = login1.New(); err != nil {
log.Fatal("Failed to connect to host system bus")
}
if Dbus, err = dbus.New(); err != nil {
log.Fatal("Failed to connect to host system bus")
}
}