mirror of
https://github.com/micromdm/micromdm/
synced 2026-08-13 14:05:42 +08:00
add list devices api (#114)
This commit is contained in:
59
cmd/mdmctl/list.go
Normal file
59
cmd/mdmctl/list.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/go-kit/kit/log"
|
||||
"github.com/micromdm/micromdm/core/list"
|
||||
)
|
||||
|
||||
type tableOutput struct{ w *tabwriter.Writer }
|
||||
|
||||
func (out *tableOutput) BasicHeader() {
|
||||
fmt.Fprintf(out.w, "UDID\tSerialNumber\tEnrollmentStatus\tLastSeen\n")
|
||||
}
|
||||
|
||||
func (out *tableOutput) BasicFooter() {
|
||||
out.w.Flush()
|
||||
}
|
||||
|
||||
func (out *tableOutput) BasicLine(line ...string) {
|
||||
for _, l := range line {
|
||||
fmt.Fprintf(out.w, l+"\t")
|
||||
}
|
||||
fmt.Fprintf(out.w, "\n")
|
||||
}
|
||||
|
||||
func listDevices(args []string) error {
|
||||
if len(args) == 0 || args[0] != "devices" {
|
||||
return errors.New("must specify resource name as an argument (ex: devices)")
|
||||
}
|
||||
w := tabwriter.NewWriter(os.Stderr, 0, 4, 2, ' ', 0)
|
||||
out := &tableOutput{w}
|
||||
out.BasicHeader()
|
||||
defer out.BasicFooter()
|
||||
logger := log.NewLogfmtLogger(os.Stderr)
|
||||
// TODO needs some config for authentication and server url client side.
|
||||
instance := os.Getenv("MICROMDM_SERVER_URL")
|
||||
if instance == "" {
|
||||
return errors.New("MICROMDM_SERVER_URL not set")
|
||||
}
|
||||
svc, err := list.NewClient(instance, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
devices, err := svc.ListDevices(ctx, list.ListDevicesOption{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, d := range devices {
|
||||
fmt.Fprintf(out.w, "%s\t%s\t%v\t%s\n", d.UDID, d.SerialNumber, d.EnrollmentStatus, d.LastSeen)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
63
cmd/mdmctl/mdmdctl.go
Normal file
63
cmd/mdmctl/mdmdctl.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/micromdm/micromdm/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
var run func([]string) error
|
||||
switch strings.ToLower(os.Args[1]) {
|
||||
case "version", "-version":
|
||||
version.Print()
|
||||
return
|
||||
case "list":
|
||||
run = listDevices
|
||||
default:
|
||||
usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := run(os.Args[2:]); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func usage() error {
|
||||
helpText := `USAGE: mdmctl <COMMAND>
|
||||
|
||||
Available Commands:
|
||||
list
|
||||
version
|
||||
|
||||
Use micromdm <command> -h for additional usage of each command.
|
||||
Example: micromdm serve -h
|
||||
`
|
||||
fmt.Println(helpText)
|
||||
return nil
|
||||
}
|
||||
|
||||
func usageFor(fs *flag.FlagSet, short string) func() {
|
||||
return func() {
|
||||
fmt.Fprintf(os.Stderr, "USAGE\n")
|
||||
fmt.Fprintf(os.Stderr, " %s\n", short)
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
fmt.Fprintf(os.Stderr, "FLAGS\n")
|
||||
w := tabwriter.NewWriter(os.Stderr, 0, 2, 2, ' ', 0)
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
fmt.Fprintf(w, "\t-%s %s\t%s\n", f.Name, f.DefValue, f.Usage)
|
||||
})
|
||||
w.Flush()
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
}
|
||||
}
|
||||
36
core/list/client.go
Normal file
36
core/list/client.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/go-kit/kit/log"
|
||||
httptransport "github.com/go-kit/kit/transport/http"
|
||||
)
|
||||
|
||||
func NewClient(instance string, logger log.Logger) (Service, error) {
|
||||
u, err := url.Parse(instance)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var listDevicesEndpoint endpoint.Endpoint
|
||||
{
|
||||
listDevicesEndpoint = httptransport.NewClient(
|
||||
"GET",
|
||||
copyURL(u, "/v1/devices"),
|
||||
EncodeHTTPGenericRequest,
|
||||
DecodeDevicesResponse,
|
||||
).Endpoint()
|
||||
}
|
||||
|
||||
return Endpoints{
|
||||
ListDevicesEndpoint: listDevicesEndpoint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func copyURL(base *url.URL, path string) *url.URL {
|
||||
next := *base
|
||||
next.Path = path
|
||||
return &next
|
||||
}
|
||||
45
core/list/endpoint.go
Normal file
45
core/list/endpoint.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
type Endpoints struct {
|
||||
ListDevicesEndpoint endpoint.Endpoint
|
||||
}
|
||||
|
||||
func (e Endpoints) ListDevices(ctx context.Context, opts ListDevicesOption) ([]DeviceDTO, error) {
|
||||
request := devicesRequest{opts}
|
||||
response, err := e.ListDevicesEndpoint(ctx, request.Opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return response.(devicesResponse).Devices, response.(devicesResponse).Err
|
||||
}
|
||||
|
||||
func MakeListDevicesEndpoint(svc Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
|
||||
req := request.(devicesRequest)
|
||||
dto, err := svc.ListDevices(ctx, req.Opts)
|
||||
return devicesResponse{
|
||||
Devices: dto,
|
||||
Err: err,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type DeviceDTO struct {
|
||||
SerialNumber string `json:"serial_number"`
|
||||
UDID string `json:"udid"`
|
||||
EnrollmentStatus bool `json:"enrollment_status"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
}
|
||||
|
||||
type devicesRequest struct{ Opts ListDevicesOption }
|
||||
type devicesResponse struct {
|
||||
Devices []DeviceDTO `json:"devices"`
|
||||
Err error `json:"err,omitempty"`
|
||||
}
|
||||
37
core/list/service.go
Normal file
37
core/list/service.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/micromdm/micromdm/device"
|
||||
)
|
||||
|
||||
type ListDevicesOption struct {
|
||||
Page int
|
||||
PerPage int
|
||||
|
||||
FilterSerial []string
|
||||
FilterUDID []string
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
ListDevices(ctx context.Context, opt ListDevicesOption) ([]DeviceDTO, error)
|
||||
}
|
||||
|
||||
type ListService struct {
|
||||
Devices *device.DB
|
||||
}
|
||||
|
||||
func (svc *ListService) ListDevices(ctx context.Context, opt ListDevicesOption) ([]DeviceDTO, error) {
|
||||
devices, err := svc.Devices.List()
|
||||
dto := []DeviceDTO{}
|
||||
for _, d := range devices {
|
||||
dto = append(dto, DeviceDTO{
|
||||
SerialNumber: d.SerialNumber,
|
||||
UDID: d.UDID,
|
||||
EnrollmentStatus: d.Enrolled,
|
||||
LastSeen: d.LastCheckin,
|
||||
})
|
||||
}
|
||||
return dto, err
|
||||
}
|
||||
81
core/list/transport_http.go
Normal file
81
core/list/transport_http.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
httptransport "github.com/go-kit/kit/transport/http"
|
||||
)
|
||||
|
||||
func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) http.Handler {
|
||||
var h http.Handler
|
||||
h = httptransport.NewServer(
|
||||
endpoints.ListDevicesEndpoint,
|
||||
decodeListDevicesRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
)
|
||||
return h
|
||||
}
|
||||
|
||||
func decodeListDevicesRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
req := devicesRequest{
|
||||
Opts: ListDevicesOption{},
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func errorDecoder(r *http.Response) error {
|
||||
var w errorWrapper
|
||||
if err := json.NewDecoder(r.Body).Decode(&w); err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.New(w.Error)
|
||||
}
|
||||
|
||||
type errorWrapper struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type errorer interface {
|
||||
error() error
|
||||
}
|
||||
|
||||
func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
if e, ok := response.(errorer); ok && e.error() != nil {
|
||||
EncodeError(ctx, e.error(), w)
|
||||
return nil
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(response)
|
||||
}
|
||||
|
||||
func EncodeError(ctx context.Context, err error, w http.ResponseWriter) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// EncodeHTTPGenericRequest is a transport/http.EncodeRequestFunc that
|
||||
// JSON-encodes any request to the request body. Primarily useful in a client.
|
||||
func EncodeHTTPGenericRequest(_ context.Context, r *http.Request, request interface{}) error {
|
||||
var buf bytes.Buffer
|
||||
if err := json.NewEncoder(&buf).Encode(request); err != nil {
|
||||
return err
|
||||
}
|
||||
r.Body = ioutil.NopCloser(&buf)
|
||||
return nil
|
||||
}
|
||||
|
||||
func DecodeDevicesResponse(_ context.Context, r *http.Response) (interface{}, error) {
|
||||
if r.StatusCode != http.StatusOK {
|
||||
return nil, errorDecoder(r)
|
||||
}
|
||||
var resp devicesResponse
|
||||
err := json.NewDecoder(r.Body).Decode(&resp)
|
||||
return resp, err
|
||||
}
|
||||
18
device/db.go
18
device/db.go
@@ -47,6 +47,24 @@ func NewDB(db *bolt.DB, sub pubsub.Subscriber) (*DB, error) {
|
||||
return datastore, nil
|
||||
}
|
||||
|
||||
func (db *DB) List() ([]Device, error) {
|
||||
// TODO add filter/limit with ForEach
|
||||
var devices []Device
|
||||
err := db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(DeviceBucket))
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
var dev Device
|
||||
if err := UnmarshalDevice(v, &dev); err != nil {
|
||||
return err
|
||||
}
|
||||
devices = append(devices, dev)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return devices, err
|
||||
}
|
||||
|
||||
func (db *DB) Save(dev *Device) error {
|
||||
tx, err := db.DB.Begin(true)
|
||||
if err != nil {
|
||||
|
||||
19
serve.go
19
serve.go
@@ -43,6 +43,7 @@ import (
|
||||
"github.com/micromdm/micromdm/checkin"
|
||||
"github.com/micromdm/micromdm/command"
|
||||
"github.com/micromdm/micromdm/connect"
|
||||
"github.com/micromdm/micromdm/core/list"
|
||||
"github.com/micromdm/micromdm/depsync"
|
||||
"github.com/micromdm/micromdm/device"
|
||||
"github.com/micromdm/micromdm/enroll"
|
||||
@@ -136,7 +137,7 @@ func serve(args []string) error {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := device.NewDB(sm.db, sm.pubclient)
|
||||
devDB, err := device.NewDB(sm.db, sm.pubclient)
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
@@ -180,6 +181,21 @@ func serve(args []string) error {
|
||||
ConnectEndpoint: connectEndpoint,
|
||||
}
|
||||
|
||||
var listsvc list.Service
|
||||
{
|
||||
listsvc = &list.ListService{Devices: devDB}
|
||||
}
|
||||
var listDevicesEndpoint endpoint.Endpoint
|
||||
{
|
||||
listDevicesEndpoint = list.MakeListDevicesEndpoint(listsvc)
|
||||
|
||||
}
|
||||
listEndpoints := list.Endpoints{
|
||||
ListDevicesEndpoint: listDevicesEndpoint,
|
||||
}
|
||||
|
||||
listAPIHandlers := list.MakeHTTPHandlers(ctx, listEndpoints, connectOpts...)
|
||||
|
||||
connectHandlers := connect.MakeHTTPHandlers(ctx, connectEndpoints, connectOpts...)
|
||||
|
||||
pushHandlers := nanopush.MakeHTTPHandlers(ctx, pushEndpoints, checkinOpts...)
|
||||
@@ -192,6 +208,7 @@ func serve(args []string) error {
|
||||
r.Handle("/scep", scepHandler)
|
||||
r.Handle("/push/{udid}", pushHandlers.PushHandler)
|
||||
r.Handle("/v1/commands", commandHandlers.NewCommandHandler).Methods("POST")
|
||||
r.Handle("/v1/devices", listAPIHandlers).Methods("GET")
|
||||
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
io.WriteString(w, homePage)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user