Repository: vmware/govmomi
Pull request #3516 (2024-08-08 06:11:42)

Add vCenter Alarm support
vcsim: add AlarmManager
govc: add alarm commands
api: add alarm package with helpers for AlarmManager and Alarms
govc: add event.post command
fix: govc: output Message field for 'EventEx' types



Patch:
diff --git a/alarm/manager.go b/alarm/manager.go
new file mode 100644
index 00000000..e3f0df5e
--- /dev/null
+++ b/alarm/manager.go
@@ -0,0 +1,220 @@
+/*
+Copyright (c) 2024-2024 VMware, Inc. 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 alarm
+
+import (
+	"context"
+
+	"github.com/vmware/govmomi/event"
+	"github.com/vmware/govmomi/find"
+	"github.com/vmware/govmomi/object"
+	"github.com/vmware/govmomi/property"
+	"github.com/vmware/govmomi/vim25"
+	"github.com/vmware/govmomi/vim25/methods"
+	"github.com/vmware/govmomi/vim25/mo"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+type Manager struct {
+	object.Common
+
+	pc *property.Collector
+}
+
+var Severity = map[types.ManagedEntityStatus]string{
+	types.ManagedEntityStatusGray:   "Unknown",
+	types.ManagedEntityStatusGreen:  "Normal",
+	types.ManagedEntityStatusYellow: "Warning",
+	types.ManagedEntityStatusRed:    "Alert",
+}
+
+// GetManager wraps NewManager, returning ErrNotSupported
+// when the client is not connected to a vCenter instance.
+func GetManager(c *vim25.Client) (*Manager, error) {
+	if c.ServiceContent.AlarmManager == nil {
+		return nil, object.ErrNotSupported
+	}
+	return NewManager(c), nil
+}
+
+func NewManager(c *vim25.Client) *Manager {
+	m := Manager{
+		Common: object.NewCommon(c, *c.ServiceContent.AlarmManager),
+		pc:     property.DefaultCollector(c),
+	}
+
+	return &m
+}
+
+func (m Manager) CreateAlarm(ctx context.Context, entity object.Reference, spec types.BaseAlarmSpec) (*types.ManagedObjectReference, error) {
+	req := types.CreateAlarm{
+		This:   m.Reference(),
+		Entity: entity.Reference(),
+		Spec:   spec,
+	}
+
+	res, err := methods.CreateAlarm(ctx, m.Client(), &req)
+	if err != nil {
+		return nil, err
+	}
+	return &res.Returnval, err
+}
+
+func (m Manager) AcknowledgeAlarm(ctx context.Context, alarm types.ManagedObjectReference, entity object.Reference) error {
+	req := types.AcknowledgeAlarm{
+		This:   m.Reference(),
+		Alarm:  alarm,
+		Entity: entity.Reference(),
+	}
+
+	_, err := methods.AcknowledgeAlarm(ctx, m.Client(), &req)
+
+	return err
+}
+
+// GetAlarm returns available alarms defined on the entity.
+func (m Manager) GetAlarm(ctx context.Context, entity object.Reference) ([]mo.Alarm, error) {
+	req := types.GetAlarm{
+		This: m.Reference(),
+	}
+
+	if entity != nil {
+		req.Entity = types.NewReference(entity.Reference())
+	}
+
+	res, err := methods.GetAlarm(ctx, m.Client(), &req)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(res.Returnval) == 0 {
+		return nil, nil
+	}
+
+	alarms := make([]mo.Alarm, 0, len(res.Returnval))
+
+	err = m.pc.Retrieve(ctx, res.Returnval, []string{"info"}, &alarms)
+	if err != nil {
+		return nil, err
+	}
+
+	return alarms, nil
+}
+
+// StateInfo combines AlarmState with Alarm.Info
+type StateInfo struct {
+	types.AlarmState
+	Info  *types.AlarmInfo `json:"name,omitempty"`
+	Path  string           `json:"path,omitempty"`
+	Event types.BaseEvent  `json:"event,omitempty"`
+}
+
+// StateInfoOptions for the GetStateInfo method
+type StateInfoOptions struct {
+	Declared      bool
+	InventoryPath bool
+	Event         bool
+}
+
+// GetStateInfo combines AlarmState with Alarm.Info
+func (m Manager) GetStateInfo(ctx context.Context, entity object.Reference, opts StateInfoOptions) ([]StateInfo, error) {
+	prop := "triggeredAlarmState"
+	if opts.Declared {
+		prop = "declaredAlarmState"
+		opts.Event = false
+	}
+
+	var e mo.ManagedEntity
+
+	err := m.pc.RetrieveOne(ctx, entity.Reference(), []string{prop}, &e)
+	if err != nil {
+		return nil, err
+	}
+
+	var objs []types.ManagedObjectReference
+	alarms := append(e.DeclaredAlarmState, e.TriggeredAlarmState...)
+	if len(alarms) == 0 {
+		return nil, nil
+	}
+	for i := range alarms {
+		objs = append(objs, alarms[i].Alarm)
+	}
+
+	var info []mo.Alarm
+	err = m.pc.Retrieve(ctx, objs, []string{"info"}, &info)
+	if err != nil {
+		return nil, err
+	}
+
+	state := make([]StateInfo, len(alarms))
+	paths := make(map[types.ManagedObjectReference]string)
+
+	em := event.NewManager(m.Client())
+
+	for i, a := range alarms {
+		path := paths[a.Entity]
+		if opts.InventoryPath {
+			if path == "" {
+				path, err = find.InventoryPath(ctx, m.Client(), a.Entity)
+				if err != nil {
+					return nil, err
+				}
+				paths[a.Entity] = path
+			}
+		} else {
+			path = a.Entity.String()
+		}
+
+		state[i] = StateInfo{
+			AlarmState: a,
+			Path:       path,
+		}
+
+		for j := range info {
+			if info[j].Self == a.Alarm {
+				state[i].Info = &info[j].Info
+				break
+			}
+		}
+
+		if !opts.Event || a.EventKey == 0 {
+			continue
+		}
+
+		spec := types.EventFilterSpec{
+			EventChainId: a.EventKey,
+			Entity: &types.EventFilterSpecByEntity{
+				Entity:    a.Entity,
+				Recursion: types.EventFilterSpecRecursionOptionSelf,
+			},
+		}
+
+		events, err := em.QueryEvents(ctx, spec)
+		if err != nil {
+			return nil, err
+		}
+
+		for j := range events {
+			if events[j].GetEvent().Key == a.EventKey {
+				state[i].Event = events[j]
+				break
+			}
+		}
+	}
+
+	return state, nil
+}
diff --git a/govc/USAGE.md b/govc/USAGE.md
index c37f99cb..8d18b6ed 100644
--- a/govc/USAGE.md
+++ b/govc/USAGE.md
@@ -38,6 +38,8 @@ but appear via `govc $cmd -h`:
 
  - [about](#about)
  - [about.cert](#aboutcert)
+ - [alarm.info](#alarminfo)
+ - [alarms](#alarms)
  - [cluster.add](#clusteradd)
  - [cluster.change](#clusterchange)
  - [cluster.create](#clustercreate)
@@ -455,6 +457,60 @@ Options:
   -thumbprint=false      Output host hash and thumbprint only
 ```
 
+## alarm.info
+
+```
+Usage: govc alarm.info [OPTIONS] PATH
+
+Alarm definition info.
+
+Examples:
+  govc alarm.info
+  govc alarm.info /dc1/host/cluster1
+  govc alarm.info -n alarm.WCPRegisterVMFailedAlarm
+
+Options:
+  -n=[]                  Alarm name
+```
+
+## alarms
+
+```
+Usage: govc alarms [OPTIONS] [PATH]
+
+Show triggered or declared alarms.
+
+Triggered alarms: alarms triggered by this entity or by its descendants.
+Triggered alarms are propagated up the inventory hierarchy so that a user
+can readily tell when a descendant has triggered an alarm.
+
+Declared alarms: alarms that apply to this managed entity.
+Includes alarms defined on this entity and alarms inherited from the parent
+entity, or from any ancestors in the inventory hierarchy.
+
+PATH defaults to the root folder '/'.
+When PATH is provided it should be an absolute inventory path or relative
+to GOVC_DATACENTER. See also:
+  govc find -h
+  govc tree -h
+
+Examples:
+  govc alarms
+  govc alarms vm/folder/vm-name
+  govc alarms /dc1/host/cluster1
+  govc alarms /dc1/host/cluster1 -d
+
+  govc alarms -n alarm.WCPRegisterVMFailedAlarm
+  govc alarms -ack /dc1/host/cluster1
+  govc alarms -ack -n alarm.WCPRegisterVMFailedAlarm vm/vm-name
+
+Options:
+  -ack=false             Acknowledge alarms
+  -d=false               Show declared alarms
+  -l=false               Long listing output
+  -n=                    Filter by alarm name
+```
+
 ## cluster.add
 
 ```
diff --git a/govc/alarm/create.go b/govc/alarm/create.go
new file mode 100644
index 00000000..b83dde43
--- /dev/null
+++ b/govc/alarm/create.go
@@ -0,0 +1,151 @@
+/*
+Copyright (c) 2024-2024 VMware, Inc. 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 alarm
+
+import (
+	"context"
+	"flag"
+	"fmt"
+
+	"github.com/vmware/govmomi/alarm"
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vim25/methods"
+	"github.com/vmware/govmomi/vim25/mo"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+type create struct {
+	*flags.DatacenterFlag
+
+	types.AlarmSpec
+
+	r    bool
+	kind string
+
+	green, yellow, red string
+}
+
+func init() {
+	cli.Register("alarm.create", &create{}, true)
+}
+
+func (cmd *create) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.DatacenterFlag, ctx = flags.NewDatacenterFlag(ctx)
+	cmd.DatacenterFlag.Register(ctx, f)
+
+	f.StringVar(&cmd.AlarmSpec.Name, "n", "", "Alarm name")
+	f.StringVar(&cmd.AlarmSpec.Description, "d", "", "Alarm description")
+	f.BoolVar(&cmd.Enabled, "enabled", true, "Enabled")
+
+	f.StringVar(&cmd.kind, "type", "VirtualMachine", "Object type")
+	f.BoolVar(&cmd.r, "r", false, "Reconfigure existing alarm")
+
+	f.StringVar(&cmd.green, "green", "", "green status event type")
+	f.StringVar(&cmd.yellow, "yellow", "", "yellow status event type")
+	f.StringVar(&cmd.red, "red", "", "red status event type")
+}
+
+func (cmd *create) Usage() string {
+	return "[PATH]"
+}
+
+func (cmd *create) Description() string {
+	return `Create alarm.
+
+Examples:
+  govc alarm.create -n "My Alarm" -green my.alarm.success -yellow my.alarm.failure
+  govc event.post -i my.alarm.failure $vm
+  govc alarms $vm`
+}
+
+func (cmd *create) Run(ctx context.Context, f *flag.FlagSet) error {
+	c, err := cmd.Client()
+	if err != nil {
+		return err
+	}
+
+	obj := c.ServiceContent.RootFolder
+	if f.NArg() == 1 {
+		obj, err = cmd.ManagedObject(ctx, f.Arg(0))
+		if err != nil {
+			return err
+		}
+	}
+
+	var or types.OrAlarmExpression
+
+	expressions := []struct {
+		status types.ManagedEntityStatus
+		typeID string
+	}{
+		{types.ManagedEntityStatusGreen, cmd.green},
+		{types.ManagedEntityStatusYellow, cmd.yellow},
+		{types.ManagedEntityStatusRed, cmd.red},
+	}
+
+	for _, exp := range expressions {
+		if exp.typeID != "" {
+			or.Expression = append(or.Expression, &types.EventAlarmExpression{
+				EventType:   "vim.event.EventEx",
+				EventTypeId: exp.typeID,
+				ObjectType:  "vim." + cmd.kind,
+				Status:      exp.status,
+			})
+		}
+	}
+
+	cmd.AlarmSpec.Expression = &or
+
+	m, err := alarm.GetManager(c)
+	if err != nil {
+		return err
+	}
+
+	if cmd.r {
+		alarms, err := m.GetAlarm(ctx, obj)
+		if err != nil {
+			return err
+		}
+
+		var alarm *mo.Alarm
+		for i := range alarms {
+			if alarms[i].Info.Name == cmd.AlarmSpec.Name {
+				alarm = &alarms[i]
+				break
+			}
+		}
+		if alarm == nil {
+			return fmt.Errorf("%s not found", cmd.AlarmSpec.Name)
+		}
+
+		_, err = methods.ReconfigureAlarm(ctx, c, &types.ReconfigureAlarm{
+			This: alarm.Self,
+			Spec: &cmd.AlarmSpec,
+		})
+		return err
+	}
+
+	ref, err := m.CreateAlarm(ctx, obj, &cmd.AlarmSpec)
+	if err != nil {
+		return err
+	}
+
+	fmt.Println(ref.Value)
+
+	return nil
+}
diff --git a/govc/alarm/info.go b/govc/alarm/info.go
new file mode 100644
index 00000000..f8447ab1
--- /dev/null
+++ b/govc/alarm/info.go
@@ -0,0 +1,119 @@
+/*
+Copyright (c) 2024-2024 VMware, Inc. 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 alarm
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"io"
+	"text/tabwriter"
+
+	"github.com/vmware/govmomi/alarm"
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vim25/mo"
+)
+
+type info struct {
+	*flags.DatacenterFlag
+
+	name flags.StringList
+}
+
+func init() {
+	cli.Register("alarm.info", &info{})
+}
+
+func (cmd *info) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.DatacenterFlag, ctx = flags.NewDatacenterFlag(ctx)
+	cmd.DatacenterFlag.Register(ctx, f)
+
+	f.Var(&cmd.name, "n", "Alarm name")
+}
+
+func (cmd *info) Usage() string {
+	return "PATH"
+}
+
+func (cmd *info) Description() string {
+	return `Alarm definition info.
+
+Examples:
+  govc alarm.info
+  govc alarm.info /dc1/host/cluster1
+  govc alarm.info -n alarm.WCPRegisterVMFailedAlarm`
+}
+
+type infoResult []mo.Alarm
+
+func (r infoResult) Dump() any {
+	return []mo.Alarm(r)
+}
+
+func (r infoResult) Write(w io.Writer) error {
+	tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0)
+	for _, a := range r {
+		fmt.Fprintf(tw, "Name:\t%s\n", a.Info.Name)
+		fmt.Fprintf(tw, "  SystemName:\t%s\n", a.Info.SystemName)
+		fmt.Fprintf(tw, "  Description:\t%s\n", a.Info.Description)
+		fmt.Fprintf(tw, "  Enabled:\t%t\n", a.Info.Enabled)
+	}
+	return tw.Flush()
+}
+
+func (cmd *info) findAlarm(alarms []mo.Alarm) []mo.Alarm {
+	if len(cmd.name) == 0 {
+		return alarms
+	}
+	var match []mo.Alarm
+	for _, alarm := range alarms {
+		for _, name := range cmd.name {
+			if alarm.Info.SystemName == name || alarm.Info.Name == name {
+				match = append(match, alarm)
+			}
+		}
+	}
+	return match
+}
+
+func (cmd *info) Run(ctx context.Context, f *flag.FlagSet) error {
+	c, err := cmd.Client()
+	if err != nil {
+		return err
+	}
+
+	obj := c.ServiceContent.RootFolder
+	if f.NArg() == 1 {
+		obj, err = cmd.ManagedObject(ctx, f.Arg(0))
+		if err != nil {
+			return err
+		}
+	}
+
+	m, err := alarm.GetManager(c)
+	if err != nil {
+		return err
+	}
+
+	alarms, err := m.GetAlarm(ctx, obj)
+	if err != nil {
+		return err
+	}
+
+	return cmd.WriteResult(infoResult(cmd.findAlarm(alarms)))
+}
diff --git a/govc/alarm/rm.go b/govc/alarm/rm.go
new file mode 100644
index 00000000..652f1699
--- /dev/null
+++ b/govc/alarm/rm.go
@@ -0,0 +1,65 @@
+/*
+Copyright (c) 2024-2024 VMware, Inc. 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 alarm
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vim25/methods"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+type rm struct {
+	*flags.ClientFlag
+}
+
+func init() {
+	cli.Register("alarm.rm", &rm{}, true)
+}
+
+func (cmd *rm) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+}
+
+func (cmd *rm) Usage() string {
+	return "ID"
+}
+
+func (cmd *rm) Run(ctx context.Context, f *flag.FlagSet) error {
+	c, err := cmd.Client()
+	if err != nil {
+		return err
+	}
+
+	id := f.Arg(0)
+	var ref types.ManagedObjectReference
+
+	if !ref.FromString(id) {
+		ref.Type = "Alarm"
+		ref.Value = id
+	}
+
+	_, err = methods.RemoveAlarm(ctx, c, &types.RemoveAlarm{
+		This: ref,
+	})
+
+	return err
+}
diff --git a/govc/alarm/state.go b/govc/alarm/state.go
new file mode 100644
index 00000000..04b821cc
--- /dev/null
+++ b/govc/alarm/state.go
@@ -0,0 +1,197 @@
+/*
+Copyright (c) 2024-2024 VMware, Inc. 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 alarm
+
+import (
+	"context"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"io"
+	"text/tabwriter"
+	"time"
+
+	"github.com/vmware/govmomi/alarm"
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+type state struct {
+	*flags.DatacenterFlag
+	*flags.OutputFlag
+
+	ack  bool
+	name string
+	alarm.StateInfoOptions
+}
+
+func init() {
+	cli.Register("alarms", &state{})
+}
+
+func (cmd *state) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.DatacenterFlag, ctx = flags.NewDatacenterFlag(ctx)
+	cmd.DatacenterFlag.Register(ctx, f)
+
+	cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx)
+	cmd.OutputFlag.Register(ctx, f)
+
+	f.BoolVar(&cmd.ack, "ack", false, "Acknowledge alarms")
+	f.StringVar(&cmd.name, "n", "", "Filter by alarm name")
+	f.BoolVar(&cmd.Declared, "d", false, "Show declared alarms")
+	f.BoolVar(&cmd.InventoryPath, "l", false, "Long listing output")
+}
+
+func (cmd *state) Usage() string {
+	return "[PATH]"
+}
+
+func (cmd *state) Description() string {
+	return `Show triggered or declared alarms.
+
+Triggered alarms: alarms triggered by this entity or by its descendants.
+Triggered alarms are propagated up the inventory hierarchy so that a user
+can readily tell when a descendant has triggered an alarm.
+
+Declared alarms: alarms that apply to this managed entity.
+Includes alarms defined on this entity and alarms inherited from the parent
+entity, or from any ancestors in the inventory hierarchy.
+
+PATH defaults to the root folder '/'.
+When PATH is provided it should be an absolute inventory path or relative
+to GOVC_DATACENTER. See also:
+  govc find -h
+  govc tree -h
+
+Examples:
+  govc alarms
+  govc alarms vm/folder/vm-name
+  govc alarms /dc1/host/cluster1
+  govc alarms /dc1/host/cluster1 -d
+
+  govc alarms -n alarm.WCPRegisterVMFailedAlarm
+  govc alarms -ack /dc1/host/cluster1
+  govc alarms -ack -n alarm.WCPRegisterVMFailedAlarm vm/vm-name`
+}
+
+func (cmd *state) Process(ctx context.Context) error {
+	if err := cmd.DatacenterFlag.Process(ctx); err != nil {
+		return err
+	}
+	return cmd.OutputFlag.Process(ctx)
+}
+
+func (cmd *state) filterAlarms(ctx context.Context, m *alarm.Manager, obj types.ManagedObjectReference) ([]alarm.StateInfo, error) {
+	alarms, err := m.GetStateInfo(ctx, obj, cmd.StateInfoOptions)
+	if err != nil || cmd.name == "" {
+		return alarms, err
+	}
+
+	var match []alarm.StateInfo
+	for _, alarm := range alarms {
+		if alarm.Info.SystemName == cmd.name || alarm.Info.Name == cmd.name {
+			match = append(match, alarm)
+		}
+	}
+
+	return match, nil
+}
+
+type stateResult struct {
+	info []alarm.StateInfo
+	cmd  *state
+}
+
+func (r *stateResult) Dump() any {
+	return r.info
+}
+
+func (r *stateResult) MarshalJSON() ([]byte, error) {
+	return json.Marshal(r.info)
+}
+
+func (r *stateResult) Write(w io.Writer) error {
+	tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0)
+
+	fmt.Fprintf(tw, "Alarm Name\tObject\tSeverity\tTriggered Time\tAcknowledged Time\tAcknowledged By\n")
+
+	for _, a := range r.info {
+		name := a.Info.Name
+		tt := a.Time.Format(time.Stamp)
+		at := ""
+		by := a.AcknowledgedByUser
+		if a.AcknowledgedTime != nil {
+			at = a.AcknowledgedTime.Format(time.Stamp)
+		}
+		fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\n", name, a.Path, alarm.Severity[a.OverallStatus], tt, at, by)
+	}
+
+	return tw.Flush()
+}
+
+func (cmd *state) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() > 1 {
+		return flag.ErrHelp
+	}
+	if cmd.ack && cmd.Declared {
+		return flag.ErrHelp
+	}
+
+	c, err := cmd.Client()
+	if err != nil {
+		return err
+	}
+
+	obj := c.ServiceContent.RootFolder
+	if f.NArg() == 1 {
+		obj, err = cmd.ManagedObject(ctx, f.Arg(0))
+		if err != nil {
+			return err
+		}
+	}
+
+	cmd.StateInfoOptions.Event = cmd.All()
+
+	m, err := alarm.GetManager(c)
+	if err != nil {
+		return err
+	}
+
+	alarms, err := cmd.filterAlarms(ctx, m, obj)
+	if err != nil {
+		return err
+	}
+
+	if cmd.ack {
+		for _, alarm := range alarms {
+			if alarm.Acknowledged != nil && *alarm.Acknowledged {
+				continue
+			}
+			if err := m.AcknowledgeAlarm(ctx, alarm.Alarm, alarm.Entity); err != nil {
+				return err
+			}
+		}
+
+		alarms, err = cmd.filterAlarms(ctx, m, obj)
+		if err != nil {
+			return err
+		}
+	}
+
+	return cmd.WriteResult(&stateResult{alarms, cmd})
+}
diff --git a/govc/events/command.go b/govc/events/command.go
index 73efcca8..12a1a660 100644
--- a/govc/events/command.go
+++ b/govc/events/command.go
@@ -1,5 +1,5 @@
 /*
-Copyright (c) 2015-2023 VMware, Inc. All Rights Reserved.
+Copyright (c) 2015-2024 VMware, Inc. 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.
@@ -117,11 +117,21 @@ func (cmd *events) printEvents(ctx context.Context, obj *types.ManagedObjectRefe
 			r.Key = event.Key
 		}
 
-		// if this is a TaskEvent gather a little more information
-		if t, ok := e.(*types.TaskEvent); ok {
+		switch x := e.(type) {
+		case *types.TaskEvent:
 			// some tasks won't have this information, so just use the event message
-			if t.Info.Entity != nil {
-				r.Message = fmt.Sprintf("%s (target=%s %s)", r.Message, t.Info.Entity.Type, t.Info.EntityName)
+			if x.Info.Entity != nil {
+				r.Message = fmt.Sprintf("%s (target=%s %s)", r.Message, x.Info.Entity.Type, x.Info.EntityName)
+			}
+		case *types.EventEx:
+			if r.Message == "" {
+				r.Message = x.Message
+			}
+			if x.ObjectId != "" {
+				r.Message = fmt.Sprintf("%s (%s)", r.Message, x.ObjectId)
+			}
+			if cmd.Long {
+				r.Type = x.EventTypeId
 			}
 		}
 
diff --git a/govc/events/post.go b/govc/events/post.go
new file mode 100644
index 00000000..cf73f87b
--- /dev/null
+++ b/govc/events/post.go
@@ -0,0 +1,80 @@
+/*
+Copyright (c) 2024-2024 VMware, Inc. 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 events
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/event"
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+type post struct {
+	*flags.DatacenterFlag
+
+	types.EventEx
+}
+
+func init() {
+	cli.Register("event.post", &post{}, true)
+}
+
+func (cmd *post) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.DatacenterFlag, ctx = flags.NewDatacenterFlag(ctx)
+	cmd.DatacenterFlag.Register(ctx, f)
+
+	f.StringVar(&cmd.EventTypeId, "i", "", "Event Type ID")
+	f.StringVar(&cmd.Message, "m", "", "Event message")
+	f.StringVar(&cmd.Severity, "s", string(types.EventEventSeverityInfo), "Event severity")
+}
+
+func (cmd *post) Usage() string {
+	return "PATH"
+}
+
+func (cmd *post) Description() string {
+	return `Post Event.
+
+Examples:
+  govc event.post -s warning -i com.vmware.wcp.RegisterVM.failure $vm
+  govc event.post -s info -i com.vmware.wcp.RegisterVM.success $vm
+  govc event.post -m "cluster degraded" /dc1/host/cluster1`
+}
+
+func (cmd *post) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() != 1 {
+		return flag.ErrHelp
+	}
+
+	c, err := cmd.Client()
+	if err != nil {
+		return err
+	}
+
+	obj, err := cmd.ManagedObject(ctx, f.Arg(0))
+	if err != nil {
+		return err
+	}
+
+	cmd.ObjectType = obj.Type
+	cmd.ObjectId = obj.Value
+
+	return event.NewManager(c).PostEvent(ctx, &cmd.EventEx)
+}
diff --git a/govc/flags/datacenter.go b/govc/flags/datacenter.go
index d73f36a8..7cfcef3b 100644
--- a/govc/flags/datacenter.go
+++ b/govc/flags/datacenter.go
@@ -174,7 +174,7 @@ func (flag *DatacenterFlag) ManagedObject(ctx context.Context, arg string) (type
 		for _, o := range l {
 			objs = append(objs, o.Object.Reference())
 		}
-		return ref, fmt.Errorf("%d objects at path %q: %s", len(l), arg, objs)
+		return ref, fmt.Errorf("%d objects match %q: %s (unique inventory path required)", len(l), arg, objs)
 	}
 }
 
diff --git a/govc/main.go b/govc/main.go
index bae197e7..5fb8de56 100644
--- a/govc/main.go
+++ b/govc/main.go
@@ -20,6 +20,7 @@ import (
 	"os"
 
 	_ "github.com/vmware/govmomi/govc/about"
+	_ "github.com/vmware/govmomi/govc/alarm"
 	"github.com/vmware/govmomi/govc/cli"
 	_ "github.com/vmware/govmomi/govc/cluster"
 	_ "github.com/vmware/govmomi/govc/cluster/draft"
diff --git a/govc/object/save.go b/govc/object/save.go
index b6f40358..a2d52a22 100644
--- a/govc/object/save.go
+++ b/govc/object/save.go
@@ -155,12 +155,26 @@ func saveHostSystem(ctx context.Context, c *vim25.Client, ref types.ManagedObjec
 	return []saveMethod{{"QueryTpmAttestationReport", res}}, nil
 }
 
+func saveAlarmManager(ctx context.Context, c *vim25.Client, ref types.ManagedObjectReference) ([]saveMethod, error) {
+	res, err := methods.GetAlarm(ctx, c, &types.GetAlarm{This: ref})
+	if err != nil {
+		return nil, err
+	}
+	pc := property.DefaultCollector(c)
+	var content []types.ObjectContent
+	if err = pc.Retrieve(ctx, res.Returnval, nil, &content); err != nil {
+		return nil, err
+	}
+	return []saveMethod{{"GetAlarm", res}, {"", content}}, nil
+}
+
 // saveObjects maps object types to functions that can save data that isn't available via the PropertyCollector
 var saveObjects = map[string]func(context.Context, *vim25.Client, types.ManagedObjectReference) ([]saveMethod, error){
 	"VmwareDistributedVirtualSwitch": saveDVS,
 	"EnvironmentBrowser":             saveEnvironmentBrowser,
 	"HostNetworkSystem":              saveHostNetworkSystem,
 	"HostSystem":                     saveHostSystem,
+	"AlarmManager":                   saveAlarmManager,
 }
 
 func isNotConnected(err error) bool {
@@ -204,6 +218,13 @@ func (cmd *save) save(content []types.ObjectContent) error {
 				return err
 			}
 			for _, obj := range objs {
+				if obj.Name == "" {
+					err = cmd.save(obj.Data.([]types.ObjectContent))
+					if err != nil {
+						return err
+					}
+					continue
+				}
 				err = cmd.write(filepath.Join(ref, obj.Name), obj.Data)
 				if err != nil {
 					return err
diff --git a/simulator/alarm_manager.go b/simulator/alarm_manager.go
new file mode 100644
index 00000000..91c94ff8
--- /dev/null
+++ b/simulator/alarm_manager.go
@@ -0,0 +1,270 @@
+/*
+Copyright (c) 2024-2024 VMware, Inc. 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 simulator
+
+import (
+	"strings"
+	"time"
+
+	"github.com/vmware/govmomi/simulator/vpx"
+	"github.com/vmware/govmomi/vim25/methods"
+	"github.com/vmware/govmomi/vim25/mo"
+	"github.com/vmware/govmomi/vim25/soap"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+type AlarmManager struct {
+	mo.AlarmManager
+
+	types.GetAlarmResponse
+}
+
+func (m *AlarmManager) init(r *Registry) {
+	if m.GetAlarmResponse.Returnval != nil {
+		return
+	}
+
+	m.GetAlarmResponse.Returnval = make([]types.ManagedObjectReference, len(vpx.Alarm))
+	for i, alarm := range vpx.Alarm {
+		m.GetAlarmResponse.Returnval[i] = alarm.Self
+		r.Put(&Alarm{Alarm: alarm})
+	}
+}
+
+func (*AlarmManager) trimPrefix(s string) string {
+	return strings.TrimPrefix(s, "vim.")
+}
+
+func (*AlarmManager) key(refs ...types.ManagedObjectReference) string {
+	keys := make([]string, len(refs))
+	for i := range refs {
+		s := strings.Split(refs[i].Value, "-")
+		keys[i] = s[len(s)-1]
+	}
+	return strings.Join(keys, ".")
+}
+
+// only handling the common use case of EventEx for now
+func (m *AlarmManager) matchAlarm(alarm *Alarm, event *types.EventEx) (*mo.Alarm, types.ManagedEntityStatus) {
+	id := event.EventTypeId
+	kind := m.trimPrefix(event.ObjectType)
+
+	switch op := alarm.Info.Expression.(type) {
+	case *types.OrAlarmExpression:
+		for i := range op.Expression {
+			switch x := op.Expression[i].(type) {
+			case *types.EventAlarmExpression:
+				if x.EventTypeId == id && kind == m.trimPrefix(x.ObjectType) {
+					return &alarm.Alarm, x.Status
+				}
+			}
+		}
+	}
+	return nil, ""
+}
+
+// update (e.g. triggeredAlarmState) and propagate up the inventory hierarchy
+func (*AlarmManager) update(ctx *Context, me mo.Entity, update func(mo.Entity) *types.ManagedObjectReference) {
+	for {
+		if me == nil {
+			break
+		}
+		ctx.WithLock(me, func() {
+			parent := update(me)
+			if parent == nil {
+				me = nil
+			} else {
+				me = ctx.Map.Get(*parent).(mo.Entity)
+			}
+		})
+	}
+}
+
+// postEvent triggers Alarms based on Events
+func (m *AlarmManager) postEvent(ctx *Context, base types.BaseEvent) {
+	event, ok := base.(*types.EventEx)
+	if !ok {
+		return
+	}
+
+	entity := types.ManagedObjectReference{Type: event.ObjectType, Value: event.ObjectId}
+	me := ctx.Map.Get(entity).(mo.Entity)
+
+	for _, ref := range m.GetAlarmResponse.Returnval {
+		alarm := ctx.Map.Get(ref).(*Alarm)
+		match, status := m.matchAlarm(alarm, event)
+		if match == nil {
+			continue
+		}
+
+		now := time.Now()
+		key := m.key(match.Self, entity)
+
+		update := func(me mo.Entity) *types.ManagedObjectReference {
+			obj := me.Entity()
+
+			for i, state := range obj.TriggeredAlarmState {
+				if state.Key != key {
+					continue
+				}
+
+				switch status {
+				case state.OverallStatus:
+					// no change
+					return nil
+				case types.ManagedEntityStatusGreen:
+					// remove
+					obj.TriggeredAlarmState =
+						append(obj.TriggeredAlarmState[:i],
+							obj.TriggeredAlarmState[i+1:]...)
+					return obj.Parent
+				default:
+					// status change (e.g. yellow -> red)
+					obj.TriggeredAlarmState[i].OverallStatus = status
+					return obj.Parent
+				}
+			}
+
+			if status == types.ManagedEntityStatusGreen {
+				return nil // green only clears a triggered alarm
+			}
+
+			// add
+			state := types.AlarmState{
+				Key:           key,
+				Entity:        entity,
+				Alarm:         match.Self,
+				OverallStatus: status,
+				Time:          now,
+				EventKey:      event.Key,
+				Acknowledged:  types.NewBool(false),
+			}
+
+			obj.TriggeredAlarmState = append(obj.TriggeredAlarmState, state)
+
+			return obj.Parent
+		}
+
+		m.update(ctx, me, update)
+	}
+}
+
+func (m *AlarmManager) GetAlarm(ctx *Context, req *types.GetAlarm) soap.HasFault {
+	body := &methods.GetAlarmBody{
+		Res: new(types.GetAlarmResponse),
+	}
+
+	if req.Entity == nil || *req.Entity == ctx.Map.content().RootFolder {
+		body.Res.Returnval = m.GetAlarmResponse.Returnval
+	} // else TODO
+
+	return body
+}
+
+func (m *AlarmManager) CreateAlarm(ctx *Context, req *types.CreateAlarm) soap.HasFault {
+	body := new(methods.CreateAlarmBody)
+
+	name := req.Spec.GetAlarmSpec().Name
+
+	for _, alarm := range ctx.Map.AllReference("Alarm") {
+		if alarm.(*Alarm).Info.Name == name {
+			body.Fault_ = Fault("", &types.DuplicateName{Name: name})
+			return body
+		}
+	}
+
+	alarm := Alarm{
+		Alarm: mo.Alarm{
+			Info: types.AlarmInfo{
+				AlarmSpec:        *req.Spec.GetAlarmSpec(),
+				Entity:           req.Entity,
+				LastModifiedTime: time.Now(),
+				LastModifiedUser: ctx.Session.UserName,
+			},
+		},
+	}
+
+	ref := ctx.Map.Put(&alarm).Reference()
+	alarm.Info.Alarm = ref
+	m.GetAlarmResponse.Returnval = append(m.GetAlarmResponse.Returnval, ref)
+
+	body.Res = &types.CreateAlarmResponse{
+		Returnval: ref,
+	}
+
+	return body
+}
+
+func (m *AlarmManager) AcknowledgeAlarm(ctx *Context, req *types.AcknowledgeAlarm) soap.HasFault {
+	body := new(methods.AcknowledgeAlarmBody)
+
+	now := types.NewTime(time.Now())
+	key := m.key(req.Alarm, req.Entity)
+	me := ctx.Map.Get(req.Entity).(mo.Entity)
+
+	update := func(me mo.Entity) *types.ManagedObjectReference {
+		obj := me.Entity()
+
+		for i, state := range obj.TriggeredAlarmState {
+			if state.Key == key {
+				if *obj.TriggeredAlarmState[i].Acknowledged {
+					return nil // already ack-ed
+				}
+				obj.TriggeredAlarmState[i].Acknowledged = types.NewBool(true)
+				obj.TriggeredAlarmState[i].AcknowledgedTime = now
+				obj.TriggeredAlarmState[i].AcknowledgedByUser = ctx.Session.UserName
+				return obj.Parent
+			}
+		}
+
+		return nil
+	}
+
+	m.update(ctx, me, update)
+
+	body.Res = new(types.AcknowledgeAlarmResponse)
+
+	return body
+}
+
+type Alarm struct {
+	mo.Alarm
+}
+
+func (a *Alarm) ReconfigureAlarm(ctx *Context, req *types.ReconfigureAlarm) soap.HasFault {
+	body := new(methods.ReconfigureAlarmBody)
+
+	// TODO: spec validation
+
+	a.Info.AlarmSpec = *req.Spec.GetAlarmSpec()
+
+	body.Res = new(types.ReconfigureAlarmResponse)
+
+	return body
+}
+
+func (a *Alarm) RemoveAlarm(ctx *Context, req *types.RemoveAlarm) soap.HasFault {
+	m := ctx.Map.AlarmManager()
+
+	RemoveReference(&m.GetAlarmResponse.Returnval, req.This)
+
+	ctx.Map.Remove(ctx, req.This)
+
+	return &methods.RemoveAlarmBody{
+		Res: new(types.RemoveAlarmResponse),
+	}
+}
diff --git a/simulator/event_manager.go b/simulator/event_manager.go
index 72404dbc..94edea51 100644
--- a/simulator/event_manager.go
+++ b/simulator/event_manager.go
@@ -166,6 +166,10 @@ func (m *EventManager) PostEvent(ctx *Context, req *types.PostEvent) soap.HasFau
 		})
 	}
 
+	if m := ctx.Map.AlarmManager(); m != nil {
+		ctx.WithLock(m, func() { m.postEvent(ctx, req.EventToPost) })
+	}
+
 	return &methods.PostEventBody{
 		Res: new(types.PostEventResponse),
 	}
@@ -229,6 +233,11 @@ func doEntityEventArgument(event types.BaseEvent, f func(types.ManagedObjectRefe
 
 // eventFilterSelf returns true if self is one of the entity arguments in the event.
 func eventFilterSelf(event types.BaseEvent, self types.ManagedObjectReference) bool {
+	if x, ok := event.(*types.EventEx); ok {
+		if self.Type == x.ObjectType && self.Value == x.ObjectId {
+			return true
+		}
+	}
 	return doEntityEventArgument(event, func(ref types.ManagedObjectReference, _ *types.EntityEventArgument) bool {
 		return self == ref
 	})
@@ -280,6 +289,17 @@ func (c *EventHistoryCollector) entityMatches(ctx *Context, event types.BaseEven
 	return false
 }
 
+// chainMatches returns true if spec.EventChainId matches the event.
+func (c *EventHistoryCollector) chainMatches(_ *Context, event types.BaseEvent, spec *types.EventFilterSpec) bool {
+	e := event.GetEvent()
+	if spec.EventChainId != 0 {
+		if e.ChainId != spec.EventChainId {
+			return false
+		}
+	}
+	return true
+}
+
 // typeMatches returns true if one of the spec EventTypeId types matches the event.
 func (c *EventHistoryCollector) typeMatches(_ *Context, event types.BaseEvent, spec *types.EventFilterSpec) bool {
 	if len(spec.EventTypeId) == 0 {
@@ -294,6 +314,11 @@ func (c *EventHistoryCollector) typeMatches(_ *Context, event types.BaseEvent, s
 		}
 		return false
 	}
+
+	if x, ok := event.(*types.EventEx); ok {
+		return matches(x.EventTypeId)
+	}
+
 	kind := reflect.ValueOf(event).Elem().Type()
 
 	if matches(kind.Name()) {
@@ -334,6 +359,7 @@ func (c *EventHistoryCollector) eventMatches(ctx *Context, event types.BaseEvent
 	spec := c.Filter.(types.EventFilterSpec)
 
 	matchers := []func(*Context, types.BaseEvent, *types.EventFilterSpec) bool{
+		c.chainMatches,
 		c.typeMatches,
 		c.timeMatches,
 		c.entityMatches,
diff --git a/simulator/model.go b/simulator/model.go
index aa91604b..a595f189 100644
--- a/simulator/model.go
+++ b/simulator/model.go
@@ -232,6 +232,7 @@ func (*Model) fmtName(prefix string, num int) string {
 
 // kinds maps managed object types to their vcsim wrapper types
 var kinds = map[string]reflect.Type{
+	"AlarmManager":                       reflect.TypeOf((*AlarmManager)(nil)).Elem(),
 	"AuthorizationManager":               reflect.TypeOf((*AuthorizationManager)(nil)).Elem(),
 	"ClusterComputeResource":             reflect.TypeOf((*ClusterComputeResource)(nil)).Elem(),
 	"CustomFieldsManager":                reflect.TypeOf((*CustomFieldsManager)(nil)).Elem(),
diff --git a/simulator/registry.go b/simulator/registry.go
index acfc2265..eebfec90 100644
--- a/simulator/registry.go
+++ b/simulator/registry.go
@@ -516,6 +516,15 @@ func (r *Registry) SearchIndex() *SearchIndex {
 	return r.Get(r.content().SearchIndex.Reference()).(*SearchIndex)
 }
 
+// AlarmManager returns the AlarmManager singleton
+func (r *Registry) AlarmManager() *AlarmManager {
+	ref := r.content().AlarmManager
+	if ref == nil {
+		return nil // ESX
+	}
+	return r.Get(*ref).(*AlarmManager)
+}
+
 // EventManager returns the EventManager singleton
 func (r *Registry) EventManager() *EventManager {
 	return r.Get(r.content().EventManager.Reference()).(*EventManager)
diff --git a/simulator/vpx/alarm_manager.go b/simulator/vpx/alarm_manager.go
new file mode 100644
index 00000000..c649168b
--- /dev/null
+++ b/simulator/vpx/alarm_manager.go
@@ -0,0 +1,219 @@
+/*
+Copyright (c) 2024-2024 VMware, Inc. 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 vpx
+
+import (
+	"time"
+
+	"github.com/vmware/govmomi/vim25/mo"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+// Alarm captured using:
+// govc alarm.info -dump -n alarm.VmErrorAlarm -n alarm.HostErrorAlarm
+var Alarm = []mo.Alarm{
+	{
+		ExtensibleManagedObject: mo.ExtensibleManagedObject{
+			Self:           types.ManagedObjectReference{Type: "Alarm", Value: "alarm-384", ServerGUID: ""},
+			Value:          nil,
+			AvailableField: nil,
+		},
+		Info: types.AlarmInfo{
+			AlarmSpec: types.AlarmSpec{
+				Name:        "vcsim VM Alarm",
+				SystemName:  "",
+				Description: "vcsim alarm for Virtual Machines",
+				Enabled:     true,
+				Expression: &types.OrAlarmExpression{
+					AlarmExpression: types.AlarmExpression{},
+					Expression: []types.BaseAlarmExpression{
+						&types.EventAlarmExpression{
+							AlarmExpression: types.AlarmExpression{},
+							Comparisons:     nil,
+							EventType:       "EventEx",
+							EventTypeId:     "vcsim.vm.success",
+							ObjectType:      "VirtualMachine",
+							Status:          "green",
+						},
+						&types.EventAlarmExpression{
+							AlarmExpression: types.AlarmExpression{},
+							Comparisons:     nil,
+							EventType:       "EventEx",
+							EventTypeId:     "vcsim.vm.failure",
+							ObjectType:      "VirtualMachine",
+							Status:          "yellow",
+						},
+						&types.EventAlarmExpression{
+							AlarmExpression: types.AlarmExpression{},
+							Comparisons:     nil,
+							EventType:       "EventEx",
+							EventTypeId:     "vcsim.vm.fatal",
+							ObjectType:      "VirtualMachine",
+							Status:          "red",
+						},
+					},
+				},
+				Action:          nil,
+				ActionFrequency: 0,
+				Setting: &types.AlarmSetting{
+					ToleranceRange:     0,
+					ReportingFrequency: 300,
+				},
+			},
+			Key:              "",
+			Alarm:            types.ManagedObjectReference{Type: "Alarm", Value: "alarm-384", ServerGUID: ""},
+			Entity:           types.ManagedObjectReference{Type: "Folder", Value: "group-d1", ServerGUID: ""},
+			LastModifiedTime: time.Now(),
+			LastModifiedUser: "VSPHERE.LOCAL\\Administrator",
+			CreationEventId:  0,
+		},
+	},
+	{
+		ExtensibleManagedObject: mo.ExtensibleManagedObject{
+			Self:           types.ManagedObjectReference{Type: "Alarm", Value: "alarm-11", ServerGUID: ""},
+			Value:          nil,
+			AvailableField: nil,
+		},
+		Info: types.AlarmInfo{
+			AlarmSpec: types.AlarmSpec{
+				Name:        "Host error",
+				SystemName:  "alarm.HostErrorAlarm",
+				Description: "Default alarm to monitor host error and warning events",
+				Enabled:     true,
+				Expression: &types.OrAlarmExpression{
+					AlarmExpression: types.AlarmExpression{},
+					Expression: []types.BaseAlarmExpression{
+						&types.EventAlarmExpression{
+							AlarmExpression: types.AlarmExpression{},
+							Comparisons:     nil,
+							EventType:       "GeneralHostErrorEvent",
+							EventTypeId:     "",
+							ObjectType:      "HostSystem",
+							Status:          "",
+						},
+						&types.EventAlarmExpression{
+							AlarmExpression: types.AlarmExpression{},
+							Comparisons:     nil,
+							EventType:       "GeneralHostWarningEvent",
+							EventTypeId:     "",
+							ObjectType:      "HostSystem",
+							Status:          "",
+						},
+					},
+				},
+				Action: &types.GroupAlarmAction{
+					AlarmAction: types.AlarmAction{},
+					Action: []types.BaseAlarmAction{
+						&types.AlarmTriggeringAction{
+							AlarmAction: types.AlarmAction{},
+							Action:      &types.SendSNMPAction{},
+							TransitionSpecs: []types.AlarmTriggeringActionTransitionSpec{
+								{
+									StartState: "yellow",
+									FinalState: "red",
+									Repeats:    true,
+								},
+							},
+							Green2yellow: false,
+							Yellow2red:   false,
+							Red2yellow:   false,
+							Yellow2green: false,
+						},
+					},
+				},
+				ActionFrequency: 0,
+				Setting: &types.AlarmSetting{
+					ToleranceRange:     0,
+					ReportingFrequency: 300,
+				},
+			},
+			Key:              "",
+			Alarm:            types.ManagedObjectReference{Type: "Alarm", Value: "alarm-11", ServerGUID: ""},
+			Entity:           types.ManagedObjectReference{Type: "Folder", Value: "group-d1", ServerGUID: ""},
+			LastModifiedTime: time.Now(),
+			LastModifiedUser: "",
+			CreationEventId:  0,
+		},
+	},
+	{
+		ExtensibleManagedObject: mo.ExtensibleManagedObject{
+			Self:           types.ManagedObjectReference{Type: "Alarm", Value: "alarm-12", ServerGUID: ""},
+			Value:          nil,
+			AvailableField: nil,
+		},
+		Info: types.AlarmInfo{
+			AlarmSpec: types.AlarmSpec{
+				Name:        "Virtual machine error",
+				SystemName:  "alarm.VmErrorAlarm",
+				Description: "Default alarm to monitor virtual machine error and warning events",
+				Enabled:     true,
+				Expression: &types.OrAlarmExpression{
+					AlarmExpression: types.AlarmExpression{},
+					Expression: []types.BaseAlarmExpression{
+						&types.EventAlarmExpression{
+							AlarmExpression: types.AlarmExpression{},
+							Comparisons:     nil,
+							EventType:       "GeneralVmErrorEvent",
+							EventTypeId:     "",
+							ObjectType:      "VirtualMachine",
+							Status:          "",
+						},
+						&types.EventAlarmExpression{
+							AlarmExpression: types.AlarmExpression{},
+							Comparisons:     nil,
+							EventType:       "GeneralVmWarningEvent",
+							EventTypeId:     "",
+							ObjectType:      "VirtualMachine",
+							Status:          "",
+						},
+					},
+				},
+				Action: &types.GroupAlarmAction{
+					AlarmAction: types.AlarmAction{},
+					Action: []types.BaseAlarmAction{
+						&types.AlarmTriggeringAction{
+							AlarmAction: types.AlarmAction{},
+							Action:      &types.SendSNMPAction{},
+							TransitionSpecs: []types.AlarmTriggeringActionTransitionSpec{
+								{
+									StartState: "yellow",
+									FinalState: "red",
+									Repeats:    true,
+								},
+							},
+							Green2yellow: false,
+							Yellow2red:   false,
+							Red2yellow:   false,
+							Yellow2green: false,
+						},
+					},
+				},
+				ActionFrequency: 0,
+				Setting: &types.AlarmSetting{
+					ToleranceRange:     0,
+					ReportingFrequency: 300,
+				},
+			},
+			Key:              "",
+			Alarm:            types.ManagedObjectReference{Type: "Alarm", Value: "alarm-12", ServerGUID: ""},
+			Entity:           types.ManagedObjectReference{Type: "Folder", Value: "group-d1", ServerGUID: ""},
+			LastModifiedTime: time.Now(),
+			LastModifiedUser: "",
+			CreationEventId:  0,
+		},
+	},
+}


Test patch:
diff --git a/govc/test/alarms.bats b/govc/test/alarms.bats
new file mode 100755
index 00000000..a7196669
--- /dev/null
+++ b/govc/test/alarms.bats
@@ -0,0 +1,127 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test "alarms" {
+  vcsim_env
+
+  run govc alarms
+  assert_success
+
+  run govc alarms -d
+  assert_success
+
+  vm=/DC0/vm/DC0_H0_VM0
+
+  run govc alarms $vm
+  assert_success
+
+  run govc object.collect -s $vm triggeredAlarmState
+  assert_success "" # empty
+
+  run env GOVC_SHOW_UNRELEASED=true govc event.post -s info -i vcsim.vm.success $vm
+  assert_success
+
+  run govc alarms $vm
+  assert_success
+  [ ${#lines[@]} -eq 1 ] # header only, no alarm triggered
+
+  run env GOVC_SHOW_UNRELEASED=true govc event.post -s warning -i vcsim.vm.failure $vm
+  assert_success
+
+  run govc alarms $vm
+  assert_success
+  assert_matches "Warning"
+
+  run govc alarms -l $vm
+  assert_success
+
+  run govc alarms -json $vm
+  assert_success
+  alarms="$output"
+  run jq -r .[].event.eventTypeId <<<"$alarms"
+  assert_success "vcsim.vm.failure"
+
+  run govc object.collect -json -s $vm triggeredAlarmState
+  assert_success
+  state="$output"
+  run jq -r .[].overallStatus <<<"$state"
+  assert_success "yellow"
+  run jq -r .[].acknowledged <<<"$state"
+  assert_success "false"
+
+  run govc alarms -ack
+  assert_success
+
+  run govc object.collect -json -s $vm triggeredAlarmState
+  assert_success
+  state="$output"
+  run jq -r .[].overallStatus <<<"$state"
+  assert_success "yellow"
+  run jq -r .[].acknowledged <<<"$state"
+  assert_success "true"
+
+  run env GOVC_SHOW_UNRELEASED=true govc event.post -s info -i vcsim.vm.success $vm
+  assert_success
+
+  run govc object.collect -s $vm triggeredAlarmState
+  assert_success "" # empty
+
+  run govc object.collect -s / triggeredAlarmState
+  assert_success "" # empty
+}
+
+@test "alarm.info" {
+  vcsim_env
+
+  run govc alarm.info
+  assert_success
+
+  run govc alarm.info -n alarm.VmErrorAlarm
+  assert_success
+  assert_matches "Virtual machine error"
+
+  run govc alarm.info -n invalid
+  assert_success ""
+}
+
+@test "alarm -esx" {
+  vcsim_env -esx
+
+  run govc object.collect -s - content.alarmManager
+  assert_success "" # empty, no AlarmManager
+
+  run govc alarm.info
+  assert_failure # not supported
+}
+
+@test "alarm.create" {
+  vcsim_env
+
+  export GOVC_SHOW_UNRELEASED=true
+
+  run govc alarm.create -n "My Alarm" -green my.alarm.success -yellow my.alarm.failure
+  assert_success
+  id="$output"
+
+  self=$(govc alarm.info -json -n "My Alarm" | jq -r .[].self.value)
+  assert_equal "$id" "$self"
+
+  run govc alarm.info -n "My Alarm"
+  assert_success
+
+  run govc alarm.create -n "My Alarm"
+  assert_failure # DuplicateName
+
+  run govc alarm.create -n "My Alarm" -r -d "This is my alarm description"
+  assert_success
+
+  run govc alarm.rm "$id"
+  assert_success
+
+  run govc alarm.rm "$id"
+  assert_failure
+
+  run govc alarm.info -n "My Alarm"
+  assert_success ""
+}
diff --git a/govc/test/events.bats b/govc/test/events.bats
index cac4ee2c..90ddcc1b 100755
--- a/govc/test/events.bats
+++ b/govc/test/events.bats
@@ -93,3 +93,17 @@ load test_helper
   govc events 'vm/*'
   govc events -json 'vm/*' | jq .
 }
+
+@test "events post" {
+  vcsim_env
+
+  export GOVC_SHOW_UNRELEASED=true
+
+  run govc event.post -m testing123
+  assert_failure
+
+  run govc event.post -m testing123 /DC0
+  assert_success
+
+  govc events | grep testing123
+}