mirror of
https://github.com/micromdm/micromdm/
synced 2026-08-06 01:06:26 +08:00
organize essential APIs into platform, workflow and pkg folders (#337)
Add more logic to the way code is organized. /pkg -- library code not directly connected to micromdm /mdm -- packages meant for the services devices interract with. The MDM protocol. /dep -- DEP API and related packages. /platform -- Core APIs the server provides. Commands API, Devices API, queue, pubsub etc. /workflow -- Packages/API that build on top of platform. Today that's the webhook package. Depending on what ends up here, the workflow folder might become its own repository.
This commit is contained in:
34
platform/queue/command_queued.go
Normal file
34
platform/queue/command_queued.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/micromdm/micromdm/platform/queue/internal/commandqueuedproto"
|
||||
)
|
||||
|
||||
type QueueCommandQueued struct {
|
||||
DeviceUDID string
|
||||
CommandUUID string
|
||||
}
|
||||
|
||||
func MarshalQueuedCommand(cq *QueueCommandQueued) ([]byte, error) {
|
||||
if cq == nil {
|
||||
return nil, errors.New("marshalling nil QueueCommandQueued")
|
||||
}
|
||||
return proto.Marshal(&commandqueued.CommandQueued{
|
||||
DeviceUdid: cq.DeviceUDID,
|
||||
CommandUuid: cq.DeviceUDID,
|
||||
})
|
||||
}
|
||||
|
||||
func UnmarshalQueuedCommand(data []byte) (*QueueCommandQueued, error) {
|
||||
cmdQueued := commandqueued.CommandQueued{}
|
||||
if err := proto.Unmarshal(data, &cmdQueued); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queueCmdQueued := new(QueueCommandQueued)
|
||||
queueCmdQueued.DeviceUDID = cmdQueued.DeviceUdid
|
||||
queueCmdQueued.CommandUUID = cmdQueued.CommandUuid
|
||||
return queueCmdQueued, nil
|
||||
}
|
||||
174
platform/queue/device_command.go
Normal file
174
platform/queue/device_command.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/micromdm/micromdm/platform/queue/internal/devicecommandproto"
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
UUID string
|
||||
Payload []byte
|
||||
|
||||
CreatedAt time.Time
|
||||
LastSentAt time.Time
|
||||
Acknowledged time.Time
|
||||
|
||||
TimesSent int
|
||||
|
||||
LastStatus string
|
||||
FailureMessage []byte
|
||||
}
|
||||
|
||||
type DeviceCommand struct {
|
||||
DeviceUDID string
|
||||
Commands []Command
|
||||
|
||||
// These are going to scale great. We'll have to see.
|
||||
Completed []Command
|
||||
Failed []Command
|
||||
NotNow []Command
|
||||
}
|
||||
|
||||
func MarshalDeviceCommand(c *DeviceCommand) ([]byte, error) {
|
||||
protoc := devicecommandproto.DeviceCommand{
|
||||
DeviceUdid: c.DeviceUDID,
|
||||
}
|
||||
|
||||
// TODO add helper here to reduce copy/pasted boilerplate.
|
||||
for _, command := range c.Commands {
|
||||
protoc.Commands = append(protoc.Commands, &devicecommandproto.Command{
|
||||
Uuid: command.UUID,
|
||||
Payload: command.Payload,
|
||||
CreatedAt: command.CreatedAt.UnixNano(),
|
||||
LastSentAt: command.LastSentAt.UnixNano(),
|
||||
Acknowledged: command.Acknowledged.UnixNano(),
|
||||
|
||||
TimesSent: int64(command.TimesSent),
|
||||
|
||||
LastStatus: command.LastStatus,
|
||||
FailureMessage: command.FailureMessage,
|
||||
})
|
||||
}
|
||||
|
||||
for _, command := range c.Completed {
|
||||
protoc.Completed = append(protoc.Completed, &devicecommandproto.Command{
|
||||
Uuid: command.UUID,
|
||||
Payload: command.Payload,
|
||||
CreatedAt: command.CreatedAt.UnixNano(),
|
||||
LastSentAt: command.LastSentAt.UnixNano(),
|
||||
Acknowledged: command.Acknowledged.UnixNano(),
|
||||
|
||||
TimesSent: int64(command.TimesSent),
|
||||
|
||||
LastStatus: command.LastStatus,
|
||||
FailureMessage: command.FailureMessage,
|
||||
})
|
||||
}
|
||||
|
||||
for _, command := range c.Failed {
|
||||
protoc.Failed = append(protoc.Failed, &devicecommandproto.Command{
|
||||
Uuid: command.UUID,
|
||||
Payload: command.Payload,
|
||||
CreatedAt: command.CreatedAt.UnixNano(),
|
||||
LastSentAt: command.LastSentAt.UnixNano(),
|
||||
Acknowledged: command.Acknowledged.UnixNano(),
|
||||
|
||||
TimesSent: int64(command.TimesSent),
|
||||
|
||||
LastStatus: command.LastStatus,
|
||||
FailureMessage: command.FailureMessage,
|
||||
})
|
||||
}
|
||||
|
||||
for _, command := range c.NotNow {
|
||||
protoc.NotNow = append(protoc.NotNow, &devicecommandproto.Command{
|
||||
Uuid: command.UUID,
|
||||
Payload: command.Payload,
|
||||
CreatedAt: command.CreatedAt.UnixNano(),
|
||||
LastSentAt: command.LastSentAt.UnixNano(),
|
||||
Acknowledged: command.Acknowledged.UnixNano(),
|
||||
|
||||
TimesSent: int64(command.TimesSent),
|
||||
|
||||
LastStatus: command.LastStatus,
|
||||
FailureMessage: command.FailureMessage,
|
||||
})
|
||||
}
|
||||
return proto.Marshal(&protoc)
|
||||
}
|
||||
|
||||
func UnmarshalDeviceCommand(data []byte, c *DeviceCommand) error {
|
||||
var pb devicecommandproto.DeviceCommand
|
||||
if err := proto.Unmarshal(data, &pb); err != nil {
|
||||
return errors.Wrap(err, "unmarshal proto to DeviceCommand")
|
||||
}
|
||||
c.DeviceUDID = pb.GetDeviceUdid()
|
||||
protoCommands := pb.GetCommands()
|
||||
protoCommandsCompleted := pb.GetCompleted()
|
||||
protoCommandsFailed := pb.GetFailed()
|
||||
protoCommandsNotNow := pb.GetNotNow()
|
||||
for _, command := range protoCommands {
|
||||
c.Commands = append(c.Commands, Command{
|
||||
UUID: command.GetUuid(),
|
||||
Payload: command.GetPayload(),
|
||||
CreatedAt: time.Unix(0, command.GetCreatedAt()).UTC(),
|
||||
LastSentAt: time.Unix(0, command.GetLastSentAt()).UTC(),
|
||||
Acknowledged: time.Unix(0, command.GetAcknowledged()).UTC(),
|
||||
|
||||
TimesSent: int(command.TimesSent),
|
||||
|
||||
LastStatus: command.LastStatus,
|
||||
FailureMessage: command.FailureMessage,
|
||||
})
|
||||
}
|
||||
|
||||
for _, command := range protoCommandsCompleted {
|
||||
c.Completed = append(c.Completed, Command{
|
||||
UUID: command.GetUuid(),
|
||||
Payload: command.GetPayload(),
|
||||
CreatedAt: time.Unix(0, command.GetCreatedAt()).UTC(),
|
||||
LastSentAt: time.Unix(0, command.GetLastSentAt()).UTC(),
|
||||
Acknowledged: time.Unix(0, command.GetAcknowledged()).UTC(),
|
||||
|
||||
TimesSent: int(command.TimesSent),
|
||||
|
||||
LastStatus: command.LastStatus,
|
||||
FailureMessage: command.FailureMessage,
|
||||
})
|
||||
}
|
||||
|
||||
for _, command := range protoCommandsFailed {
|
||||
c.Failed = append(c.Failed, Command{
|
||||
UUID: command.GetUuid(),
|
||||
Payload: command.GetPayload(),
|
||||
CreatedAt: time.Unix(0, command.GetCreatedAt()).UTC(),
|
||||
LastSentAt: time.Unix(0, command.GetLastSentAt()).UTC(),
|
||||
Acknowledged: time.Unix(0, command.GetAcknowledged()).UTC(),
|
||||
|
||||
TimesSent: int(command.TimesSent),
|
||||
|
||||
LastStatus: command.LastStatus,
|
||||
FailureMessage: command.FailureMessage,
|
||||
})
|
||||
}
|
||||
|
||||
for _, command := range protoCommandsNotNow {
|
||||
c.NotNow = append(c.NotNow, Command{
|
||||
UUID: command.GetUuid(),
|
||||
Payload: command.GetPayload(),
|
||||
CreatedAt: time.Unix(0, command.GetCreatedAt()).UTC(),
|
||||
LastSentAt: time.Unix(0, command.GetLastSentAt()).UTC(),
|
||||
Acknowledged: time.Unix(0, command.GetAcknowledged()).UTC(),
|
||||
|
||||
TimesSent: int(command.TimesSent),
|
||||
|
||||
LastStatus: command.LastStatus,
|
||||
FailureMessage: command.FailureMessage,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Code generated by protoc-gen-go.
|
||||
// source: command_queued.proto
|
||||
// DO NOT EDIT!
|
||||
|
||||
/*
|
||||
Package commandqueued is a generated protocol buffer package.
|
||||
|
||||
It is generated from these files:
|
||||
command_queued.proto
|
||||
|
||||
It has these top-level messages:
|
||||
CommandQueued
|
||||
*/
|
||||
package commandqueued
|
||||
|
||||
import proto "github.com/golang/protobuf/proto"
|
||||
import fmt "fmt"
|
||||
import math "math"
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
var _ = fmt.Errorf
|
||||
var _ = math.Inf
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the proto package it is being compiled against.
|
||||
// A compilation error at this line likely means your copy of the
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
|
||||
|
||||
type CommandQueued struct {
|
||||
DeviceUdid string `protobuf:"bytes,1,opt,name=device_udid,json=deviceUdid" json:"device_udid,omitempty"`
|
||||
CommandUuid string `protobuf:"bytes,2,opt,name=command_uuid,json=commandUuid" json:"command_uuid,omitempty"`
|
||||
}
|
||||
|
||||
func (m *CommandQueued) Reset() { *m = CommandQueued{} }
|
||||
func (m *CommandQueued) String() string { return proto.CompactTextString(m) }
|
||||
func (*CommandQueued) ProtoMessage() {}
|
||||
func (*CommandQueued) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
|
||||
|
||||
func (m *CommandQueued) GetDeviceUdid() string {
|
||||
if m != nil {
|
||||
return m.DeviceUdid
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *CommandQueued) GetCommandUuid() string {
|
||||
if m != nil {
|
||||
return m.CommandUuid
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*CommandQueued)(nil), "commandqueued.CommandQueued")
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("command_queued.proto", fileDescriptor0) }
|
||||
|
||||
var fileDescriptor0 = []byte{
|
||||
// 117 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x49, 0xce, 0xcf, 0xcd,
|
||||
0x4d, 0xcc, 0x4b, 0x89, 0x2f, 0x2c, 0x4d, 0x2d, 0x4d, 0x4d, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9,
|
||||
0x17, 0xe2, 0x85, 0x8a, 0x42, 0x04, 0x95, 0x82, 0xb9, 0x78, 0x9d, 0x21, 0x02, 0x81, 0x60, 0x01,
|
||||
0x21, 0x79, 0x2e, 0xee, 0x94, 0xd4, 0xb2, 0xcc, 0xe4, 0xd4, 0xf8, 0xd2, 0x94, 0xcc, 0x14, 0x09,
|
||||
0x46, 0x05, 0x46, 0x0d, 0xce, 0x20, 0x2e, 0x88, 0x50, 0x68, 0x4a, 0x66, 0x8a, 0x90, 0x22, 0x17,
|
||||
0x0f, 0xcc, 0xe0, 0xd2, 0xd2, 0xcc, 0x14, 0x09, 0x26, 0xb0, 0x0a, 0x6e, 0xa8, 0x58, 0x68, 0x69,
|
||||
0x66, 0x4a, 0x12, 0x1b, 0xd8, 0x2a, 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0x6e, 0x34, 0x2e,
|
||||
0x49, 0x82, 0x00, 0x00, 0x00,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package commandqueued;
|
||||
|
||||
message CommandQueued {
|
||||
string device_udid = 1;
|
||||
string command_uuid = 2;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package devicecommandproto
|
||||
|
||||
//go:generate protoc --go_out=. device_command.proto
|
||||
181
platform/queue/internal/devicecommandproto/device_command.pb.go
Normal file
181
platform/queue/internal/devicecommandproto/device_command.pb.go
Normal file
@@ -0,0 +1,181 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// source: device_command.proto
|
||||
|
||||
/*
|
||||
Package devicecommandproto is a generated protocol buffer package.
|
||||
|
||||
It is generated from these files:
|
||||
device_command.proto
|
||||
|
||||
It has these top-level messages:
|
||||
Command
|
||||
DeviceCommand
|
||||
*/
|
||||
package devicecommandproto
|
||||
|
||||
import proto "github.com/golang/protobuf/proto"
|
||||
import fmt "fmt"
|
||||
import math "math"
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
var _ = fmt.Errorf
|
||||
var _ = math.Inf
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the proto package it is being compiled against.
|
||||
// A compilation error at this line likely means your copy of the
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
|
||||
|
||||
type Command struct {
|
||||
Uuid string `protobuf:"bytes,1,opt,name=uuid" json:"uuid,omitempty"`
|
||||
Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"`
|
||||
CreatedAt int64 `protobuf:"varint,3,opt,name=created_at,json=createdAt" json:"created_at,omitempty"`
|
||||
LastSentAt int64 `protobuf:"varint,4,opt,name=last_sent_at,json=lastSentAt" json:"last_sent_at,omitempty"`
|
||||
Acknowledged int64 `protobuf:"varint,5,opt,name=acknowledged" json:"acknowledged,omitempty"`
|
||||
TimesSent int64 `protobuf:"varint,6,opt,name=times_sent,json=timesSent" json:"times_sent,omitempty"`
|
||||
LastStatus string `protobuf:"bytes,7,opt,name=last_status,json=lastStatus" json:"last_status,omitempty"`
|
||||
FailureMessage []byte `protobuf:"bytes,8,opt,name=failure_message,json=failureMessage,proto3" json:"failure_message,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Command) Reset() { *m = Command{} }
|
||||
func (m *Command) String() string { return proto.CompactTextString(m) }
|
||||
func (*Command) ProtoMessage() {}
|
||||
func (*Command) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
|
||||
|
||||
func (m *Command) GetUuid() string {
|
||||
if m != nil {
|
||||
return m.Uuid
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Command) GetPayload() []byte {
|
||||
if m != nil {
|
||||
return m.Payload
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Command) GetCreatedAt() int64 {
|
||||
if m != nil {
|
||||
return m.CreatedAt
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *Command) GetLastSentAt() int64 {
|
||||
if m != nil {
|
||||
return m.LastSentAt
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *Command) GetAcknowledged() int64 {
|
||||
if m != nil {
|
||||
return m.Acknowledged
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *Command) GetTimesSent() int64 {
|
||||
if m != nil {
|
||||
return m.TimesSent
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *Command) GetLastStatus() string {
|
||||
if m != nil {
|
||||
return m.LastStatus
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Command) GetFailureMessage() []byte {
|
||||
if m != nil {
|
||||
return m.FailureMessage
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DeviceCommand struct {
|
||||
DeviceUdid string `protobuf:"bytes,1,opt,name=device_udid,json=deviceUdid" json:"device_udid,omitempty"`
|
||||
Commands []*Command `protobuf:"bytes,2,rep,name=commands" json:"commands,omitempty"`
|
||||
Completed []*Command `protobuf:"bytes,3,rep,name=completed" json:"completed,omitempty"`
|
||||
Failed []*Command `protobuf:"bytes,4,rep,name=failed" json:"failed,omitempty"`
|
||||
NotNow []*Command `protobuf:"bytes,5,rep,name=not_now,json=notNow" json:"not_now,omitempty"`
|
||||
}
|
||||
|
||||
func (m *DeviceCommand) Reset() { *m = DeviceCommand{} }
|
||||
func (m *DeviceCommand) String() string { return proto.CompactTextString(m) }
|
||||
func (*DeviceCommand) ProtoMessage() {}
|
||||
func (*DeviceCommand) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
|
||||
|
||||
func (m *DeviceCommand) GetDeviceUdid() string {
|
||||
if m != nil {
|
||||
return m.DeviceUdid
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *DeviceCommand) GetCommands() []*Command {
|
||||
if m != nil {
|
||||
return m.Commands
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *DeviceCommand) GetCompleted() []*Command {
|
||||
if m != nil {
|
||||
return m.Completed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *DeviceCommand) GetFailed() []*Command {
|
||||
if m != nil {
|
||||
return m.Failed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *DeviceCommand) GetNotNow() []*Command {
|
||||
if m != nil {
|
||||
return m.NotNow
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*Command)(nil), "devicecommandproto.Command")
|
||||
proto.RegisterType((*DeviceCommand)(nil), "devicecommandproto.DeviceCommand")
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("device_command.proto", fileDescriptor0) }
|
||||
|
||||
var fileDescriptor0 = []byte{
|
||||
// 324 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x90, 0x41, 0x4b, 0xeb, 0x40,
|
||||
0x10, 0xc7, 0x49, 0xd2, 0x36, 0xed, 0xb4, 0xef, 0x3d, 0x18, 0xde, 0x61, 0xe1, 0xf1, 0x68, 0xe8,
|
||||
0xc5, 0x9c, 0x7a, 0xb0, 0x82, 0x78, 0x2c, 0x7a, 0xd5, 0x43, 0xc4, 0x73, 0x58, 0xb3, 0x63, 0x09,
|
||||
0x26, 0xbb, 0xa5, 0x3b, 0xb1, 0xf8, 0x01, 0x3c, 0xfb, 0x95, 0x25, 0xbb, 0xdb, 0x8a, 0x78, 0xa8,
|
||||
0xb7, 0xe4, 0xc7, 0x7f, 0xe6, 0x3f, 0xfb, 0x83, 0xbf, 0x8a, 0x5e, 0xea, 0x8a, 0xca, 0xca, 0xb4,
|
||||
0xad, 0xd4, 0x6a, 0xb9, 0xdd, 0x19, 0x36, 0x88, 0x9e, 0x06, 0xe8, 0xd8, 0xe2, 0x2d, 0x86, 0xf4,
|
||||
0xda, 0x03, 0x44, 0x18, 0x74, 0x5d, 0xad, 0x44, 0x94, 0x45, 0xf9, 0xa4, 0x70, 0xdf, 0x28, 0x20,
|
||||
0xdd, 0xca, 0xd7, 0xc6, 0x48, 0x25, 0xe2, 0x2c, 0xca, 0x67, 0xc5, 0xe1, 0x17, 0xff, 0x03, 0x54,
|
||||
0x3b, 0x92, 0x4c, 0xaa, 0x94, 0x2c, 0x92, 0x2c, 0xca, 0x93, 0x62, 0x12, 0xc8, 0x9a, 0x31, 0x83,
|
||||
0x59, 0x23, 0x2d, 0x97, 0x96, 0x34, 0xf7, 0x81, 0x81, 0x0b, 0x40, 0xcf, 0xee, 0x49, 0xf3, 0x9a,
|
||||
0x71, 0x01, 0x33, 0x59, 0x3d, 0x6b, 0xb3, 0x6f, 0x48, 0x6d, 0x48, 0x89, 0xa1, 0x4b, 0x7c, 0x61,
|
||||
0x7d, 0x09, 0xd7, 0x2d, 0x59, 0xb7, 0x46, 0x8c, 0x7c, 0x89, 0x23, 0xfd, 0x12, 0x9c, 0xc3, 0xd4,
|
||||
0x97, 0xb0, 0xe4, 0xce, 0x8a, 0xd4, 0x1d, 0xee, 0x3b, 0x1c, 0xc1, 0x33, 0xf8, 0xf3, 0x24, 0xeb,
|
||||
0xa6, 0xdb, 0x51, 0xd9, 0x92, 0xb5, 0x72, 0x43, 0x62, 0xec, 0x9e, 0xf1, 0x3b, 0xe0, 0x5b, 0x4f,
|
||||
0x17, 0xef, 0x31, 0xfc, 0xba, 0x71, 0x7a, 0x0e, 0x36, 0xe6, 0x30, 0x0d, 0x16, 0x3b, 0x75, 0x94,
|
||||
0x02, 0x1e, 0x3d, 0xa8, 0x5a, 0xe1, 0x25, 0x8c, 0x83, 0x4a, 0x2b, 0xe2, 0x2c, 0xc9, 0xa7, 0xe7,
|
||||
0xff, 0x96, 0xdf, 0x0d, 0x2f, 0xc3, 0xbe, 0xe2, 0x18, 0xc6, 0x2b, 0x98, 0x54, 0xa6, 0xdd, 0x36,
|
||||
0xc4, 0xa4, 0x44, 0x72, 0x7a, 0xf2, 0x33, 0x8d, 0x2b, 0x18, 0xf5, 0x87, 0x93, 0x12, 0x83, 0xd3,
|
||||
0x73, 0x21, 0x8a, 0x17, 0x90, 0x6a, 0xc3, 0xa5, 0x36, 0x7b, 0x31, 0xfc, 0xc1, 0x94, 0x36, 0x7c,
|
||||
0x67, 0xf6, 0x8f, 0x23, 0x87, 0x57, 0x1f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x83, 0x8d, 0x73, 0x78,
|
||||
0x4c, 0x02, 0x00, 0x00,
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package devicecommandproto;
|
||||
|
||||
message Command {
|
||||
string uuid = 1;
|
||||
bytes payload = 2;
|
||||
|
||||
int64 created_at = 3;
|
||||
int64 last_sent_at = 4;
|
||||
int64 acknowledged = 5;
|
||||
|
||||
int64 times_sent = 6;
|
||||
|
||||
string last_status = 7;
|
||||
bytes failure_message = 8;
|
||||
}
|
||||
|
||||
message DeviceCommand {
|
||||
string device_udid = 1;
|
||||
repeated Command commands = 2;
|
||||
repeated Command completed = 3;
|
||||
repeated Command failed = 4;
|
||||
repeated Command not_now = 5;
|
||||
}
|
||||
246
platform/queue/queue.go
Normal file
246
platform/queue/queue.go
Normal file
@@ -0,0 +1,246 @@
|
||||
// Package queue implements a boldDB backed queue for MDM Commands.
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/groob/plist"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/micromdm/mdm"
|
||||
"github.com/micromdm/micromdm/platform/command"
|
||||
"github.com/micromdm/micromdm/platform/pubsub"
|
||||
)
|
||||
|
||||
const (
|
||||
DeviceCommandBucket = "mdm.DeviceCommands"
|
||||
|
||||
CommandQueuedTopic = "mdm.CommandQueued"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
*bolt.DB
|
||||
}
|
||||
|
||||
func (db *Store) Next(ctx context.Context, resp mdm.Response) (*Command, error) {
|
||||
udid := resp.UDID
|
||||
if resp.UserID != nil {
|
||||
// use the user id for user level commands
|
||||
udid = *resp.UserID
|
||||
}
|
||||
dc, err := db.DeviceCommand(udid)
|
||||
if err != nil {
|
||||
if isNotFound(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.Wrapf(err, "get device command from queue, udid: %s", resp.UDID)
|
||||
}
|
||||
|
||||
var cmd *Command
|
||||
switch resp.Status {
|
||||
case "NotNow":
|
||||
// We will try this command later when the device is not
|
||||
// responding with NotNow
|
||||
x, a := cut(dc.Commands, resp.CommandUUID)
|
||||
dc.Commands = a
|
||||
if x == nil {
|
||||
break
|
||||
}
|
||||
dc.NotNow = append(dc.NotNow, *x)
|
||||
|
||||
case "Acknowledged":
|
||||
// move to completed, send next
|
||||
x, a := cut(dc.Commands, resp.CommandUUID)
|
||||
dc.Commands = a
|
||||
if x == nil {
|
||||
break
|
||||
}
|
||||
dc.Completed = append(dc.Completed, *x)
|
||||
case "Error":
|
||||
// move to failed, send next
|
||||
x, a := cut(dc.Commands, resp.CommandUUID)
|
||||
dc.Commands = a
|
||||
if x == nil { // must've already bin ackd
|
||||
break
|
||||
}
|
||||
dc.Failed = append(dc.Failed, *x)
|
||||
|
||||
case "CommandFormatError":
|
||||
// move to failed
|
||||
x, a := cut(dc.Commands, resp.CommandUUID)
|
||||
dc.Commands = a
|
||||
if x == nil {
|
||||
break
|
||||
}
|
||||
dc.Failed = append(dc.Failed, *x)
|
||||
|
||||
case "Idle":
|
||||
// will send next command below
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown response status: %s", resp.Status)
|
||||
}
|
||||
|
||||
// pop the first command from the queue and add it to the end.
|
||||
// If the regular queue is empty, send a command that got
|
||||
// refused with NotNow before.
|
||||
cmd, dc.Commands = popFirst(dc.Commands)
|
||||
if cmd != nil {
|
||||
dc.Commands = append(dc.Commands, *cmd)
|
||||
} else if resp.Status != "NotNow" {
|
||||
cmd, dc.NotNow = popFirst(dc.NotNow)
|
||||
if cmd != nil {
|
||||
dc.Commands = append(dc.Commands, *cmd)
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.Save(dc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func popFirst(all []Command) (*Command, []Command) {
|
||||
if len(all) == 0 {
|
||||
return nil, all
|
||||
}
|
||||
first := all[0]
|
||||
all = append(all[:0], all[1:]...)
|
||||
return &first, all
|
||||
}
|
||||
|
||||
func cut(all []Command, uuid string) (*Command, []Command) {
|
||||
for i, cmd := range all {
|
||||
if cmd.UUID == uuid {
|
||||
all = append(all[:i], all[i+1:]...)
|
||||
return &cmd, all
|
||||
}
|
||||
}
|
||||
return nil, all
|
||||
}
|
||||
|
||||
func NewQueue(db *bolt.DB, pubsub pubsub.PublishSubscriber) (*Store, error) {
|
||||
err := db.Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(DeviceCommandBucket))
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "creating %s bucket", DeviceCommandBucket)
|
||||
}
|
||||
datastore := &Store{DB: db}
|
||||
if err := datastore.pollCommands(pubsub); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return datastore, nil
|
||||
}
|
||||
|
||||
func (db *Store) Save(cmd *DeviceCommand) error {
|
||||
tx, err := db.DB.Begin(true)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "begin transaction")
|
||||
}
|
||||
bkt := tx.Bucket([]byte(DeviceCommandBucket))
|
||||
if bkt == nil {
|
||||
return fmt.Errorf("bucket %q not found!", DeviceCommandBucket)
|
||||
}
|
||||
devproto, err := MarshalDeviceCommand(cmd)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshalling DeviceCommand")
|
||||
}
|
||||
key := []byte(cmd.DeviceUDID)
|
||||
if err := bkt.Put(key, devproto); err != nil {
|
||||
return errors.Wrap(err, "put DeviceCommand to boltdb")
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (db *Store) DeviceCommand(udid string) (*DeviceCommand, error) {
|
||||
var dev DeviceCommand
|
||||
err := db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(DeviceCommandBucket))
|
||||
v := b.Get([]byte(udid))
|
||||
if v == nil {
|
||||
return ¬Found{"DeviceCommand", fmt.Sprintf("udid %s", udid)}
|
||||
}
|
||||
return UnmarshalDeviceCommand(v, &dev)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dev, nil
|
||||
}
|
||||
|
||||
type notFound struct {
|
||||
ResourceType string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *notFound) Error() string {
|
||||
return fmt.Sprintf("not found: %s %s", e.ResourceType, e.Message)
|
||||
}
|
||||
|
||||
func (db *Store) pollCommands(pubsub pubsub.PublishSubscriber) error {
|
||||
commandEvents, err := pubsub.Subscribe(context.TODO(), "command-queue", command.CommandTopic)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err,
|
||||
"subscribing push to %s topic", command.CommandTopic)
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event := <-commandEvents:
|
||||
var ev command.Event
|
||||
if err := command.UnmarshalEvent(event.Message, &ev); err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
cmd := new(DeviceCommand)
|
||||
cmd.DeviceUDID = ev.DeviceUDID
|
||||
byUDID, err := db.DeviceCommand(ev.DeviceUDID)
|
||||
if err == nil && byUDID != nil {
|
||||
cmd = byUDID
|
||||
}
|
||||
newPayload, err := plist.Marshal(&ev.Payload)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
newCmd := Command{
|
||||
UUID: ev.Payload.CommandUUID,
|
||||
Payload: newPayload,
|
||||
}
|
||||
cmd.Commands = append(cmd.Commands, newCmd)
|
||||
if err := db.Save(cmd); err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("queued event for device: %s\n", ev.DeviceUDID)
|
||||
|
||||
cq := new(QueueCommandQueued)
|
||||
cq.DeviceUDID = ev.DeviceUDID
|
||||
cq.CommandUUID = ev.Payload.CommandUUID
|
||||
|
||||
msgBytes, err := MarshalQueuedCommand(cq)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
pubsub.Publish(context.TODO(), CommandQueuedTopic, msgBytes)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isNotFound(err error) bool {
|
||||
if _, ok := err.(*notFound); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
178
platform/queue/queue_test.go
Normal file
178
platform/queue/queue_test.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/micromdm/mdm"
|
||||
)
|
||||
|
||||
func TestNext_Error(t *testing.T) {
|
||||
store, teardown := setupDB(t)
|
||||
defer teardown()
|
||||
|
||||
dc := &DeviceCommand{DeviceUDID: "TestDevice"}
|
||||
dc.Commands = append(dc.Commands, Command{UUID: "xCmd"})
|
||||
dc.Commands = append(dc.Commands, Command{UUID: "yCmd"})
|
||||
dc.Commands = append(dc.Commands, Command{UUID: "zCmd"})
|
||||
if err := store.Save(dc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
resp := mdm.Response{
|
||||
UDID: dc.DeviceUDID,
|
||||
CommandUUID: "xCmd",
|
||||
Status: "Error",
|
||||
}
|
||||
for range dc.Commands {
|
||||
cmd, err := store.Next(ctx, resp)
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil, but got err: %s", err)
|
||||
}
|
||||
if cmd == nil {
|
||||
t.Fatal("expected cmd but got nil")
|
||||
}
|
||||
|
||||
if have, errd := cmd.UUID, resp.CommandUUID; have == errd {
|
||||
t.Error("got back command which previously failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNext_NotNow(t *testing.T) {
|
||||
store, teardown := setupDB(t)
|
||||
defer teardown()
|
||||
|
||||
dc := &DeviceCommand{DeviceUDID: "TestDevice"}
|
||||
dc.Commands = append(dc.Commands, Command{UUID: "xCmd"})
|
||||
dc.Commands = append(dc.Commands, Command{UUID: "yCmd"})
|
||||
if err := store.Save(dc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
tf := func(t *testing.T) {
|
||||
|
||||
|
||||
resp := mdm.Response{
|
||||
UDID: dc.DeviceUDID,
|
||||
CommandUUID: "yCmd",
|
||||
Status: "NotNow",
|
||||
}
|
||||
cmd, err := store.Next(ctx, resp)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil, but got err: %s", err)
|
||||
}
|
||||
|
||||
resp = mdm.Response{
|
||||
UDID: dc.DeviceUDID,
|
||||
CommandUUID: cmd.UUID,
|
||||
Status: "NotNow",
|
||||
}
|
||||
|
||||
cmd, err = store.Next(ctx, resp)
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil, but got err: %s", err)
|
||||
}
|
||||
if cmd != nil {
|
||||
t.Error("Got back a notnowed command.")
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("withManyCommands", tf)
|
||||
dc.Commands = []Command{{UUID: "xCmd"}}
|
||||
if err := store.Save(dc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Run("withOneCommand", tf)
|
||||
}
|
||||
|
||||
func TestNext_Idle(t *testing.T) {
|
||||
store, teardown := setupDB(t)
|
||||
defer teardown()
|
||||
|
||||
dc := &DeviceCommand{DeviceUDID: "TestDevice"}
|
||||
dc.Commands = append(dc.Commands, Command{UUID: "xCmd"})
|
||||
dc.Commands = append(dc.Commands, Command{UUID: "yCmd"})
|
||||
dc.Commands = append(dc.Commands, Command{UUID: "zCmd"})
|
||||
if err := store.Save(dc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
resp := mdm.Response{
|
||||
UDID: dc.DeviceUDID,
|
||||
CommandUUID: "xCmd",
|
||||
Status: "Idle",
|
||||
}
|
||||
for i, _ := range dc.Commands {
|
||||
cmd, err := store.Next(ctx, resp)
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil, but got err: %s", err)
|
||||
}
|
||||
if cmd == nil {
|
||||
t.Fatal("expected cmd but got nil")
|
||||
}
|
||||
|
||||
if have, want := cmd.UUID, dc.Commands[i].UUID; have != want {
|
||||
t.Errorf("have %s, want %s, index %d", have, want, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNext_zeroCommands(t *testing.T) {
|
||||
store, teardown := setupDB(t)
|
||||
defer teardown()
|
||||
|
||||
dc := &DeviceCommand{DeviceUDID: "TestDevice"}
|
||||
if err := store.Save(dc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var allStatuses = []string{
|
||||
"Acknowledged",
|
||||
"NotNow",
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
for _, s := range allStatuses {
|
||||
t.Run(s, func(t *testing.T) {
|
||||
resp := mdm.Response{CommandUUID: s, Status: s}
|
||||
cmd, err := store.Next(ctx, resp)
|
||||
if err != nil {
|
||||
t.Errorf("expected nil, but got err: %s", err)
|
||||
}
|
||||
if cmd != nil {
|
||||
t.Errorf("expected nil cmd but got %s", cmd.UUID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func setupDB(t *testing.T) (*Store, func()) {
|
||||
f, _ := ioutil.TempFile("", "bolt-")
|
||||
teardown := func() {
|
||||
f.Close()
|
||||
os.Remove(f.Name())
|
||||
}
|
||||
|
||||
db, err := bolt.Open(f.Name(), 0777, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("couldn't open bolt, err %s\n", err)
|
||||
}
|
||||
err = db.Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(DeviceCommandBucket))
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store := &Store{db}
|
||||
return store, teardown
|
||||
}
|
||||
Reference in New Issue
Block a user