blob: 2d6d18d808240580847446c62ea8010d9d3304b0 [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"os"
"sort"
"strings"
"github.com/olekukonko/tablewriter"
"v.io/x/lib/cmdline"
)
var cmdMadbGroup = &cmdline.Command{
Children: []*cmdline.Command{
cmdMadbGroupAdd,
cmdMadbGroupClearAll,
cmdMadbGroupDelete,
cmdMadbGroupList,
cmdMadbGroupRemove,
cmdMadbGroupRename,
},
Name: "group",
DontInheritFlags: true,
Short: "Manage device groups",
Long: `
Manages device groups, each of which can have one or more device members. The
device groups can be used for specifying the target devices of other madb
commands.
`,
}
var cmdMadbGroupAdd = &cmdline.Command{
Runner: subCommandRunnerWithFilepath{runMadbGroupAdd, getDefaultConfigFilePath},
Name: "add",
Short: "Add members to a device group",
Long: `
Adds members to a device group. This command also creates the group, if the
group does not exist yet. The device group can be used when specifying devices
in any madb commands.
When creating a new device group with this command, the provided name must not
conflict with an existing device nickname (see: madb help name set).
A group can contain another device group, in which case all the members of the
other group will also be considered as members of the current group.
`,
ArgsName: "<group_name> <member1> [<member2> ...]",
ArgsLong: `
<group_name> is an alpha-numeric string with no special characters or spaces.
This name must not be an existing device nickname.
<member> is a member specifier, which can be one of device serial, qualifier,
device index (e.g., '@1', '@2'), device nickname, or another device group.
`,
}
func runMadbGroupAdd(env *cmdline.Env, args []string, filename string) error {
// Check if the arguments are valid.
if len(args) < 2 {
return env.UsageErrorf("There must be at least two arguments.")
}
groupName := args[0]
if !isValidName(groupName) {
return fmt.Errorf("Not a valid group name: %q", groupName)
}
cfg, err := readConfig(filename)
if err != nil {
return err
}
if isDeviceNickname(groupName, cfg) {
return fmt.Errorf("The group name %q conflicts with a device nickname.", groupName)
}
members := removeDuplicates(args[1:])
for _, member := range members {
if err := isValidDeviceSpecifier(member); err != nil {
return fmt.Errorf("Invalid member %q: %v", member, err)
}
}
oldMembers, ok := cfg.Groups[groupName]
if !ok {
oldMembers = []string{}
}
cfg.Groups[groupName] = removeDuplicates(append(oldMembers, members...))
return writeConfig(cfg, filename)
}
var cmdMadbGroupClearAll = &cmdline.Command{
Runner: subCommandRunnerWithFilepath{runMadbGroupClearAll, getDefaultConfigFilePath},
Name: "clear-all",
Short: "Clear all the existing device groups",
Long: `
Clears all the existing device groups.
`,
}
func runMadbGroupClearAll(env *cmdline.Env, args []string, filename string) error {
cfg, err := readConfig(filename)
if err != nil {
return err
}
// Reset the groups
cfg.Groups = make(map[string][]string)
return writeConfig(cfg, filename)
}
var cmdMadbGroupDelete = &cmdline.Command{
Runner: subCommandRunnerWithFilepath{runMadbGroupDelete, getDefaultConfigFilePath},
Name: "delete",
Short: "Delete an existing device group",
Long: `
Deletes an existing device group.
`,
ArgsName: "<group_name1> [<group_name2> ...]",
ArgsLong: `
<group_name> the name of an existing device group.
You can specify more than one group names.
`,
}
func runMadbGroupDelete(env *cmdline.Env, args []string, filename string) error {
// Check if the arguments are valid.
if len(args) < 1 {
return env.UsageErrorf("There must be at least one argument.")
}
cfg, err := readConfig(filename)
if err != nil {
return err
}
for _, groupName := range args {
if !isGroupName(groupName, cfg) {
return fmt.Errorf("Not an existing group name: %q", groupName)
}
}
// Delete the groups
for _, groupName := range args {
delete(cfg.Groups, groupName)
}
return writeConfig(cfg, filename)
}
var cmdMadbGroupList = &cmdline.Command{
Runner: subCommandRunnerWithFilepath{runMadbGroupList, getDefaultConfigFilePath},
Name: "list",
Short: "List all the existing device groups",
Long: `
Lists the name and members of all the existing device groups.
`,
}
func runMadbGroupList(env *cmdline.Env, args []string, filename string) error {
cfg, err := readConfig(filename)
if err != nil {
return err
}
tw := tablewriter.NewWriter(os.Stdout)
tw.SetHeader([]string{"Group Name", "Members"})
tw.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
tw.SetAutoFormatHeaders(false)
tw.SetAlignment(tablewriter.ALIGN_LEFT)
data := make([][]string, 0, len(cfg.Groups))
for group, members := range cfg.Groups {
data = append(data, []string{group, strings.Join(members, " ")})
}
sort.Sort(byFirstElement(data))
for _, row := range data {
tw.Append(row)
}
tw.Render()
return nil
}
var cmdMadbGroupRemove = &cmdline.Command{
Runner: subCommandRunnerWithFilepath{runMadbGroupRemove, getDefaultConfigFilePath},
Name: "remove",
Short: "Remove members from a device group",
Long: `
Removes members from an existing device group. If there are no remaining members
after that, the group gets deleted.
`,
ArgsName: "<group_name> <member1> [<member2> ...]",
ArgsLong: `
<group_name> is an alpha-numeric string with no special characters or spaces.
This name must be an existing device group name.
<member> is a member specifier, which can be one of device serial, qualifier,
device index (e.g., '@1', '@2'), device nickname, or another device group.
`,
}
func runMadbGroupRemove(env *cmdline.Env, args []string, filename string) error {
// Check if the arguments are valid.
if len(args) < 2 {
return env.UsageErrorf("There must be at least two arguments.")
}
groupName := args[0]
if !isValidName(groupName) {
return fmt.Errorf("Not a valid group name: %q", groupName)
}
cfg, err := readConfig(filename)
if err != nil {
return err
}
if !isGroupName(groupName, cfg) {
return fmt.Errorf("Not an existing group name: %q", groupName)
}
members := removeDuplicates(args[1:])
oldMembers := cfg.Groups[groupName]
cfg.Groups[groupName] = subtractSlices(oldMembers, members)
if len(cfg.Groups[groupName]) == 0 {
delete(cfg.Groups, groupName)
}
return writeConfig(cfg, filename)
}
var cmdMadbGroupRename = &cmdline.Command{
Runner: subCommandRunnerWithFilepath{runMadbGroupRename, getDefaultConfigFilePath},
Name: "rename",
Short: "Rename an existing device group",
Long: `
Renames an existing device group.
`,
ArgsName: "<old_name> <new_name>",
ArgsLong: `
<old_name> is the name of an existing device group.
<new_name> is the new name for the existing group.
This must be an alpha-numeric string with no special characters or spaces,
and must not conflict with another existing device or group name.
`,
}
func runMadbGroupRename(env *cmdline.Env, args []string, filename string) error {
// Check if the arguments are valid.
if len(args) != 2 {
return env.UsageErrorf("There must be exactly two arguments.")
}
oldName, newName := args[0], args[1]
if !isValidName(oldName) {
return fmt.Errorf("Not a valid group name: %q", oldName)
}
if !isValidName(newName) {
return fmt.Errorf("Not a valid group name: %q", newName)
}
cfg, err := readConfig(filename)
if err != nil {
return err
}
if !isGroupName(oldName, cfg) {
return fmt.Errorf("Not an existing group name: %q", oldName)
}
if isNameInUse(newName, cfg) {
return fmt.Errorf("The provided name is already in use: %q", newName)
}
cfg.Groups[newName] = cfg.Groups[oldName]
delete(cfg.Groups, oldName)
return writeConfig(cfg, filename)
}
// removeDuplicates takes a string slice and removes all the duplicates.
func removeDuplicates(s []string) []string {
result := make([]string, 0, len(s))
used := map[string]bool{}
for _, elem := range s {
if !used[elem] {
result = append(result, elem)
used[elem] = true
}
}
return result
}
// subtractSlices takes two slices and returns a new slice formed by removing
// all the elements in s2 from s1.
func subtractSlices(s1, s2 []string) []string {
result := make([]string, 0, len(s1))
m := map[string]bool{}
for _, e2 := range s2 {
m[e2] = true
}
for _, e1 := range s1 {
if !m[e1] {
result = append(result, e1)
}
}
return result
}
// expandGroups takes a slice of device specifier tokens and returns a new slice
// where all the group name tokens are expanded to include all their members.
// The expansion process is transitive; if a group includes other groups, all
// the members of the other groups are also included in the returned slice. Each
// group is processed at most once, in order to avoid infinite loops.
func expandGroups(tokens []string, cfg *config) []string {
expanded := make([]string, 0, len(tokens))
queue := make([]string, len(tokens))
copy(queue, tokens)
visitedGroups := make(map[string]bool)
for len(queue) > 0 {
// Pop a token from the queue
token := queue[0]
queue = queue[1:]
if isGroupName(token, cfg) {
// If this group was already processed before, ignore and proceed
// to the next token to avoid infinite loops.
if visitedGroups[token] {
continue
}
visitedGroups[token] = true
// Otherwise, expand the group and add all the members to the queue.
queue = append(queue, cfg.Groups[token]...)
} else {
expanded = append(expanded, token)
}
}
return expanded
}