Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Pagerduty trigger example in go #201

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/site/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ examples:
url: "/tree/master/examples/python/tagging"
- language: powercli
url: "/tree/master/examples/powercli/tagging"
- language: golang
- language: go
url: "/tree/master/examples/go/tagging"

- title: Send VM Configuration Changes to Slack
Expand Down Expand Up @@ -91,6 +91,8 @@ examples:
links:
- language: python
url: "/tree/master/examples/python/trigger-pagerduty-incident"
- language: go
url: "/tree/master/examples/go/pagerduty-trigger"

- title: POST to any REST API
usecases:
Expand Down
2 changes: 2 additions & 0 deletions examples/go/pagerduty-trigger/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
template
build
128 changes: 128 additions & 0 deletions examples/go/pagerduty-trigger/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Installing and using PagerDuty Trigger Function in Go

## Function Purpose

A benefit of using VEBA is being able to integrate vSphere events with external
applications. This can be done by writing a handler function that makes an HTTP
request to an API. This function is an example written in Go that responds to a
`VmReconfiguredEvent`, sends information regarding the event to PagerDuty.

## Clone the VEBA repo

The VEBA repository contains many function examples, including go-pagerduty-trigger.
Clone it to get a local copy.

```bash
git clone https://github.com/vmware-samples/vcenter-event-broker-appliance
cd vcenter-event-broker-appliance/examples/go/pagerduty-trigger
git checkout master
```

## PagerDuty

For go-pagerduty-trigger function to work, you'll need a PagerDuty account and
setup, which will not be covered here. PK goes over the setup in (a Medium blog)[https://medium.com/@pkblah/integrating-vcenter-with-pagerduty-241d3813871c].

## Customize the function

For security reasons, do not expose sensitive data. We will create a Kubernetes [secret](https://kubernetes.io/docs/concepts/configuration/secret/)
which will hold the PagerDuty routing key and trigger. This secret will be mounted
(by the appliance) into the function during runtime. The secret will need to be
created via `faas-cli`.

First, change the configuration file [pdconfig.toml](pdconfig.toml) holding your secret vCenter
information located in this folder:

```toml
# pdconfig.toml contents
# Replace with your own values.
[pagerduty]
routingKey = "<replace with your routing key>"
eventAction = "trigger"
```

Store the pdconfig.toml configuration file as secret in the appliance using the
following:

```bash
# Set up faas-cli for first use.
export OPENFAAS_URL=https://VEBA_FQDN_OR_IP

# Log into the OpenFaaS client. The username and password were set during the vCenter
# Event Broker Appliance deployment process. The username can be excluded in the
# command if the default, admin, was used.
faas-cli login -p VEBA_OPENFAAS_PASSWORD --tls-no-verify

# Create the secret
faas-cli secret create pdconfig --from-file=pdconfig.toml --tls-no-verify

# Update the secret if needed
faas-cli secret update pdconfig --from-file=pdconfig.toml --tls-no-verify
```

> **Note:** Delete the local `pdconfig.toml` after you're done with this exercise
to not expose this sensitive information.

Lastly, define the vCenter event which will trigger this function. Such function-
specific settings are performed in the `stack.yml` file. Open and edit the `stack.yml`
provided in the examples/go/pagerduty-trigger directory. Change `gateway` and `topic`
as per your environment/needs.

> **Note:** A key-value annotation under `topic` defines which VM event should
trigger the function. A list of VM events from vCenter can be found [here](https://code.vmware.com/doc/preview?id=4206#/doc/vim.event.VmEvent.html).
A single topic can be written as `topic: VmPoweredOnEvent`. Multiple topics can
be specified using a `","` delimiter syntax, e.g. "`topic: "VmPoweredOnEvent,VmPoweredOffEvent"`".

```yaml
version: 1.0
provider:
name: openfaas
gateway: https://veba.yourdomain.com # Replace with your VEBA environment.
functions:
go-pagerduty-trigger-fn:
lang: golang-http
handler: ./handler
image: vmware/veba-go-pagerduty-trigger:latest
secrets:
- pdconfig # Ensure this name matches the secret you created.
annotations:
topic: VmReconfiguredEvent
```

> **Note:** If you are running a vSphere DRS-enabled cluster the topic annotation
above should be `DrsVmPoweredOnEvent`. Otherwise, the function would never be triggered.

## Deploy the function

After you've performed the steps and modifications above, you can deploy the function:

```bash
faas template store pull golang-http # only required during the first deployment
faas-cli deploy -f stack.yml --tls-no-verify
Deployed. 202 Accepted.
```

## Trigger the function

1. Reconfigure the VM (e.g. change numCPU, memoryMB, cpuHotAddEnabled, etc.)
1. Log into PagerDuty to see the reaction to the trigger event.

> **Note:** If the function doesn't trigger a PagerDuty notification, then verify
that you correctly followed each step above. Ensure the IPs/FQDNs and credentials
are correct and see the [troubleshooting](#troubleshooting) section below.

## Troubleshooting

If you reconfigured your VM but don't see a PagerDuty notification, verify:

- PagerDuty routing key
- Whether the components can talk to each other (VMware Event Router to vCenter
and OpenFaaS, function to vCenter)
- Check the logs:

```bash
faas-cli logs go-pagerduty-trigger-fn --follow --tls-no-verify

# Successful log message will look something similar to
POST / - 200 OK - ContentLength: 95
```
9 changes: 9 additions & 0 deletions examples/go/pagerduty-trigger/handler/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module github.com/codegold79/vcenter-event-broker-appliance/examples/go/pagerduty-trigger/handler

go 1.14

require (
github.com/openfaas/templates-sdk v0.0.0-20200723110415-a699ec277c12
github.com/pelletier/go-toml v1.8.0
github.com/vmware/govmomi v0.23.1
)
20 changes: 20 additions & 0 deletions examples/go/pagerduty-trigger/handler/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
codegold79 marked this conversation as resolved.
Show resolved Hide resolved
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892/go.mod h1:CTDl0pzVzE5DEzZhPfvhY/9sPFMQIxaJ9VAMs9AagrE=
github.com/google/uuid v0.0.0-20170306145142-6a5e28554805/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/openfaas/templates-sdk v0.0.0-20200723110415-a699ec277c12 h1:P5IjrDQug3GRIfNC9/S7+UvXAbNdenhnHtjz4Qs8Kd8=
github.com/openfaas/templates-sdk v0.0.0-20200723110415-a699ec277c12/go.mod h1:JWcVHdzlHcR7nLuaDL88Mz68wOqDvOn0CLO6t27OMhk=
github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw=
github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs=
github.com/vmware/govmomi v0.23.1 h1:vU09hxnNR/I7e+4zCJvW+5vHu5dO64Aoe2Lw7Yi/KRg=
github.com/vmware/govmomi v0.23.1/go.mod h1:Y+Wq4lst78L85Ge/F8+ORXIWiKYqaro1vhAulACy9Lc=
github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728/go.mod h1:x9oS4Wk2s2u4tS29nEaDLdzvuHdB19CvSGJjPgkZJNk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
193 changes: 193 additions & 0 deletions examples/go/pagerduty-trigger/handler/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package function

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"

handler "github.com/openfaas/templates-sdk/go-http"
toml "github.com/pelletier/go-toml"
)

const (
pdApiPath = "https://events.pagerduty.com/v2/enqueue"
pdConfigPath = "/var/openfaas/secrets/pdconfig"
httpClientTimeout = 10 * time.Second
)

// Handle a function invocation.
func Handle(req handler.Request) (handler.Response, error) {
ctx := context.Background()

// Load the PagerDuty config file at every function invocation.
pdCfg, err := loadConfig(pdConfigPath)
if err != nil {
return handlerResponseWithError(
"load PagerDuty configs: %w",
http.StatusBadRequest,
err,
)
}

// Transfer event information data to PagerDuty payload.
pdPld, err := parseEvent(req.Body, pdCfg)
if err != nil {
return handlerResponseWithError(
"parse event data: %w",
http.StatusInternalServerError,
err,
)
}

// Send information to PagerDuty.
pdRes, err := pdSendRequest(ctx, pdApiPath, pdPld)
if err != nil {
return handlerResponseWithError(
"connect to PagerDuty API: %w",
http.StatusInternalServerError,
err,
)
}

// Display PagerDuty response.
return handler.Response{
Body: pdRes,
StatusCode: http.StatusOK,
}, nil
}

func handlerResponseWithError(msg string, code int, err error) (handler.Response, error) {
wrapErr := fmt.Errorf(msg, err)
log.Println(wrapErr.Error())

return handler.Response{
Body: []byte(wrapErr.Error()),
StatusCode: code,
}, wrapErr
}

func loadConfig(path string) (pdConfig, error) {
var pdc pdConfig

secret, err := toml.LoadFile(path)
if err != nil {
return pdConfig{}, fmt.Errorf("read pdconfig.toml: %w", err)
}

if err := secret.Unmarshal(&pdc); err != nil {
return pdConfig{}, fmt.Errorf("unmarshal pdconfig.toml: %w", err)
}

if err := validatePdConf(pdc); err != nil {
return pdConfig{}, err
}

return pdc, nil
}

func validatePdConf(pdc pdConfig) error {
if pdc.PagerDuty.RoutingKey == "" {
return errors.New("PagerDuty routing key cannot be empty in pdconfig.toml")
}

if pdc.PagerDuty.EventAction == "" {
return errors.New("PagerDuty event action cannot be empty in pdconfig.toml")
}

return nil
}

// parseEvent saves info from the triggering event into a PagerDuty data object.
func parseEvent(req []byte, pdc pdConfig) (pdPayload, error) {
var ce cloudEvent
var pd pdPayload

if err := json.Unmarshal(req, &ce); err != nil {
return pd, err
}

if err := validateEvent(ce); err != nil {
return pd, err
}

pd.RoutingKey = pdc.PagerDuty.RoutingKey
pd.EventAction = pdc.PagerDuty.EventAction
pd.Client = "VMware Event Broker Appliance"
pd.ClientURL = ce.Source
pd.Payload.Summary = ce.Data.FullFormattedMessage
pd.Payload.Timestamp = ce.Data.CreatedTime
pd.Payload.Source = ce.Source
pd.Payload.Severity = "info"
pd.Payload.Component = ce.Data.Vm.Name
pd.Payload.Group = ce.Data.Host.Name
pd.Payload.Class = ce.Subject
pd.Payload.CustomDetails.User = ce.Data.UserName
pd.Payload.CustomDetails.Datacenter = ce.Data.Datacenter
pd.Payload.CustomDetails.ComputeResource = ce.Data.ComputeResource
pd.Payload.CustomDetails.Host = ce.Data.Host
pd.Payload.CustomDetails.VM = ce.Data.Vm

return pd, nil
}

func validateEvent(event cloudEvent) error {
var msg string

switch true {
case event.Source == "":
msg = "does not contain Source"
case event.Subject == "":
msg = "does not contain Subject"
case event.Data.FullFormattedMessage == "":
msg = "does not contain Data.FullFormattedMessage"
case (event.Data.CreatedTime == time.Time{}):
msg = "does not contain Data.CreatedTime"
case event.Data.Vm.Name == "":
msg = "does not contain Data.Vm.Name"
case event.Data.Host.Name == "":
msg = "does not contain Data.Vm.Name"
}

if msg != "" {
return errors.New("invalid event: " + msg)
}

return nil
}

func pdSendRequest(ctx context.Context, path string, pdp pdPayload) ([]byte, error) {
clt := &http.Client{
Timeout: httpClientTimeout,
}

reqBody, err := json.Marshal(pdp)
if err != nil {
return []byte{}, err
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: These returns could be simplified with return nil, err

}

res, err := clt.Post(path, "application/json", bytes.NewBuffer(reqBody))
if res != nil {
defer res.Body.Close()
}

if err != nil {
return []byte{}, fmt.Errorf("execute http request %w", err)
}

body, err := ioutil.ReadAll(res.Body)
if err != nil {
return []byte{}, fmt.Errorf("read http response body: %w", err)
}

if res.StatusCode < 200 || res.StatusCode >= 400 {
return []byte{}, fmt.Errorf("http response: %d, %+v", res.StatusCode, string(body))
}

return body, nil
}
Loading