diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 870ee679..23a4503d 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -218,6 +218,7 @@ parameter with the [Noop](#Noop) resource. * [Exec](#Exec): Execute shell commands on the system. * [File](#File): Manage files and directories. +* [Hostname](#Hostname): Manages the hostname on the system. * [Msg](#Msg): Send log messages. * [Noop](#Noop): A simple resource that does nothing. * [Pkg](#Pkg): Manage system packages with PackageKit. @@ -263,6 +264,30 @@ The force property is required if we want the file resource to be able to change a file into a directory or vice-versa. If such a change is needed, but the force property is not set to `true`, then this file resource will error. +###Hostname + +The hostname resource manages static, transient/dynamic and pretty hostnames +on the system and watches them for changes. + +#### static_hostname +The static hostname is the one configured in /etc/hostname or a similar +file. +It is chosen by the local user. It is not always in sync with the current +host name as returned by the gethostname() system call. + +#### transient_hostname +The transient / dynamic hostname is the one configured via the kernel's +sethostbyname(). +It can be different from the static hostname in case DHCP or mDNS have been +configured to change the name based on network information. + +#### pretty_hostname +The pretty hostname is a free-form UTF8 host name for presentation to the user. + +#### hostname +Hostname is the fallback value for all 3 fields above, if only `hostname` is +specified, it will set all 3 fields to this value. + ###Msg The msg resource sends messages to the main log, or an external service such diff --git a/examples/hostname.yml b/examples/hostname.yml new file mode 100644 index 00000000..c27e9304 --- /dev/null +++ b/examples/hostname.yml @@ -0,0 +1,7 @@ +--- +graph: mygraph +resources: + hostname: + - name: Hostname Watcher @ TestHost + hostname: test.hostname.example.com +edges: [] diff --git a/resources/hostname.go b/resources/hostname.go new file mode 100644 index 00000000..60cc7714 --- /dev/null +++ b/resources/hostname.go @@ -0,0 +1,326 @@ +// Mgmt +// Copyright (C) 2013-2016+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package resources + +import ( + "encoding/gob" + "errors" + "fmt" + "log" + "time" + + "github.com/purpleidea/mgmt/event" + "github.com/purpleidea/mgmt/util" + + "github.com/godbus/dbus" + errwrap "github.com/pkg/errors" +) + +// ErrResourceInsufficientParameters is returned when the configuration of the resource +// is insufficient for the resource to do any useful work. +var ErrResourceInsufficientParameters = errors.New( + "Insufficient parameters for this resource") + +func init() { + gob.Register(&HostnameRes{}) +} + +const ( + hostname1Path = "/org/freedesktop/hostname1" + hostname1Iface = "org.freedesktop.hostname1" + dbusAddMatch = "org.freedesktop.DBus.AddMatch" +) + +// HostnameRes is a resource that allows setting and watching the hostname. +// +// StaticHostname is the one configured in /etc/hostname or a similar file. +// It is chosen by the local user. It is not always in sync with the current +// host name as returned by the gethostname() system call. +// +// TransientHostname is the one configured via the kernel's sethostbyname(). +// It can be different from the static hostname in case DHCP or mDNS have been +// configured to change the name based on network information. +// +// PrettyHostname is a free-form UTF8 host name for presentation to the user. +// +// Hostname is the fallback value for all 3 fields above, if only Hostname is +// specified, it will set all 3 fields to this value. +type HostnameRes struct { + BaseRes `yaml:",inline"` + Hostname string `yaml:"hostname"` + PrettyHostname string `yaml:"pretty_hostname"` + StaticHostname string `yaml:"static_hostname"` + TransientHostname string `yaml:"transient_hostname"` + + conn *dbus.Conn +} + +// NewHostnameRes is a constructor for this resource. It also calls Init() for you. +func NewHostnameRes(name, staticHostname, transientHostname, prettyHostname string) (*HostnameRes, error) { + obj := &HostnameRes{ + BaseRes: BaseRes{ + Name: name, + }, + PrettyHostname: prettyHostname, + StaticHostname: staticHostname, + TransientHostname: transientHostname, + } + return obj, obj.Init() +} + +// Init runs some startup code for this resource. +func (obj *HostnameRes) Init() error { + obj.BaseRes.kind = "Hostname" + if obj.PrettyHostname == "" { + obj.PrettyHostname = obj.Hostname + } + if obj.StaticHostname == "" { + obj.StaticHostname = obj.Hostname + } + if obj.TransientHostname == "" { + obj.TransientHostname = obj.Hostname + } + return obj.BaseRes.Init() // call base init, b/c we're overriding +} + +// Validate if the params passed in are valid data. +// FIXME: where should this get called ? +func (obj *HostnameRes) Validate() error { + if obj.PrettyHostname == "" && obj.StaticHostname == "" && obj.TransientHostname == "" { + return ErrResourceInsufficientParameters + } + return nil +} + +// Watch is the primary listener for this resource and it outputs events. +func (obj *HostnameRes) Watch(processChan chan event.Event) error { + if obj.IsWatching() { + return nil // TODO: should this be an error? + } + obj.SetWatching(true) + defer obj.SetWatching(false) + cuid := obj.converger.Register() + defer cuid.Unregister() + + var startup bool + Startup := func(block bool) <-chan time.Time { + if block { + return nil // blocks forever + //return make(chan time.Time) // blocks forever + } + return time.After(time.Duration(500) * time.Millisecond) // 1/2 the resolution of converged timeout + } + + // if we share the bus with others, we will get each others messages!! + bus, err := util.SystemBusPrivateUsable() // don't share the bus connection! + if err != nil { + return errwrap.Wrap(err, "Failed to connect to bus") + } + defer bus.Close() + callResult := bus.BusObject().Call( + "org.freedesktop.DBus.AddMatch", 0, + fmt.Sprintf("type='signal',path='%s',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'", hostname1Path)) + if callResult.Err != nil { + return errwrap.Wrap(callResult.Err, "Failed to subscribe to DBus events for hostname1") + } + + signals := make(chan *dbus.Signal, 10) // closed by dbus package + bus.Signal(signals) + + var send = false // send event? + var dirty = false + + for { + obj.SetState(ResStateWatching) // reset + select { + case <-signals: + cuid.SetConverged(false) + send = true + dirty = true + + case event := <-obj.Events(): + cuid.SetConverged(false) + // we avoid sending events on unpause + if exit, _ := obj.ReadEvent(&event); exit { + return nil // exit + } + send = true + dirty = true + + case <-cuid.ConvergedTimer(): + cuid.SetConverged(true) // converged! + continue + + case <-Startup(startup): + cuid.SetConverged(false) + send = true + } + + // do all our event sending all together to avoid duplicate msgs + if send { + startup = true // startup finished + send = false + if dirty { + dirty = false + obj.isStateOK = false // something made state dirty + } + // only do this on certain types of events + if exit, err := obj.DoSend(processChan, ""); exit || err != nil { + return err // we exit or bubble up a NACK... + } + } + } +} + +func updateHostnameProperty(object dbus.BusObject, expectedValue, property, setterName string, apply bool) (checkOK bool, err error) { + propertyObject, err := object.GetProperty("org.freedesktop.hostname1." + property) + if err != nil { + return false, errwrap.Wrapf(err, "failed to get org.freedesktop.hostname1.%s", property) + } + if propertyObject.Value() == nil { + return false, errwrap.Errorf("Unexpected nil value received when reading property %s", property) + } + + propertyValue, ok := propertyObject.Value().(string) + if !ok { + return false, fmt.Errorf("Received unexpected type as %s value, expected string got '%T'", property, propertyValue) + } + + // expected value and actual value match => checkOk + if propertyValue == expectedValue { + return true, nil + } + + // nothing to do anymore + if !apply { + return false, nil + } + + // attempting to apply the changes + log.Printf("Changing %s: %s => %s", property, propertyValue, expectedValue) + if err := object.Call("org.freedesktop.hostname1."+setterName, 0, expectedValue, false).Err; err != nil { + return false, errwrap.Wrapf(err, "failed to call org.freedesktop.hostname1.%s", setterName) + } + + // all good changes should now be applied again + return false, nil +} + +// CheckApply method for Hostname resource. +func (obj *HostnameRes) CheckApply(apply bool) (checkOK bool, err error) { + log.Printf("%v[%v]: CheckApply(%t)", obj.Kind(), obj.GetName(), apply) + + if obj.isStateOK { // cached state + return true, nil + } + + conn, err := util.SystemBusPrivateUsable() + if err != nil { + return false, errwrap.Wrap(err, "Failed to connect to the private system bus") + } + defer conn.Close() + + hostnameObject := conn.Object(hostname1Iface, hostname1Path) + + checkOK = true + if obj.PrettyHostname != "" { + propertyCheckOK, err := updateHostnameProperty(hostnameObject, obj.PrettyHostname, "PrettyHostname", "SetPrettyHostname", apply) + if err != nil { + return false, err + } + checkOK = checkOK && propertyCheckOK + } + if obj.StaticHostname != "" { + propertyCheckOK, err := updateHostnameProperty(hostnameObject, obj.StaticHostname, "StaticHostname", "SetStaticHostname", apply) + if err != nil { + return false, err + } + checkOK = checkOK && propertyCheckOK + } + if obj.TransientHostname != "" { + propertyCheckOK, err := updateHostnameProperty(hostnameObject, obj.TransientHostname, "Hostname", "SetHostname", apply) + if err != nil { + return false, err + } + checkOK = checkOK && propertyCheckOK + } + + if apply || checkOK { + obj.isStateOK = true + } + + return checkOK, nil +} + +// HostnameUID is the UID struct for HostnameRes. +type HostnameUID struct { + BaseUID + name string + prettyHostname string + staticHostname string + transientHostname string +} + +// AutoEdges returns the AutoEdge interface. In this case no autoedges are used. +func (obj *HostnameRes) AutoEdges() AutoEdge { + return nil +} + +// GetUIDs includes all params to make a unique identification of this object. +// Most resources only return one, although some resources can return multiple. +func (obj *HostnameRes) GetUIDs() []ResUID { + x := &HostnameUID{ + BaseUID: BaseUID{name: obj.GetName(), kind: obj.Kind()}, + name: obj.Name, + prettyHostname: obj.PrettyHostname, + staticHostname: obj.StaticHostname, + transientHostname: obj.TransientHostname, + } + return []ResUID{x} +} + +// GroupCmp returns whether two resources can be grouped together or not. +func (obj *HostnameRes) GroupCmp(r Res) bool { + return false +} + +// Compare two resources and return if they are equivalent. +func (obj *HostnameRes) Compare(res Res) bool { + switch res := res.(type) { + // we can only compare HostnameRes to others of the same resource + case *HostnameRes: + if !obj.BaseRes.Compare(res) { // call base Compare + return false + } + if obj.Name != res.Name { + return false + } + if obj.PrettyHostname != res.PrettyHostname { + return false + } + if obj.StaticHostname != res.StaticHostname { + return false + } + if obj.TransientHostname != res.TransientHostname { + return false + } + default: + return false + } + return true +} diff --git a/yamlgraph/gconfig.go b/yamlgraph/gconfig.go index 93fcd7a6..c1014e07 100644 --- a/yamlgraph/gconfig.go +++ b/yamlgraph/gconfig.go @@ -56,14 +56,15 @@ type Edge struct { // Resources is the data structure of the set of resources. type Resources struct { // in alphabetical order - Exec []*resources.ExecRes `yaml:"exec"` - File []*resources.FileRes `yaml:"file"` - Msg []*resources.MsgRes `yaml:"msg"` - Noop []*resources.NoopRes `yaml:"noop"` - Pkg []*resources.PkgRes `yaml:"pkg"` - Svc []*resources.SvcRes `yaml:"svc"` - Timer []*resources.TimerRes `yaml:"timer"` - Virt []*resources.VirtRes `yaml:"virt"` + Exec []*resources.ExecRes `yaml:"exec"` + File []*resources.FileRes `yaml:"file"` + Hostname []*resources.HostnameRes `yaml:"hostname"` + Msg []*resources.MsgRes `yaml:"msg"` + Noop []*resources.NoopRes `yaml:"noop"` + Pkg []*resources.PkgRes `yaml:"pkg"` + Svc []*resources.SvcRes `yaml:"svc"` + Timer []*resources.TimerRes `yaml:"timer"` + Virt []*resources.VirtRes `yaml:"virt"` } // GraphConfig is the data structure that describes a single graph to run.