implement mdmctl dep commands (#161)

adds subcommands for endpoints implemented in #160
   mdmctl get dep-devices
   mdmctl get dep-account
   mdmctl get dep-profiles
   mdmctl apply dep-profiles
  
Closes #154
This commit is contained in:
Victor Vrantchan
2017-05-27 23:22:32 -04:00
committed by GitHub
parent c63d0fc152
commit e0efa31d28
10 changed files with 231 additions and 321 deletions

View File

@@ -1,73 +0,0 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/micromdm/dep"
"github.com/pkg/errors"
)
func applyResource(args []string) error {
if len(args) < 1 {
return errors.New("apply requires at least one resource name")
}
sm := &config{}
sm.setupBolt()
client, err := sm.depClient()
if err != nil {
return err
}
var run func(dep.Client, []string) error
switch strings.ToLower(args[0]) {
case "dep-profile":
run = defineDEPProfile
default:
usage()
os.Exit(1)
}
if err := run(client, args[1:]); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
return nil
}
func defineDEPProfile(client dep.Client, args []string) error {
flagset := flag.NewFlagSet("dep-profile", flag.ExitOnError)
var (
flPath = flagset.String("f", "", "path to dep profile JSON")
)
if err := flagset.Parse(args); err != nil {
return err
}
if *flPath == "" {
return errors.New("must specify a path to a profile json")
}
data, err := ioutil.ReadFile(*flPath)
if err != nil {
return errors.Wrapf(err, "reading DEP profile JSON file: %s", *flPath)
}
var profile dep.Profile
if err := json.Unmarshal(data, &profile); err != nil {
return err
}
resp, err := client.DefineProfile(&profile)
if err != nil {
return err
}
fmt.Printf("updated profile %s.\n", resp.ProfileUUID)
return nil
}

View File

@@ -52,6 +52,8 @@ func (cmd *applyCommand) Run(args []string) error {
run = cmd.applyBlueprint
case "dep-tokens":
run = cmd.applyDEPTokens
case "dep-profiles":
run = cmd.applyDEPProfile
case "profiles":
run = cmd.applyProfile
default:
@@ -70,11 +72,15 @@ Valid resource types:
* blueprints
* profiles
* dep-tokens
* dep-profiles
Examples:
# Get a list of devices
# Apply a Blueprint.
mdmctl apply blueprints -f /path/to/blueprint.json
# Apply a DEP Profile.
mdmctl apply dep-profiles -f /path/to/dep-profile.json
`
fmt.Println(applyUsage)
return nil
@@ -177,7 +183,8 @@ func (cmd *applyCommand) applyProfile(args []string) error {
return err
}
if *flProfilePath == "" {
return errors.New("must provide -f parameter")
flagset.Usage()
return errors.New("bad input: must provide -f parameter")
}
if _, err := os.Stat(*flProfilePath); os.IsNotExist(err) {
return err

View File

@@ -0,0 +1,80 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"github.com/micromdm/dep"
"github.com/pkg/errors"
)
func (cmd *applyCommand) applyDEPProfile(args []string) error {
flagset := flag.NewFlagSet("dep-profiles", flag.ExitOnError)
var (
flProfilePath = flagset.String("f", "", "filename of DEP profile to apply")
flTemplate = flagset.Bool("template", false, "print a JSON example of a DEP profile")
)
flagset.Usage = usageFor(flagset, "mdmctl apply dep-profiles [flags]")
if err := flagset.Parse(args); err != nil {
return err
}
if *flTemplate {
printDEPProfileTemplate()
return nil
}
if *flProfilePath == "" {
flagset.Usage()
return errors.New("bad input: must provide -f parameter")
}
pf, err := os.Open(*flProfilePath)
if err != nil {
return errors.Wrap(err, "opening DEP profile file")
}
defer pf.Close()
var profile dep.Profile
if err := json.NewDecoder(pf).Decode(&profile); err != nil {
return errors.Wrap(err, "decode DEP Profile JSON")
}
resp, err := cmd.applysvc.DefineDEPProfile(context.TODO(), &profile)
if err != nil {
return errors.Wrap(err, "define dep profile")
}
// 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)
return nil
}
func printDEPProfileTemplate() {
resp := `
{
"profile_name": "(Required) Human readable name",
"url": "https://mymdm.example.org/mdm/enroll",
"allow_pairing": true,
"is_supervised": false,
"is_multi_user": false,
"is_mandatory": false,
"await_device_configured": false,
"is_mdm_removable": true,
"support_phone_number": "(Optional) +1 408 555 1010",
"support_email_address": "(Optional) support@example.com",
"org_magic": "(Optional)",
"anchor_certs": [],
"supervising_host_certs": [],
"skip_setup_items": ["AppleID", "Android"],
"department": "(Optional) support@example.com",
"devices": ["SERIAL1","SERIAL2"]
}
`
fmt.Println(resp)
}

View File

@@ -51,6 +51,12 @@ func (cmd *getCommand) Run(args []string) error {
switch strings.ToLower(args[0]) {
case "devices":
run = cmd.getDevices
case "dep-devices":
run = cmd.getDEPDevices
case "dep-account":
run = cmd.getDEPAccount
case "dep-profiles":
run = cmd.getDEPProfiles
case "dep-tokens":
run = cmd.getDepTokens
case "blueprints":
@@ -74,6 +80,9 @@ Valid resource types:
* devices
* blueprints
* dep-tokens
* dep-devices
* dep-account
* dep-profiles
* profiles
Examples:

View File

@@ -0,0 +1,38 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"text/tabwriter"
)
type depaccountTableOutput struct{ w *tabwriter.Writer }
func (out *depaccountTableOutput) BasicHeader() {
fmt.Fprintf(out.w, "OrgName\tOrgPhone\tOrgEmail\tServerName\n")
}
func (out *depaccountTableOutput) BasicFooter() {
out.w.Flush()
}
func (cmd *getCommand) getDEPAccount(args []string) error {
flagset := flag.NewFlagSet("dep-account", flag.ExitOnError)
flagset.Usage = usageFor(flagset, "mdmctl get dep-account [flags]")
if err := flagset.Parse(args); err != nil {
return err
}
w := tabwriter.NewWriter(os.Stderr, 0, 4, 2, ' ', 0)
out := &depaccountTableOutput{w}
out.BasicHeader()
defer out.BasicFooter()
ctx := context.Background()
resp, err := cmd.list.GetDEPAccountInfo(ctx)
if err != nil {
return err
}
fmt.Fprintf(out.w, "%s\t%s\t%s\t%s\n", resp.OrgName, resp.OrgPhone, resp.OrgEmail, resp.ServerName)
return nil
}

View File

@@ -0,0 +1,48 @@
package main
import (
"context"
"errors"
"flag"
"fmt"
"os"
"strings"
"text/tabwriter"
)
type depDevicesTableOutput struct{ w *tabwriter.Writer }
func (out *depDevicesTableOutput) BasicHeader() {
fmt.Fprintf(out.w, "SerialNumber\tModel\tProfileStatus\tProfileUUID\n")
}
func (out *depDevicesTableOutput) BasicFooter() {
out.w.Flush()
}
func (cmd *getCommand) getDEPDevices(args []string) error {
flagset := flag.NewFlagSet("dep-devices", flag.ExitOnError)
flSerials := flagset.String("serials", "", "comma separated list of device serials")
flagset.Usage = usageFor(flagset, "mdmctl get dep-devices [flags]")
if err := flagset.Parse(args); err != nil {
return err
}
if *flSerials == "" {
flagset.Usage()
return errors.New("bad input: must provide a comma separated list of DEP serials")
}
w := tabwriter.NewWriter(os.Stderr, 0, 4, 2, ' ', 0)
out := &depDevicesTableOutput{w}
out.BasicHeader()
defer out.BasicFooter()
ctx := context.Background()
serials := strings.Split(*flSerials, ",")
resp, err := cmd.list.GetDEPDevice(ctx, serials)
if err != nil {
return err
}
for _, d := range resp.Devices {
fmt.Fprintf(out.w, "%s\t%s\t%s\t%s\n", d.SerialNumber, d.Model, d.ProfileStatus, d.ProfileUUID)
}
return nil
}

View File

@@ -0,0 +1,47 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"strings"
"text/tabwriter"
)
type depProfilesTableOutput struct{ w *tabwriter.Writer }
func (out *depProfilesTableOutput) BasicHeader() {
fmt.Fprintf(out.w, "Name\tMandatory\tRemovable\tAwaitConfigured\tSkippedItems\n")
}
func (out *depProfilesTableOutput) BasicFooter() {
out.w.Flush()
}
func (cmd *getCommand) getDEPProfiles(args []string) error {
flagset := flag.NewFlagSet("dep-profiles", flag.ExitOnError)
flUUID := flagset.String("uuid", "", "DEP Profile UUID")
flagset.Usage = usageFor(flagset, "mdmctl get dep-profiles [flags]")
if err := flagset.Parse(args); err != nil {
return err
}
w := tabwriter.NewWriter(os.Stderr, 0, 4, 2, ' ', 0)
out := &depProfilesTableOutput{w}
out.BasicHeader()
defer out.BasicFooter()
ctx := context.Background()
resp, err := cmd.list.GetDEPProfile(ctx, *flUUID)
if err != nil {
return err
}
fmt.Fprintf(out.w, "%s\t%v\t%v\t%v\t%s\n",
resp.ProfileName,
resp.IsMandatory,
resp.IsMDMRemovable,
resp.AwaitDeviceConfigured,
strings.Join(resp.SkipSetupItems, ","),
)
return nil
}

View File

@@ -1,73 +0,0 @@
package main
import (
"errors"
"flag"
"fmt"
"github.com/boltdb/bolt"
"github.com/micromdm/micromdm/device"
)
// TODO this is a temporary command to get some stuff working
// we need to remove this and add more robust/general "list/describe" commands
func dev(args []string) error {
flagset := flag.NewFlagSet("devinfo", flag.ExitOnError)
var (
flList = flagset.Bool("list", false, "list all bucket keys")
flDescribe = flagset.String("describe", "", "describe a serial")
)
flagset.Usage = usageFor(flagset, "micromdm serve [flags]")
if err := flagset.Parse(args); err != nil {
return err
}
sm := &config{}
sm.setupBolt()
db, err := device.NewDB(sm.db, nil)
if err != nil {
return err
}
if *flDescribe != "" {
err := describeDevice(db, *flDescribe)
return err
}
if *flList {
err := listDevices(db)
return err
}
return nil
}
func describeDevice(db *device.DB, serial string) error {
dev, err := db.DeviceBySerial(serial)
if err != nil {
return err
}
fmt.Printf(" udid=%s\n", dev.UDID)
fmt.Printf(" serial=%s\n", dev.SerialNumber)
fmt.Printf(" prduct_name=%s\n", dev.ProductName)
return nil
}
func listDevices(db *device.DB) error {
err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(device.DeviceBucket))
if b == nil {
return errors.New("no device bucket found")
}
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
fmt.Printf("key=%s, value=%s\n", k, v)
}
return nil
})
if err != nil {
return err
}
return nil
}

164
get.go
View File

@@ -1,164 +0,0 @@
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"strings"
"github.com/micromdm/dep"
)
func getResource(args []string) error {
if len(args) < 1 {
return errors.New("get requires at least one resource name")
}
var run func([]string) error
switch strings.ToLower(args[0]) {
case "dep":
run = getDep
default:
return errors.New("invalid dep resource name")
}
if err := run(args[1:]); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
return nil
}
func getDep(args []string) error {
if len(args) < 1 {
return errors.New("get dep requires at least one resource name")
}
sm := &config{}
sm.setupBolt()
client, err := sm.depClient()
if err != nil {
return err
}
var run func(dep.Client, []string) error
switch strings.ToLower(args[0]) {
case "account-info":
acc, err := client.Account()
if err != nil {
return err
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
err = enc.Encode(acc)
return err
case "device":
run = getDepDevice
case "profile":
run = getDepProfile
case "profile-template":
run = getDepProfileTpl
default:
usage()
os.Exit(1)
}
if err := run(client, args[1:]); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
return nil
}
func getDepProfile(client dep.Client, args []string) error {
flagset := flag.NewFlagSet("profile", flag.ExitOnError)
var (
flUUID = flagset.String("uuid", "", "profile uuid")
)
if err := flagset.Parse(args); err != nil {
return err
}
profile, err := client.FetchProfile(*flUUID)
if err != nil {
return err
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
err = enc.Encode(profile)
return err
}
func getDepDevice(client dep.Client, args []string) error {
flagset := flag.NewFlagSet("device", flag.ExitOnError)
var (
flSerial = flagset.String("serial", "", "device serial number")
)
if err := flagset.Parse(args); err != nil {
return err
}
resp, err := client.DeviceDetails([]string{*flSerial})
if err != nil {
return err
}
if dev, ok := resp.Devices[*flSerial]; ok && dev.SerialNumber != "" {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
err = enc.Encode(dev)
return err
} else {
return errors.New("no device information")
}
return nil
}
func getDepProfileTpl(client dep.Client, args []string) error {
/* omitempty flags on the Profile struct are preventing the (Apple-
defined) false defaults from showing up in the example.
p := dep.Profile{
ProfileName: "(Required) Human readable name",
URL: "https://mymdm.example.org",
AllowPairing: true,
IsSupervised: false,
IsMultiUser: false,
IsMandatory: false,
AwaitDeviceConfigured: false,
IsMDMRemovable: true,
SupportPhoneNumber: "(Optional) +1 408 555 1010",
SupportEmailAddress: "(Optional) support@example.com",
OrgMagic: "(Optional)",
AnchorCerts: []string{},
SupervisingHostCerts: []string{},
SkipSetupItems: []string{"AppleID", "Android"},
Department: "(Optional) support@example.com",
Devices: []string{},
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
err := enc.Encode(&p)
*/
resp := `{
"profile_name": "(Required) Human readable name",
"url": "https://mymdm.example.org",
"allow_pairing": true,
"is_supervised": false,
"is_multi_user": false,
"is_mandatory": false,
"await_device_configured": false,
"is_mdm_removable": true,
"support_phone_number": "(Optional) +1 408 555 1010",
"support_email_address": "(Optional) support@example.com",
"org_magic": "(Optional)",
"anchor_certs": [],
"supervising_host_certs": [],
"skip_setup_items": ["AppleID", "Android"],
"deparment": "(Optional) support@example.com",
"devices": []
}`
fmt.Println(resp)
return nil
}

View File

@@ -28,12 +28,6 @@ func main() {
return
case "serve":
run = serve
case "get":
run = getResource
case "apply":
run = applyResource
case "dev":
run = dev
default:
usage()
os.Exit(1)
@@ -50,9 +44,6 @@ func usage() error {
Available Commands:
serve
dev
get
apply
version
Use micromdm <command> -h for additional usage of each command.