From c45125fbe04b3f8f139deae2844d36fa5e7be250 Mon Sep 17 00:00:00 2001
From: Florent Gluck <florent.gluck@hesge.ch>
Date: Thu, 18 May 2023 00:50:04 +0200
Subject: [PATCH] Added new command: --------------------- vmstartwithcreds   
 Starts one or more VMs with user-defined credentials.                    
 Requires VM_START VM access capability or VM_START_ANY user capability.
 --------------------- client: fixed ReadCSVColumn which didn't work if
 separator char was not a comma

---
 docs/README_client.md                |  18 ++++-
 src/client/cmdVM/vmStartWithCreds.go | 106 +++++++++++++++++++++++++++
 src/client/nexus-cli/nexus-cli.go    |   1 +
 src/client/nexush/nexush.go          |   1 +
 src/client/utils/csv.go              |   8 +-
 src/server/router/routerVMs.go       |   7 +-
 src/server/vms/vms.go                |   6 ++
 7 files changed, 138 insertions(+), 9 deletions(-)
 create mode 100644 src/client/cmdVM/vmStartWithCreds.go

diff --git a/docs/README_client.md b/docs/README_client.md
index 090d001..f732439 100644
--- a/docs/README_client.md
+++ b/docs/README_client.md
@@ -140,6 +140,14 @@ nexush> help
 ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
 ls                  List files in the specified dir or in the current dir if no argument is specified.
 ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+refresh             Obtains a new access token.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+version             Display nexus server and client's versions.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+whoami              Displays the current user's details.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+passwd              Updates the current user's password.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
 userlist            Lists users.
                     Requires USER_LIST user capability.
 ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
@@ -158,12 +166,16 @@ vmlist              Lists VMs.
 vmcred2pdf          Creates a PDF with the credentials required to attach to running VMs.
                     Requires VM_LIST VM access capability or VM_LIST_ANY user capability.
 ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
-vmcred2txt          Creates a text file with the credentials required to attach to running VMs.
+vmcred2csv          Creates a CSV file with the credentials required to attach to running VMs.
+                    The written CSV file contains 4 columns: VM ID;VM name;port;password
                     Requires VM_LIST VM access capability or VM_LIST_ANY user capability.
 ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
 vmstart             Starts one or more VMs.
                     Requires VM_START VM access capability or VM_START_ANY user capability.
 ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+vmstartwithcreds    Starts one or more VMs with user-defined credentials.
+                    Requires VM_START VM access capability or VM_START_ANY user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
 vmkill              Kills one or more VMs.
                     Requires VM_STOP VM access capability or VM_STOP_ANY user capability.
 ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
@@ -191,13 +203,13 @@ vmaddaccess         Adds a user's VM access in one or more VMs.
 vmdelaccess         Removes a user's VM access in one or more VMs.
                     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.
+vmexportdir         Exports one or more VMs' directory into one or more compressed archives.
                     Requires VM_READFS VM access capability or VM_READFS_ANY user capability.
 ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
 vmimportdir         Copies a local directory (or file) and all its content into one or more VMs.
                     Requires VM_WRITEFS VM access capability or VM_WRITEFS_ANY user capability.
 ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
-tpllist             Lists available templates.
+tpllist             Lists templates.
                     Requires TPL_LIST or TPL_LIST_ANY user capability.
 ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
 tplcreate           Creates a template from an existing VM.
diff --git a/src/client/cmdVM/vmStartWithCreds.go b/src/client/cmdVM/vmStartWithCreds.go
new file mode 100644
index 0000000..e8bbbef
--- /dev/null
+++ b/src/client/cmdVM/vmStartWithCreds.go
@@ -0,0 +1,106 @@
+package cmdVM
+
+import (
+    "errors"
+    "strconv"
+    u "nexus-client/utils"
+    g "nexus-client/globals"
+)
+
+type StartWithCreds struct {
+    Name string
+}
+
+func (cmd *StartWithCreds)GetName() string {
+    return cmd.Name
+}
+
+func (cmd *StartWithCreds)GetDesc() []string {
+    return []string{
+        "Starts one or more VMs with user-defined credentials.",
+        "Requires VM_START VM access capability or VM_START_ANY user capability."}
+}
+
+func (cmd *StartWithCreds)PrintUsage() {
+    for _, desc := range cmd.GetDesc() {
+        u.PrintlnErr(desc)
+    }
+    u.PrintlnErr("―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――")
+    u.PrintlnErr("USAGE: "+cmd.GetName()+" file.csv")
+    u.PrintlnErr("―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――")
+    const usage string = `file.csv    4-column CSV file defining the VMs to start and their credentials.
+            Content of the 4 columns: VM ID;VM name;port;password.`
+    u.PrintlnErr(usage)
+}
+
+func (cmd *StartWithCreds)parseCSVFile(csvFile string) ([]string, []string, []string, error) {
+    // Column 0: VM IDs
+    vmIDs, err := u.ReadCSVColumn(csvFile, 0)
+    if err != nil {
+        return nil, nil, nil, err
+    }
+    // Column 2: ports
+    ports, err := u.ReadCSVColumn(csvFile, 2)
+    if err != nil {
+        return nil, nil, nil, err
+    }
+    // Column 3: passwords
+    pwds, err := u.ReadCSVColumn(csvFile, 3)
+    if err != nil {
+        return nil, nil, nil, err
+    }
+    count := len(vmIDs)
+    if (len(ports) != count) || (len(pwds) != count) {
+        return nil, nil, nil, errors.New("Invalid CSV file: all columns must have the same number of entries!")
+    }
+    return vmIDs, ports, pwds, nil
+}
+
+func (cmd *StartWithCreds)Run(args []string) int {
+    client := g.GetInstance().Client
+    host := g.GetInstance().Host
+
+    argc := len(args)
+    if argc != 1 {
+        cmd.PrintUsage()
+        return 1
+    }
+
+    vmIDs, ports, pwds, err := cmd.parseCSVFile(args[0])
+    if err != nil {
+        u.PrintlnErr(err.Error())
+        return 1
+    }
+
+    statusCode := 0
+
+    type VMArgs struct {
+        Port int
+        Pwd string
+    }
+
+    vmArgs := &VMArgs {}
+
+    for i, vmID := range(vmIDs) {
+        port, err := strconv.Atoi(ports[i])
+        if err != nil {
+            u.PrintlnErr("Line ",i,": invalid port number")
+        }
+        vmArgs.Port = port
+        vmArgs.Pwd = pwds[i]
+        resp, err := client.R().SetBody(vmArgs).Put(host+"/vms/"+vmID+"/startwithcreds")
+        if err != nil {
+            u.PrintlnErr("Failed starting VM \""+vmID+"\": "+err.Error())
+            statusCode = 1
+        } else {
+            if resp.IsSuccess() {
+                u.Println("Started VM \""+vmID+"\"")
+            } else {
+                u.PrintlnErr("Failed starting VM \""+vmID+"\": "+resp.Status()+": "+resp.String())
+                statusCode = 1
+            }
+        }
+    }
+
+    return statusCode
+}
diff --git a/src/client/nexus-cli/nexus-cli.go b/src/client/nexus-cli/nexus-cli.go
index d704775..e8a4732 100644
--- a/src/client/nexus-cli/nexus-cli.go
+++ b/src/client/nexus-cli/nexus-cli.go
@@ -38,6 +38,7 @@ var cmdList = []cmd.Command {
     &cmdVM.Cred2pdf{"vmcred2pdf"},
     &cmdVM.Cred2csv{"vmcred2csv"},
     &cmdVM.Start{"vmstart"},
+    &cmdVM.StartWithCreds{"vmstartwithcreds"},
     &cmdVM.Stop{"vmkill"},
     &cmdVM.Shutdown{"vmshutdown"},
     &cmdVM.Reboot{"vmreboot"},
diff --git a/src/client/nexush/nexush.go b/src/client/nexush/nexush.go
index e156e7c..152172d 100644
--- a/src/client/nexush/nexush.go
+++ b/src/client/nexush/nexush.go
@@ -45,6 +45,7 @@ var cmdList = []cmd.Command {
     &cmdVM.Cred2pdf{"vmcred2pdf"},
     &cmdVM.Cred2csv{"vmcred2csv"},
     &cmdVM.Start{"vmstart"},
+    &cmdVM.StartWithCreds{"vmstartwithcreds"},
     &cmdVM.Stop{"vmkill"},
     &cmdVM.Shutdown{"vmshutdown"},
     &cmdVM.Reboot{"vmreboot"},
diff --git a/src/client/utils/csv.go b/src/client/utils/csv.go
index 552f5f3..8441477 100644
--- a/src/client/utils/csv.go
+++ b/src/client/utils/csv.go
@@ -4,6 +4,7 @@ import (
     "os"
     "io"
     "errors"
+    "strconv"
     "encoding/csv"
 )
 
@@ -16,6 +17,8 @@ func ReadCSVColumn(csvFile string, column int) ([]string, error) {
     }
     defer file.Close()
     reader := csv.NewReader(file)
+    reader.Comma = ';'    // specify the column separator
+    reader.Comment = '#'  // lines starting with this symbol are to be ignored
 
     var entries []string
     for {
@@ -26,8 +29,9 @@ func ReadCSVColumn(csvFile string, column int) ([]string, error) {
         if err != nil {
             return nil, errors.New("Failed reading "+err.Error())
         }
-        if column >= len(record) {
-            return nil, errors.New("Failed reading \""+csvFile+"\": column out of bound")
+        colCount := len(record)
+        if column >= colCount {
+            return nil, errors.New("Failed reading \""+csvFile+"\": column out of bound (detected "+strconv.Itoa(colCount)+" columns)")
         }
         entries = append(entries, record[column])
     }
diff --git a/src/server/router/routerVMs.go b/src/server/router/routerVMs.go
index 6467f70..0664134 100644
--- a/src/server/router/routerVMs.go
+++ b/src/server/router/routerVMs.go
@@ -244,16 +244,15 @@ func (r *RouterVMs)StartVMWithCreds(c echo.Context) error {
 
         // Deserializes and validates client's parameters.
         type Parameters struct {
-            port int              `json:"port"       validate:"required,gte=1100,lte=65535"`
-            pwd string            `json:"pwd"        validate:"required,min=8,max=64"`
+            Port int              `json:"port"       validate:"required,gte=1100,lte=65535"`
+            Pwd string            `json:"pwd"        validate:"required,min=8,max=64"`
         }
         p := new(Parameters)
         if err := decodeJson(c, &p); err != nil {
             return echo.NewHTTPError(http.StatusBadRequest, err.Error())
         }
 
-        _, _, err := r.vms.StartVM(vm.GetID())
-        if err != nil {
+        if err := r.vms.StartVMWithCreds(vm.GetID(), p.Port, p.Pwd); err != nil {
             return echo.NewHTTPError(http.StatusBadRequest, err.Error())
         }
         return c.JSONPretty(http.StatusOK, jsonMsg("OK"), "    ")
diff --git a/src/server/vms/vms.go b/src/server/vms/vms.go
index 6d2edad..15447ad 100644
--- a/src/server/vms/vms.go
+++ b/src/server/vms/vms.go
@@ -177,6 +177,12 @@ func (vms *VMs)StartVMWithCreds(vmID uuid.UUID, port int, pwd string) error {
     vms.rwlock.Lock()
     defer vms.rwlock.Unlock()
 
+    if vms.usedPorts[port] || !utils.IsPortAvailable(port) {
+        return errors.New("Failed starting VM: port already in use")
+    } else {
+        vms.usedPorts[port] = true
+    }
+
     vm, err := vms.getVMUnsafe(vmID)
     if err != nil {
         return err
-- 
GitLab