Skip to content

Instantly share code, notes, and snippets.

@bearmini
Created December 3, 2019 03:53
Show Gist options
  • Save bearmini/99e99ead08ed0e8aff39c6379f5aeeae to your computer and use it in GitHub Desktop.
Save bearmini/99e99ead08ed0e8aff39c6379f5aeeae to your computer and use it in GitHub Desktop.
AWS Lambda code for SORACOM LTE-M Button powered by AWS - Start/stop instances
package main
import (
"encoding/json"
"strings"
"fmt"
"os"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/pkg/errors"
"github.com/soracom/common-utils-go/slack"
)
type ButtonEvent struct {
DeviceEvent DeviceEvent `json:"deviceEvent"`
DeviceInfo DeviceInfo `json:"deviceInfo"`
PlacementInfo PlacementInfo `json:"placementInfo"`
}
type DeviceEvent struct {
ButtonClicked ButtonClicked `json:"buttonClicked"`
}
type ButtonClicked struct {
ClickType string `json:"clickType"`
ReportedTime string `json:"reportedTime"`
}
type DeviceInfo struct {
Attributes DeviceAttributes `json:"attributes"`
DeviceID string `json:"deviceId"`
RemainingLife float32 `json:"remainingLife"`
Type string `json:"type"`
}
type DeviceAttributes struct {
DeviceTemplateName string `json:"deviceTemplateName"`
PlacementName string `json:"placementName"`
ProjectName string `json:"projectName"`
ProjectRegion string `json:"projectRegion"`
}
type PlacementInfo struct {
Attributes map[string]string `json:"attributes"`
Devices map[string]string `json:"devices"`
PlacementName string `json:"placementName"`
ProjectName string `json:"projectName"`
}
func main() {
log("starting process")
lambda.Start(handleFunc)
log("exiting process ...")
}
//func handleFunc(ibe *events.IoTButtonEvent) error {
func handleFunc(be ButtonEvent) error {
log("event: %+v", toJSON(be))
var err error
switch strings.ToLower(be.DeviceEvent.ButtonClicked.ClickType) {
case "single":
err = onSingleClick(be)
case "double":
err = onDoubleClick(be)
case "long":
err = onLongClick(be)
default:
err = errors.Errorf("unknown click type: %s", be.DeviceEvent.ButtonClicked.ClickType)
}
if err != nil {
return reportError(be, err)
}
return nil
}
func onSingleClick(be ButtonEvent) error {
state, err := getInstanceState(be)
if err != nil {
return err
}
return reportInstanceStatus(be, state)
}
func onDoubleClick(be ButtonEvent) error {
state, err := startInstance(be)
if err != nil {
return err
}
return reportInstanceStatus(be, state)
}
func onLongClick(be ButtonEvent) error {
state, err := stopInstance(be)
if err != nil {
return err
}
return reportInstanceStatus(be, state)
}
func startInstance(be ButtonEvent) (InstanceState, error) {
instanceID := be.PlacementInfo.Attributes["instance-id"]
region := be.PlacementInfo.Attributes["instance-region"]
rc := ec2.New(session.New(&aws.Config{
Region: aws.String(region),
}))
in := &ec2.StartInstancesInput{
InstanceIds: []*string{aws.String(instanceID)},
}
out, err := rc.StartInstances(in)
if err != nil {
return 0, err
}
for _, si := range out.StartingInstances {
if *si.InstanceId == instanceID {
return InstanceState(*si.CurrentState.Code), nil
}
}
return 0, errors.New("starting instance not found")
}
func stopInstance(be ButtonEvent) (InstanceState, error) {
instanceID := be.PlacementInfo.Attributes["instance-id"]
region := be.PlacementInfo.Attributes["instance-region"]
rc := ec2.New(session.New(&aws.Config{
Region: aws.String(region),
}))
in := &ec2.StopInstancesInput{
InstanceIds: []*string{aws.String(instanceID)},
}
out, err := rc.StopInstances(in)
if err != nil {
return 0, err
}
for _, si := range out.StoppingInstances {
if *si.InstanceId == instanceID {
return InstanceState(*si.CurrentState.Code), nil
}
}
return 0, errors.New("stopping instance not found")
}
func reportInstanceStatus(be ButtonEvent, state InstanceState) error {
url := be.PlacementInfo.Attributes["slack-webhook-url"]
return slack.PostTo(url, slack.Message{
Attachments: []slack.Attachment{
{
Color: "good",
Fields: []slack.AttachmentField{
{
Title: "Current state",
Value: fmt.Sprintf("%s", instanceStateToString(state)),
},
},
},
},
})
}
type InstanceState uint8
const (
InstanceStatePending InstanceState = 0
InstanceStateRunning = 16
InstanceStateShuttingDown = 32
InstanceStateTerminated = 48
InstanceStateStopping = 64
InstanceStateStopped = 80
)
func instanceStateToString(state InstanceState) string {
switch state {
case InstanceStatePending: return "pending"
case InstanceStateRunning: return "running"
case InstanceStateShuttingDown: return "shutting down"
case InstanceStateTerminated: return "terminated"
case InstanceStateStopping: return "stopping"
case InstanceStateStopped: return "stopped"
default: return "(unknown)"
}
}
func getInstanceState(be ButtonEvent) (InstanceState, error) {
instanceID := be.PlacementInfo.Attributes["instance-id"]
region := be.PlacementInfo.Attributes["instance-region"]
rc := ec2.New(session.New(&aws.Config{
Region: aws.String(region),
}))
in := &ec2.DescribeInstanceStatusInput{
IncludeAllInstances: aws.Bool(true),
InstanceIds: []*string{aws.String(instanceID)},
}
out, err := rc.DescribeInstanceStatus(in)
if err != nil {
log("ERROR: ec2.DescribeInstanceStatus() - %+v", err)
return 0, err
}
for _, is := range out.InstanceStatuses {
if *is.InstanceId == instanceID {
return InstanceState(uint8(*is.InstanceState.Code)), nil
}
}
return 0, errors.New("unable to get instance state")
}
func reportError(be ButtonEvent, err error) error {
url := be.PlacementInfo.Attributes["slack-webhook-url"]
return slack.PostTo(url, slack.Message{
Attachments: []slack.Attachment{
{
Color: "danger",
Fields: []slack.AttachmentField{
{
Title: "Error",
Value: fmt.Sprintf("%+v", err),
},
},
},
},
})
}
func toJSON(x interface{}) string {
b, err := json.Marshal(x)
if err != nil {
return fmt.Sprintf("error: %+v", err)
}
return string(b)
}
func log(format string, v ...interface{}) {
fmt.Fprintf(os.Stderr, format+"\n", v...)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment