From ca1a374370d5560a79d1afa76f321ee0222c9f12 Mon Sep 17 00:00:00 2001 From: Justin Mott Date: Thu, 2 May 2024 20:08:35 +0000 Subject: [PATCH] feat: add UI controls for VM inject partition and disk persistence Makes the following changes: * Adds column in StoppedExperiments for setting inject partition * Adds column in StoppedExperiments for toggling persistence (snapshot) on and off * Fixes issue with disk dropdowns not populating * Removes date in known_policy.go generation so file isn't updated every build * Minor tweaks to column with in StoppedExperiments table Note: I did not directly write all of these changes, but have pulled them up from an internal repo. --- src/go/api/vm/option.go | 30 ++- src/go/api/vm/vm.go | 68 ++++--- src/go/go.mod | 2 +- src/go/go.sum | 4 +- src/go/types/interfaces/topology.go | 2 + src/go/types/version/v0/node.go | 9 + src/go/types/version/v1/node.go | 8 + src/go/util/mm/types.go | 46 +++-- src/go/web/handlers.go | 11 + src/go/web/proto/vm.proto | 9 +- src/go/web/rbac/known_policy.go | 4 +- src/go/web/rbac/known_policy_gen.go | 8 +- src/go/web/util/protobuf.go | 40 ++-- src/js/src/components/StoppedExperiment.vue | 215 ++++++++++++++++---- 14 files changed, 333 insertions(+), 123 deletions(-) diff --git a/src/go/api/vm/option.go b/src/go/api/vm/option.go index 56ea36f6..ac42cce0 100644 --- a/src/go/api/vm/option.go +++ b/src/go/api/vm/option.go @@ -8,14 +8,16 @@ type iface struct { } type updateOptions struct { - exp string - vm string - cpu int - mem int - disk string - dnb *bool - iface *iface - host *string + exp string + vm string + cpu int + mem int + disk string + partition int + dnb *bool + iface *iface + host *string + snapshot *bool } func newUpdateOptions(opts ...UpdateOption) updateOptions { @@ -58,6 +60,12 @@ func UpdateWithDisk(d string) UpdateOption { } } +func UpdateWithPartition(p int) UpdateOption { + return func(o *updateOptions) { + o.partition = p + } +} + func UpdateWithInterface(i int, v string) UpdateOption { return func(o *updateOptions) { o.iface = &iface{index: i, vlan: v} @@ -70,6 +78,12 @@ func UpdateWithDNB(b bool) UpdateOption { } } +func UpdateWithSnapshot(b bool) UpdateOption { + return func(o *updateOptions) { + o.snapshot = &b + } +} + func UpdateWithHost(h string) UpdateOption { return func(o *updateOptions) { o.host = &h diff --git a/src/go/api/vm/vm.go b/src/go/api/vm/vm.go index 457cb177..111aff33 100644 --- a/src/go/api/vm/vm.go +++ b/src/go/api/vm/vm.go @@ -65,29 +65,37 @@ func List(expName string) ([]mm.VM, error) { for idx, node := range exp.Spec.Topology().Nodes() { var ( - disk string - dnb bool + disk string + dnb bool + snapshot bool + injectPartition int ) if drives := node.Hardware().Drives(); len(drives) > 0 { - disk = drives[0].Image() + disk = util.GetMMFullPath(drives[0].Image()) + injectPartition = *drives[0].InjectPartition() } if node.General().DoNotBoot() != nil { dnb = *node.General().DoNotBoot() } + if node.General().Snapshot() != nil { + snapshot = *node.General().Snapshot() + } vm := mm.VM{ - ID: idx, - Name: node.General().Hostname(), - Experiment: exp.Spec.ExperimentName(), - CPUs: node.Hardware().VCPU(), - RAM: node.Hardware().Memory(), - Disk: disk, - Interfaces: make(map[string]string), - DoNotBoot: dnb, - Type: node.Type(), - OSType: node.Hardware().OSType(), + ID: idx, + Name: node.General().Hostname(), + Experiment: exp.Spec.ExperimentName(), + CPUs: node.Hardware().VCPU(), + RAM: node.Hardware().Memory(), + Disk: disk, + InjectPartition: injectPartition, + Interfaces: make(map[string]string), + DoNotBoot: dnb, + Type: node.Type(), + OSType: node.Hardware().OSType(), + Snapshot: snapshot, } for _, iface := range node.Network().Interfaces() { @@ -179,18 +187,20 @@ func Get(expName, vmName string) (*mm.VM, error) { } vm = &mm.VM{ - ID: idx, - Name: node.General().Hostname(), - Experiment: exp.Spec.ExperimentName(), - CPUs: node.Hardware().VCPU(), - RAM: node.Hardware().Memory(), - Disk: util.GetMMFullPath(node.Hardware().Drives()[0].Image()), - Interfaces: make(map[string]string), - DoNotBoot: *node.General().DoNotBoot(), - OSType: string(node.Hardware().OSType()), - Metadata: make(map[string]interface{}), - Labels: node.Labels(), - Annotations: node.Annotations(), + ID: idx, + Name: node.General().Hostname(), + Experiment: exp.Spec.ExperimentName(), + CPUs: node.Hardware().VCPU(), + RAM: node.Hardware().Memory(), + Disk: util.GetMMFullPath(node.Hardware().Drives()[0].Image()), + InjectPartition: *node.Hardware().Drives()[0].InjectPartition(), + Interfaces: make(map[string]string), + DoNotBoot: *node.General().DoNotBoot(), + OSType: string(node.Hardware().OSType()), + Metadata: make(map[string]interface{}), + Labels: node.Labels(), + Annotations: node.Annotations(), + Snapshot: *node.General().Snapshot(), } for _, iface := range node.Network().Interfaces() { @@ -312,6 +322,10 @@ func Update(opts ...UpdateOption) error { vm.Hardware().Drives()[0].SetImage(o.disk) } + if o.partition != 0 { + vm.Hardware().Drives()[0].SetInjectPartition(&o.partition) + } + if o.dnb != nil { vm.General().SetDoNotBoot(*o.dnb) } @@ -324,6 +338,10 @@ func Update(opts ...UpdateOption) error { } } + if o.snapshot != nil { + vm.General().SetSnapshot(*o.snapshot) + } + err = experiment.Save(experiment.SaveWithName(o.exp), experiment.SaveWithSpec(exp.Spec)) if err != nil { return fmt.Errorf("unable to save experiment with updated VM: %w", err) diff --git a/src/go/go.mod b/src/go/go.mod index e1ca2c6b..4a0d2843 100644 --- a/src/go/go.mod +++ b/src/go/go.mod @@ -35,7 +35,7 @@ require ( golang.org/x/crypto v0.1.0 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 golang.org/x/net v0.1.0 - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 golang.org/x/term v0.1.0 google.golang.org/protobuf v1.25.0 gopkg.in/yaml.v2 v2.4.0 diff --git a/src/go/go.sum b/src/go/go.sum index 72780c89..12fe60a9 100644 --- a/src/go/go.sum +++ b/src/go/go.sum @@ -447,8 +447,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/src/go/types/interfaces/topology.go b/src/go/types/interfaces/topology.go index be10950e..e801506b 100644 --- a/src/go/types/interfaces/topology.go +++ b/src/go/types/interfaces/topology.go @@ -56,6 +56,7 @@ type NodeGeneral interface { Description() string VMType() string Snapshot() *bool + SetSnapshot(bool) DoNotBoot() *bool SetDoNotBoot(bool) @@ -80,6 +81,7 @@ type NodeDrive interface { CacheMode() string InjectPartition() *int + SetInjectPartition(*int) SetImage(string) } diff --git a/src/go/types/version/v0/node.go b/src/go/types/version/v0/node.go index 8bc6c82d..2798f5a7 100644 --- a/src/go/types/version/v0/node.go +++ b/src/go/types/version/v0/node.go @@ -18,6 +18,7 @@ type Node struct { HardwareF *Hardware `json:"hardware" yaml:"hardware" structs:"hardware" mapstructure:"hardware"` NetworkF *Network `json:"network" yaml:"network" structs:"network" mapstructure:"network"` InjectionsF []*Injection `json:"injections" yaml:"injections" structs:"injections" mapstructure:"injections"` + } func (this Node) Annotations() map[string]interface{} { @@ -168,6 +169,7 @@ type General struct { VMTypeF string `json:"vm_type" yaml:"vm_type" structs:"vm_type" mapstructure:"vm_type"` SnapshotF *bool `json:"snapshot" yaml:"snapshot" structs:"snapshot" mapstructure:"snapshot"` DoNotBootF *bool `json:"do_not_boot" yaml:"do_not_boot" structs:"do_not_boot" mapstructure:"do_not_boot"` + } func (this General) Hostname() string { @@ -185,6 +187,9 @@ func (this General) VMType() string { func (this General) Snapshot() *bool { return this.SnapshotF } +func (this *General) SetSnapshot(b bool) { + this.SnapshotF = &b +} func (this General) DoNotBoot() *bool { return this.DoNotBootF @@ -279,6 +284,10 @@ func (this *Drive) SetImage(i string) { this.ImageF = i } +func (this *Drive) SetInjectPartition(p *int) { + this.InjectPartitionF = p +} + type Injection struct { SrcF string `json:"src" yaml:"src" structs:"src" mapstructure:"src"` DstF string `json:"dst" yaml:"dst" structs:"dst" mapstructure:"dst"` diff --git a/src/go/types/version/v1/node.go b/src/go/types/version/v1/node.go index dbbd3c65..d19ef391 100644 --- a/src/go/types/version/v1/node.go +++ b/src/go/types/version/v1/node.go @@ -296,6 +296,10 @@ func (this *General) Snapshot() *bool { return this.SnapshotF } +func (this *General) SetSnapshot(b bool) { + this.SnapshotF = &b +} + func (this *General) DoNotBoot() *bool { if this == nil { return nil @@ -418,6 +422,10 @@ func (this *Drive) SetImage(i string) { this.ImageF = i } +func (this *Drive) SetInjectPartition(p *int) { + this.InjectPartitionF = p +} + type Injection struct { SrcF string `json:"src" yaml:"src" structs:"src" mapstructure:"src"` DstF string `json:"dst" yaml:"dst" structs:"dst" mapstructure:"dst"` diff --git a/src/go/util/mm/types.go b/src/go/util/mm/types.go index dff10cc9..5f3ea162 100644 --- a/src/go/util/mm/types.go +++ b/src/go/util/mm/types.go @@ -206,28 +206,30 @@ func (this VMs) Paginate(page, size int) VMs { } type VM struct { - ID int `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Experiment string `json:"experiment"` - Host string `json:"host"` - IPv4 []string `json:"ipv4"` - CPUs int `json:"cpus"` - RAM int `json:"ram"` - Disk string `json:"disk"` - OSType string `json:"osType"` - DoNotBoot bool `json:"dnb"` - Networks []string `json:"networks"` - Taps []string `json:"taps"` - Captures []Capture `json:"captures"` - State string `json:"state"` - Running bool `json:"running"` - Busy bool `json:"busy"` - CCActive bool `json:"ccActive"` - Uptime float64 `json:"uptime"` - Screenshot string `json:"screenshot,omitempty"` - CdRom string `json:"cdRom"` - Tags []string `json:"tags"` + ID int `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Experiment string `json:"experiment"` + Host string `json:"host"` + IPv4 []string `json:"ipv4"` + CPUs int `json:"cpus"` + RAM int `json:"ram"` + Disk string `json:"disk"` + InjectPartition int `json:"inject_partition` + OSType string `json:"osType"` + DoNotBoot bool `json:"dnb"` + Networks []string `json:"networks"` + Taps []string `json:"taps"` + Captures []Capture `json:"captures"` + State string `json:"state"` + Running bool `json:"running"` + Busy bool `json:"busy"` + CCActive bool `json:"ccActive"` + Uptime float64 `json:"uptime"` + Screenshot string `json:"screenshot,omitempty"` + CdRom string `json:"cdRom"` + Tags []string `json:"tags"` + Snapshot bool `json:"snapshot"` // Used internally to track network <--> IP relationship, since // network ordering from minimega may not be the same as network diff --git a/src/go/web/handlers.go b/src/go/web/handlers.go index 33792b8e..89a541bd 100644 --- a/src/go/web/handlers.go +++ b/src/go/web/handlers.go @@ -1038,6 +1038,7 @@ func UpdateVM(w http.ResponseWriter, r *http.Request) { vm.UpdateWithCPU(int(req.Cpus)), vm.UpdateWithMem(int(req.Ram)), vm.UpdateWithDisk(req.Disk), + vm.UpdateWithPartition(int(req.InjectPartition)), } if req.Interface != nil { @@ -1053,6 +1054,11 @@ func UpdateVM(w http.ResponseWriter, r *http.Request) { case *proto.UpdateVMRequest_Host: opts = append(opts, vm.UpdateWithHost(req.GetHost())) } + + switch req.SnapshotOption.(type) { + case *proto.UpdateVMRequest_Snapshot: + opts = append(opts, vm.UpdateWithSnapshot(req.GetSnapshot())) + } if err := vm.Update(opts...); err != nil { plog.Error("updating VM", "err", err) @@ -1151,6 +1157,11 @@ func UpdateVMs(w http.ResponseWriter, r *http.Request) { opts = append(opts, vm.UpdateWithHost(vmRequest.GetHost())) } + switch vmRequest.SnapshotOption.(type) { + case *proto.UpdateVMRequest_Snapshot: + opts = append(opts, vm.UpdateWithSnapshot(vmRequest.GetSnapshot())) + } + if err := vm.Update(opts...); err != nil { plog.Error("updating VM", "err", err) http.Error(w, "unable to update VM", http.StatusInternalServerError) diff --git a/src/go/web/proto/vm.proto b/src/go/web/proto/vm.proto index febc7e48..91f2bca0 100644 --- a/src/go/web/proto/vm.proto +++ b/src/go/web/proto/vm.proto @@ -23,8 +23,9 @@ message VM { repeated string tags = 18; bool cc_active = 19; bool external = 20; - string delayed_start = 21 [json_name="delayed_start"]; + bool snapshot = 22 [json_name="snapshot"]; + uint32 inject_partition = 23 [json_name="inject_partition"]; } message VMList { @@ -70,6 +71,12 @@ message UpdateVMRequest { oneof cluster_host { string host = 8; } + + oneof snapshot_option { + bool snapshot = 9 [json_name="snapshot"]; + } + + uint32 inject_partition = 10 [json_name="inject_partition"]; } message UpdateVMRequestList { diff --git a/src/go/web/rbac/known_policy.go b/src/go/web/rbac/known_policy.go index b1fa8767..f3442a3e 100644 --- a/src/go/web/rbac/known_policy.go +++ b/src/go/web/rbac/known_policy.go @@ -1,6 +1,6 @@ // Code generated by go generate; DO NOT EDIT. -// This file was generated at build time 2024-06-14 09:29:39.356147531 -0600 MDT m=+0.125261219 -// This contains all known role checks used in codebase +// This file was generated at build time. +// This contains all known role checks used in codebase. package rbac diff --git a/src/go/web/rbac/known_policy_gen.go b/src/go/web/rbac/known_policy_gen.go index fb58f233..05f7232b 100644 --- a/src/go/web/rbac/known_policy_gen.go +++ b/src/go/web/rbac/known_policy_gen.go @@ -13,7 +13,6 @@ import ( "sort" "strings" "text/template" - "time" "golang.org/x/exp/slices" ) @@ -24,8 +23,8 @@ type Permission struct { } var code = `// Code generated by go generate; DO NOT EDIT. -// This file was generated at build time {{ .Timestamp }} -// This contains all known role checks used in codebase +// This file was generated at build time. +// This contains all known role checks used in codebase. package rbac @@ -40,7 +39,6 @@ var Permissions = []Permission{ {{- end }} } ` - var packageTemplate = template.Must(template.New("").Parse(code)) func main() { @@ -86,10 +84,8 @@ func main() { defer f.Close() packageTemplate.Execute(f, struct { - Timestamp time.Time Permissions []Permission }{ - Timestamp: time.Now(), Permissions: permissions, }) } diff --git a/src/go/web/util/protobuf.go b/src/go/web/util/protobuf.go index 3866b051..f4ed3920 100644 --- a/src/go/web/util/protobuf.go +++ b/src/go/web/util/protobuf.go @@ -90,25 +90,27 @@ func ExperimentToProtobuf(exp types.Experiment, status cache.Status, vms []mm.VM func VMToProtobuf(exp string, vm mm.VM, topology ifaces.TopologySpec) *proto.VM { v := &proto.VM{ - Name: vm.Name, - Host: vm.Host, - Ipv4: vm.IPv4, - Cpus: uint32(vm.CPUs), - Ram: uint32(vm.RAM), - Disk: vm.Disk, - Uptime: vm.Uptime, - Networks: vm.Networks, - Taps: vm.Taps, - Captures: CapturesToProtobuf(vm.Captures), - DoNotBoot: vm.DoNotBoot, - Screenshot: vm.Screenshot, - Running: vm.Running, - Busy: vm.Busy, - Experiment: exp, - State: vm.State, - CdRom: vm.CdRom, - Tags: vm.Tags, - CcActive: vm.CCActive, + Name: vm.Name, + Host: vm.Host, + Ipv4: vm.IPv4, + Cpus: uint32(vm.CPUs), + Ram: uint32(vm.RAM), + Disk: vm.Disk, + InjectPartition: uint32(vm.InjectPartition), + Uptime: vm.Uptime, + Networks: vm.Networks, + Taps: vm.Taps, + Captures: CapturesToProtobuf(vm.Captures), + DoNotBoot: vm.DoNotBoot, + Screenshot: vm.Screenshot, + Running: vm.Running, + Busy: vm.Busy, + Experiment: exp, + State: vm.State, + CdRom: vm.CdRom, + Tags: vm.Tags, + CcActive: vm.CCActive, + Snapshot: vm.Snapshot, } if topology == nil { diff --git a/src/js/src/components/StoppedExperiment.vue b/src/js/src/components/StoppedExperiment.vue index fb66601f..3e1fbddb 100644 --- a/src/js/src/components/StoppedExperiment.vue +++ b/src/js/src/components/StoppedExperiment.vue @@ -223,7 +223,7 @@ {{ ip || 'unknown' }} - + - + + + + + + + +
@@ -465,6 +490,24 @@ return 'boot'; } }, + getSnapshotStatus ( vm, persistanceLabel ) { + if (vm.external) { + return true; + } + + if ( vm.snapshot && persistanceLabel ) { + return true; + } else if (vm.snapshot && !persistanceLabel) { + return false; + } + else if (!vm.snapshot && persistanceLabel){ + return false; + } + else { + + return true; + } + }, changePaginate () { var user = localStorage.getItem( 'user' ); @@ -1008,6 +1051,47 @@ } }) }, + assignPartition ( name, partition ) { + this.$buefy.dialog.confirm({ + title: 'Assign an Image Partition', + message: 'This will assign the image partition ' + partition + ' to the ' + name + ' VM.', + cancelText: 'Cancel', + confirmText: 'Assign Partition', + type: 'is-success', + hasIcon: true, + onConfirm: () => { + this.isWaiting = true; + + let update = { "inject_partition": partition }; + + this.$http.patch( + 'experiments/' + this.$route.params.id + '/vms/' + name, update + ).then( + response => { + let vms = this.experiment.vms; + + for ( let i = 0; i < vms.length; i++ ) { + if ( vms[ i ].name == response.body.name ) { + vms[ i ] = response.body; + break; + } + } + + this.experiment.vms = [ ...vms ]; + + this.isWaiting = false; + }, err => { + this.errorNotification(err); + this.isWaiting = false; + } + ) + }, + onCancel: () => { + // force table to be rerendered so selected value resets + this.table.key += 1; + } + }) + }, updateDnb ( vm ) { if (vm.external) { @@ -1041,44 +1125,98 @@ ) }, - updateSchedule () { - this.$buefy.dialog.confirm({ - title: 'Assign a Host Schedule', - message: 'This will schedule host(s) with the ' - + this.algorithm - + ' algorithm for the ' - + this.$route.params.id - + ' experiment.', - cancelText: 'Cancel', - confirmText: 'Assign Schedule', - type: 'is-success', - hasIcon: true, - onConfirm: () => { - this.isWaiting = true; + updateSnapshot( name, persistence ) { + let persistenceMessage = "" + if (persistence == "true"){ + persistenceMessage = "Non-Persistent" + } + else { + persistenceMessage = "Persistent" + } + if (persistence == "true"){ + persistence = true + } + else { + persistence = false + } + this.$buefy.dialog.confirm({ + title: 'Assign Image Persistence', + message: 'This will assign the ' + name + ' VM\'s disk to be ' + persistenceMessage, + cancelText: 'Cancel', + confirmText: 'Confirm', + type: 'is-success', + hasIcon: true, + onConfirm: () => { + this.isWaiting = true; + + let update = { "snapshot": persistence}; - this.$http.post( - 'experiments/' + this.$route.params.id + '/schedule', { "algorithm": this.algorithm } - ).then( - response => { - let vms = this.experiment.vms; - - for ( let i = 0; i < vms.length; i++ ) { - if ( vms[ i ].name == response.body.name ) { - vms[ i ] = response.body; - break; - } - } - - this.experiment.vms = [ ...vms ]; - - this.isWaiting = false; - }, err => { - this.errorNotification(err); - this.isWaiting = false; + this.$http.patch( + 'experiments/' + this.$route.params.id + '/vms/' + name, update + ).then( + response => { + let vms = this.experiment.vms; + + for ( let i = 0; i < vms.length; i++ ) { + if ( vms[ i ].name == response.body.name ) { + vms[ i ] = response.body; + break; } - ) + } + + this.experiment.vms = [ ...vms ]; + + this.isWaiting = false; + }, err => { + this.errorNotification(err); + this.isWaiting = false; } - }) + ) + }, + onCancel: () => { + // force table to be rerendered so selected value resets + this.table.key += 1; + } + }) + }, + updateSchedule () { + this.$buefy.dialog.confirm({ + title: 'Assign a Host Schedule', + message: 'This will schedule host(s) with the ' + + this.algorithm + + ' algorithm for the ' + + this.$route.params.id + + ' experiment.', + cancelText: 'Cancel', + confirmText: 'Assign Schedule', + type: 'is-success', + hasIcon: true, + onConfirm: () => { + this.isWaiting = true; + + this.$http.post( + 'experiments/' + this.$route.params.id + '/schedule', { "algorithm": this.algorithm } + ).then( + response => { + let vms = this.experiment.vms; + + for ( let i = 0; i < vms.length; i++ ) { + if ( vms[ i ].name == response.body.name ) { + vms[ i ] = response.body; + break; + } + } + + this.experiment.vms = [ ...vms ]; + + this.isWaiting = false; + }, err => { + this.errorNotification(err); + this.isWaiting = false; + } + ) + } + }) }, getUniqueItems(inputArray) { @@ -1103,6 +1241,9 @@ getBootLabel (vm) { return vm.dnb ? `Boot ${vm.name}` : `Do Not Boot ${vm.name}`; }, + getSnapshotLabel (vm) { + return vm.snapshot ? `${vm.name}'s disk will not persist` : `${vm.name}'s disk will persist`; + }, selectAllVMs () { var visibleItems = this.$refs["vmTable"].visibleData