Skip to content

Commit

Permalink
feat(ui): connect ISO file to VM as optical disc
Browse files Browse the repository at this point in the history
Adds button to UI VM modal for adding, changing, and ejecting ISO files
to/from VMs. The ISO file to connect to the VM must already be on the
phēnix head node in the minimega files directory.
  • Loading branch information
eric-c-wood authored and activeshadow committed Oct 3, 2023
1 parent 8c938fb commit 12192c1
Show file tree
Hide file tree
Showing 10 changed files with 331 additions and 67 deletions.
63 changes: 33 additions & 30 deletions src/go/api/cluster/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import (
"phenix/util/mm/mmcli"
)

type ImageKind int
type ImageKind uint8
type CopyStatus func(float64)

const (
_ ImageKind = iota
UNKNOWN ImageKind = 1 << iota
VM_IMAGE
CONTAINER_IMAGE
ISO_IMAGE
)

type ImageDetails struct {
Expand All @@ -29,6 +30,7 @@ type ImageDetails struct {
}

var DefaultClusterFiles ClusterFiles = new(MMClusterFiles)
var mmFilesDirectory = util.GetMMFilesDirectory()

type ClusterFiles interface {
// Get list of VM disk images, container filesystems, or both.
Expand Down Expand Up @@ -85,8 +87,8 @@ func (MMClusterFiles) GetImages(expName string, kind ImageKind) ([]ImageDetails,
// Using a map here to weed out duplicates.
details := make(map[string]ImageDetails)

// Add all the files relative to the minimega files directory
if err := getAllFiles("", expName, details); err != nil {
// Add all the files from the minimega files directory
if err := getAllFiles(details); err != nil {
return nil, err
}

Expand All @@ -101,6 +103,11 @@ func (MMClusterFiles) GetImages(expName string, kind ImageKind) ([]ImageDetails,
var images []ImageDetails

for name := range details {
// Only return image types that were requested
if kind & details[name].Kind == 0 {
continue
}

images = append(images, details[name])
}

Expand Down Expand Up @@ -263,17 +270,11 @@ func (MMClusterFiles) DeleteFile(path string) error {
return nil
}

// Get all image files relative to the minimega files directory
func getAllFiles(path, expName string, details map[string]ImageDetails) error {

expNames, err := getExperimentNames()

if err != nil {
return err
}
// Get all image files from the minimega files directory
func getAllFiles(details map[string]ImageDetails) error {

// First get file listings from mesh, then from headnode.
commands := []string{"mesh send all file list " + path, "file list " + path}
commands := []string{"mesh send all file list", "file list"}

// First, get file listings from cluster nodes.
cmd := mmcli.NewCommand()
Expand All @@ -283,25 +284,13 @@ func getAllFiles(path, expName string, details map[string]ImageDetails) error {

for _, row := range mmcli.RunTabular(cmd) {

// Enumerate files for any sudirectories found
// Only look in the base directory
if row["dir"] != "" {
// Only explore experiment subdirectories relevant to the experiment
if _, ok := expNames[row["name"]]; ok {

// Make sure the experiment exists
if _, ok := expNames[expName]; !ok {
continue
}

if !strings.Contains(row["name"], expName) {
continue
}
}

getAllFiles(row["name"], expName, details)
continue
}

baseName := filepath.Base(row["name"])

// Avoid adding the same image twice
if _, ok := details[baseName]; ok {
continue
Expand All @@ -318,6 +307,8 @@ func getAllFiles(path, expName string, details map[string]ImageDetails) error {
image.Kind = CONTAINER_IMAGE
} else if strings.HasSuffix(image.Name, ".hdd") {
image.Kind = VM_IMAGE
} else if strings.HasSuffix(image.Name, ".iso") {
image.Kind = ISO_IMAGE
} else {
continue
}
Expand Down Expand Up @@ -345,10 +336,22 @@ func getTopologyFiles(expName string, details map[string]ImageDetails) error {
return fmt.Errorf("unable to retrieve %v", expName)
}


for _, node := range exp.Spec.Topology().Nodes() {
for _, drive := range node.Hardware().Drives() {
cmd := mmcli.NewCommand()
cmd.Command = "file list " + drive.Image()

if len(drive.Image()) == 0 {
continue
}

relMMPath,_ := filepath.Rel(mmFilesDirectory,drive.Image())

if len(relMMPath) == 0 {
relMMPath = drive.Image()
}

cmd.Command = "file list " + relMMPath

for _, row := range mmcli.RunTabular(cmd) {
if row["dir"] != "" {
Expand All @@ -364,7 +367,7 @@ func getTopologyFiles(expName string, details map[string]ImageDetails) error {

image := ImageDetails{
Name: baseName,
FullPath: util.GetMMFullPath(drive.Image()),
FullPath: util.GetMMFullPath(row["name"]),
Kind: VM_IMAGE,
}

Expand Down
54 changes: 54 additions & 0 deletions src/go/api/vm/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func List(expName string) ([]mm.VM, error) {
vm.Taps = details.Taps
vm.IPv4 = details.IPv4
vm.Captures = details.Captures
vm.CdRom = details.CdRom
vm.Tags = details.Tags
vm.Uptime = details.Uptime
vm.CPUs = details.CPUs
Expand Down Expand Up @@ -229,6 +230,7 @@ func Get(expName, vmName string) (*mm.VM, error) {
vm.Taps = details[0].Taps
vm.IPv4 = details[0].IPv4
vm.Captures = details[0].Captures
vm.CdRom = details[0].CdRom
vm.Tags = details[0].Tags
vm.Uptime = details[0].Uptime
vm.CPUs = details[0].CPUs
Expand Down Expand Up @@ -1459,3 +1461,55 @@ func StopCaptureSubnet(expName, subnet string, vmList []string) ([]string, error
return matchedVMs, nil

}

// Changes the optical disc in the first drive
func ChangeOpticalDisc(expName, vmName, isoPath string) error {

if expName == "" {
return fmt.Errorf("no experiment name provided")
}

if vmName == "" {
return fmt.Errorf("no VM name provided")
}

if isoPath == "" {
return fmt.Errorf("no optical disc path provided")
}


cmd := mmcli.NewNamespacedCommand(expName)
cmd.Command = fmt.Sprintf("vm cdrom change %s %s",vmName,isoPath)

if err := mmcli.ErrorResponse(mmcli.Run(cmd)); err != nil {
return fmt.Errorf("changing optical disc for VM %s: %w", vmName, err)
}



return nil
}

// Ejects the optical disc in the first drive
func EjectOpticalDisc(expName, vmName string) error {

if expName == "" {
return fmt.Errorf("no experiment name provided")
}

if vmName == "" {
return fmt.Errorf("no VM name provided")
}


cmd := mmcli.NewNamespacedCommand(expName)
cmd.Command = fmt.Sprintf("vm cdrom eject %s",vmName)

if err := mmcli.ErrorResponse(mmcli.Run(cmd)); err != nil {
return fmt.Errorf("ejecting optical disc for VM %s: %w", vmName, err)
}


return nil
}

4 changes: 2 additions & 2 deletions src/go/util/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

var (
filePathRe = regexp.MustCompile(`filepath=([^ ]+)`)
mmFilesDirectory = getMMFilesDirectory()
mmFilesDirectory = GetMMFilesDirectory()
)

// Returns the full path relative to the minimega files directory
Expand All @@ -29,7 +29,7 @@ func GetMMFullPath(path string) string {
}

// Tries to extract the minimega files directory from a process listing
func getMMFilesDirectory() string {
func GetMMFilesDirectory() string {
defaultMMFilesDirectory := fmt.Sprintf("%s/images", common.PhenixBase)

cmd := "ps"
Expand Down
3 changes: 2 additions & 1 deletion src/go/util/mm/minimega.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func (this Minimega) GetVMInfo(opts ...Option) VMs {

cmd := mmcli.NewNamespacedCommand(o.ns)
cmd.Command = "vm info"
cmd.Columns = []string{"uuid", "host", "name", "state", "uptime", "vlan", "tap", "ip", "memory", "vcpus", "disks", "snapshot", "tags"}
cmd.Columns = []string{"uuid", "host", "name", "state", "uptime", "vlan", "tap", "ip", "memory", "vcpus", "disks", "snapshot", "cdrom", "tags"}

if o.vm != "" {
cmd.Filters = []string{"name=" + o.vm}
Expand All @@ -155,6 +155,7 @@ func (this Minimega) GetVMInfo(opts ...Option) VMs {
State: row["state"],
Running: row["state"] == "RUNNING",
CCActive: activeC2[row["uuid"]],
CdRom: row["cdrom"],
}

s := row["vlan"]
Expand Down
1 change: 1 addition & 0 deletions src/go/util/mm/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ type VM struct {
CCActive bool `json:"ccActive"`
Uptime float64 `json:"uptime"`
Screenshot string `json:"screenshot,omitempty"`
CdRom string `json:"cdRom"`
Tags []string `json:"tags"`

// Used internally to track network <--> IP relationship, since
Expand Down
88 changes: 83 additions & 5 deletions src/go/web/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2646,18 +2646,26 @@ func GetDisks(w http.ResponseWriter, r *http.Request) {
plog.Debug("HTTP handler called", "handler", "GetDisks")

var (
ctx = r.Context()
role = ctx.Value("role").(rbac.Role)
query = r.URL.Query()
expName = query.Get("expName")
ctx = r.Context()
role = ctx.Value("role").(rbac.Role)
query = r.URL.Query()
expName = query.Get("expName")
diskType = query.Get("diskType")
defaultDiskType = cluster.VM_IMAGE | cluster.CONTAINER_IMAGE
)

if !role.Allowed("disks", "list") {
http.Error(w, "forbidden", http.StatusForbidden)
return
}

disks, err := cluster.GetImages(expName, cluster.VM_IMAGE)
if len(diskType) > 0 {
if strings.Contains(diskType, "ISO") {
defaultDiskType = cluster.ISO_IMAGE
}
}

disks, err := cluster.GetImages(expName, defaultDiskType)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
Expand Down Expand Up @@ -2908,6 +2916,76 @@ func WsConsole(w http.ResponseWriter, r *http.Request) {
}).ServeHTTP(w, r)
}

// POST /experiments/{exp}/vms/{name}/cdrom
func ChangeOpticalDisc(w http.ResponseWriter, r *http.Request) {
plog.Debug("HTTP handler called", "handler", "ChangeOpticalDisc")

var (
ctx = r.Context()
role = ctx.Value("role").(rbac.Role)
query = r.URL.Query()
vars = mux.Vars(r)
exp = vars["exp"]
name = vars["name"]
isoPath = query.Get("isoPath")
fullName = exp + "/" + name
)

if !role.Allowed("vms/cdrom", "update", fullName) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}

if err := vm.ChangeOpticalDisc(exp, name, isoPath); err != nil {
plog.Error("changing disc for VM", "exp", exp, "vm", name, "err", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

broker.Broadcast(
bt.NewRequestPolicy("vms/cdrom", "update", fullName),
bt.NewResource("experiment/vm", fullName, "cdrom-inserted"),
nil,
)

w.WriteHeader(http.StatusNoContent)

}

// DELETE /experiments/{exp}/vms/{name}/cdrom
func EjectOpticalDisc(w http.ResponseWriter, r *http.Request) {
plog.Debug("HTTP handler called", "handler", "EjectOpticalDisc")

var (
ctx = r.Context()
role = ctx.Value("role").(rbac.Role)
vars = mux.Vars(r)
exp = vars["exp"]
name = vars["name"]
fullName = exp + "/" + name
)

if !role.Allowed("vms/cdrom", "delete", fullName) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}

if err := vm.EjectOpticalDisc(exp, name); err != nil {
plog.Error("ejecting disc for VM", "exp", exp, "vm", name, "err", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

broker.Broadcast(
bt.NewRequestPolicy("vms/cdrom", "delete", fullName),
bt.NewResource("experiment/vm", fullName, "cdrom-ejected"),
nil,
)

w.WriteHeader(http.StatusNoContent)

}

func parseDuration(v string, d *time.Duration) error {
var err error
*d, err = time.ParseDuration(v)
Expand Down
9 changes: 5 additions & 4 deletions src/go/web/proto/vm.proto
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ message VM {
bool busy = 14;
string experiment = 15;
string state = 16;
repeated string tags = 17;
bool cc_active = 18;
bool external = 19;
string cd_rom = 17;
repeated string tags = 18;
bool cc_active = 19;
bool external = 20;

string delayed_start = 20 [json_name="delayed_start"];
string delayed_start = 21 [json_name="delayed_start"];
}

message VMList {
Expand Down
2 changes: 2 additions & 0 deletions src/go/web/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ func Start(opts ...ServerOption) error {
api.HandleFunc("/experiments/{exp}/vms/{name}/stop", StopVM).Methods("POST", "OPTIONS")
api.HandleFunc("/experiments/{exp}/vms/{name}/shutdown", ShutdownVM).Methods("GET", "OPTIONS")
api.HandleFunc("/experiments/{exp}/vms/{name}/redeploy", RedeployVM).Methods("POST", "OPTIONS")
api.HandleFunc("/experiments/{exp}/vms/{name}/cdrom", ChangeOpticalDisc).Methods("POST", "OPTIONS")
api.HandleFunc("/experiments/{exp}/vms/{name}/cdrom", EjectOpticalDisc).Methods("DELETE", "OPTIONS")
api.HandleFunc("/experiments/{exp}/vms/{name}/screenshot.png", GetScreenshot).Methods("GET", "OPTIONS")
api.HandleFunc("/experiments/{exp}/vms/{name}/vnc", GetVNC).Methods("GET", "OPTIONS")
api.HandleFunc("/experiments/{exp}/vms/{name}/vnc/ws", GetVNCWebSocket).Methods("GET", "OPTIONS")
Expand Down
Loading

0 comments on commit 12192c1

Please sign in to comment.