From 0682bb6946f75d9641e453f5b80a04a1bb060e04 Mon Sep 17 00:00:00 2001
From: Florent Gluck <florent.gluck@hesge.ch>
Date: Thu, 18 Aug 2022 14:41:46 +0200
Subject: [PATCH] Updated commands' help to display which capabilities are
 required

---
 README.md                                    | 115 ++++++++++++++-----
 src/client_cli/cmd/Command.go                |   2 +-
 src/client_cli/cmdLogin/login.go             |  12 +-
 src/client_cli/cmdTemplate/helper.go         |   6 +-
 src/client_cli/cmdTemplate/templateCreate.go |  12 +-
 src/client_cli/cmdTemplate/templateDel.go    |   6 +-
 src/client_cli/cmdTemplate/templateList.go   |   6 +-
 src/client_cli/cmdUser/helper.go             |   6 +-
 src/client_cli/cmdUser/userAdd.go            |  12 +-
 src/client_cli/cmdUser/userDel.go            |  12 +-
 src/client_cli/cmdUser/userList.go           |   6 +-
 src/client_cli/cmdUser/userSetCaps.go        |  12 +-
 src/client_cli/cmdUser/userUpdatePwd.go      |  12 +-
 src/client_cli/cmdUser/userWhoami.go         |  12 +-
 src/client_cli/cmdVM/helper.go               |   6 +-
 src/client_cli/cmdVM/vmAttach.go             |   6 +-
 src/client_cli/cmdVM/vmCreate.go             |  12 +-
 src/client_cli/cmdVM/vmCred2pdf.go           |  12 +-
 src/client_cli/cmdVM/vmDel.go                |   6 +-
 src/client_cli/cmdVM/vmDelAccess.go          |  12 +-
 src/client_cli/cmdVM/vmEdit.go               |  12 +-
 src/client_cli/cmdVM/vmExportDir.go          |  13 ++-
 src/client_cli/cmdVM/vmList.go               |  12 +-
 src/client_cli/cmdVM/vmListAttach.go         |   6 +-
 src/client_cli/cmdVM/vmListDel.go            |   6 +-
 src/client_cli/cmdVM/vmListEdit.go           |   6 +-
 src/client_cli/cmdVM/vmListEditAccess.go     |   6 +-
 src/client_cli/cmdVM/vmListExportDir.go      |   6 +-
 src/client_cli/cmdVM/vmListStart.go          |   6 +-
 src/client_cli/cmdVM/vmListStop.go           |   6 +-
 src/client_cli/cmdVM/vmSetAccess.go          |  12 +-
 src/client_cli/cmdVM/vmShutdown.go           |   6 +-
 src/client_cli/cmdVM/vmStart.go              |   6 +-
 src/client_cli/cmdVM/vmStop.go               |   6 +-
 src/client_cli/nexus-client.go               |  11 +-
 35 files changed, 282 insertions(+), 123 deletions(-)

diff --git a/README.md b/README.md
index f3ff08f..2f916c9 100644
--- a/README.md
+++ b/README.md
@@ -33,35 +33,94 @@ Usage: nexus-client CMD
 CMD is the Command to run. Except for "login", all Commands require an access token.
 The access token is read from the env. variable "NEXUS_TOKEN".
 List of supported Commands:
-    login               Login and obtain an access token.
-    whoami              Display the current user's details.
-    passwd              Update the current user's password.
-    userlist            List users (regex matching).
-    useradd             Add a user.
-    userdel             Delete one or more users.
-    usersetcaps         Set a user's capabilities.
-    vmlist              List VMs that can be listed (regex matching).
-    vmliststart         List VMs that can be started (regex matching).
-    vmlistattach        List VMs that can be attached to (regex matching).
-    vmliststop          List VMs that can be stopped or shutdown (regex matching).
-    vmlistedit          List VMs that can be edited (regex matching).
-    vmlisteditaccess    List VMs that can have their VM access edited (regex matching).
-    vmlistdel           List VMs that can be deleted (regex matching).
+
+    login               Obtains an access token.
+                        Requires no capabilities.
+
+    whoami              Displays the current user's details.
+                        Requires no capabilities.
+
+    passwd              Updates the current user's password.
+                        Requires no capabilities.
+
+    userlist            Lists users (regex matching).
+                        Requires USER_LIST user capability.
+
+    useradd             Adds a user.
+                        Requires USER_CREATE user capability.
+
+    userdel             Deletes one or more users.
+                        Requires USER_DESTROY user capability.
+
+    usersetcaps         Sets a user's capabilities.
+                        Requires USER_SET_CAPS user capability.
+
+    vmlist              Lists VMs that can be listed (regex matching).
+                        Requires VM_LIST VM access capability or VM_LIST_ANY user capability.
+
+    vmliststart         Lists VMs that can be started (regex matching).
+                        Requires VM_START VM access capability or VM_START_ANY user capability.
+
+    vmlistattach        Lists VMs that can be attached to (regex matching).
+                        Requires VM_LIST VM access capability or VM_LIST_ANY user capability.
+
+    vmliststop          Lists VMs that can be stopped or shutdown (regex matching).
+                        Requires VM_STOP VM access capability or VM_STOP_ANY user capability.
+
+    vmlistedit          Lists VMs that can be edited (regex matching).
+                        Requires VM_EDIT VM access capability or VM_EDIT_ANY user capability.
+
+    vmlisteditaccess    Lists VMs that can have their VM access edited (regex matching).
+                        Requires VM_SET_ACCESS user capability and VM_SET_ACCESS VM access capability.
+
+    vmlistdel           Lists VMs that can be deleted (regex matching).
+                        Requires VM_DESTROY VM access capability or VM_DESTROY_ANY user capability.
+
     vmlistexportdir     List VMs that can have a directory exported (regex matching).
-    vmcred2pdf          Create a PDF with the credentials required to attach to running VMs (regex matching).
-    vmstart             Start one or more VMs (regex matching).
-    vmstop              Stop by force one or more VMs (regex matching).
-    vmshutdown          Gracefully shutdown one or more VMs (regex matching).
-    vmattach            Attach to one or more VMs in order to use their desktop environment (regex matching).
-    vmcreate            Create one or more VMs (regex matching).
-    vmedit              Edit one or more VMs' properties: name, cpus, ram or nic (regex matching).
-    vmdel               Delete one or more VMs (regex matching).
-    vmsetaccess         Set a user's VM access in one or more VMs (regex matching).
-    vmdelaccess         Delete a user's VM access in one or more VMs (regex matching).
-    vmexportdir         Export one or more VMs' directory into one or more tar archives. Create one archive per VM (regex matching).
-    tpllist             List available templates (regex matching).
-    tplcreate           Create a template, either from an existing VM or from a .qcow file.
-    tpldel              Delete one or more templates (regex matching).
+                        Requires VM_READFS VM access capability or VM_READFS_ANY user capability.
+
+    vmcred2pdf          Creates a PDF with the credentials required to attach to running VMs (regex matching).
+                        Requires VM_LIST VM access capability or VM_LIST_ANY user capability.
+
+    vmstart             Starts one or more VMs (regex matching).
+                        Requires VM_START VM access capability or VM_START_ANY user capability.
+
+    vmstop              Stops by force one or more VMs (regex matching).
+                        Requires VM_STOP VM access capability or VM_STOP_ANY user capability.
+
+    vmshutdown          Gracefully shutdowns one or more VMs (regex matching).
+                        Requires VM_STOP VM access capability or VM_STOP_ANY user capability.
+
+    vmattach            Attaches to one or more VMs in order to use their desktop environment (regex matching).
+                        Requires VM_LIST VM access capability or VM_LIST_ANY user capability.
+
+    vmcreate            Creates one or more VMs (regex matching).
+                        Requires VM_CREATE user capability.
+
+    vmedit              Edits one or more VMs' properties: name, cpus, ram or nic (regex matching).
+                        Requires VM_EDIT VM access capability or VM_EDIT_ANY user capability.
+
+    vmdel               Deletes one or more VMs (regex matching).
+                        Requires VM_DESTROY VM access capability or VM_DESTROY_ANY user capability.
+
+    vmsetaccess         Sets a user's VM access in one or more VMs (regex matching).
+                        Requires VM_SET_ACCESS user capability and VM_SET_ACCESS VM access capability.
+
+    vmdelaccess         Deletes a user's VM access in one or more VMs (regex matching).
+                        Requires VM_SET_ACCESS user capability and VM_SET_ACCESS VM access capability.
+
+    vmexportdir         Exports one or more VMs' directory into one or more tar archives.
+                        Creates one archive per VM (regex matching).
+                        Requires VM_READFS VM access capability or VM_READFS_ANY user capability.
+
+    tpllist             Lists available templates (regex matching).
+                        Requires TPL_LIST or TPL_LIST_ANY user capabilities.
+
+    tplcreate           Creates a template, either from an existing VM or from a .qcow file.
+                        Requires TPL_CREATE user capability.
+
+    tpldel              Deletes one or more templates (regex matching).
+                        Requires TPL_DESTROY or TPL_DESTROY_ANY user capabilities.
 ```
 
 When attached to a VM's desktop (`vmattach` command), ctrl+F12 toggles between fullscreen/non-fullscreen modes.
diff --git a/src/client_cli/cmd/Command.go b/src/client_cli/cmd/Command.go
index b59e32c..261bef7 100644
--- a/src/client_cli/cmd/Command.go
+++ b/src/client_cli/cmd/Command.go
@@ -2,7 +2,7 @@ package cmd
 
 type Command interface {
 	GetName() string
-	GetDesc() string
+	GetDesc() []string
 	PrintUsage()
 	Run(args []string) int   // Returns 0 if the Command was successful.
 }
diff --git a/src/client_cli/cmdLogin/login.go b/src/client_cli/cmdLogin/login.go
index a34fcd8..788a95b 100644
--- a/src/client_cli/cmdLogin/login.go
+++ b/src/client_cli/cmdLogin/login.go
@@ -18,13 +18,17 @@ func (cmd *Login)GetName() string {
 	return cmd.Name
 }
 
-func (cmd *Login)GetDesc() string {
-	return "Login and obtain an access token."
+func (cmd *Login)GetDesc() []string {
+	return []string{
+		"Obtains an access token.",
+		"Requires no capabilities."}
 }
 
 func (cmd *Login)PrintUsage() {
-	u.PrintlnErr(cmd.GetDesc())
-	u.PrintlnErr("Usage: "+cmd.Name+" email password")
+	for _, desc := range cmd.GetDesc() {
+		u.PrintlnErr(desc)
+	}
+	u.PrintlnErr("USAGE: "+cmd.Name+" email password")
 }
 
 func (cmd *Login)Run(args []string) int {
diff --git a/src/client_cli/cmdTemplate/helper.go b/src/client_cli/cmdTemplate/helper.go
index 394f685..5931674 100644
--- a/src/client_cli/cmdTemplate/helper.go
+++ b/src/client_cli/cmdTemplate/helper.go
@@ -32,8 +32,10 @@ func (tpl *Template)String() string {
 }
 
 func printUsage(c cmd.Command, action string) {
-	u.PrintlnErr(c.GetDesc())
-	u.PrintlnErr("Usage: ",c.GetName()," [ID ...] [regex ...]")
+	for _, desc := range c.GetDesc() {
+		u.PrintlnErr(desc)
+	}
+	u.PrintlnErr("USAGE: ",c.GetName()," [ID ...] [regex ...]")
 	u.PrintlnErr("Only templates matching the specified IDs or regexes will be "+action+".")
 	const usage string = `Any number of IDs or regexes can be specified.
 The regex only matches the templates's name and is case-insensitive.
diff --git a/src/client_cli/cmdTemplate/templateCreate.go b/src/client_cli/cmdTemplate/templateCreate.go
index b159edf..25f30f1 100644
--- a/src/client_cli/cmdTemplate/templateCreate.go
+++ b/src/client_cli/cmdTemplate/templateCreate.go
@@ -22,13 +22,17 @@ func (cmd *Create)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *Create)GetDesc() string {
-	return "Create a template, either from an existing VM or from a .qcow file."
+func (cmd *Create)GetDesc() []string {
+	return []string{
+		"Creates a template, either from an existing VM or from a .qcow file.",
+		"Requires TPL_CREATE user capability."}
 }
 
 func (cmd *Create)PrintUsage() {
-	u.PrintlnErr(cmd.GetDesc())
-	u.PrintlnErr("Usage: "+cmd.Name+" vmID|qcowFile name access")
+	for _, desc := range cmd.GetDesc() {
+		u.PrintlnErr(desc)
+	}
+	u.PrintlnErr("USAGE: "+cmd.Name+" vmID|qcowFile name access")
 	u.PrintlnErr("Notes: access is either \"public\" or \"private\"")
 }
 
diff --git a/src/client_cli/cmdTemplate/templateDel.go b/src/client_cli/cmdTemplate/templateDel.go
index 0c70aa8..e8d872d 100644
--- a/src/client_cli/cmdTemplate/templateDel.go
+++ b/src/client_cli/cmdTemplate/templateDel.go
@@ -13,8 +13,10 @@ func (cmd *Del)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *Del)GetDesc() string {
-	return "Delete one or more templates (regex matching)."
+func (cmd *Del)GetDesc() []string {
+	return []string{
+		"Deletes one or more templates (regex matching).",
+		"Requires TPL_DESTROY or TPL_DESTROY_ANY user capabilities."}
 }
 
 func (cmd *Del)PrintUsage() {
diff --git a/src/client_cli/cmdTemplate/templateList.go b/src/client_cli/cmdTemplate/templateList.go
index 5e62104..d361fe2 100644
--- a/src/client_cli/cmdTemplate/templateList.go
+++ b/src/client_cli/cmdTemplate/templateList.go
@@ -8,8 +8,10 @@ func (cmd *List)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *List)GetDesc() string {
-	return "List available templates (regex matching)."
+func (cmd *List)GetDesc() []string {
+	return []string{
+		"Lists available templates (regex matching).",
+		"Requires TPL_LIST or TPL_LIST_ANY user capabilities."}
 }
 
 func (cmd *List)PrintUsage() {
diff --git a/src/client_cli/cmdUser/helper.go b/src/client_cli/cmdUser/helper.go
index 332c283..38070ec 100644
--- a/src/client_cli/cmdUser/helper.go
+++ b/src/client_cli/cmdUser/helper.go
@@ -33,8 +33,10 @@ func (user *User)String() string {
 }
 
 func printUsage(c cmd.Command) {
-	u.PrintlnErr(c.GetDesc())
-	u.PrintlnErr("Usage: ",c.GetName()," [regex ...]")
+	for _, desc := range c.GetDesc() {
+		u.PrintlnErr(desc)
+	}
+	u.PrintlnErr("USAGE: ",c.GetName()," [regex ...]")
 	const usage string = `Only users matching the specified regexes will be listed.
 Any number of regexes can be specified.
 The regex matches the user email, first name and last name and is case-insensitive.
diff --git a/src/client_cli/cmdUser/userAdd.go b/src/client_cli/cmdUser/userAdd.go
index bba0ddd..028f9a4 100644
--- a/src/client_cli/cmdUser/userAdd.go
+++ b/src/client_cli/cmdUser/userAdd.go
@@ -13,13 +13,17 @@ func (cmd *Add)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *Add)GetDesc() string {
-	return "Add a user."
+func (cmd *Add)GetDesc() []string {
+	return []string{
+		"Adds a user.",
+		"Requires USER_CREATE user capability."}
 }
 
 func (cmd *Add)PrintUsage() {
-	u.PrintlnErr(cmd.GetDesc())
-	u.PrintlnErr("Usage: "+cmd.Name+" email firstname lastname password [capability ...]")
+	for _, desc := range cmd.GetDesc() {
+		u.PrintlnErr(desc)
+	}
+	u.PrintlnErr("USAGE: "+cmd.Name+" email firstname lastname password [capability ...]")
 }
 
 func (cmd *Add)Run(args []string) int {
diff --git a/src/client_cli/cmdUser/userDel.go b/src/client_cli/cmdUser/userDel.go
index 2f57f5d..00a80a1 100644
--- a/src/client_cli/cmdUser/userDel.go
+++ b/src/client_cli/cmdUser/userDel.go
@@ -13,13 +13,17 @@ func (cmd *Del)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *Del)GetDesc() string {
-	return "Delete one or more users."
+func (cmd *Del)GetDesc() []string {
+	return []string{
+		"Deletes one or more users.",
+		"Requires USER_DESTROY user capability."}
 }
 
 func (cmd *Del)PrintUsage() {
-	u.PrintlnErr(cmd.GetDesc())
-	u.PrintlnErr("Usage: "+cmd.Name+" email [email ...]")
+	for _, desc := range cmd.GetDesc() {
+		u.PrintlnErr(desc)
+	}
+	u.PrintlnErr("USAGE: "+cmd.Name+" email [email ...]")
 }
 
 func (cmd *Del)Run(args []string) int {
diff --git a/src/client_cli/cmdUser/userList.go b/src/client_cli/cmdUser/userList.go
index 5b0712d..c00ab4e 100644
--- a/src/client_cli/cmdUser/userList.go
+++ b/src/client_cli/cmdUser/userList.go
@@ -8,8 +8,10 @@ func (cmd *List)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *List)GetDesc() string {
-	return "List users (regex matching)."
+func (cmd *List)GetDesc() []string {
+	return []string{
+		"Lists users (regex matching).",
+		"Requires USER_LIST user capability."}
 }
 
 func (cmd *List)PrintUsage() {
diff --git a/src/client_cli/cmdUser/userSetCaps.go b/src/client_cli/cmdUser/userSetCaps.go
index ea6eaca..e40fe80 100644
--- a/src/client_cli/cmdUser/userSetCaps.go
+++ b/src/client_cli/cmdUser/userSetCaps.go
@@ -13,13 +13,17 @@ func (cmd *SetCaps)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *SetCaps)GetDesc() string {
-	return "Set a user's capabilities."
+func (cmd *SetCaps)GetDesc() []string {
+	return []string{
+		"Sets a user's capabilities.",
+		"Requires USER_SET_CAPS user capability."}
 }
 
 func (cmd *SetCaps)PrintUsage() {
-	u.PrintlnErr(cmd.GetDesc())
-	u.PrintlnErr("Usage: "+cmd.Name+" email [capability ...]")
+	for _, desc := range cmd.GetDesc() {
+		u.PrintlnErr(desc)
+	}
+	u.PrintlnErr("USAGE: "+cmd.Name+" email [capability ...]")
 }
 
 func (cmd *SetCaps)Run(args []string) int {
diff --git a/src/client_cli/cmdUser/userUpdatePwd.go b/src/client_cli/cmdUser/userUpdatePwd.go
index 52661ea..4a8df0a 100644
--- a/src/client_cli/cmdUser/userUpdatePwd.go
+++ b/src/client_cli/cmdUser/userUpdatePwd.go
@@ -18,13 +18,17 @@ func (cmd *UpdatePwd)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *UpdatePwd)GetDesc() string {
-	return "Update the current user's password."
+func (cmd *UpdatePwd)GetDesc() []string {
+	return []string{
+		"Updates the current user's password.",
+		"Requires no capabilities."}
 }
 
 func (cmd *UpdatePwd)PrintUsage() {
-	u.PrintlnErr(cmd.GetDesc())
-	u.PrintlnErr("Usage: "+cmd.Name)
+	for _, desc := range cmd.GetDesc() {
+		u.PrintlnErr(desc)
+	}
+	u.PrintlnErr("USAGE: "+cmd.Name)
 }
 
 func (cmd *UpdatePwd)Run(args []string) int {
diff --git a/src/client_cli/cmdUser/userWhoami.go b/src/client_cli/cmdUser/userWhoami.go
index 2275313..dd46ed9 100644
--- a/src/client_cli/cmdUser/userWhoami.go
+++ b/src/client_cli/cmdUser/userWhoami.go
@@ -13,13 +13,17 @@ func (cmd *Whoami)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *Whoami)GetDesc() string {
-	return "Display the current user's details."
+func (cmd *Whoami)GetDesc() []string {
+	return []string{
+		"Displays the current user's details.",
+		"Requires no capabilities."}
 }
 
 func (cmd *Whoami)PrintUsage() {
-	u.PrintlnErr(cmd.GetDesc())
-	u.PrintlnErr("Usage: "+cmd.Name)
+	for _, desc := range cmd.GetDesc() {
+		u.PrintlnErr(desc)
+	}
+	u.PrintlnErr("USAGE: "+cmd.Name)
 }
 
 func (cmd *Whoami)Run(args []string) int {
diff --git a/src/client_cli/cmdVM/helper.go b/src/client_cli/cmdVM/helper.go
index 7d3121e..f553a99 100644
--- a/src/client_cli/cmdVM/helper.go
+++ b/src/client_cli/cmdVM/helper.go
@@ -53,8 +53,10 @@ func (vm *VM)String() string {
 }
 
 func printRegexUsage(c cmd.Command) {
-	u.PrintlnErr(c.GetDesc())
-	u.PrintlnErr("Usage: ",c.GetName()," [ID ...] [regex ...]")
+	for _, desc := range c.GetDesc() {
+		u.PrintlnErr(desc)
+	}
+	u.PrintlnErr("USAGE: ",c.GetName()," [ID ...] [regex ...]")
 }
 
 func printRegexUsageDetails() {
diff --git a/src/client_cli/cmdVM/vmAttach.go b/src/client_cli/cmdVM/vmAttach.go
index ff3ee6b..b67bba7 100644
--- a/src/client_cli/cmdVM/vmAttach.go
+++ b/src/client_cli/cmdVM/vmAttach.go
@@ -22,8 +22,10 @@ func (cmd *Attach)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *Attach)GetDesc() string {
-	return "Attach to one or more VMs in order to use their desktop environment (regex matching)."
+func (cmd *Attach)GetDesc() []string {
+	return []string{
+		"Attaches to one or more VMs in order to use their desktop environment (regex matching).",
+		"Requires VM_LIST VM access capability or VM_LIST_ANY user capability."}
 }
 
 func (cmd *Attach)PrintUsage() {
diff --git a/src/client_cli/cmdVM/vmCreate.go b/src/client_cli/cmdVM/vmCreate.go
index 1c507ee..984f2a4 100644
--- a/src/client_cli/cmdVM/vmCreate.go
+++ b/src/client_cli/cmdVM/vmCreate.go
@@ -17,13 +17,17 @@ func (cmd *Create)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *Create)GetDesc() string {
-	return "Create one or more VMs (regex matching)."
+func (cmd *Create)GetDesc() []string {
+	return []string{
+		"Creates one or more VMs (regex matching).",
+		"Requires VM_CREATE user capability."}
 }
 
 func (cmd *Create)PrintUsage() {
-	u.PrintlnErr(cmd.GetDesc())
-	u.PrintlnErr("Usage: "+cmd.Name+" name cpus ram nic template [count|file.csv]")
+	for _, desc := range cmd.GetDesc() {
+		u.PrintlnErr(desc)
+	}
+	u.PrintlnErr("USAGE: "+cmd.Name+" name cpus ram nic template [count|file.csv]")
 	const usage string = `name        name of the VM
 cpus        Number of CPUs, between 1 and 16.
 ram         Amount of RAM in MB,  between 512 and 32768.
diff --git a/src/client_cli/cmdVM/vmCred2pdf.go b/src/client_cli/cmdVM/vmCred2pdf.go
index 1f791a8..1c3f7bd 100644
--- a/src/client_cli/cmdVM/vmCred2pdf.go
+++ b/src/client_cli/cmdVM/vmCred2pdf.go
@@ -14,13 +14,17 @@ func (cmd *Cred2pdf)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *Cred2pdf)GetDesc() string {
-	return "Create a PDF with the credentials required to attach to running VMs (regex matching)."
+func (cmd *Cred2pdf)GetDesc() []string {
+	return []string{
+		"Creates a PDF with the credentials required to attach to running VMs (regex matching).",
+		"Requires VM_LIST VM access capability or VM_LIST_ANY user capability."}
 }
 
 func (cmd *Cred2pdf)PrintUsage() {
-	u.PrintlnErr(cmd.GetDesc())
-	u.PrintlnErr("Usage: ",cmd.GetName()," [ID ...] [regex ...] pdfile")
+	for _, desc := range cmd.GetDesc() {
+		u.PrintlnErr(desc)
+	}
+	u.PrintlnErr("USAGE: ",cmd.GetName()," [ID ...] [regex ...] pdfile")
 	printRegexUsageDetails()
 }
 
diff --git a/src/client_cli/cmdVM/vmDel.go b/src/client_cli/cmdVM/vmDel.go
index 71a27f7..a5cf919 100644
--- a/src/client_cli/cmdVM/vmDel.go
+++ b/src/client_cli/cmdVM/vmDel.go
@@ -13,8 +13,10 @@ func (cmd *Del)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *Del)GetDesc() string {
-	return "Delete one or more VMs (regex matching)."
+func (cmd *Del)GetDesc() []string {
+	return []string{
+		"Deletes one or more VMs (regex matching).",
+		"Requires VM_DESTROY VM access capability or VM_DESTROY_ANY user capability."}
 }
 
 func (cmd *Del)PrintUsage() {
diff --git a/src/client_cli/cmdVM/vmDelAccess.go b/src/client_cli/cmdVM/vmDelAccess.go
index 3c80b2e..f57a089 100644
--- a/src/client_cli/cmdVM/vmDelAccess.go
+++ b/src/client_cli/cmdVM/vmDelAccess.go
@@ -19,13 +19,17 @@ func (cmd *DelAccess)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *DelAccess)GetDesc() string {
-	return "Delete a user's VM access in one or more VMs (regex matching)."
+func (cmd *DelAccess)GetDesc() []string {
+	return []string{
+		"Deletes a user's VM access in one or more VMs (regex matching).",
+		"Requires VM_SET_ACCESS user capability and VM_SET_ACCESS VM access capability."}
 }
 
 func (cmd *DelAccess)PrintUsage() {
-	u.PrintlnErr(cmd.GetDesc())
-	u.PrintlnErr("Usage: "+cmd.GetName()+" [ID ...] [regex ...] email")
+	for _, desc := range cmd.GetDesc() {
+		u.PrintlnErr(desc)
+	}
+	u.PrintlnErr("USAGE: "+cmd.GetName()+" [ID ...] [regex ...] email")
 	const usage string = `Only VMs matching the specified IDs or regexes will have their VM access deleted.
 Any number of IDs or regexes can be specified.
 The regex only matches the VM's name and is case-insensitive.
diff --git a/src/client_cli/cmdVM/vmEdit.go b/src/client_cli/cmdVM/vmEdit.go
index dc81d3c..02f1290 100644
--- a/src/client_cli/cmdVM/vmEdit.go
+++ b/src/client_cli/cmdVM/vmEdit.go
@@ -23,13 +23,17 @@ func (cmd *Edit)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *Edit)GetDesc() string {
-	return "Edit one or more VMs' properties: name, cpus, ram or nic (regex matching)."
+func (cmd *Edit)GetDesc() []string {
+	return []string{
+		"Edits one or more VMs' properties: name, cpus, ram or nic (regex matching).",
+		"Requires VM_EDIT VM access capability or VM_EDIT_ANY user capability."}
 }
 
 func (cmd *Edit)PrintUsage() {
-	u.PrintlnErr(cmd.GetDesc())
-	u.PrintlnErr("Usage: "+cmd.GetName()+" [ID ...] [regex ...] [name=\"new name\"] [cpus=n] [ram=n] [nic=none/user]")
+	for _, desc := range cmd.GetDesc() {
+		u.PrintlnErr(desc)
+	}
+	u.PrintlnErr("USAGE: "+cmd.GetName()+" [ID ...] [regex ...] [name=\"new name\"] [cpus=n] [ram=n] [nic=none/user]")
 	const usage string = `Only VMs matching the specified IDs or regexes will be edited.
 Any number of IDs or regexes can be specified.
 The regex only matches the VM's name and is case-insensitive.
diff --git a/src/client_cli/cmdVM/vmExportDir.go b/src/client_cli/cmdVM/vmExportDir.go
index b9c5694..9d9af86 100644
--- a/src/client_cli/cmdVM/vmExportDir.go
+++ b/src/client_cli/cmdVM/vmExportDir.go
@@ -17,13 +17,18 @@ func (cmd *ExportDir)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *ExportDir)GetDesc() string {
-	return "Export one or more VMs' directory into one or more tar archives. Create one archive per VM (regex matching)."
+func (cmd *ExportDir)GetDesc() []string {
+	return []string{
+		"Exports one or more VMs' directory into one or more tar archives.",
+		"Creates one archive per VM (regex matching).",
+		"Requires VM_READFS VM access capability or VM_READFS_ANY user capability."}
 }
 
 func (cmd *ExportDir)PrintUsage() {
-	u.PrintlnErr(cmd.GetDesc())
-	u.PrintlnErr("Usage: ",cmd.GetName()," [ID ...] [regex ...] dir")
+	for _, desc := range cmd.GetDesc() {
+		u.PrintlnErr(desc)
+	}
+	u.PrintlnErr("USAGE: ",cmd.GetName()," [ID ...] [regex ...] dir")
 	u.PrintlnErr("\"dir\" is the directory in the VM to export into a tar archive named after the VM ID.")
 	printRegexUsageDetails()
 }
diff --git a/src/client_cli/cmdVM/vmList.go b/src/client_cli/cmdVM/vmList.go
index d8b1fe1..83a3a4a 100644
--- a/src/client_cli/cmdVM/vmList.go
+++ b/src/client_cli/cmdVM/vmList.go
@@ -12,13 +12,17 @@ func (cmd *List)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *List)GetDesc() string {
-	return "List VMs that can be listed (regex matching)."
+func (cmd *List)GetDesc() []string {
+	return []string{
+		"Lists VMs that can be listed (regex matching).",
+		"Requires VM_LIST VM access capability or VM_LIST_ANY user capability."}
 }
 
 func (cmd *List)PrintUsage() {
-	u.PrintlnErr(cmd.GetDesc())
-	u.PrintlnErr("Usage: ",cmd.GetName(), " [-l] [ID ...] [regex ...]")
+	for _, desc := range cmd.GetDesc() {
+		u.PrintlnErr(desc)
+	}
+	u.PrintlnErr("USAGE: ",cmd.GetName(), " [-l] [ID ...] [regex ...]")
 	u.PrintlnErr("Use \"-l\" to specify detailed VMs output.")
 	printRegexUsageDetails()
 }
diff --git a/src/client_cli/cmdVM/vmListAttach.go b/src/client_cli/cmdVM/vmListAttach.go
index bc5a3ad..77b7322 100644
--- a/src/client_cli/cmdVM/vmListAttach.go
+++ b/src/client_cli/cmdVM/vmListAttach.go
@@ -8,8 +8,10 @@ func (cmd *ListAttach)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *ListAttach)GetDesc() string {
-	return "List VMs that can be attached to (regex matching)."
+func (cmd *ListAttach)GetDesc() []string {
+	return []string{
+		"Lists VMs that can be attached to (regex matching).",
+		"Requires VM_LIST VM access capability or VM_LIST_ANY user capability."}
 }
 
 func (cmd *ListAttach)PrintUsage() {
diff --git a/src/client_cli/cmdVM/vmListDel.go b/src/client_cli/cmdVM/vmListDel.go
index 0e22640..0da557d 100644
--- a/src/client_cli/cmdVM/vmListDel.go
+++ b/src/client_cli/cmdVM/vmListDel.go
@@ -8,8 +8,10 @@ func (cmd *ListDel)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *ListDel)GetDesc() string {
-	return "List VMs that can be deleted (regex matching)."
+func (cmd *ListDel)GetDesc() []string {
+	return []string{
+		"Lists VMs that can be deleted (regex matching).",
+		"Requires VM_DESTROY VM access capability or VM_DESTROY_ANY user capability."}
 }
 
 func (cmd *ListDel)PrintUsage() {
diff --git a/src/client_cli/cmdVM/vmListEdit.go b/src/client_cli/cmdVM/vmListEdit.go
index 7d45a9b..4c3c207 100644
--- a/src/client_cli/cmdVM/vmListEdit.go
+++ b/src/client_cli/cmdVM/vmListEdit.go
@@ -8,8 +8,10 @@ func (cmd *ListEdit)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *ListEdit)GetDesc() string {
-	return "List VMs that can be edited (regex matching)."
+func (cmd *ListEdit)GetDesc() []string {
+	return []string{
+		"Lists VMs that can be edited (regex matching).",
+		"Requires VM_EDIT VM access capability or VM_EDIT_ANY user capability."}
 }
 
 func (cmd *ListEdit)PrintUsage() {
diff --git a/src/client_cli/cmdVM/vmListEditAccess.go b/src/client_cli/cmdVM/vmListEditAccess.go
index 7de1add..47bf73c 100644
--- a/src/client_cli/cmdVM/vmListEditAccess.go
+++ b/src/client_cli/cmdVM/vmListEditAccess.go
@@ -8,8 +8,10 @@ func (cmd *ListEditAccess)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *ListEditAccess)GetDesc() string {
-	return "List VMs that can have their VM access edited (regex matching)."
+func (cmd *ListEditAccess)GetDesc() []string {
+	return []string{
+		"Lists VMs that can have their VM access edited (regex matching).",
+		"Requires VM_SET_ACCESS user capability and VM_SET_ACCESS VM access capability."}
 }
 
 func (cmd *ListEditAccess)PrintUsage() {
diff --git a/src/client_cli/cmdVM/vmListExportDir.go b/src/client_cli/cmdVM/vmListExportDir.go
index d6e5653..abe2593 100644
--- a/src/client_cli/cmdVM/vmListExportDir.go
+++ b/src/client_cli/cmdVM/vmListExportDir.go
@@ -8,8 +8,10 @@ func (cmd *ListExportDir)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *ListExportDir)GetDesc() string {
-	return "List VMs that can have a directory exported (regex matching)."
+func (cmd *ListExportDir)GetDesc() []string {
+	return []string{
+		"List VMs that can have a directory exported (regex matching).",
+		"Requires VM_READFS VM access capability or VM_READFS_ANY user capability."}
 }
 
 func (cmd *ListExportDir)PrintUsage() {
diff --git a/src/client_cli/cmdVM/vmListStart.go b/src/client_cli/cmdVM/vmListStart.go
index 2a04678..85ba5d6 100644
--- a/src/client_cli/cmdVM/vmListStart.go
+++ b/src/client_cli/cmdVM/vmListStart.go
@@ -8,8 +8,10 @@ func (cmd *ListStart)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *ListStart)GetDesc() string {
-	return "List VMs that can be started (regex matching)."
+func (cmd *ListStart)GetDesc() []string {
+	return []string{
+		"Lists VMs that can be started (regex matching).",
+		"Requires VM_START VM access capability or VM_START_ANY user capability."}
 }
 
 func (cmd *ListStart)PrintUsage() {
diff --git a/src/client_cli/cmdVM/vmListStop.go b/src/client_cli/cmdVM/vmListStop.go
index 99bac9f..76da9b6 100644
--- a/src/client_cli/cmdVM/vmListStop.go
+++ b/src/client_cli/cmdVM/vmListStop.go
@@ -8,8 +8,10 @@ func (cmd *ListStop)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *ListStop)GetDesc() string {
-	return "List VMs that can be stopped or shutdown (regex matching)."
+func (cmd *ListStop)GetDesc() []string {
+	return []string{
+		"Lists VMs that can be stopped or shutdown (regex matching).",
+		"Requires VM_STOP VM access capability or VM_STOP_ANY user capability."}
 }
 
 func (cmd *ListStop)PrintUsage() {
diff --git a/src/client_cli/cmdVM/vmSetAccess.go b/src/client_cli/cmdVM/vmSetAccess.go
index 50cd240..f6c1c80 100644
--- a/src/client_cli/cmdVM/vmSetAccess.go
+++ b/src/client_cli/cmdVM/vmSetAccess.go
@@ -15,13 +15,17 @@ func (cmd *SetAccess)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *SetAccess)GetDesc() string {
-	return "Set a user's VM access in one or more VMs (regex matching)."
+func (cmd *SetAccess)GetDesc() []string {
+	return []string{
+		"Sets a user's VM access in one or more VMs (regex matching).",
+		"Requires VM_SET_ACCESS user capability and VM_SET_ACCESS VM access capability."}
 }
 
 func (cmd *SetAccess)PrintUsage() {
-	u.PrintlnErr(cmd.GetDesc())
-	u.PrintlnErr("Usage: "+cmd.GetName()+" [ID ...] [regex ...] email [capability ...]")
+	for _, desc := range cmd.GetDesc() {
+		u.PrintlnErr(desc)
+	}
+	u.PrintlnErr("USAGE: "+cmd.GetName()+" [ID ...] [regex ...] email [capability ...]")
 	const usage string = `Capabilities must be specified after the email.
 Only VMs matching the specified IDs or regexes will have their VM access set.
 Any number of IDs or regexes can be specified.
diff --git a/src/client_cli/cmdVM/vmShutdown.go b/src/client_cli/cmdVM/vmShutdown.go
index 22be937..a091036 100644
--- a/src/client_cli/cmdVM/vmShutdown.go
+++ b/src/client_cli/cmdVM/vmShutdown.go
@@ -13,8 +13,10 @@ func (cmd *Shutdown)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *Shutdown)GetDesc() string {
-	return "Gracefully shutdown one or more VMs (regex matching)."
+func (cmd *Shutdown)GetDesc() []string {
+	return []string{
+		"Gracefully shutdowns one or more VMs (regex matching).",
+		"Requires VM_STOP VM access capability or VM_STOP_ANY user capability."}
 }
 
 func (cmd *Shutdown)PrintUsage() {
diff --git a/src/client_cli/cmdVM/vmStart.go b/src/client_cli/cmdVM/vmStart.go
index 33c7340..1fa62e5 100644
--- a/src/client_cli/cmdVM/vmStart.go
+++ b/src/client_cli/cmdVM/vmStart.go
@@ -13,8 +13,10 @@ func (cmd *Start)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *Start)GetDesc() string {
-	return "Start one or more VMs (regex matching)."
+func (cmd *Start)GetDesc() []string {
+	return []string{
+		"Starts one or more VMs (regex matching).",
+		"Requires VM_START VM access capability or VM_START_ANY user capability."}
 }
 
 func (cmd *Start)PrintUsage() {
diff --git a/src/client_cli/cmdVM/vmStop.go b/src/client_cli/cmdVM/vmStop.go
index f67528b..edc3a8e 100644
--- a/src/client_cli/cmdVM/vmStop.go
+++ b/src/client_cli/cmdVM/vmStop.go
@@ -13,8 +13,10 @@ func (cmd *Stop)GetName() string {
 	return cmd.Name
 }
  
-func (cmd *Stop)GetDesc() string {
-	return "Stop by force one or more VMs (regex matching)."
+func (cmd *Stop)GetDesc() []string {
+	return []string{
+		"Stops by force one or more VMs (regex matching).",
+		"Requires VM_STOP VM access capability or VM_STOP_ANY user capability."}
 }
 
 func (cmd *Stop)PrintUsage() {
diff --git a/src/client_cli/nexus-client.go b/src/client_cli/nexus-client.go
index 939387d..8202ece 100644
--- a/src/client_cli/nexus-client.go
+++ b/src/client_cli/nexus-client.go
@@ -91,9 +91,16 @@ func main() {
 		u.PrintlnErr("List of supported Commands:")
 		padding := 20
 		for _, cmd := range cmdList {
-			u.PrintErr("    "+cmd.GetName())
+			u.PrintlnErr("")
+			cmdStr := "    "+cmd.GetName()
+			u.PrintErr(cmdStr)
 			pad := strings.Repeat(" ", padding-len(cmd.GetName()))
-			u.PrintlnErr(pad+cmd.GetDesc())
+			for i, desc := range cmd.GetDesc() {
+				if i > 0 {
+					u.PrintErr(strings.Repeat(" ", len(cmdStr)))
+				}
+				u.PrintlnErr(pad+desc)
+			}
 		}
 		os.Exit(1)
 	}
-- 
GitLab