mirror of
https://github.com/micromdm/micromdm/
synced 2026-08-13 05:45:41 +08:00
DEP auto-assignment (v2) (#405)
Support DEP auto-assignment in MicroMDM. Resolves #227. Resolves #178.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
### Auto-assigner
|
||||
|
||||
* Added DEP auto-assigner logic. #405
|
||||
* Added support for querying devices by serial. #363
|
||||
* Added support for triggering a DEP sync via API. #404
|
||||
* Added support for mdmcert.download directly to `mdmctl` #401
|
||||
|
||||
@@ -64,6 +64,8 @@ func (cmd *applyCommand) Run(args []string) error {
|
||||
run = cmd.applyBlock
|
||||
case "users":
|
||||
run = cmd.applyUser
|
||||
case "dep-autoassigner":
|
||||
run = cmd.applyDEPAutoAssigner
|
||||
default:
|
||||
cmd.Usage()
|
||||
os.Exit(1)
|
||||
@@ -82,6 +84,7 @@ Valid resource types:
|
||||
* users
|
||||
* dep-tokens
|
||||
* dep-profiles
|
||||
* dep-autoassigner
|
||||
* app
|
||||
* block
|
||||
|
||||
|
||||
37
cmd/mdmctl/apply_dep_autoassigner.go
Normal file
37
cmd/mdmctl/apply_dep_autoassigner.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/micromdm/micromdm/dep/depsync"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (cmd *applyCommand) applyDEPAutoAssigner(args []string) error {
|
||||
flagset := flag.NewFlagSet("dep-autoassigner", flag.ExitOnError)
|
||||
var (
|
||||
flFilter = flagset.String("filter", "*", "filter string (only '*' supported right now)")
|
||||
flProfileUUID = flagset.String("uuid", "", "DEP profile UUID to set")
|
||||
)
|
||||
flagset.Usage = usageFor(flagset, "mdmctl apply dep-autoassigner [flags]")
|
||||
if err := flagset.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if *flFilter == "" || *flProfileUUID == "" {
|
||||
return errors.New("bad input: must provide both -filter and -uuid")
|
||||
}
|
||||
|
||||
assigner := depsync.AutoAssigner{*flFilter, *flProfileUUID}
|
||||
|
||||
err := cmd.depsyncsvc.ApplyAutoAssigner(context.TODO(), &assigner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("saved auto-assign filter '%s' to DEP profile UUID '%s'\n", assigner.Filter, assigner.ProfileUUID)
|
||||
fmt.Println("newly added DEP devices will be auto-assigned to the above profile UUID")
|
||||
return nil
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/micromdm/dep"
|
||||
"github.com/micromdm/micromdm/dep/depsync"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/micromdm/micromdm/pkg/crypto"
|
||||
@@ -40,6 +41,7 @@ func (cmd *applyCommand) applyDEPProfile(args []string) error {
|
||||
flTemplate = flagset.Bool("template", false, "print a JSON example of a DEP profile")
|
||||
flAnchorFile = flagset.String("anchor", "", "filename of PEM cert(s) to add to anchor certs in template")
|
||||
flUseServer = flagset.Bool("use-server-cert", false, "use the server cert(s) to add to anchor certs in template")
|
||||
flFilter = flagset.String("filter", "", "set the auto-assign filter to for the defined profile")
|
||||
)
|
||||
flagset.Usage = usageFor(flagset, "mdmctl apply dep-profiles [flags]")
|
||||
if err := flagset.Parse(args); err != nil {
|
||||
@@ -97,6 +99,16 @@ func (cmd *applyCommand) applyDEPProfile(args []string) error {
|
||||
// TODO: it would be nice to encode back a profile that save the
|
||||
// UUID for future reference.
|
||||
fmt.Printf("Defined DEP Profile with UUID %s\n", resp.ProfileUUID)
|
||||
|
||||
if *flFilter != "" {
|
||||
assigner := depsync.AutoAssigner{*flFilter, resp.ProfileUUID}
|
||||
err := cmd.depsyncsvc.ApplyAutoAssigner(context.TODO(), &assigner)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "set auto-assigner")
|
||||
}
|
||||
fmt.Printf("Saved auto-assign filter '%s' for this DEP profile\n", *flFilter)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,8 @@ func (cmd *getCommand) Run(args []string) error {
|
||||
run = cmd.getUsers
|
||||
case "apps":
|
||||
run = cmd.getApps
|
||||
case "dep-autoassigners":
|
||||
run = cmd.getDEPAutoAssigners
|
||||
default:
|
||||
cmd.Usage()
|
||||
os.Exit(1)
|
||||
@@ -92,6 +94,7 @@ Valid resource types:
|
||||
* dep-devices
|
||||
* dep-account
|
||||
* dep-profiles
|
||||
* dep-autoassigners
|
||||
* users
|
||||
* profiles
|
||||
* apps
|
||||
|
||||
31
cmd/mdmctl/get_dep_autoassigner.go
Normal file
31
cmd/mdmctl/get_dep_autoassigner.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
func (cmd *getCommand) getDEPAutoAssigners(args []string) error {
|
||||
flagset := flag.NewFlagSet("dep-autoassigner", flag.ExitOnError)
|
||||
flagset.Usage = usageFor(flagset, "mdmctl get dep-autoassigner [flags]")
|
||||
if err := flagset.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assigners, err := cmd.depsyncsvc.GetAutoAssigners(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "Filter\tDEP Profile UUID\n")
|
||||
for _, a := range assigners {
|
||||
fmt.Fprintf(w, "%s\t%s\n", a.Filter, a.ProfileUUID)
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -46,6 +46,8 @@ func (cmd *removeCommand) Run(args []string) error {
|
||||
run = cmd.removeProfiles
|
||||
case "block":
|
||||
run = cmd.removeBlock
|
||||
case "dep-autoassigner":
|
||||
run = cmd.removeDEPAutoAssigner
|
||||
default:
|
||||
cmd.Usage()
|
||||
os.Exit(1)
|
||||
@@ -63,6 +65,7 @@ Valid resource types:
|
||||
* blueprints
|
||||
* profiles
|
||||
* block
|
||||
* dep-autoassigner
|
||||
`
|
||||
|
||||
fmt.Println(getUsage)
|
||||
|
||||
32
cmd/mdmctl/remove_dep_autoassigner.go
Normal file
32
cmd/mdmctl/remove_dep_autoassigner.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (cmd *removeCommand) removeDEPAutoAssigner(args []string) error {
|
||||
flagset := flag.NewFlagSet("dep-autoassigner", flag.ExitOnError)
|
||||
var (
|
||||
flFilter = flagset.String("filter", "*", "filter string (only '*' supported right now)")
|
||||
)
|
||||
flagset.Usage = usageFor(flagset, "mdmctl remove dep-autoassigner [flags]")
|
||||
if err := flagset.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if *flFilter == "" {
|
||||
return errors.New("bad input: must provide -filter")
|
||||
}
|
||||
|
||||
err := cmd.depsyncsvc.RemoveAutoAssigner(context.TODO(), *flFilter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("removed DEP profile associated with filter '%s'\n", *flFilter)
|
||||
return nil
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/go-kit/kit/log"
|
||||
httptransport "github.com/go-kit/kit/transport/http"
|
||||
|
||||
"github.com/micromdm/micromdm/dep/depsync"
|
||||
"github.com/micromdm/micromdm/platform/appstore"
|
||||
"github.com/micromdm/micromdm/platform/blueprint"
|
||||
"github.com/micromdm/micromdm/platform/config"
|
||||
@@ -23,6 +24,7 @@ type remoteServices struct {
|
||||
configsvc config.Service
|
||||
appsvc appstore.Service
|
||||
depsvc dep.Service
|
||||
depsyncsvc depsync.Service
|
||||
}
|
||||
|
||||
func setupClient(logger log.Logger) (*remoteServices, error) {
|
||||
@@ -87,6 +89,13 @@ func setupClient(logger log.Logger) (*remoteServices, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
depsyncsvc, err := depsync.NewHTTPClient(
|
||||
cfg.ServerURL, cfg.APIToken, logger,
|
||||
httptransport.SetClient(skipVerifyHTTPClient(cfg.SkipVerify)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &remoteServices{
|
||||
profilesvc: profilesvc,
|
||||
blueprintsvc: blueprintsvc,
|
||||
@@ -96,5 +105,6 @@ func setupClient(logger log.Logger) (*remoteServices, error) {
|
||||
configsvc: configsvc,
|
||||
appsvc: appsvc,
|
||||
depsvc: depsvc,
|
||||
depsyncsvc: depsyncsvc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -301,8 +301,7 @@ func serve(args []string) error {
|
||||
scepHandler := scep.ServiceHandler(ctx, sm.scepService, httpLogger)
|
||||
enrollHandlers := enroll.MakeHTTPHandlers(ctx, enroll.MakeServerEndpoints(sm.enrollService, sm.scepDepot), httptransport.ServerErrorLogger(httpLogger))
|
||||
|
||||
syncNowEndpoint := depsync.MakeSyncNowEndpoint(depsync.NewRPC(syncer))
|
||||
depsyncHandlers := depsync.MakeHTTPHandlers(ctx, depsync.Endpoints{SyncNowEndpoint: syncNowEndpoint}, connectOpts...)
|
||||
depsyncEndpoints := depsync.MakeServerEndpoints(depsync.NewService(syncer))
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.Handle("/version", version.Handler())
|
||||
@@ -325,6 +324,7 @@ func serve(args []string) error {
|
||||
deviceHandler := device.MakeHTTPHandler(deviceEndpoints, logger)
|
||||
depHandlers := depapi.MakeHTTPHandler(depEndpoints, logger)
|
||||
apnsHandlers := apns.MakeHTTPHandler(apnsEndpoints, logger)
|
||||
depsyncHandlers := depsync.MakeHTTPHandler(depsyncEndpoints, logger)
|
||||
|
||||
// API commands. Only handled if the user provides an api key.
|
||||
if *flAPIKey != "" {
|
||||
@@ -341,7 +341,8 @@ func serve(args []string) error {
|
||||
r.Handle("/v1/dep/devices", apiAuthMiddleware(*flAPIKey, depHandlers))
|
||||
r.Handle("/v1/dep/account", apiAuthMiddleware(*flAPIKey, depHandlers))
|
||||
r.Handle("/v1/dep/profiles", apiAuthMiddleware(*flAPIKey, depHandlers))
|
||||
r.Handle("/v1/dep/syncnow", apiAuthMiddleware(*flAPIKey, depsyncHandlers.SyncNowHandler)).Methods("POST")
|
||||
r.Handle("/v1/dep/syncnow", apiAuthMiddleware(*flAPIKey, depsyncHandlers))
|
||||
r.Handle("/v1/dep/autoassigners", apiAuthMiddleware(*flAPIKey, depsyncHandlers))
|
||||
r.Handle("/v1/commands", apiAuthMiddleware(*flAPIKey, commandHandlers.NewCommandHandler)).Methods("POST")
|
||||
r.Handle("/push/{udid}", apiAuthMiddleware(*flAPIKey, apnsHandlers))
|
||||
} else {
|
||||
|
||||
56
dep/depsync/apply_autoassigner.go
Normal file
56
dep/depsync/apply_autoassigner.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package depsync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
|
||||
"github.com/micromdm/micromdm/pkg/httputil"
|
||||
)
|
||||
|
||||
func (s DEPSyncService) ApplyAutoAssigner(ctx context.Context, aa *AutoAssigner) error {
|
||||
conf := s.syncer.GetConfig()
|
||||
return conf.saveAutoAssigner(aa)
|
||||
}
|
||||
|
||||
type applyAutoAssignerRequest struct {
|
||||
*AutoAssigner
|
||||
}
|
||||
type applyAutoAssignerResponse struct {
|
||||
Err error `json:"err,omitempty"`
|
||||
}
|
||||
|
||||
func (r applyAutoAssignerResponse) Failed() error { return r.Err }
|
||||
|
||||
func MakeApplyAutoAssignerEndpoint(s Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(applyAutoAssignerRequest)
|
||||
err := s.ApplyAutoAssigner(ctx, req.AutoAssigner)
|
||||
return &applyAutoAssignerResponse{
|
||||
Err: err,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func decodeApplyAutoAssignerResponse(ctx context.Context, r *http.Response) (interface{}, error) {
|
||||
var req applyAutoAssignerResponse
|
||||
err := httputil.DecodeJSONResponse(r, &req)
|
||||
return req, err
|
||||
}
|
||||
|
||||
func decodeApplyAutoAssignerRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
var req applyAutoAssignerRequest
|
||||
err := httputil.DecodeJSONRequest(r, &req)
|
||||
return req, err
|
||||
}
|
||||
|
||||
func (e Endpoints) ApplyAutoAssigner(ctx context.Context, aa *AutoAssigner) error {
|
||||
request := applyAutoAssignerRequest{AutoAssigner: aa}
|
||||
resp, err := e.ApplyAutoAssignerEndpoint(ctx, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response := resp.(applyAutoAssignerResponse)
|
||||
return response.Err
|
||||
}
|
||||
69
dep/depsync/client.go
Normal file
69
dep/depsync/client.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package depsync
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/go-kit/kit/log"
|
||||
httptransport "github.com/go-kit/kit/transport/http"
|
||||
|
||||
"github.com/micromdm/micromdm/pkg/httputil"
|
||||
)
|
||||
|
||||
func NewHTTPClient(instance, token string, logger log.Logger, opts ...httptransport.ClientOption) (Service, error) {
|
||||
u, err := url.Parse(instance)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var syncNowEndpoint endpoint.Endpoint
|
||||
{
|
||||
syncNowEndpoint = httptransport.NewClient(
|
||||
"PUT",
|
||||
httputil.CopyURL(u, "/v1/dep/autoassign"),
|
||||
httputil.EncodeRequestWithToken(token, httptransport.EncodeJSONRequest),
|
||||
decodeEmptyResponse,
|
||||
opts...,
|
||||
).Endpoint()
|
||||
}
|
||||
|
||||
var applyAutoAssignerEndpoint endpoint.Endpoint
|
||||
{
|
||||
applyAutoAssignerEndpoint = httptransport.NewClient(
|
||||
"POST",
|
||||
httputil.CopyURL(u, "/v1/dep/autoassigners"),
|
||||
httputil.EncodeRequestWithToken(token, httptransport.EncodeJSONRequest),
|
||||
decodeApplyAutoAssignerResponse,
|
||||
opts...,
|
||||
).Endpoint()
|
||||
}
|
||||
|
||||
var getAutoAssignersEndpoint endpoint.Endpoint
|
||||
{
|
||||
getAutoAssignersEndpoint = httptransport.NewClient(
|
||||
"GET",
|
||||
httputil.CopyURL(u, "/v1/dep/autoassigners"),
|
||||
httputil.EncodeRequestWithToken(token, httptransport.EncodeJSONRequest),
|
||||
decodeGetAutoAssignersResponse,
|
||||
opts...,
|
||||
).Endpoint()
|
||||
}
|
||||
|
||||
var removeAutoAssignerEndpoint endpoint.Endpoint
|
||||
{
|
||||
removeAutoAssignerEndpoint = httptransport.NewClient(
|
||||
"DELETE",
|
||||
httputil.CopyURL(u, "/v1/dep/autoassigners"),
|
||||
httputil.EncodeRequestWithToken(token, httptransport.EncodeJSONRequest),
|
||||
decodeRemoveAutoAssignerResponse,
|
||||
opts...,
|
||||
).Endpoint()
|
||||
}
|
||||
|
||||
return Endpoints{
|
||||
SyncNowEndpoint: syncNowEndpoint,
|
||||
ApplyAutoAssignerEndpoint: applyAutoAssignerEndpoint,
|
||||
GetAutoAssignersEndpoint: getAutoAssignersEndpoint,
|
||||
RemoveAutoAssignerEndpoint: removeAutoAssignerEndpoint,
|
||||
}, nil
|
||||
}
|
||||
@@ -27,6 +27,49 @@ func (cfg *config) Save() error {
|
||||
return errors.Wrap(err, "saving dep sync cursor")
|
||||
}
|
||||
|
||||
func (cfg *config) saveAutoAssigner(assigner *AutoAssigner) error {
|
||||
if assigner.Filter != "*" {
|
||||
return errors.New("only '*' filter auto-assigners supported")
|
||||
}
|
||||
err := cfg.Update(func(tx *bolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte(AutoAssignBucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put([]byte(assigner.Filter), []byte(assigner.ProfileUUID))
|
||||
})
|
||||
return errors.Wrap(err, "saving auto-assigner")
|
||||
}
|
||||
|
||||
func (cfg *config) loadAutoAssigners() ([]*AutoAssigner, error) {
|
||||
assigners := []*AutoAssigner{}
|
||||
err := cfg.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(AutoAssignBucket))
|
||||
if b == nil { // bucket doesn't exist yet
|
||||
return nil
|
||||
}
|
||||
|
||||
return b.ForEach(func(k, v []byte) error {
|
||||
assigners = append(assigners, &AutoAssigner{
|
||||
Filter: string(k),
|
||||
ProfileUUID: string(v),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
})
|
||||
return assigners, errors.Wrap(err, "loading auto-assigners")
|
||||
}
|
||||
|
||||
func (cfg *config) deleteAutoAssigner(filter string) error {
|
||||
return cfg.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(AutoAssignBucket))
|
||||
if b == nil { // bucket doesn't exist yet
|
||||
return nil
|
||||
}
|
||||
return b.Delete([]byte(filter))
|
||||
})
|
||||
}
|
||||
|
||||
func LoadConfig(db *bolt.DB) (*config, error) {
|
||||
conf := config{DB: db}
|
||||
err := db.Update(func(tx *bolt.Tx) error {
|
||||
|
||||
@@ -18,8 +18,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
SyncTopic = "mdm.DepSync"
|
||||
ConfigBucket = "mdm.DEPConfig"
|
||||
SyncTopic = "mdm.DepSync"
|
||||
ConfigBucket = "mdm.DEPConfig"
|
||||
AutoAssignBucket = "mdm.DEPAutoAssign"
|
||||
|
||||
syncDuration = 30 * time.Minute
|
||||
cursorValidDuration = 7 * 24 * time.Hour
|
||||
@@ -27,6 +28,12 @@ const (
|
||||
|
||||
type Syncer interface {
|
||||
SyncNow()
|
||||
GetConfig() *config // TODO: #302
|
||||
}
|
||||
|
||||
type AutoAssigner struct {
|
||||
Filter string `json:"filter"`
|
||||
ProfileUUID string `json:"profile_uuid"`
|
||||
}
|
||||
|
||||
type watcher struct {
|
||||
@@ -157,6 +164,10 @@ func (w *watcher) SyncNow() {
|
||||
w.syncNow <- true
|
||||
}
|
||||
|
||||
func (w *watcher) GetConfig() *config {
|
||||
return w.conf
|
||||
}
|
||||
|
||||
// TODO this needs to be a proper error in the micromdm/dep package.
|
||||
func isCursorExhausted(err error) bool {
|
||||
return strings.Contains(err.Error(), "EXHAUSTED_CURSOR")
|
||||
@@ -166,6 +177,109 @@ func isCursorExpired(err error) bool {
|
||||
return strings.Contains(err.Error(), "EXPIRED_CURSOR")
|
||||
}
|
||||
|
||||
// Process DEP messages and pull out filter-matching serial numbers
|
||||
// associated to profile UUIDs for auto-assignment.
|
||||
func (w *watcher) filteredAutoAssignments(devices []dep.Device) (map[string][]string, error) {
|
||||
// load auto-assigners every run to make sure we get the latest set of
|
||||
// auto-assigner profile UUIDs/filters. Note this makes every *watcher
|
||||
// (i.e. every DEP sync instance) share the current DB set of auto-
|
||||
// assigners. perhaps to refactor to be more separated.
|
||||
assigners, err := w.conf.loadAutoAssigners()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assigned := make(map[string][]string)
|
||||
// skip looping over serials if we have no autoassigners
|
||||
if len(assigners) < 1 {
|
||||
return assigned, nil
|
||||
}
|
||||
for _, d := range devices {
|
||||
// only process DEP "added" OpType messages
|
||||
if d.OpType != "added" {
|
||||
continue
|
||||
}
|
||||
// filter our devices by our assigner filters and get list of
|
||||
// which devices are to be assigned to which profiles
|
||||
for _, assigner := range assigners {
|
||||
if assigner.Filter == "*" { // only supported filter type right now
|
||||
if serials, ok := assigned[assigner.ProfileUUID]; ok {
|
||||
assigned[assigner.ProfileUUID] = append(serials, d.SerialNumber)
|
||||
} else {
|
||||
assigned[assigner.ProfileUUID] = []string{d.SerialNumber}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return assigned, nil
|
||||
}
|
||||
|
||||
func (w *watcher) processAutoAssign(devices []dep.Device) error {
|
||||
assignments, err := w.filteredAutoAssignments(devices)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for profileUUID, serials := range assignments {
|
||||
resp, err := w.client.AssignProfile(profileUUID, serials)
|
||||
if err != nil {
|
||||
level.Info(w.logger).Log(
|
||||
"err", err,
|
||||
"msg", "auto-assign error assigning serials to profile",
|
||||
"profile", profileUUID,
|
||||
)
|
||||
continue
|
||||
}
|
||||
// count our results for logging
|
||||
resultCounts := map[string]int{
|
||||
"SUCCESS": 0,
|
||||
"NOT_ACCESSIBLE": 0,
|
||||
"FAILED": 0,
|
||||
}
|
||||
for _, result := range resp.Devices {
|
||||
if ct, ok := resultCounts[result]; ok {
|
||||
// NOTE: we're logging _only_ the above pre-defined result types
|
||||
resultCounts[result] = ct + 1
|
||||
}
|
||||
}
|
||||
// TODO: alternate strategy is to log all failed devices
|
||||
// TODO: handle/requeue failed devices?
|
||||
level.Info(w.logger).Log(
|
||||
"msg", "DEP auto-assigned",
|
||||
"profile", profileUUID,
|
||||
"success", resultCounts["SUCCESS"],
|
||||
"not_accessible", resultCounts["NOT_ACCESSIBLE"],
|
||||
"failed", resultCounts["FAILED"],
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *watcher) publishAndProcessDevices(devices []dep.Device) error {
|
||||
e := NewEvent(devices)
|
||||
data, err := MarshalEvent(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.publisher.Publish(context.TODO(), SyncTopic, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: instead of directly kicking off the auto-assigner process
|
||||
// consider placing a subscriber on the DEP pubsub topic. The same
|
||||
// information gets marshalled but it allows us the future
|
||||
// flexibility to separate out that component if we desired.
|
||||
go func() {
|
||||
err := w.processAutoAssign(devices)
|
||||
if err != nil {
|
||||
level.Info(w.logger).Log("err", err, "msg", "auto-assign error")
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *watcher) Run() error {
|
||||
ticker := time.NewTicker(syncDuration).C
|
||||
FETCH:
|
||||
@@ -176,17 +290,18 @@ FETCH:
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
level.Info(w.logger).Log("msg", "DEP fetch", "more", resp.MoreToFollow, "cursor", resp.Cursor, "fetched", resp.FetchedUntil)
|
||||
level.Info(w.logger).Log(
|
||||
"msg", "DEP fetch",
|
||||
"more", resp.MoreToFollow,
|
||||
"cursor", resp.Cursor,
|
||||
"fetched", resp.FetchedUntil,
|
||||
"devices", len(resp.Devices),
|
||||
)
|
||||
w.conf.Cursor = cursor{Value: resp.Cursor, CreatedAt: time.Now()}
|
||||
if err := w.conf.Save(); err != nil {
|
||||
return errors.Wrap(err, "saving cursor from fetch")
|
||||
}
|
||||
e := NewEvent(resp.Devices)
|
||||
data, err := MarshalEvent(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.publisher.Publish(context.TODO(), SyncTopic, data); err != nil {
|
||||
if err := w.publishAndProcessDevices(resp.Devices); err != nil {
|
||||
return err
|
||||
}
|
||||
if !resp.MoreToFollow {
|
||||
@@ -203,22 +318,19 @@ SYNC:
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(resp.Devices) != 0 {
|
||||
level.Info(w.logger).Log("msg", "DEP sync", "more", resp.MoreToFollow, "cursor", resp.Cursor, "fetched", resp.FetchedUntil)
|
||||
}
|
||||
level.Info(w.logger).Log(
|
||||
"msg", "DEP sync",
|
||||
"more", resp.MoreToFollow,
|
||||
"cursor", resp.Cursor,
|
||||
"fetched", resp.FetchedUntil,
|
||||
"devices", len(resp.Devices),
|
||||
)
|
||||
w.conf.Cursor = cursor{Value: resp.Cursor, CreatedAt: time.Now()}
|
||||
if err := w.conf.Save(); err != nil {
|
||||
return errors.Wrap(err, "saving cursor from sync")
|
||||
}
|
||||
if len(resp.Devices) > 0 {
|
||||
e := NewEvent(resp.Devices)
|
||||
data, err := MarshalEvent(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.publisher.Publish(context.TODO(), SyncTopic, data); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.publishAndProcessDevices(resp.Devices); err != nil {
|
||||
return err
|
||||
}
|
||||
if !resp.MoreToFollow {
|
||||
select {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package depsync
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
type Endpoints struct {
|
||||
SyncNowEndpoint endpoint.Endpoint
|
||||
}
|
||||
|
||||
func MakeSyncNowEndpoint(s Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
s.SyncNow(ctx)
|
||||
return syncNowResponse{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type syncNowResponse struct{}
|
||||
47
dep/depsync/get_autoassigners.go
Normal file
47
dep/depsync/get_autoassigners.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package depsync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
|
||||
"github.com/micromdm/micromdm/pkg/httputil"
|
||||
)
|
||||
|
||||
func (s DEPSyncService) GetAutoAssigners(ctx context.Context) ([]*AutoAssigner, error) {
|
||||
conf := s.syncer.GetConfig()
|
||||
return conf.loadAutoAssigners()
|
||||
}
|
||||
|
||||
type getAutoAssignersResponse struct {
|
||||
AutoAssigners []*AutoAssigner `json:"autoassigners"`
|
||||
Err error `json:"err,omitempty"`
|
||||
}
|
||||
|
||||
func (r getAutoAssignersResponse) Failed() error { return r.Err }
|
||||
|
||||
func MakeGetAutoAssignersEndpoint(s Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
assigners, err := s.GetAutoAssigners(ctx)
|
||||
return &getAutoAssignersResponse{
|
||||
AutoAssigners: assigners,
|
||||
Err: err,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func decodeGetAutoAssignersResponse(ctx context.Context, r *http.Response) (interface{}, error) {
|
||||
var req getAutoAssignersResponse
|
||||
err := httputil.DecodeJSONResponse(r, &req)
|
||||
return req, err
|
||||
}
|
||||
|
||||
func (e Endpoints) GetAutoAssigners(ctx context.Context) ([]*AutoAssigner, error) {
|
||||
resp, err := e.GetAutoAssignersEndpoint(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := resp.(getAutoAssignersResponse)
|
||||
return response.AutoAssigners, response.Err
|
||||
}
|
||||
56
dep/depsync/remove_autoassigners.go
Normal file
56
dep/depsync/remove_autoassigners.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package depsync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/micromdm/micromdm/pkg/httputil"
|
||||
)
|
||||
|
||||
func (s DEPSyncService) RemoveAutoAssigner(ctx context.Context, filter string) error {
|
||||
conf := s.syncer.GetConfig()
|
||||
return conf.deleteAutoAssigner(filter)
|
||||
}
|
||||
|
||||
type removeAutoAssignerRequest struct {
|
||||
Filter string `json:"filter"`
|
||||
}
|
||||
|
||||
type removeAutoAssignerResponse struct {
|
||||
Err error `json:"err,omitempty"`
|
||||
}
|
||||
|
||||
func (r removeAutoAssignerResponse) Failed() error { return r.Err }
|
||||
|
||||
func MakeRemoveAutoAssignerEndpoint(s Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(removeAutoAssignerRequest)
|
||||
err := s.RemoveAutoAssigner(ctx, req.Filter)
|
||||
return &removeAutoAssignerResponse{
|
||||
Err: err,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func decodeRemoveAutoAssignerResponse(ctx context.Context, r *http.Response) (interface{}, error) {
|
||||
var req removeAutoAssignerResponse
|
||||
err := httputil.DecodeJSONResponse(r, &req)
|
||||
return req, err
|
||||
}
|
||||
|
||||
func decodeRemoveAutoAssignerRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
var req removeAutoAssignerRequest
|
||||
err := httputil.DecodeJSONRequest(r, &req)
|
||||
return req, err
|
||||
}
|
||||
|
||||
func (e Endpoints) RemoveAutoAssigner(ctx context.Context, filter string) error {
|
||||
request := removeAutoAssignerRequest{Filter: filter}
|
||||
resp, err := e.RemoveAutoAssignerEndpoint(ctx, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response := resp.(removeAutoAssignerResponse)
|
||||
return response.Err
|
||||
}
|
||||
69
dep/depsync/server.go
Normal file
69
dep/depsync/server.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package depsync
|
||||
|
||||
import (
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/go-kit/kit/log"
|
||||
httptransport "github.com/go-kit/kit/transport/http"
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/micromdm/micromdm/pkg/httputil"
|
||||
)
|
||||
|
||||
func NewService(syncer Syncer) *DEPSyncService {
|
||||
return &DEPSyncService{syncer: syncer}
|
||||
}
|
||||
|
||||
type Endpoints struct {
|
||||
SyncNowEndpoint endpoint.Endpoint
|
||||
ApplyAutoAssignerEndpoint endpoint.Endpoint
|
||||
GetAutoAssignersEndpoint endpoint.Endpoint
|
||||
RemoveAutoAssignerEndpoint endpoint.Endpoint
|
||||
}
|
||||
|
||||
func MakeServerEndpoints(s Service) Endpoints {
|
||||
return Endpoints{
|
||||
SyncNowEndpoint: MakeSyncNowEndpoint(s),
|
||||
ApplyAutoAssignerEndpoint: MakeApplyAutoAssignerEndpoint(s),
|
||||
GetAutoAssignersEndpoint: MakeGetAutoAssignersEndpoint(s),
|
||||
RemoveAutoAssignerEndpoint: MakeRemoveAutoAssignerEndpoint(s),
|
||||
}
|
||||
}
|
||||
|
||||
func MakeHTTPHandler(e Endpoints, logger log.Logger) *mux.Router {
|
||||
r, options := httputil.NewRouter(logger)
|
||||
|
||||
// POST /v1/dep/syncnow request a DEP sync operation to happen now
|
||||
// POST /v1/dep/autoassigners set a DEP auto-assigner
|
||||
// GET /v1/dep/autoassigners get list of DEP auto-assigners
|
||||
// DELETE /v1/dep/autoassigners remove a DEP auto-assigner
|
||||
|
||||
r.Methods("POST").Path("/v1/dep/syncnow").Handler(httptransport.NewServer(
|
||||
e.SyncNowEndpoint,
|
||||
decodeEmptyRequest,
|
||||
encodeEmptyResponse,
|
||||
options...,
|
||||
))
|
||||
|
||||
r.Methods("POST").Path("/v1/dep/autoassigners").Handler(httptransport.NewServer(
|
||||
e.ApplyAutoAssignerEndpoint,
|
||||
decodeApplyAutoAssignerRequest,
|
||||
httputil.EncodeJSONResponse,
|
||||
options...,
|
||||
))
|
||||
|
||||
r.Methods("GET").Path("/v1/dep/autoassigners").Handler(httptransport.NewServer(
|
||||
e.GetAutoAssignersEndpoint,
|
||||
decodeEmptyRequest,
|
||||
httputil.EncodeJSONResponse,
|
||||
options...,
|
||||
))
|
||||
|
||||
r.Methods("DELETE").Path("/v1/dep/autoassigners").Handler(httptransport.NewServer(
|
||||
e.RemoveAutoAssignerEndpoint,
|
||||
decodeRemoveAutoAssignerRequest,
|
||||
httputil.EncodeJSONResponse,
|
||||
options...,
|
||||
))
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -5,18 +5,12 @@ import (
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
SyncNow(ctx context.Context)
|
||||
SyncNow(context.Context) error
|
||||
ApplyAutoAssigner(context.Context, *AutoAssigner) error
|
||||
GetAutoAssigners(context.Context) ([]*AutoAssigner, error)
|
||||
RemoveAutoAssigner(context.Context, string) error
|
||||
}
|
||||
|
||||
type syncNowService struct {
|
||||
type DEPSyncService struct {
|
||||
syncer Syncer
|
||||
}
|
||||
|
||||
func (s *syncNowService) SyncNow(_ context.Context) {
|
||||
s.syncer.SyncNow()
|
||||
return
|
||||
}
|
||||
|
||||
func NewRPC(syncer Syncer) *syncNowService {
|
||||
return &syncNowService{syncer: syncer}
|
||||
}
|
||||
|
||||
40
dep/depsync/syncnow.go
Normal file
40
dep/depsync/syncnow.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package depsync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
func (s *DEPSyncService) SyncNow(_ context.Context) error {
|
||||
s.syncer.SyncNow()
|
||||
return nil
|
||||
}
|
||||
|
||||
type syncNowResponse struct{}
|
||||
type syncNowRequest struct{}
|
||||
|
||||
func MakeSyncNowEndpoint(s Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, _ interface{}) (interface{}, error) {
|
||||
s.SyncNow(ctx)
|
||||
return syncNowResponse{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func decodeEmptyRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func encodeEmptyResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeEmptyResponse(ctx context.Context, r *http.Response) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (e Endpoints) SyncNow(ctx context.Context) error {
|
||||
_, err := e.SyncNowEndpoint(ctx, nil)
|
||||
return err
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package depsync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
httptransport "github.com/go-kit/kit/transport/http"
|
||||
)
|
||||
|
||||
type HTTPHandlers struct {
|
||||
SyncNowHandler http.Handler
|
||||
}
|
||||
|
||||
func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) HTTPHandlers {
|
||||
return HTTPHandlers{
|
||||
SyncNowHandler: httptransport.NewServer(
|
||||
endpoints.SyncNowEndpoint,
|
||||
decodeEmptyRequest,
|
||||
encodeEmptyResponse,
|
||||
opts...,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeEmptyRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func encodeEmptyResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
return nil
|
||||
}
|
||||
@@ -293,31 +293,46 @@ func (db *DB) pollCheckin(pubsubSvc pubsub.PublishSubscriber) error {
|
||||
}
|
||||
fmt.Printf("got %d devices from DEP\n", len(ev.Devices))
|
||||
for _, d := range ev.Devices {
|
||||
newDevice := new(device.Device)
|
||||
bySerial, err := db.DeviceBySerial(d.SerialNumber)
|
||||
if err == nil && bySerial != nil { // must be a DEP device
|
||||
fmt.Printf("existing device checked in from DEP: %s\n", d.SerialNumber)
|
||||
newDevice = bySerial
|
||||
}
|
||||
if err != nil && !isNotFound(err) {
|
||||
fmt.Println(err) // some other issue is going on
|
||||
updDevice, updDeviceErr := db.DeviceBySerial(d.SerialNumber)
|
||||
if updDeviceErr != nil && !isNotFound(updDeviceErr) {
|
||||
fmt.Printf("error getting device %s: %s\n", d.SerialNumber, err)
|
||||
continue
|
||||
}
|
||||
if newDevice.UUID == "" { // previously unknown
|
||||
newDevice.UUID = uuid.NewV4().String()
|
||||
|
||||
if updDeviceErr != nil && isNotFound(updDeviceErr) {
|
||||
updDevice = new(device.Device)
|
||||
if d.OpType == "modified" {
|
||||
fmt.Printf("warning: no existing device for DEP device update: %s\n", d.SerialNumber)
|
||||
}
|
||||
}
|
||||
newDevice.SerialNumber = d.SerialNumber
|
||||
newDevice.Model = d.Model
|
||||
newDevice.Description = d.Description
|
||||
newDevice.Color = d.Color
|
||||
newDevice.AssetTag = d.AssetTag
|
||||
newDevice.DEPProfileStatus = device.DEPProfileStatus(d.ProfileStatus)
|
||||
newDevice.DEPProfileUUID = d.ProfileUUID
|
||||
newDevice.DEPProfileAssignTime = d.ProfileAssignTime
|
||||
newDevice.DEPProfileAssignedDate = d.DeviceAssignedDate
|
||||
newDevice.DEPProfileAssignedBy = d.DeviceAssignedBy
|
||||
// TODO: deal with sync fields OpType, OpDate
|
||||
if err := db.Save(newDevice); err != nil {
|
||||
if updDeviceErr == nil && d.OpType == "added" {
|
||||
// consider issuing this warning if op_type == "" as well.
|
||||
// in that case it's likely the device came from a DEP
|
||||
// fetch (vs. a sync) which could be a re-fetch of devices
|
||||
fmt.Printf("warning: re-adding existing DEP device: %s\n", d.SerialNumber)
|
||||
}
|
||||
if d.OpType == "deleted" {
|
||||
fmt.Printf("warning: DEP device unassigned: %s\n", d.SerialNumber)
|
||||
}
|
||||
|
||||
if updDevice.UUID == "" {
|
||||
// generate UUID for any device that doesn't have one
|
||||
updDevice.UUID = uuid.NewV4().String()
|
||||
}
|
||||
|
||||
updDevice.SerialNumber = d.SerialNumber
|
||||
updDevice.Model = d.Model
|
||||
updDevice.Description = d.Description
|
||||
updDevice.Color = d.Color
|
||||
updDevice.AssetTag = d.AssetTag
|
||||
updDevice.DEPProfileStatus = device.DEPProfileStatus(d.ProfileStatus)
|
||||
updDevice.DEPProfileUUID = d.ProfileUUID
|
||||
updDevice.DEPProfileAssignTime = d.ProfileAssignTime
|
||||
updDevice.DEPProfileAssignedDate = d.DeviceAssignedDate
|
||||
updDevice.DEPProfileAssignedBy = d.DeviceAssignedBy
|
||||
// TODO: support profile_push_time, os, device_family, op_date
|
||||
|
||||
if err := db.Save(updDevice); err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user